diff options
65 files changed, 4143 insertions, 1447 deletions
diff --git a/.gitignore b/.gitignore index a7cadc7..1ca77eb 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -1,5 +1,6 @@ | |||
1 | build/ | 1 | build/ |
2 | builds/ | 2 | builds/ |
3 | assets/LL1.yaml | 3 | assets/LL1.yaml |
4 | assets/ids.yaml | ||
4 | .DS_Store | 5 | .DS_Store |
5 | .vs | 6 | .vs |
diff --git a/.gitmodules b/.gitmodules index ebe016f..1a69477 100644 --- a/.gitmodules +++ b/.gitmodules | |||
@@ -16,3 +16,6 @@ | |||
16 | [submodule "vendor/vcpkg"] | 16 | [submodule "vendor/vcpkg"] |
17 | path = vendor/vcpkg | 17 | path = vendor/vcpkg |
18 | url = https://github.com/Microsoft/vcpkg.git | 18 | url = https://github.com/Microsoft/vcpkg.git |
19 | [submodule "vendor/websocketpp"] | ||
20 | path = vendor/websocketpp | ||
21 | url = https://github.com/zaphoyd/websocketpp | ||
diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ce76d..e2444b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -1,5 +1,159 @@ | |||
1 | # lingo-ap-tracker Releases | 1 | # lingo-ap-tracker Releases |
2 | 2 | ||
3 | ## v2.0.2 - 2025-05-24 | ||
4 | |||
5 | - Fixed issue connecting to the Archipelago 0.6.2 RC server. | ||
6 | |||
7 | Download: | ||
8 | [lingo-ap-tracker-v2.0.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.2-win64.zip)<br/> | ||
9 | Source: [v2.0.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.2) | ||
10 | |||
11 | ## v2.0.1 - 2025-04-06 | ||
12 | |||
13 | - The tracker now assumes postgame shuffle is enabled when the flag is not | ||
14 | present in slot data (as is the case with Archipelago 0.6.1 and earlier). | ||
15 | Players who have postgame shuffle disabled will unfortunately see locations | ||
16 | that are not in their world, until this problem can be fixed. | ||
17 | |||
18 | Download: | ||
19 | [lingo-ap-tracker-v2.0.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.1-win64.zip)<br/> | ||
20 | Source: [v2.0.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.1) | ||
21 | |||
22 | ## v2.0.0 - 2025-04-01 | ||
23 | |||
24 | - Compatibility update for Archipelago 0.6.0. | ||
25 | |||
26 | Download: | ||
27 | [lingo-ap-tracker-v2.0.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.0-win64.zip)<br/> | ||
28 | Source: [v2.0.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.0) | ||
29 | |||
30 | ## v1.0.0 - 2025-03-21 | ||
31 | |||
32 | After almost two years of development, we have finally hit version 1! | ||
33 | |||
34 | - The subway map now uses Kinrah's updated map image, including fully separated | ||
35 | paintings for places like The Wondrous and Orange Tower Sixth Floor, as well | ||
36 | as indicators for the color items. | ||
37 | - Paintings have friendly names now instead of the internal game IDs. They are | ||
38 | also distinguished from regular checks by using an owl icon instead of an eye. | ||
39 | - The tracker is now able to read your solved panel state from the multiworld | ||
40 | (requires v5.3.0 of the client). This obsoletes save file parsing and | ||
41 | receiving panel solves by connecting to the game. | ||
42 | - Numerous optimizations to reachability detection and rendering. | ||
43 | - Added a pane that shows all items received, similar to the web tracker. | ||
44 | - Added a pane that shows your currently revealed painting mapping, as well as a | ||
45 | button that reveals it all immediately. | ||
46 | - Added a pane that shows your slot options. | ||
47 | - Postgame shuffle being disabled is handled properly now, and removed locations | ||
48 | are no longer shown. | ||
49 | - Improved support for high-DPI screens. The tracker should no longer appear | ||
50 | "blurry" on a high-DPI screen, and should also be able to react properly to | ||
51 | being moved between screens with different DPIs. | ||
52 | - The update checker is now able to download and install tracker updates in | ||
53 | place. | ||
54 | |||
55 | Download: | ||
56 | [lingo-ap-tracker-v1.0.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v1.0.0-win64.zip)<br/> | ||
57 | Source: [v1.0.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v1.0.0) | ||
58 | |||
59 | ## v0.12.3 - 2025-03-03 | ||
60 | |||
61 | - Fixed issue with non-ASCII connection details. | ||
62 | |||
63 | Download: | ||
64 | [lingo-ap-tracker-v0.12.3-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.3-win64.zip)<br/> | ||
65 | Source: [v0.12.3](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.3) | ||
66 | |||
67 | ## v0.12.2 - 2025-02-10 | ||
68 | |||
69 | - Added a scrollbar to the subway map access requirement popups. | ||
70 | - Fixed an issue with a small number of doors having too-strict access | ||
71 | requirements in vanilla or panels mode door shuffle. | ||
72 | |||
73 | Download: | ||
74 | [lingo-ap-tracker-v0.12.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.2-win64.zip)<br/> | ||
75 | Source: [v0.12.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.2) | ||
76 | |||
77 | ## v0.12.1 - 2025-01-27 | ||
78 | |||
79 | - Fixed sunwarp mapping not showing up on metro map when sunwarp shuffle is | ||
80 | enabled. | ||
81 | - Fixed metro map door requirements sometimes saying you cannot reach areas when | ||
82 | you really can. | ||
83 | |||
84 | Download: | ||
85 | [lingo-ap-tracker-v0.12.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.1-win64.zip)<br/> | ||
86 | Source: [v0.12.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.1) | ||
87 | |||
88 | ## v0.12.0 - 2024-12-20 | ||
89 | |||
90 | - The tracker can now connect to a game of Lingo that is running the Archipelago | ||
91 | client (requires v5.1.0 or later). This allows the tracker to show the | ||
92 | player's position more precisely, as well as get automatically updated on what | ||
93 | non-check panels have been solved. | ||
94 | - Fixed one of the doors (Outside The Initiated - Eight Door) showing up | ||
95 | incorrectly on the subway map. | ||
96 | |||
97 | Download: | ||
98 | [lingo-ap-tracker-v0.12.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.12.0-win64.zip)<br/> | ||
99 | Source: [v0.12.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.12.0) | ||
100 | |||
101 | ## v0.11.5 - 2024-12-08 | ||
102 | |||
103 | - [A logic error](https://github.com/ArchipelagoMW/Archipelago/pull/4342) was | ||
104 | found in Archipelago 0.5.1 related to the number hunt when playing on panels | ||
105 | mode door shuffle. In order to work around this, the tracker now uses the same | ||
106 | incorrect logic as the generator. This means that you will be able to tell | ||
107 | what panels you are expected to solve, even if it is not intuitive. There is | ||
108 | already a fix for the logic error, which will likely be included in the next | ||
109 | major Archipelago release, at which point this workaround will be removed. | ||
110 | |||
111 | Download: | ||
112 | [lingo-ap-tracker-v0.11.5-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.5-win64.zip)<br/> | ||
113 | Source: [v0.11.5](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.5) | ||
114 | |||
115 | ## v0.11.4 - 2024-12-05 | ||
116 | |||
117 | - Fixed an issue where Starting Room panels would show up as checks when playing | ||
118 | panels mode door shuffle. | ||
119 | |||
120 | Download: | ||
121 | [lingo-ap-tracker-v0.11.4-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.4-win64.zip)<br/> | ||
122 | Source: [v0.11.4](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.4) | ||
123 | |||
124 | ## v0.11.3 - 2024-11-26 | ||
125 | |||
126 | - Compatibility update for Archipelago 0.5.1 | ||
127 | |||
128 | Download: | ||
129 | [lingo-ap-tracker-v0.11.3-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.3-win64.zip)<br/> | ||
130 | Source: [v0.11.3](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.3) | ||
131 | |||
132 | ## v0.11.2 - 2024-09-24 | ||
133 | |||
134 | - One-way connections on the subway map are now indicated by a circle at the | ||
135 | exit. Contributed by art0007i. | ||
136 | |||
137 | Download: | ||
138 | [lingo-ap-tracker-v0.11.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.2-win64.zip)<br/> | ||
139 | Source: [v0.11.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.2) | ||
140 | |||
141 | ## v0.11.1 - 2024-07-25 | ||
142 | |||
143 | - The Pilgrim Antechamber sunwarp on the subway map now shows all sunwarp | ||
144 | connections, and is red if a pilgrimage is not possible. | ||
145 | - The save analysis panel now uses the remote location status for non-counting | ||
146 | panels. | ||
147 | - Fixed positioning of Outside The Undeterred - Number Hunt door on subway map. | ||
148 | - Fixed subway map issue when sunwarp shuffle and individual/progressive sunwarp | ||
149 | access were combined where the icons on the map would show unshuffled access. | ||
150 | - Map area indicators now correctly treat unreachable pre-checked paintings as | ||
151 | unchecked. | ||
152 | |||
153 | Download: | ||
154 | [lingo-ap-tracker-v0.11.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.1-win64.zip)<br/> | ||
155 | Source: [v0.11.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.1) | ||
156 | |||
3 | ## v0.11.0 - 2024-07-19 | 157 | ## v0.11.0 - 2024-07-19 |
4 | 158 | ||
5 | - Added a savedata analyzer. When connected to a world, the user can open up the | 159 | - Added a savedata analyzer. When connected to a world, the user can open up the |
diff --git a/CMakeLists.txt b/CMakeLists.txt index e1cb7f0..ef741fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -1,15 +1,15 @@ | |||
1 | cmake_minimum_required (VERSION 3.1) | 1 | cmake_minimum_required (VERSION 3.20) |
2 | project (lingo_ap_tracker) | 2 | project (lingo_ap_tracker) |
3 | 3 | ||
4 | if (MSVC) | 4 | if (MSVC) |
5 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") | 5 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") |
6 | set(CMAKE_WIN32_EXECUTABLE true) | 6 | set(CMAKE_WIN32_EXECUTABLE true) |
7 | set(CMAKE_EXE_LINKER_FLAGS /MANIFEST:NO) | ||
7 | endif(MSVC) | 8 | endif(MSVC) |
8 | 9 | ||
9 | find_package(wxWidgets CONFIG REQUIRED) | 10 | find_package(wxWidgets CONFIG REQUIRED) |
10 | find_package(OpenSSL REQUIRED) | 11 | find_package(OpenSSL REQUIRED) |
11 | find_package(yaml-cpp REQUIRED) | 12 | find_package(yaml-cpp REQUIRED) |
12 | find_package(websocketpp REQUIRED) | ||
13 | find_package(fmt REQUIRED) | 13 | find_package(fmt REQUIRED) |
14 | 14 | ||
15 | include_directories( | 15 | include_directories( |
@@ -18,7 +18,7 @@ include_directories( | |||
18 | vendor/asio/asio/include | 18 | vendor/asio/asio/include |
19 | vendor/nlohmann | 19 | vendor/nlohmann |
20 | vendor/valijson/include | 20 | vendor/valijson/include |
21 | ${websocketpp_INCLUDE_DIRS} | 21 | vendor/websocketpp |
22 | vendor/wswrap/include | 22 | vendor/wswrap/include |
23 | ${yaml-cpp_INCLUDE_DIRS} | 23 | ${yaml-cpp_INCLUDE_DIRS} |
24 | ${OpenSSL_INCLUDE_DIRS} | 24 | ${OpenSSL_INCLUDE_DIRS} |
@@ -32,7 +32,7 @@ include_directories(${SYSTEM_INCLUDE_DIR}) | |||
32 | 32 | ||
33 | link_directories(${openssl_LIBRARY_DIRS}) | 33 | link_directories(${openssl_LIBRARY_DIRS}) |
34 | 34 | ||
35 | add_executable(lingo_ap_tracker | 35 | set(SOURCE_FILES |
36 | "src/main.cpp" | 36 | "src/main.cpp" |
37 | "src/tracker_frame.cpp" | 37 | "src/tracker_frame.cpp" |
38 | "src/tracker_panel.cpp" | 38 | "src/tracker_panel.cpp" |
@@ -48,9 +48,33 @@ add_executable(lingo_ap_tracker | |||
48 | "src/subway_map.cpp" | 48 | "src/subway_map.cpp" |
49 | "src/network_set.cpp" | 49 | "src/network_set.cpp" |
50 | "src/logger.cpp" | 50 | "src/logger.cpp" |
51 | "src/godot_variant.cpp" | 51 | "src/ipc_state.cpp" |
52 | "src/ipc_dialog.cpp" | ||
53 | "src/report_popup.cpp" | ||
54 | "src/updater.cpp" | ||
55 | "src/icons.cpp" | ||
56 | "src/paintings_pane.cpp" | ||
57 | "src/items_pane.cpp" | ||
58 | "src/options_pane.cpp" | ||
59 | "src/log_dialog.cpp" | ||
52 | "vendor/whereami/whereami.c" | 60 | "vendor/whereami/whereami.c" |
53 | ) | 61 | ) |
62 | |||
63 | if (MSVC) | ||
64 | list(APPEND SOURCE_FILES "src/windows.rc") | ||
65 | endif(MSVC) | ||
66 | |||
67 | add_executable(lingo_ap_tracker ${SOURCE_FILES}) | ||
54 | set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) | 68 | set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) |
55 | set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON) | 69 | set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON) |
56 | target_link_libraries(lingo_ap_tracker PRIVATE fmt::fmt OpenSSL::SSL OpenSSL::Crypto websocketpp::websocketpp wx::core wx::base wx::net yaml-cpp::yaml-cpp) | 70 | target_link_libraries(lingo_ap_tracker PRIVATE fmt::fmt OpenSSL::SSL OpenSSL::Crypto wx::core wx::base wx::net yaml-cpp::yaml-cpp) |
71 | |||
72 | set(SRC_DIR "${CMAKE_SOURCE_DIR}/assets") | ||
73 | set(DST_DIR "${CMAKE_BINARY_DIR}/$<CONFIG>/assets") | ||
74 | |||
75 | add_custom_target(copy_assets ALL | ||
76 | COMMAND ${CMAKE_COMMAND} -E copy_directory ${SRC_DIR} ${DST_DIR} | ||
77 | COMMENT "Copying folder from ${SRC_DIR} to ${DST_DIR}" | ||
78 | ) | ||
79 | |||
80 | add_dependencies(lingo_ap_tracker copy_assets) | ||
diff --git a/README.md b/README.md index 83525dd..cb3e90d 100644 --- a/README.md +++ b/README.md | |||
@@ -9,4 +9,23 @@ Releases of the tracker can be found [on the releases page](https://code.fourisl | |||
9 | 9 | ||
10 | ## Acknowledgments | 10 | ## Acknowledgments |
11 | 11 | ||
12 | Thanks to Kinrah for making the subway map image! | 12 | * Brenton Wildes: Created Lingo, and drew some of the images used in the tracker. |
13 | * Kinrah: Made the subway map image. | ||
14 | * art0007i: Contributed to the display of the subway map. | ||
15 | |||
16 | ## Building | ||
17 | |||
18 | To build the app: | ||
19 | |||
20 | 1. Clone the repository including submodules: `git clone --recursive https://code.fourisland.com/lingo-ap-tracker` | ||
21 | 2. Put [LL1.yaml from archipelago](https://github.com/ArchipelagoMW/Archipelago/raw/main/worlds/lingo/data/LL1.yaml) in ./assets | ||
22 | 3. Put [ids.yaml from archipelago](https://github.com/ArchipelagoMW/Archipelago/raw/main/worlds/lingo/data/ids.yaml) in ./assets | ||
23 | 4. Configure the project: `cmake --preset=lingo-ap-tracker-preset` | ||
24 | 5. Build the application in debug mode: `cmake --build --preset=lingo-ap-tracker-preset` | ||
25 | 6. (Optional) Build the application in release mode: `cmake --build --preset=x64-release-preset` | ||
26 | |||
27 | LL1.yaml and ids.yaml sometimes receive breaking changes that need to be kept in sync with the tracker. If the application crashes with an unknown error, try making sure that you are using the right versions of those files. In general, the main branch of the tracker will require config files from the latest Archipelago release. Branches may require config files from Archipelago main, or from pending pull requests. | ||
28 | |||
29 | ### Using Visual Studio | ||
30 | |||
31 | If you're using Visual Studio on Windows, you can simplify the above process. You can clone the repository directly from Visual Studio, and it will automatically download the submodules as well as configure cmake and build the vcpkg packages. You will still have to do steps 2 and 3 manually. Then, look for the dropdown next to the green arrow and select `lingo_ap_tracker.exe` so that you can build it. You can build in release mode by choosing it from the preset dropdown. | ||
diff --git a/VERSION b/VERSION index e88c34f..b02d37b 100644 --- a/VERSION +++ b/VERSION | |||
@@ -1 +1 @@ | |||
v0.11.0 \ No newline at end of file | v2.0.2 \ No newline at end of file | ||
diff --git a/VERSION.yaml b/VERSION.yaml new file mode 100644 index 0000000..8f86a39 --- /dev/null +++ b/VERSION.yaml | |||
@@ -0,0 +1,31 @@ | |||
1 | version: v2.0.2 | ||
2 | packages: | ||
3 | win64: | ||
4 | url: "https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v2.0.2-win64.zip" | ||
5 | checksum: "dfb2fce2f5b14c09f4af7e7fcf6478e420a070db9fb23f9f9ad196f2db4b7518" | ||
6 | files: | ||
7 | - fmt.dll | ||
8 | - jpeg62.dll | ||
9 | - libcrypto-3-x64.dll | ||
10 | - liblzma.dll | ||
11 | - libpng16.dll | ||
12 | - libssl-3-x64.dll | ||
13 | - lingo_ap_tracker.exe | ||
14 | - pcre2-16.dll | ||
15 | - tiff.dll | ||
16 | - wxbase32u_net_vc_custom.dll | ||
17 | - wxbase32u_vc_custom.dll | ||
18 | - wxmsw32u_core_vc_custom.dll | ||
19 | - yaml-cpp.dll | ||
20 | - zlib1.dll | ||
21 | - assets/areas.yaml | ||
22 | - assets/checked.png | ||
23 | - assets/checked_owl.png | ||
24 | - assets/ids.yaml | ||
25 | - assets/lingo_map.png | ||
26 | - assets/LL1.yaml | ||
27 | - assets/owl.png | ||
28 | - assets/player.png | ||
29 | - assets/subway.png | ||
30 | - assets/subway.yaml | ||
31 | - assets/unchecked.png | ||
diff --git a/assets/checked_owl.png b/assets/checked_owl.png new file mode 100644 index 0000000..f53ddd3 --- /dev/null +++ b/assets/checked_owl.png | |||
Binary files differ | |||
diff --git a/assets/subway.png b/assets/subway.png index 3860d2c..8128004 100644 --- a/assets/subway.png +++ b/assets/subway.png | |||
Binary files differ | |||
diff --git a/assets/subway.yaml b/assets/subway.yaml index dcc58b2..8c0df38 100644 --- a/assets/subway.yaml +++ b/assets/subway.yaml | |||
@@ -1,123 +1,117 @@ | |||
1 | --- | 1 | --- |
2 | - pos: [1050, 954] | 2 | - pos: [1051, 954] |
3 | room: Starting Room | 3 | room: Starting Room |
4 | door: Back Right Door | 4 | door: Back Right Door |
5 | - pos: [986, 1034] | 5 | - pos: [986, 1034] |
6 | room: Starting Room | 6 | room: Starting Room |
7 | door: Rhyme Room Entrance | 7 | door: Rhyme Room Entrance |
8 | - pos: [990, 956] | 8 | - pos: [996, 964] |
9 | special: starting_room_paintings # Early Color Hallways painting is a hardcoded special case | 9 | special: starting_room_overhead |
10 | paintings: | 10 | painting: arrows_painting |
11 | - arrows_painting | 11 | - pos: [1044, 1012] |
12 | - pos: [905, 841] | 12 | special: early_color_hallways |
13 | painting: color_hallways | ||
14 | - pos: [915, 851] | ||
13 | room: Hedge Maze | 15 | room: Hedge Maze |
14 | door: Painting Shortcut | 16 | door: Painting Shortcut |
15 | paintings: | 17 | painting: garden_painting_tower2 |
16 | - garden_painting_tower2 | 18 | entrances: |
17 | tags: | ||
18 | - garden_starting | 19 | - garden_starting |
19 | - pos: [1066, 841] | 20 | - pos: [1076, 851] |
20 | room: Courtyard | 21 | room: Courtyard |
21 | door: Painting Shortcut | 22 | door: Painting Shortcut |
22 | paintings: | 23 | painting: flower_painting_8 |
23 | - flower_painting_8 | 24 | entrances: |
24 | tags: | ||
25 | - flower_starting | 25 | - flower_starting |
26 | - pos: [905, 895] | 26 | - pos: [915, 905] |
27 | room: The Wondrous (Doorknob) | 27 | room: The Wondrous (Doorknob) |
28 | door: Painting Shortcut | 28 | door: Painting Shortcut |
29 | paintings: | 29 | painting: symmetry_painting_a_starter |
30 | - symmetry_painting_a_starter | 30 | entrances: |
31 | tags: | ||
32 | - symmetry_starting | 31 | - symmetry_starting |
33 | - pos: [1066, 868] | 32 | - pos: [1076, 905] |
34 | room: Outside The Bold | 33 | room: Outside The Bold |
35 | door: Painting Shortcut | 34 | door: Painting Shortcut |
36 | paintings: | 35 | painting: pencil_painting6 |
37 | - pencil_painting6 | 36 | entrances: |
38 | tags: | ||
39 | - pencil_starting | 37 | - pencil_starting |
40 | - pos: [1066, 895] | 38 | - pos: [1076, 878] |
41 | room: Outside The Undeterred | 39 | room: Outside The Undeterred |
42 | door: Painting Shortcut | 40 | door: Painting Shortcut |
43 | paintings: | 41 | painting: blueman_painting_3 |
44 | - blueman_painting_3 | 42 | entrances: |
45 | tags: | ||
46 | - blueman_starting | 43 | - blueman_starting |
47 | - pos: [905, 868] | 44 | - pos: [915, 878] |
48 | room: Outside The Agreeable | 45 | room: Outside The Agreeable |
49 | door: Painting Shortcut | 46 | door: Painting Shortcut |
50 | paintings: | 47 | painting: eyes_yellow_painting2 |
51 | - eyes_yellow_painting2 | 48 | entrances: |
52 | tags: | ||
53 | - street_starting | 49 | - street_starting |
54 | - pos: [1211, 879] | 50 | - pos: [1211, 879] |
55 | room: Hidden Room | 51 | room: Hidden Room |
56 | door: Dead End Door | 52 | door: Dead End Door |
57 | - pos: [1291, 906] | 53 | - pos: [1292, 906] |
58 | room: Hidden Room | 54 | room: Hidden Room |
59 | door: Knight Night Entrance | 55 | door: Knight Night Entrance |
60 | - pos: [1103, 980] | 56 | - pos: [1104, 981] |
61 | room: Hidden Room | 57 | room: Hidden Room |
62 | door: Seeker Entrance | 58 | door: Seeker Entrance |
63 | - pos: [1173, 980] | 59 | - pos: [1174, 981] |
64 | room: Hidden Room | 60 | room: Hidden Room |
65 | door: Rhyme Room Entrance | 61 | door: Rhyme Room Entrance |
66 | - pos: [1116, 939] | 62 | - pos: [1114, 937] |
67 | paintings: | 63 | painting: owl_painting |
68 | - owl_painting | 64 | entrances: |
69 | tags: | ||
70 | - owl_hidden | 65 | - owl_hidden |
71 | - pos: [986, 793] | 66 | - pos: [986, 793] |
72 | room: Second Room | 67 | room: Second Room |
73 | door: Exit Door | 68 | door: Exit Door |
74 | - pos: [798, 584] | 69 | - pos: [799, 584] |
75 | room: Hub Room | 70 | room: Hub Room |
76 | door: Crossroads Entrance | 71 | door: Crossroads Entrance |
77 | - pos: [932, 665] | 72 | - pos: [933, 665] |
78 | room: Hub Room | 73 | room: Hub Room |
79 | door: Tenacious Entrance | 74 | door: Tenacious Entrance |
80 | - pos: [1361, 578] | 75 | - pos: [1361, 579] |
81 | room: Hub Room | 76 | room: Hub Room |
82 | door: Shortcut to Hedge Maze | 77 | door: Shortcut to Hedge Maze |
83 | - pos: [1312, 841] | 78 | - pos: [1313, 842] |
84 | room: Hub Room | 79 | room: Hub Room |
85 | door: Near RAT Door | 80 | door: Near RAT Door |
86 | - pos: [1371, 729] | 81 | - pos: [1372, 729] |
87 | room: Hub Room | 82 | room: Hub Room |
88 | door: Traveled Entrance | 83 | door: Traveled Entrance |
89 | - pos: [1313, 686] | 84 | - pos: [1312, 696] |
90 | paintings: | 85 | painting: maze_painting |
91 | - maze_painting | 86 | exits: |
92 | tags: | ||
93 | - green_owl | 87 | - green_owl |
94 | - green_numbers | 88 | - green_numbers |
95 | - pos: [1172, 760] | 89 | - pos: [1174, 761] |
96 | sunwarp: | 90 | sunwarp: |
97 | dots: 1 | 91 | dots: 1 |
98 | type: enter | 92 | type: enter |
99 | - pos: [1302, 638] | 93 | - pos: [1302, 638] |
100 | room: Outside The Undeterred | 94 | room: Outside The Undeterred |
101 | door: Fours | 95 | door: Fours |
102 | - pos: [1243, 819] | 96 | - pos: [1243, 820] |
103 | room: Outside The Undeterred | 97 | room: Outside The Undeterred |
104 | door: Fours | 98 | door: Fours |
105 | - pos: [1276, 819] | 99 | - pos: [1276, 820] |
106 | room: Number Hunt | 100 | room: Number Hunt |
107 | door: Eights | 101 | door: Eights |
108 | - pos: [1263, 867] | 102 | - pos: [1253, 889] |
109 | paintings: | 103 | painting: smile_painting_6 |
110 | - smile_painting_6 | 104 | entrances: |
111 | tags: | ||
112 | - smiley_deadend | 105 | - smiley_deadend |
113 | - pos: [1012, 1086] | 106 | - pos: [1013, 1087] |
114 | sunwarp: | 107 | sunwarp: |
115 | dots: 6 | 108 | dots: 6 |
116 | type: final | 109 | type: final |
117 | - pos: [938, 1002] | 110 | - pos: [948, 1012] |
118 | room: Pilgrim Antechamber | 111 | room: Pilgrim Antechamber |
119 | door: Sun Painting | 112 | door: Sun Painting |
120 | special: sun_painting | 113 | special: sun_painting |
114 | painting: pilgrim_painting2 | ||
121 | - pos: [1053, 1090] | 115 | - pos: [1053, 1090] |
122 | invisible: true | 116 | invisible: true |
123 | special: sun_painting_exit | 117 | special: sun_painting_exit |
@@ -136,7 +130,7 @@ | |||
136 | - pos: [932, 477] | 130 | - pos: [932, 477] |
137 | room: Crossroads | 131 | room: Crossroads |
138 | door: Tenacious Entrance | 132 | door: Tenacious Entrance |
139 | - pos: [638, 477] | 133 | - pos: [509, 477] |
140 | room: Crossroads | 134 | room: Crossroads |
141 | door: Discerning Entrance | 135 | door: Discerning Entrance |
142 | - pos: [905, 290] | 136 | - pos: [905, 290] |
@@ -145,16 +139,15 @@ | |||
145 | - pos: [894, 423] | 139 | - pos: [894, 423] |
146 | room: Crossroads | 140 | room: Crossroads |
147 | door: Words Sword Door | 141 | door: Words Sword Door |
148 | - pos: [632, 643] | 142 | - pos: [718, 557] |
149 | room: Crossroads | 143 | room: Crossroads |
150 | door: Eye Wall | 144 | door: Eye Wall |
151 | - pos: [638, 520] | 145 | - pos: [509, 434] |
152 | room: Crossroads | 146 | room: Crossroads |
153 | door: Roof Access | 147 | door: Roof Access |
154 | - pos: [756, 400] | 148 | - pos: [712, 514] |
155 | paintings: | 149 | painting: smile_painting_4 |
156 | - smile_painting_4 | 150 | entrances: |
157 | tags: | ||
158 | - smiley_crossroads | 151 | - smiley_crossroads |
159 | - pos: [878, 509] | 152 | - pos: [878, 509] |
160 | sunwarp: | 153 | sunwarp: |
@@ -168,13 +161,12 @@ | |||
168 | door: Exit | 161 | door: Exit |
169 | - pos: [986, 290] | 162 | - pos: [986, 290] |
170 | room: Number Hunt | 163 | room: Number Hunt |
171 | door: Eights | 164 | door: Nines |
172 | - pos: [954, 247] | 165 | - pos: [954, 247] |
173 | room: Amen Name Area | 166 | room: Amen Name Area |
174 | door: Exit | 167 | door: Exit |
175 | - pos: [954, 222] | 168 | - pos: [985, 235] |
176 | paintings: | 169 | painting: west_afar |
177 | - west_afar | ||
178 | - pos: [986, 697] | 170 | - pos: [986, 697] |
179 | room: The Tenacious | 171 | room: The Tenacious |
180 | door: Shortcut to Hub Room | 172 | door: Shortcut to Hub Room |
@@ -205,22 +197,20 @@ | |||
205 | - pos: [1216, 525] | 197 | - pos: [1216, 525] |
206 | room: Outside The Agreeable | 198 | room: Outside The Agreeable |
207 | door: Agreeable Entrance | 199 | door: Agreeable Entrance |
208 | - pos: [1138, 287] | 200 | - pos: [1156, 299] |
209 | paintings: | 201 | painting: eyes_yellow_painting |
210 | - eyes_yellow_painting | 202 | exits: |
211 | tags: | ||
212 | - street_starting | 203 | - street_starting |
213 | - pos: [1088, 385] | 204 | - pos: [1088, 385] |
214 | sunwarp: | 205 | sunwarp: |
215 | dots: 6 | 206 | dots: 6 |
216 | type: enter | 207 | type: enter |
217 | - pos: [1195, 450] | 208 | - pos: [1195, 445] |
218 | room: Compass Room | 209 | room: Compass Room |
219 | door: Lookout Entrance | 210 | door: Lookout Entrance |
220 | - pos: [1214, 457] | 211 | - pos: [1226, 481] |
221 | paintings: | 212 | painting: pencil_painting7 |
222 | - pencil_painting7 | 213 | entrances: |
223 | tags: | ||
224 | - pencil_compass | 214 | - pencil_compass |
225 | - pos: [1196, 417] | 215 | - pos: [1196, 417] |
226 | invisible: true | 216 | invisible: true |
@@ -248,10 +238,9 @@ | |||
248 | - pos: [1714, 434] | 238 | - pos: [1714, 434] |
249 | room: Hedge Maze | 239 | room: Hedge Maze |
250 | door: Observant Entrance | 240 | door: Observant Entrance |
251 | - pos: [1477, 343] | 241 | - pos: [1526, 401] |
252 | paintings: | 242 | painting: garden_painting_tower |
253 | - garden_painting_tower | 243 | exits: |
254 | tags: | ||
255 | - garden_starting | 244 | - garden_starting |
256 | - pos: [1565, 311] | 245 | - pos: [1565, 311] |
257 | room: The Fearless (First Floor) | 246 | room: The Fearless (First Floor) |
@@ -262,45 +251,48 @@ | |||
262 | - pos: [1414, 209] | 251 | - pos: [1414, 209] |
263 | room: The Observant | 252 | room: The Observant |
264 | door: Backside Door | 253 | door: Backside Door |
265 | - pos: [1624, 188] | 254 | - pos: [1592, 188] |
266 | room: The Observant | 255 | room: The Observant |
267 | door: Stairs | 256 | door: Stairs |
268 | - pos: [1667, 686] | 257 | - pos: [1694, 659] |
269 | room: The Incomparable | 258 | room: The Incomparable |
270 | door: Eight Door | 259 | door: Eight Door |
271 | - pos: [1784, 569] | 260 | - pos: [1799, 583] |
272 | paintings: | 261 | painting: crown_painting |
273 | - crown_painting | 262 | exits: |
274 | tags: | ||
275 | - crown_tower6 | 263 | - crown_tower6 |
276 | - pos: [1653, 717] | 264 | - pos: [1644, 685] |
277 | paintings: | 265 | painting: eight_painting2 |
278 | - eight_painting2 | 266 | entrances: |
279 | tags: | ||
280 | - eight_alcove | 267 | - eight_alcove |
281 | - pos: [1653, 662] | 268 | - pos: [1660, 664] |
282 | paintings: | 269 | painting: eight_painting |
283 | - eight_painting | 270 | exits: |
284 | tags: | ||
285 | - eight_alcove | 271 | - eight_alcove |
286 | - pos: [697, 1471] | 272 | - pos: [695, 1471] |
287 | room: Orange Tower | 273 | room: Orange Tower |
288 | door: Second Floor | 274 | door: Second Floor |
289 | - pos: [633, 1406] | 275 | tilted: true |
276 | - pos: [631, 1406] | ||
290 | room: Orange Tower | 277 | room: Orange Tower |
291 | door: Third Floor | 278 | door: Third Floor |
292 | - pos: [570, 1343] | 279 | tilted: true |
280 | - pos: [567, 1341] | ||
293 | room: Orange Tower | 281 | room: Orange Tower |
294 | door: Fourth Floor | 282 | door: Fourth Floor |
295 | - pos: [504, 1279] | 283 | tilted: true |
284 | - pos: [502, 1277] | ||
296 | room: Orange Tower | 285 | room: Orange Tower |
297 | door: Fifth Floor | 286 | door: Fifth Floor |
298 | - pos: [440, 1215] | 287 | tilted: true |
288 | - pos: [438, 1213] | ||
299 | room: Orange Tower | 289 | room: Orange Tower |
300 | door: Sixth Floor | 290 | door: Sixth Floor |
301 | - pos: [379, 1153] | 291 | tilted: true |
292 | - pos: [378, 1152] | ||
302 | room: Orange Tower | 293 | room: Orange Tower |
303 | door: Seventh Floor | 294 | door: Seventh Floor |
295 | tilted: true | ||
304 | - pos: [905, 793] | 296 | - pos: [905, 793] |
305 | room: Orange Tower First Floor | 297 | room: Orange Tower First Floor |
306 | door: Shortcut to Hub Room | 298 | door: Shortcut to Hub Room |
@@ -320,7 +312,7 @@ | |||
320 | - pos: [722, 1439] | 312 | - pos: [722, 1439] |
321 | tags: | 313 | tags: |
322 | - tower2_undeterred | 314 | - tower2_undeterred |
323 | - pos: [533, 1375] | 315 | - pos: [549, 1375] |
324 | tags: | 316 | tags: |
325 | - tower3_tower3 | 317 | - tower3_tower3 |
326 | - pos: [662, 1375] | 318 | - pos: [662, 1375] |
@@ -377,10 +369,10 @@ | |||
377 | - pos: [1173, 1248] | 369 | - pos: [1173, 1248] |
378 | room: Orange Tower Third Floor | 370 | room: Orange Tower Third Floor |
379 | door: Rhyme Room Entrance | 371 | door: Rhyme Room Entrance |
380 | - pos: [1270, 1231] | 372 | - pos: [1274, 1253] |
381 | paintings: | 373 | painting: arrows_painting_6 |
382 | - arrows_painting_6 | 374 | - pos: [1293, 1234] |
383 | - flower_painting_5 | 375 | painting: flower_painting_5 |
384 | - pos: [1216, 1216] | 376 | - pos: [1216, 1216] |
385 | sunwarp: | 377 | sunwarp: |
386 | dots: 2 | 378 | dots: 2 |
@@ -396,77 +388,83 @@ | |||
396 | sunwarp: | 388 | sunwarp: |
397 | dots: 5 | 389 | dots: 5 |
398 | type: enter | 390 | type: enter |
399 | - pos: [877, 155] | 391 | - pos: [878, 155] |
400 | room: Number Hunt | 392 | room: Number Hunt |
401 | door: Eights | 393 | door: Eights |
402 | - pos: [844, 134] | 394 | - pos: [862, 144] |
403 | paintings: | 395 | painting: smile_painting_8 |
404 | - smile_painting_8 | 396 | entrances: |
405 | tags: | ||
406 | - smiley_hotcrusts | 397 | - smiley_hotcrusts |
407 | - pos: [797, 155] | 398 | - pos: [798, 155] |
408 | sunwarp: | 399 | sunwarp: |
409 | dots: 2 | 400 | dots: 2 |
410 | type: enter | 401 | type: enter |
411 | - pos: [679, 985] | 402 | - pos: [680, 986] |
412 | room: Number Hunt | 403 | room: Number Hunt |
413 | door: Nines | 404 | door: Nines |
414 | - pos: [723, 953] | 405 | - pos: [723, 953] |
415 | room: Orange Tower Fifth Floor | 406 | room: Orange Tower Fifth Floor |
416 | door: Welcome Back | 407 | door: Welcome Back |
417 | - pos: [683, 944] | 408 | - pos: [690, 937] |
418 | paintings: | 409 | painting: east_afar |
419 | - east_afar | 410 | - pos: [535, 1205] |
420 | - pos: [548, 1221] | 411 | painting: hi_solved_painting3 |
421 | paintings: | 412 | - pos: [1574, 1424] |
422 | - hi_solved_painting3 | 413 | painting: hi_solved_painting2 |
423 | - pos: [1574, 1425] | 414 | - pos: [316, 1269] |
424 | paintings: | 415 | painting: arrows_painting_10 |
425 | - hi_solved_painting2 | 416 | - pos: [332, 1253] |
426 | - pos: [411, 1186] | 417 | painting: scenery_painting_5d_2 |
427 | paintings: | 418 | - pos: [347, 1237] |
428 | - arrows_painting_10 | 419 | painting: panda_painting_2 |
429 | - owl_painting_3 | 420 | entrances: |
430 | - clock_painting | ||
431 | - scenery_painting_5d_2 | ||
432 | - symmetry_painting_b_7 | ||
433 | - panda_painting_2 | ||
434 | - crown_painting2 | ||
435 | - colors_painting2 | ||
436 | - cherry_painting2 | ||
437 | - hi_solved_painting | ||
438 | tags: | ||
439 | - owl_tower6 | ||
440 | - clock_tower6 | ||
441 | - panda_tower6 | 421 | - panda_tower6 |
422 | - pos: [363, 1221] | ||
423 | painting: colors_painting2 | ||
424 | - pos: [380, 1205] | ||
425 | painting: clock_painting | ||
426 | entrances: | ||
427 | - clock_tower6 | ||
428 | - pos: [396, 1221] | ||
429 | painting: hi_solved_painting | ||
430 | exits: | ||
431 | - hi_scientific | ||
432 | - pos: [380, 1237] | ||
433 | painting: crown_painting2 | ||
434 | entrances: | ||
442 | - crown_tower6 | 435 | - crown_tower6 |
436 | - pos: [363, 1253] | ||
437 | painting: owl_painting_3 | ||
438 | entrances: | ||
439 | - owl_tower6 | ||
440 | - pos: [347, 1269] | ||
441 | painting: symmetry_painting_b_7 | ||
442 | - pos: [332, 1285] | ||
443 | painting: cherry_painting2 | ||
444 | entrances: | ||
443 | - apple_tower6 | 445 | - apple_tower6 |
444 | - hi_scientific | 446 | - pos: [360, 1135] |
445 | - pos: [349, 1124] | 447 | # TODO: there isn't really a spot for this one |
446 | paintings: | 448 | painting: map_painting2 |
447 | - map_painting2 | ||
448 | - pos: [436, 1159] | 449 | - pos: [436, 1159] |
449 | room: Orange Tower Seventh Floor | 450 | room: Orange Tower Seventh Floor |
450 | door: Mastery | 451 | door: Mastery |
451 | - pos: [544, 1159] | 452 | - pos: [535, 1146] |
452 | paintings: | 453 | painting: arrows_painting_11 |
453 | - arrows_painting_11 | ||
454 | - pos: [498, 284] | 454 | - pos: [498, 284] |
455 | room: Courtyard | 455 | room: Courtyard |
456 | door: Green Barrier | 456 | door: Green Barrier |
457 | - pos: [556, 233] | 457 | - pos: [589, 230] |
458 | paintings: | 458 | painting: flower_painting_7 |
459 | - flower_painting_7 | 459 | exits: |
460 | tags: | ||
461 | - flower_starting | 460 | - flower_starting |
462 | - flower_arrow | 461 | - flower_arrow |
463 | - pos: [600, 332] | 462 | - pos: [600, 332] |
464 | room: Number Hunt | 463 | room: Number Hunt |
465 | door: Nines | 464 | door: Nines |
466 | - pos: [579, 350] | 465 | - pos: [589, 369] |
467 | paintings: | 466 | painting: blueman_painting |
468 | - blueman_painting | 467 | entrances: |
469 | tags: | ||
470 | - blueman_courtyard | 468 | - blueman_courtyard |
471 | - pos: [530, 310] | 469 | - pos: [530, 310] |
472 | room: First Second Third Fourth | 470 | room: First Second Third Fourth |
@@ -501,14 +499,13 @@ | |||
501 | - pos: [922, 107] | 499 | - pos: [922, 107] |
502 | room: The Colorful (Gray) | 500 | room: The Colorful (Gray) |
503 | door: Progress Door | 501 | door: Progress Door |
504 | - pos: [967, 107] | 502 | - pos: [969, 96] |
505 | paintings: | 503 | painting: arrows_painting_12 |
506 | - arrows_painting_12 | ||
507 | - pos: [878, 954] | 504 | - pos: [878, 954] |
508 | room: Welcome Back Area | 505 | room: Welcome Back Area |
509 | door: Shortcut to Starting Room | 506 | door: Shortcut to Starting Room |
510 | - pos: [773, 954] | 507 | - pos: [773, 954] |
511 | tags: | 508 | exits: |
512 | - hub_wb | 509 | - hub_wb |
513 | - wondrous_wb | 510 | - wondrous_wb |
514 | - undeterred_wb | 511 | - undeterred_wb |
@@ -519,45 +516,48 @@ | |||
519 | - scientific_wb | 516 | - scientific_wb |
520 | - cellar_wb | 517 | - cellar_wb |
521 | - pos: [1107, 749] | 518 | - pos: [1107, 749] |
522 | tags: | 519 | entrances: |
523 | - hub_wb | 520 | - hub_wb |
524 | - pos: [408, 817] | 521 | - pos: [408, 817] |
525 | tags: | 522 | entrances: |
526 | - wondrous_wb | 523 | - wondrous_wb |
527 | - pos: [281, 1017] | 524 | - pos: [281, 1017] |
528 | tags: | 525 | entrances: |
529 | - undeterred_wb | 526 | - undeterred_wb |
530 | - pos: [1017, 289] | 527 | - pos: [1017, 289] |
531 | tags: | 528 | entrances: |
532 | - agreeable_wb | 529 | - agreeable_wb |
533 | - pos: [907, 1385] | 530 | - pos: [907, 1385] |
534 | tags: | 531 | entrances: |
535 | - wanderer_wb | 532 | - wanderer_wb |
536 | - pos: [1737, 1053] | 533 | - pos: [1737, 1053] |
537 | tags: | 534 | entrances: |
538 | - gallery_wb | 535 | - gallery_wb |
539 | - pos: [1690, 268] | 536 | - pos: [1784, 395] |
540 | tags: | 537 | entrances: |
541 | - observant_wb | 538 | - observant_wb |
542 | - pos: [250, 604] | 539 | - pos: [122, 350] |
543 | tags: | 540 | entrances: |
544 | - scientific_wb | 541 | - scientific_wb |
545 | - pos: [1553, 1467] | 542 | - pos: [1553, 1467] |
546 | tags: | 543 | entrances: |
547 | - cellar_wb | 544 | - cellar_wb |
548 | - pos: [1478, 498] | 545 | - pos: [1478, 498] |
549 | room: Owl Hallway | 546 | room: Owl Hallway |
550 | door: Shortcut to Hedge Maze | 547 | door: Shortcut to Hedge Maze |
551 | - pos: [1480, 551] | 548 | - pos: [1467, 535] |
552 | paintings: | 549 | painting: arrows_painting_8 |
553 | - arrows_painting_8 | 550 | - pos: [1467, 562] |
554 | - maze_painting_2 | 551 | painting: maze_painting_2 |
555 | - owl_painting_2 | 552 | entrances: |
556 | - clock_painting_4 | ||
557 | tags: | ||
558 | - green_owl | 553 | - green_owl |
554 | - pos: [1510, 535] | ||
555 | painting: owl_painting_2 | ||
556 | exits: | ||
559 | - owl_hidden | 557 | - owl_hidden |
560 | - owl_tower6 | 558 | - owl_tower6 |
559 | - pos: [1510, 562] | ||
560 | painting: clock_painting_4 | ||
561 | - pos: [1478, 938] | 561 | - pos: [1478, 938] |
562 | room: Number Hunt | 562 | room: Number Hunt |
563 | door: Sevens | 563 | door: Sevens |
@@ -582,41 +582,39 @@ | |||
582 | - pos: [1511, 841] | 582 | - pos: [1511, 841] |
583 | room: Outside The Initiated | 583 | room: Outside The Initiated |
584 | door: Initiated Entrance | 584 | door: Initiated Entrance |
585 | - pos: [1141, 1441] | 585 | - pos: [1071, 1441] |
586 | room: Orange Tower Third Floor | 586 | room: Orange Tower Third Floor |
587 | door: Orange Barrier | 587 | door: Orange Barrier |
588 | - pos: [1173, 1441] | 588 | - pos: [1104, 1441] |
589 | room: Outside The Initiated | 589 | room: Outside The Initiated |
590 | door: Green Barrier | 590 | door: Green Barrier |
591 | - pos: [1206, 1441] | 591 | - pos: [1137, 1441] |
592 | room: Outside The Initiated | 592 | room: Outside The Initiated |
593 | door: Purple Barrier | 593 | door: Purple Barrier |
594 | - pos: [1189, 1355] | 594 | - pos: [1120, 1355] |
595 | room: Outside The Initiated | 595 | room: Outside The Initiated |
596 | door: Entrance | 596 | door: Entrance |
597 | - pos: [1580, 729] | 597 | - pos: [1580, 729] |
598 | room: Outside The Initiated | 598 | room: The Incomparable |
599 | door: Eight Door | 599 | door: "Eight Door (Outside The Initiated)" |
600 | - pos: [1530, 938] | 600 | - pos: [1548, 948] |
601 | paintings: | 601 | painting: clock_painting_5 |
602 | - clock_painting_5 | 602 | entrances: |
603 | tags: | ||
604 | - clock_initiated | 603 | - clock_initiated |
605 | - pos: [1546, 938] | 604 | - pos: [1575, 969] |
606 | paintings: | 605 | painting: arrows_painting_2 |
607 | - clock_painting_2 | 606 | - pos: [1575, 926] |
608 | - arrows_painting_2 | 607 | painting: clock_painting_2 |
609 | tags: | 608 | exits: |
610 | - clock_tower6 | 609 | - clock_tower6 |
611 | - clock_initiated | 610 | - clock_initiated |
612 | - pos: [1579, 813] | 611 | - pos: [1580, 814] |
613 | sunwarp: | 612 | sunwarp: |
614 | dots: 3 | 613 | dots: 3 |
615 | type: exit | 614 | type: exit |
616 | - pos: [1444, 896] | 615 | - pos: [1462, 915] |
617 | paintings: | 616 | painting: smile_painting_1 |
618 | - smile_painting_1 | 617 | entrances: |
619 | tags: | ||
620 | - smiley_initiated | 618 | - smiley_initiated |
621 | - pos: [1430, 691] | 619 | - pos: [1430, 691] |
622 | room: Outside The Undeterred | 620 | room: Outside The Undeterred |
@@ -624,17 +622,25 @@ | |||
624 | - pos: [1468, 728] | 622 | - pos: [1468, 728] |
625 | room: The Traveled | 623 | room: The Traveled |
626 | door: Color Hallways Entrance | 624 | door: Color Hallways Entrance |
627 | - pos: [1533, 707] | 625 | - pos: [1535, 644] |
628 | tags: | 626 | tags: |
629 | - red_ch | 627 | - red_ch |
628 | - pos: [1535, 667] | ||
629 | tags: | ||
630 | - blue_ch | 630 | - blue_ch |
631 | - pos: [1535, 689] | ||
632 | tags: | ||
631 | - yellow_ch | 633 | - yellow_ch |
634 | - pos: [1535, 711] | ||
635 | tags: | ||
632 | - green_ch | 636 | - green_ch |
637 | - pos: [1499, 678] | ||
638 | exits: | ||
633 | - early_ch | 639 | - early_ch |
634 | - pos: [1567, 1264] | 640 | - pos: [1567, 1264] |
635 | tags: | 641 | tags: |
636 | - red_ch | 642 | - red_ch |
637 | - pos: [150, 808] | 643 | - pos: [150, 748] |
638 | tags: | 644 | tags: |
639 | - blue_ch | 645 | - blue_ch |
640 | - pos: [626, 371] | 646 | - pos: [626, 371] |
@@ -652,21 +658,21 @@ | |||
652 | - pos: [1468, 1377] | 658 | - pos: [1468, 1377] |
653 | room: Outside The Bold | 659 | room: Outside The Bold |
654 | door: Bold Entrance | 660 | door: Bold Entrance |
655 | - pos: [1425, 1358] | 661 | - pos: [1446, 1344] |
656 | paintings: | 662 | painting: pencil_painting2 |
657 | - pencil_painting2 | 663 | exits: |
658 | - north_missing2 | ||
659 | tags: | ||
660 | - pencil_compass | 664 | - pencil_compass |
661 | - pencil_starting | 665 | - pencil_starting |
662 | - pencil_directional | 666 | - pencil_directional |
667 | - pos: [1580, 1344] | ||
668 | painting: north_missing2 | ||
663 | - pos: [1334, 1419] | 669 | - pos: [1334, 1419] |
664 | room: Outside The Bold | 670 | room: Outside The Bold |
665 | door: Steady Entrance | 671 | door: Steady Entrance |
666 | - pos: [445, 1048] | 672 | - pos: [445, 1048] |
667 | tags: | 673 | exits: |
668 | - undeterred_artistic | 674 | - undeterred_artistic |
669 | - pos: [279, 1071] | 675 | - pos: [273, 1071] |
670 | room: Number Hunt | 676 | room: Number Hunt |
671 | door: Zero Door | 677 | door: Zero Door |
672 | - pos: [338, 1071] | 678 | - pos: [338, 1071] |
@@ -681,18 +687,16 @@ | |||
681 | - pos: [242, 1071] | 687 | - pos: [242, 1071] |
682 | room: Outside The Undeterred | 688 | room: Outside The Undeterred |
683 | door: Undeterred Entrance | 689 | door: Undeterred Entrance |
684 | - pos: [60, 1017] | 690 | - pos: [149, 937] |
685 | paintings: | 691 | painting: blueman_painting_2 |
686 | - blueman_painting_2 | 692 | exits: |
687 | tags: | ||
688 | - blueman_courtyard | 693 | - blueman_courtyard |
689 | - blueman_starting | 694 | - blueman_starting |
690 | - pos: [402, 1012] | 695 | - pos: [412, 1017] |
691 | room: Outside The Undeterred | 696 | room: Outside The Undeterred |
692 | door: Green Painting | 697 | door: Green Painting |
693 | paintings: | 698 | painting: maze_painting_3 |
694 | - maze_painting_3 | 699 | entrances: |
695 | tags: | ||
696 | - green_numbers | 700 | - green_numbers |
697 | - pos: [134, 1007] | 701 | - pos: [134, 1007] |
698 | sunwarp: | 702 | sunwarp: |
@@ -701,7 +705,7 @@ | |||
701 | - pos: [719, 1039] | 705 | - pos: [719, 1039] |
702 | room: Outside The Undeterred | 706 | room: Outside The Undeterred |
703 | door: Challenge Entrance | 707 | door: Challenge Entrance |
704 | - pos: [483, 1039] | 708 | - pos: [456, 1039] |
705 | room: Outside The Undeterred | 709 | room: Outside The Undeterred |
706 | door: Number Hunt | 710 | door: Number Hunt |
707 | - pos: [563, 1071] | 711 | - pos: [563, 1071] |
@@ -722,12 +726,11 @@ | |||
722 | - pos: [525, 1002] | 726 | - pos: [525, 1002] |
723 | room: Number Hunt | 727 | room: Number Hunt |
724 | door: Door to Directional Gallery | 728 | door: Door to Directional Gallery |
725 | - pos: [659, 1014] | 729 | - pos: [572, 1017] |
726 | room: Number Hunt | 730 | room: Number Hunt |
727 | door: Eights | 731 | door: Eights |
728 | paintings: | 732 | painting: smile_painting_5 |
729 | - smile_painting_5 | 733 | entrances: |
730 | tags: | ||
731 | - smiley_numbers | 734 | - smiley_numbers |
732 | - pos: [557, 953] | 735 | - pos: [557, 953] |
733 | room: Number Hunt | 736 | room: Number Hunt |
@@ -744,87 +747,67 @@ | |||
744 | - pos: [268, 825] | 747 | - pos: [268, 825] |
745 | room: Directional Gallery | 748 | room: Directional Gallery |
746 | door: Yellow Barrier | 749 | door: Yellow Barrier |
747 | - pos: [231, 681] | 750 | - pos: [214, 563] |
748 | room: Number Hunt | 751 | room: Number Hunt |
749 | door: Sixes | 752 | door: Sixes |
750 | - pos: [242, 980] | 753 | - pos: [242, 980] |
751 | room: Directional Gallery | 754 | room: Directional Gallery |
752 | door: Shortcut to The Undeterred | 755 | door: Shortcut to The Undeterred |
753 | - pos: [351, 927] | 756 | - pos: [364, 942] |
754 | paintings: | 757 | painting: boxes_painting |
755 | - boxes_painting | 758 | entrances: |
756 | tags: | ||
757 | - lattice_directional | 759 | - lattice_directional |
758 | - pos: [272, 927] | 760 | - pos: [278, 942] |
759 | paintings: | 761 | painting: smile_painting_7 |
760 | - smile_painting_7 | 762 | entrances: |
761 | tags: | ||
762 | - smiley_directional | 763 | - smiley_directional |
763 | - pos: [214, 822] | 764 | - pos: [278, 803] |
764 | paintings: | 765 | painting: cherry_painting |
765 | - cherry_painting | 766 | entrances: |
766 | tags: | ||
767 | - apple_directional | 767 | - apple_directional |
768 | - pos: [266, 735] | 768 | - pos: [262, 573] |
769 | room: Number Hunt | 769 | room: Number Hunt |
770 | door: Sixes | 770 | door: Sixes |
771 | paintings: | 771 | painting: pencil_painting3 |
772 | - pencil_painting3 | 772 | entrances: |
773 | tags: | ||
774 | - pencil_directional | 773 | - pencil_directional |
775 | - pos: [215, 735] | 774 | - pos: [278, 685] |
776 | paintings: | 775 | painting: flower_painting_4 |
777 | - flower_painting_4 | ||
778 | - pos: [626, 851] | 776 | - pos: [626, 851] |
779 | sunwarp: | 777 | sunwarp: |
780 | dots: 6 | 778 | dots: 6 |
781 | type: exit | 779 | type: exit |
782 | - pos: [1141, 1441] | ||
783 | room: Orange Tower Third Floor | ||
784 | door: Orange Barrier | ||
785 | - pos: [1174, 1441] | ||
786 | room: Outside The Initiated | ||
787 | door: Green Barrier | ||
788 | - pos: [1205, 1441] | ||
789 | room: Outside The Initiated | ||
790 | door: Purple Barrier | ||
791 | - pos: [1334, 1377] | 780 | - pos: [1334, 1377] |
792 | room: Color Hunt | 781 | room: Color Hunt |
793 | door: Shortcut to The Steady | 782 | door: Shortcut to The Steady |
794 | - pos: [1280, 1375] | 783 | - pos: [1312, 1333] |
795 | paintings: | 784 | painting: arrows_painting_7 |
796 | - arrows_painting_7 | 785 | - pos: [1226, 1333] |
797 | - pos: [1233, 1321] | ||
798 | room: Outside The Initiated | 786 | room: Outside The Initiated |
799 | door: Entrance | 787 | door: Entrance |
800 | paintings: | 788 | painting: fruitbowl_painting3 |
801 | - fruitbowl_painting3 | 789 | - pos: [1260, 1323] |
802 | - pos: [1290, 1323] | ||
803 | sunwarp: | 790 | sunwarp: |
804 | dots: 5 | 791 | dots: 5 |
805 | type: exit | 792 | type: exit |
806 | - pos: [1189, 1356] | 793 | - pos: [1092, 1333] |
807 | room: Outside The Initiated | 794 | painting: colors_painting |
808 | door: Entrance | ||
809 | - pos: [1154, 1332] | ||
810 | paintings: | ||
811 | - colors_painting | ||
812 | - pos: [1640, 1260] | 795 | - pos: [1640, 1260] |
813 | room: The Bearer | 796 | room: The Bearer |
814 | door: Backside Door | 797 | door: Backside Door |
815 | - pos: [1468, 1287] | 798 | - pos: [1468, 1287] |
816 | room: The Bearer | 799 | room: The Bearer |
817 | door: Entrance | 800 | door: Entrance |
818 | - pos: [1430, 1232] | 801 | - pos: [1431, 1233] |
819 | room: Number Hunt | 802 | room: Number Hunt |
820 | door: Sixes | 803 | door: Sixes |
821 | - pos: [1388, 1152] | 804 | - pos: [1388, 1152] |
822 | room: Bearer Side Area | 805 | room: Bearer Side Area |
823 | door: Shortcut to Tower | 806 | door: Shortcut to Tower |
824 | - pos: [1273, 1442] | 807 | - pos: [1264, 1430] |
825 | paintings: | 808 | painting: pencil_painting5 |
826 | - pencil_painting5 | 809 | - pos: [1291, 1430] |
827 | - pencil_painting4 | 810 | painting: pencil_painting4 |
828 | - pos: [1355, 1092] | 811 | - pos: [1355, 1092] |
829 | room: Knight Night (Final) | 812 | room: Knight Night (Final) |
830 | door: Exit | 813 | door: Exit |
@@ -835,10 +818,9 @@ | |||
835 | # Complex case, because this is also blocked by Knight Night (Final) - Exit | 818 | # Complex case, because this is also blocked by Knight Night (Final) - Exit |
836 | room: Number Hunt | 819 | room: Number Hunt |
837 | door: Sevens | 820 | door: Sevens |
838 | - pos: [1653, 101] | 821 | - pos: [1687, 117] |
839 | paintings: | 822 | painting: smile_painting_9 |
840 | - smile_painting_9 | 823 | exits: |
841 | tags: | ||
842 | - smiley_crossroads | 824 | - smiley_crossroads |
843 | - smiley_deadend | 825 | - smiley_deadend |
844 | - smiley_hotcrusts | 826 | - smiley_hotcrusts |
@@ -847,88 +829,113 @@ | |||
847 | - smiley_initiated | 829 | - smiley_initiated |
848 | - smiley_gallery | 830 | - smiley_gallery |
849 | - smiley_theysee | 831 | - smiley_theysee |
850 | - pos: [1656, 139] | 832 | - pos: [1677, 161] |
851 | room: The Artistic (Smiley) | 833 | room: The Artistic (Smiley) |
852 | door: Door to Panda | 834 | door: Door to Panda |
853 | - pos: [1711, 140] | 835 | - pos: [1711, 140] |
854 | tags: | 836 | entrances: |
855 | - undeterred_artistic | 837 | - undeterred_artistic |
856 | - pos: [1653, 169] | 838 | - pos: [1687, 224] |
857 | paintings: | 839 | painting: panda_painting_3 |
858 | - panda_painting_3 | 840 | exits: |
859 | tags: | ||
860 | - panda_tower6 | 841 | - panda_tower6 |
861 | - panda_hallway | 842 | - panda_hallway |
862 | - pos: [1708, 171] | 843 | - pos: [1731, 215] |
863 | room: The Artistic (Panda) | 844 | room: The Artistic (Panda) |
864 | door: Door to Lattice | 845 | door: Door to Lattice |
865 | - pos: [1761, 169] | 846 | - pos: [1794, 224] |
866 | paintings: | 847 | painting: boxes_painting2 |
867 | - boxes_painting2 | 848 | exits: |
868 | tags: | ||
869 | - lattice_directional | 849 | - lattice_directional |
870 | - pos: [1762, 139] | 850 | - pos: [1785, 161] |
871 | room: The Artistic (Lattice) | 851 | room: The Artistic (Lattice) |
872 | door: Door to Apple | 852 | door: Door to Apple |
873 | - pos: [1761, 101] | 853 | - pos: [1794, 117] |
874 | paintings: | 854 | painting: cherry_painting3 |
875 | - cherry_painting3 | 855 | exits: |
876 | tags: | ||
877 | - apple_tower6 | 856 | - apple_tower6 |
878 | - apple_directional | 857 | - apple_directional |
879 | - pos: [1708, 107] | 858 | - pos: [1731, 107] |
880 | room: The Artistic (Apple) | 859 | room: The Artistic (Apple) |
881 | door: Door to Smiley | 860 | door: Door to Smiley |
882 | - pos: [370, 681] | 861 | - pos: [370, 563] |
883 | room: Number Hunt | 862 | room: Number Hunt |
884 | door: Eights | 863 | door: Eights |
885 | - pos: [411, 685] | 864 | - pos: [637, 605] |
886 | paintings: | 865 | painting: eye_painting |
887 | - eye_painting_2 | 866 | entrances: |
888 | - smile_painting_2 | 867 | - crossroads_eyewall |
889 | tags: | 868 | - pos: [610, 605] |
869 | exits: | ||
870 | - crossroads_eyewall | ||
871 | - pos: [417, 573] | ||
872 | painting: eye_painting_2 | ||
873 | exits: | ||
874 | - crossroads_eyewall | ||
875 | - pos: [342, 573] | ||
876 | painting: smile_painting_2 | ||
877 | entrances: | ||
890 | - smiley_theysee | 878 | - smiley_theysee |
891 | - pos: [310, 750] | 879 | - pos: [311, 750] |
892 | room: The Eyes They See | 880 | room: The Eyes They See |
893 | door: Exit | 881 | door: Exit |
894 | - pos: [334, 798] | 882 | - pos: [348, 803] |
895 | paintings: | 883 | painting: arrows_painting_5 |
896 | - arrows_painting_5 | 884 | - pos: [370, 751] |
897 | - pos: [370, 792] | ||
898 | room: Outside The Wondrous | 885 | room: Outside The Wondrous |
899 | door: Wondrous Entrance | 886 | door: Wondrous Entrance |
900 | - pos: [367, 752] | 887 | - pos: [428, 696] |
901 | paintings: | 888 | painting: symmetry_painting_a_1 |
902 | - symmetry_painting_a_1 | 889 | exits: |
903 | - symmetry_painting_b_1 | ||
904 | - symmetry_painting_a_3 | ||
905 | - symmetry_painting_a_5 | ||
906 | - symmetry_painting_b_4 | ||
907 | - symmetry_painting_a_2 | ||
908 | - symmetry_painting_b_2 | ||
909 | - symmetry_painting_a_6 | ||
910 | - symmetry_painting_b_6 | ||
911 | tags: | ||
912 | - symmetry_starting | 890 | - symmetry_starting |
913 | - pos: [407, 755] | 891 | - symmetry_a_chandelier |
892 | - symmetry_a_table | ||
893 | - pos: [428, 749] | ||
894 | painting: symmetry_painting_b_1 | ||
895 | entrances: | ||
896 | - symmetry_b_table | ||
897 | - pos: [471, 696] | ||
898 | painting: symmetry_painting_a_3 | ||
899 | - pos: [538, 699] | ||
900 | painting: symmetry_painting_a_5 | ||
901 | entrances: | ||
902 | - symmetry_a_chandelier | ||
903 | - pos: [562, 728] | ||
904 | painting: symmetry_painting_b_4 | ||
905 | - pos: [422, 647] | ||
906 | painting: symmetry_painting_a_2 | ||
907 | entrances: | ||
908 | - symmetry_a_table | ||
909 | - pos: [364, 648] | ||
910 | painting: symmetry_painting_b_2 | ||
911 | exits: | ||
912 | - symmetry_b_table | ||
913 | - symmetry_b_fire | ||
914 | - pos: [449, 647] | ||
915 | painting: symmetry_painting_a_6 | ||
916 | - pos: [508, 647] | ||
917 | painting: symmetry_painting_b_6 | ||
918 | entrances: | ||
919 | - symmetry_b_fire | ||
920 | - pos: [558, 665] | ||
914 | room: The Wondrous | 921 | room: The Wondrous |
915 | door: Exit | 922 | door: Exit |
916 | paintings: | 923 | - pos: [535, 647] |
917 | - arrows_painting_9 | 924 | room: The Wondrous |
918 | - pos: [449, 755] | 925 | door: Exit |
919 | paintings: | 926 | painting: arrows_painting_9 |
920 | - flower_painting_6 | 927 | - pos: [610, 674] |
921 | tags: | 928 | painting: flower_painting_6 |
929 | entrances: | ||
922 | - flower_arrow | 930 | - flower_arrow |
923 | - pos: [1101, 222] | 931 | - pos: [1156, 262] |
924 | paintings: | 932 | painting: panda_painting |
925 | - panda_painting | 933 | entrances: |
926 | tags: | ||
927 | - panda_hallway | 934 | - panda_hallway |
928 | - pos: [1152, 209] | 935 | - pos: [1152, 209] |
929 | room: Hallway Room (1) | 936 | room: Hallway Room (1) |
930 | door: Exit | 937 | door: Exit |
931 | - pos: [1189, 170] | 938 | - pos: [1190, 171] |
932 | room: Hallway Room (2) | 939 | room: Hallway Room (2) |
933 | door: Exit | 940 | door: Exit |
934 | - pos: [1238, 124] | 941 | - pos: [1238, 124] |
@@ -943,16 +950,17 @@ | |||
943 | - pos: [1415, 140] | 950 | - pos: [1415, 140] |
944 | room: Number Hunt | 951 | room: Number Hunt |
945 | door: Nines | 952 | door: Nines |
946 | - pos: [1458, 133] | 953 | - pos: [1424, 85] |
947 | paintings: | 954 | painting: south_afar |
948 | - south_afar | ||
949 | - pos: [826, 1452] | 955 | - pos: [826, 1452] |
950 | room: Outside The Wanderer | 956 | room: Outside The Wanderer |
951 | door: Wanderer Entrance | 957 | door: Wanderer Entrance |
958 | tilted: true | ||
952 | - pos: [763, 1465] | 959 | - pos: [763, 1465] |
953 | room: Outside The Wanderer | 960 | room: Outside The Wanderer |
954 | door: Tower Entrance | 961 | door: Tower Entrance |
955 | - pos: [1655, 1151] | 962 | tilted: true |
963 | - pos: [1656, 1152] | ||
956 | room: Number Hunt | 964 | room: Number Hunt |
957 | door: Eights | 965 | door: Eights |
958 | - pos: [1623, 1044] | 966 | - pos: [1623, 1044] |
@@ -970,23 +978,26 @@ | |||
970 | - pos: [1511, 1119] | 978 | - pos: [1511, 1119] |
971 | room: Art Gallery | 979 | room: Art Gallery |
972 | door: Exit | 980 | door: Exit |
973 | - pos: [1654, 1116] | 981 | - pos: [1730, 1162] |
974 | paintings: | 982 | painting: flower_painting_2 |
975 | - smile_painting_3 | 983 | - pos: [1730, 1189] |
976 | - flower_painting_2 | 984 | painting: map_painting |
977 | - scenery_painting_0a | 985 | - pos: [1698, 1189] |
978 | - map_painting | 986 | painting: smile_painting_3 |
979 | - fruitbowl_painting4 | 987 | entrances: |
980 | tags: | ||
981 | - smiley_gallery | 988 | - smiley_gallery |
989 | - pos: [1714, 1215] | ||
990 | painting: fruitbowl_painting4 | ||
991 | - pos: [1714, 1242] | ||
992 | painting: scenery_painting_0a | ||
982 | - pos: [1120, 1286] | 993 | - pos: [1120, 1286] |
983 | room: Rhyme Room (Smiley) | 994 | room: Rhyme Room (Smiley) |
984 | door: Door to Target | 995 | door: Door to Target |
985 | - pos: [1120, 1315] | 996 | - pos: [1120, 1315] |
986 | tags: | 997 | entrances: # this could be considered 2 way since the subway map has a one way gate at the exit anyway |
987 | - rhyme_smiley_target | 998 | - rhyme_smiley_target |
988 | - pos: [792, 1137] | 999 | - pos: [792, 1137] |
989 | tags: | 1000 | exits: |
990 | - rhyme_smiley_target | 1001 | - rhyme_smiley_target |
991 | - pos: [895, 1217] | 1002 | - pos: [895, 1217] |
992 | room: Number Hunt | 1003 | room: Number Hunt |
@@ -997,9 +1008,8 @@ | |||
997 | - pos: [1120, 1195] | 1008 | - pos: [1120, 1195] |
998 | room: Rhyme Room (Circle) | 1009 | room: Rhyme Room (Circle) |
999 | door: Door to Smiley | 1010 | door: Door to Smiley |
1000 | - pos: [1118, 1137] | 1011 | - pos: [1130, 1124] |
1001 | paintings: | 1012 | painting: arrows_painting_3 |
1002 | - arrows_painting_3 | ||
1003 | - pos: [1050, 1142] | 1013 | - pos: [1050, 1142] |
1004 | room: Rhyme Room (Looped Square) | 1014 | room: Rhyme Room (Looped Square) |
1005 | door: Door to Circle | 1015 | door: Door to Circle |
@@ -1012,26 +1022,41 @@ | |||
1012 | - pos: [852, 1200] | 1022 | - pos: [852, 1200] |
1013 | room: Rhyme Room (Target) | 1023 | room: Rhyme Room (Target) |
1014 | door: Door to Cross | 1024 | door: Door to Cross |
1015 | - pos: [850, 1138] | 1025 | - pos: [862, 1124] |
1016 | paintings: | 1026 | painting: arrows_painting_4 |
1017 | - arrows_painting_4 | ||
1018 | - pos: [1592, 1442] | 1027 | - pos: [1592, 1442] |
1019 | room: Room Room | 1028 | room: Room Room |
1020 | door: Cellar Exit | 1029 | door: Cellar Exit |
1021 | - pos: [1570, 938] | 1030 | - pos: [1623, 938] |
1022 | room: Outside The Wise | 1031 | room: Outside The Wise |
1023 | door: Wise Entrance | 1032 | door: Wise Entrance |
1024 | - pos: [1653, 935] | 1033 | - pos: [1665, 920] |
1025 | paintings: | 1034 | painting: clock_painting_3 |
1026 | - clock_painting_3 | 1035 | - pos: [241, 348] |
1027 | - pos: [369, 605] | ||
1028 | room: Outside The Scientific | 1036 | room: Outside The Scientific |
1029 | door: Scientific Entrance | 1037 | door: Scientific Entrance |
1030 | - pos: [294, 602] | 1038 | - pos: [176, 326] |
1031 | paintings: | 1039 | painting: hi_solved_painting4 |
1032 | - hi_solved_painting4 | 1040 | entrances: |
1033 | tags: | ||
1034 | - hi_scientific | 1041 | - hi_scientific |
1035 | - pos: [814, 1001] | 1042 | - pos: [815, 1002] |
1036 | room: Challenge Room | 1043 | room: Challenge Room |
1037 | door: Welcome Door | 1044 | door: Welcome Door |
1045 | - pos: [104, 1208] | ||
1046 | special: color_black | ||
1047 | - pos: [104, 1249] | ||
1048 | special: color_red | ||
1049 | - pos: [104, 1290] | ||
1050 | special: color_yellow | ||
1051 | - pos: [104, 1330] | ||
1052 | special: color_blue | ||
1053 | - pos: [104, 1371] | ||
1054 | special: color_purple | ||
1055 | - pos: [104, 1411] | ||
1056 | special: color_orange | ||
1057 | - pos: [104, 1451] | ||
1058 | special: color_green | ||
1059 | - pos: [104, 1491] | ||
1060 | special: color_brown | ||
1061 | - pos: [104, 1531] | ||
1062 | special: color_gray | ||
diff --git a/src/achievements_pane.cpp b/src/achievements_pane.cpp index 8ec3727..d23c434 100644 --- a/src/achievements_pane.cpp +++ b/src/achievements_pane.cpp | |||
@@ -8,23 +8,24 @@ AchievementsPane::AchievementsPane(wxWindow* parent) | |||
8 | AppendColumn("Achievement"); | 8 | AppendColumn("Achievement"); |
9 | 9 | ||
10 | for (int panel_id : GD_GetAchievementPanels()) { | 10 | for (int panel_id : GD_GetAchievementPanels()) { |
11 | achievement_names_.push_back(GD_GetPanel(panel_id).achievement_name); | 11 | const Panel& panel = GD_GetPanel(panel_id); |
12 | achievements_.emplace_back(panel.achievement_name, panel.solve_index); | ||
12 | } | 13 | } |
13 | 14 | ||
14 | std::sort(std::begin(achievement_names_), std::end(achievement_names_)); | 15 | std::sort(std::begin(achievements_), std::end(achievements_)); |
15 | 16 | ||
16 | for (int i = 0; i < achievement_names_.size(); i++) { | 17 | for (int i = 0; i < achievements_.size(); i++) { |
17 | InsertItem(i, achievement_names_.at(i)); | 18 | InsertItem(i, std::get<0>(achievements_.at(i))); |
18 | } | 19 | } |
19 | 20 | ||
20 | SetColumnWidth(0, wxLIST_AUTOSIZE); | 21 | SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER); |
21 | 22 | ||
22 | UpdateIndicators(); | 23 | UpdateIndicators(); |
23 | } | 24 | } |
24 | 25 | ||
25 | void AchievementsPane::UpdateIndicators() { | 26 | void AchievementsPane::UpdateIndicators() { |
26 | for (int i = 0; i < achievement_names_.size(); i++) { | 27 | for (int i = 0; i < achievements_.size(); i++) { |
27 | if (AP_HasAchievement(achievement_names_.at(i))) { | 28 | if (AP_IsPanelSolved(std::get<1>(achievements_.at(i)))) { |
28 | SetItemTextColour(i, *wxBLACK); | 29 | SetItemTextColour(i, *wxBLACK); |
29 | } else { | 30 | } else { |
30 | SetItemTextColour(i, *wxRED); | 31 | SetItemTextColour(i, *wxRED); |
diff --git a/src/achievements_pane.h b/src/achievements_pane.h index ac88cac..941b5e3 100644 --- a/src/achievements_pane.h +++ b/src/achievements_pane.h | |||
@@ -9,6 +9,10 @@ | |||
9 | 9 | ||
10 | #include <wx/listctrl.h> | 10 | #include <wx/listctrl.h> |
11 | 11 | ||
12 | #include <string> | ||
13 | #include <tuple> | ||
14 | #include <vector> | ||
15 | |||
12 | class AchievementsPane : public wxListView { | 16 | class AchievementsPane : public wxListView { |
13 | public: | 17 | public: |
14 | explicit AchievementsPane(wxWindow* parent); | 18 | explicit AchievementsPane(wxWindow* parent); |
@@ -16,7 +20,7 @@ class AchievementsPane : public wxListView { | |||
16 | void UpdateIndicators(); | 20 | void UpdateIndicators(); |
17 | 21 | ||
18 | private: | 22 | private: |
19 | std::vector<std::string> achievement_names_; | 23 | std::vector<std::tuple<std::string, int>> achievements_; // name, solve index |
20 | }; | 24 | }; |
21 | 25 | ||
22 | #endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file | 26 | #endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file |
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index f8d4ee0..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp | |||
@@ -10,6 +10,7 @@ | |||
10 | #include <any> | 10 | #include <any> |
11 | #include <apclient.hpp> | 11 | #include <apclient.hpp> |
12 | #include <apuuid.hpp> | 12 | #include <apuuid.hpp> |
13 | #include <bitset> | ||
13 | #include <chrono> | 14 | #include <chrono> |
14 | #include <exception> | 15 | #include <exception> |
15 | #include <filesystem> | 16 | #include <filesystem> |
@@ -22,34 +23,59 @@ | |||
22 | #include <tuple> | 23 | #include <tuple> |
23 | 24 | ||
24 | #include "game_data.h" | 25 | #include "game_data.h" |
26 | #include "ipc_state.h" | ||
25 | #include "logger.h" | 27 | #include "logger.h" |
26 | #include "tracker_frame.h" | 28 | #include "tracker_frame.h" |
27 | #include "tracker_state.h" | 29 | #include "tracker_state.h" |
28 | 30 | ||
29 | constexpr int AP_MAJOR = 0; | 31 | constexpr int AP_MAJOR = 0; |
30 | constexpr int AP_MINOR = 4; | 32 | constexpr int AP_MINOR = 6; |
31 | constexpr int AP_REVISION = 5; | 33 | constexpr int AP_REVISION = 1; |
32 | 34 | ||
33 | constexpr const char* CERT_STORE_PATH = "cacert.pem"; | 35 | constexpr const char* CERT_STORE_PATH = "cacert.pem"; |
34 | constexpr int ITEM_HANDLING = 7; // <- all | 36 | constexpr int ITEM_HANDLING = 7; // <- all |
35 | 37 | ||
38 | constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds | ||
39 | constexpr int CONNECTION_BACKOFF_INTERVAL = 100; | ||
40 | |||
41 | constexpr int PANEL_COUNT = 803; | ||
42 | constexpr int PANEL_BITFIELD_LENGTH = 48; | ||
43 | constexpr int PANEL_BITFIELDS = 17; | ||
44 | |||
36 | namespace { | 45 | namespace { |
37 | 46 | ||
38 | struct APState { | 47 | const std::set<long> kNonProgressionItems = { |
39 | std::unique_ptr<APClient> apclient; | 48 | 444409, // :) |
49 | 444575, // The Feeling of Being Lost | ||
50 | 444576, // Wanderlust | ||
51 | 444577, // Empty White Hallways | ||
52 | 444410, // Slowness Trap | ||
53 | 444411, // Iceland Trap | ||
54 | 444412, // Atbash Trap | ||
55 | 444413, // Puzzle Skip | ||
56 | 444680, // Speed Boost | ||
57 | }; | ||
40 | 58 | ||
59 | struct APState { | ||
60 | // Initialized on main thread | ||
41 | bool initialized = false; | 61 | bool initialized = false; |
42 | |||
43 | TrackerFrame* tracker_frame = nullptr; | 62 | TrackerFrame* tracker_frame = nullptr; |
63 | std::list<std::string> tracked_data_storage_keys; | ||
44 | 64 | ||
45 | bool client_active = false; | 65 | // Client |
46 | std::mutex client_mutex; | 66 | std::mutex client_mutex; |
67 | std::unique_ptr<APClient> apclient; | ||
68 | |||
69 | // Protected state | ||
70 | std::mutex state_mutex; | ||
71 | |||
72 | std::string status_message = "Not connected to Archipelago."; | ||
47 | 73 | ||
48 | bool connected = false; | 74 | bool connected = false; |
49 | bool has_connection_result = false; | 75 | std::string connection_failure; |
76 | int remaining_loops = 0; | ||
50 | 77 | ||
51 | std::string data_storage_prefix; | 78 | std::string data_storage_prefix; |
52 | std::list<std::string> tracked_data_storage_keys; | ||
53 | std::string victory_data_storage_key; | 79 | std::string victory_data_storage_key; |
54 | 80 | ||
55 | std::string save_name; | 81 | std::string save_name; |
@@ -58,9 +84,12 @@ struct APState { | |||
58 | std::set<int64_t> checked_locations; | 84 | std::set<int64_t> checked_locations; |
59 | std::map<std::string, std::any> data_storage; | 85 | std::map<std::string, std::any> data_storage; |
60 | std::optional<std::tuple<int, int>> player_pos; | 86 | std::optional<std::tuple<int, int>> player_pos; |
87 | std::bitset<PANEL_COUNT> solved_panels; | ||
61 | 88 | ||
62 | DoorShuffleMode door_shuffle_mode = kNO_DOORS; | 89 | DoorShuffleMode door_shuffle_mode = kNO_DOORS; |
90 | bool group_doors = false; | ||
63 | bool color_shuffle = false; | 91 | bool color_shuffle = false; |
92 | PanelShuffleMode panel_shuffle_mode = kNO_PANELS; | ||
64 | bool painting_shuffle = false; | 93 | bool painting_shuffle = false; |
65 | int mastery_requirement = 21; | 94 | int mastery_requirement = 21; |
66 | int level_2_requirement = 223; | 95 | int level_2_requirement = 223; |
@@ -72,171 +101,387 @@ struct APState { | |||
72 | bool pilgrimage_allows_paintings = false; | 101 | bool pilgrimage_allows_paintings = false; |
73 | SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; | 102 | SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; |
74 | bool sunwarp_shuffle = false; | 103 | bool sunwarp_shuffle = false; |
104 | bool postgame_shuffle = true; | ||
75 | 105 | ||
76 | std::map<std::string, std::string> painting_mapping; | 106 | std::map<std::string, std::string> painting_mapping; |
77 | std::set<std::string> painting_codomain; | 107 | std::set<std::string> painting_codomain; |
78 | std::map<int, SunwarpMapping> sunwarp_mapping; | 108 | std::map<int, SunwarpMapping> sunwarp_mapping; |
79 | 109 | ||
80 | void Connect(std::string server, std::string player, std::string password) { | 110 | void Connect(std::string server, std::string player, std::string password) { |
111 | Initialize(); | ||
112 | |||
113 | { | ||
114 | std::lock_guard state_guard(state_mutex); | ||
115 | SetStatusMessage("Connecting to Archipelago server...."); | ||
116 | } | ||
117 | TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server)); | ||
118 | |||
119 | // Creating and setting up the client has to all be done while holding the | ||
120 | // client mutex, so that the other thread doesn't try to poll before we add | ||
121 | // handlers, etc. | ||
122 | { | ||
123 | TrackerLog("Destroying old AP client..."); | ||
124 | |||
125 | std::lock_guard client_guard(client_mutex); | ||
126 | |||
127 | if (apclient) { | ||
128 | DestroyClient(); | ||
129 | } | ||
130 | |||
131 | std::string cert_store = ""; | ||
132 | if (std::filesystem::exists(CERT_STORE_PATH)) { | ||
133 | cert_store = CERT_STORE_PATH; | ||
134 | } | ||
135 | |||
136 | apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, | ||
137 | cert_store); | ||
138 | |||
139 | { | ||
140 | std::lock_guard state_guard(state_mutex); | ||
141 | |||
142 | connected = false; | ||
143 | connection_failure.clear(); | ||
144 | remaining_loops = CONNECTION_TIMEOUT / CONNECTION_BACKOFF_INTERVAL; | ||
145 | |||
146 | save_name.clear(); | ||
147 | inventory.clear(); | ||
148 | checked_locations.clear(); | ||
149 | data_storage.clear(); | ||
150 | player_pos = std::nullopt; | ||
151 | solved_panels.reset(); | ||
152 | victory_data_storage_key.clear(); | ||
153 | door_shuffle_mode = kNO_DOORS; | ||
154 | group_doors = false; | ||
155 | color_shuffle = false; | ||
156 | panel_shuffle_mode = kNO_PANELS; | ||
157 | painting_shuffle = false; | ||
158 | painting_mapping.clear(); | ||
159 | painting_codomain.clear(); | ||
160 | mastery_requirement = 21; | ||
161 | level_2_requirement = 223; | ||
162 | location_checks = kNORMAL_LOCATIONS; | ||
163 | victory_condition = kTHE_END; | ||
164 | early_color_hallways = false; | ||
165 | pilgrimage_enabled = false; | ||
166 | pilgrimage_allows_roof_access = false; | ||
167 | pilgrimage_allows_paintings = false; | ||
168 | sunwarp_access = kSUNWARP_ACCESS_NORMAL; | ||
169 | sunwarp_shuffle = false; | ||
170 | sunwarp_mapping.clear(); | ||
171 | postgame_shuffle = true; | ||
172 | } | ||
173 | |||
174 | apclient->set_room_info_handler( | ||
175 | [this, player, password]() { OnRoomInfo(player, password); }); | ||
176 | |||
177 | apclient->set_location_checked_handler( | ||
178 | [this](const std::list<int64_t>& locations) { | ||
179 | OnLocationChecked(locations); | ||
180 | }); | ||
181 | |||
182 | apclient->set_slot_disconnected_handler( | ||
183 | [this]() { OnSlotDisconnected(); }); | ||
184 | |||
185 | apclient->set_socket_disconnected_handler( | ||
186 | [this]() { OnSocketDisconnected(); }); | ||
187 | |||
188 | apclient->set_items_received_handler( | ||
189 | [this](const std::list<APClient::NetworkItem>& items) { | ||
190 | OnItemsReceived(items); | ||
191 | }); | ||
192 | |||
193 | apclient->set_retrieved_handler( | ||
194 | [this](const std::map<std::string, nlohmann::json>& data) { | ||
195 | OnRetrieved(data); | ||
196 | }); | ||
197 | |||
198 | apclient->set_set_reply_handler( | ||
199 | [this](const std::string& key, const nlohmann::json& value, | ||
200 | const nlohmann::json&) { OnSetReply(key, value); }); | ||
201 | |||
202 | apclient->set_slot_connected_handler( | ||
203 | [this, player, server](const nlohmann::json& slot_data) { | ||
204 | OnSlotConnected(player, server, slot_data); | ||
205 | }); | ||
206 | |||
207 | apclient->set_slot_refused_handler( | ||
208 | [this](const std::list<std::string>& errors) { | ||
209 | OnSlotRefused(errors); | ||
210 | }); | ||
211 | } | ||
212 | } | ||
213 | |||
214 | std::string GetStatusMessage() { | ||
215 | std::lock_guard state_guard(state_mutex); | ||
216 | |||
217 | return status_message; | ||
218 | } | ||
219 | |||
220 | bool HasCheckedGameLocation(int location_id) { | ||
221 | std::lock_guard state_guard(state_mutex); | ||
222 | |||
223 | return checked_locations.count(location_id); | ||
224 | } | ||
225 | |||
226 | bool HasItem(int item_id, int quantity) { | ||
227 | return inventory.count(item_id) && inventory.at(item_id) >= quantity; | ||
228 | } | ||
229 | |||
230 | bool HasItemSafe(int item_id, int quantity) { | ||
231 | std::lock_guard state_guard(state_mutex); | ||
232 | return HasItem(item_id, quantity); | ||
233 | } | ||
234 | |||
235 | const std::set<std::string>& GetCheckedPaintings() { | ||
236 | std::lock_guard state_guard(state_mutex); | ||
237 | |||
238 | std::string key = fmt::format("{}Paintings", data_storage_prefix); | ||
239 | if (!data_storage.count(key)) { | ||
240 | data_storage[key] = std::set<std::string>(); | ||
241 | } | ||
242 | |||
243 | return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); | ||
244 | } | ||
245 | |||
246 | bool IsPaintingChecked(const std::string& painting_id) { | ||
247 | const auto& checked_paintings = GetCheckedPaintings(); | ||
248 | |||
249 | std::lock_guard state_guard(state_mutex); | ||
250 | |||
251 | return checked_paintings.count(painting_id) || | ||
252 | (painting_mapping.count(painting_id) && | ||
253 | checked_paintings.count(painting_mapping.at(painting_id))); | ||
254 | } | ||
255 | |||
256 | void RevealPaintings() { | ||
257 | std::lock_guard state_guard(state_mutex); | ||
258 | |||
259 | std::vector<std::string> paintings; | ||
260 | for (const PaintingExit& painting : GD_GetPaintings()) { | ||
261 | paintings.push_back(painting.internal_id); | ||
262 | } | ||
263 | |||
264 | APClient::DataStorageOperation operation; | ||
265 | operation.operation = "replace"; | ||
266 | operation.value = paintings; | ||
267 | |||
268 | apclient->Set(fmt::format("{}Paintings", data_storage_prefix), "", true, | ||
269 | {operation}); | ||
270 | } | ||
271 | |||
272 | bool HasReachedGoal() { | ||
273 | std::lock_guard state_guard(state_mutex); | ||
274 | |||
275 | return data_storage.count(victory_data_storage_key) && | ||
276 | std::any_cast<int>(data_storage.at(victory_data_storage_key)) == | ||
277 | 30; // CLIENT_GOAL | ||
278 | } | ||
279 | |||
280 | bool IsPanelSolved(int solve_index) { | ||
281 | std::lock_guard state_guard(state_mutex); | ||
282 | |||
283 | return solved_panels.test(solve_index); | ||
284 | } | ||
285 | |||
286 | private: | ||
287 | void Initialize() { | ||
81 | if (!initialized) { | 288 | if (!initialized) { |
82 | TrackerLog("Initializing APState..."); | 289 | TrackerLog("Initializing APState..."); |
83 | 290 | ||
84 | std::thread([this]() { | 291 | std::thread([this]() { Thread(); }).detach(); |
85 | for (;;) { | 292 | |
293 | for (int i = 0; i < PANEL_BITFIELDS; i++) { | ||
294 | tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i)); | ||
295 | } | ||
296 | |||
297 | tracked_data_storage_keys.push_back("PlayerPos"); | ||
298 | tracked_data_storage_keys.push_back("Paintings"); | ||
299 | |||
300 | initialized = true; | ||
301 | } | ||
302 | } | ||
303 | |||
304 | void Thread() { | ||
305 | std::string display_error; | ||
306 | |||
307 | for (;;) { | ||
308 | { | ||
309 | std::lock_guard client_guard(client_mutex); | ||
310 | if (apclient) { | ||
311 | apclient->poll(); | ||
312 | |||
86 | { | 313 | { |
87 | std::lock_guard client_guard(client_mutex); | 314 | std::lock_guard state_guard(state_mutex); |
88 | if (apclient) { | 315 | |
89 | apclient->poll(); | 316 | if (!connected) { |
317 | if (!connection_failure.empty()) { | ||
318 | TrackerLog(connection_failure); | ||
319 | |||
320 | display_error = connection_failure; | ||
321 | connection_failure.clear(); | ||
322 | |||
323 | DestroyClient(); | ||
324 | } else { | ||
325 | remaining_loops--; | ||
326 | |||
327 | if (remaining_loops <= 0) { | ||
328 | DestroyClient(); | ||
329 | |||
330 | SetStatusMessage("Disconnected from Archipelago."); | ||
331 | TrackerLog("Timeout while connecting to Archipelago server."); | ||
332 | |||
333 | display_error = | ||
334 | "Timeout while connecting to Archipelago server."; | ||
335 | } | ||
336 | } | ||
90 | } | 337 | } |
91 | } | 338 | } |
92 | |||
93 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
94 | } | 339 | } |
95 | }).detach(); | ||
96 | |||
97 | for (int panel_id : GD_GetAchievementPanels()) { | ||
98 | tracked_data_storage_keys.push_back(fmt::format( | ||
99 | "Achievement|{}", GD_GetPanel(panel_id).achievement_name)); | ||
100 | } | 340 | } |
101 | 341 | ||
102 | for (const MapArea& map_area : GD_GetMapAreas()) { | 342 | if (!display_error.empty()) { |
103 | for (const Location& location : map_area.locations) { | 343 | wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR); |
104 | tracked_data_storage_keys.push_back( | 344 | display_error.clear(); |
105 | fmt::format("Hunt|{}", location.ap_location_id)); | ||
106 | } | ||
107 | } | 345 | } |
108 | 346 | ||
109 | tracked_data_storage_keys.push_back("PlayerPos"); | 347 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
110 | tracked_data_storage_keys.push_back("Paintings"); | 348 | } |
349 | } | ||
111 | 350 | ||
112 | initialized = true; | 351 | void OnRoomInfo(std::string player, std::string password) { |
352 | { | ||
353 | std::lock_guard state_guard(state_mutex); | ||
354 | |||
355 | inventory.clear(); | ||
356 | |||
357 | SetStatusMessage("Connected to Archipelago server. Authenticating..."); | ||
113 | } | 358 | } |
114 | 359 | ||
115 | tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); | 360 | TrackerLog(fmt::format( |
116 | TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server)); | 361 | "Connected to Archipelago server. Authenticating as {} {}", player, |
362 | (password.empty() ? "without password" : "with password " + password))); | ||
363 | |||
364 | apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"}, | ||
365 | {AP_MAJOR, AP_MINOR, AP_REVISION}); | ||
366 | } | ||
117 | 367 | ||
368 | void OnLocationChecked(const std::list<int64_t>& locations) { | ||
118 | { | 369 | { |
119 | TrackerLog("Destroying old AP client..."); | 370 | std::lock_guard state_guard(state_mutex); |
120 | 371 | ||
121 | std::lock_guard client_guard(client_mutex); | 372 | for (const int64_t location_id : locations) { |
373 | checked_locations.insert(location_id); | ||
374 | TrackerLog(fmt::format("Location: {}", location_id)); | ||
375 | } | ||
376 | } | ||
122 | 377 | ||
123 | if (apclient) { | 378 | RefreshTracker(StateUpdate{.cleared_locations = true}); |
124 | DestroyClient(); | 379 | } |
380 | |||
381 | void OnSlotDisconnected() { | ||
382 | std::lock_guard state_guard(state_mutex); | ||
383 | |||
384 | SetStatusMessage( | ||
385 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
386 | TrackerLog( | ||
387 | "Slot disconnected from Archipelago. Attempting to reconnect..."); | ||
388 | } | ||
389 | |||
390 | void OnSocketDisconnected() { | ||
391 | std::lock_guard state_guard(state_mutex); | ||
392 | |||
393 | SetStatusMessage( | ||
394 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
395 | TrackerLog( | ||
396 | "Socket disconnected from Archipelago. Attempting to reconnect..."); | ||
397 | } | ||
398 | |||
399 | void OnItemsReceived(const std::list<APClient::NetworkItem>& items) { | ||
400 | std::vector<ItemState> item_states; | ||
401 | bool progression_items = false; | ||
402 | |||
403 | { | ||
404 | std::lock_guard state_guard(state_mutex); | ||
405 | |||
406 | std::map<int64_t, int> index_by_item; | ||
407 | |||
408 | for (const APClient::NetworkItem& item : items) { | ||
409 | inventory[item.item]++; | ||
410 | TrackerLog(fmt::format("Item: {}", item.item)); | ||
411 | |||
412 | index_by_item[item.item] = item.index; | ||
413 | |||
414 | if (!kNonProgressionItems.count(item.item)) { | ||
415 | progression_items = true; | ||
416 | } | ||
125 | } | 417 | } |
126 | 418 | ||
127 | std::string cert_store = ""; | 419 | for (const auto& [item_id, item_index] : index_by_item) { |
128 | if (std::filesystem::exists(CERT_STORE_PATH)) { | 420 | item_states.push_back(ItemState{.name = GD_GetItemName(item_id), |
129 | cert_store = CERT_STORE_PATH; | 421 | .amount = inventory[item_id], |
422 | .index = item_index}); | ||
130 | } | 423 | } |
424 | } | ||
131 | 425 | ||
132 | apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, | 426 | RefreshTracker(StateUpdate{.items = item_states, |
133 | cert_store); | 427 | .progression_items = progression_items}); |
428 | } | ||
429 | |||
430 | void OnRetrieved(const std::map<std::string, nlohmann::json>& data) { | ||
431 | StateUpdate state_update; | ||
432 | |||
433 | { | ||
434 | std::lock_guard state_guard(state_mutex); | ||
435 | |||
436 | for (const auto& [key, value] : data) { | ||
437 | HandleDataStorage(key, value, state_update); | ||
438 | } | ||
134 | } | 439 | } |
135 | 440 | ||
136 | save_name.clear(); | 441 | RefreshTracker(state_update); |
137 | inventory.clear(); | 442 | } |
138 | checked_locations.clear(); | ||
139 | data_storage.clear(); | ||
140 | player_pos = std::nullopt; | ||
141 | victory_data_storage_key.clear(); | ||
142 | door_shuffle_mode = kNO_DOORS; | ||
143 | color_shuffle = false; | ||
144 | painting_shuffle = false; | ||
145 | painting_mapping.clear(); | ||
146 | painting_codomain.clear(); | ||
147 | mastery_requirement = 21; | ||
148 | level_2_requirement = 223; | ||
149 | location_checks = kNORMAL_LOCATIONS; | ||
150 | victory_condition = kTHE_END; | ||
151 | early_color_hallways = false; | ||
152 | pilgrimage_enabled = false; | ||
153 | pilgrimage_allows_roof_access = false; | ||
154 | pilgrimage_allows_paintings = false; | ||
155 | sunwarp_access = kSUNWARP_ACCESS_NORMAL; | ||
156 | sunwarp_shuffle = false; | ||
157 | sunwarp_mapping.clear(); | ||
158 | |||
159 | std::mutex connection_mutex; | ||
160 | connected = false; | ||
161 | has_connection_result = false; | ||
162 | |||
163 | apclient->set_room_info_handler([this, player, password]() { | ||
164 | inventory.clear(); | ||
165 | 443 | ||
166 | TrackerLog(fmt::format( | 444 | void OnSetReply(const std::string& key, const nlohmann::json& value) { |
167 | "Connected to Archipelago server. Authenticating as {} {}", player, | 445 | StateUpdate state_update; |
168 | (password.empty() ? "without password" | ||
169 | : "with password " + password))); | ||
170 | tracker_frame->SetStatusMessage( | ||
171 | "Connected to Archipelago server. Authenticating..."); | ||
172 | |||
173 | apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"}, | ||
174 | {AP_MAJOR, AP_MINOR, AP_REVISION}); | ||
175 | }); | ||
176 | |||
177 | apclient->set_location_checked_handler( | ||
178 | [this](const std::list<int64_t>& locations) { | ||
179 | for (const int64_t location_id : locations) { | ||
180 | checked_locations.insert(location_id); | ||
181 | TrackerLog(fmt::format("Location: {}", location_id)); | ||
182 | } | ||
183 | 446 | ||
184 | RefreshTracker(false); | 447 | { |
185 | }); | 448 | std::lock_guard state_guard(state_mutex); |
186 | 449 | HandleDataStorage(key, value, state_update); | |
187 | apclient->set_slot_disconnected_handler([this]() { | 450 | } |
188 | tracker_frame->SetStatusMessage( | ||
189 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
190 | TrackerLog( | ||
191 | "Slot disconnected from Archipelago. Attempting to reconnect..."); | ||
192 | }); | ||
193 | |||
194 | apclient->set_socket_disconnected_handler([this]() { | ||
195 | tracker_frame->SetStatusMessage( | ||
196 | "Disconnected from Archipelago. Attempting to reconnect..."); | ||
197 | TrackerLog( | ||
198 | "Socket disconnected from Archipelago. Attempting to reconnect..."); | ||
199 | }); | ||
200 | |||
201 | apclient->set_items_received_handler( | ||
202 | [this](const std::list<APClient::NetworkItem>& items) { | ||
203 | for (const APClient::NetworkItem& item : items) { | ||
204 | inventory[item.item]++; | ||
205 | TrackerLog(fmt::format("Item: {}", item.item)); | ||
206 | } | ||
207 | 451 | ||
208 | RefreshTracker(false); | 452 | RefreshTracker(state_update); |
209 | }); | 453 | } |
210 | 454 | ||
211 | apclient->set_retrieved_handler( | 455 | void OnSlotConnected(std::string player, std::string server, |
212 | [this](const std::map<std::string, nlohmann::json>& data) { | 456 | const nlohmann::json& slot_data) { |
213 | for (const auto& [key, value] : data) { | 457 | IPC_SetTrackerSlot(server, player); |
214 | HandleDataStorage(key, value); | ||
215 | } | ||
216 | 458 | ||
217 | RefreshTracker(false); | 459 | TrackerLog("Connected to Archipelago!"); |
218 | }); | ||
219 | 460 | ||
220 | apclient->set_set_reply_handler([this](const std::string& key, | 461 | { |
221 | const nlohmann::json& value, | 462 | std::lock_guard state_guard(state_mutex); |
222 | const nlohmann::json&) { | ||
223 | HandleDataStorage(key, value); | ||
224 | RefreshTracker(false); | ||
225 | }); | ||
226 | 463 | ||
227 | apclient->set_slot_connected_handler([this, player, server, | 464 | SetStatusMessage( |
228 | &connection_mutex]( | 465 | fmt::format("Connected to Archipelago! ({}@{}).", player, server)); |
229 | const nlohmann::json& slot_data) { | ||
230 | tracker_frame->SetStatusMessage( | ||
231 | fmt::format("Connected to Archipelago! ({}@{})", player, server)); | ||
232 | TrackerLog("Connected to Archipelago!"); | ||
233 | 466 | ||
234 | save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), | 467 | save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), |
235 | apclient->get_player_number()); | 468 | apclient->get_player_number()); |
236 | data_storage_prefix = | 469 | data_storage_prefix = |
237 | fmt::format("Lingo_{}_", apclient->get_player_number()); | 470 | fmt::format("Lingo_{}_", apclient->get_player_number()); |
238 | door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); | 471 | door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); |
472 | if (slot_data.contains("group_doors")) { | ||
473 | group_doors = slot_data.contains("group_doors") && | ||
474 | slot_data["group_doors"].get<int>() == 1; | ||
475 | } else { | ||
476 | // If group_doors doesn't exist yet, that means kPANELS_MODE is | ||
477 | // actually kSIMPLE_DOORS. | ||
478 | if (door_shuffle_mode == kPANELS_MODE) { | ||
479 | door_shuffle_mode = kDOORS_MODE; | ||
480 | group_doors = true; | ||
481 | } | ||
482 | } | ||
239 | color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; | 483 | color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; |
484 | panel_shuffle_mode = slot_data["shuffle_panels"].get<PanelShuffleMode>(); | ||
240 | painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; | 485 | painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; |
241 | mastery_requirement = slot_data["mastery_achievements"].get<int>(); | 486 | mastery_requirement = slot_data["mastery_achievements"].get<int>(); |
242 | level_2_requirement = slot_data["level_2_requirement"].get<int>(); | 487 | level_2_requirement = slot_data["level_2_requirement"].get<int>(); |
@@ -258,6 +503,9 @@ struct APState { | |||
258 | : kSUNWARP_ACCESS_NORMAL; | 503 | : kSUNWARP_ACCESS_NORMAL; |
259 | sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && | 504 | sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && |
260 | slot_data["shuffle_sunwarps"].get<int>() == 1; | 505 | slot_data["shuffle_sunwarps"].get<int>() == 1; |
506 | postgame_shuffle = slot_data.contains("shuffle_postgame") | ||
507 | ? (slot_data["shuffle_postgame"].get<int>() == 1) | ||
508 | : true; | ||
261 | 509 | ||
262 | if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { | 510 | if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { |
263 | painting_mapping.clear(); | 511 | painting_mapping.clear(); |
@@ -295,111 +543,87 @@ struct APState { | |||
295 | apclient->Get(corrected_keys); | 543 | apclient->Get(corrected_keys); |
296 | apclient->SetNotify(corrected_keys); | 544 | apclient->SetNotify(corrected_keys); |
297 | 545 | ||
298 | ResetReachabilityRequirements(); | 546 | connected = true; |
299 | RefreshTracker(true); | 547 | } |
300 | |||
301 | { | ||
302 | std::lock_guard connection_lock(connection_mutex); | ||
303 | if (!has_connection_result) { | ||
304 | connected = true; | ||
305 | has_connection_result = true; | ||
306 | } | ||
307 | } | ||
308 | }); | ||
309 | |||
310 | apclient->set_slot_refused_handler( | ||
311 | [this, &connection_mutex](const std::list<std::string>& errors) { | ||
312 | { | ||
313 | std::lock_guard connection_lock(connection_mutex); | ||
314 | connected = false; | ||
315 | has_connection_result = true; | ||
316 | } | ||
317 | |||
318 | tracker_frame->SetStatusMessage("Disconnected from Archipelago."); | ||
319 | |||
320 | std::vector<std::string> error_messages; | ||
321 | error_messages.push_back("Could not connect to Archipelago."); | ||
322 | |||
323 | for (const std::string& error : errors) { | ||
324 | if (error == "InvalidSlot") { | ||
325 | error_messages.push_back("Invalid player name."); | ||
326 | } else if (error == "InvalidGame") { | ||
327 | error_messages.push_back( | ||
328 | "The specified player is not playing Lingo."); | ||
329 | } else if (error == "IncompatibleVersion") { | ||
330 | error_messages.push_back( | ||
331 | "The Archipelago server is not the correct version for this " | ||
332 | "client."); | ||
333 | } else if (error == "InvalidPassword") { | ||
334 | error_messages.push_back("Incorrect password."); | ||
335 | } else if (error == "InvalidItemsHandling") { | ||
336 | error_messages.push_back( | ||
337 | "Invalid item handling flag. This is a bug with the tracker. " | ||
338 | "Please report it to the lingo-ap-tracker GitHub."); | ||
339 | } else { | ||
340 | error_messages.push_back("Unknown error."); | ||
341 | } | ||
342 | } | ||
343 | |||
344 | std::string full_message = hatkirby::implode(error_messages, " "); | ||
345 | TrackerLog(full_message); | ||
346 | |||
347 | wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); | ||
348 | }); | ||
349 | |||
350 | client_active = true; | ||
351 | |||
352 | int timeout = 5000; // 5 seconds | ||
353 | int interval = 100; | ||
354 | int remaining_loops = timeout / interval; | ||
355 | while (true) { | ||
356 | { | ||
357 | std::lock_guard connection_lock(connection_mutex); | ||
358 | if (has_connection_result) { | ||
359 | break; | ||
360 | } | ||
361 | } | ||
362 | |||
363 | if (interval == 0) { | ||
364 | DestroyClient(); | ||
365 | |||
366 | tracker_frame->SetStatusMessage("Disconnected from Archipelago."); | ||
367 | TrackerLog("Timeout while connecting to Archipelago server."); | ||
368 | wxMessageBox("Timeout while connecting to Archipelago server.", | ||
369 | "Connection failed", wxOK | wxICON_ERROR); | ||
370 | 548 | ||
371 | { | 549 | ResetReachabilityRequirements(); |
372 | std::lock_guard connection_lock(connection_mutex); | 550 | RefreshTracker(std::nullopt); |
373 | connected = false; | 551 | } |
374 | has_connection_result = true; | ||
375 | } | ||
376 | 552 | ||
377 | break; | 553 | void OnSlotRefused(const std::list<std::string>& errors) { |
554 | std::vector<std::string> error_messages; | ||
555 | error_messages.push_back("Could not connect to Archipelago."); | ||
556 | |||
557 | for (const std::string& error : errors) { | ||
558 | if (error == "InvalidSlot") { | ||
559 | error_messages.push_back("Invalid player name."); | ||
560 | } else if (error == "InvalidGame") { | ||
561 | error_messages.push_back("The specified player is not playing Lingo."); | ||
562 | } else if (error == "IncompatibleVersion") { | ||
563 | error_messages.push_back( | ||
564 | "The Archipelago server is not the correct version for this " | ||
565 | "client."); | ||
566 | } else if (error == "InvalidPassword") { | ||
567 | error_messages.push_back("Incorrect password."); | ||
568 | } else if (error == "InvalidItemsHandling") { | ||
569 | error_messages.push_back( | ||
570 | "Invalid item handling flag. This is a bug with the tracker. " | ||
571 | "Please report it to the lingo-ap-tracker GitHub."); | ||
572 | } else { | ||
573 | error_messages.push_back("Unknown error."); | ||
378 | } | 574 | } |
575 | } | ||
379 | 576 | ||
380 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | 577 | { |
578 | std::lock_guard state_guard(state_mutex); | ||
579 | connection_failure = hatkirby::implode(error_messages, " "); | ||
381 | 580 | ||
382 | interval--; | 581 | SetStatusMessage("Disconnected from Archipelago."); |
383 | } | 582 | } |
583 | } | ||
384 | 584 | ||
385 | if (connected) { | 585 | // Assumes state mutex is locked. |
386 | client_active = false; | 586 | void SetStatusMessage(std::string msg) { |
387 | } | 587 | status_message = std::move(msg); |
588 | |||
589 | tracker_frame->UpdateStatusMessage(); | ||
388 | } | 590 | } |
389 | 591 | ||
390 | void HandleDataStorage(const std::string& key, const nlohmann::json& value) { | 592 | // Assumes state mutex is locked. |
593 | void HandleDataStorage(const std::string& key, const nlohmann::json& value, StateUpdate& state_update) { | ||
391 | if (value.is_boolean()) { | 594 | if (value.is_boolean()) { |
392 | data_storage[key] = value.get<bool>(); | 595 | data_storage[key] = value.get<bool>(); |
393 | TrackerLog(fmt::format("Data storage {} retrieved as {}", key, | 596 | TrackerLog(fmt::format("Data storage {} retrieved as {}", key, |
394 | (value.get<bool>() ? "true" : "false"))); | 597 | (value.get<bool>() ? "true" : "false"))); |
598 | |||
395 | } else if (value.is_number()) { | 599 | } else if (value.is_number()) { |
396 | data_storage[key] = value.get<int>(); | 600 | data_storage[key] = value.get<int>(); |
397 | TrackerLog(fmt::format("Data storage {} retrieved as {}", key, | 601 | TrackerLog(fmt::format("Data storage {} retrieved as {}", key, |
398 | value.get<int>())); | 602 | value.get<int>())); |
603 | |||
604 | if (key == victory_data_storage_key) { | ||
605 | state_update.cleared_locations = true; | ||
606 | } else if (key.find("Panels_") != std::string::npos) { | ||
607 | int bitfield_num = | ||
608 | std::stoi(key.substr(data_storage_prefix.size() + 7)); | ||
609 | uint64_t bitfield_value = value.get<uint64_t>(); | ||
610 | for (int i = 0; i < PANEL_BITFIELD_LENGTH; i++) { | ||
611 | if ((bitfield_value & (1LL << i)) != 0) { | ||
612 | int solve_index = bitfield_num * PANEL_BITFIELD_LENGTH + i; | ||
613 | |||
614 | if (!solved_panels.test(solve_index)) { | ||
615 | state_update.panels.insert(solve_index); | ||
616 | } | ||
617 | |||
618 | solved_panels.set(solve_index); | ||
619 | } | ||
620 | } | ||
621 | } | ||
399 | } else if (value.is_object()) { | 622 | } else if (value.is_object()) { |
400 | if (key.ends_with("PlayerPos")) { | 623 | if (key.ends_with("PlayerPos")) { |
401 | auto map_value = value.get<std::map<std::string, int>>(); | 624 | auto map_value = value.get<std::map<std::string, int>>(); |
402 | player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]); | 625 | player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]); |
626 | state_update.player_position = true; | ||
403 | } else { | 627 | } else { |
404 | data_storage[key] = value.get<std::map<std::string, int>>(); | 628 | data_storage[key] = value.get<std::map<std::string, int>>(); |
405 | } | 629 | } |
@@ -408,6 +632,7 @@ struct APState { | |||
408 | } else if (value.is_null()) { | 632 | } else if (value.is_null()) { |
409 | if (key.ends_with("PlayerPos")) { | 633 | if (key.ends_with("PlayerPos")) { |
410 | player_pos = std::nullopt; | 634 | player_pos = std::nullopt; |
635 | state_update.player_position = true; | ||
411 | } else { | 636 | } else { |
412 | data_storage.erase(key); | 637 | data_storage.erase(key); |
413 | } | 638 | } |
@@ -419,6 +644,8 @@ struct APState { | |||
419 | if (key.ends_with("Paintings")) { | 644 | if (key.ends_with("Paintings")) { |
420 | data_storage[key] = | 645 | data_storage[key] = |
421 | std::set<std::string>(list_value.begin(), list_value.end()); | 646 | std::set<std::string>(list_value.begin(), list_value.end()); |
647 | state_update.paintings = | ||
648 | std::vector<std::string>(list_value.begin(), list_value.end()); | ||
422 | } else { | 649 | } else { |
423 | data_storage[key] = list_value; | 650 | data_storage[key] = list_value; |
424 | } | 651 | } |
@@ -428,74 +655,39 @@ struct APState { | |||
428 | } | 655 | } |
429 | } | 656 | } |
430 | 657 | ||
431 | bool HasCheckedGameLocation(int location_id) { | 658 | // State mutex should NOT be locked. |
432 | return checked_locations.count(location_id); | 659 | // nullopt state_update indicates a reset. |
433 | } | 660 | void RefreshTracker(std::optional<StateUpdate> state_update) { |
434 | 661 | TrackerLog("Refreshing display..."); | |
435 | bool HasCheckedHuntPanel(int location_id) { | ||
436 | std::string key = | ||
437 | fmt::format("{}Hunt|{}", data_storage_prefix, location_id); | ||
438 | return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); | ||
439 | } | ||
440 | |||
441 | bool HasItem(int item_id, int quantity) { | ||
442 | return inventory.count(item_id) && inventory.at(item_id) >= quantity; | ||
443 | } | ||
444 | |||
445 | bool HasAchievement(const std::string& name) { | ||
446 | std::string key = | ||
447 | fmt::format("{}Achievement|{}", data_storage_prefix, name); | ||
448 | return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); | ||
449 | } | ||
450 | |||
451 | const std::set<std::string>& GetCheckedPaintings() { | ||
452 | std::string key = fmt::format("{}Paintings", data_storage_prefix); | ||
453 | if (!data_storage.count(key)) { | ||
454 | data_storage[key] = std::set<std::string>(); | ||
455 | } | ||
456 | 662 | ||
457 | return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); | 663 | if (!state_update || state_update->progression_items || |
458 | } | 664 | !state_update->paintings.empty()) { |
665 | std::string prev_msg; | ||
666 | { | ||
667 | std::lock_guard state_guard(state_mutex); | ||
459 | 668 | ||
460 | bool IsPaintingChecked(const std::string& painting_id) { | 669 | prev_msg = status_message; |
461 | const auto& checked_paintings = GetCheckedPaintings(); | 670 | SetStatusMessage(fmt::format("{} Recalculating...", status_message)); |
671 | } | ||
462 | 672 | ||
463 | return checked_paintings.count(painting_id) || | 673 | RecalculateReachability(); |
464 | (painting_mapping.count(painting_id) && | ||
465 | checked_paintings.count(painting_mapping.at(painting_id))); | ||
466 | } | ||
467 | 674 | ||
468 | void RefreshTracker(bool reset) { | 675 | { |
469 | TrackerLog("Refreshing display..."); | 676 | std::lock_guard state_guard(state_mutex); |
470 | 677 | ||
471 | RecalculateReachability(); | 678 | SetStatusMessage(prev_msg); |
679 | } | ||
680 | } | ||
681 | |||
472 | 682 | ||
473 | if (reset) { | 683 | if (!state_update) { |
474 | tracker_frame->ResetIndicators(); | 684 | tracker_frame->ResetIndicators(); |
475 | } else { | 685 | } else { |
476 | tracker_frame->UpdateIndicators(); | 686 | tracker_frame->UpdateIndicators(*state_update); |
477 | } | ||
478 | } | ||
479 | |||
480 | int64_t GetItemId(const std::string& item_name) { | ||
481 | int64_t ap_id = apclient->get_item_id(item_name); | ||
482 | if (ap_id == APClient::INVALID_NAME_ID) { | ||
483 | TrackerLog(fmt::format("Could not find AP item ID for {}", item_name)); | ||
484 | } | 687 | } |
485 | |||
486 | return ap_id; | ||
487 | } | ||
488 | |||
489 | std::string GetItemName(int id) { return apclient->get_item_name(id); } | ||
490 | |||
491 | bool HasReachedGoal() { | ||
492 | return data_storage.count(victory_data_storage_key) && | ||
493 | std::any_cast<int>(data_storage.at(victory_data_storage_key)) == | ||
494 | 30; // CLIENT_GOAL | ||
495 | } | 688 | } |
496 | 689 | ||
497 | void DestroyClient() { | 690 | void DestroyClient() { |
498 | client_active = false; | ||
499 | apclient->reset(); | 691 | apclient->reset(); |
500 | apclient.reset(); | 692 | apclient.reset(); |
501 | } | 693 | } |
@@ -514,39 +706,63 @@ void AP_Connect(std::string server, std::string player, std::string password) { | |||
514 | GetState().Connect(server, player, password); | 706 | GetState().Connect(server, player, password); |
515 | } | 707 | } |
516 | 708 | ||
517 | std::string AP_GetSaveName() { return GetState().save_name; } | 709 | std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); } |
518 | 710 | ||
519 | bool AP_HasCheckedGameLocation(int location_id) { | 711 | std::string AP_GetSaveName() { |
520 | return GetState().HasCheckedGameLocation(location_id); | 712 | std::lock_guard state_guard(GetState().state_mutex); |
713 | |||
714 | return GetState().save_name; | ||
521 | } | 715 | } |
522 | 716 | ||
523 | bool AP_HasCheckedHuntPanel(int location_id) { | 717 | bool AP_HasCheckedGameLocation(int location_id) { |
524 | return GetState().HasCheckedHuntPanel(location_id); | 718 | return GetState().HasCheckedGameLocation(location_id); |
525 | } | 719 | } |
526 | 720 | ||
527 | bool AP_HasItem(int item_id, int quantity) { | 721 | bool AP_HasItem(int item_id, int quantity) { |
528 | return GetState().HasItem(item_id, quantity); | 722 | return GetState().HasItem(item_id, quantity); |
529 | } | 723 | } |
530 | 724 | ||
531 | std::string AP_GetItemName(int item_id) { | 725 | bool AP_HasItemSafe(int item_id, int quantity) { |
532 | return GetState().GetItemName(item_id); | 726 | return GetState().HasItemSafe(item_id, quantity); |
727 | } | ||
728 | |||
729 | DoorShuffleMode AP_GetDoorShuffleMode() { | ||
730 | std::lock_guard state_guard(GetState().state_mutex); | ||
731 | |||
732 | return GetState().door_shuffle_mode; | ||
733 | } | ||
734 | |||
735 | bool AP_AreDoorsGrouped() { | ||
736 | std::lock_guard state_guard(GetState().state_mutex); | ||
737 | |||
738 | return GetState().group_doors; | ||
533 | } | 739 | } |
534 | 740 | ||
535 | DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } | 741 | bool AP_IsColorShuffle() { |
742 | std::lock_guard state_guard(GetState().state_mutex); | ||
536 | 743 | ||
537 | bool AP_IsColorShuffle() { return GetState().color_shuffle; } | 744 | return GetState().color_shuffle; |
745 | } | ||
538 | 746 | ||
539 | bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } | 747 | bool AP_IsPaintingShuffle() { |
748 | std::lock_guard state_guard(GetState().state_mutex); | ||
749 | |||
750 | return GetState().painting_shuffle; | ||
751 | } | ||
752 | |||
753 | std::map<std::string, std::string> AP_GetPaintingMapping() { | ||
754 | std::lock_guard state_guard(GetState().state_mutex); | ||
540 | 755 | ||
541 | const std::map<std::string, std::string>& AP_GetPaintingMapping() { | ||
542 | return GetState().painting_mapping; | 756 | return GetState().painting_mapping; |
543 | } | 757 | } |
544 | 758 | ||
545 | bool AP_IsPaintingMappedTo(const std::string& painting_id) { | 759 | bool AP_IsPaintingMappedTo(const std::string& painting_id) { |
760 | std::lock_guard state_guard(GetState().state_mutex); | ||
761 | |||
546 | return GetState().painting_codomain.count(painting_id); | 762 | return GetState().painting_codomain.count(painting_id); |
547 | } | 763 | } |
548 | 764 | ||
549 | const std::set<std::string>& AP_GetCheckedPaintings() { | 765 | std::set<std::string> AP_GetCheckedPaintings() { |
550 | return GetState().GetCheckedPaintings(); | 766 | return GetState().GetCheckedPaintings(); |
551 | } | 767 | } |
552 | 768 | ||
@@ -554,11 +770,29 @@ bool AP_IsPaintingChecked(const std::string& painting_id) { | |||
554 | return GetState().IsPaintingChecked(painting_id); | 770 | return GetState().IsPaintingChecked(painting_id); |
555 | } | 771 | } |
556 | 772 | ||
557 | int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } | 773 | void AP_RevealPaintings() { GetState().RevealPaintings(); } |
774 | |||
775 | int AP_GetMasteryRequirement() { | ||
776 | std::lock_guard state_guard(GetState().state_mutex); | ||
777 | |||
778 | return GetState().mastery_requirement; | ||
779 | } | ||
558 | 780 | ||
559 | int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } | 781 | int AP_GetLevel2Requirement() { |
782 | std::lock_guard state_guard(GetState().state_mutex); | ||
783 | |||
784 | return GetState().level_2_requirement; | ||
785 | } | ||
786 | |||
787 | LocationChecks AP_GetLocationsChecks() { | ||
788 | std::lock_guard state_guard(GetState().state_mutex); | ||
789 | |||
790 | return GetState().location_checks; | ||
791 | } | ||
560 | 792 | ||
561 | bool AP_IsLocationVisible(int classification) { | 793 | bool AP_IsLocationVisible(int classification) { |
794 | std::lock_guard state_guard(GetState().state_mutex); | ||
795 | |||
562 | int world_state = 0; | 796 | int world_state = 0; |
563 | 797 | ||
564 | switch (GetState().location_checks) { | 798 | switch (GetState().location_checks) { |
@@ -575,43 +809,76 @@ bool AP_IsLocationVisible(int classification) { | |||
575 | return false; | 809 | return false; |
576 | } | 810 | } |
577 | 811 | ||
578 | if (GetState().door_shuffle_mode && !GetState().early_color_hallways) { | 812 | if (GetState().door_shuffle_mode == kDOORS_MODE && |
813 | !GetState().early_color_hallways) { | ||
579 | world_state |= kLOCATION_SMALL_SPHERE_ONE; | 814 | world_state |= kLOCATION_SMALL_SPHERE_ONE; |
580 | } | 815 | } |
581 | 816 | ||
582 | return (world_state & classification); | 817 | return (world_state & classification); |
583 | } | 818 | } |
584 | 819 | ||
820 | PanelShuffleMode AP_GetPanelShuffleMode() { | ||
821 | std::lock_guard state_guard(GetState().state_mutex); | ||
822 | |||
823 | return GetState().panel_shuffle_mode; | ||
824 | } | ||
825 | |||
585 | VictoryCondition AP_GetVictoryCondition() { | 826 | VictoryCondition AP_GetVictoryCondition() { |
827 | std::lock_guard state_guard(GetState().state_mutex); | ||
828 | |||
586 | return GetState().victory_condition; | 829 | return GetState().victory_condition; |
587 | } | 830 | } |
588 | 831 | ||
589 | bool AP_HasAchievement(const std::string& achievement_name) { | 832 | bool AP_HasEarlyColorHallways() { |
590 | return GetState().HasAchievement(achievement_name); | 833 | std::lock_guard state_guard(GetState().state_mutex); |
834 | |||
835 | return GetState().early_color_hallways; | ||
591 | } | 836 | } |
592 | 837 | ||
593 | bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } | 838 | bool AP_IsPilgrimageEnabled() { |
839 | std::lock_guard state_guard(GetState().state_mutex); | ||
594 | 840 | ||
595 | bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } | 841 | return GetState().pilgrimage_enabled; |
842 | } | ||
596 | 843 | ||
597 | bool AP_DoesPilgrimageAllowRoofAccess() { | 844 | bool AP_DoesPilgrimageAllowRoofAccess() { |
845 | std::lock_guard state_guard(GetState().state_mutex); | ||
846 | |||
598 | return GetState().pilgrimage_allows_roof_access; | 847 | return GetState().pilgrimage_allows_roof_access; |
599 | } | 848 | } |
600 | 849 | ||
601 | bool AP_DoesPilgrimageAllowPaintings() { | 850 | bool AP_DoesPilgrimageAllowPaintings() { |
851 | std::lock_guard state_guard(GetState().state_mutex); | ||
852 | |||
602 | return GetState().pilgrimage_allows_paintings; | 853 | return GetState().pilgrimage_allows_paintings; |
603 | } | 854 | } |
604 | 855 | ||
605 | SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } | 856 | SunwarpAccess AP_GetSunwarpAccess() { |
857 | std::lock_guard state_guard(GetState().state_mutex); | ||
858 | |||
859 | return GetState().sunwarp_access; | ||
860 | } | ||
606 | 861 | ||
607 | bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } | 862 | bool AP_IsSunwarpShuffle() { |
863 | std::lock_guard state_guard(GetState().state_mutex); | ||
864 | |||
865 | return GetState().sunwarp_shuffle; | ||
866 | } | ||
608 | 867 | ||
609 | const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { | 868 | std::map<int, SunwarpMapping> AP_GetSunwarpMapping() { |
610 | return GetState().sunwarp_mapping; | 869 | return GetState().sunwarp_mapping; |
611 | } | 870 | } |
612 | 871 | ||
872 | bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; } | ||
873 | |||
613 | bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } | 874 | bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } |
614 | 875 | ||
615 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { | 876 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { |
877 | std::lock_guard state_guard(GetState().state_mutex); | ||
878 | |||
616 | return GetState().player_pos; | 879 | return GetState().player_pos; |
617 | } | 880 | } |
881 | |||
882 | bool AP_IsPanelSolved(int solve_index) { | ||
883 | return GetState().IsPanelSolved(solve_index); | ||
884 | } | ||
diff --git a/src/ap_state.h b/src/ap_state.h index f8936e5..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h | |||
@@ -11,7 +11,7 @@ | |||
11 | 11 | ||
12 | class TrackerFrame; | 12 | class TrackerFrame; |
13 | 13 | ||
14 | enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 }; | 14 | enum DoorShuffleMode { kNO_DOORS = 0, kPANELS_MODE = 1, kDOORS_MODE = 2 }; |
15 | 15 | ||
16 | enum VictoryCondition { | 16 | enum VictoryCondition { |
17 | kTHE_END = 0, | 17 | kTHE_END = 0, |
@@ -26,6 +26,8 @@ enum LocationChecks { | |||
26 | kPANELSANITY = 2 | 26 | kPANELSANITY = 2 |
27 | }; | 27 | }; |
28 | 28 | ||
29 | enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 }; | ||
30 | |||
29 | enum SunwarpAccess { | 31 | enum SunwarpAccess { |
30 | kSUNWARP_ACCESS_NORMAL = 0, | 32 | kSUNWARP_ACCESS_NORMAL = 0, |
31 | kSUNWARP_ACCESS_DISABLED = 1, | 33 | kSUNWARP_ACCESS_DISABLED = 1, |
@@ -39,43 +41,57 @@ struct SunwarpMapping { | |||
39 | int exit_index; | 41 | int exit_index; |
40 | }; | 42 | }; |
41 | 43 | ||
44 | struct ItemState { | ||
45 | std::string name; | ||
46 | int amount = 0; | ||
47 | int index = 0; | ||
48 | }; | ||
49 | |||
42 | void AP_SetTrackerFrame(TrackerFrame* tracker_frame); | 50 | void AP_SetTrackerFrame(TrackerFrame* tracker_frame); |
43 | 51 | ||
44 | void AP_Connect(std::string server, std::string player, std::string password); | 52 | void AP_Connect(std::string server, std::string player, std::string password); |
45 | 53 | ||
54 | std::string AP_GetStatusMessage(); | ||
55 | |||
46 | std::string AP_GetSaveName(); | 56 | std::string AP_GetSaveName(); |
47 | 57 | ||
48 | bool AP_HasCheckedGameLocation(int location_id); | 58 | bool AP_HasCheckedGameLocation(int location_id); |
49 | 59 | ||
50 | bool AP_HasCheckedHuntPanel(int location_id); | 60 | // This doesn't lock the state mutex, for speed, so it must ONLY be called from |
51 | 61 | // RecalculateReachability, which is only called from the APState thread anyway. | |
52 | bool AP_HasItem(int item_id, int quantity = 1); | 62 | bool AP_HasItem(int item_id, int quantity = 1); |
53 | 63 | ||
54 | std::string AP_GetItemName(int item_id); | 64 | bool AP_HasItemSafe(int item_id, int quantity = 1); |
55 | 65 | ||
56 | DoorShuffleMode AP_GetDoorShuffleMode(); | 66 | DoorShuffleMode AP_GetDoorShuffleMode(); |
57 | 67 | ||
68 | bool AP_AreDoorsGrouped(); | ||
69 | |||
58 | bool AP_IsColorShuffle(); | 70 | bool AP_IsColorShuffle(); |
59 | 71 | ||
60 | bool AP_IsPaintingShuffle(); | 72 | bool AP_IsPaintingShuffle(); |
61 | 73 | ||
62 | const std::map<std::string, std::string>& AP_GetPaintingMapping(); | 74 | std::map<std::string, std::string> AP_GetPaintingMapping(); |
63 | 75 | ||
64 | bool AP_IsPaintingMappedTo(const std::string& painting_id); | 76 | bool AP_IsPaintingMappedTo(const std::string& painting_id); |
65 | 77 | ||
66 | const std::set<std::string>& AP_GetCheckedPaintings(); | 78 | std::set<std::string> AP_GetCheckedPaintings(); |
67 | 79 | ||
68 | bool AP_IsPaintingChecked(const std::string& painting_id); | 80 | bool AP_IsPaintingChecked(const std::string& painting_id); |
69 | 81 | ||
82 | void AP_RevealPaintings(); | ||
83 | |||
70 | int AP_GetMasteryRequirement(); | 84 | int AP_GetMasteryRequirement(); |
71 | 85 | ||
72 | int AP_GetLevel2Requirement(); | 86 | int AP_GetLevel2Requirement(); |
73 | 87 | ||
88 | LocationChecks AP_GetLocationsChecks(); | ||
89 | |||
74 | bool AP_IsLocationVisible(int classification); | 90 | bool AP_IsLocationVisible(int classification); |
75 | 91 | ||
76 | VictoryCondition AP_GetVictoryCondition(); | 92 | PanelShuffleMode AP_GetPanelShuffleMode(); |
77 | 93 | ||
78 | bool AP_HasAchievement(const std::string& achievement_name); | 94 | VictoryCondition AP_GetVictoryCondition(); |
79 | 95 | ||
80 | bool AP_HasEarlyColorHallways(); | 96 | bool AP_HasEarlyColorHallways(); |
81 | 97 | ||
@@ -89,10 +105,14 @@ SunwarpAccess AP_GetSunwarpAccess(); | |||
89 | 105 | ||
90 | bool AP_IsSunwarpShuffle(); | 106 | bool AP_IsSunwarpShuffle(); |
91 | 107 | ||
92 | const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); | 108 | std::map<int, SunwarpMapping> AP_GetSunwarpMapping(); |
109 | |||
110 | bool AP_IsPostgameShuffle(); | ||
93 | 111 | ||
94 | bool AP_HasReachedGoal(); | 112 | bool AP_HasReachedGoal(); |
95 | 113 | ||
96 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); | 114 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); |
97 | 115 | ||
116 | bool AP_IsPanelSolved(int solve_index); | ||
117 | |||
98 | #endif /* end of include guard: AP_STATE_H_664A4180 */ | 118 | #endif /* end of include guard: AP_STATE_H_664A4180 */ |
diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 8d6487e..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp | |||
@@ -7,6 +7,7 @@ | |||
7 | #include "ap_state.h" | 7 | #include "ap_state.h" |
8 | #include "game_data.h" | 8 | #include "game_data.h" |
9 | #include "global.h" | 9 | #include "global.h" |
10 | #include "icons.h" | ||
10 | #include "tracker_config.h" | 11 | #include "tracker_config.h" |
11 | #include "tracker_panel.h" | 12 | #include "tracker_panel.h" |
12 | #include "tracker_state.h" | 13 | #include "tracker_state.h" |
@@ -15,60 +16,54 @@ AreaPopup::AreaPopup(wxWindow* parent, int area_id) | |||
15 | : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { | 16 | : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { |
16 | SetBackgroundStyle(wxBG_STYLE_PAINT); | 17 | SetBackgroundStyle(wxBG_STYLE_PAINT); |
17 | 18 | ||
18 | unchecked_eye_ = | 19 | LoadIcons(); |
19 | wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), | ||
20 | wxBITMAP_TYPE_PNG) | ||
21 | .Scale(32, 32)); | ||
22 | checked_eye_ = wxBitmap( | ||
23 | wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG) | ||
24 | .Scale(32, 32)); | ||
25 | 20 | ||
21 | // TODO: This is slow on high-DPI screens. | ||
26 | SetScrollRate(5, 5); | 22 | SetScrollRate(5, 5); |
27 | 23 | ||
28 | SetBackgroundColour(*wxBLACK); | 24 | SetBackgroundColour(*wxBLACK); |
29 | Hide(); | 25 | Hide(); |
30 | 26 | ||
31 | Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); | 27 | Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); |
28 | Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this); | ||
32 | 29 | ||
33 | UpdateIndicators(); | 30 | ResetIndicators(); |
34 | } | 31 | } |
35 | 32 | ||
36 | void AreaPopup::UpdateIndicators() { | 33 | void AreaPopup::ResetIndicators() { |
34 | indicators_.clear(); | ||
35 | |||
37 | const MapArea& map_area = GD_GetMapArea(area_id_); | 36 | const MapArea& map_area = GD_GetMapArea(area_id_); |
37 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
38 | TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent()); | ||
38 | 39 | ||
39 | // Start calculating extents. | 40 | // Start calculating extents. |
40 | wxMemoryDC mem_dc; | 41 | wxMemoryDC mem_dc; |
41 | mem_dc.SetFont(GetFont().Bold()); | 42 | mem_dc.SetFont(the_font.Bold()); |
42 | wxSize header_extent = mem_dc.GetTextExtent(map_area.name); | 43 | header_extent_ = mem_dc.GetTextExtent(map_area.name); |
43 | 44 | ||
44 | int acc_height = header_extent.GetHeight() + 20; | 45 | int acc_height = header_extent_.GetHeight() + FromDIP(20); |
45 | int col_width = 0; | 46 | int col_width = 0; |
46 | 47 | ||
47 | mem_dc.SetFont(GetFont()); | 48 | mem_dc.SetFont(the_font); |
48 | |||
49 | TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent()); | ||
50 | |||
51 | std::vector<int> real_locations; | ||
52 | 49 | ||
53 | for (int section_id = 0; section_id < map_area.locations.size(); | 50 | for (int section_id = 0; section_id < map_area.locations.size(); |
54 | section_id++) { | 51 | section_id++) { |
55 | const Location& location = map_area.locations.at(section_id); | 52 | const Location& location = map_area.locations.at(section_id); |
56 | 53 | if ((!AP_IsLocationVisible(location.classification) || | |
57 | if (tracker_panel->IsPanelsMode()) { | 54 | IsLocationPostgame(location.ap_location_id)) && |
58 | if (!location.single_panel) { | 55 | !(location.hunt && |
59 | continue; | 56 | GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) && |
60 | } | 57 | !(location.single_panel && |
61 | } else { | 58 | GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) { |
62 | if (!AP_IsLocationVisible(location.classification) && | 59 | continue; |
63 | !(location.hunt && GetTrackerConfig().show_hunt_panels)) { | ||
64 | continue; | ||
65 | } | ||
66 | } | 60 | } |
67 | 61 | ||
68 | real_locations.push_back(section_id); | 62 | indicators_.emplace_back(section_id, kLOCATION, acc_height); |
69 | 63 | ||
70 | wxSize item_extent = mem_dc.GetTextExtent(location.name); | 64 | wxSize item_extent = mem_dc.GetTextExtent(location.name); |
71 | int item_height = std::max(32, item_extent.GetHeight()) + 10; | 65 | int item_height = |
66 | std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10); | ||
72 | acc_height += item_height; | 67 | acc_height += item_height; |
73 | 68 | ||
74 | if (item_extent.GetWidth() > col_width) { | 69 | if (item_extent.GetWidth() > col_width) { |
@@ -76,11 +71,18 @@ void AreaPopup::UpdateIndicators() { | |||
76 | } | 71 | } |
77 | } | 72 | } |
78 | 73 | ||
79 | if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { | 74 | if (AP_IsPaintingShuffle()) { |
80 | for (int painting_id : map_area.paintings) { | 75 | for (int painting_id : map_area.paintings) { |
76 | if (IsPaintingPostgame(painting_id)) { | ||
77 | continue; | ||
78 | } | ||
79 | |||
80 | indicators_.emplace_back(painting_id, kPAINTING, acc_height); | ||
81 | |||
81 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); | 82 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); |
82 | wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name. | 83 | wxSize item_extent = mem_dc.GetTextExtent(painting.display_name); |
83 | int item_height = std::max(32, item_extent.GetHeight()) + 10; | 84 | int item_height = |
85 | std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10); | ||
84 | acc_height += item_height; | 86 | acc_height += item_height; |
85 | 87 | ||
86 | if (item_extent.GetWidth() > col_width) { | 88 | if (item_extent.GetWidth() > col_width) { |
@@ -89,80 +91,86 @@ void AreaPopup::UpdateIndicators() { | |||
89 | } | 91 | } |
90 | } | 92 | } |
91 | 93 | ||
92 | int item_width = col_width + 10 + 32; | 94 | int item_width = col_width + FromDIP(10 + 32); |
93 | int full_width = std::max(header_extent.GetWidth(), item_width) + 20; | 95 | full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20); |
96 | full_height_ = acc_height; | ||
94 | 97 | ||
95 | Fit(); | 98 | Fit(); |
96 | SetVirtualSize(full_width, acc_height); | 99 | SetVirtualSize(full_width_, full_height_); |
100 | |||
101 | UpdateIndicators(); | ||
102 | } | ||
103 | |||
104 | void AreaPopup::UpdateIndicators() { | ||
105 | const MapArea& map_area = GD_GetMapArea(area_id_); | ||
106 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
107 | TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent()); | ||
108 | |||
109 | rendered_ = wxBitmap(full_width_, full_height_); | ||
97 | 110 | ||
98 | rendered_ = wxBitmap(full_width, acc_height); | 111 | wxMemoryDC mem_dc; |
99 | mem_dc.SelectObject(rendered_); | 112 | mem_dc.SelectObject(rendered_); |
100 | mem_dc.SetPen(*wxTRANSPARENT_PEN); | 113 | mem_dc.SetPen(*wxTRANSPARENT_PEN); |
101 | mem_dc.SetBrush(*wxBLACK_BRUSH); | 114 | mem_dc.SetBrush(*wxBLACK_BRUSH); |
102 | mem_dc.DrawRectangle({0, 0}, {full_width, acc_height}); | 115 | mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_}); |
103 | 116 | ||
104 | mem_dc.SetFont(GetFont().Bold()); | 117 | mem_dc.SetFont(the_font.Bold()); |
105 | mem_dc.SetTextForeground(*wxWHITE); | 118 | mem_dc.SetTextForeground(*wxWHITE); |
106 | mem_dc.DrawText(map_area.name, | 119 | mem_dc.DrawText(map_area.name, |
107 | {(full_width - header_extent.GetWidth()) / 2, 10}); | 120 | {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)}); |
108 | 121 | ||
109 | int cur_height = header_extent.GetHeight() + 20; | 122 | mem_dc.SetFont(the_font); |
110 | 123 | ||
111 | mem_dc.SetFont(GetFont()); | 124 | for (const IndicatorInfo& indicator : indicators_) { |
125 | switch (indicator.type) { | ||
126 | case kLOCATION: { | ||
127 | const Location& location = map_area.locations.at(indicator.id); | ||
112 | 128 | ||
113 | for (int section_id : real_locations) { | 129 | bool checked = false; |
114 | const Location& location = map_area.locations.at(section_id); | 130 | if (IsLocationWinCondition(location)) { |
131 | checked = AP_HasReachedGoal(); | ||
132 | } else { | ||
133 | checked = AP_HasCheckedGameLocation(location.ap_location_id) || | ||
134 | (location.single_panel && | ||
135 | AP_IsPanelSolved( | ||
136 | GD_GetPanel(*location.single_panel).solve_index)); | ||
137 | } | ||
115 | 138 | ||
116 | bool checked = false; | 139 | const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_; |
117 | if (IsLocationWinCondition(location)) { | ||
118 | checked = AP_HasReachedGoal(); | ||
119 | } else if (tracker_panel->IsPanelsMode()) { | ||
120 | const Panel& panel = GD_GetPanel(*location.single_panel); | ||
121 | if (panel.non_counting) { | ||
122 | checked = AP_HasCheckedGameLocation(location.ap_location_id); | ||
123 | } else { | ||
124 | checked = tracker_panel->GetSolvedPanels().contains(panel.nodepath); | ||
125 | } | ||
126 | } else { | ||
127 | checked = | ||
128 | AP_HasCheckedGameLocation(location.ap_location_id) || | ||
129 | (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)); | ||
130 | } | ||
131 | |||
132 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; | ||
133 | 140 | ||
134 | mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); | 141 | mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y}); |
135 | 142 | ||
136 | bool reachable = IsLocationReachable(location.ap_location_id); | 143 | bool reachable = IsLocationReachable(location.ap_location_id); |
137 | const wxColour* text_color = reachable ? wxWHITE : wxRED; | 144 | const wxColour* text_color = reachable ? wxWHITE : wxRED; |
138 | mem_dc.SetTextForeground(*text_color); | 145 | mem_dc.SetTextForeground(*text_color); |
139 | 146 | ||
140 | wxSize item_extent = mem_dc.GetTextExtent(location.name); | 147 | wxSize item_extent = mem_dc.GetTextExtent(location.name); |
141 | mem_dc.DrawText( | 148 | mem_dc.DrawText( |
142 | location.name, | 149 | location.name, |
143 | {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); | 150 | {FromDIP(10 + 32 + 10), |
144 | 151 | indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2}); | |
145 | cur_height += 10 + 32; | ||
146 | } | ||
147 | 152 | ||
148 | if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { | 153 | break; |
149 | for (int painting_id : map_area.paintings) { | 154 | } |
150 | const PaintingExit& painting = GD_GetPaintingExit(painting_id); | 155 | case kPAINTING: { |
156 | const PaintingExit& painting = GD_GetPaintingExit(indicator.id); | ||
151 | 157 | ||
152 | bool reachable = IsPaintingReachable(painting_id); | 158 | bool reachable = IsPaintingReachable(indicator.id); |
153 | const wxColour* text_color = reachable ? wxWHITE : wxRED; | 159 | const wxColour* text_color = reachable ? wxWHITE : wxRED; |
154 | mem_dc.SetTextForeground(*text_color); | 160 | mem_dc.SetTextForeground(*text_color); |
155 | 161 | ||
156 | bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); | 162 | bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); |
157 | wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; | 163 | const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_; |
158 | mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); | 164 | mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y}); |
159 | 165 | ||
160 | wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name. | 166 | wxSize item_extent = mem_dc.GetTextExtent(painting.display_name); |
161 | mem_dc.DrawText(painting.internal_id, | 167 | mem_dc.DrawText( |
162 | {10 + 32 + 10, | 168 | painting.display_name, |
163 | cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); | 169 | {FromDIP(10 + 32 + 10), |
170 | indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2}); | ||
164 | 171 | ||
165 | cur_height += 10 + 32; | 172 | break; |
173 | } | ||
166 | } | 174 | } |
167 | } | 175 | } |
168 | } | 176 | } |
@@ -174,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) { | |||
174 | 182 | ||
175 | event.Skip(); | 183 | event.Skip(); |
176 | } | 184 | } |
185 | |||
186 | void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) { | ||
187 | LoadIcons(); | ||
188 | ResetIndicators(); | ||
189 | |||
190 | event.Skip(); | ||
191 | } | ||
192 | |||
193 | void AreaPopup::LoadIcons() { | ||
194 | unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png", | ||
195 | FromDIP(wxSize{32, 32})); | ||
196 | checked_eye_ = | ||
197 | GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32})); | ||
198 | unchecked_owl_ = | ||
199 | GetTheIconCache().GetIcon("assets/owl.png", FromDIP(wxSize{32, 32})); | ||
200 | checked_owl_ = GetTheIconCache().GetIcon("assets/checked_owl.png", | ||
201 | FromDIP(wxSize{32, 32})); | ||
202 | } | ||
diff --git a/src/area_popup.h b/src/area_popup.h index 00c644d..f8a2355 100644 --- a/src/area_popup.h +++ b/src/area_popup.h | |||
@@ -7,19 +7,53 @@ | |||
7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
8 | #endif | 8 | #endif |
9 | 9 | ||
10 | #include <vector> | ||
11 | |||
10 | class AreaPopup : public wxScrolledCanvas { | 12 | class AreaPopup : public wxScrolledCanvas { |
11 | public: | 13 | public: |
12 | AreaPopup(wxWindow* parent, int area_id); | 14 | AreaPopup(wxWindow* parent, int area_id); |
13 | 15 | ||
16 | void ResetIndicators(); | ||
14 | void UpdateIndicators(); | 17 | void UpdateIndicators(); |
15 | 18 | ||
19 | int GetFullWidth() const { return full_width_; } | ||
20 | int GetFullHeight() const { return full_height_; } | ||
21 | |||
16 | private: | 22 | private: |
23 | enum IndicatorType { | ||
24 | kLOCATION, | ||
25 | kPAINTING, | ||
26 | }; | ||
27 | |||
28 | struct IndicatorInfo { | ||
29 | // For locations, the id is an index into the map area's locations list. | ||
30 | // For paintings, it is a real painting id. | ||
31 | int id; | ||
32 | IndicatorType type; | ||
33 | int y; | ||
34 | |||
35 | IndicatorInfo(int id, IndicatorType type, int y) | ||
36 | : id(id), type(type), y(y) {} | ||
37 | }; | ||
38 | |||
17 | void OnPaint(wxPaintEvent& event); | 39 | void OnPaint(wxPaintEvent& event); |
40 | void OnDPIChanged(wxDPIChangedEvent& event); | ||
41 | |||
42 | void LoadIcons(); | ||
18 | 43 | ||
19 | int area_id_; | 44 | int area_id_; |
20 | 45 | ||
21 | wxBitmap unchecked_eye_; | 46 | const wxBitmap* unchecked_eye_; |
22 | wxBitmap checked_eye_; | 47 | const wxBitmap* checked_eye_; |
48 | const wxBitmap* unchecked_owl_; | ||
49 | const wxBitmap* checked_owl_; | ||
50 | |||
51 | int full_width_ = 0; | ||
52 | int full_height_ = 0; | ||
53 | wxSize header_extent_; | ||
54 | |||
55 | std::vector<IndicatorInfo> indicators_; | ||
56 | |||
23 | wxBitmap rendered_; | 57 | wxBitmap rendered_; |
24 | }; | 58 | }; |
25 | 59 | ||
diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp index 64fee98..b55a138 100644 --- a/src/connection_dialog.cpp +++ b/src/connection_dialog.cpp | |||
@@ -4,17 +4,21 @@ | |||
4 | 4 | ||
5 | ConnectionDialog::ConnectionDialog() | 5 | ConnectionDialog::ConnectionDialog() |
6 | : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { | 6 | : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { |
7 | server_box_ = | 7 | server_box_ = new wxTextCtrl( |
8 | new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_server, | 8 | this, -1, |
9 | wxDefaultPosition, {300, -1}); | 9 | wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server), |
10 | player_box_ = | 10 | wxDefaultPosition, FromDIP(wxSize{300, -1})); |
11 | new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_player, | 11 | player_box_ = new wxTextCtrl( |
12 | wxDefaultPosition, {300, -1}); | 12 | this, -1, |
13 | wxString::FromUTF8(GetTrackerConfig().connection_details.ap_player), | ||
14 | wxDefaultPosition, FromDIP(wxSize{300, -1})); | ||
13 | password_box_ = new wxTextCtrl( | 15 | password_box_ = new wxTextCtrl( |
14 | this, -1, GetTrackerConfig().connection_details.ap_password, | 16 | this, -1, |
15 | wxDefaultPosition, {300, -1}); | 17 | wxString::FromUTF8(GetTrackerConfig().connection_details.ap_password), |
18 | wxDefaultPosition, FromDIP(wxSize{300, -1})); | ||
16 | 19 | ||
17 | wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10); | 20 | wxFlexGridSizer* form_sizer = |
21 | new wxFlexGridSizer(2, FromDIP(10), FromDIP(10)); | ||
18 | 22 | ||
19 | form_sizer->Add( | 23 | form_sizer->Add( |
20 | new wxStaticText(this, -1, "Server:"), | 24 | new wxStaticText(this, -1, "Server:"), |
@@ -30,17 +34,19 @@ ConnectionDialog::ConnectionDialog() | |||
30 | form_sizer->Add(password_box_, wxSizerFlags().Expand()); | 34 | form_sizer->Add(password_box_, wxSizerFlags().Expand()); |
31 | 35 | ||
32 | history_list_ = new wxListBox(this, -1); | 36 | history_list_ = new wxListBox(this, -1); |
33 | for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { | 37 | for (const ConnectionDetails& details : |
38 | GetTrackerConfig().connection_history) { | ||
34 | wxString display_text; | 39 | wxString display_text; |
35 | display_text << details.ap_player; | 40 | display_text << wxString::FromUTF8(details.ap_player); |
36 | display_text << " ("; | 41 | display_text << " ("; |
37 | display_text << details.ap_server; | 42 | display_text << wxString::FromUTF8(details.ap_server); |
38 | display_text << ")"; | 43 | display_text << ")"; |
39 | 44 | ||
40 | history_list_->Append(display_text); | 45 | history_list_->Append(display_text); |
41 | } | 46 | } |
42 | 47 | ||
43 | history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, this); | 48 | history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, |
49 | this); | ||
44 | 50 | ||
45 | wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); | 51 | wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); |
46 | mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); | 52 | mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); |
@@ -52,7 +58,8 @@ ConnectionDialog::ConnectionDialog() | |||
52 | this, -1, "Enter the details to connect to Archipelago."), | 58 | this, -1, "Enter the details to connect to Archipelago."), |
53 | wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); | 59 | wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); |
54 | top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); | 60 | top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); |
55 | top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Border().Center()); | 61 | top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), |
62 | wxSizerFlags().Border().Center()); | ||
56 | 63 | ||
57 | SetSizerAndFit(top_sizer); | 64 | SetSizerAndFit(top_sizer); |
58 | 65 | ||
@@ -62,9 +69,10 @@ ConnectionDialog::ConnectionDialog() | |||
62 | 69 | ||
63 | void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { | 70 | void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { |
64 | if (e.IsSelection()) { | 71 | if (e.IsSelection()) { |
65 | const ConnectionDetails& details = GetTrackerConfig().connection_history.at(e.GetSelection()); | 72 | const ConnectionDetails& details = |
66 | server_box_->SetValue(details.ap_server); | 73 | GetTrackerConfig().connection_history.at(e.GetSelection()); |
67 | player_box_->SetValue(details.ap_player); | 74 | server_box_->SetValue(wxString::FromUTF8(details.ap_server)); |
68 | password_box_->SetValue(details.ap_password); | 75 | player_box_->SetValue(wxString::FromUTF8(details.ap_player)); |
76 | password_box_->SetValue(wxString::FromUTF8(details.ap_password)); | ||
69 | } | 77 | } |
70 | } | 78 | } |
diff --git a/src/connection_dialog.h b/src/connection_dialog.h index 9fe62fd..ec2ee72 100644 --- a/src/connection_dialog.h +++ b/src/connection_dialog.h | |||
@@ -14,12 +14,12 @@ class ConnectionDialog : public wxDialog { | |||
14 | public: | 14 | public: |
15 | ConnectionDialog(); | 15 | ConnectionDialog(); |
16 | 16 | ||
17 | std::string GetServerValue() { return server_box_->GetValue().ToStdString(); } | 17 | std::string GetServerValue() { return server_box_->GetValue().utf8_string(); } |
18 | 18 | ||
19 | std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); } | 19 | std::string GetPlayerValue() { return player_box_->GetValue().utf8_string(); } |
20 | 20 | ||
21 | std::string GetPasswordValue() { | 21 | std::string GetPasswordValue() { |
22 | return password_box_->GetValue().ToStdString(); | 22 | return password_box_->GetValue().utf8_string(); |
23 | } | 23 | } |
24 | 24 | ||
25 | private: | 25 | private: |
diff --git a/src/game_data.cpp b/src/game_data.cpp index c39e239..94b9888 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp | |||
@@ -12,36 +12,11 @@ | |||
12 | 12 | ||
13 | namespace { | 13 | namespace { |
14 | 14 | ||
15 | LingoColor GetColorForString(const std::string &str) { | ||
16 | if (str == "black") { | ||
17 | return LingoColor::kBlack; | ||
18 | } else if (str == "red") { | ||
19 | return LingoColor::kRed; | ||
20 | } else if (str == "blue") { | ||
21 | return LingoColor::kBlue; | ||
22 | } else if (str == "yellow") { | ||
23 | return LingoColor::kYellow; | ||
24 | } else if (str == "orange") { | ||
25 | return LingoColor::kOrange; | ||
26 | } else if (str == "green") { | ||
27 | return LingoColor::kGreen; | ||
28 | } else if (str == "gray") { | ||
29 | return LingoColor::kGray; | ||
30 | } else if (str == "brown") { | ||
31 | return LingoColor::kBrown; | ||
32 | } else if (str == "purple") { | ||
33 | return LingoColor::kPurple; | ||
34 | } else { | ||
35 | TrackerLog(fmt::format("Invalid color: {}", str)); | ||
36 | |||
37 | return LingoColor::kNone; | ||
38 | } | ||
39 | } | ||
40 | |||
41 | struct GameData { | 15 | struct GameData { |
42 | std::vector<Room> rooms_; | 16 | std::vector<Room> rooms_; |
43 | std::vector<Door> doors_; | 17 | std::vector<Door> doors_; |
44 | std::vector<Panel> panels_; | 18 | std::vector<Panel> panels_; |
19 | std::vector<PanelDoor> panel_doors_; | ||
45 | std::vector<MapArea> map_areas_; | 20 | std::vector<MapArea> map_areas_; |
46 | std::vector<SubwayItem> subway_items_; | 21 | std::vector<SubwayItem> subway_items_; |
47 | std::vector<PaintingExit> paintings_; | 22 | std::vector<PaintingExit> paintings_; |
@@ -49,6 +24,7 @@ struct GameData { | |||
49 | std::map<std::string, int> room_by_id_; | 24 | std::map<std::string, int> room_by_id_; |
50 | std::map<std::string, int> door_by_id_; | 25 | std::map<std::string, int> door_by_id_; |
51 | std::map<std::string, int> panel_by_id_; | 26 | std::map<std::string, int> panel_by_id_; |
27 | std::map<std::string, int> panel_doors_by_id_; | ||
52 | std::map<std::string, int> area_by_id_; | 28 | std::map<std::string, int> area_by_id_; |
53 | std::map<std::string, int> painting_by_id_; | 29 | std::map<std::string, int> painting_by_id_; |
54 | 30 | ||
@@ -56,6 +32,7 @@ struct GameData { | |||
56 | 32 | ||
57 | std::map<std::string, int> room_by_painting_; | 33 | std::map<std::string, int> room_by_painting_; |
58 | std::map<int, int> room_by_sunwarp_; | 34 | std::map<int, int> room_by_sunwarp_; |
35 | std::map<int, int> panel_by_solve_index_; | ||
59 | 36 | ||
60 | std::vector<int> achievement_panels_; | 37 | std::vector<int> achievement_panels_; |
61 | 38 | ||
@@ -66,6 +43,8 @@ struct GameData { | |||
66 | std::map<std::string, int> subway_item_by_painting_; | 43 | std::map<std::string, int> subway_item_by_painting_; |
67 | std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; | 44 | std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; |
68 | 45 | ||
46 | std::map<int, std::string> item_by_ap_id_; | ||
47 | |||
69 | bool loaded_area_data_ = false; | 48 | bool loaded_area_data_ = false; |
70 | std::set<std::string> malconfigured_areas_; | 49 | std::set<std::string> malconfigured_areas_; |
71 | 50 | ||
@@ -81,7 +60,7 @@ struct GameData { | |||
81 | ids_config["special_items"][color_name]) { | 60 | ids_config["special_items"][color_name]) { |
82 | std::string input_name = color_name; | 61 | std::string input_name = color_name; |
83 | input_name[0] = std::tolower(input_name[0]); | 62 | input_name[0] = std::tolower(input_name[0]); |
84 | ap_id_by_color_[GetColorForString(input_name)] = | 63 | ap_id_by_color_[GetLingoColorForString(input_name)] = |
85 | ids_config["special_items"][color_name].as<int>(); | 64 | ids_config["special_items"][color_name].as<int>(); |
86 | } else { | 65 | } else { |
87 | TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); | 66 | TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); |
@@ -98,8 +77,18 @@ struct GameData { | |||
98 | init_color_id("Brown"); | 77 | init_color_id("Brown"); |
99 | init_color_id("Gray"); | 78 | init_color_id("Gray"); |
100 | 79 | ||
80 | if (ids_config["special_items"]) { | ||
81 | for (const auto& special_item_it : ids_config["special_items"]) | ||
82 | { | ||
83 | item_by_ap_id_[special_item_it.second.as<int>()] = | ||
84 | special_item_it.first.as<std::string>(); | ||
85 | } | ||
86 | } | ||
87 | |||
101 | rooms_.reserve(lingo_config.size() * 2); | 88 | rooms_.reserve(lingo_config.size() * 2); |
102 | 89 | ||
90 | std::vector<int> panel_location_ids; | ||
91 | |||
103 | for (const auto &room_it : lingo_config) { | 92 | for (const auto &room_it : lingo_config) { |
104 | int room_id = AddOrGetRoom(room_it.first.as<std::string>()); | 93 | int room_id = AddOrGetRoom(room_it.first.as<std::string>()); |
105 | 94 | ||
@@ -177,12 +166,12 @@ struct GameData { | |||
177 | 166 | ||
178 | if (panel_it.second["colors"]) { | 167 | if (panel_it.second["colors"]) { |
179 | if (panel_it.second["colors"].IsScalar()) { | 168 | if (panel_it.second["colors"].IsScalar()) { |
180 | panels_[panel_id].colors.push_back(GetColorForString( | 169 | panels_[panel_id].colors.push_back(GetLingoColorForString( |
181 | panel_it.second["colors"].as<std::string>())); | 170 | panel_it.second["colors"].as<std::string>())); |
182 | } else { | 171 | } else { |
183 | for (const auto &color_node : panel_it.second["colors"]) { | 172 | for (const auto &color_node : panel_it.second["colors"]) { |
184 | panels_[panel_id].colors.push_back( | 173 | panels_[panel_id].colors.push_back( |
185 | GetColorForString(color_node.as<std::string>())); | 174 | GetLingoColorForString(color_node.as<std::string>())); |
186 | } | 175 | } |
187 | } | 176 | } |
188 | } | 177 | } |
@@ -288,10 +277,11 @@ struct GameData { | |||
288 | ids_config["panels"][rooms_[room_id].name] && | 277 | ids_config["panels"][rooms_[room_id].name] && |
289 | ids_config["panels"][rooms_[room_id].name] | 278 | ids_config["panels"][rooms_[room_id].name] |
290 | [panels_[panel_id].name]) { | 279 | [panels_[panel_id].name]) { |
291 | panels_[panel_id].ap_location_id = | 280 | int location_id = ids_config["panels"][rooms_[room_id].name] |
292 | ids_config["panels"][rooms_[room_id].name] | 281 | [panels_[panel_id].name] |
293 | [panels_[panel_id].name] | 282 | .as<int>(); |
294 | .as<int>(); | 283 | panels_[panel_id].ap_location_id = location_id; |
284 | panel_location_ids.push_back(location_id); | ||
295 | } else { | 285 | } else { |
296 | TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", | 286 | TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", |
297 | rooms_[room_id].name, | 287 | rooms_[room_id].name, |
@@ -357,6 +347,9 @@ struct GameData { | |||
357 | ids_config["doors"][rooms_[room_id].name] | 347 | ids_config["doors"][rooms_[room_id].name] |
358 | [doors_[door_id].name]["item"] | 348 | [doors_[door_id].name]["item"] |
359 | .as<int>(); | 349 | .as<int>(); |
350 | |||
351 | item_by_ap_id_[doors_[door_id].ap_item_id] = | ||
352 | doors_[door_id].item_name; | ||
360 | } else { | 353 | } else { |
361 | TrackerLog(fmt::format("Missing AP item ID for door {} - {}", | 354 | TrackerLog(fmt::format("Missing AP item ID for door {} - {}", |
362 | rooms_[room_id].name, | 355 | rooms_[room_id].name, |
@@ -373,6 +366,9 @@ struct GameData { | |||
373 | doors_[door_id].group_ap_item_id = | 366 | doors_[door_id].group_ap_item_id = |
374 | ids_config["door_groups"][doors_[door_id].group_name] | 367 | ids_config["door_groups"][doors_[door_id].group_name] |
375 | .as<int>(); | 368 | .as<int>(); |
369 | |||
370 | item_by_ap_id_[doors_[door_id].group_ap_item_id] = | ||
371 | doors_[door_id].group_name; | ||
376 | } else { | 372 | } else { |
377 | TrackerLog(fmt::format("Missing AP item ID for door group {}", | 373 | TrackerLog(fmt::format("Missing AP item ID for door group {}", |
378 | doors_[door_id].group_name)); | 374 | doors_[door_id].group_name)); |
@@ -430,6 +426,90 @@ struct GameData { | |||
430 | } | 426 | } |
431 | } | 427 | } |
432 | 428 | ||
429 | if (room_it.second["panel_doors"]) { | ||
430 | for (const auto &panel_door_it : room_it.second["panel_doors"]) { | ||
431 | std::string panel_door_name = panel_door_it.first.as<std::string>(); | ||
432 | int panel_door_id = | ||
433 | AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name); | ||
434 | |||
435 | std::map<std::string, std::vector<std::string>> panel_per_room; | ||
436 | int num_panels = 0; | ||
437 | for (const auto &panel_node : panel_door_it.second["panels"]) { | ||
438 | num_panels++; | ||
439 | |||
440 | int panel_id = -1; | ||
441 | |||
442 | if (panel_node.IsScalar()) { | ||
443 | panel_id = AddOrGetPanel(rooms_[room_id].name, | ||
444 | panel_node.as<std::string>()); | ||
445 | |||
446 | panel_per_room[rooms_[room_id].name].push_back( | ||
447 | panel_node.as<std::string>()); | ||
448 | } else { | ||
449 | panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(), | ||
450 | panel_node["panel"].as<std::string>()); | ||
451 | |||
452 | panel_per_room[panel_node["room"].as<std::string>()].push_back( | ||
453 | panel_node["panel"].as<std::string>()); | ||
454 | } | ||
455 | |||
456 | Panel &panel = panels_[panel_id]; | ||
457 | panel.panel_door = panel_door_id; | ||
458 | } | ||
459 | |||
460 | if (panel_door_it.second["item_name"]) { | ||
461 | panel_doors_[panel_door_id].item_name = | ||
462 | panel_door_it.second["item_name"].as<std::string>(); | ||
463 | } else { | ||
464 | std::vector<std::string> room_strs; | ||
465 | for (const auto &[room_str, panels_str] : panel_per_room) { | ||
466 | room_strs.push_back(fmt::format( | ||
467 | "{} - {}", room_str, hatkirby::implode(panels_str, ", "))); | ||
468 | } | ||
469 | |||
470 | if (num_panels == 1) { | ||
471 | panel_doors_[panel_door_id].item_name = | ||
472 | fmt::format("{} (Panel)", room_strs[0]); | ||
473 | } else { | ||
474 | panel_doors_[panel_door_id].item_name = fmt::format( | ||
475 | "{} (Panels)", hatkirby::implode(room_strs, " and ")); | ||
476 | } | ||
477 | } | ||
478 | |||
479 | if (ids_config["panel_doors"] && | ||
480 | ids_config["panel_doors"][rooms_[room_id].name] && | ||
481 | ids_config["panel_doors"][rooms_[room_id].name] | ||
482 | [panel_door_name]) { | ||
483 | panel_doors_[panel_door_id].ap_item_id = | ||
484 | ids_config["panel_doors"][rooms_[room_id].name][panel_door_name] | ||
485 | .as<int>(); | ||
486 | |||
487 | item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] = | ||
488 | panel_doors_[panel_door_id].item_name; | ||
489 | } else { | ||
490 | TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}", | ||
491 | rooms_[room_id].name, panel_door_name)); | ||
492 | } | ||
493 | |||
494 | if (panel_door_it.second["panel_group"]) { | ||
495 | std::string panel_group = | ||
496 | panel_door_it.second["panel_group"].as<std::string>(); | ||
497 | |||
498 | if (ids_config["panel_groups"] && | ||
499 | ids_config["panel_groups"][panel_group]) { | ||
500 | panel_doors_[panel_door_id].group_ap_item_id = | ||
501 | ids_config["panel_groups"][panel_group].as<int>(); | ||
502 | |||
503 | item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] = | ||
504 | panel_group; | ||
505 | } else { | ||
506 | TrackerLog(fmt::format( | ||
507 | "Missing AP item ID for panel door group {}", panel_group)); | ||
508 | } | ||
509 | } | ||
510 | } | ||
511 | } | ||
512 | |||
433 | if (room_it.second["paintings"]) { | 513 | if (room_it.second["paintings"]) { |
434 | for (const auto &painting : room_it.second["paintings"]) { | 514 | for (const auto &painting : room_it.second["paintings"]) { |
435 | std::string internal_id = painting["id"].as<std::string>(); | 515 | std::string internal_id = painting["id"].as<std::string>(); |
@@ -437,6 +517,13 @@ struct GameData { | |||
437 | PaintingExit &painting_exit = paintings_[painting_id]; | 517 | PaintingExit &painting_exit = paintings_[painting_id]; |
438 | painting_exit.room = room_id; | 518 | painting_exit.room = room_id; |
439 | 519 | ||
520 | if (painting["display_name"]) { | ||
521 | painting_exit.display_name = | ||
522 | painting["display_name"].as<std::string>(); | ||
523 | } else { | ||
524 | painting_exit.display_name = painting_exit.internal_id; | ||
525 | } | ||
526 | |||
440 | if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && | 527 | if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && |
441 | (!painting["disable"] || !painting["disable"].as<bool>())) { | 528 | (!painting["disable"] || !painting["disable"].as<bool>())) { |
442 | painting_exit.entrance = true; | 529 | painting_exit.entrance = true; |
@@ -478,33 +565,74 @@ struct GameData { | |||
478 | ids_config["progression"][progressive_item_name]) { | 565 | ids_config["progression"][progressive_item_name]) { |
479 | progressive_item_id = | 566 | progressive_item_id = |
480 | ids_config["progression"][progressive_item_name].as<int>(); | 567 | ids_config["progression"][progressive_item_name].as<int>(); |
568 | |||
569 | item_by_ap_id_[progressive_item_id] = progressive_item_name; | ||
481 | } else { | 570 | } else { |
482 | TrackerLog(fmt::format("Missing AP item ID for progressive item {}", | 571 | TrackerLog(fmt::format("Missing AP item ID for progressive item {}", |
483 | progressive_item_name)); | 572 | progressive_item_name)); |
484 | } | 573 | } |
485 | 574 | ||
486 | int index = 1; | 575 | if (progression_it.second["doors"]) { |
487 | for (const auto &stage : progression_it.second) { | 576 | int index = 1; |
488 | int door_id = -1; | 577 | for (const auto &stage : progression_it.second["doors"]) { |
578 | int door_id = -1; | ||
579 | |||
580 | if (stage.IsScalar()) { | ||
581 | door_id = | ||
582 | AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); | ||
583 | } else { | ||
584 | door_id = AddOrGetDoor(stage["room"].as<std::string>(), | ||
585 | stage["door"].as<std::string>()); | ||
586 | } | ||
489 | 587 | ||
490 | if (stage.IsScalar()) { | 588 | doors_[door_id].progressives.push_back( |
491 | door_id = | 589 | {.item_name = progressive_item_name, |
492 | AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); | 590 | .ap_item_id = progressive_item_id, |
493 | } else { | 591 | .quantity = index}); |
494 | door_id = AddOrGetDoor(stage["room"].as<std::string>(), | 592 | index++; |
495 | stage["door"].as<std::string>()); | ||
496 | } | 593 | } |
594 | } | ||
595 | |||
596 | if (progression_it.second["panel_doors"]) { | ||
597 | int index = 1; | ||
598 | for (const auto &stage : progression_it.second["panel_doors"]) { | ||
599 | int panel_door_id = -1; | ||
600 | |||
601 | if (stage.IsScalar()) { | ||
602 | panel_door_id = AddOrGetPanelDoor(rooms_[room_id].name, | ||
603 | stage.as<std::string>()); | ||
604 | } else { | ||
605 | panel_door_id = | ||
606 | AddOrGetPanelDoor(stage["room"].as<std::string>(), | ||
607 | stage["panel_door"].as<std::string>()); | ||
608 | } | ||
497 | 609 | ||
498 | doors_[door_id].progressives.push_back( | 610 | panel_doors_[panel_door_id].progressives.push_back( |
499 | {.item_name = progressive_item_name, | 611 | {.item_name = progressive_item_name, |
500 | .ap_item_id = progressive_item_id, | 612 | .ap_item_id = progressive_item_id, |
501 | .quantity = index}); | 613 | .quantity = index}); |
502 | index++; | 614 | index++; |
615 | } | ||
503 | } | 616 | } |
504 | } | 617 | } |
505 | } | 618 | } |
506 | } | 619 | } |
507 | 620 | ||
621 | // Determine the panel solve indices from the sorted location IDs. | ||
622 | std::sort(panel_location_ids.begin(), panel_location_ids.end()); | ||
623 | |||
624 | std::map<int, int> solve_index_by_location_id; | ||
625 | for (int i = 0; i < panel_location_ids.size(); i++) { | ||
626 | solve_index_by_location_id[panel_location_ids[i]] = i; | ||
627 | } | ||
628 | |||
629 | for (Panel &panel : panels_) { | ||
630 | if (panel.ap_location_id != -1) { | ||
631 | panel.solve_index = solve_index_by_location_id[panel.ap_location_id]; | ||
632 | panel_by_solve_index_[panel.solve_index] = panel.id; | ||
633 | } | ||
634 | } | ||
635 | |||
508 | map_areas_.reserve(areas_config.size()); | 636 | map_areas_.reserve(areas_config.size()); |
509 | 637 | ||
510 | std::map<std::string, int> fold_areas; | 638 | std::map<std::string, int> fold_areas; |
@@ -525,7 +653,7 @@ struct GameData { | |||
525 | // Only locations for the panels are kept here. | 653 | // Only locations for the panels are kept here. |
526 | std::map<std::string, std::tuple<int, int>> locations_by_name; | 654 | std::map<std::string, std::tuple<int, int>> locations_by_name; |
527 | 655 | ||
528 | for (const Panel &panel : panels_) { | 656 | for (Panel &panel : panels_) { |
529 | int room_id = panel.room; | 657 | int room_id = panel.room; |
530 | std::string room_name = rooms_[room_id].name; | 658 | std::string room_name = rooms_[room_id].name; |
531 | 659 | ||
@@ -541,6 +669,8 @@ struct GameData { | |||
541 | area_name = location_name.substr(0, divider_pos); | 669 | area_name = location_name.substr(0, divider_pos); |
542 | section_name = location_name.substr(divider_pos + 3); | 670 | section_name = location_name.substr(divider_pos + 3); |
543 | } | 671 | } |
672 | } else { | ||
673 | panel.location_name = location_name; | ||
544 | } | 674 | } |
545 | 675 | ||
546 | if (fold_areas.count(area_name)) { | 676 | if (fold_areas.count(area_name)) { |
@@ -639,7 +769,8 @@ struct GameData { | |||
639 | MapArea &map_area = map_areas_[area_id]; | 769 | MapArea &map_area = map_areas_[area_id]; |
640 | 770 | ||
641 | for (int painting_id : room.paintings) { | 771 | for (int painting_id : room.paintings) { |
642 | const PaintingExit &painting_obj = paintings_.at(painting_id); | 772 | PaintingExit &painting_obj = paintings_.at(painting_id); |
773 | painting_obj.map_area = area_id; | ||
643 | if (painting_obj.entrance) { | 774 | if (painting_obj.entrance) { |
644 | map_area.paintings.push_back(painting_id); | 775 | map_area.paintings.push_back(painting_id); |
645 | } | 776 | } |
@@ -666,13 +797,10 @@ struct GameData { | |||
666 | subway_it["door"].as<std::string>()); | 797 | subway_it["door"].as<std::string>()); |
667 | } | 798 | } |
668 | 799 | ||
669 | if (subway_it["paintings"]) { | 800 | if (subway_it["painting"]) { |
670 | for (const auto &painting_it : subway_it["paintings"]) { | 801 | std::string painting_id = subway_it["painting"].as<std::string>(); |
671 | std::string painting_id = painting_it.as<std::string>(); | 802 | subway_item.painting = painting_id; |
672 | 803 | subway_item_by_painting_[painting_id] = subway_item.id; | |
673 | subway_item.paintings.push_back(painting_id); | ||
674 | subway_item_by_painting_[painting_id] = subway_item.id; | ||
675 | } | ||
676 | } | 804 | } |
677 | 805 | ||
678 | if (subway_it["tags"]) { | 806 | if (subway_it["tags"]) { |
@@ -681,6 +809,18 @@ struct GameData { | |||
681 | } | 809 | } |
682 | } | 810 | } |
683 | 811 | ||
812 | if (subway_it["entrances"]) { | ||
813 | for (const auto &entrance_it : subway_it["entrances"]) { | ||
814 | subway_item.entrances.push_back(entrance_it.as<std::string>()); | ||
815 | } | ||
816 | } | ||
817 | |||
818 | if (subway_it["exits"]) { | ||
819 | for (const auto &exit_it : subway_it["exits"]) { | ||
820 | subway_item.exits.push_back(exit_it.as<std::string>()); | ||
821 | } | ||
822 | } | ||
823 | |||
684 | if (subway_it["sunwarp"]) { | 824 | if (subway_it["sunwarp"]) { |
685 | SubwaySunwarp sunwarp; | 825 | SubwaySunwarp sunwarp; |
686 | sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); | 826 | sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); |
@@ -707,6 +847,10 @@ struct GameData { | |||
707 | subway_item.special = subway_it["special"].as<std::string>(); | 847 | subway_item.special = subway_it["special"].as<std::string>(); |
708 | } | 848 | } |
709 | 849 | ||
850 | if (subway_it["tilted"]) { | ||
851 | subway_item.tilted = subway_it["tilted"].as<bool>(); | ||
852 | } | ||
853 | |||
710 | subway_items_.push_back(subway_item); | 854 | subway_items_.push_back(subway_item); |
711 | } | 855 | } |
712 | 856 | ||
@@ -760,6 +904,18 @@ struct GameData { | |||
760 | return panel_by_id_[full_name]; | 904 | return panel_by_id_[full_name]; |
761 | } | 905 | } |
762 | 906 | ||
907 | int AddOrGetPanelDoor(std::string room, std::string panel) { | ||
908 | std::string full_name = room + " - " + panel; | ||
909 | |||
910 | if (!panel_doors_by_id_.count(full_name)) { | ||
911 | int panel_door_id = panel_doors_.size(); | ||
912 | panel_doors_by_id_[full_name] = panel_door_id; | ||
913 | panel_doors_.push_back({}); | ||
914 | } | ||
915 | |||
916 | return panel_doors_by_id_[full_name]; | ||
917 | } | ||
918 | |||
763 | int AddOrGetArea(std::string area) { | 919 | int AddOrGetArea(std::string area) { |
764 | if (!area_by_id_.count(area)) { | 920 | if (!area_by_id_.count(area)) { |
765 | if (loaded_area_data_) { | 921 | if (loaded_area_data_) { |
@@ -792,6 +948,11 @@ GameData &GetState() { | |||
792 | 948 | ||
793 | } // namespace | 949 | } // namespace |
794 | 950 | ||
951 | bool SubwayItem::HasWarps() const { | ||
952 | return !(this->tags.empty() && this->entrances.empty() && | ||
953 | this->exits.empty()); | ||
954 | } | ||
955 | |||
795 | bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const { | 956 | bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const { |
796 | return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); | 957 | return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); |
797 | } | 958 | } |
@@ -810,6 +971,10 @@ const std::vector<Door> &GD_GetDoors() { return GetState().doors_; } | |||
810 | 971 | ||
811 | const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } | 972 | const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } |
812 | 973 | ||
974 | const PanelDoor &GD_GetPanelDoor(int panel_door_id) { | ||
975 | return GetState().panel_doors_.at(panel_door_id); | ||
976 | } | ||
977 | |||
813 | int GD_GetDoorByName(const std::string &name) { | 978 | int GD_GetDoorByName(const std::string &name) { |
814 | return GetState().door_by_id_.at(name); | 979 | return GetState().door_by_id_.at(name); |
815 | } | 980 | } |
@@ -818,6 +983,14 @@ const Panel &GD_GetPanel(int panel_id) { | |||
818 | return GetState().panels_.at(panel_id); | 983 | return GetState().panels_.at(panel_id); |
819 | } | 984 | } |
820 | 985 | ||
986 | int GD_GetPanelBySolveIndex(int solve_index) { | ||
987 | return GetState().panel_by_solve_index_.at(solve_index); | ||
988 | } | ||
989 | |||
990 | const std::vector<PaintingExit> &GD_GetPaintings() { | ||
991 | return GetState().paintings_; | ||
992 | } | ||
993 | |||
821 | const PaintingExit &GD_GetPaintingExit(int painting_id) { | 994 | const PaintingExit &GD_GetPaintingExit(int painting_id) { |
822 | return GetState().paintings_.at(painting_id); | 995 | return GetState().paintings_.at(painting_id); |
823 | } | 996 | } |
@@ -860,3 +1033,38 @@ std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) { | |||
860 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { | 1033 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { |
861 | return GetState().subway_item_by_sunwarp_.at(sunwarp); | 1034 | return GetState().subway_item_by_sunwarp_.at(sunwarp); |
862 | } | 1035 | } |
1036 | |||
1037 | std::string GD_GetItemName(int id) { | ||
1038 | auto it = GetState().item_by_ap_id_.find(id); | ||
1039 | if (it != GetState().item_by_ap_id_.end()) { | ||
1040 | return it->second; | ||
1041 | } else { | ||
1042 | return "Unknown"; | ||
1043 | } | ||
1044 | } | ||
1045 | |||
1046 | LingoColor GetLingoColorForString(const std::string &str) { | ||
1047 | if (str == "black") { | ||
1048 | return LingoColor::kBlack; | ||
1049 | } else if (str == "red") { | ||
1050 | return LingoColor::kRed; | ||
1051 | } else if (str == "blue") { | ||
1052 | return LingoColor::kBlue; | ||
1053 | } else if (str == "yellow") { | ||
1054 | return LingoColor::kYellow; | ||
1055 | } else if (str == "orange") { | ||
1056 | return LingoColor::kOrange; | ||
1057 | } else if (str == "green") { | ||
1058 | return LingoColor::kGreen; | ||
1059 | } else if (str == "gray") { | ||
1060 | return LingoColor::kGray; | ||
1061 | } else if (str == "brown") { | ||
1062 | return LingoColor::kBrown; | ||
1063 | } else if (str == "purple") { | ||
1064 | return LingoColor::kPurple; | ||
1065 | } else { | ||
1066 | TrackerLog(fmt::format("Invalid color: {}", str)); | ||
1067 | |||
1068 | return LingoColor::kNone; | ||
1069 | } | ||
1070 | } | ||
diff --git a/src/game_data.h b/src/game_data.h index 3179365..ac911e5 100644 --- a/src/game_data.h +++ b/src/game_data.h | |||
@@ -56,6 +56,8 @@ struct Panel { | |||
56 | bool non_counting = false; | 56 | bool non_counting = false; |
57 | int ap_location_id = -1; | 57 | int ap_location_id = -1; |
58 | bool hunt = false; | 58 | bool hunt = false; |
59 | int panel_door = -1; | ||
60 | int solve_index = -1; | ||
59 | }; | 61 | }; |
60 | 62 | ||
61 | struct ProgressiveRequirement { | 63 | struct ProgressiveRequirement { |
@@ -83,6 +85,13 @@ struct Door { | |||
83 | DoorType type = DoorType::kNormal; | 85 | DoorType type = DoorType::kNormal; |
84 | }; | 86 | }; |
85 | 87 | ||
88 | struct PanelDoor { | ||
89 | int ap_item_id = -1; | ||
90 | int group_ap_item_id = -1; | ||
91 | std::vector<ProgressiveRequirement> progressives; | ||
92 | std::string item_name; | ||
93 | }; | ||
94 | |||
86 | struct Exit { | 95 | struct Exit { |
87 | int source_room; | 96 | int source_room; |
88 | int destination_room; | 97 | int destination_room; |
@@ -94,8 +103,10 @@ struct PaintingExit { | |||
94 | int id; | 103 | int id; |
95 | int room; | 104 | int room; |
96 | std::string internal_id; | 105 | std::string internal_id; |
106 | std::string display_name; | ||
97 | std::optional<int> door; | 107 | std::optional<int> door; |
98 | bool entrance = false; | 108 | bool entrance = false; |
109 | int map_area; | ||
99 | }; | 110 | }; |
100 | 111 | ||
101 | struct Room { | 112 | struct Room { |
@@ -146,11 +157,16 @@ struct SubwayItem { | |||
146 | int id; | 157 | int id; |
147 | int x; | 158 | int x; |
148 | int y; | 159 | int y; |
160 | bool tilted = false; | ||
149 | std::optional<int> door; | 161 | std::optional<int> door; |
150 | std::vector<std::string> paintings; | 162 | std::optional<std::string> painting; |
151 | std::vector<std::string> tags; | 163 | std::vector<std::string> tags; // 2-way teleports |
164 | std::vector<std::string> entrances; // teleport entrances | ||
165 | std::vector<std::string> exits; // teleport exits | ||
152 | std::optional<SubwaySunwarp> sunwarp; | 166 | std::optional<SubwaySunwarp> sunwarp; |
153 | std::optional<std::string> special; | 167 | std::optional<std::string> special; |
168 | |||
169 | bool HasWarps() const; | ||
154 | }; | 170 | }; |
155 | 171 | ||
156 | const std::vector<MapArea>& GD_GetMapAreas(); | 172 | const std::vector<MapArea>& GD_GetMapAreas(); |
@@ -161,6 +177,9 @@ const std::vector<Door>& GD_GetDoors(); | |||
161 | const Door& GD_GetDoor(int door_id); | 177 | const Door& GD_GetDoor(int door_id); |
162 | int GD_GetDoorByName(const std::string& name); | 178 | int GD_GetDoorByName(const std::string& name); |
163 | const Panel& GD_GetPanel(int panel_id); | 179 | const Panel& GD_GetPanel(int panel_id); |
180 | int GD_GetPanelBySolveIndex(int solve_index); | ||
181 | const PanelDoor& GD_GetPanelDoor(int panel_door_id); | ||
182 | const std::vector<PaintingExit>& GD_GetPaintings(); | ||
164 | const PaintingExit& GD_GetPaintingExit(int painting_id); | 183 | const PaintingExit& GD_GetPaintingExit(int painting_id); |
165 | int GD_GetPaintingByName(const std::string& name); | 184 | int GD_GetPaintingByName(const std::string& name); |
166 | const std::vector<int>& GD_GetAchievementPanels(); | 185 | const std::vector<int>& GD_GetAchievementPanels(); |
@@ -171,5 +190,8 @@ const std::vector<SubwayItem>& GD_GetSubwayItems(); | |||
171 | const SubwayItem& GD_GetSubwayItem(int id); | 190 | const SubwayItem& GD_GetSubwayItem(int id); |
172 | std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); | 191 | std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); |
173 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); | 192 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); |
193 | std::string GD_GetItemName(int id); | ||
194 | |||
195 | LingoColor GetLingoColorForString(const std::string& str); | ||
174 | 196 | ||
175 | #endif /* end of include guard: GAME_DATA_H_9C42AC51 */ | 197 | #endif /* end of include guard: GAME_DATA_H_9C42AC51 */ |
diff --git a/src/global.cpp b/src/global.cpp index 1eb3f8d..63f4a19 100644 --- a/src/global.cpp +++ b/src/global.cpp | |||
@@ -26,17 +26,19 @@ std::string GetAbsolutePath(std::string_view path) { | |||
26 | return (GetExecutableDirectory() / path).string(); | 26 | return (GetExecutableDirectory() / path).string(); |
27 | } | 27 | } |
28 | 28 | ||
29 | bool IsLocationWinCondition(const Location& location) { | 29 | std::string GetWinCondition() { |
30 | switch (AP_GetVictoryCondition()) { | 30 | switch (AP_GetVictoryCondition()) { |
31 | case kTHE_END: | 31 | case kTHE_END: |
32 | return location.ap_location_name == | 32 | return "Orange Tower Seventh Floor - THE END"; |
33 | "Orange Tower Seventh Floor - THE END"; | ||
34 | case kTHE_MASTER: | 33 | case kTHE_MASTER: |
35 | return location.ap_location_name == | 34 | return "Orange Tower Seventh Floor - THE MASTER"; |
36 | "Orange Tower Seventh Floor - THE MASTER"; | ||
37 | case kLEVEL_2: | 35 | case kLEVEL_2: |
38 | return location.ap_location_name == "Second Room - LEVEL 2"; | 36 | return "Second Room - LEVEL 2"; |
39 | case kPILGRIMAGE: | 37 | case kPILGRIMAGE: |
40 | return location.ap_location_name == "Pilgrim Antechamber - PILGRIM"; | 38 | return "Pilgrim Antechamber - PILGRIM"; |
41 | } | 39 | } |
42 | } | 40 | } |
41 | |||
42 | bool IsLocationWinCondition(const Location& location) { | ||
43 | return location.ap_location_name == GetWinCondition(); | ||
44 | } | ||
diff --git a/src/global.h b/src/global.h index 31ebde3..bdfa7ae 100644 --- a/src/global.h +++ b/src/global.h | |||
@@ -10,6 +10,8 @@ const std::filesystem::path& GetExecutableDirectory(); | |||
10 | 10 | ||
11 | std::string GetAbsolutePath(std::string_view path); | 11 | std::string GetAbsolutePath(std::string_view path); |
12 | 12 | ||
13 | std::string GetWinCondition(); | ||
14 | |||
13 | bool IsLocationWinCondition(const Location& location); | 15 | bool IsLocationWinCondition(const Location& location); |
14 | 16 | ||
15 | #endif /* end of include guard: GLOBAL_H_44945DBA */ | 17 | #endif /* end of include guard: GLOBAL_H_44945DBA */ |
diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp deleted file mode 100644 index 1bc906f..0000000 --- a/src/godot_variant.cpp +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | // Godot save decoder algorithm by Chris Souvey. | ||
2 | |||
3 | #include "godot_variant.h" | ||
4 | |||
5 | #include <algorithm> | ||
6 | #include <charconv> | ||
7 | #include <cstddef> | ||
8 | #include <fstream> | ||
9 | #include <string> | ||
10 | #include <tuple> | ||
11 | #include <variant> | ||
12 | #include <vector> | ||
13 | |||
14 | namespace { | ||
15 | |||
16 | uint16_t ReadUint16(std::basic_istream<char>& stream) { | ||
17 | uint16_t result; | ||
18 | stream.read(reinterpret_cast<char*>(&result), 2); | ||
19 | return result; | ||
20 | } | ||
21 | |||
22 | uint32_t ReadUint32(std::basic_istream<char>& stream) { | ||
23 | uint32_t result; | ||
24 | stream.read(reinterpret_cast<char*>(&result), 4); | ||
25 | return result; | ||
26 | } | ||
27 | |||
28 | GodotVariant ParseVariant(std::basic_istream<char>& stream) { | ||
29 | uint16_t type = ReadUint16(stream); | ||
30 | stream.ignore(2); | ||
31 | |||
32 | switch (type) { | ||
33 | case 1: { | ||
34 | // bool | ||
35 | bool boolval = (ReadUint32(stream) == 1); | ||
36 | return {boolval}; | ||
37 | } | ||
38 | case 15: { | ||
39 | // nodepath | ||
40 | uint32_t name_length = ReadUint32(stream) & 0x7fffffff; | ||
41 | uint32_t subname_length = ReadUint32(stream) & 0x7fffffff; | ||
42 | uint32_t flags = ReadUint32(stream); | ||
43 | |||
44 | std::vector<std::string> result; | ||
45 | for (size_t i = 0; i < name_length + subname_length; i++) { | ||
46 | uint32_t char_length = ReadUint32(stream); | ||
47 | uint32_t padded_length = (char_length % 4 == 0) | ||
48 | ? char_length | ||
49 | : (char_length + 4 - (char_length % 4)); | ||
50 | std::vector<char> next_bytes(padded_length); | ||
51 | stream.read(next_bytes.data(), padded_length); | ||
52 | std::string next_piece; | ||
53 | std::copy(next_bytes.begin(), | ||
54 | std::next(next_bytes.begin(), char_length), | ||
55 | std::back_inserter(next_piece)); | ||
56 | result.push_back(next_piece); | ||
57 | } | ||
58 | |||
59 | return {result}; | ||
60 | } | ||
61 | case 19: { | ||
62 | // array | ||
63 | uint32_t length = ReadUint32(stream) & 0x7fffffff; | ||
64 | std::vector<GodotVariant> result; | ||
65 | for (size_t i = 0; i < length; i++) { | ||
66 | result.push_back(ParseVariant(stream)); | ||
67 | } | ||
68 | return {result}; | ||
69 | } | ||
70 | default: { | ||
71 | // eh | ||
72 | return {std::monostate{}}; | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | |||
77 | } // namespace | ||
78 | |||
79 | GodotVariant ParseGodotFile(std::string filename) { | ||
80 | std::ifstream file_stream(filename, std::ios_base::binary); | ||
81 | file_stream.ignore(4); | ||
82 | return ParseVariant(file_stream); | ||
83 | } | ||
diff --git a/src/godot_variant.h b/src/godot_variant.h deleted file mode 100644 index 620e569..0000000 --- a/src/godot_variant.h +++ /dev/null | |||
@@ -1,28 +0,0 @@ | |||
1 | #ifndef GODOT_VARIANT_H_ED7F2EB6 | ||
2 | #define GODOT_VARIANT_H_ED7F2EB6 | ||
3 | |||
4 | #include <string> | ||
5 | #include <variant> | ||
6 | #include <vector> | ||
7 | |||
8 | struct GodotVariant { | ||
9 | using value_type = std::variant<std::monostate, bool, std::vector<std::string>, std::vector<GodotVariant>>; | ||
10 | |||
11 | value_type value; | ||
12 | |||
13 | GodotVariant(value_type v) : value(v) {} | ||
14 | |||
15 | bool AsBool() const { return std::get<bool>(value); } | ||
16 | |||
17 | const std::vector<std::string>& AsNodePath() const { | ||
18 | return std::get<std::vector<std::string>>(value); | ||
19 | } | ||
20 | |||
21 | const std::vector<GodotVariant>& AsArray() const { | ||
22 | return std::get<std::vector<GodotVariant>>(value); | ||
23 | } | ||
24 | }; | ||
25 | |||
26 | GodotVariant ParseGodotFile(std::string filename); | ||
27 | |||
28 | #endif /* end of include guard: GODOT_VARIANT_H_ED7F2EB6 */ | ||
diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..87ba037 --- /dev/null +++ b/src/icons.cpp | |||
@@ -0,0 +1,22 @@ | |||
1 | #include "icons.h" | ||
2 | |||
3 | #include "global.h" | ||
4 | |||
5 | const wxBitmap* IconCache::GetIcon(const std::string& filename, wxSize size) { | ||
6 | std::tuple<std::string, int, int> key = {filename, size.x, size.y}; | ||
7 | |||
8 | if (!icons_.count(key)) { | ||
9 | icons_[key] = | ||
10 | wxBitmap(wxImage(GetAbsolutePath(filename).c_str(), | ||
11 | wxBITMAP_TYPE_PNG) | ||
12 | .Scale(size.x, size.y)); | ||
13 | } | ||
14 | |||
15 | return &icons_.at(key); | ||
16 | } | ||
17 | |||
18 | static IconCache* ICON_CACHE_INSTANCE = nullptr; | ||
19 | |||
20 | void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; } | ||
21 | |||
22 | IconCache& GetTheIconCache() { return *ICON_CACHE_INSTANCE; } | ||
diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 0000000..23dca2a --- /dev/null +++ b/src/icons.h | |||
@@ -0,0 +1,25 @@ | |||
1 | #ifndef ICONS_H_B95159A6 | ||
2 | #define ICONS_H_B95159A6 | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | #include <map> | ||
11 | #include <string> | ||
12 | #include <tuple> | ||
13 | |||
14 | class IconCache { | ||
15 | public: | ||
16 | const wxBitmap* GetIcon(const std::string& filename, wxSize size); | ||
17 | |||
18 | private: | ||
19 | std::map<std::tuple<std::string, int, int>, wxBitmap> icons_; | ||
20 | }; | ||
21 | |||
22 | void SetTheIconCache(IconCache* instance); | ||
23 | IconCache& GetTheIconCache(); | ||
24 | |||
25 | #endif /* end of include guard: ICONS_H_B95159A6 */ | ||
diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp new file mode 100644 index 0000000..6763b7f --- /dev/null +++ b/src/ipc_dialog.cpp | |||
@@ -0,0 +1,54 @@ | |||
1 | #include "ipc_dialog.h" | ||
2 | |||
3 | #include "tracker_config.h" | ||
4 | |||
5 | constexpr const char* kDefaultIpcAddress = "ws://127.0.0.1:41253"; | ||
6 | |||
7 | IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") { | ||
8 | std::string address_value; | ||
9 | if (GetTrackerConfig().ipc_address.empty()) { | ||
10 | address_value = kDefaultIpcAddress; | ||
11 | } else { | ||
12 | address_value = GetTrackerConfig().ipc_address; | ||
13 | } | ||
14 | |||
15 | address_box_ = new wxTextCtrl(this, -1, wxString::FromUTF8(address_value), | ||
16 | wxDefaultPosition, FromDIP(wxSize{300, -1})); | ||
17 | |||
18 | wxButton* reset_button = new wxButton(this, -1, "Use Default"); | ||
19 | reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this); | ||
20 | |||
21 | wxFlexGridSizer* form_sizer = | ||
22 | new wxFlexGridSizer(3, FromDIP(10), FromDIP(10)); | ||
23 | form_sizer->Add( | ||
24 | new wxStaticText(this, -1, "Address:"), | ||
25 | wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); | ||
26 | form_sizer->Add(address_box_, wxSizerFlags().Expand()); | ||
27 | form_sizer->Add(reset_button); | ||
28 | |||
29 | wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); | ||
30 | wxStaticText* top_text = new wxStaticText(this, -1, ""); | ||
31 | top_sizer->Add(top_text, wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder().Expand()); | ||
32 | top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand()); | ||
33 | top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), | ||
34 | wxSizerFlags().Border().Center()); | ||
35 | |||
36 | SetSizer(top_sizer); | ||
37 | Layout(); | ||
38 | Fit(); | ||
39 | |||
40 | int width = top_text->GetClientSize().GetWidth(); | ||
41 | top_text->SetLabel( | ||
42 | "This allows you to connect to a running Lingo game and track non-multiworld " | ||
43 | "state, such as the player's position and what panels are solved. Unless " | ||
44 | "you are doing something weird, the default value for the address is " | ||
45 | "probably correct."); | ||
46 | top_text->Wrap(width); | ||
47 | |||
48 | Fit(); | ||
49 | Center(); | ||
50 | } | ||
51 | |||
52 | void IpcDialog::OnResetClicked(wxCommandEvent& event) { | ||
53 | address_box_->SetValue(kDefaultIpcAddress); | ||
54 | } | ||
diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h new file mode 100644 index 0000000..a8c4512 --- /dev/null +++ b/src/ipc_dialog.h | |||
@@ -0,0 +1,24 @@ | |||
1 | #ifndef IPC_DIALOG_H_F4C5680C | ||
2 | #define IPC_DIALOG_H_F4C5680C | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | #include <string> | ||
11 | |||
12 | class IpcDialog : public wxDialog { | ||
13 | public: | ||
14 | IpcDialog(); | ||
15 | |||
16 | std::string GetIpcAddress() { return address_box_->GetValue().utf8_string(); } | ||
17 | |||
18 | private: | ||
19 | void OnResetClicked(wxCommandEvent& event); | ||
20 | |||
21 | wxTextCtrl* address_box_; | ||
22 | }; | ||
23 | |||
24 | #endif /* end of include guard: IPC_DIALOG_H_F4C5680C */ | ||
diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp new file mode 100644 index 0000000..6e2a440 --- /dev/null +++ b/src/ipc_state.cpp | |||
@@ -0,0 +1,367 @@ | |||
1 | #include "ipc_state.h" | ||
2 | |||
3 | #define _WEBSOCKETPP_CPP11_STRICT_ | ||
4 | |||
5 | #include <fmt/core.h> | ||
6 | |||
7 | #include <chrono> | ||
8 | #include <memory> | ||
9 | #include <mutex> | ||
10 | #include <nlohmann/json.hpp> | ||
11 | #include <optional> | ||
12 | #include <set> | ||
13 | #include <string> | ||
14 | #include <thread> | ||
15 | #include <tuple> | ||
16 | #include <wswrap.hpp> | ||
17 | |||
18 | #include "ap_state.h" | ||
19 | #include "logger.h" | ||
20 | #include "tracker_frame.h" | ||
21 | |||
22 | namespace { | ||
23 | |||
24 | struct IPCState { | ||
25 | std::mutex state_mutex; | ||
26 | TrackerFrame* tracker_frame = nullptr; | ||
27 | |||
28 | // Protected state | ||
29 | bool initialized = false; | ||
30 | std::string address; | ||
31 | bool should_disconnect = false; | ||
32 | |||
33 | std::optional<std::string> status_message; | ||
34 | |||
35 | bool slot_matches = false; | ||
36 | std::string tracker_ap_server; | ||
37 | std::string tracker_ap_user; | ||
38 | std::string game_ap_server; | ||
39 | std::string game_ap_user; | ||
40 | |||
41 | std::optional<std::tuple<int, int>> player_position; | ||
42 | |||
43 | // Thread state | ||
44 | std::unique_ptr<wswrap::WS> ws; | ||
45 | bool connected = false; | ||
46 | |||
47 | void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; } | ||
48 | |||
49 | void Connect(std::string a) { | ||
50 | // This is the main concurrency concern, as it mutates protected state in an | ||
51 | // important way. Thread() is documented with how it interacts with this | ||
52 | // function. | ||
53 | std::lock_guard state_guard(state_mutex); | ||
54 | |||
55 | if (!initialized) { | ||
56 | std::thread([this]() { Thread(); }).detach(); | ||
57 | |||
58 | initialized = true; | ||
59 | } else if (address != a) { | ||
60 | should_disconnect = true; | ||
61 | } | ||
62 | |||
63 | address = a; | ||
64 | } | ||
65 | |||
66 | std::optional<std::string> GetStatusMessage() { | ||
67 | std::lock_guard state_guard(state_mutex); | ||
68 | |||
69 | return status_message; | ||
70 | } | ||
71 | |||
72 | void SetTrackerSlot(std::string server, std::string user) { | ||
73 | // This function is called from the APState thread, not the main thread, and | ||
74 | // it mutates protected state. It only really competes with OnMessage(), when | ||
75 | // a "Connect" message is received. If this is called right before, and the | ||
76 | // tracker slot does not match the old game slot, it will initiate a | ||
77 | // disconnect, and then the OnMessage() handler will see should_disconnect | ||
78 | // and stop processing the "Connect" message. If this is called right after | ||
79 | // and the slot does not match, IPC will disconnect, which is tolerable. | ||
80 | std::lock_guard state_guard(state_mutex); | ||
81 | |||
82 | tracker_ap_server = std::move(server); | ||
83 | tracker_ap_user = std::move(user); | ||
84 | |||
85 | CheckIfSlotMatches(); | ||
86 | |||
87 | if (!slot_matches) { | ||
88 | should_disconnect = true; | ||
89 | address.clear(); | ||
90 | } | ||
91 | } | ||
92 | |||
93 | bool IsConnected() { | ||
94 | std::lock_guard state_guard(state_mutex); | ||
95 | |||
96 | return slot_matches; | ||
97 | } | ||
98 | |||
99 | std::optional<std::tuple<int, int>> GetPlayerPosition() { | ||
100 | std::lock_guard state_guard(state_mutex); | ||
101 | |||
102 | return player_position; | ||
103 | } | ||
104 | |||
105 | private: | ||
106 | void Thread() { | ||
107 | for (;;) { | ||
108 | // initialized is definitely true because it is set to true when the thread | ||
109 | // is created and only set to false within this block, when the thread is | ||
110 | // killed. Thus, a call to Connect would always at most set | ||
111 | // should_disconnect and address. If this happens before this block, it is | ||
112 | // as if we are starting from a new thread anyway because should_disconnect | ||
113 | // is immediately reset. If a call to Connect happens after this block, | ||
114 | // then a connection attempt will be made to the wrong address, but the | ||
115 | // thread will grab the mutex right after this and back out the wrong | ||
116 | // connection. | ||
117 | std::string ipc_address; | ||
118 | { | ||
119 | std::lock_guard state_guard(state_mutex); | ||
120 | |||
121 | SetStatusMessage("Disconnected from game."); | ||
122 | |||
123 | should_disconnect = false; | ||
124 | |||
125 | slot_matches = false; | ||
126 | game_ap_server.clear(); | ||
127 | game_ap_user.clear(); | ||
128 | |||
129 | player_position = std::nullopt; | ||
130 | |||
131 | if (address.empty()) { | ||
132 | initialized = false; | ||
133 | return; | ||
134 | } | ||
135 | |||
136 | ipc_address = address; | ||
137 | |||
138 | SetStatusMessage("Connecting to game..."); | ||
139 | } | ||
140 | |||
141 | int backoff_amount = 0; | ||
142 | |||
143 | TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address)); | ||
144 | |||
145 | while (!connected) { | ||
146 | if (TryConnect(ipc_address)) { | ||
147 | int backoff_limit = (backoff_amount + 1) * 10; | ||
148 | |||
149 | for (int i = 0; i < backoff_limit && !connected; i++) { | ||
150 | // If Connect is called right before this block, we will see and | ||
151 | // handle should_disconnect. If it is called right after, we will do | ||
152 | // one bad poll, one sleep, and then grab the mutex again right | ||
153 | // after. | ||
154 | { | ||
155 | std::lock_guard state_guard(state_mutex); | ||
156 | if (should_disconnect) { | ||
157 | break; | ||
158 | } | ||
159 | } | ||
160 | |||
161 | ws->poll(); | ||
162 | |||
163 | // Back off | ||
164 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
165 | } | ||
166 | |||
167 | backoff_amount++; | ||
168 | } else { | ||
169 | std::lock_guard state_guard(state_mutex); | ||
170 | |||
171 | if (!should_disconnect) { | ||
172 | should_disconnect = true; | ||
173 | address.clear(); | ||
174 | |||
175 | SetStatusMessage("Disconnected from game."); | ||
176 | } | ||
177 | |||
178 | break; | ||
179 | } | ||
180 | |||
181 | // If Connect is called right before this block, we will see and handle | ||
182 | // should_disconnect. If it is called right after, and the connection | ||
183 | // was unsuccessful, we will grab the mutex after one bad connection | ||
184 | // attempt. If the connection was successful, we grab the mutex right | ||
185 | // after exiting the loop. | ||
186 | bool show_error = false; | ||
187 | { | ||
188 | std::lock_guard state_guard(state_mutex); | ||
189 | |||
190 | if (should_disconnect) { | ||
191 | break; | ||
192 | } else if (!connected) { | ||
193 | if (backoff_amount >= 10) { | ||
194 | should_disconnect = true; | ||
195 | address.clear(); | ||
196 | |||
197 | SetStatusMessage("Disconnected from game."); | ||
198 | |||
199 | show_error = true; | ||
200 | } else { | ||
201 | TrackerLog(fmt::format("Retrying IPC in {} second(s)...", | ||
202 | backoff_amount + 1)); | ||
203 | } | ||
204 | } | ||
205 | } | ||
206 | |||
207 | // We do this after giving up the mutex because otherwise we could | ||
208 | // deadlock with the main thread. | ||
209 | if (show_error) { | ||
210 | TrackerLog("Giving up on IPC."); | ||
211 | |||
212 | wxMessageBox("Connection to Lingo timed out.", "Connection failed", | ||
213 | wxOK | wxICON_ERROR); | ||
214 | break; | ||
215 | } | ||
216 | } | ||
217 | |||
218 | // Pretty much every lock guard in the thread is the same. We check for | ||
219 | // should_disconnect, and if it gets set directly after the block, we do | ||
220 | // minimal bad work before checking for it again. | ||
221 | { | ||
222 | std::lock_guard state_guard(state_mutex); | ||
223 | if (should_disconnect) { | ||
224 | ws.reset(); | ||
225 | continue; | ||
226 | } | ||
227 | } | ||
228 | |||
229 | while (connected) { | ||
230 | ws->poll(); | ||
231 | |||
232 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
233 | |||
234 | { | ||
235 | std::lock_guard state_guard(state_mutex); | ||
236 | if (should_disconnect) { | ||
237 | ws.reset(); | ||
238 | break; | ||
239 | } | ||
240 | } | ||
241 | } | ||
242 | } | ||
243 | } | ||
244 | |||
245 | bool TryConnect(std::string ipc_address) { | ||
246 | try { | ||
247 | ws = std::make_unique<wswrap::WS>( | ||
248 | ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); }, | ||
249 | [this](const std::string& s) { OnMessage(s); }, | ||
250 | [this](const std::string& s) { OnError(s); }); | ||
251 | return true; | ||
252 | } catch (const std::exception& ex) { | ||
253 | TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what())); | ||
254 | wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR); | ||
255 | ws.reset(); | ||
256 | return false; | ||
257 | } | ||
258 | } | ||
259 | |||
260 | void OnConnect() { | ||
261 | connected = true; | ||
262 | |||
263 | { | ||
264 | std::lock_guard state_guard(state_mutex); | ||
265 | |||
266 | slot_matches = false; | ||
267 | player_position = std::nullopt; | ||
268 | } | ||
269 | } | ||
270 | |||
271 | void OnClose() { | ||
272 | connected = false; | ||
273 | |||
274 | { | ||
275 | std::lock_guard state_guard(state_mutex); | ||
276 | |||
277 | slot_matches = false; | ||
278 | } | ||
279 | } | ||
280 | |||
281 | void OnMessage(const std::string& s) { | ||
282 | TrackerLog(s); | ||
283 | |||
284 | auto msg = nlohmann::json::parse(s); | ||
285 | |||
286 | if (msg["cmd"] == "Connect") { | ||
287 | std::lock_guard state_guard(state_mutex); | ||
288 | if (should_disconnect) { | ||
289 | return; | ||
290 | } | ||
291 | |||
292 | game_ap_server = msg["slot"]["server"]; | ||
293 | game_ap_user = msg["slot"]["player"]; | ||
294 | |||
295 | CheckIfSlotMatches(); | ||
296 | |||
297 | if (!slot_matches) { | ||
298 | tracker_frame->ConnectToAp(game_ap_server, game_ap_user, | ||
299 | msg["slot"]["password"]); | ||
300 | } | ||
301 | } else if (msg["cmd"] == "UpdatePosition") { | ||
302 | std::lock_guard state_guard(state_mutex); | ||
303 | |||
304 | player_position = | ||
305 | std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]); | ||
306 | |||
307 | tracker_frame->UpdateIndicators(StateUpdate{.player_position = true}); | ||
308 | } | ||
309 | } | ||
310 | |||
311 | void OnError(const std::string& s) {} | ||
312 | |||
313 | // Assumes mutex is locked. | ||
314 | void CheckIfSlotMatches() { | ||
315 | slot_matches = (tracker_ap_server == game_ap_server && | ||
316 | tracker_ap_user == game_ap_user); | ||
317 | |||
318 | if (slot_matches) { | ||
319 | SetStatusMessage("Connected to game."); | ||
320 | |||
321 | Sync(); | ||
322 | } else if (connected) { | ||
323 | SetStatusMessage("Local game doesn't match AP slot."); | ||
324 | } | ||
325 | } | ||
326 | |||
327 | // Assumes mutex is locked. | ||
328 | void SetStatusMessage(std::optional<std::string> msg) { | ||
329 | status_message = msg; | ||
330 | |||
331 | tracker_frame->UpdateStatusMessage(); | ||
332 | } | ||
333 | |||
334 | void Sync() { | ||
335 | nlohmann::json msg; | ||
336 | msg["cmd"] = "Sync"; | ||
337 | |||
338 | ws->send_text(msg.dump()); | ||
339 | } | ||
340 | }; | ||
341 | |||
342 | IPCState& GetState() { | ||
343 | static IPCState* instance = new IPCState(); | ||
344 | return *instance; | ||
345 | } | ||
346 | |||
347 | } // namespace | ||
348 | |||
349 | void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) { | ||
350 | GetState().SetTrackerFrame(tracker_frame); | ||
351 | } | ||
352 | |||
353 | void IPC_Connect(std::string address) { GetState().Connect(address); } | ||
354 | |||
355 | std::optional<std::string> IPC_GetStatusMessage() { | ||
356 | return GetState().GetStatusMessage(); | ||
357 | } | ||
358 | |||
359 | void IPC_SetTrackerSlot(std::string server, std::string user) { | ||
360 | GetState().SetTrackerSlot(server, user); | ||
361 | } | ||
362 | |||
363 | bool IPC_IsConnected() { return GetState().IsConnected(); } | ||
364 | |||
365 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { | ||
366 | return GetState().GetPlayerPosition(); | ||
367 | } | ||
diff --git a/src/ipc_state.h b/src/ipc_state.h new file mode 100644 index 0000000..0e6fa51 --- /dev/null +++ b/src/ipc_state.h | |||
@@ -0,0 +1,23 @@ | |||
1 | #ifndef IPC_STATE_H_6B3B0958 | ||
2 | #define IPC_STATE_H_6B3B0958 | ||
3 | |||
4 | #include <optional> | ||
5 | #include <set> | ||
6 | #include <string> | ||
7 | #include <tuple> | ||
8 | |||
9 | class TrackerFrame; | ||
10 | |||
11 | void IPC_SetTrackerFrame(TrackerFrame* tracker_frame); | ||
12 | |||
13 | void IPC_Connect(std::string address); | ||
14 | |||
15 | std::optional<std::string> IPC_GetStatusMessage(); | ||
16 | |||
17 | void IPC_SetTrackerSlot(std::string server, std::string user); | ||
18 | |||
19 | bool IPC_IsConnected(); | ||
20 | |||
21 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition(); | ||
22 | |||
23 | #endif /* end of include guard: IPC_STATE_H_6B3B0958 */ | ||
diff --git a/src/items_pane.cpp b/src/items_pane.cpp new file mode 100644 index 0000000..055eec0 --- /dev/null +++ b/src/items_pane.cpp | |||
@@ -0,0 +1,145 @@ | |||
1 | #include "items_pane.h" | ||
2 | |||
3 | #include <map> | ||
4 | |||
5 | namespace { | ||
6 | |||
7 | enum SortInstruction { | ||
8 | SI_NONE = 0, | ||
9 | SI_ASC = 1 << 0, | ||
10 | SI_DESC = 1 << 1, | ||
11 | SI_NAME = 1 << 2, | ||
12 | SI_AMOUNT = 1 << 3, | ||
13 | SI_ORDER = 1 << 4, | ||
14 | }; | ||
15 | |||
16 | inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) { | ||
17 | return static_cast<SortInstruction>(static_cast<int>(lhs) | | ||
18 | static_cast<int>(rhs)); | ||
19 | } | ||
20 | |||
21 | template <typename T> | ||
22 | int ItemCompare(const T& lhs, const T& rhs, bool ascending) { | ||
23 | if (lhs < rhs) { | ||
24 | return ascending ? -1 : 1; | ||
25 | } else if (lhs > rhs) { | ||
26 | return ascending ? 1 : -1; | ||
27 | } else { | ||
28 | return 0; | ||
29 | } | ||
30 | } | ||
31 | |||
32 | int wxCALLBACK RowCompare(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) { | ||
33 | const ItemState& lhs = *reinterpret_cast<const ItemState*>(item1); | ||
34 | const ItemState& rhs = *reinterpret_cast<const ItemState*>(item2); | ||
35 | SortInstruction instruction = static_cast<SortInstruction>(sortData); | ||
36 | |||
37 | bool ascending = (instruction & SI_ASC) != 0; | ||
38 | if ((instruction & SI_NAME) != 0) { | ||
39 | return ItemCompare(lhs.name, rhs.name, ascending); | ||
40 | } else if ((instruction & SI_AMOUNT) != 0) { | ||
41 | return ItemCompare(lhs.amount, rhs.amount, ascending); | ||
42 | } else if ((instruction & SI_ORDER) != 0) { | ||
43 | return ItemCompare(lhs.index, rhs.index, ascending); | ||
44 | } else { | ||
45 | return 0; | ||
46 | } | ||
47 | } | ||
48 | |||
49 | } // namespace | ||
50 | |||
51 | ItemsPane::ItemsPane(wxWindow* parent) | ||
52 | : wxListView(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, | ||
53 | wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_HRULES) { | ||
54 | AppendColumn("Item", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER); | ||
55 | AppendColumn("Amount", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER); | ||
56 | AppendColumn("Order", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER); | ||
57 | |||
58 | Bind(wxEVT_LIST_COL_CLICK, &ItemsPane::OnColClick, this); | ||
59 | Bind(wxEVT_DPI_CHANGED, &ItemsPane::OnDPIChanged, this); | ||
60 | } | ||
61 | |||
62 | void ItemsPane::ResetIndicators() { | ||
63 | DeleteAllItems(); | ||
64 | items_.clear(); | ||
65 | } | ||
66 | |||
67 | void ItemsPane::UpdateIndicators(const std::vector<ItemState>& items) { | ||
68 | std::map<std::string, ItemState> items_by_name; | ||
69 | |||
70 | for (const ItemState& item : items) { | ||
71 | items_by_name[item.name] = item; | ||
72 | } | ||
73 | |||
74 | for (int i = 0; i < GetItemCount(); i++) { | ||
75 | std::string item_name = GetItemText(i).utf8_string(); | ||
76 | auto it = items_by_name.find(item_name); | ||
77 | |||
78 | if (it != items_by_name.end()) { | ||
79 | SetItem(i, 1, std::to_string(it->second.amount)); | ||
80 | SetItem(i, 2, std::to_string(it->second.index)); | ||
81 | |||
82 | *reinterpret_cast<ItemState*>(GetItemData(i)) = it->second; | ||
83 | |||
84 | items_by_name.erase(item_name); | ||
85 | } | ||
86 | } | ||
87 | |||
88 | for (const auto& [name, item] : items_by_name) { | ||
89 | int i = InsertItem(GetItemCount(), name); | ||
90 | SetItem(i, 1, std::to_string(item.amount)); | ||
91 | SetItem(i, 2, std::to_string(item.index)); | ||
92 | |||
93 | auto item_ptr = std::make_unique<ItemState>(item); | ||
94 | SetItemPtrData(i, reinterpret_cast<wxUIntPtr>(item_ptr.get())); | ||
95 | items_.push_back(std::move(item_ptr)); | ||
96 | } | ||
97 | |||
98 | SetColumnWidth(0, wxLIST_AUTOSIZE); | ||
99 | SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); | ||
100 | SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER); | ||
101 | |||
102 | if (GetSortIndicator() != -1) { | ||
103 | DoSort(GetSortIndicator(), IsAscendingSortIndicator()); | ||
104 | } | ||
105 | } | ||
106 | |||
107 | void ItemsPane::OnColClick(wxListEvent& event) { | ||
108 | int col = event.GetColumn(); | ||
109 | if (col == -1) { | ||
110 | return; | ||
111 | } | ||
112 | |||
113 | bool ascending = GetUpdatedAscendingSortIndicator(col); | ||
114 | |||
115 | DoSort(col, ascending); | ||
116 | } | ||
117 | |||
118 | void ItemsPane::OnDPIChanged(wxDPIChangedEvent& event) { | ||
119 | SetColumnWidth(0, wxLIST_AUTOSIZE); | ||
120 | SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER); | ||
121 | SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER); | ||
122 | |||
123 | event.Skip(); | ||
124 | } | ||
125 | |||
126 | void ItemsPane::DoSort(int col, bool ascending) { | ||
127 | SortInstruction instruction = SI_NONE; | ||
128 | if (ascending) { | ||
129 | instruction = instruction | SI_ASC; | ||
130 | } else { | ||
131 | instruction = instruction | SI_DESC; | ||
132 | } | ||
133 | |||
134 | if (col == 0) { | ||
135 | instruction = instruction | SI_NAME; | ||
136 | } else if (col == 1) { | ||
137 | instruction = instruction | SI_AMOUNT; | ||
138 | } else if (col == 2) { | ||
139 | instruction = instruction | SI_ORDER; | ||
140 | } | ||
141 | |||
142 | if (SortItems(RowCompare, instruction)) { | ||
143 | ShowSortIndicator(col, ascending); | ||
144 | } | ||
145 | } | ||
diff --git a/src/items_pane.h b/src/items_pane.h new file mode 100644 index 0000000..aa09c49 --- /dev/null +++ b/src/items_pane.h | |||
@@ -0,0 +1,33 @@ | |||
1 | #ifndef ITEMS_PANE_H_EB637EE3 | ||
2 | #define ITEMS_PANE_H_EB637EE3 | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | #include <wx/listctrl.h> | ||
11 | |||
12 | #include <memory> | ||
13 | #include <vector> | ||
14 | |||
15 | #include "ap_state.h" | ||
16 | |||
17 | class ItemsPane : public wxListView { | ||
18 | public: | ||
19 | explicit ItemsPane(wxWindow* parent); | ||
20 | |||
21 | void ResetIndicators(); | ||
22 | void UpdateIndicators(const std::vector<ItemState>& items); | ||
23 | |||
24 | private: | ||
25 | void OnColClick(wxListEvent& event); | ||
26 | void OnDPIChanged(wxDPIChangedEvent& event); | ||
27 | |||
28 | void DoSort(int col, bool ascending); | ||
29 | |||
30 | std::vector<std::unique_ptr<ItemState>> items_; | ||
31 | }; | ||
32 | |||
33 | #endif /* end of include guard: ITEMS_PANE_H_EB637EE3 */ | ||
diff --git a/src/log_dialog.cpp b/src/log_dialog.cpp new file mode 100644 index 0000000..3f0a8ad --- /dev/null +++ b/src/log_dialog.cpp | |||
@@ -0,0 +1,37 @@ | |||
1 | #include "log_dialog.h" | ||
2 | |||
3 | #include "logger.h" | ||
4 | |||
5 | wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent); | ||
6 | |||
7 | LogDialog::LogDialog(wxWindow* parent) | ||
8 | : wxDialog(parent, wxID_ANY, "Debug Log", wxDefaultPosition, wxDefaultSize, | ||
9 | wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { | ||
10 | SetSize(FromDIP(wxSize{512, 280})); | ||
11 | |||
12 | text_area_ = | ||
13 | new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, | ||
14 | wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP); | ||
15 | text_area_->SetValue(TrackerReadPastLog()); | ||
16 | |||
17 | wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); | ||
18 | top_sizer->Add(text_area_, | ||
19 | wxSizerFlags().DoubleBorder().Expand().Proportion(1)); | ||
20 | |||
21 | SetSizer(top_sizer); | ||
22 | |||
23 | Bind(LOG_MESSAGE, &LogDialog::OnLogMessage, this); | ||
24 | } | ||
25 | |||
26 | void LogDialog::LogMessage(const std::string& message) { | ||
27 | wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE); | ||
28 | event->SetString(message); | ||
29 | QueueEvent(event); | ||
30 | } | ||
31 | |||
32 | void LogDialog::OnLogMessage(wxCommandEvent& event) { | ||
33 | if (!text_area_->IsEmpty()) { | ||
34 | text_area_->AppendText("\n"); | ||
35 | } | ||
36 | text_area_->AppendText(event.GetString()); | ||
37 | } | ||
diff --git a/src/log_dialog.h b/src/log_dialog.h new file mode 100644 index 0000000..c29251f --- /dev/null +++ b/src/log_dialog.h | |||
@@ -0,0 +1,24 @@ | |||
1 | #ifndef LOG_DIALOG_H_EEFD45B6 | ||
2 | #define LOG_DIALOG_H_EEFD45B6 | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent); | ||
11 | |||
12 | class LogDialog : public wxDialog { | ||
13 | public: | ||
14 | explicit LogDialog(wxWindow* parent); | ||
15 | |||
16 | void LogMessage(const std::string& message); | ||
17 | |||
18 | private: | ||
19 | void OnLogMessage(wxCommandEvent& event); | ||
20 | |||
21 | wxTextCtrl* text_area_; | ||
22 | }; | ||
23 | |||
24 | #endif /* end of include guard: LOG_DIALOG_H_EEFD45B6 */ | ||
diff --git a/src/logger.cpp b/src/logger.cpp index 09fc331..8a08b58 100644 --- a/src/logger.cpp +++ b/src/logger.cpp | |||
@@ -3,8 +3,10 @@ | |||
3 | #include <chrono> | 3 | #include <chrono> |
4 | #include <fstream> | 4 | #include <fstream> |
5 | #include <mutex> | 5 | #include <mutex> |
6 | #include <sstream> | ||
6 | 7 | ||
7 | #include "global.h" | 8 | #include "global.h" |
9 | #include "log_dialog.h" | ||
8 | 10 | ||
9 | namespace { | 11 | namespace { |
10 | 12 | ||
@@ -14,19 +16,49 @@ class Logger { | |||
14 | 16 | ||
15 | void LogLine(const std::string& text) { | 17 | void LogLine(const std::string& text) { |
16 | std::lock_guard guard(file_mutex_); | 18 | std::lock_guard guard(file_mutex_); |
17 | logfile_ << "[" << std::chrono::system_clock::now() << "] " << text | 19 | std::ostringstream line; |
18 | << std::endl; | 20 | line << "[" << std::chrono::system_clock::now() << "] " << text; |
21 | |||
22 | logfile_ << line.str() << std::endl; | ||
19 | logfile_.flush(); | 23 | logfile_.flush(); |
24 | |||
25 | if (log_dialog_ != nullptr) { | ||
26 | log_dialog_->LogMessage(line.str()); | ||
27 | } | ||
28 | } | ||
29 | |||
30 | std::string GetContents() { | ||
31 | std::lock_guard guard(file_mutex_); | ||
32 | |||
33 | std::ifstream file_in(GetAbsolutePath("debug.log")); | ||
34 | std::ostringstream buffer; | ||
35 | buffer << file_in.rdbuf(); | ||
36 | |||
37 | return buffer.str(); | ||
38 | } | ||
39 | |||
40 | void SetLogDialog(LogDialog* log_dialog) { | ||
41 | std::lock_guard guard(file_mutex_); | ||
42 | log_dialog_ = log_dialog; | ||
20 | } | 43 | } |
21 | 44 | ||
22 | private: | 45 | private: |
23 | std::ofstream logfile_; | 46 | std::ofstream logfile_; |
24 | std::mutex file_mutex_; | 47 | std::mutex file_mutex_; |
48 | LogDialog* log_dialog_ = nullptr; | ||
25 | }; | 49 | }; |
26 | 50 | ||
51 | Logger& GetLogger() { | ||
52 | static Logger* instance = new Logger(); | ||
53 | return *instance; | ||
54 | } | ||
55 | |||
27 | } // namespace | 56 | } // namespace |
28 | 57 | ||
29 | void TrackerLog(std::string text) { | 58 | void TrackerLog(std::string text) { GetLogger().LogLine(text); } |
30 | static Logger* instance = new Logger(); | 59 | |
31 | instance->LogLine(text); | 60 | std::string TrackerReadPastLog() { return GetLogger().GetContents(); } |
61 | |||
62 | void TrackerSetLogDialog(LogDialog* log_dialog) { | ||
63 | GetLogger().SetLogDialog(log_dialog); | ||
32 | } | 64 | } |
diff --git a/src/logger.h b/src/logger.h index a27839f..f669790 100644 --- a/src/logger.h +++ b/src/logger.h | |||
@@ -3,6 +3,12 @@ | |||
3 | 3 | ||
4 | #include <string> | 4 | #include <string> |
5 | 5 | ||
6 | class LogDialog; | ||
7 | |||
6 | void TrackerLog(std::string message); | 8 | void TrackerLog(std::string message); |
7 | 9 | ||
10 | std::string TrackerReadPastLog(); | ||
11 | |||
12 | void TrackerSetLogDialog(LogDialog* log_dialog); | ||
13 | |||
8 | #endif /* end of include guard: LOGGER_H_9BDD07EA */ | 14 | #endif /* end of include guard: LOGGER_H_9BDD07EA */ |
diff --git a/src/main.cpp b/src/main.cpp index 1d7cc9e..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
@@ -10,7 +10,7 @@ | |||
10 | 10 | ||
11 | class TrackerApp : public wxApp { | 11 | class TrackerApp : public wxApp { |
12 | public: | 12 | public: |
13 | virtual bool OnInit() { | 13 | virtual bool OnInit() override { |
14 | GetTrackerConfig().Load(); | 14 | GetTrackerConfig().Load(); |
15 | 15 | ||
16 | TrackerFrame *frame = new TrackerFrame(); | 16 | TrackerFrame *frame = new TrackerFrame(); |
diff --git a/src/network_set.cpp b/src/network_set.cpp index 2a9e12c..45911e3 100644 --- a/src/network_set.cpp +++ b/src/network_set.cpp | |||
@@ -4,9 +4,8 @@ void NetworkSet::Clear() { | |||
4 | network_by_item_.clear(); | 4 | network_by_item_.clear(); |
5 | } | 5 | } |
6 | 6 | ||
7 | void NetworkSet::AddLink(int id1, int id2) { | 7 | void NetworkSet::AddLink(int id1, int id2, bool two_way) { |
8 | if (id2 > id1) { | 8 | if (two_way && id2 > id1) { |
9 | // Make sure id1 < id2 | ||
10 | std::swap(id1, id2); | 9 | std::swap(id1, id2); |
11 | } | 10 | } |
12 | 11 | ||
@@ -17,13 +16,14 @@ void NetworkSet::AddLink(int id1, int id2) { | |||
17 | network_by_item_[id2] = {}; | 16 | network_by_item_[id2] = {}; |
18 | } | 17 | } |
19 | 18 | ||
20 | network_by_item_[id1].insert({id1, id2}); | 19 | NetworkNode node = {id1, id2, two_way}; |
21 | network_by_item_[id2].insert({id1, id2}); | 20 | |
21 | network_by_item_[id1].insert(node); | ||
22 | network_by_item_[id2].insert(node); | ||
22 | } | 23 | } |
23 | 24 | ||
24 | void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2) { | 25 | void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2, bool two_way) { |
25 | if (id2 > id1) { | 26 | if (two_way && id2 > id1) { |
26 | // Make sure id1 < id2 | ||
27 | std::swap(id1, id2); | 27 | std::swap(id1, id2); |
28 | } | 28 | } |
29 | 29 | ||
@@ -31,13 +31,22 @@ void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2) { | |||
31 | network_by_item_[network_id] = {}; | 31 | network_by_item_[network_id] = {}; |
32 | } | 32 | } |
33 | 33 | ||
34 | network_by_item_[network_id].insert({id1, id2}); | 34 | NetworkNode node = {id1, id2, two_way}; |
35 | |||
36 | network_by_item_[network_id].insert(node); | ||
35 | } | 37 | } |
36 | 38 | ||
37 | bool NetworkSet::IsItemInNetwork(int id) const { | 39 | bool NetworkSet::IsItemInNetwork(int id) const { |
38 | return network_by_item_.count(id); | 40 | return network_by_item_.count(id); |
39 | } | 41 | } |
40 | 42 | ||
41 | const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const { | 43 | const std::set<NetworkNode>& NetworkSet::GetNetworkGraph(int id) const { |
42 | return network_by_item_.at(id); | 44 | return network_by_item_.at(id); |
43 | } | 45 | } |
46 | |||
47 | bool NetworkNode::operator<(const NetworkNode& rhs) const { | ||
48 | if (entry != rhs.entry) return entry < rhs.entry; | ||
49 | if (exit != rhs.exit) return exit < rhs.exit; | ||
50 | if (two_way != rhs.two_way) return two_way < rhs.two_way; | ||
51 | return false; | ||
52 | } | ||
diff --git a/src/network_set.h b/src/network_set.h index cec3f39..0f72052 100644 --- a/src/network_set.h +++ b/src/network_set.h | |||
@@ -7,21 +7,29 @@ | |||
7 | #include <utility> | 7 | #include <utility> |
8 | #include <vector> | 8 | #include <vector> |
9 | 9 | ||
10 | struct NetworkNode { | ||
11 | int entry; | ||
12 | int exit; | ||
13 | bool two_way; | ||
14 | |||
15 | bool operator<(const NetworkNode& rhs) const; | ||
16 | }; | ||
17 | |||
10 | class NetworkSet { | 18 | class NetworkSet { |
11 | public: | 19 | public: |
12 | void Clear(); | 20 | void Clear(); |
13 | 21 | ||
14 | void AddLink(int id1, int id2); | 22 | void AddLink(int id1, int id2, bool two_way); |
15 | 23 | ||
16 | void AddLinkToNetwork(int network_id, int id1, int id2); | 24 | void AddLinkToNetwork(int network_id, int id1, int id2, bool two_way); |
17 | 25 | ||
18 | bool IsItemInNetwork(int id) const; | 26 | bool IsItemInNetwork(int id) const; |
19 | 27 | ||
20 | const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const; | 28 | const std::set<NetworkNode>& GetNetworkGraph(int id) const; |
21 | 29 | ||
22 | private: | 30 | private: |
23 | 31 | ||
24 | std::map<int, std::set<std::pair<int, int>>> network_by_item_; | 32 | std::map<int, std::set<NetworkNode>> network_by_item_; |
25 | }; | 33 | }; |
26 | 34 | ||
27 | #endif /* end of include guard: NETWORK_SET_H_3036B8E3 */ | 35 | #endif /* end of include guard: NETWORK_SET_H_3036B8E3 */ |
diff --git a/src/options_pane.cpp b/src/options_pane.cpp new file mode 100644 index 0000000..844e145 --- /dev/null +++ b/src/options_pane.cpp | |||
@@ -0,0 +1,71 @@ | |||
1 | #include "options_pane.h" | ||
2 | |||
3 | #include "ap_state.h" | ||
4 | |||
5 | namespace { | ||
6 | |||
7 | const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"}; | ||
8 | const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"}; | ||
9 | const char* kPanelShuffleLabels[] = {"None", "Rearrange"}; | ||
10 | const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2", | ||
11 | "Pilgrimage"}; | ||
12 | const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock", | ||
13 | "Individual", "Progressive"}; | ||
14 | |||
15 | void AddRow(wxDataViewListCtrl* list, const std::string& text) { | ||
16 | wxVector<wxVariant> data; | ||
17 | data.push_back(wxVariant{text + ": "}); | ||
18 | data.push_back(wxVariant{""}); | ||
19 | list->AppendItem(data); | ||
20 | } | ||
21 | |||
22 | } // namespace | ||
23 | |||
24 | OptionsPane::OptionsPane(wxWindow* parent) | ||
25 | : wxDataViewListCtrl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, | ||
26 | wxDV_ROW_LINES) { | ||
27 | AppendTextColumn("Name", wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE); | ||
28 | AppendTextColumn("Value"); | ||
29 | AddRow(this, "Shuffle Doors"); | ||
30 | AddRow(this, "Group Doors"); | ||
31 | AddRow(this, "Location Checks"); | ||
32 | AddRow(this, "Shuffle Colors"); | ||
33 | AddRow(this, "Shuffle Panels"); | ||
34 | AddRow(this, "Shuffle Paintings"); | ||
35 | AddRow(this, "Victory Condition"); | ||
36 | AddRow(this, "Early Color Hallways"); | ||
37 | AddRow(this, "Shuffle Postgame"); | ||
38 | AddRow(this, "Enable Pilgrimage"); | ||
39 | AddRow(this, "Pilgrimage Roof Access"); | ||
40 | AddRow(this, "Pilgrimage Paintings"); | ||
41 | AddRow(this, "Sunwarp Access"); | ||
42 | AddRow(this, "Shuffle Sunwarps"); | ||
43 | AddRow(this, "Mastery Achievements"); | ||
44 | AddRow(this, "Level 2 Requirement"); | ||
45 | } | ||
46 | |||
47 | void OptionsPane::OnConnect() { | ||
48 | SetTextValue(kDoorShuffleLabels[static_cast<size_t>(AP_GetDoorShuffleMode())], | ||
49 | 0, 1); | ||
50 | SetTextValue(AP_AreDoorsGrouped() ? "Yes" : "No", 1, 1); | ||
51 | SetTextValue( | ||
52 | kLocationChecksLabels[static_cast<size_t>(AP_GetLocationsChecks())], 2, | ||
53 | 1); | ||
54 | SetTextValue(AP_IsColorShuffle() ? "Yes" : "No", 3, 1); | ||
55 | SetTextValue( | ||
56 | kPanelShuffleLabels[static_cast<size_t>(AP_GetPanelShuffleMode())], 4, 1); | ||
57 | SetTextValue(AP_IsPaintingShuffle() ? "Yes" : "No", 5, 1); | ||
58 | SetTextValue( | ||
59 | kVictoryConditionLabels[static_cast<size_t>(AP_GetVictoryCondition())], 6, | ||
60 | 1); | ||
61 | SetTextValue(AP_HasEarlyColorHallways() ? "Yes" : "No", 7, 1); | ||
62 | SetTextValue(AP_IsPostgameShuffle() ? "Yes" : "No", 8, 1); | ||
63 | SetTextValue(AP_IsPilgrimageEnabled() ? "Yes" : "No", 9, 1); | ||
64 | SetTextValue(AP_DoesPilgrimageAllowRoofAccess() ? "Yes" : "No", 10, 1); | ||
65 | SetTextValue(AP_DoesPilgrimageAllowPaintings() ? "Yes" : "No", 11, 1); | ||
66 | SetTextValue(kSunwarpAccessLabels[static_cast<size_t>(AP_GetSunwarpAccess())], | ||
67 | 12, 1); | ||
68 | SetTextValue(AP_IsSunwarpShuffle() ? "Yes" : "No", 13, 1); | ||
69 | SetTextValue(std::to_string(AP_GetMasteryRequirement()), 14, 1); | ||
70 | SetTextValue(std::to_string(AP_GetLevel2Requirement()), 15, 1); | ||
71 | } | ||
diff --git a/src/options_pane.h b/src/options_pane.h new file mode 100644 index 0000000..e9df9f0 --- /dev/null +++ b/src/options_pane.h | |||
@@ -0,0 +1,19 @@ | |||
1 | #ifndef OPTIONS_PANE_H_026A0EC0 | ||
2 | #define OPTIONS_PANE_H_026A0EC0 | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | #include <wx/dataview.h> | ||
11 | |||
12 | class OptionsPane : public wxDataViewListCtrl { | ||
13 | public: | ||
14 | explicit OptionsPane(wxWindow* parent); | ||
15 | |||
16 | void OnConnect(); | ||
17 | }; | ||
18 | |||
19 | #endif /* end of include guard: OPTIONS_PANE_H_026A0EC0 */ | ||
diff --git a/src/paintings_pane.cpp b/src/paintings_pane.cpp new file mode 100644 index 0000000..bf5d71b --- /dev/null +++ b/src/paintings_pane.cpp | |||
@@ -0,0 +1,86 @@ | |||
1 | #include "paintings_pane.h" | ||
2 | |||
3 | #include <fmt/core.h> | ||
4 | #include <wx/dataview.h> | ||
5 | |||
6 | #include <map> | ||
7 | #include <set> | ||
8 | |||
9 | #include "ap_state.h" | ||
10 | #include "game_data.h" | ||
11 | #include "tracker_state.h" | ||
12 | |||
13 | namespace { | ||
14 | |||
15 | std::string GetPaintingDisplayName(const std::string& id) { | ||
16 | const PaintingExit& painting = GD_GetPaintingExit(GD_GetPaintingByName(id)); | ||
17 | const MapArea& map_area = GD_GetMapArea(painting.map_area); | ||
18 | |||
19 | return fmt::format("{} - {}", map_area.name, painting.display_name); | ||
20 | } | ||
21 | |||
22 | } // namespace | ||
23 | |||
24 | PaintingsPane::PaintingsPane(wxWindow* parent) : wxPanel(parent, wxID_ANY) { | ||
25 | wxStaticText* label = new wxStaticText( | ||
26 | this, wxID_ANY, "Shuffled paintings grouped by destination:"); | ||
27 | tree_ctrl_ = new wxDataViewTreeCtrl(this, wxID_ANY); | ||
28 | |||
29 | reveal_btn_ = new wxButton(this, wxID_ANY, "Reveal shuffled paintings"); | ||
30 | reveal_btn_->Disable(); | ||
31 | |||
32 | wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); | ||
33 | top_sizer->Add(label, wxSizerFlags().Border()); | ||
34 | top_sizer->Add(tree_ctrl_, wxSizerFlags().Expand().Proportion(1)); | ||
35 | top_sizer->Add(reveal_btn_, wxSizerFlags().Border().Expand()); | ||
36 | |||
37 | SetSizerAndFit(top_sizer); | ||
38 | |||
39 | tree_ctrl_->Bind(wxEVT_DATAVIEW_ITEM_START_EDITING, | ||
40 | &PaintingsPane::OnStartEditingCell, this); | ||
41 | reveal_btn_->Bind(wxEVT_BUTTON, &PaintingsPane::OnClickRevealPaintings, this); | ||
42 | } | ||
43 | |||
44 | void PaintingsPane::ResetIndicators() { | ||
45 | tree_ctrl_->DeleteAllItems(); | ||
46 | reveal_btn_->Enable(AP_IsPaintingShuffle()); | ||
47 | } | ||
48 | |||
49 | void PaintingsPane::UpdateIndicators(const std::vector<std::string>&) { | ||
50 | // TODO: Optimize this by using the paintings delta. | ||
51 | |||
52 | tree_ctrl_->DeleteAllItems(); | ||
53 | |||
54 | std::map<std::string, std::set<std::string>> grouped_paintings; | ||
55 | |||
56 | for (const auto& [from, to] : AP_GetPaintingMapping()) { | ||
57 | if (IsPaintingReachable(GD_GetPaintingByName(from)) && | ||
58 | AP_IsPaintingChecked(from)) { | ||
59 | grouped_paintings[GetPaintingDisplayName(to)].insert( | ||
60 | GetPaintingDisplayName(from)); | ||
61 | } | ||
62 | } | ||
63 | |||
64 | for (const auto& [to, froms] : grouped_paintings) { | ||
65 | wxDataViewItem tree_branch = | ||
66 | tree_ctrl_->AppendContainer(wxDataViewItem(0), to); | ||
67 | |||
68 | for (const std::string& from : froms) { | ||
69 | tree_ctrl_->AppendItem(tree_branch, from); | ||
70 | } | ||
71 | } | ||
72 | } | ||
73 | |||
74 | void PaintingsPane::OnClickRevealPaintings(wxCommandEvent& event) { | ||
75 | if (wxMessageBox("Clicking yes will reveal the mapping between all shuffled " | ||
76 | "paintings. This is usually considered a spoiler, and is " | ||
77 | "likely not allowed during competitions. This action is not " | ||
78 | "reversible. Are you sure you want to proceed?", | ||
79 | "Warning", wxYES_NO | wxICON_WARNING) == wxNO) { | ||
80 | return; | ||
81 | } | ||
82 | |||
83 | AP_RevealPaintings(); | ||
84 | } | ||
85 | |||
86 | void PaintingsPane::OnStartEditingCell(wxDataViewEvent& event) { event.Veto(); } | ||
diff --git a/src/paintings_pane.h b/src/paintings_pane.h new file mode 100644 index 0000000..1d14510 --- /dev/null +++ b/src/paintings_pane.h | |||
@@ -0,0 +1,28 @@ | |||
1 | #ifndef PAINTINGS_PANE_H_815370D2 | ||
2 | #define PAINTINGS_PANE_H_815370D2 | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | class wxDataViewEvent; | ||
11 | class wxDataViewTreeCtrl; | ||
12 | |||
13 | class PaintingsPane : public wxPanel { | ||
14 | public: | ||
15 | explicit PaintingsPane(wxWindow* parent); | ||
16 | |||
17 | void ResetIndicators(); | ||
18 | void UpdateIndicators(const std::vector<std::string>& paintings); | ||
19 | |||
20 | private: | ||
21 | void OnClickRevealPaintings(wxCommandEvent& event); | ||
22 | void OnStartEditingCell(wxDataViewEvent& event); | ||
23 | |||
24 | wxDataViewTreeCtrl* tree_ctrl_; | ||
25 | wxButton* reveal_btn_; | ||
26 | }; | ||
27 | |||
28 | #endif /* end of include guard: PAINTINGS_PANE_H_815370D2 */ | ||
diff --git a/src/report_popup.cpp b/src/report_popup.cpp new file mode 100644 index 0000000..703e87f --- /dev/null +++ b/src/report_popup.cpp | |||
@@ -0,0 +1,131 @@ | |||
1 | #include "report_popup.h" | ||
2 | |||
3 | #include <wx/dcbuffer.h> | ||
4 | |||
5 | #include <map> | ||
6 | #include <string> | ||
7 | |||
8 | #include "global.h" | ||
9 | #include "icons.h" | ||
10 | #include "tracker_state.h" | ||
11 | |||
12 | ReportPopup::ReportPopup(wxWindow* parent) | ||
13 | : wxScrolledCanvas(parent, wxID_ANY) { | ||
14 | SetBackgroundStyle(wxBG_STYLE_PAINT); | ||
15 | |||
16 | LoadIcons(); | ||
17 | |||
18 | // TODO: This is slow on high-DPI screens. | ||
19 | SetScrollRate(5, 5); | ||
20 | |||
21 | SetBackgroundColour(*wxBLACK); | ||
22 | Hide(); | ||
23 | |||
24 | Bind(wxEVT_PAINT, &ReportPopup::OnPaint, this); | ||
25 | Bind(wxEVT_DPI_CHANGED, &ReportPopup::OnDPIChanged, this); | ||
26 | } | ||
27 | |||
28 | void ReportPopup::SetDoorId(int door_id) { | ||
29 | door_id_ = door_id; | ||
30 | |||
31 | ResetIndicators(); | ||
32 | } | ||
33 | |||
34 | void ReportPopup::Reset() { | ||
35 | door_id_ = -1; | ||
36 | } | ||
37 | |||
38 | void ReportPopup::ResetIndicators() { | ||
39 | if (door_id_ == -1) { | ||
40 | return; | ||
41 | } | ||
42 | |||
43 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
44 | const std::map<std::string, bool>& report = GetDoorRequirements(door_id_); | ||
45 | |||
46 | wxMemoryDC mem_dc; | ||
47 | mem_dc.SetFont(the_font); | ||
48 | |||
49 | int acc_height = FromDIP(10); | ||
50 | int col_width = 0; | ||
51 | |||
52 | for (const auto& [text, obtained] : report) { | ||
53 | wxSize item_extent = mem_dc.GetTextExtent(text); | ||
54 | int item_height = | ||
55 | std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10); | ||
56 | acc_height += item_height; | ||
57 | |||
58 | if (item_extent.GetWidth() > col_width) { | ||
59 | col_width = item_extent.GetWidth(); | ||
60 | } | ||
61 | } | ||
62 | |||
63 | int item_width = col_width + FromDIP(10 + 32); | ||
64 | full_width_ = item_width + FromDIP(20); | ||
65 | full_height_ = acc_height; | ||
66 | |||
67 | Fit(); | ||
68 | SetVirtualSize(full_width_, full_height_); | ||
69 | |||
70 | UpdateIndicators(); | ||
71 | } | ||
72 | |||
73 | void ReportPopup::UpdateIndicators() { | ||
74 | if (door_id_ == -1) { | ||
75 | return; | ||
76 | } | ||
77 | |||
78 | wxFont the_font = GetFont().Scale(GetDPIScaleFactor()); | ||
79 | const std::map<std::string, bool>& report = GetDoorRequirements(door_id_); | ||
80 | |||
81 | rendered_ = wxBitmap(full_width_, full_height_); | ||
82 | |||
83 | wxMemoryDC mem_dc; | ||
84 | mem_dc.SelectObject(rendered_); | ||
85 | mem_dc.SetPen(*wxTRANSPARENT_PEN); | ||
86 | mem_dc.SetBrush(*wxBLACK_BRUSH); | ||
87 | mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_}); | ||
88 | |||
89 | mem_dc.SetFont(the_font); | ||
90 | |||
91 | int cur_height = FromDIP(10); | ||
92 | |||
93 | for (const auto& [text, obtained] : report) { | ||
94 | const wxBitmap* eye_ptr = obtained ? checked_eye_ : unchecked_eye_; | ||
95 | |||
96 | mem_dc.DrawBitmap(*eye_ptr, wxPoint{FromDIP(10), cur_height}); | ||
97 | |||
98 | mem_dc.SetTextForeground(obtained ? *wxWHITE : *wxRED); | ||
99 | wxSize item_extent = mem_dc.GetTextExtent(text); | ||
100 | mem_dc.DrawText( | ||
101 | text, wxPoint{FromDIP(10 + 32 + 10), | ||
102 | cur_height + | ||
103 | (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2}); | ||
104 | |||
105 | cur_height += FromDIP(10 + 32); | ||
106 | } | ||
107 | } | ||
108 | |||
109 | void ReportPopup::OnPaint(wxPaintEvent& event) { | ||
110 | if (door_id_ != -1) { | ||
111 | wxBufferedPaintDC dc(this); | ||
112 | PrepareDC(dc); | ||
113 | dc.DrawBitmap(rendered_, 0, 0); | ||
114 | } | ||
115 | |||
116 | event.Skip(); | ||
117 | } | ||
118 | |||
119 | void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) { | ||
120 | LoadIcons(); | ||
121 | ResetIndicators(); | ||
122 | |||
123 | event.Skip(); | ||
124 | } | ||
125 | |||
126 | void ReportPopup::LoadIcons() { | ||
127 | unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png", | ||
128 | FromDIP(wxSize{32, 32})); | ||
129 | checked_eye_ = | ||
130 | GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32})); | ||
131 | } | ||
diff --git a/src/report_popup.h b/src/report_popup.h new file mode 100644 index 0000000..bbb0bef --- /dev/null +++ b/src/report_popup.h | |||
@@ -0,0 +1,38 @@ | |||
1 | #ifndef REPORT_POPUP_H_E065BED4 | ||
2 | #define REPORT_POPUP_H_E065BED4 | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | class ReportPopup : public wxScrolledCanvas { | ||
11 | public: | ||
12 | explicit ReportPopup(wxWindow* parent); | ||
13 | |||
14 | void SetDoorId(int door_id); | ||
15 | |||
16 | void Reset(); | ||
17 | |||
18 | void ResetIndicators(); | ||
19 | void UpdateIndicators(); | ||
20 | |||
21 | private: | ||
22 | void OnPaint(wxPaintEvent& event); | ||
23 | void OnDPIChanged(wxDPIChangedEvent& event); | ||
24 | |||
25 | void LoadIcons(); | ||
26 | |||
27 | int door_id_ = -1; | ||
28 | |||
29 | const wxBitmap* unchecked_eye_; | ||
30 | const wxBitmap* checked_eye_; | ||
31 | |||
32 | int full_width_ = 0; | ||
33 | int full_height_ = 0; | ||
34 | |||
35 | wxBitmap rendered_; | ||
36 | }; | ||
37 | |||
38 | #endif /* end of include guard: REPORT_POPUP_H_E065BED4 */ | ||
diff --git a/src/settings_dialog.cpp b/src/settings_dialog.cpp index 0321b5a..95df577 100644 --- a/src/settings_dialog.cpp +++ b/src/settings_dialog.cpp | |||
@@ -3,30 +3,43 @@ | |||
3 | #include "tracker_config.h" | 3 | #include "tracker_config.h" |
4 | 4 | ||
5 | SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { | 5 | SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { |
6 | should_check_for_updates_box_ = new wxCheckBox( | 6 | wxStaticBoxSizer* main_box = |
7 | this, wxID_ANY, "Check for updates when the tracker opens"); | 7 | new wxStaticBoxSizer(wxVERTICAL, this, "General settings"); |
8 | |||
9 | should_check_for_updates_box_ = | ||
10 | new wxCheckBox(main_box->GetStaticBox(), wxID_ANY, | ||
11 | "Check for updates when the tracker opens"); | ||
8 | hybrid_areas_box_ = new wxCheckBox( | 12 | hybrid_areas_box_ = new wxCheckBox( |
9 | this, wxID_ANY, | 13 | main_box->GetStaticBox(), wxID_ANY, |
10 | "Use two colors to show that an area has partial availability"); | 14 | "Use two colors to show that an area has partial availability"); |
11 | show_hunt_panels_box_ = new wxCheckBox(this, wxID_ANY, "Show hunt panels"); | 15 | track_position_box_ = new wxCheckBox(main_box->GetStaticBox(), wxID_ANY, |
16 | "Track player position"); | ||
12 | 17 | ||
13 | should_check_for_updates_box_->SetValue( | 18 | should_check_for_updates_box_->SetValue( |
14 | GetTrackerConfig().should_check_for_updates); | 19 | GetTrackerConfig().should_check_for_updates); |
15 | hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); | 20 | hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); |
16 | show_hunt_panels_box_->SetValue(GetTrackerConfig().show_hunt_panels); | 21 | track_position_box_->SetValue(GetTrackerConfig().track_position); |
22 | |||
23 | main_box->Add(should_check_for_updates_box_, wxSizerFlags().Border()); | ||
24 | main_box->AddSpacer(2); | ||
25 | main_box->Add(hybrid_areas_box_, wxSizerFlags().Border()); | ||
26 | main_box->AddSpacer(2); | ||
27 | main_box->Add(track_position_box_, wxSizerFlags().Border()); | ||
28 | |||
29 | const wxString visible_panels_choices[] = {"Only show locations", | ||
30 | "Show locations and hunt panels", | ||
31 | "Show all panels"}; | ||
32 | visible_panels_box_ = | ||
33 | new wxRadioBox(this, wxID_ANY, "Visible panels", wxDefaultPosition, | ||
34 | wxDefaultSize, 3, visible_panels_choices, 1); | ||
35 | visible_panels_box_->SetSelection( | ||
36 | static_cast<int>(GetTrackerConfig().visible_panels)); | ||
17 | 37 | ||
18 | wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); | 38 | wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); |
19 | 39 | form_sizer->Add(main_box, wxSizerFlags().Border().Expand()); | |
20 | form_sizer->Add(should_check_for_updates_box_, wxSizerFlags().HorzBorder()); | 40 | form_sizer->Add(visible_panels_box_, wxSizerFlags().Border().Expand()); |
21 | form_sizer->AddSpacer(2); | 41 | form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), |
22 | 42 | wxSizerFlags().Center().Border()); | |
23 | form_sizer->Add(hybrid_areas_box_, wxSizerFlags().HorzBorder()); | ||
24 | form_sizer->AddSpacer(2); | ||
25 | |||
26 | form_sizer->Add(show_hunt_panels_box_, wxSizerFlags().HorzBorder()); | ||
27 | form_sizer->AddSpacer(2); | ||
28 | |||
29 | form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center()); | ||
30 | 43 | ||
31 | SetSizerAndFit(form_sizer); | 44 | SetSizerAndFit(form_sizer); |
32 | 45 | ||
diff --git a/src/settings_dialog.h b/src/settings_dialog.h index d7c1ed3..c4dacfa 100644 --- a/src/settings_dialog.h +++ b/src/settings_dialog.h | |||
@@ -7,6 +7,10 @@ | |||
7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
8 | #endif | 8 | #endif |
9 | 9 | ||
10 | #include <wx/radiobox.h> | ||
11 | |||
12 | #include "tracker_config.h" | ||
13 | |||
10 | class SettingsDialog : public wxDialog { | 14 | class SettingsDialog : public wxDialog { |
11 | public: | 15 | public: |
12 | SettingsDialog(); | 16 | SettingsDialog(); |
@@ -15,12 +19,17 @@ class SettingsDialog : public wxDialog { | |||
15 | return should_check_for_updates_box_->GetValue(); | 19 | return should_check_for_updates_box_->GetValue(); |
16 | } | 20 | } |
17 | bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } | 21 | bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } |
18 | bool GetShowHuntPanels() const { return show_hunt_panels_box_->GetValue(); } | 22 | TrackerConfig::VisiblePanels GetVisiblePanels() const { |
23 | return static_cast<TrackerConfig::VisiblePanels>( | ||
24 | visible_panels_box_->GetSelection()); | ||
25 | } | ||
26 | bool GetTrackPosition() const { return track_position_box_->GetValue(); } | ||
19 | 27 | ||
20 | private: | 28 | private: |
21 | wxCheckBox* should_check_for_updates_box_; | 29 | wxCheckBox* should_check_for_updates_box_; |
22 | wxCheckBox* hybrid_areas_box_; | 30 | wxCheckBox* hybrid_areas_box_; |
23 | wxCheckBox* show_hunt_panels_box_; | 31 | wxRadioBox* visible_panels_box_; |
32 | wxCheckBox* track_position_box_; | ||
24 | }; | 33 | }; |
25 | 34 | ||
26 | #endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ | 35 | #endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ |
diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 5b3ff5f..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp | |||
@@ -9,31 +9,25 @@ | |||
9 | #include "ap_state.h" | 9 | #include "ap_state.h" |
10 | #include "game_data.h" | 10 | #include "game_data.h" |
11 | #include "global.h" | 11 | #include "global.h" |
12 | #include "report_popup.h" | ||
12 | #include "tracker_state.h" | 13 | #include "tracker_state.h" |
13 | 14 | ||
14 | constexpr int AREA_ACTUAL_SIZE = 21; | 15 | constexpr int AREA_ACTUAL_SIZE = 21; |
15 | constexpr int OWL_ACTUAL_SIZE = 32; | 16 | constexpr int OWL_ACTUAL_SIZE = 32; |
17 | constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11 | ||
18 | constexpr int PAINTING_EXIT_RADIUS = 6; | ||
16 | 19 | ||
17 | enum class ItemDrawType { kNone, kBox, kOwl }; | 20 | enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit }; |
18 | 21 | ||
19 | namespace { | 22 | namespace { |
20 | 23 | ||
21 | std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) { | 24 | wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) { |
22 | if (AP_IsSunwarpShuffle() && subway_item.sunwarp && | 25 | if (subway_item.painting) { |
23 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | 26 | return {subway_item.x, subway_item.y}; |
24 | int sunwarp_index = subway_item.sunwarp->dots - 1; | 27 | } else { |
25 | if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) { | 28 | return {subway_item.x + AREA_ACTUAL_SIZE / 2, |
26 | sunwarp_index += 6; | 29 | subway_item.y + AREA_ACTUAL_SIZE / 2}; |
27 | } | ||
28 | |||
29 | for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) { | ||
30 | if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) { | ||
31 | return GD_GetSunwarpDoors().at(mapping.dots - 1); | ||
32 | } | ||
33 | } | ||
34 | } | 30 | } |
35 | |||
36 | return subway_item.door; | ||
37 | } | 31 | } |
38 | 32 | ||
39 | } // namespace | 33 | } // namespace |
@@ -53,14 +47,6 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | |||
53 | return; | 47 | return; |
54 | } | 48 | } |
55 | 49 | ||
56 | unchecked_eye_ = | ||
57 | wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), | ||
58 | wxBITMAP_TYPE_PNG) | ||
59 | .Scale(32, 32)); | ||
60 | checked_eye_ = wxBitmap( | ||
61 | wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG) | ||
62 | .Scale(32, 32)); | ||
63 | |||
64 | tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( | 50 | tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( |
65 | quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), | 51 | quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), |
66 | static_cast<float>(map_image_.GetHeight())}); | 52 | static_cast<float>(map_image_.GetHeight())}); |
@@ -79,50 +65,72 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | |||
79 | Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); | 65 | Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); |
80 | Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); | 66 | Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); |
81 | 67 | ||
82 | zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15}); | 68 | zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15})); |
83 | zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); | 69 | zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); |
84 | 70 | ||
85 | help_button_ = new wxButton(this, wxID_ANY, "Help"); | 71 | help_button_ = new wxButton(this, wxID_ANY, "Help"); |
86 | help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); | 72 | help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); |
87 | SetUpHelpButton(); | 73 | SetUpHelpButton(); |
74 | |||
75 | report_popup_ = new ReportPopup(this); | ||
88 | } | 76 | } |
89 | 77 | ||
90 | void SubwayMap::OnConnect() { | 78 | void SubwayMap::OnConnect() { |
91 | networks_.Clear(); | 79 | networks_.Clear(); |
92 | 80 | ||
93 | std::map<std::string, std::vector<int>> tagged; | 81 | std::map<std::string, std::vector<int>> tagged; |
82 | std::map<std::string, std::vector<int>> entrances; | ||
83 | std::map<std::string, std::vector<int>> exits; | ||
94 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | 84 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { |
95 | if (AP_HasEarlyColorHallways() && | 85 | if (AP_HasEarlyColorHallways() && |
96 | subway_item.special == "starting_room_paintings") { | 86 | subway_item.special == "early_color_hallways") { |
97 | tagged["early_ch"].push_back(subway_item.id); | 87 | entrances["early_ch"].push_back(subway_item.id); |
98 | } | 88 | } |
99 | 89 | ||
100 | if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { | 90 | if (AP_IsPaintingShuffle() && subway_item.painting) { |
101 | continue; | 91 | continue; |
102 | } | 92 | } |
103 | 93 | ||
104 | for (const std::string &tag : subway_item.tags) { | 94 | for (const std::string &tag : subway_item.tags) { |
105 | tagged[tag].push_back(subway_item.id); | 95 | tagged[tag].push_back(subway_item.id); |
106 | } | 96 | } |
97 | for (const std::string &tag : subway_item.entrances) { | ||
98 | entrances[tag].push_back(subway_item.id); | ||
99 | } | ||
100 | for (const std::string &tag : subway_item.exits) { | ||
101 | exits[tag].push_back(subway_item.id); | ||
102 | } | ||
107 | 103 | ||
108 | if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && | 104 | if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) { |
109 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | ||
110 | std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots); | 105 | std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots); |
111 | tagged[tag].push_back(subway_item.id); | 106 | switch (subway_item.sunwarp->type) { |
107 | case SubwaySunwarpType::kEnter: | ||
108 | entrances[tag].push_back(subway_item.id); | ||
109 | break; | ||
110 | case SubwaySunwarpType::kExit: | ||
111 | exits[tag].push_back(subway_item.id); | ||
112 | break; | ||
113 | default: | ||
114 | break; | ||
115 | } | ||
112 | } | 116 | } |
113 | 117 | ||
114 | if (!AP_IsPilgrimageEnabled() && | 118 | if (!AP_IsPilgrimageEnabled()) { |
115 | (subway_item.special == "sun_painting" || | 119 | if (subway_item.special == "sun_painting") { |
116 | subway_item.special == "sun_painting_exit")) { | 120 | entrances["sun_painting"].push_back(subway_item.id); |
117 | tagged["sun_painting"].push_back(subway_item.id); | 121 | } else if (subway_item.special == "sun_painting_exit") { |
122 | exits["sun_painting"].push_back(subway_item.id); | ||
123 | } | ||
118 | } | 124 | } |
119 | } | 125 | } |
120 | 126 | ||
121 | if (AP_IsSunwarpShuffle()) { | 127 | if (AP_IsSunwarpShuffle()) { |
128 | sunwarp_mapping_ = AP_GetSunwarpMapping(); | ||
129 | |||
122 | SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal}; | 130 | SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal}; |
123 | int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp); | 131 | int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp); |
124 | 132 | ||
125 | for (const auto &[index, mapping] : AP_GetSunwarpMapping()) { | 133 | for (const auto &[index, mapping] : sunwarp_mapping_) { |
126 | std::string tag = fmt::format("sunwarp{}", mapping.dots); | 134 | std::string tag = fmt::format("sunwarp{}", mapping.dots); |
127 | 135 | ||
128 | SubwaySunwarp fromWarp; | 136 | SubwaySunwarp fromWarp; |
@@ -143,13 +151,14 @@ void SubwayMap::OnConnect() { | |||
143 | toWarp.type = SubwaySunwarpType::kExit; | 151 | toWarp.type = SubwaySunwarpType::kExit; |
144 | } | 152 | } |
145 | 153 | ||
146 | tagged[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); | 154 | entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); |
147 | tagged[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp)); | 155 | exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp)); |
148 | 156 | ||
149 | networks_.AddLinkToNetwork( | 157 | networks_.AddLinkToNetwork( |
150 | final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp), | 158 | final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp), |
151 | mapping.dots == 6 ? final_sunwarp_item | 159 | mapping.dots == 6 ? final_sunwarp_item |
152 | : GD_GetSubwayItemForSunwarp(toWarp)); | 160 | : GD_GetSubwayItemForSunwarp(toWarp), |
161 | false); | ||
153 | } | 162 | } |
154 | } | 163 | } |
155 | 164 | ||
@@ -159,40 +168,61 @@ void SubwayMap::OnConnect() { | |||
159 | tag_it1++) { | 168 | tag_it1++) { |
160 | for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); | 169 | for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); |
161 | tag_it2++) { | 170 | tag_it2++) { |
162 | networks_.AddLink(*tag_it1, *tag_it2); | 171 | // two links because tags are bi-directional |
172 | networks_.AddLink(*tag_it1, *tag_it2, true); | ||
173 | } | ||
174 | } | ||
175 | } | ||
176 | |||
177 | for (const auto &[tag, items] : entrances) { | ||
178 | if (!exits.contains(tag)) continue; | ||
179 | for (auto exit : exits[tag]) { | ||
180 | for (auto entrance : items) { | ||
181 | networks_.AddLink(entrance, exit, false); | ||
163 | } | 182 | } |
164 | } | 183 | } |
165 | } | 184 | } |
166 | 185 | ||
167 | checked_paintings_.clear(); | 186 | checked_paintings_.clear(); |
187 | |||
188 | UpdateIndicators(); | ||
168 | } | 189 | } |
169 | 190 | ||
170 | void SubwayMap::UpdateIndicators() { | 191 | void SubwayMap::UpdateIndicators() { |
192 | if (AP_IsSunwarpShuffle()) { | ||
193 | sunwarp_mapping_ = AP_GetSunwarpMapping(); | ||
194 | } | ||
195 | |||
171 | if (AP_IsPaintingShuffle()) { | 196 | if (AP_IsPaintingShuffle()) { |
172 | for (const std::string &painting_id : AP_GetCheckedPaintings()) { | 197 | std::map<std::string, std::string> painting_mapping = |
198 | AP_GetPaintingMapping(); | ||
199 | std::set<std::string> remote_checked_paintings = AP_GetCheckedPaintings(); | ||
200 | |||
201 | for (const std::string &painting_id : remote_checked_paintings) { | ||
173 | if (!checked_paintings_.count(painting_id)) { | 202 | if (!checked_paintings_.count(painting_id)) { |
174 | checked_paintings_.insert(painting_id); | 203 | checked_paintings_.insert(painting_id); |
175 | 204 | ||
176 | if (AP_GetPaintingMapping().count(painting_id)) { | 205 | if (painting_mapping.count(painting_id)) { |
177 | std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id); | 206 | std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id); |
178 | std::optional<int> to_id = GD_GetSubwayItemForPainting( | 207 | std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id)); |
179 | AP_GetPaintingMapping().at(painting_id)); | ||
180 | 208 | ||
181 | if (from_id && to_id) { | 209 | if (from_id && to_id) { |
182 | networks_.AddLink(*from_id, *to_id); | 210 | networks_.AddLink(*from_id, *to_id, false); |
183 | } | 211 | } |
184 | } | 212 | } |
185 | } | 213 | } |
186 | } | 214 | } |
187 | } | 215 | } |
188 | 216 | ||
217 | report_popup_->UpdateIndicators(); | ||
218 | |||
189 | Redraw(); | 219 | Redraw(); |
190 | } | 220 | } |
191 | 221 | ||
192 | void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, | 222 | void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, |
193 | SubwaySunwarp to_sunwarp) { | 223 | SubwaySunwarp to_sunwarp) { |
194 | networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), | 224 | networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), |
195 | GD_GetSubwayItemForSunwarp(to_sunwarp)); | 225 | GD_GetSubwayItemForSunwarp(to_sunwarp), false); |
196 | } | 226 | } |
197 | 227 | ||
198 | void SubwayMap::Zoom(bool in) { | 228 | void SubwayMap::Zoom(bool in) { |
@@ -239,6 +269,9 @@ void SubwayMap::OnPaint(wxPaintEvent &event) { | |||
239 | SetZoomPos({zoom_x_, zoom_y_}); | 269 | SetZoomPos({zoom_x_, zoom_y_}); |
240 | 270 | ||
241 | SetUpHelpButton(); | 271 | SetUpHelpButton(); |
272 | |||
273 | zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord, | ||
274 | wxDefaultCoord, wxSIZE_AUTO); | ||
242 | } | 275 | } |
243 | 276 | ||
244 | wxBufferedPaintDC dc(this); | 277 | wxBufferedPaintDC dc(this); |
@@ -294,79 +327,15 @@ void SubwayMap::OnPaint(wxPaintEvent &event) { | |||
294 | } | 327 | } |
295 | 328 | ||
296 | if (hovered_item_) { | 329 | if (hovered_item_) { |
297 | // Note that these requirements are duplicated on OnMouseClick so that it | ||
298 | // knows when an item has a hover effect. | ||
299 | const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_); | ||
300 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); | ||
301 | |||
302 | if (subway_door && !GetDoorRequirements(*subway_door).empty()) { | ||
303 | const std::map<std::string, bool> &report = | ||
304 | GetDoorRequirements(*subway_door); | ||
305 | |||
306 | int acc_height = 10; | ||
307 | int col_width = 0; | ||
308 | |||
309 | for (const auto &[text, obtained] : report) { | ||
310 | wxSize item_extent = dc.GetTextExtent(text); | ||
311 | int item_height = std::max(32, item_extent.GetHeight()) + 10; | ||
312 | acc_height += item_height; | ||
313 | |||
314 | if (item_extent.GetWidth() > col_width) { | ||
315 | col_width = item_extent.GetWidth(); | ||
316 | } | ||
317 | } | ||
318 | |||
319 | int item_width = col_width + 10 + 32; | ||
320 | int full_width = item_width + 20; | ||
321 | |||
322 | wxPoint popup_pos = | ||
323 | MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2, | ||
324 | subway_item.y + AREA_ACTUAL_SIZE / 2}); | ||
325 | |||
326 | if (popup_pos.x + full_width > GetSize().GetWidth()) { | ||
327 | popup_pos.x = GetSize().GetWidth() - full_width; | ||
328 | } | ||
329 | if (popup_pos.y + acc_height > GetSize().GetHeight()) { | ||
330 | popup_pos.y = GetSize().GetHeight() - acc_height; | ||
331 | } | ||
332 | |||
333 | dc.SetPen(*wxTRANSPARENT_PEN); | ||
334 | dc.SetBrush(*wxBLACK_BRUSH); | ||
335 | dc.DrawRectangle(popup_pos, {full_width, acc_height}); | ||
336 | |||
337 | dc.SetFont(GetFont()); | ||
338 | |||
339 | int cur_height = 10; | ||
340 | |||
341 | for (const auto &[text, obtained] : report) { | ||
342 | wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_; | ||
343 | |||
344 | dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height}); | ||
345 | |||
346 | dc.SetTextForeground(obtained ? *wxWHITE : *wxRED); | ||
347 | wxSize item_extent = dc.GetTextExtent(text); | ||
348 | dc.DrawText( | ||
349 | text, | ||
350 | popup_pos + | ||
351 | wxPoint{10 + 32 + 10, | ||
352 | cur_height + (32 - dc.GetFontMetrics().height) / 2}); | ||
353 | |||
354 | cur_height += 10 + 32; | ||
355 | } | ||
356 | } | ||
357 | |||
358 | if (networks_.IsItemInNetwork(*hovered_item_)) { | 330 | if (networks_.IsItemInNetwork(*hovered_item_)) { |
359 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | 331 | dc.SetBrush(*wxTRANSPARENT_BRUSH); |
360 | 332 | ||
361 | for (const auto &[item_id1, item_id2] : | 333 | for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) { |
362 | networks_.GetNetworkGraph(*hovered_item_)) { | 334 | const SubwayItem &item1 = GD_GetSubwayItem(node.entry); |
363 | const SubwayItem &item1 = GD_GetSubwayItem(item_id1); | 335 | const SubwayItem &item2 = GD_GetSubwayItem(node.exit); |
364 | const SubwayItem &item2 = GD_GetSubwayItem(item_id2); | ||
365 | 336 | ||
366 | wxPoint item1_pos = MapPosToRenderPos( | 337 | wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1)); |
367 | {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); | 338 | wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2)); |
368 | wxPoint item2_pos = MapPosToRenderPos( | ||
369 | {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2}); | ||
370 | 339 | ||
371 | int left = std::min(item1_pos.x, item2_pos.x); | 340 | int left = std::min(item1_pos.x, item2_pos.x); |
372 | int top = std::min(item1_pos.y, item2_pos.y); | 341 | int top = std::min(item1_pos.y, item2_pos.y); |
@@ -381,6 +350,12 @@ void SubwayMap::OnPaint(wxPaintEvent &event) { | |||
381 | dc.DrawLine(item1_pos, item2_pos); | 350 | dc.DrawLine(item1_pos, item2_pos); |
382 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | 351 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); |
383 | dc.DrawLine(item1_pos, item2_pos); | 352 | dc.DrawLine(item1_pos, item2_pos); |
353 | if (!node.two_way) { | ||
354 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2)); | ||
355 | dc.SetBrush(*wxCYAN_BRUSH); | ||
356 | dc.DrawCircle(item2_pos, 4); | ||
357 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
358 | } | ||
384 | } else { | 359 | } else { |
385 | int ellipse_x; | 360 | int ellipse_x; |
386 | int ellipse_y; | 361 | int ellipse_y; |
@@ -423,6 +398,12 @@ void SubwayMap::OnPaint(wxPaintEvent &event) { | |||
423 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | 398 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); |
424 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, | 399 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, |
425 | halfheight * 2, start, end); | 400 | halfheight * 2, start, end); |
401 | if (!node.two_way) { | ||
402 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2)); | ||
403 | dc.SetBrush(*wxCYAN_BRUSH); | ||
404 | dc.DrawCircle(item2_pos, 4); | ||
405 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
406 | } | ||
426 | } | 407 | } |
427 | } | 408 | } |
428 | } | 409 | } |
@@ -443,9 +424,7 @@ void SubwayMap::OnMouseMove(wxMouseEvent &event) { | |||
443 | } | 424 | } |
444 | 425 | ||
445 | if (!sticky_hover_ && actual_hover_ != hovered_item_) { | 426 | if (!sticky_hover_ && actual_hover_ != hovered_item_) { |
446 | hovered_item_ = actual_hover_; | 427 | EvaluateHover(); |
447 | |||
448 | Refresh(); | ||
449 | } | 428 | } |
450 | 429 | ||
451 | if (scroll_mode_) { | 430 | if (scroll_mode_) { |
@@ -487,13 +466,11 @@ void SubwayMap::OnMouseClick(wxMouseEvent &event) { | |||
487 | if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || | 466 | if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || |
488 | networks_.IsItemInNetwork(*hovered_item_)) { | 467 | networks_.IsItemInNetwork(*hovered_item_)) { |
489 | if (actual_hover_ != hovered_item_) { | 468 | if (actual_hover_ != hovered_item_) { |
490 | hovered_item_ = actual_hover_; | 469 | EvaluateHover(); |
491 | 470 | ||
492 | if (!hovered_item_) { | 471 | if (!hovered_item_) { |
493 | sticky_hover_ = false; | 472 | sticky_hover_ = false; |
494 | } | 473 | } |
495 | |||
496 | Refresh(); | ||
497 | } else { | 474 | } else { |
498 | sticky_hover_ = !sticky_hover_; | 475 | sticky_hover_ = !sticky_hover_; |
499 | } | 476 | } |
@@ -543,11 +520,13 @@ void SubwayMap::OnClickHelp(wxCommandEvent &event) { | |||
543 | "corner.\nClick on a side of the screen to start panning. It will follow " | 520 | "corner.\nClick on a side of the screen to start panning. It will follow " |
544 | "your mouse. Click again to stop.\nHover over a door to see the " | 521 | "your mouse. Click again to stop.\nHover over a door to see the " |
545 | "requirements to open it.\nHover over a warp or active painting to see " | 522 | "requirements to open it.\nHover over a warp or active painting to see " |
546 | "what it is connected to.\nIn painting shuffle, paintings that have not " | 523 | "what it is connected to.\nFor one-way connections, there will be a " |
547 | "yet been checked will not show their connections.\nA green shaded owl " | 524 | "circle at the exit.\nCircles represent paintings.\nA red circle means " |
548 | "means that there is a painting entrance there.\nA red shaded owl means " | 525 | "that the painting is locked by a door.\nA blue circle means painting " |
549 | "that there are only painting exits there.\nClick on a door or " | 526 | "shuffle is enabled and the painting has not been checked yet.\nA black " |
550 | "warp to make the popup stick until you click again.", | 527 | "circle means the painting is not a warp.\nA green circle means that the " |
528 | "painting is a warp.\nPainting exits will be indicated with an X.\nClick " | ||
529 | "on a door or warp to make the popup stick until you click again.", | ||
551 | "Subway Map Help"); | 530 | "Subway Map Help"); |
552 | } | 531 | } |
553 | 532 | ||
@@ -559,23 +538,37 @@ void SubwayMap::Redraw() { | |||
559 | 538 | ||
560 | wxGCDC gcdc(dc); | 539 | wxGCDC gcdc(dc); |
561 | 540 | ||
541 | std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping(); | ||
542 | |||
562 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | 543 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { |
563 | ItemDrawType draw_type = ItemDrawType::kNone; | 544 | ItemDrawType draw_type = ItemDrawType::kNone; |
564 | const wxBrush *brush_color = wxGREY_BRUSH; | 545 | const wxBrush *brush_color = wxGREY_BRUSH; |
565 | std::optional<wxColour> shade_color; | ||
566 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); | 546 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); |
567 | 547 | ||
568 | if (AP_HasEarlyColorHallways() && | 548 | if (AP_HasEarlyColorHallways() && |
569 | subway_item.special == "starting_room_paintings") { | 549 | subway_item.special == "early_color_hallways") { |
570 | draw_type = ItemDrawType::kOwl; | 550 | draw_type = ItemDrawType::kOwl; |
571 | shade_color = wxColour(0, 255, 0, 128); | 551 | brush_color = wxGREEN_BRUSH; |
552 | } else if (subway_item.special == "starting_room_overhead") { | ||
553 | // Do not draw. | ||
554 | } else if (AP_IsColorShuffle() && subway_item.special && | ||
555 | subway_item.special->starts_with("color_")) { | ||
556 | std::string color_name = subway_item.special->substr(6); | ||
557 | LingoColor lingo_color = GetLingoColorForString(color_name); | ||
558 | int color_item_id = GD_GetItemIdForColor(lingo_color); | ||
559 | |||
560 | draw_type = ItemDrawType::kBox; | ||
561 | if (AP_HasItemSafe(color_item_id)) { | ||
562 | brush_color = wxGREEN_BRUSH; | ||
563 | } else { | ||
564 | brush_color = wxRED_BRUSH; | ||
565 | } | ||
572 | } else if (subway_item.special == "sun_painting") { | 566 | } else if (subway_item.special == "sun_painting") { |
573 | if (!AP_IsPilgrimageEnabled()) { | 567 | if (!AP_IsPilgrimageEnabled()) { |
568 | draw_type = ItemDrawType::kOwl; | ||
574 | if (IsDoorOpen(*subway_item.door)) { | 569 | if (IsDoorOpen(*subway_item.door)) { |
575 | draw_type = ItemDrawType::kOwl; | 570 | brush_color = wxGREEN_BRUSH; |
576 | shade_color = wxColour(0, 255, 0, 128); | ||
577 | } else { | 571 | } else { |
578 | draw_type = ItemDrawType::kBox; | ||
579 | brush_color = wxRED_BRUSH; | 572 | brush_color = wxRED_BRUSH; |
580 | } | 573 | } |
581 | } | 574 | } |
@@ -589,41 +582,28 @@ void SubwayMap::Redraw() { | |||
589 | } else { | 582 | } else { |
590 | brush_color = wxRED_BRUSH; | 583 | brush_color = wxRED_BRUSH; |
591 | } | 584 | } |
592 | } else if (!subway_item.paintings.empty()) { | 585 | } else if (subway_item.painting) { |
593 | if (AP_IsPaintingShuffle()) { | 586 | if (subway_door && !IsDoorOpen(*subway_door)) { |
594 | bool has_checked_painting = false; | 587 | draw_type = ItemDrawType::kOwl; |
595 | bool has_unchecked_painting = false; | 588 | brush_color = wxRED_BRUSH; |
596 | bool has_mapped_painting = false; | 589 | } else if (AP_IsPaintingShuffle()) { |
597 | bool has_codomain_painting = false; | 590 | if (!checked_paintings_.count(*subway_item.painting)) { |
598 | 591 | draw_type = ItemDrawType::kOwl; | |
599 | for (const std::string &painting_id : subway_item.paintings) { | 592 | brush_color = wxBLUE_BRUSH; |
600 | if (checked_paintings_.count(painting_id)) { | 593 | } else if (painting_mapping.count(*subway_item.painting)) { |
601 | has_checked_painting = true; | 594 | draw_type = ItemDrawType::kOwl; |
602 | 595 | brush_color = wxGREEN_BRUSH; | |
603 | if (AP_GetPaintingMapping().count(painting_id)) { | 596 | } else if (AP_IsPaintingMappedTo(*subway_item.painting)) { |
604 | has_mapped_painting = true; | 597 | draw_type = ItemDrawType::kOwlExit; |
605 | } else if (AP_IsPaintingMappedTo(painting_id)) { | 598 | brush_color = wxGREEN_BRUSH; |
606 | has_codomain_painting = true; | ||
607 | } | ||
608 | } else { | ||
609 | has_unchecked_painting = true; | ||
610 | } | ||
611 | } | 599 | } |
612 | 600 | } else if (subway_item.HasWarps()) { | |
613 | if (has_unchecked_painting || has_mapped_painting || | 601 | brush_color = wxGREEN_BRUSH; |
614 | has_codomain_painting) { | 602 | if (!subway_item.exits.empty()) { |
603 | draw_type = ItemDrawType::kOwlExit; | ||
604 | } else { | ||
615 | draw_type = ItemDrawType::kOwl; | 605 | draw_type = ItemDrawType::kOwl; |
616 | |||
617 | if (has_checked_painting) { | ||
618 | if (has_mapped_painting) { | ||
619 | shade_color = wxColour(0, 255, 0, 128); | ||
620 | } else { | ||
621 | shade_color = wxColour(255, 0, 0, 128); | ||
622 | } | ||
623 | } | ||
624 | } | 606 | } |
625 | } else if (!subway_item.tags.empty()) { | ||
626 | draw_type = ItemDrawType::kOwl; | ||
627 | } | 607 | } |
628 | } else if (subway_door) { | 608 | } else if (subway_door) { |
629 | draw_type = ItemDrawType::kBox; | 609 | draw_type = ItemDrawType::kBox; |
@@ -643,21 +623,40 @@ void SubwayMap::Redraw() { | |||
643 | if (draw_type == ItemDrawType::kBox) { | 623 | if (draw_type == ItemDrawType::kBox) { |
644 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); | 624 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); |
645 | gcdc.SetBrush(*brush_color); | 625 | gcdc.SetBrush(*brush_color); |
646 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | 626 | |
647 | } else if (draw_type == ItemDrawType::kOwl) { | 627 | if (subway_item.tilted) { |
648 | wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( | 628 | constexpr int AREA_TILTED_SIDE = |
649 | real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); | 629 | static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237); |
650 | gcdc.DrawBitmap(owl_bitmap, real_area_pos); | 630 | const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0}, |
651 | 631 | {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE}, | |
652 | if (shade_color) { | 632 | {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE}, |
653 | gcdc.SetBrush(wxBrush(*shade_color)); | 633 | {0, AREA_TILTED_SIDE}}; |
634 | gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y); | ||
635 | } else { | ||
654 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | 636 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); |
655 | } | 637 | } |
638 | } else if (draw_type == ItemDrawType::kOwl || draw_type == ItemDrawType::kOwlExit) { | ||
639 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); | ||
640 | gcdc.SetBrush(*brush_color); | ||
641 | gcdc.DrawCircle(real_area_pos, PAINTING_RADIUS); | ||
642 | |||
643 | if (draw_type == ItemDrawType::kOwlExit) { | ||
644 | gcdc.DrawLine(subway_item.x - PAINTING_EXIT_RADIUS, | ||
645 | subway_item.y - PAINTING_EXIT_RADIUS, | ||
646 | subway_item.x + PAINTING_EXIT_RADIUS, | ||
647 | subway_item.y + PAINTING_EXIT_RADIUS); | ||
648 | gcdc.DrawLine(subway_item.x + PAINTING_EXIT_RADIUS, | ||
649 | subway_item.y - PAINTING_EXIT_RADIUS, | ||
650 | subway_item.x - PAINTING_EXIT_RADIUS, | ||
651 | subway_item.y + PAINTING_EXIT_RADIUS); | ||
652 | } | ||
656 | } | 653 | } |
657 | } | 654 | } |
658 | } | 655 | } |
659 | 656 | ||
660 | void SubwayMap::SetUpHelpButton() { | 657 | void SubwayMap::SetUpHelpButton() { |
658 | help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord, | ||
659 | wxDefaultCoord, wxSIZE_AUTO); | ||
661 | help_button_->SetPosition({ | 660 | help_button_->SetPosition({ |
662 | GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, | 661 | GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, |
663 | 15, | 662 | 15, |
@@ -693,6 +692,51 @@ void SubwayMap::EvaluateScroll(wxPoint pos) { | |||
693 | SetScrollSpeed(scroll_x, scroll_y); | 692 | SetScrollSpeed(scroll_x, scroll_y); |
694 | } | 693 | } |
695 | 694 | ||
695 | void SubwayMap::EvaluateHover() { | ||
696 | hovered_item_ = actual_hover_; | ||
697 | |||
698 | if (hovered_item_) { | ||
699 | // Note that these requirements are duplicated on OnMouseClick so that it | ||
700 | // knows when an item has a hover effect. | ||
701 | const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_); | ||
702 | std::optional<int> subway_door = GetRealSubwayDoor(subway_item); | ||
703 | |||
704 | if (subway_door && !GetDoorRequirements(*subway_door).empty()) { | ||
705 | report_popup_->SetDoorId(*subway_door); | ||
706 | |||
707 | wxPoint popupPos = | ||
708 | MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2, | ||
709 | subway_item.y + AREA_ACTUAL_SIZE / 2}); | ||
710 | |||
711 | report_popup_->SetClientSize( | ||
712 | report_popup_->GetVirtualSize().GetWidth(), | ||
713 | std::min(GetSize().GetHeight(), | ||
714 | report_popup_->GetVirtualSize().GetHeight())); | ||
715 | |||
716 | if (popupPos.x + report_popup_->GetSize().GetWidth() > | ||
717 | GetSize().GetWidth()) { | ||
718 | popupPos.x = GetSize().GetWidth() - report_popup_->GetSize().GetWidth(); | ||
719 | } | ||
720 | if (popupPos.y + report_popup_->GetSize().GetHeight() > | ||
721 | GetSize().GetHeight()) { | ||
722 | popupPos.y = | ||
723 | GetSize().GetHeight() - report_popup_->GetSize().GetHeight(); | ||
724 | } | ||
725 | report_popup_->SetPosition(popupPos); | ||
726 | |||
727 | report_popup_->Show(); | ||
728 | } else { | ||
729 | report_popup_->Reset(); | ||
730 | report_popup_->Hide(); | ||
731 | } | ||
732 | } else { | ||
733 | report_popup_->Reset(); | ||
734 | report_popup_->Hide(); | ||
735 | } | ||
736 | |||
737 | Refresh(); | ||
738 | } | ||
739 | |||
696 | wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { | 740 | wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { |
697 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | 741 | return {static_cast<int>(pos.x * render_width_ * zoom_ / |
698 | map_image_.GetSize().GetWidth() + | 742 | map_image_.GetSize().GetWidth() + |
@@ -762,8 +806,33 @@ void SubwayMap::SetZoom(double zoom, wxPoint static_point) { | |||
762 | zoom_slider_->SetValue((zoom - 1.0) / 0.25); | 806 | zoom_slider_->SetValue((zoom - 1.0) / 0.25); |
763 | } | 807 | } |
764 | 808 | ||
809 | std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) { | ||
810 | if (AP_IsSunwarpShuffle() && subway_item.sunwarp && | ||
811 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | ||
812 | int sunwarp_index = subway_item.sunwarp->dots - 1; | ||
813 | if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) { | ||
814 | sunwarp_index += 6; | ||
815 | } | ||
816 | |||
817 | for (const auto &[start_index, mapping] : sunwarp_mapping_) { | ||
818 | if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) { | ||
819 | return GD_GetSunwarpDoors().at(mapping.dots - 1); | ||
820 | } | ||
821 | } | ||
822 | } | ||
823 | |||
824 | return subway_item.door; | ||
825 | } | ||
826 | |||
765 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { | 827 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { |
766 | const SubwayItem &subway_item = GD_GetSubwayItem(id); | 828 | const SubwayItem &subway_item = GD_GetSubwayItem(id); |
767 | return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), | 829 | if (subway_item.painting) { |
768 | AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; | 830 | return {static_cast<float>(subway_item.x) - PAINTING_RADIUS, |
831 | static_cast<float>(subway_item.y) - PAINTING_RADIUS, | ||
832 | PAINTING_RADIUS * 2, PAINTING_RADIUS * 2}; | ||
833 | } else { | ||
834 | return {static_cast<float>(subway_item.x), | ||
835 | static_cast<float>(subway_item.y), AREA_ACTUAL_SIZE, | ||
836 | AREA_ACTUAL_SIZE}; | ||
837 | } | ||
769 | } | 838 | } |
diff --git a/src/subway_map.h b/src/subway_map.h index feee8ff..b04c2fd 100644 --- a/src/subway_map.h +++ b/src/subway_map.h | |||
@@ -15,9 +15,12 @@ | |||
15 | 15 | ||
16 | #include <quadtree/Quadtree.h> | 16 | #include <quadtree/Quadtree.h> |
17 | 17 | ||
18 | #include "ap_state.h" | ||
18 | #include "game_data.h" | 19 | #include "game_data.h" |
19 | #include "network_set.h" | 20 | #include "network_set.h" |
20 | 21 | ||
22 | class ReportPopup; | ||
23 | |||
21 | class SubwayMap : public wxPanel { | 24 | class SubwayMap : public wxPanel { |
22 | public: | 25 | public: |
23 | SubwayMap(wxWindow *parent); | 26 | SubwayMap(wxWindow *parent); |
@@ -45,15 +48,16 @@ class SubwayMap : public wxPanel { | |||
45 | wxPoint RenderPosToMapPos(wxPoint pos) const; | 48 | wxPoint RenderPosToMapPos(wxPoint pos) const; |
46 | 49 | ||
47 | void EvaluateScroll(wxPoint pos); | 50 | void EvaluateScroll(wxPoint pos); |
51 | void EvaluateHover(); | ||
48 | 52 | ||
49 | void SetZoomPos(wxPoint pos); | 53 | void SetZoomPos(wxPoint pos); |
50 | void SetScrollSpeed(int scroll_x, int scroll_y); | 54 | void SetScrollSpeed(int scroll_x, int scroll_y); |
51 | void SetZoom(double zoom, wxPoint static_point); | 55 | void SetZoom(double zoom, wxPoint static_point); |
52 | 56 | ||
57 | std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item); | ||
58 | |||
53 | wxImage map_image_; | 59 | wxImage map_image_; |
54 | wxImage owl_image_; | 60 | wxImage owl_image_; |
55 | wxBitmap unchecked_eye_; | ||
56 | wxBitmap checked_eye_; | ||
57 | 61 | ||
58 | wxBitmap rendered_; | 62 | wxBitmap rendered_; |
59 | int render_x_ = 0; | 63 | int render_x_ = 0; |
@@ -85,8 +89,13 @@ class SubwayMap : public wxPanel { | |||
85 | std::optional<int> actual_hover_; | 89 | std::optional<int> actual_hover_; |
86 | bool sticky_hover_ = false; | 90 | bool sticky_hover_ = false; |
87 | 91 | ||
92 | ReportPopup *report_popup_; | ||
93 | |||
88 | NetworkSet networks_; | 94 | NetworkSet networks_; |
89 | std::set<std::string> checked_paintings_; | 95 | std::set<std::string> checked_paintings_; |
96 | |||
97 | // Cached from APState. | ||
98 | std::map<int, SunwarpMapping> sunwarp_mapping_; | ||
90 | }; | 99 | }; |
91 | 100 | ||
92 | #endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */ | 101 | #endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */ |
diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 85164d5..da5d60a 100644 --- a/src/tracker_config.cpp +++ b/src/tracker_config.cpp | |||
@@ -16,7 +16,9 @@ void TrackerConfig::Load() { | |||
16 | asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>(); | 16 | asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>(); |
17 | should_check_for_updates = file["should_check_for_updates"].as<bool>(); | 17 | should_check_for_updates = file["should_check_for_updates"].as<bool>(); |
18 | hybrid_areas = file["hybrid_areas"].as<bool>(); | 18 | hybrid_areas = file["hybrid_areas"].as<bool>(); |
19 | show_hunt_panels = file["show_hunt_panels"].as<bool>(); | 19 | if (file["show_hunt_panels"] && file["show_hunt_panels"].as<bool>()) { |
20 | visible_panels = kHUNT_PANELS; | ||
21 | } | ||
20 | 22 | ||
21 | if (file["connection_history"]) { | 23 | if (file["connection_history"]) { |
22 | for (const auto& connection : file["connection_history"]) { | 24 | for (const auto& connection : file["connection_history"]) { |
@@ -27,6 +29,11 @@ void TrackerConfig::Load() { | |||
27 | }); | 29 | }); |
28 | } | 30 | } |
29 | } | 31 | } |
32 | |||
33 | ipc_address = file["ipc_address"].as<std::string>(); | ||
34 | track_position = file["track_position"].as<bool>(); | ||
35 | visible_panels = | ||
36 | static_cast<VisiblePanels>(file["visible_panels"].as<int>()); | ||
30 | } catch (const std::exception&) { | 37 | } catch (const std::exception&) { |
31 | // It's fine if the file can't be loaded. | 38 | // It's fine if the file can't be loaded. |
32 | } | 39 | } |
@@ -40,7 +47,6 @@ void TrackerConfig::Save() { | |||
40 | output["asked_to_check_for_updates"] = asked_to_check_for_updates; | 47 | output["asked_to_check_for_updates"] = asked_to_check_for_updates; |
41 | output["should_check_for_updates"] = should_check_for_updates; | 48 | output["should_check_for_updates"] = should_check_for_updates; |
42 | output["hybrid_areas"] = hybrid_areas; | 49 | output["hybrid_areas"] = hybrid_areas; |
43 | output["show_hunt_panels"] = show_hunt_panels; | ||
44 | 50 | ||
45 | output.remove("connection_history"); | 51 | output.remove("connection_history"); |
46 | for (const ConnectionDetails& details : connection_history) { | 52 | for (const ConnectionDetails& details : connection_history) { |
@@ -52,6 +58,10 @@ void TrackerConfig::Save() { | |||
52 | output["connection_history"].push_back(connection); | 58 | output["connection_history"].push_back(connection); |
53 | } | 59 | } |
54 | 60 | ||
61 | output["ipc_address"] = ipc_address; | ||
62 | output["track_position"] = track_position; | ||
63 | output["visible_panels"] = static_cast<int>(visible_panels); | ||
64 | |||
55 | std::ofstream filewriter(filename_); | 65 | std::ofstream filewriter(filename_); |
56 | filewriter << output; | 66 | filewriter << output; |
57 | } | 67 | } |
diff --git a/src/tracker_config.h b/src/tracker_config.h index a1a6c1d..df4105d 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h | |||
@@ -23,12 +23,20 @@ class TrackerConfig { | |||
23 | 23 | ||
24 | void Save(); | 24 | void Save(); |
25 | 25 | ||
26 | enum VisiblePanels { | ||
27 | kLOCATIONS_ONLY, | ||
28 | kHUNT_PANELS, | ||
29 | kALL_PANELS, | ||
30 | }; | ||
31 | |||
26 | ConnectionDetails connection_details; | 32 | ConnectionDetails connection_details; |
27 | bool asked_to_check_for_updates = false; | 33 | bool asked_to_check_for_updates = false; |
28 | bool should_check_for_updates = false; | 34 | bool should_check_for_updates = false; |
29 | bool hybrid_areas = false; | 35 | bool hybrid_areas = false; |
30 | bool show_hunt_panels = false; | ||
31 | std::deque<ConnectionDetails> connection_history; | 36 | std::deque<ConnectionDetails> connection_history; |
37 | std::string ipc_address; | ||
38 | bool track_position = true; | ||
39 | VisiblePanels visible_panels = kLOCATIONS_ONLY; | ||
32 | 40 | ||
33 | private: | 41 | private: |
34 | std::string filename_; | 42 | std::string filename_; |
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 3b6beda..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp | |||
@@ -1,37 +1,64 @@ | |||
1 | #include "tracker_frame.h" | 1 | #include "tracker_frame.h" |
2 | 2 | ||
3 | #include <fmt/core.h> | ||
3 | #include <wx/aboutdlg.h> | 4 | #include <wx/aboutdlg.h> |
4 | #include <wx/choicebk.h> | 5 | #include <wx/choicebk.h> |
5 | #include <wx/filedlg.h> | 6 | #include <wx/filedlg.h> |
6 | #include <wx/notebook.h> | 7 | #include <wx/notebook.h> |
8 | #include <wx/splitter.h> | ||
7 | #include <wx/stdpaths.h> | 9 | #include <wx/stdpaths.h> |
8 | #include <wx/webrequest.h> | 10 | #include <wx/webrequest.h> |
9 | 11 | ||
10 | #include <fmt/core.h> | 12 | #include <algorithm> |
11 | #include <nlohmann/json.hpp> | 13 | #include <nlohmann/json.hpp> |
12 | #include <sstream> | 14 | #include <sstream> |
13 | 15 | ||
14 | #include "achievements_pane.h" | 16 | #include "achievements_pane.h" |
15 | #include "ap_state.h" | 17 | #include "ap_state.h" |
16 | #include "connection_dialog.h" | 18 | #include "connection_dialog.h" |
19 | #include "ipc_dialog.h" | ||
20 | #include "ipc_state.h" | ||
21 | #include "items_pane.h" | ||
22 | #include "log_dialog.h" | ||
23 | #include "logger.h" | ||
24 | #include "options_pane.h" | ||
25 | #include "paintings_pane.h" | ||
17 | #include "settings_dialog.h" | 26 | #include "settings_dialog.h" |
18 | #include "subway_map.h" | 27 | #include "subway_map.h" |
19 | #include "tracker_config.h" | 28 | #include "tracker_config.h" |
20 | #include "tracker_panel.h" | 29 | #include "tracker_panel.h" |
21 | #include "version.h" | 30 | #include "version.h" |
22 | 31 | ||
32 | namespace { | ||
33 | |||
34 | std::string GetStatusMessage() { | ||
35 | std::string msg = AP_GetStatusMessage(); | ||
36 | |||
37 | std::optional<std::string> ipc_msg = IPC_GetStatusMessage(); | ||
38 | if (ipc_msg) { | ||
39 | msg += " "; | ||
40 | msg += *ipc_msg; | ||
41 | } | ||
42 | |||
43 | return msg; | ||
44 | } | ||
45 | |||
46 | } // namespace | ||
47 | |||
23 | enum TrackerFrameIds { | 48 | enum TrackerFrameIds { |
24 | ID_CONNECT = 1, | 49 | ID_AP_CONNECT = 1, |
25 | ID_CHECK_FOR_UPDATES = 2, | 50 | ID_CHECK_FOR_UPDATES = 2, |
26 | ID_SETTINGS = 3, | 51 | ID_SETTINGS = 3, |
27 | ID_ZOOM_IN = 4, | 52 | ID_ZOOM_IN = 4, |
28 | ID_ZOOM_OUT = 5, | 53 | ID_ZOOM_OUT = 5, |
29 | ID_OPEN_SAVE_FILE = 6, | 54 | ID_IPC_CONNECT = 7, |
55 | ID_LOG_DIALOG = 8, | ||
30 | }; | 56 | }; |
31 | 57 | ||
32 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); | 58 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); |
33 | wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); | 59 | wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent); |
34 | wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); | 60 | wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); |
61 | wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); | ||
35 | 62 | ||
36 | TrackerFrame::TrackerFrame() | 63 | TrackerFrame::TrackerFrame() |
37 | : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, | 64 | : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, |
@@ -39,16 +66,24 @@ TrackerFrame::TrackerFrame() | |||
39 | ::wxInitAllImageHandlers(); | 66 | ::wxInitAllImageHandlers(); |
40 | 67 | ||
41 | AP_SetTrackerFrame(this); | 68 | AP_SetTrackerFrame(this); |
69 | IPC_SetTrackerFrame(this); | ||
70 | |||
71 | SetTheIconCache(&icons_); | ||
72 | |||
73 | updater_ = std::make_unique<Updater>(this); | ||
74 | updater_->Cleanup(); | ||
42 | 75 | ||
43 | wxMenu *menuFile = new wxMenu(); | 76 | wxMenu *menuFile = new wxMenu(); |
44 | menuFile->Append(ID_CONNECT, "&Connect"); | 77 | menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago"); |
45 | menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O"); | 78 | menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo"); |
46 | menuFile->Append(ID_SETTINGS, "&Settings"); | 79 | menuFile->Append(ID_SETTINGS, "&Settings"); |
47 | menuFile->Append(wxID_EXIT); | 80 | menuFile->Append(wxID_EXIT); |
48 | 81 | ||
49 | wxMenu *menuView = new wxMenu(); | 82 | wxMenu *menuView = new wxMenu(); |
50 | zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+"); | 83 | zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+"); |
51 | zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--"); | 84 | zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--"); |
85 | menuView->AppendSeparator(); | ||
86 | menuView->Append(ID_LOG_DIALOG, "Show Log Window\tCtrl-L"); | ||
52 | 87 | ||
53 | zoom_in_menu_item_->Enable(false); | 88 | zoom_in_menu_item_->Enable(false); |
54 | zoom_out_menu_item_->Enable(false); | 89 | zoom_out_menu_item_->Enable(false); |
@@ -65,38 +100,53 @@ TrackerFrame::TrackerFrame() | |||
65 | SetMenuBar(menuBar); | 100 | SetMenuBar(menuBar); |
66 | 101 | ||
67 | CreateStatusBar(); | 102 | CreateStatusBar(); |
68 | SetStatusText("Not connected to Archipelago."); | ||
69 | 103 | ||
70 | Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); | 104 | Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); |
71 | Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); | 105 | Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); |
72 | Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT); | 106 | Bind(wxEVT_MENU, &TrackerFrame::OnApConnect, this, ID_AP_CONNECT); |
107 | Bind(wxEVT_MENU, &TrackerFrame::OnIpcConnect, this, ID_IPC_CONNECT); | ||
73 | Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); | 108 | Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); |
74 | Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, | 109 | Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, |
75 | ID_CHECK_FOR_UPDATES); | 110 | ID_CHECK_FOR_UPDATES); |
76 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); | 111 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); |
77 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); | 112 | Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); |
113 | Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG); | ||
78 | Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); | 114 | Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); |
79 | Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE); | 115 | Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged, |
116 | this); | ||
80 | Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); | 117 | Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); |
81 | Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); | 118 | Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); |
82 | Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); | 119 | Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); |
120 | Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this); | ||
121 | |||
122 | wxSize logicalSize = FromDIP(wxSize(1280, 728)); | ||
123 | |||
124 | splitter_window_ = new wxSplitterWindow(this, wxID_ANY); | ||
125 | splitter_window_->SetMinimumPaneSize(logicalSize.x / 5); | ||
126 | |||
127 | wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY); | ||
83 | 128 | ||
84 | wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); | ||
85 | achievements_pane_ = new AchievementsPane(choicebook); | 129 | achievements_pane_ = new AchievementsPane(choicebook); |
86 | choicebook->AddPage(achievements_pane_, "Achievements"); | 130 | choicebook->AddPage(achievements_pane_, "Achievements"); |
87 | 131 | ||
88 | notebook_ = new wxNotebook(this, wxID_ANY); | 132 | items_pane_ = new ItemsPane(choicebook); |
133 | choicebook->AddPage(items_pane_, "Items"); | ||
134 | |||
135 | options_pane_ = new OptionsPane(choicebook); | ||
136 | choicebook->AddPage(options_pane_, "Options"); | ||
137 | |||
138 | paintings_pane_ = new PaintingsPane(choicebook); | ||
139 | choicebook->AddPage(paintings_pane_, "Paintings"); | ||
140 | |||
141 | notebook_ = new wxNotebook(splitter_window_, wxID_ANY); | ||
89 | tracker_panel_ = new TrackerPanel(notebook_); | 142 | tracker_panel_ = new TrackerPanel(notebook_); |
90 | subway_map_ = new SubwayMap(notebook_); | 143 | subway_map_ = new SubwayMap(notebook_); |
91 | notebook_->AddPage(tracker_panel_, "Map"); | 144 | notebook_->AddPage(tracker_panel_, "Map"); |
92 | notebook_->AddPage(subway_map_, "Subway"); | 145 | notebook_->AddPage(subway_map_, "Subway"); |
93 | 146 | ||
94 | wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); | 147 | splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4); |
95 | top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1)); | ||
96 | top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3)); | ||
97 | 148 | ||
98 | SetSizerAndFit(top_sizer); | 149 | SetSize(logicalSize); |
99 | SetSize(1280, 728); | ||
100 | 150 | ||
101 | if (!GetTrackerConfig().asked_to_check_for_updates) { | 151 | if (!GetTrackerConfig().asked_to_check_for_updates) { |
102 | GetTrackerConfig().asked_to_check_for_updates = true; | 152 | GetTrackerConfig().asked_to_check_for_updates = true; |
@@ -113,23 +163,28 @@ TrackerFrame::TrackerFrame() | |||
113 | } | 163 | } |
114 | 164 | ||
115 | if (GetTrackerConfig().should_check_for_updates) { | 165 | if (GetTrackerConfig().should_check_for_updates) { |
116 | CheckForUpdates(/*manual=*/false); | 166 | updater_->CheckForUpdates(/*invisible=*/true); |
117 | } | 167 | } |
168 | |||
169 | SetStatusText(GetStatusMessage()); | ||
118 | } | 170 | } |
119 | 171 | ||
120 | void TrackerFrame::SetStatusMessage(std::string message) { | 172 | void TrackerFrame::ConnectToAp(std::string server, std::string user, |
121 | wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED); | 173 | std::string pass) { |
122 | event->SetString(message.c_str()); | 174 | QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server), |
175 | std::move(user), std::move(pass))); | ||
176 | } | ||
123 | 177 | ||
124 | QueueEvent(event); | 178 | void TrackerFrame::UpdateStatusMessage() { |
179 | QueueEvent(new wxCommandEvent(STATUS_CHANGED)); | ||
125 | } | 180 | } |
126 | 181 | ||
127 | void TrackerFrame::ResetIndicators() { | 182 | void TrackerFrame::ResetIndicators() { |
128 | QueueEvent(new wxCommandEvent(STATE_RESET)); | 183 | QueueEvent(new wxCommandEvent(STATE_RESET)); |
129 | } | 184 | } |
130 | 185 | ||
131 | void TrackerFrame::UpdateIndicators() { | 186 | void TrackerFrame::UpdateIndicators(StateUpdate state) { |
132 | QueueEvent(new wxCommandEvent(STATE_CHANGED)); | 187 | QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state))); |
133 | } | 188 | } |
134 | 189 | ||
135 | void TrackerFrame::OnAbout(wxCommandEvent &event) { | 190 | void TrackerFrame::OnAbout(wxCommandEvent &event) { |
@@ -137,6 +192,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) { | |||
137 | about_info.SetName("Lingo Archipelago Tracker"); | 192 | about_info.SetName("Lingo Archipelago Tracker"); |
138 | about_info.SetVersion(kTrackerVersion.ToString()); | 193 | about_info.SetVersion(kTrackerVersion.ToString()); |
139 | about_info.AddDeveloper("hatkirby"); | 194 | about_info.AddDeveloper("hatkirby"); |
195 | about_info.AddDeveloper("art0007i"); | ||
140 | about_info.AddArtist("Brenton Wildes"); | 196 | about_info.AddArtist("Brenton Wildes"); |
141 | about_info.AddArtist("kinrah"); | 197 | about_info.AddArtist("kinrah"); |
142 | 198 | ||
@@ -145,7 +201,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) { | |||
145 | 201 | ||
146 | void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } | 202 | void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } |
147 | 203 | ||
148 | void TrackerFrame::OnConnect(wxCommandEvent &event) { | 204 | void TrackerFrame::OnApConnect(wxCommandEvent &event) { |
149 | ConnectionDialog dlg; | 205 | ConnectionDialog dlg; |
150 | 206 | ||
151 | if (dlg.ShowModal() == wxID_OK) { | 207 | if (dlg.ShowModal() == wxID_OK) { |
@@ -175,6 +231,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) { | |||
175 | } | 231 | } |
176 | } | 232 | } |
177 | 233 | ||
234 | void TrackerFrame::OnIpcConnect(wxCommandEvent &event) { | ||
235 | IpcDialog dlg; | ||
236 | |||
237 | if (dlg.ShowModal() == wxID_OK) { | ||
238 | GetTrackerConfig().ipc_address = dlg.GetIpcAddress(); | ||
239 | GetTrackerConfig().Save(); | ||
240 | |||
241 | IPC_Connect(dlg.GetIpcAddress()); | ||
242 | } | ||
243 | } | ||
244 | |||
178 | void TrackerFrame::OnSettings(wxCommandEvent &event) { | 245 | void TrackerFrame::OnSettings(wxCommandEvent &event) { |
179 | SettingsDialog dlg; | 246 | SettingsDialog dlg; |
180 | 247 | ||
@@ -182,15 +249,18 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) { | |||
182 | GetTrackerConfig().should_check_for_updates = | 249 | GetTrackerConfig().should_check_for_updates = |
183 | dlg.GetShouldCheckForUpdates(); | 250 | dlg.GetShouldCheckForUpdates(); |
184 | GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); | 251 | GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); |
185 | GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); | 252 | GetTrackerConfig().visible_panels = dlg.GetVisiblePanels(); |
253 | GetTrackerConfig().track_position = dlg.GetTrackPosition(); | ||
186 | GetTrackerConfig().Save(); | 254 | GetTrackerConfig().Save(); |
187 | 255 | ||
188 | UpdateIndicators(); | 256 | UpdateIndicators(StateUpdate{.cleared_locations = true, |
257 | .player_position = true, | ||
258 | .changed_settings = true}); | ||
189 | } | 259 | } |
190 | } | 260 | } |
191 | 261 | ||
192 | void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { | 262 | void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { |
193 | CheckForUpdates(/*manual=*/true); | 263 | updater_->CheckForUpdates(/*invisible=*/false); |
194 | } | 264 | } |
195 | 265 | ||
196 | void TrackerFrame::OnZoomIn(wxCommandEvent &event) { | 266 | void TrackerFrame::OnZoomIn(wxCommandEvent &event) { |
@@ -199,112 +269,99 @@ void TrackerFrame::OnZoomIn(wxCommandEvent &event) { | |||
199 | } | 269 | } |
200 | } | 270 | } |
201 | 271 | ||
202 | void TrackerFrame::OnZoomOut(wxCommandEvent& event) { | 272 | void TrackerFrame::OnZoomOut(wxCommandEvent &event) { |
203 | if (notebook_->GetSelection() == 1) { | 273 | if (notebook_->GetSelection() == 1) { |
204 | subway_map_->Zoom(false); | 274 | subway_map_->Zoom(false); |
205 | } | 275 | } |
206 | } | 276 | } |
207 | 277 | ||
208 | void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { | 278 | void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) { |
209 | zoom_in_menu_item_->Enable(event.GetSelection() == 1); | 279 | if (log_dialog_ == nullptr) { |
210 | zoom_out_menu_item_->Enable(event.GetSelection() == 1); | 280 | log_dialog_ = new LogDialog(this); |
211 | } | 281 | log_dialog_->Show(); |
282 | TrackerSetLogDialog(log_dialog_); | ||
212 | 283 | ||
213 | void TrackerFrame::OnOpenFile(wxCommandEvent& event) { | 284 | log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow, |
214 | wxFileDialog open_file_dialog( | 285 | this); |
215 | this, "Open Lingo Save File", | 286 | } else { |
216 | fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable", | 287 | log_dialog_->SetFocus(); |
217 | wxStandardPaths::Get().GetUserConfigDir().ToStdString()), | ||
218 | AP_GetSaveName(), "Lingo save file (*.save)|*.save", | ||
219 | wxFD_OPEN | wxFD_FILE_MUST_EXIST); | ||
220 | if (open_file_dialog.ShowModal() == wxID_CANCEL) { | ||
221 | return; | ||
222 | } | 288 | } |
289 | } | ||
223 | 290 | ||
224 | std::string savedata_path = open_file_dialog.GetPath().ToStdString(); | 291 | void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) { |
292 | TrackerSetLogDialog(nullptr); | ||
293 | log_dialog_ = nullptr; | ||
225 | 294 | ||
226 | if (panels_panel_ == nullptr) { | 295 | event.Skip(); |
227 | panels_panel_ = new TrackerPanel(notebook_); | 296 | } |
228 | notebook_->AddPage(panels_panel_, "Panels"); | ||
229 | } | ||
230 | 297 | ||
231 | notebook_->SetSelection(notebook_->FindPage(panels_panel_)); | 298 | void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { |
232 | panels_panel_->SetSavedataPath(savedata_path); | 299 | zoom_in_menu_item_->Enable(event.GetSelection() == 1); |
300 | zoom_out_menu_item_->Enable(event.GetSelection() == 1); | ||
233 | } | 301 | } |
234 | 302 | ||
235 | void TrackerFrame::OnStateReset(wxCommandEvent& event) { | 303 | void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) { |
236 | tracker_panel_->UpdateIndicators(); | 304 | notebook_->Refresh(); |
237 | achievements_pane_->UpdateIndicators(); | ||
238 | subway_map_->OnConnect(); | ||
239 | if (panels_panel_ != nullptr) { | ||
240 | notebook_->DeletePage(notebook_->FindPage(panels_panel_)); | ||
241 | panels_panel_ = nullptr; | ||
242 | } | ||
243 | Refresh(); | ||
244 | } | 305 | } |
245 | 306 | ||
246 | void TrackerFrame::OnStateChanged(wxCommandEvent &event) { | 307 | void TrackerFrame::OnStateReset(wxCommandEvent &event) { |
247 | tracker_panel_->UpdateIndicators(); | 308 | tracker_panel_->UpdateIndicators(/*reset=*/true); |
248 | achievements_pane_->UpdateIndicators(); | 309 | achievements_pane_->UpdateIndicators(); |
249 | subway_map_->UpdateIndicators(); | 310 | items_pane_->ResetIndicators(); |
250 | if (panels_panel_ != nullptr) { | 311 | options_pane_->OnConnect(); |
251 | panels_panel_->UpdateIndicators(); | 312 | paintings_pane_->ResetIndicators(); |
252 | } | 313 | subway_map_->OnConnect(); |
253 | Refresh(); | 314 | Refresh(); |
254 | } | 315 | } |
255 | 316 | ||
256 | void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { | 317 | void TrackerFrame::OnStateChanged(StateChangedEvent &event) { |
257 | SetStatusText(event.GetString()); | 318 | const StateUpdate &state = event.GetState(); |
258 | } | 319 | |
320 | bool hunt_panels = false; | ||
321 | if (GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) { | ||
322 | hunt_panels = std::any_of( | ||
323 | state.panels.begin(), state.panels.end(), [](int solve_index) { | ||
324 | return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index)).hunt; | ||
325 | }); | ||
326 | } else if (GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) { | ||
327 | hunt_panels = true; | ||
328 | } | ||
329 | |||
330 | if (!state.items.empty() || !state.paintings.empty() || | ||
331 | state.cleared_locations || hunt_panels) { | ||
332 | // TODO: The only real reason to reset tracker_panel during an active | ||
333 | // connection is if the hunt panels setting changes. If we remove hunt | ||
334 | // panels later, we can get rid of this. | ||
335 | tracker_panel_->UpdateIndicators(/*reset=*/state.changed_settings); | ||
336 | subway_map_->UpdateIndicators(); | ||
337 | Refresh(); | ||
338 | } else if (state.player_position && GetTrackerConfig().track_position) { | ||
339 | if (notebook_->GetSelection() == 0) { | ||
340 | tracker_panel_->Refresh(); | ||
341 | } | ||
342 | } | ||
259 | 343 | ||
260 | void TrackerFrame::CheckForUpdates(bool manual) { | 344 | if (std::any_of(state.panels.begin(), state.panels.end(), |
261 | wxWebRequest request = wxWebSession::GetDefault().CreateRequest( | 345 | [](int solve_index) { |
262 | this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); | 346 | return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index)) |
347 | .achievement; | ||
348 | })) { | ||
349 | achievements_pane_->UpdateIndicators(); | ||
350 | } | ||
263 | 351 | ||
264 | if (!request.IsOk()) { | 352 | if (!state.items.empty()) { |
265 | if (manual) { | 353 | items_pane_->UpdateIndicators(state.items); |
266 | wxMessageBox("Could not check for updates.", "Error", | 354 | } |
267 | wxOK | wxICON_ERROR); | ||
268 | } else { | ||
269 | SetStatusText("Could not check for updates."); | ||
270 | } | ||
271 | 355 | ||
272 | return; | 356 | if (!state.paintings.empty()) { |
357 | paintings_pane_->UpdateIndicators(state.paintings); | ||
273 | } | 358 | } |
359 | } | ||
274 | 360 | ||
275 | Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { | 361 | void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { |
276 | if (evt.GetState() == wxWebRequest::State_Completed) { | 362 | SetStatusText(wxString::FromUTF8(GetStatusMessage())); |
277 | std::string response = evt.GetResponse().AsString().ToStdString(); | 363 | } |
278 | |||
279 | Version latest_version(response); | ||
280 | if (kTrackerVersion < latest_version) { | ||
281 | std::ostringstream message_text; | ||
282 | message_text << "There is a newer version of Lingo AP Tracker " | ||
283 | "available. You have " | ||
284 | << kTrackerVersion.ToString() | ||
285 | << ", and the latest version is " | ||
286 | << latest_version.ToString() | ||
287 | << ". Would you like to update?"; | ||
288 | |||
289 | if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) == | ||
290 | wxYES) { | ||
291 | wxLaunchDefaultBrowser( | ||
292 | "https://code.fourisland.com/lingo-ap-tracker/about/" | ||
293 | "CHANGELOG.md"); | ||
294 | } | ||
295 | } else if (manual) { | ||
296 | wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", | ||
297 | wxOK); | ||
298 | } | ||
299 | } else if (evt.GetState() == wxWebRequest::State_Failed) { | ||
300 | if (manual) { | ||
301 | wxMessageBox("Could not check for updates.", "Error", | ||
302 | wxOK | wxICON_ERROR); | ||
303 | } else { | ||
304 | SetStatusText("Could not check for updates."); | ||
305 | } | ||
306 | } | ||
307 | }); | ||
308 | 364 | ||
309 | request.Start(); | 365 | void TrackerFrame::OnConnectToAp(ApConnectEvent &event) { |
366 | AP_Connect(event.GetServer(), event.GetUser(), event.GetPass()); | ||
310 | } | 367 | } |
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index 19bd0b3..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h | |||
@@ -7,50 +7,121 @@ | |||
7 | #include <wx/wx.h> | 7 | #include <wx/wx.h> |
8 | #endif | 8 | #endif |
9 | 9 | ||
10 | #include <memory> | ||
11 | #include <set> | ||
12 | |||
13 | #include "ap_state.h" | ||
14 | #include "icons.h" | ||
15 | #include "updater.h" | ||
16 | |||
10 | class AchievementsPane; | 17 | class AchievementsPane; |
18 | class ItemsPane; | ||
19 | class LogDialog; | ||
20 | class OptionsPane; | ||
21 | class PaintingsPane; | ||
11 | class SubwayMap; | 22 | class SubwayMap; |
12 | class TrackerPanel; | 23 | class TrackerPanel; |
13 | class wxBookCtrlEvent; | 24 | class wxBookCtrlEvent; |
14 | class wxNotebook; | 25 | class wxNotebook; |
26 | class wxSplitterEvent; | ||
27 | class wxSplitterWindow; | ||
28 | |||
29 | class ApConnectEvent : public wxEvent { | ||
30 | public: | ||
31 | ApConnectEvent(wxEventType eventType, int winid, std::string server, | ||
32 | std::string user, std::string pass) | ||
33 | : wxEvent(winid, eventType), | ||
34 | ap_server_(std::move(server)), | ||
35 | ap_user_(std::move(user)), | ||
36 | ap_pass_(std::move(pass)) {} | ||
37 | |||
38 | const std::string &GetServer() const { return ap_server_; } | ||
39 | |||
40 | const std::string &GetUser() const { return ap_user_; } | ||
41 | |||
42 | const std::string &GetPass() const { return ap_pass_; } | ||
43 | |||
44 | virtual wxEvent *Clone() const { return new ApConnectEvent(*this); } | ||
45 | |||
46 | private: | ||
47 | std::string ap_server_; | ||
48 | std::string ap_user_; | ||
49 | std::string ap_pass_; | ||
50 | }; | ||
51 | |||
52 | struct StateUpdate { | ||
53 | std::vector<ItemState> items; | ||
54 | bool progression_items = false; | ||
55 | std::vector<std::string> paintings; | ||
56 | bool cleared_locations = false; | ||
57 | std::set<int> panels; | ||
58 | bool player_position = false; | ||
59 | bool changed_settings = false; | ||
60 | }; | ||
61 | |||
62 | class StateChangedEvent : public wxEvent { | ||
63 | public: | ||
64 | StateChangedEvent(wxEventType eventType, int winid, StateUpdate state) | ||
65 | : wxEvent(winid, eventType), state_(std::move(state)) {} | ||
66 | |||
67 | const StateUpdate &GetState() const { return state_; } | ||
68 | |||
69 | virtual wxEvent *Clone() const { return new StateChangedEvent(*this); } | ||
70 | |||
71 | private: | ||
72 | StateUpdate state_; | ||
73 | }; | ||
15 | 74 | ||
16 | wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); | 75 | wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); |
17 | wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); | 76 | wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent); |
18 | wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); | 77 | wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); |
78 | wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); | ||
19 | 79 | ||
20 | class TrackerFrame : public wxFrame { | 80 | class TrackerFrame : public wxFrame { |
21 | public: | 81 | public: |
22 | TrackerFrame(); | 82 | TrackerFrame(); |
23 | 83 | ||
24 | void SetStatusMessage(std::string message); | 84 | void ConnectToAp(std::string server, std::string user, std::string pass); |
85 | void UpdateStatusMessage(); | ||
25 | 86 | ||
26 | void ResetIndicators(); | 87 | void ResetIndicators(); |
27 | void UpdateIndicators(); | 88 | void UpdateIndicators(StateUpdate state); |
28 | 89 | ||
29 | private: | 90 | private: |
30 | void OnExit(wxCommandEvent &event); | 91 | void OnExit(wxCommandEvent &event); |
31 | void OnAbout(wxCommandEvent &event); | 92 | void OnAbout(wxCommandEvent &event); |
32 | void OnConnect(wxCommandEvent &event); | 93 | void OnApConnect(wxCommandEvent &event); |
94 | void OnIpcConnect(wxCommandEvent &event); | ||
33 | void OnSettings(wxCommandEvent &event); | 95 | void OnSettings(wxCommandEvent &event); |
34 | void OnCheckForUpdates(wxCommandEvent &event); | 96 | void OnCheckForUpdates(wxCommandEvent &event); |
35 | void OnZoomIn(wxCommandEvent &event); | 97 | void OnZoomIn(wxCommandEvent &event); |
36 | void OnZoomOut(wxCommandEvent &event); | 98 | void OnZoomOut(wxCommandEvent &event); |
99 | void OnOpenLogWindow(wxCommandEvent &event); | ||
100 | void OnCloseLogWindow(wxCloseEvent &event); | ||
37 | void OnChangePage(wxBookCtrlEvent &event); | 101 | void OnChangePage(wxBookCtrlEvent &event); |
38 | void OnOpenFile(wxCommandEvent &event); | 102 | void OnSashPositionChanged(wxSplitterEvent &event); |
39 | 103 | ||
40 | void OnStateReset(wxCommandEvent &event); | 104 | void OnStateReset(wxCommandEvent &event); |
41 | void OnStateChanged(wxCommandEvent &event); | 105 | void OnStateChanged(StateChangedEvent &event); |
42 | void OnStatusChanged(wxCommandEvent &event); | 106 | void OnStatusChanged(wxCommandEvent &event); |
107 | void OnConnectToAp(ApConnectEvent &event); | ||
108 | |||
109 | std::unique_ptr<Updater> updater_; | ||
43 | 110 | ||
44 | void CheckForUpdates(bool manual); | 111 | wxSplitterWindow *splitter_window_; |
45 | |||
46 | wxNotebook *notebook_; | 112 | wxNotebook *notebook_; |
47 | TrackerPanel *tracker_panel_; | 113 | TrackerPanel *tracker_panel_; |
48 | AchievementsPane *achievements_pane_; | 114 | AchievementsPane *achievements_pane_; |
115 | ItemsPane *items_pane_; | ||
116 | OptionsPane *options_pane_; | ||
117 | PaintingsPane *paintings_pane_; | ||
49 | SubwayMap *subway_map_; | 118 | SubwayMap *subway_map_; |
50 | TrackerPanel *panels_panel_ = nullptr; | 119 | LogDialog *log_dialog_ = nullptr; |
51 | 120 | ||
52 | wxMenuItem *zoom_in_menu_item_; | 121 | wxMenuItem *zoom_in_menu_item_; |
53 | wxMenuItem *zoom_out_menu_item_; | 122 | wxMenuItem *zoom_out_menu_item_; |
123 | |||
124 | IconCache icons_; | ||
54 | }; | 125 | }; |
55 | 126 | ||
56 | #endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ | 127 | #endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ |
diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index 27e825a..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp | |||
@@ -9,7 +9,7 @@ | |||
9 | #include "area_popup.h" | 9 | #include "area_popup.h" |
10 | #include "game_data.h" | 10 | #include "game_data.h" |
11 | #include "global.h" | 11 | #include "global.h" |
12 | #include "godot_variant.h" | 12 | #include "ipc_state.h" |
13 | #include "tracker_config.h" | 13 | #include "tracker_config.h" |
14 | #include "tracker_state.h" | 14 | #include "tracker_state.h" |
15 | 15 | ||
@@ -43,67 +43,74 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | |||
43 | areas_.push_back(area); | 43 | areas_.push_back(area); |
44 | } | 44 | } |
45 | 45 | ||
46 | Resize(); | ||
46 | Redraw(); | 47 | Redraw(); |
47 | 48 | ||
48 | Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); | 49 | Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); |
49 | Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); | 50 | Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); |
50 | } | 51 | } |
51 | 52 | ||
52 | void TrackerPanel::UpdateIndicators() { | 53 | void TrackerPanel::UpdateIndicators(bool reset) { |
53 | for (AreaIndicator &area : areas_) { | 54 | if (reset) { |
54 | area.popup->UpdateIndicators(); | 55 | for (AreaIndicator &area : areas_) { |
55 | } | 56 | const MapArea &map_area = GD_GetMapArea(area.area_id); |
56 | 57 | ||
57 | Redraw(); | 58 | if ((!AP_IsLocationVisible(map_area.classification) || |
58 | } | 59 | IsAreaPostgame(area.area_id)) && |
59 | 60 | !(map_area.hunt && | |
60 | void TrackerPanel::SetSavedataPath(std::string savedata_path) { | 61 | GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) && |
61 | if (!panels_mode_) { | 62 | !(map_area.has_single_panel && |
62 | wxButton *refresh_button = new wxButton(this, wxID_ANY, "Refresh", {15, 15}); | 63 | GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) && |
63 | refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this); | 64 | !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) { |
64 | } | 65 | area.active = false; |
65 | 66 | } else { | |
66 | savedata_path_ = savedata_path; | 67 | area.active = true; |
67 | panels_mode_ = true; | 68 | } |
68 | |||
69 | RefreshSavedata(); | ||
70 | } | ||
71 | 69 | ||
72 | void TrackerPanel::RefreshSavedata() { | 70 | area.popup->ResetIndicators(); |
73 | solved_panels_.clear(); | 71 | } |
74 | 72 | ||
75 | GodotVariant godot_variant = ParseGodotFile(*savedata_path_); | 73 | Resize(); |
76 | for (const GodotVariant &panel_node : godot_variant.AsArray()) { | 74 | } else { |
77 | const std::vector<GodotVariant> &fields = panel_node.AsArray(); | 75 | for (AreaIndicator &area : areas_) { |
78 | if (fields[1].AsBool()) { | 76 | area.popup->UpdateIndicators(); |
79 | const std::vector<std::string> &nodepath = fields[0].AsNodePath(); | ||
80 | std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]); | ||
81 | solved_panels_.insert(key); | ||
82 | } | 77 | } |
83 | } | 78 | } |
84 | 79 | ||
85 | UpdateIndicators(); | 80 | Redraw(); |
86 | Refresh(); | ||
87 | } | 81 | } |
88 | 82 | ||
89 | void TrackerPanel::OnPaint(wxPaintEvent &event) { | 83 | void TrackerPanel::OnPaint(wxPaintEvent &event) { |
90 | if (GetSize() != rendered_.GetSize()) { | 84 | if (GetSize() != rendered_.GetSize()) { |
85 | Resize(); | ||
91 | Redraw(); | 86 | Redraw(); |
92 | } | 87 | } |
93 | 88 | ||
94 | wxBufferedPaintDC dc(this); | 89 | wxBufferedPaintDC dc(this); |
95 | dc.DrawBitmap(rendered_, 0, 0); | 90 | dc.DrawBitmap(rendered_, 0, 0); |
96 | 91 | ||
97 | if (AP_GetPlayerPosition().has_value()) { | 92 | std::optional<std::tuple<int, int>> player_position; |
93 | if (GetTrackerConfig().track_position) | ||
94 | { | ||
95 | if (IPC_IsConnected()) { | ||
96 | player_position = IPC_GetPlayerPosition(); | ||
97 | } else { | ||
98 | player_position = AP_GetPlayerPosition(); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | if (player_position.has_value()) { | ||
98 | // 1588, 1194 | 103 | // 1588, 1194 |
99 | // 14x14 -> 154x154 | 104 | // 14x14 -> 154x154 |
100 | double intended_x = | 105 | double intended_x = |
101 | 1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); | 106 | 1588.0 + (std::get<0>(*player_position) * (154.0 / 14.0)); |
102 | double intended_y = | 107 | double intended_y = |
103 | 1194.0 + (std::get<1>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); | 108 | 1194.0 + (std::get<1>(*player_position) * (154.0 / 14.0)); |
104 | 109 | ||
105 | int real_x = offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; | 110 | int real_x = |
106 | int real_y = offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2; | 111 | offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; |
112 | int real_y = | ||
113 | offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2; | ||
107 | 114 | ||
108 | dc.DrawBitmap(scaled_player_, real_x, real_y); | 115 | dc.DrawBitmap(scaled_player_, real_x, real_y); |
109 | } | 116 | } |
@@ -125,12 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) { | |||
125 | event.Skip(); | 132 | event.Skip(); |
126 | } | 133 | } |
127 | 134 | ||
128 | void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { | 135 | void TrackerPanel::Resize() { |
129 | RefreshSavedata(); | 136 | wxSize panel_size = GetClientSize(); |
130 | } | ||
131 | |||
132 | void TrackerPanel::Redraw() { | ||
133 | wxSize panel_size = GetSize(); | ||
134 | wxSize image_size = map_image_.GetSize(); | 137 | wxSize image_size = map_image_.GetSize(); |
135 | 138 | ||
136 | int final_x = 0; | 139 | int final_x = 0; |
@@ -149,7 +152,7 @@ void TrackerPanel::Redraw() { | |||
149 | final_x = (panel_size.GetWidth() - final_width) / 2; | 152 | final_x = (panel_size.GetWidth() - final_width) / 2; |
150 | } | 153 | } |
151 | 154 | ||
152 | rendered_ = wxBitmap( | 155 | scaled_map_ = wxBitmap( |
153 | map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) | 156 | map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) |
154 | .Size(panel_size, {final_x, final_y}, 0, 0, 0)); | 157 | .Size(panel_size, {final_x, final_y}, 0, 0, 0)); |
155 | 158 | ||
@@ -164,30 +167,61 @@ void TrackerPanel::Redraw() { | |||
164 | wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, | 167 | wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, |
165 | player_height > 0 ? player_height : 1)); | 168 | player_height > 0 ? player_height : 1)); |
166 | 169 | ||
170 | real_area_size_ = final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth(); | ||
171 | |||
172 | for (AreaIndicator &area : areas_) { | ||
173 | const MapArea &map_area = GD_GetMapArea(area.area_id); | ||
174 | |||
175 | int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * | ||
176 | final_width / image_size.GetWidth(); | ||
177 | int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * | ||
178 | final_width / image_size.GetWidth(); | ||
179 | |||
180 | area.real_x1 = real_area_x; | ||
181 | area.real_x2 = real_area_x + real_area_size_; | ||
182 | area.real_y1 = real_area_y; | ||
183 | area.real_y2 = real_area_y + real_area_size_; | ||
184 | |||
185 | int popup_x = | ||
186 | final_x + map_area.map_x * final_width / image_size.GetWidth(); | ||
187 | int popup_y = | ||
188 | final_y + map_area.map_y * final_width / image_size.GetWidth(); | ||
189 | |||
190 | area.popup->SetClientSize( | ||
191 | area.popup->GetFullWidth(), | ||
192 | std::min(panel_size.GetHeight(), area.popup->GetFullHeight())); | ||
193 | |||
194 | if (area.popup->GetSize().GetHeight() > panel_size.GetHeight()) { | ||
195 | area.popup->SetSize(area.popup->GetSize().GetWidth(), | ||
196 | panel_size.GetHeight()); | ||
197 | } | ||
198 | |||
199 | if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) { | ||
200 | popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth(); | ||
201 | } | ||
202 | if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) { | ||
203 | popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight(); | ||
204 | } | ||
205 | area.popup->SetPosition({popup_x, popup_y}); | ||
206 | } | ||
207 | } | ||
208 | |||
209 | void TrackerPanel::Redraw() { | ||
210 | rendered_ = scaled_map_; | ||
211 | |||
167 | wxMemoryDC dc; | 212 | wxMemoryDC dc; |
168 | dc.SelectObject(rendered_); | 213 | dc.SelectObject(rendered_); |
169 | 214 | ||
170 | int real_area_size = | ||
171 | final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth(); | ||
172 | int actual_border_size = | 215 | int actual_border_size = |
173 | real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; | 216 | real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; |
174 | const wxPoint upper_left_triangle[] = { | 217 | const wxPoint upper_left_triangle[] = { |
175 | {0, 0}, {0, real_area_size}, {real_area_size, 0}}; | 218 | {0, 0}, {0, real_area_size_}, {real_area_size_, 0}}; |
176 | const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, | 219 | const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1}, |
177 | {real_area_size - 1, 0}, | 220 | {real_area_size_ - 1, 0}, |
178 | {real_area_size, real_area_size}}; | 221 | {real_area_size_, real_area_size_}}; |
179 | 222 | ||
180 | for (AreaIndicator &area : areas_) { | 223 | for (AreaIndicator &area : areas_) { |
181 | const MapArea &map_area = GD_GetMapArea(area.area_id); | 224 | const MapArea &map_area = GD_GetMapArea(area.area_id); |
182 | if (panels_mode_) { | ||
183 | area.active = map_area.has_single_panel; | ||
184 | } else if (!AP_IsLocationVisible(map_area.classification) && | ||
185 | !(map_area.hunt && GetTrackerConfig().show_hunt_panels) && | ||
186 | !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) { | ||
187 | area.active = false; | ||
188 | } else { | ||
189 | area.active = true; | ||
190 | } | ||
191 | 225 | ||
192 | if (!area.active) { | 226 | if (!area.active) { |
193 | continue; | 227 | continue; |
@@ -199,19 +233,15 @@ void TrackerPanel::Redraw() { | |||
199 | bool has_unchecked = false; | 233 | bool has_unchecked = false; |
200 | if (IsLocationWinCondition(section)) { | 234 | if (IsLocationWinCondition(section)) { |
201 | has_unchecked = !AP_HasReachedGoal(); | 235 | has_unchecked = !AP_HasReachedGoal(); |
202 | } else if (panels_mode_) { | 236 | } else if (AP_IsLocationVisible(section.classification) && |
203 | if (section.single_panel) { | 237 | !IsLocationPostgame(section.ap_location_id)) { |
204 | const Panel &panel = GD_GetPanel(*section.single_panel); | ||
205 | if (panel.non_counting) { | ||
206 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); | ||
207 | } else { | ||
208 | has_unchecked = !solved_panels_.contains(panel.nodepath); | ||
209 | } | ||
210 | } | ||
211 | } else if (AP_IsLocationVisible(section.classification)) { | ||
212 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); | 238 | has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); |
213 | } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { | 239 | } else if ((section.hunt && GetTrackerConfig().visible_panels == |
214 | has_unchecked = !AP_HasCheckedHuntPanel(section.ap_location_id); | 240 | TrackerConfig::kHUNT_PANELS) || |
241 | (section.single_panel && GetTrackerConfig().visible_panels == | ||
242 | TrackerConfig::kALL_PANELS)) { | ||
243 | has_unchecked = | ||
244 | !AP_IsPanelSolved(GD_GetPanel(*section.single_panel).solve_index); | ||
215 | } | 245 | } |
216 | 246 | ||
217 | if (has_unchecked) { | 247 | if (has_unchecked) { |
@@ -223,8 +253,12 @@ void TrackerPanel::Redraw() { | |||
223 | } | 253 | } |
224 | } | 254 | } |
225 | 255 | ||
226 | if (AP_IsPaintingShuffle() && !panels_mode_) { | 256 | if (AP_IsPaintingShuffle()) { |
227 | for (int painting_id : map_area.paintings) { | 257 | for (int painting_id : map_area.paintings) { |
258 | if (IsPaintingPostgame(painting_id)) { | ||
259 | continue; | ||
260 | } | ||
261 | |||
228 | const PaintingExit &painting = GD_GetPaintingExit(painting_id); | 262 | const PaintingExit &painting = GD_GetPaintingExit(painting_id); |
229 | bool reachable = IsPaintingReachable(painting_id); | 263 | bool reachable = IsPaintingReachable(painting_id); |
230 | if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { | 264 | if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { |
@@ -237,10 +271,8 @@ void TrackerPanel::Redraw() { | |||
237 | } | 271 | } |
238 | } | 272 | } |
239 | 273 | ||
240 | int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * | 274 | int real_area_x = area.real_x1; |
241 | final_width / image_size.GetWidth(); | 275 | int real_area_y = area.real_y1; |
242 | int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * | ||
243 | final_width / image_size.GetWidth(); | ||
244 | 276 | ||
245 | if (has_reachable_unchecked && has_unreachable_unchecked && | 277 | if (has_reachable_unchecked && has_unreachable_unchecked && |
246 | GetTrackerConfig().hybrid_areas) { | 278 | GetTrackerConfig().hybrid_areas) { |
@@ -254,7 +286,7 @@ void TrackerPanel::Redraw() { | |||
254 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); | 286 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); |
255 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | 287 | dc.SetBrush(*wxTRANSPARENT_BRUSH); |
256 | dc.DrawRectangle({real_area_x, real_area_y}, | 288 | dc.DrawRectangle({real_area_x, real_area_y}, |
257 | {real_area_size, real_area_size}); | 289 | {real_area_size_, real_area_size_}); |
258 | 290 | ||
259 | } else { | 291 | } else { |
260 | const wxBrush *brush_color = wxGREY_BRUSH; | 292 | const wxBrush *brush_color = wxGREY_BRUSH; |
@@ -269,30 +301,7 @@ void TrackerPanel::Redraw() { | |||
269 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); | 301 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); |
270 | dc.SetBrush(*brush_color); | 302 | dc.SetBrush(*brush_color); |
271 | dc.DrawRectangle({real_area_x, real_area_y}, | 303 | dc.DrawRectangle({real_area_x, real_area_y}, |
272 | {real_area_size, real_area_size}); | 304 | {real_area_size_, real_area_size_}); |
273 | } | ||
274 | |||
275 | area.real_x1 = real_area_x; | ||
276 | area.real_x2 = real_area_x + real_area_size; | ||
277 | area.real_y1 = real_area_y; | ||
278 | area.real_y2 = real_area_y + real_area_size; | ||
279 | |||
280 | int popup_x = | ||
281 | final_x + map_area.map_x * final_width / image_size.GetWidth(); | ||
282 | int popup_y = | ||
283 | final_y + map_area.map_y * final_width / image_size.GetWidth(); | ||
284 | |||
285 | area.popup->SetClientSize( | ||
286 | area.popup->GetVirtualSize().GetWidth(), | ||
287 | std::min(panel_size.GetHeight(), | ||
288 | area.popup->GetVirtualSize().GetHeight())); | ||
289 | |||
290 | if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) { | ||
291 | popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth(); | ||
292 | } | 305 | } |
293 | if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) { | ||
294 | popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight(); | ||
295 | } | ||
296 | area.popup->SetPosition({popup_x, popup_y}); | ||
297 | } | 306 | } |
298 | } | 307 | } |
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index e1f515d..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h | |||
@@ -17,15 +17,7 @@ class TrackerPanel : public wxPanel { | |||
17 | public: | 17 | public: |
18 | TrackerPanel(wxWindow *parent); | 18 | TrackerPanel(wxWindow *parent); |
19 | 19 | ||
20 | void UpdateIndicators(); | 20 | void UpdateIndicators(bool reset); |
21 | |||
22 | void SetSavedataPath(std::string savedata_path); | ||
23 | |||
24 | bool IsPanelsMode() const { return panels_mode_; } | ||
25 | |||
26 | const std::set<std::string> &GetSolvedPanels() const { | ||
27 | return solved_panels_; | ||
28 | } | ||
29 | 21 | ||
30 | private: | 22 | private: |
31 | struct AreaIndicator { | 23 | struct AreaIndicator { |
@@ -40,14 +32,13 @@ class TrackerPanel : public wxPanel { | |||
40 | 32 | ||
41 | void OnPaint(wxPaintEvent &event); | 33 | void OnPaint(wxPaintEvent &event); |
42 | void OnMouseMove(wxMouseEvent &event); | 34 | void OnMouseMove(wxMouseEvent &event); |
43 | void OnRefreshSavedata(wxCommandEvent &event); | ||
44 | 35 | ||
36 | void Resize(); | ||
45 | void Redraw(); | 37 | void Redraw(); |
46 | 38 | ||
47 | void RefreshSavedata(); | ||
48 | |||
49 | wxImage map_image_; | 39 | wxImage map_image_; |
50 | wxImage player_image_; | 40 | wxImage player_image_; |
41 | wxBitmap scaled_map_; | ||
51 | wxBitmap rendered_; | 42 | wxBitmap rendered_; |
52 | wxBitmap scaled_player_; | 43 | wxBitmap scaled_player_; |
53 | 44 | ||
@@ -55,12 +46,9 @@ class TrackerPanel : public wxPanel { | |||
55 | int offset_y_ = 0; | 46 | int offset_y_ = 0; |
56 | double scale_x_ = 0; | 47 | double scale_x_ = 0; |
57 | double scale_y_ = 0; | 48 | double scale_y_ = 0; |
49 | int real_area_size_ = 0; | ||
58 | 50 | ||
59 | std::vector<AreaIndicator> areas_; | 51 | std::vector<AreaIndicator> areas_; |
60 | |||
61 | bool panels_mode_ = false; | ||
62 | std::optional<std::string> savedata_path_; | ||
63 | std::set<std::string> solved_panels_; | ||
64 | }; | 52 | }; |
65 | 53 | ||
66 | #endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ | 54 | #endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ |
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 2ee705c..bf2725a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp | |||
@@ -12,6 +12,7 @@ | |||
12 | 12 | ||
13 | #include "ap_state.h" | 13 | #include "ap_state.h" |
14 | #include "game_data.h" | 14 | #include "game_data.h" |
15 | #include "global.h" | ||
15 | #include "logger.h" | 16 | #include "logger.h" |
16 | 17 | ||
17 | namespace { | 18 | namespace { |
@@ -19,11 +20,13 @@ namespace { | |||
19 | struct Requirements { | 20 | struct Requirements { |
20 | bool disabled = false; | 21 | bool disabled = false; |
21 | 22 | ||
22 | std::set<int> doors; // non-grouped, handles progressive | 23 | std::set<int> doors; // non-grouped, handles progressive |
23 | std::set<int> items; // all other items | 24 | std::set<int> panel_doors; // non-grouped, handles progressive |
24 | std::set<int> rooms; // maybe | 25 | std::set<int> items; // all other items |
25 | bool mastery = false; // maybe | 26 | std::set<int> rooms; // maybe |
26 | bool panel_hunt = false; // maybe | 27 | bool mastery = false; // maybe |
28 | bool panel_hunt = false; // maybe | ||
29 | bool postgame = false; | ||
27 | 30 | ||
28 | void Merge(const Requirements& rhs) { | 31 | void Merge(const Requirements& rhs) { |
29 | if (rhs.disabled) { | 32 | if (rhs.disabled) { |
@@ -33,6 +36,9 @@ struct Requirements { | |||
33 | for (int id : rhs.doors) { | 36 | for (int id : rhs.doors) { |
34 | doors.insert(id); | 37 | doors.insert(id); |
35 | } | 38 | } |
39 | for (int id : rhs.panel_doors) { | ||
40 | panel_doors.insert(id); | ||
41 | } | ||
36 | for (int id : rhs.items) { | 42 | for (int id : rhs.items) { |
37 | items.insert(id); | 43 | items.insert(id); |
38 | } | 44 | } |
@@ -41,6 +47,7 @@ struct Requirements { | |||
41 | } | 47 | } |
42 | mastery = mastery || rhs.mastery; | 48 | mastery = mastery || rhs.mastery; |
43 | panel_hunt = panel_hunt || rhs.panel_hunt; | 49 | panel_hunt = panel_hunt || rhs.panel_hunt; |
50 | postgame = postgame || rhs.postgame; | ||
44 | } | 51 | } |
45 | }; | 52 | }; |
46 | 53 | ||
@@ -78,15 +85,12 @@ class RequirementCalculator { | |||
78 | requirements.doors.insert(door_obj.id); | 85 | requirements.doors.insert(door_obj.id); |
79 | break; | 86 | break; |
80 | } | 87 | } |
81 | } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) { | 88 | } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) { |
82 | requirements.rooms.insert(door_obj.room); | ||
83 | |||
84 | for (int panel_id : door_obj.panels) { | 89 | for (int panel_id : door_obj.panels) { |
85 | const Requirements& panel_reqs = GetPanel(panel_id); | 90 | const Requirements& panel_reqs = GetPanel(panel_id); |
86 | requirements.Merge(panel_reqs); | 91 | requirements.Merge(panel_reqs); |
87 | } | 92 | } |
88 | } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS && | 93 | } else if (AP_AreDoorsGrouped() && !door_obj.group_name.empty()) { |
89 | !door_obj.group_name.empty()) { | ||
90 | requirements.items.insert(door_obj.group_ap_item_id); | 94 | requirements.items.insert(door_obj.group_ap_item_id); |
91 | } else { | 95 | } else { |
92 | requirements.doors.insert(door_obj.id); | 96 | requirements.doors.insert(door_obj.id); |
@@ -133,6 +137,21 @@ class RequirementCalculator { | |||
133 | requirements.items.insert(GD_GetItemIdForColor(color)); | 137 | requirements.items.insert(GD_GetItemIdForColor(color)); |
134 | } | 138 | } |
135 | } | 139 | } |
140 | |||
141 | if (panel_obj.panel_door != -1 && | ||
142 | AP_GetDoorShuffleMode() == kPANELS_MODE) { | ||
143 | const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_obj.panel_door); | ||
144 | |||
145 | if (panel_door_obj.group_ap_item_id != -1 && AP_AreDoorsGrouped()) { | ||
146 | requirements.items.insert(panel_door_obj.group_ap_item_id); | ||
147 | } else { | ||
148 | requirements.panel_doors.insert(panel_obj.panel_door); | ||
149 | } | ||
150 | } | ||
151 | |||
152 | if (panel_obj.location_name == GetWinCondition()) { | ||
153 | requirements.postgame = true; | ||
154 | } | ||
136 | 155 | ||
137 | panels_[panel_id] = requirements; | 156 | panels_[panel_id] = requirements; |
138 | } | 157 | } |
@@ -148,11 +167,17 @@ class RequirementCalculator { | |||
148 | struct TrackerState { | 167 | struct TrackerState { |
149 | std::map<int, bool> reachability; | 168 | std::map<int, bool> reachability; |
150 | std::set<int> reachable_doors; | 169 | std::set<int> reachable_doors; |
170 | std::set<int> solveable_panels; | ||
151 | std::set<int> reachable_paintings; | 171 | std::set<int> reachable_paintings; |
152 | std::mutex reachability_mutex; | 172 | std::mutex reachability_mutex; |
153 | RequirementCalculator requirements; | 173 | RequirementCalculator requirements; |
154 | std::map<int, std::map<std::string, bool>> door_reports; | 174 | std::map<int, std::map<std::string, bool>> door_reports; |
155 | bool pilgrimage_doable = false; | 175 | bool pilgrimage_doable = false; |
176 | |||
177 | // If these are empty, it actually means everything is non-postgame. | ||
178 | std::set<int> non_postgame_areas; | ||
179 | std::set<int> non_postgame_locations; | ||
180 | std::set<int> non_postgame_paintings; | ||
156 | }; | 181 | }; |
157 | 182 | ||
158 | enum Decision { kYes, kNo, kMaybe }; | 183 | enum Decision { kYes, kNo, kMaybe }; |
@@ -167,6 +192,11 @@ class StateCalculator; | |||
167 | struct StateCalculatorOptions { | 192 | struct StateCalculatorOptions { |
168 | int start; | 193 | int start; |
169 | bool pilgrimage = false; | 194 | bool pilgrimage = false; |
195 | |||
196 | // Treats all items as collected and all paintings as checked, but postgame | ||
197 | // areas cannot be reached. | ||
198 | bool postgame_detection = false; | ||
199 | |||
170 | StateCalculator* parent = nullptr; | 200 | StateCalculator* parent = nullptr; |
171 | }; | 201 | }; |
172 | 202 | ||
@@ -177,7 +207,21 @@ class StateCalculator { | |||
177 | explicit StateCalculator(StateCalculatorOptions options) | 207 | explicit StateCalculator(StateCalculatorOptions options) |
178 | : options_(options) {} | 208 | : options_(options) {} |
179 | 209 | ||
210 | void PreloadPanels(const std::set<int>& panels) { | ||
211 | solveable_panels_ = panels; | ||
212 | } | ||
213 | |||
214 | void PreloadDoors(const std::set<int>& doors) { | ||
215 | for (int door_id : doors) { | ||
216 | door_decisions_[door_id] = kYes; | ||
217 | } | ||
218 | } | ||
219 | |||
180 | void Calculate() { | 220 | void Calculate() { |
221 | painting_mapping_ = AP_GetPaintingMapping(); | ||
222 | checked_paintings_ = AP_GetCheckedPaintings(); | ||
223 | sunwarp_mapping_ = AP_GetSunwarpMapping(); | ||
224 | |||
181 | std::list<int> panel_boundary; | 225 | std::list<int> panel_boundary; |
182 | std::list<int> painting_boundary; | 226 | std::list<int> painting_boundary; |
183 | std::list<Exit> flood_boundary; | 227 | std::list<Exit> flood_boundary; |
@@ -217,12 +261,13 @@ class StateCalculator { | |||
217 | reachable_changed = true; | 261 | reachable_changed = true; |
218 | 262 | ||
219 | PaintingExit cur_painting = GD_GetPaintingExit(painting_id); | 263 | PaintingExit cur_painting = GD_GetPaintingExit(painting_id); |
220 | if (AP_GetPaintingMapping().count(cur_painting.internal_id) && | 264 | if (painting_mapping_.count(cur_painting.internal_id) && |
221 | AP_GetCheckedPaintings().count(cur_painting.internal_id)) { | 265 | (checked_paintings_.count(cur_painting.internal_id) || |
266 | options_.postgame_detection)) { | ||
222 | Exit painting_exit; | 267 | Exit painting_exit; |
223 | PaintingExit target_painting = | 268 | PaintingExit target_painting = |
224 | GD_GetPaintingExit(GD_GetPaintingByName( | 269 | GD_GetPaintingExit(GD_GetPaintingByName( |
225 | AP_GetPaintingMapping().at(cur_painting.internal_id))); | 270 | painting_mapping_.at(cur_painting.internal_id))); |
226 | painting_exit.source_room = cur_painting.room; | 271 | painting_exit.source_room = cur_painting.room; |
227 | painting_exit.destination_room = target_painting.room; | 272 | painting_exit.destination_room = target_painting.room; |
228 | painting_exit.type = EntranceType::kPainting; | 273 | painting_exit.type = EntranceType::kPainting; |
@@ -281,8 +326,8 @@ class StateCalculator { | |||
281 | 326 | ||
282 | if (AP_IsSunwarpShuffle()) { | 327 | if (AP_IsSunwarpShuffle()) { |
283 | for (int index : room_obj.sunwarps) { | 328 | for (int index : room_obj.sunwarps) { |
284 | if (AP_GetSunwarpMapping().count(index)) { | 329 | if (sunwarp_mapping_.count(index)) { |
285 | const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); | 330 | const SunwarpMapping& sm = sunwarp_mapping_.at(index); |
286 | 331 | ||
287 | new_boundary.push_back( | 332 | new_boundary.push_back( |
288 | {.source_room = room_exit.destination_room, | 333 | {.source_room = room_exit.destination_room, |
@@ -296,15 +341,14 @@ class StateCalculator { | |||
296 | if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { | 341 | if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { |
297 | new_boundary.push_back( | 342 | new_boundary.push_back( |
298 | {.source_room = room_exit.destination_room, | 343 | {.source_room = room_exit.destination_room, |
299 | .destination_room = GD_GetRoomByName("Outside The Undeterred"), | 344 | .destination_room = GD_GetRoomByName("Color Hallways"), |
300 | .type = EntranceType::kPainting}); | 345 | .type = EntranceType::kPainting}); |
301 | } | 346 | } |
302 | 347 | ||
303 | if (AP_IsPilgrimageEnabled()) { | 348 | if (AP_IsPilgrimageEnabled()) { |
304 | int pilgrimage_start_id = GD_GetRoomByName("Hub Room"); | 349 | int pilgrimage_start_id = GD_GetRoomByName("Hub Room"); |
305 | if (AP_IsSunwarpShuffle()) { | 350 | if (AP_IsSunwarpShuffle()) { |
306 | for (const auto& [start_index, mapping] : | 351 | for (const auto& [start_index, mapping] : sunwarp_mapping_) { |
307 | AP_GetSunwarpMapping()) { | ||
308 | if (mapping.dots == 1) { | 352 | if (mapping.dots == 1) { |
309 | pilgrimage_start_id = GD_GetRoomForSunwarp(start_index); | 353 | pilgrimage_start_id = GD_GetRoomForSunwarp(start_index); |
310 | } | 354 | } |
@@ -343,6 +387,10 @@ class StateCalculator { | |||
343 | // evaluated. | 387 | // evaluated. |
344 | for (const Door& door : GD_GetDoors()) { | 388 | for (const Door& door : GD_GetDoors()) { |
345 | int discard = IsDoorReachable(door.id); | 389 | int discard = IsDoorReachable(door.id); |
390 | |||
391 | door_report_[door.id] = {}; | ||
392 | discard = AreRequirementsSatisfied( | ||
393 | GetState().requirements.GetDoor(door.id), &door_report_[door.id]); | ||
346 | } | 394 | } |
347 | } | 395 | } |
348 | 396 | ||
@@ -378,7 +426,8 @@ class StateCalculator { | |||
378 | } | 426 | } |
379 | 427 | ||
380 | private: | 428 | private: |
381 | Decision IsNonGroupedDoorReachable(const Door& door_obj) { | 429 | template <typename T> |
430 | Decision IsNonGroupedDoorReachable(const T& door_obj) { | ||
382 | bool has_item = AP_HasItem(door_obj.ap_item_id); | 431 | bool has_item = AP_HasItem(door_obj.ap_item_id); |
383 | 432 | ||
384 | if (!has_item) { | 433 | if (!has_item) { |
@@ -399,29 +448,48 @@ class StateCalculator { | |||
399 | return kNo; | 448 | return kNo; |
400 | } | 449 | } |
401 | 450 | ||
451 | if (reqs.postgame && options_.postgame_detection) { | ||
452 | return kNo; | ||
453 | } | ||
454 | |||
402 | Decision final_decision = kYes; | 455 | Decision final_decision = kYes; |
403 | 456 | ||
404 | for (int door_id : reqs.doors) { | 457 | if (!options_.postgame_detection) { |
405 | const Door& door_obj = GD_GetDoor(door_id); | 458 | for (int door_id : reqs.doors) { |
406 | Decision decision = IsNonGroupedDoorReachable(door_obj); | 459 | const Door& door_obj = GD_GetDoor(door_id); |
460 | Decision decision = IsNonGroupedDoorReachable(door_obj); | ||
407 | 461 | ||
408 | if (report) { | 462 | if (report) { |
409 | (*report)[door_obj.item_name] = (decision == kYes); | 463 | (*report)[door_obj.item_name] = (decision == kYes); |
410 | } | 464 | } |
411 | 465 | ||
412 | if (decision != kYes) { | 466 | if (decision != kYes) { |
413 | final_decision = decision; | 467 | final_decision = decision; |
468 | } | ||
414 | } | 469 | } |
415 | } | ||
416 | 470 | ||
417 | for (int item_id : reqs.items) { | 471 | for (int panel_door_id : reqs.panel_doors) { |
418 | bool has_item = AP_HasItem(item_id); | 472 | const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id); |
419 | if (report) { | 473 | Decision decision = IsNonGroupedDoorReachable(panel_door_obj); |
420 | (*report)[AP_GetItemName(item_id)] = has_item; | 474 | |
475 | if (report) { | ||
476 | (*report)[panel_door_obj.item_name] = (decision == kYes); | ||
477 | } | ||
478 | |||
479 | if (decision != kYes) { | ||
480 | final_decision = decision; | ||
481 | } | ||
421 | } | 482 | } |
422 | 483 | ||
423 | if (!has_item) { | 484 | for (int item_id : reqs.items) { |
424 | final_decision = kNo; | 485 | bool has_item = AP_HasItem(item_id); |
486 | if (report) { | ||
487 | (*report)[GD_GetItemName(item_id)] = has_item; | ||
488 | } | ||
489 | |||
490 | if (!has_item) { | ||
491 | final_decision = kNo; | ||
492 | } | ||
425 | } | 493 | } |
426 | } | 494 | } |
427 | 495 | ||
@@ -490,14 +558,7 @@ class StateCalculator { | |||
490 | } | 558 | } |
491 | 559 | ||
492 | Decision IsDoorReachable_Helper(int door_id) { | 560 | Decision IsDoorReachable_Helper(int door_id) { |
493 | if (door_report_.count(door_id)) { | 561 | return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id)); |
494 | door_report_[door_id].clear(); | ||
495 | } else { | ||
496 | door_report_[door_id] = {}; | ||
497 | } | ||
498 | |||
499 | return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id), | ||
500 | &door_report_[door_id]); | ||
501 | } | 562 | } |
502 | 563 | ||
503 | Decision IsDoorReachable(int door_id) { | 564 | Decision IsDoorReachable(int door_id) { |
@@ -549,7 +610,7 @@ class StateCalculator { | |||
549 | if (AP_IsSunwarpShuffle()) { | 610 | if (AP_IsSunwarpShuffle()) { |
550 | pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); | 611 | pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); |
551 | 612 | ||
552 | for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { | 613 | for (const auto& [start_index, mapping] : sunwarp_mapping_) { |
553 | if (mapping.dots > 1) { | 614 | if (mapping.dots > 1) { |
554 | std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; | 615 | std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; |
555 | } | 616 | } |
@@ -620,23 +681,94 @@ class StateCalculator { | |||
620 | bool pilgrimage_doable_ = false; | 681 | bool pilgrimage_doable_ = false; |
621 | 682 | ||
622 | std::map<int, std::list<int>> paths_; | 683 | std::map<int, std::list<int>> paths_; |
684 | |||
685 | std::map<std::string, std::string> painting_mapping_; | ||
686 | std::set<std::string> checked_paintings_; | ||
687 | std::map<int, SunwarpMapping> sunwarp_mapping_; | ||
623 | }; | 688 | }; |
624 | 689 | ||
625 | } // namespace | 690 | } // namespace |
626 | 691 | ||
627 | void ResetReachabilityRequirements() { | 692 | void ResetReachabilityRequirements() { |
693 | TrackerLog("Resetting tracker state..."); | ||
694 | |||
628 | std::lock_guard reachability_guard(GetState().reachability_mutex); | 695 | std::lock_guard reachability_guard(GetState().reachability_mutex); |
629 | GetState().requirements.Reset(); | 696 | GetState().requirements.Reset(); |
697 | GetState().reachable_doors.clear(); | ||
698 | GetState().solveable_panels.clear(); | ||
699 | |||
700 | if (AP_IsPostgameShuffle()) { | ||
701 | GetState().non_postgame_areas.clear(); | ||
702 | GetState().non_postgame_locations.clear(); | ||
703 | GetState().non_postgame_paintings.clear(); | ||
704 | } else { | ||
705 | StateCalculator postgame_calculator( | ||
706 | {.start = GD_GetRoomByName("Menu"), .postgame_detection = true}); | ||
707 | postgame_calculator.Calculate(); | ||
708 | |||
709 | std::set<int>& non_postgame_areas = GetState().non_postgame_areas; | ||
710 | non_postgame_areas.clear(); | ||
711 | |||
712 | std::set<int>& non_postgame_locations = GetState().non_postgame_locations; | ||
713 | non_postgame_locations.clear(); | ||
714 | |||
715 | const std::set<int>& reachable_rooms = | ||
716 | postgame_calculator.GetReachableRooms(); | ||
717 | const std::set<int>& solveable_panels = | ||
718 | postgame_calculator.GetSolveablePanels(); | ||
719 | |||
720 | for (const MapArea& map_area : GD_GetMapAreas()) { | ||
721 | bool area_reachable = false; | ||
722 | |||
723 | for (const Location& location_section : map_area.locations) { | ||
724 | bool reachable = reachable_rooms.count(location_section.room); | ||
725 | if (reachable) { | ||
726 | for (int panel_id : location_section.panels) { | ||
727 | reachable &= (solveable_panels.count(panel_id) == 1); | ||
728 | } | ||
729 | } | ||
730 | |||
731 | if (!reachable && IsLocationWinCondition(location_section)) { | ||
732 | reachable = true; | ||
733 | } | ||
734 | |||
735 | if (reachable) { | ||
736 | non_postgame_locations.insert(location_section.ap_location_id); | ||
737 | area_reachable = true; | ||
738 | } | ||
739 | } | ||
740 | |||
741 | for (int painting_id : map_area.paintings) { | ||
742 | if (postgame_calculator.GetReachablePaintings().count(painting_id)) { | ||
743 | area_reachable = true; | ||
744 | } | ||
745 | } | ||
746 | |||
747 | if (area_reachable) { | ||
748 | non_postgame_areas.insert(map_area.id); | ||
749 | } | ||
750 | } | ||
751 | |||
752 | GetState().non_postgame_paintings = | ||
753 | postgame_calculator.GetReachablePaintings(); | ||
754 | } | ||
630 | } | 755 | } |
631 | 756 | ||
632 | void RecalculateReachability() { | 757 | void RecalculateReachability() { |
758 | TrackerLog("Calculating reachability..."); | ||
759 | |||
633 | std::lock_guard reachability_guard(GetState().reachability_mutex); | 760 | std::lock_guard reachability_guard(GetState().reachability_mutex); |
634 | 761 | ||
762 | // Receiving items and checking paintings should never remove access to doors | ||
763 | // or panels, so we can preload any doors and panels we already know are | ||
764 | // accessible from previous runs, in order to reduce the work. | ||
635 | StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); | 765 | StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); |
766 | state_calculator.PreloadDoors(GetState().reachable_doors); | ||
767 | state_calculator.PreloadPanels(GetState().solveable_panels); | ||
636 | state_calculator.Calculate(); | 768 | state_calculator.Calculate(); |
637 | 769 | ||
638 | const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); | 770 | const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); |
639 | const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); | 771 | std::set<int> solveable_panels = state_calculator.GetSolveablePanels(); |
640 | 772 | ||
641 | std::map<int, bool> new_reachability; | 773 | std::map<int, bool> new_reachability; |
642 | for (const MapArea& map_area : GD_GetMapAreas()) { | 774 | for (const MapArea& map_area : GD_GetMapAreas()) { |
@@ -667,6 +799,7 @@ void RecalculateReachability() { | |||
667 | 799 | ||
668 | std::swap(GetState().reachability, new_reachability); | 800 | std::swap(GetState().reachability, new_reachability); |
669 | std::swap(GetState().reachable_doors, new_reachable_doors); | 801 | std::swap(GetState().reachable_doors, new_reachable_doors); |
802 | std::swap(GetState().solveable_panels, solveable_panels); | ||
670 | std::swap(GetState().reachable_paintings, reachable_paintings); | 803 | std::swap(GetState().reachable_paintings, reachable_paintings); |
671 | std::swap(GetState().door_reports, door_reports); | 804 | std::swap(GetState().door_reports, door_reports); |
672 | GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); | 805 | GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); |
@@ -705,3 +838,33 @@ bool IsPilgrimageDoable() { | |||
705 | 838 | ||
706 | return GetState().pilgrimage_doable; | 839 | return GetState().pilgrimage_doable; |
707 | } | 840 | } |
841 | |||
842 | bool IsAreaPostgame(int area_id) { | ||
843 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
844 | |||
845 | if (GetState().non_postgame_areas.empty()) { | ||
846 | return false; | ||
847 | } else { | ||
848 | return !GetState().non_postgame_areas.count(area_id); | ||
849 | } | ||
850 | } | ||
851 | |||
852 | bool IsLocationPostgame(int location_id) { | ||
853 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
854 | |||
855 | if (GetState().non_postgame_locations.empty()) { | ||
856 | return false; | ||
857 | } else { | ||
858 | return !GetState().non_postgame_locations.count(location_id); | ||
859 | } | ||
860 | } | ||
861 | |||
862 | bool IsPaintingPostgame(int painting_id) { | ||
863 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
864 | |||
865 | if (GetState().non_postgame_paintings.empty()) { | ||
866 | return false; | ||
867 | } else { | ||
868 | return !GetState().non_postgame_paintings.count(painting_id); | ||
869 | } | ||
870 | } | ||
diff --git a/src/tracker_state.h b/src/tracker_state.h index a8f155d..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h | |||
@@ -18,4 +18,10 @@ const std::map<std::string, bool>& GetDoorRequirements(int door_id); | |||
18 | 18 | ||
19 | bool IsPilgrimageDoable(); | 19 | bool IsPilgrimageDoable(); |
20 | 20 | ||
21 | bool IsAreaPostgame(int area_id); | ||
22 | |||
23 | bool IsLocationPostgame(int location_id); | ||
24 | |||
25 | bool IsPaintingPostgame(int painting_id); | ||
26 | |||
21 | #endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ | 27 | #endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ |
diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..2b05daf --- /dev/null +++ b/src/updater.cpp | |||
@@ -0,0 +1,309 @@ | |||
1 | #include "updater.h" | ||
2 | |||
3 | #include <fmt/core.h> | ||
4 | #include <openssl/evp.h> | ||
5 | #include <openssl/sha.h> | ||
6 | #include <wx/evtloop.h> | ||
7 | #include <wx/progdlg.h> | ||
8 | #include <wx/webrequest.h> | ||
9 | #include <wx/wfstream.h> | ||
10 | #include <wx/zipstrm.h> | ||
11 | #include <yaml-cpp/yaml.h> | ||
12 | |||
13 | #include <cstdio> | ||
14 | #include <deque> | ||
15 | #include <filesystem> | ||
16 | #include <fstream> | ||
17 | |||
18 | #include "global.h" | ||
19 | #include "logger.h" | ||
20 | #include "version.h" | ||
21 | |||
22 | constexpr const char* kVersionFileUrl = | ||
23 | "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml"; | ||
24 | constexpr const char* kChangelogUrl = | ||
25 | "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md"; | ||
26 | |||
27 | namespace { | ||
28 | |||
29 | std::string CalculateStringSha256(const wxString& data) { | ||
30 | unsigned char hash[SHA256_DIGEST_LENGTH]; | ||
31 | EVP_MD_CTX* sha256 = EVP_MD_CTX_new(); | ||
32 | EVP_DigestInit(sha256, EVP_sha256()); | ||
33 | EVP_DigestUpdate(sha256, data.c_str(), data.length()); | ||
34 | EVP_DigestFinal_ex(sha256, hash, nullptr); | ||
35 | EVP_MD_CTX_free(sha256); | ||
36 | |||
37 | char output[65] = {0}; | ||
38 | for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) { | ||
39 | snprintf(output + (i * 2), 3, "%02x", hash[i]); | ||
40 | } | ||
41 | |||
42 | return std::string(output); | ||
43 | } | ||
44 | |||
45 | } // namespace | ||
46 | |||
47 | Updater::Updater(wxFrame* parent) : parent_(parent) { | ||
48 | Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this); | ||
49 | } | ||
50 | |||
51 | void Updater::Cleanup() { | ||
52 | std::filesystem::path oldDir = GetExecutableDirectory() / "old"; | ||
53 | if (std::filesystem::is_directory(oldDir)) { | ||
54 | std::filesystem::remove_all(oldDir); | ||
55 | } | ||
56 | } | ||
57 | |||
58 | void Updater::CheckForUpdates(bool invisible) { | ||
59 | wxWebRequest versionRequest = | ||
60 | wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl); | ||
61 | |||
62 | if (invisible) { | ||
63 | update_state_ = UpdateState::GetVersionInvisible; | ||
64 | |||
65 | versionRequest.Start(); | ||
66 | } else { | ||
67 | update_state_ = UpdateState::GetVersionManual; | ||
68 | |||
69 | if (DownloadWithProgress(versionRequest)) { | ||
70 | if (versionRequest.GetState() == wxWebRequest::State_Failed) { | ||
71 | wxMessageBox("Could not check for updates.", "Error", | ||
72 | wxOK | wxICON_ERROR); | ||
73 | } else if (versionRequest.GetState() == wxWebRequest::State_Completed) { | ||
74 | ProcessVersionFile( | ||
75 | versionRequest.GetResponse().AsString().utf8_string()); | ||
76 | } | ||
77 | } | ||
78 | } | ||
79 | } | ||
80 | |||
81 | void Updater::OnWebRequestState(wxWebRequestEvent& evt) { | ||
82 | if (update_state_ == UpdateState::GetVersionInvisible) { | ||
83 | if (evt.GetState() == wxWebRequest::State_Completed) { | ||
84 | ProcessVersionFile(evt.GetResponse().AsString().utf8_string()); | ||
85 | } else if (evt.GetState() == wxWebRequest::State_Failed) { | ||
86 | parent_->SetStatusText("Could not check for updates."); | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | |||
91 | void Updater::ProcessVersionFile(std::string data) { | ||
92 | try { | ||
93 | YAML::Node versionInfo = YAML::Load(data); | ||
94 | Version latestVersion(versionInfo["version"].as<std::string>()); | ||
95 | |||
96 | if (kTrackerVersion < latestVersion) { | ||
97 | if (versionInfo["packages"]) { | ||
98 | std::string platformIdentifier; | ||
99 | |||
100 | if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) { | ||
101 | platformIdentifier = "win64"; | ||
102 | } | ||
103 | |||
104 | if (!platformIdentifier.empty() && | ||
105 | versionInfo["packages"][platformIdentifier]) { | ||
106 | wxMessageDialog dialog( | ||
107 | nullptr, | ||
108 | fmt::format("There is a newer version of Lingo AP Tracker " | ||
109 | "available. You have {}, and the latest version is " | ||
110 | "{}. Would you like to update?", | ||
111 | kTrackerVersion.ToString(), latestVersion.ToString()), | ||
112 | "Update available", wxYES_NO | wxCANCEL); | ||
113 | dialog.SetYesNoLabels("Install update", "Open changelog"); | ||
114 | |||
115 | int dlgResult = dialog.ShowModal(); | ||
116 | if (dlgResult == wxID_YES) { | ||
117 | const YAML::Node& packageInfo = | ||
118 | versionInfo["packages"][platformIdentifier]; | ||
119 | std::string packageUrl = packageInfo["url"].as<std::string>(); | ||
120 | std::string packageChecksum = | ||
121 | packageInfo["checksum"].as<std::string>(); | ||
122 | |||
123 | std::vector<std::filesystem::path> packageFiles; | ||
124 | if (packageInfo["files"]) { | ||
125 | for (const YAML::Node& filename : packageInfo["files"]) { | ||
126 | packageFiles.push_back(filename.as<std::string>()); | ||
127 | } | ||
128 | } | ||
129 | |||
130 | std::vector<std::filesystem::path> deletedFiles; | ||
131 | if (packageInfo["deleted_files"]) { | ||
132 | for (const YAML::Node& filename : packageInfo["deleted_files"]) { | ||
133 | deletedFiles.push_back(filename.as<std::string>()); | ||
134 | } | ||
135 | } | ||
136 | |||
137 | InstallUpdate(packageUrl, packageChecksum, packageFiles, | ||
138 | deletedFiles); | ||
139 | } else if (dlgResult == wxID_NO) { | ||
140 | wxLaunchDefaultBrowser(kChangelogUrl); | ||
141 | } | ||
142 | |||
143 | return; | ||
144 | } | ||
145 | } | ||
146 | |||
147 | if (wxMessageBox( | ||
148 | fmt::format("There is a newer version of Lingo AP Tracker " | ||
149 | "available. You have {}, and the latest version is " | ||
150 | "{}. Would you like to update?", | ||
151 | kTrackerVersion.ToString(), latestVersion.ToString()), | ||
152 | "Update available", wxYES_NO) == wxYES) { | ||
153 | wxLaunchDefaultBrowser(kChangelogUrl); | ||
154 | } | ||
155 | } else if (update_state_ == UpdateState::GetVersionManual) { | ||
156 | wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK); | ||
157 | } | ||
158 | } catch (const std::exception& ex) { | ||
159 | wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR); | ||
160 | } | ||
161 | } | ||
162 | |||
163 | void Updater::InstallUpdate(std::string url, std::string checksum, | ||
164 | std::vector<std::filesystem::path> files, | ||
165 | std::vector<std::filesystem::path> deletedFiles) { | ||
166 | update_state_ = UpdateState::GetPackage; | ||
167 | |||
168 | wxWebRequest packageRequest = | ||
169 | wxWebSession::GetDefault().CreateRequest(this, url); | ||
170 | |||
171 | if (!DownloadWithProgress(packageRequest)) { | ||
172 | return; | ||
173 | } | ||
174 | |||
175 | bool download_issue = false; | ||
176 | |||
177 | wxFileName package_path; | ||
178 | package_path.AssignTempFileName(""); | ||
179 | |||
180 | if (!package_path.IsOk()) { | ||
181 | download_issue = true; | ||
182 | } else { | ||
183 | wxFileOutputStream writeOut(package_path.GetFullPath()); | ||
184 | wxString fileData = packageRequest.GetResponse().AsString(); | ||
185 | writeOut.WriteAll(fileData.c_str(), fileData.length()); | ||
186 | |||
187 | std::string downloadedChecksum = CalculateStringSha256(fileData); | ||
188 | if (downloadedChecksum != checksum) { | ||
189 | download_issue = true; | ||
190 | } | ||
191 | } | ||
192 | |||
193 | if (download_issue) { | ||
194 | if (wxMessageBox("There was an issue downloading the update. Would you " | ||
195 | "like to manually download it instead?", | ||
196 | "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) { | ||
197 | wxLaunchDefaultBrowser(kChangelogUrl); | ||
198 | } | ||
199 | return; | ||
200 | } | ||
201 | |||
202 | std::filesystem::path newArea = GetExecutableDirectory(); | ||
203 | std::filesystem::path oldArea = newArea / "old"; | ||
204 | std::set<std::filesystem::path> folders; | ||
205 | std::set<std::filesystem::path> filesToMove; | ||
206 | for (const std::filesystem::path& existingFile : files) { | ||
207 | std::filesystem::path movedPath = oldArea / existingFile; | ||
208 | std::filesystem::path movedDir = movedPath; | ||
209 | movedDir.remove_filename(); | ||
210 | folders.insert(movedDir); | ||
211 | filesToMove.insert(existingFile); | ||
212 | } | ||
213 | for (const std::filesystem::path& existingFile : deletedFiles) { | ||
214 | std::filesystem::path movedPath = oldArea / existingFile; | ||
215 | std::filesystem::path movedDir = movedPath; | ||
216 | movedDir.remove_filename(); | ||
217 | folders.insert(movedDir); | ||
218 | } | ||
219 | |||
220 | for (const std::filesystem::path& newFolder : folders) { | ||
221 | TrackerLog(fmt::format("Creating directory {}", newFolder.string())); | ||
222 | |||
223 | std::filesystem::create_directories(newFolder); | ||
224 | } | ||
225 | |||
226 | for (const std::filesystem::path& existingFile : files) { | ||
227 | std::filesystem::path existingPath = newArea / existingFile; | ||
228 | |||
229 | if (std::filesystem::is_regular_file(existingPath)) { | ||
230 | std::filesystem::path movedPath = oldArea / existingFile; | ||
231 | |||
232 | TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(), | ||
233 | movedPath.string())); | ||
234 | |||
235 | std::filesystem::rename(existingPath, movedPath); | ||
236 | } | ||
237 | } | ||
238 | for (const std::filesystem::path& existingFile : deletedFiles) { | ||
239 | std::filesystem::path existingPath = newArea / existingFile; | ||
240 | |||
241 | if (std::filesystem::is_regular_file(existingPath)) { | ||
242 | std::filesystem::path movedPath = oldArea / existingFile; | ||
243 | |||
244 | TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(), | ||
245 | movedPath.string())); | ||
246 | |||
247 | std::filesystem::rename(existingPath, movedPath); | ||
248 | } | ||
249 | } | ||
250 | |||
251 | wxFileInputStream fileInputStream(package_path.GetFullPath()); | ||
252 | wxZipInputStream zipStream(fileInputStream); | ||
253 | std::unique_ptr<wxZipEntry> zipEntry; | ||
254 | while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) != | ||
255 | nullptr) { | ||
256 | if (zipEntry->IsDir()) { | ||
257 | continue; | ||
258 | } | ||
259 | |||
260 | std::filesystem::path archivePath = zipEntry->GetName().utf8_string(); | ||
261 | |||
262 | TrackerLog(fmt::format("Found {} in archive", archivePath.string())); | ||
263 | |||
264 | // Cut off the root folder name | ||
265 | std::filesystem::path subPath; | ||
266 | for (auto it = std::next(archivePath.begin()); it != archivePath.end(); | ||
267 | it++) { | ||
268 | subPath /= *it; | ||
269 | } | ||
270 | |||
271 | std::filesystem::path pastePath = newArea / subPath; | ||
272 | |||
273 | wxFileOutputStream fileOutput(pastePath.string()); | ||
274 | zipStream.Read(fileOutput); | ||
275 | } | ||
276 | |||
277 | if (wxMessageBox( | ||
278 | "Update installed! The tracker must be restarted for the changes to take " | ||
279 | "effect. Do you want to close the tracker?", | ||
280 | "Update installed", wxYES_NO) == wxYES) { | ||
281 | wxExit(); | ||
282 | } | ||
283 | } | ||
284 | |||
285 | bool Updater::DownloadWithProgress(wxWebRequest& request) { | ||
286 | request.Start(); | ||
287 | |||
288 | wxProgressDialog dialog("Checking for updates...", "Checking for updates...", | ||
289 | 100, nullptr, | ||
290 | wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT | | ||
291 | wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME); | ||
292 | while (request.GetState() != wxWebRequest::State_Completed && | ||
293 | request.GetState() != wxWebRequest::State_Failed) { | ||
294 | if (request.GetBytesExpectedToReceive() == -1) { | ||
295 | if (!dialog.Pulse()) { | ||
296 | request.Cancel(); | ||
297 | return false; | ||
298 | } | ||
299 | } else { | ||
300 | dialog.SetRange(request.GetBytesExpectedToReceive()); | ||
301 | if (!dialog.Update(request.GetBytesReceived())) { | ||
302 | request.Cancel(); | ||
303 | return false; | ||
304 | } | ||
305 | } | ||
306 | } | ||
307 | |||
308 | return true; | ||
309 | } | ||
diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..c604a49 --- /dev/null +++ b/src/updater.h | |||
@@ -0,0 +1,46 @@ | |||
1 | #ifndef UPDATER_H_809E7381 | ||
2 | #define UPDATER_H_809E7381 | ||
3 | |||
4 | #include <filesystem> | ||
5 | #include <set> | ||
6 | #include <string> | ||
7 | |||
8 | #include <wx/wxprec.h> | ||
9 | |||
10 | #ifndef WX_PRECOMP | ||
11 | #include <wx/wx.h> | ||
12 | #endif | ||
13 | |||
14 | class wxWebRequest; | ||
15 | class wxWebRequestEvent; | ||
16 | |||
17 | class Updater : public wxEvtHandler { | ||
18 | public: | ||
19 | explicit Updater(wxFrame* parent); | ||
20 | |||
21 | void Cleanup(); | ||
22 | |||
23 | void CheckForUpdates(bool invisible); | ||
24 | |||
25 | private: | ||
26 | enum class UpdateState { | ||
27 | GetVersionInvisible, | ||
28 | GetVersionManual, | ||
29 | GetPackage, | ||
30 | }; | ||
31 | |||
32 | void OnWebRequestState(wxWebRequestEvent& event); | ||
33 | |||
34 | void ProcessVersionFile(std::string data); | ||
35 | |||
36 | void InstallUpdate(std::string url, std::string checksum, | ||
37 | std::vector<std::filesystem::path> files, | ||
38 | std::vector<std::filesystem::path> deletedFiles); | ||
39 | |||
40 | bool DownloadWithProgress(wxWebRequest& request); | ||
41 | |||
42 | wxFrame* parent_; | ||
43 | UpdateState update_state_ = UpdateState::GetVersionInvisible; | ||
44 | }; | ||
45 | |||
46 | #endif /* end of include guard: UPDATER_H_809E7381 */ | ||
diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..3b4d5f3 --- /dev/null +++ b/src/version.cpp | |||
@@ -0,0 +1,5 @@ | |||
1 | #include "version.h" | ||
2 | |||
3 | std::ostream& operator<<(std::ostream& out, const Version& ver) { | ||
4 | return out << "v" << ver.major << "." << ver.minor << "." << ver.revision; | ||
5 | } | ||
diff --git a/src/version.h b/src/version.h index 24c04b4..3439fda 100644 --- a/src/version.h +++ b/src/version.h | |||
@@ -36,6 +36,6 @@ struct Version { | |||
36 | } | 36 | } |
37 | }; | 37 | }; |
38 | 38 | ||
39 | constexpr const Version kTrackerVersion = Version(0, 11, 1); | 39 | constexpr const Version kTrackerVersion = Version(2, 0, 2); |
40 | 40 | ||
41 | #endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file | 41 | #endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file |
diff --git a/src/windows.rc b/src/windows.rc new file mode 100644 index 0000000..8ba30ed --- /dev/null +++ b/src/windows.rc | |||
@@ -0,0 +1,3 @@ | |||
1 | #define wxUSE_RC_MANIFEST 1 | ||
2 | #define wxUSE_DPI_AWARE_MANIFEST 2 | ||
3 | #include "wx/msw/wx.rc" | ||
diff --git a/vcpkg.json b/vcpkg.json index e13d228..581a507 100644 --- a/vcpkg.json +++ b/vcpkg.json | |||
@@ -1,6 +1,5 @@ | |||
1 | { | 1 | { |
2 | "dependencies": [ | 2 | "dependencies": [ |
3 | "websocketpp", | ||
4 | "wxwidgets", | 3 | "wxwidgets", |
5 | "openssl", | 4 | "openssl", |
6 | "yaml-cpp", | 5 | "yaml-cpp", |
diff --git a/vendor/vcpkg b/vendor/vcpkg | |||
Subproject 3dd44b931481d7a8e9ba412621fa810232b6628 | Subproject df8bfe519564ae001903e5cdd32af0999531ef7 | ||
diff --git a/vendor/websocketpp b/vendor/websocketpp new file mode 160000 | |||
Subproject 1b11fd301531e6df35a6107c1e8665b1e77a2d8 | |||