about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitmodules3
-rw-r--r--CHANGELOG.md271
-rw-r--r--CMakeLists.txt38
-rw-r--r--README.md23
-rw-r--r--VERSION2
-rw-r--r--VERSION.yaml31
-rwxr-xr-xassets/areas.yaml14
-rw-r--r--assets/checked_owl.pngbin0 -> 262 bytes
-rw-r--r--assets/subway.pngbin184533 -> 232357 bytes
-rw-r--r--assets/subway.yaml725
-rw-r--r--src/achievements_pane.cpp15
-rw-r--r--src/achievements_pane.h6
-rw-r--r--src/ap_state.cpp873
-rw-r--r--src/ap_state.h44
-rw-r--r--src/area_popup.cpp188
-rw-r--r--src/area_popup.h38
-rw-r--r--src/connection_dialog.cpp44
-rw-r--r--src/connection_dialog.h6
-rw-r--r--src/game_data.cpp496
-rw-r--r--src/game_data.h46
-rw-r--r--src/global.cpp16
-rw-r--r--src/global.h2
-rw-r--r--src/icons.cpp22
-rw-r--r--src/icons.h25
-rw-r--r--src/ipc_dialog.cpp54
-rw-r--r--src/ipc_dialog.h24
-rw-r--r--src/ipc_state.cpp367
-rw-r--r--src/ipc_state.h23
-rw-r--r--src/items_pane.cpp145
-rw-r--r--src/items_pane.h33
-rw-r--r--src/log_dialog.cpp37
-rw-r--r--src/log_dialog.h24
-rw-r--r--src/logger.cpp64
-rw-r--r--src/logger.h14
-rw-r--r--src/main.cpp24
-rw-r--r--src/network_set.cpp34
-rw-r--r--src/network_set.h16
-rw-r--r--src/options_pane.cpp71
-rw-r--r--src/options_pane.h19
-rw-r--r--src/paintings_pane.cpp86
-rw-r--r--src/paintings_pane.h28
-rw-r--r--src/report_popup.cpp131
-rw-r--r--src/report_popup.h38
-rw-r--r--src/settings_dialog.cpp45
-rw-r--r--src/settings_dialog.h13
-rw-r--r--src/subway_map.cpp842
-rw-r--r--src/subway_map.h50
-rw-r--r--src/tracker_config.cpp14
-rw-r--r--src/tracker_config.h10
-rw-r--r--src/tracker_frame.cpp294
-rw-r--r--src/tracker_frame.h96
-rw-r--r--src/tracker_panel.cpp185
-rw-r--r--src/tracker_panel.h9
-rw-r--r--src/tracker_state.cpp646
-rw-r--r--src/tracker_state.h17
-rw-r--r--src/updater.cpp309
-rw-r--r--src/updater.h46
-rw-r--r--src/version.cpp5
-rw-r--r--src/version.h9
-rw-r--r--src/windows.rc3
-rw-r--r--vcpkg.json4
m---------vendor/apclientpp0
m---------vendor/asio0
m---------vendor/vcpkg0
m---------vendor/websocketpp0
m---------vendor/wswrap0
67 files changed, 5431 insertions, 1327 deletions
diff --git a/.gitignore b/.gitignore index a7cadc7..1ca77eb 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,5 +1,6 @@
1build/ 1build/
2builds/ 2builds/
3assets/LL1.yaml 3assets/LL1.yaml
4assets/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 9ee737e..e2444b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,276 @@
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
7Download:
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/>
9Source: [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
18Download:
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/>
20Source: [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
26Download:
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/>
28Source: [v2.0.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v2.0.0)
29
30## v1.0.0 - 2025-03-21
31
32After 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
55Download:
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/>
57Source: [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
63Download:
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/>
65Source: [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
73Download:
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/>
75Source: [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
84Download:
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/>
86Source: [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
97Download:
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/>
99Source: [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
111Download:
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/>
113Source: [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
120Download:
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/>
122Source: [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
128Download:
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/>
130Source: [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
137Download:
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/>
139Source: [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
153Download:
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/>
155Source: [v0.11.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.1)
156
157## v0.11.0 - 2024-07-19
158
159- Added a savedata analyzer. When connected to a world, the user can open up the
160 Lingo save file associated with the connected world, and a new tab will open
161 up showing unsolved panels that are accessible, even if the world is not a
162 panelsanity world.
163
164Download:
165[lingo-ap-tracker-v0.11.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.11.0-win64.zip)<br/>
166Source: [v0.11.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.11.0)
167
168## v0.10.7 - 2024-07-17
169
170- Fixed issue with pilgrimage detection when sunwarps are shuffled where it
171 would expect you to use sunwarps mid-pilgrimage.
172- Fixed unreachable paintings sometimes being shown as already checked.
173
174Download:
175[lingo-ap-tracker-v0.10.7-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.7-win64.zip)<br/>
176Source: [v0.10.7](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.7)
177
178## v0.10.6 - 2024-07-16
179
180- The status bar now shows the name and server for the connected slot.
181- Fixed an issue with pilgrimage detection when paintings are shuffled and
182 paintings are not allowed for pilgrimage.
183- Fixed an issue with pilgrimage starting from the wrong place when sunwarps are
184 shuffled.
185- Fixed incorrect doors showing for sunwarps on subway map when sunwarps are
186 shuffled.
187
188Download:
189[lingo-ap-tracker-v0.10.6-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.6-win64.zip)<br/>
190Source: [v0.10.6](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.6)
191
192## v0.10.5 - 2024-07-12
193
194- Increased length of connection history to 10.
195- Fixed position of The Steady's paintings on the subway map.
196
197Download:
198[lingo-ap-tracker-v0.10.5-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.5-win64.zip)<br/>
199Source: [v0.10.5](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.5)
200
201## v0.10.4 - 2024-07-01
202
203- Compatibility update for Archipelago 0.5.0
204
205Download:
206[lingo-ap-tracker-v0.10.4-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.4-win64.zip)<br/>
207Source: [v0.10.4](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.4)
208
209## v0.10.3 - 2024-06-10
210
211- Fixed crash that occurred when the Eye Wall painting was in your painting
212 mapping.
213- Fixed getting bombarded with message boxes when the tracker crashes.
214- Moved early color hallways painting to match the new in-game location.
215
216Download:
217[lingo-ap-tracker-v0.10.3-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.3-win64.zip)<br/>
218Source: [v0.10.3](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.3)
219
220## v0.10.2 - 2024-06-09
221
222- Fixed intermittent reachability detection issue.
223- Increased debug logging.
224
225Download:
226[lingo-ap-tracker-v0.10.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.2-win64.zip)<br/>
227Source: [v0.10.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.2)
228
229## v0.10.1 - 2024-06-08
230
231- Fixed display not updating when game state changes.
232- Fixed broken reachability calculation.
233
234Download:
235[lingo-ap-tracker-v0.10.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.1-win64.zip)<br/>
236Source: [v0.10.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.1)
237
238## v0.10.0 - 2024-06-06
239
240- Added a subway map tab to the tracker. This displays the topology of the game,
241 along with all of the doors, paintings, sunwarps, and regular warps. It can
242 show you whether doors are open or closed, and you can hover over them to see
243 what's needed to open them. You can also hover over paintings, sunwarps, and
244 regular warps to see what they're connected to. Thanks to Kinrah for creating
245 the image!
246- When playing on painting shuffle, the tracker will no longer automatically
247 take your painting mapping into consideration. Instead, unchecked paintings
248 will show up in the regular map tab as if they were unchecked locations.
249 Looking into a painting in-game marks it as checked in the tracker, and if the
250 painting goes somewhere, the tracker will record that and recalculate your
251 reachable areas accordingly. v3.1.0 or later of the client is required for
252 this to work.
253
254Download:
255[lingo-ap-tracker-v0.10.0-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.10.0-win64.zip)<br/>
256Source: [v0.10.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.0)
257
258## v0.9.2 - 2024-06-04
259
260- Updated apclient and dependencies.
261
262Download:
263[lingo-ap-tracker-v0.9.2-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.9.2-win64.zip)<br/>
264Source: [v0.9.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.9.2)
265
266## v0.9.1 - 2024-05-15
267
268- Fixed pilgrimage detection when sunwarp shuffle is on.
269
270Download:
271[lingo-ap-tracker-v0.9.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.9.1-win64.zip)<br/>
272Source: [v0.9.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.9.1)
273
3## v0.9.0 - 2024-04-22 274## v0.9.0 - 2024-04-22
4 275
5- Compatibility update for Archipelago 0.4.6 276- Compatibility update for Archipelago 0.4.6
diff --git a/CMakeLists.txt b/CMakeLists.txt index cd62c55..ef741fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -1,15 +1,16 @@
1cmake_minimum_required (VERSION 3.1) 1cmake_minimum_required (VERSION 3.20)
2project (lingo_ap_tracker) 2project (lingo_ap_tracker)
3 3
4if (MSVC) 4if (MSVC)
5set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") 5set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
6set(CMAKE_WIN32_EXECUTABLE true) 6set(CMAKE_WIN32_EXECUTABLE true)
7set(CMAKE_EXE_LINKER_FLAGS /MANIFEST:NO)
7endif(MSVC) 8endif(MSVC)
8 9
9find_package(wxWidgets CONFIG REQUIRED) 10find_package(wxWidgets CONFIG REQUIRED)
10find_package(OpenSSL REQUIRED) 11find_package(OpenSSL REQUIRED)
11find_package(yaml-cpp REQUIRED) 12find_package(yaml-cpp REQUIRED)
12find_package(websocketpp REQUIRED) 13find_package(fmt REQUIRED)
13 14
14include_directories( 15include_directories(
15 vendor/hkutil 16 vendor/hkutil
@@ -17,11 +18,12 @@ include_directories(
17 vendor/asio/asio/include 18 vendor/asio/asio/include
18 vendor/nlohmann 19 vendor/nlohmann
19 vendor/valijson/include 20 vendor/valijson/include
20 ${websocketpp_INCLUDE_DIRS} 21 vendor/websocketpp
21 vendor/wswrap/include 22 vendor/wswrap/include
22 ${yaml-cpp_INCLUDE_DIRS} 23 ${yaml-cpp_INCLUDE_DIRS}
23 ${OpenSSL_INCLUDE_DIRS} 24 ${OpenSSL_INCLUDE_DIRS}
24 vendor/whereami 25 vendor/whereami
26 ${fmt_INCLUDE_DIRS}
25 vendor 27 vendor
26) 28)
27 29
@@ -30,7 +32,7 @@ include_directories(${SYSTEM_INCLUDE_DIR})
30 32
31link_directories(${openssl_LIBRARY_DIRS}) 33link_directories(${openssl_LIBRARY_DIRS})
32 34
33add_executable(lingo_ap_tracker 35set(SOURCE_FILES
34 "src/main.cpp" 36 "src/main.cpp"
35 "src/tracker_frame.cpp" 37 "src/tracker_frame.cpp"
36 "src/tracker_panel.cpp" 38 "src/tracker_panel.cpp"
@@ -45,8 +47,34 @@ add_executable(lingo_ap_tracker
45 "src/global.cpp" 47 "src/global.cpp"
46 "src/subway_map.cpp" 48 "src/subway_map.cpp"
47 "src/network_set.cpp" 49 "src/network_set.cpp"
50 "src/logger.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"
48 "vendor/whereami/whereami.c" 60 "vendor/whereami/whereami.c"
49) 61)
62
63if (MSVC)
64list(APPEND SOURCE_FILES "src/windows.rc")
65endif(MSVC)
66
67add_executable(lingo_ap_tracker ${SOURCE_FILES})
50set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) 68set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
51set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON) 69set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON)
52target_link_libraries(lingo_ap_tracker PRIVATE OpenSSL::SSL OpenSSL::Crypto websocketpp::websocketpp wx::core wx::base wx::net yaml-cpp::yaml-cpp) 70target_link_libraries(lingo_ap_tracker PRIVATE fmt::fmt OpenSSL::SSL OpenSSL::Crypto wx::core wx::base wx::net yaml-cpp::yaml-cpp)
71
72set(SRC_DIR "${CMAKE_SOURCE_DIR}/assets")
73set(DST_DIR "${CMAKE_BINARY_DIR}/$<CONFIG>/assets")
74
75add_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
80add_dependencies(lingo_ap_tracker copy_assets)
diff --git a/README.md b/README.md index 0b928cd..cb3e90d 100644 --- a/README.md +++ b/README.md
@@ -6,3 +6,26 @@ This app is a tool that helps you keep track of your game state when playing Lin
6## Download 6## Download
7 7
8Releases of the tracker can be found [on the releases page](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md). 8Releases of the tracker can be found [on the releases page](https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md).
9
10## Acknowledgments
11
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
18To build the app:
19
201. Clone the repository including submodules: `git clone --recursive https://code.fourisland.com/lingo-ap-tracker`
212. Put [LL1.yaml from archipelago](https://github.com/ArchipelagoMW/Archipelago/raw/main/worlds/lingo/data/LL1.yaml) in ./assets
223. Put [ids.yaml from archipelago](https://github.com/ArchipelagoMW/Archipelago/raw/main/worlds/lingo/data/ids.yaml) in ./assets
234. Configure the project: `cmake --preset=lingo-ap-tracker-preset`
245. Build the application in debug mode: `cmake --build --preset=lingo-ap-tracker-preset`
256. (Optional) Build the application in release mode: `cmake --build --preset=x64-release-preset`
26
27LL1.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
31If 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 7965b36..b02d37b 100644 --- a/VERSION +++ b/VERSION
@@ -1 +1 @@
v0.9.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 @@
1version: v2.0.2
2packages:
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/areas.yaml b/assets/areas.yaml index d38ceb8..a615e2c 100755 --- a/assets/areas.yaml +++ b/assets/areas.yaml
@@ -67,6 +67,8 @@
67 map: [2642, 872] 67 map: [2642, 872]
68 Eight Room: 68 Eight Room:
69 fold_into: The Incomparable 69 fold_into: The Incomparable
70 Eight Alcove:
71 fold_into: The Incomparable
70 Orange Tower First Floor: 72 Orange Tower First Floor:
71 map: [1285, 928] 73 map: [1285, 928]
72 Color Hunt: 74 Color Hunt:
@@ -83,8 +85,10 @@
83 map: [1252, 1259] 85 map: [1252, 1259]
84 Orange Tower Seventh Floor: 86 Orange Tower Seventh Floor:
85 map: [1587, 1900] 87 map: [1587, 1900]
86 Orange Tower Basement: 88 Orange Tower Sixth Floor:
87 map: [1587, 2000] 89 map: [1587, 2000]
90 Orange Tower Basement:
91 map: [1587, 2100]
88 Courtyard: 92 Courtyard:
89 map: [863, 387] 93 map: [863, 387]
90 First Second Third Fourth: 94 First Second Third Fourth:
@@ -224,11 +228,15 @@
224 The Eyes They See: 228 The Eyes They See:
225 map: [955, 933] 229 map: [955, 933]
226 Far Window: 230 Far Window:
227 fold_into: The Eyes They See 231 fold_into: The Eyes They See
232 Wondrous Lobby:
233 fold_into: The Eyes They See
228 Outside The Wondrous: 234 Outside The Wondrous:
229 map: [691, 524] 235 map: [691, 524]
230 The Wondrous: 236 The Wondrous:
231 map: [648, 338] 237 map: [648, 338]
238 The Wondrous (Doorknob):
239 fold_into: The Wondrous
232 The Wondrous (Bookcase): 240 The Wondrous (Bookcase):
233 fold_into: The Wondrous 241 fold_into: The Wondrous
234 The Wondrous (Chandelier): 242 The Wondrous (Chandelier):
@@ -271,6 +279,8 @@
271 map: [1368, 2103] 279 map: [1368, 2103]
272 Art Gallery: 280 Art Gallery:
273 map: [2474, 1366] 281 map: [2474, 1366]
282 Art Gallery (First Floor):
283 fold_into: Art Gallery
274 Art Gallery (Second Floor): 284 Art Gallery (Second Floor):
275 fold_into: Art Gallery 285 fold_into: Art Gallery
276 Art Gallery (Third Floor): 286 Art Gallery (Third Floor):
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 8f13f09..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: Outside The Undeterred 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,16 +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:
639 - early_ch
633- pos: [1567, 1264] 640- pos: [1567, 1264]
634 tags: 641 tags:
635 - red_ch 642 - red_ch
636- pos: [150, 808] 643- pos: [150, 748]
637 tags: 644 tags:
638 - blue_ch 645 - blue_ch
639- pos: [626, 371] 646- pos: [626, 371]
@@ -651,21 +658,21 @@
651- pos: [1468, 1377] 658- pos: [1468, 1377]
652 room: Outside The Bold 659 room: Outside The Bold
653 door: Bold Entrance 660 door: Bold Entrance
654- pos: [1425, 1358] 661- pos: [1446, 1344]
655 paintings: 662 painting: pencil_painting2
656 - pencil_painting2 663 exits:
657 - north_missing2
658 tags:
659 - pencil_compass 664 - pencil_compass
660 - pencil_starting 665 - pencil_starting
661 - pencil_directional 666 - pencil_directional
667- pos: [1580, 1344]
668 painting: north_missing2
662- pos: [1334, 1419] 669- pos: [1334, 1419]
663 room: Outside The Bold 670 room: Outside The Bold
664 door: Steady Entrance 671 door: Steady Entrance
665- pos: [445, 1048] 672- pos: [445, 1048]
666 tags: 673 exits:
667 - undeterred_artistic 674 - undeterred_artistic
668- pos: [279, 1071] 675- pos: [273, 1071]
669 room: Number Hunt 676 room: Number Hunt
670 door: Zero Door 677 door: Zero Door
671- pos: [338, 1071] 678- pos: [338, 1071]
@@ -680,20 +687,16 @@
680- pos: [242, 1071] 687- pos: [242, 1071]
681 room: Outside The Undeterred 688 room: Outside The Undeterred
682 door: Undeterred Entrance 689 door: Undeterred Entrance
683- pos: [60, 1017] 690- pos: [149, 937]
684 paintings: 691 painting: blueman_painting_2
685 - blueman_painting_2 692 exits:
686 tags:
687 - blueman_courtyard 693 - blueman_courtyard
688 - blueman_starting 694 - blueman_starting
689- pos: [60, 970] 695- pos: [412, 1017]
690 special: early_color_hallways
691- pos: [402, 1012]
692 room: Outside The Undeterred 696 room: Outside The Undeterred
693 door: Green Painting 697 door: Green Painting
694 paintings: 698 painting: maze_painting_3
695 - maze_painting_3 699 entrances:
696 tags:
697 - green_numbers 700 - green_numbers
698- pos: [134, 1007] 701- pos: [134, 1007]
699 sunwarp: 702 sunwarp:
@@ -702,7 +705,7 @@
702- pos: [719, 1039] 705- pos: [719, 1039]
703 room: Outside The Undeterred 706 room: Outside The Undeterred
704 door: Challenge Entrance 707 door: Challenge Entrance
705- pos: [438, 1039] 708- pos: [456, 1039]
706 room: Outside The Undeterred 709 room: Outside The Undeterred
707 door: Number Hunt 710 door: Number Hunt
708- pos: [563, 1071] 711- pos: [563, 1071]
@@ -723,12 +726,11 @@
723- pos: [525, 1002] 726- pos: [525, 1002]
724 room: Number Hunt 727 room: Number Hunt
725 door: Door to Directional Gallery 728 door: Door to Directional Gallery
726- pos: [659, 1014] 729- pos: [572, 1017]
727 room: Number Hunt 730 room: Number Hunt
728 door: Eights 731 door: Eights
729 paintings: 732 painting: smile_painting_5
730 - smile_painting_5 733 entrances:
731 tags:
732 - smiley_numbers 734 - smiley_numbers
733- pos: [557, 953] 735- pos: [557, 953]
734 room: Number Hunt 736 room: Number Hunt
@@ -745,87 +747,67 @@
745- pos: [268, 825] 747- pos: [268, 825]
746 room: Directional Gallery 748 room: Directional Gallery
747 door: Yellow Barrier 749 door: Yellow Barrier
748- pos: [231, 681] 750- pos: [214, 563]
749 room: Number Hunt 751 room: Number Hunt
750 door: Sixes 752 door: Sixes
751- pos: [242, 980] 753- pos: [242, 980]
752 room: Directional Gallery 754 room: Directional Gallery
753 door: Shortcut to The Undeterred 755 door: Shortcut to The Undeterred
754- pos: [351, 927] 756- pos: [364, 942]
755 paintings: 757 painting: boxes_painting
756 - boxes_painting 758 entrances:
757 tags:
758 - lattice_directional 759 - lattice_directional
759- pos: [272, 927] 760- pos: [278, 942]
760 paintings: 761 painting: smile_painting_7
761 - smile_painting_7 762 entrances:
762 tags:
763 - smiley_directional 763 - smiley_directional
764- pos: [214, 822] 764- pos: [278, 803]
765 paintings: 765 painting: cherry_painting
766 - cherry_painting 766 entrances:
767 tags:
768 - apple_directional 767 - apple_directional
769- pos: [266, 735] 768- pos: [262, 573]
770 room: Number Hunt 769 room: Number Hunt
771 door: Sixes 770 door: Sixes
772 paintings: 771 painting: pencil_painting3
773 - pencil_painting3 772 entrances:
774 tags:
775 - pencil_directional 773 - pencil_directional
776- pos: [215, 735] 774- pos: [278, 685]
777 paintings: 775 painting: flower_painting_4
778 - flower_painting_4
779- pos: [626, 851] 776- pos: [626, 851]
780 sunwarp: 777 sunwarp:
781 dots: 6 778 dots: 6
782 type: exit 779 type: exit
783- pos: [1141, 1441]
784 room: Orange Tower Third Floor
785 door: Orange Barrier
786- pos: [1174, 1441]
787 room: Outside The Initiated
788 door: Green Barrier
789- pos: [1205, 1441]
790 room: Outside The Initiated
791 door: Purple Barrier
792- pos: [1334, 1377] 780- pos: [1334, 1377]
793 room: Color Hunt 781 room: Color Hunt
794 door: Shortcut to The Steady 782 door: Shortcut to The Steady
795- pos: [1280, 1375] 783- pos: [1312, 1333]
796 paintings: 784 painting: arrows_painting_7
797 - arrows_painting_7 785- pos: [1226, 1333]
798- pos: [1233, 1321]
799 room: Outside The Initiated 786 room: Outside The Initiated
800 door: Entrance 787 door: Entrance
801 paintings: 788 painting: fruitbowl_painting3
802 - fruitbowl_painting3 789- pos: [1260, 1323]
803- pos: [1290, 1323]
804 sunwarp: 790 sunwarp:
805 dots: 5 791 dots: 5
806 type: exit 792 type: exit
807- pos: [1189, 1356] 793- pos: [1092, 1333]
808 room: Outside The Initiated 794 painting: colors_painting
809 door: Entrance
810- pos: [1154, 1332]
811 paintings:
812 - colors_painting
813- pos: [1640, 1260] 795- pos: [1640, 1260]
814 room: The Bearer 796 room: The Bearer
815 door: Backside Door 797 door: Backside Door
816- pos: [1468, 1287] 798- pos: [1468, 1287]
817 room: The Bearer 799 room: The Bearer
818 door: Entrance 800 door: Entrance
819- pos: [1430, 1232] 801- pos: [1431, 1233]
820 room: Number Hunt 802 room: Number Hunt
821 door: Sixes 803 door: Sixes
822- pos: [1388, 1152] 804- pos: [1388, 1152]
823 room: Bearer Side Area 805 room: Bearer Side Area
824 door: Shortcut to Tower 806 door: Shortcut to Tower
825- pos: [1273, 1142] 807- pos: [1264, 1430]
826 paintings: 808 painting: pencil_painting5
827 - pencil_painting5 809- pos: [1291, 1430]
828 - pencil_painting4 810 painting: pencil_painting4
829- pos: [1355, 1092] 811- pos: [1355, 1092]
830 room: Knight Night (Final) 812 room: Knight Night (Final)
831 door: Exit 813 door: Exit
@@ -836,10 +818,9 @@
836 # 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
837 room: Number Hunt 819 room: Number Hunt
838 door: Sevens 820 door: Sevens
839- pos: [1653, 101] 821- pos: [1687, 117]
840 paintings: 822 painting: smile_painting_9
841 - smile_painting_9 823 exits:
842 tags:
843 - smiley_crossroads 824 - smiley_crossroads
844 - smiley_deadend 825 - smiley_deadend
845 - smiley_hotcrusts 826 - smiley_hotcrusts
@@ -848,88 +829,113 @@
848 - smiley_initiated 829 - smiley_initiated
849 - smiley_gallery 830 - smiley_gallery
850 - smiley_theysee 831 - smiley_theysee
851- pos: [1656, 139] 832- pos: [1677, 161]
852 room: The Artistic (Smiley) 833 room: The Artistic (Smiley)
853 door: Door to Panda 834 door: Door to Panda
854- pos: [1711, 140] 835- pos: [1711, 140]
855 tags: 836 entrances:
856 - undeterred_artistic 837 - undeterred_artistic
857- pos: [1653, 169] 838- pos: [1687, 224]
858 paintings: 839 painting: panda_painting_3
859 - panda_painting_3 840 exits:
860 tags:
861 - panda_tower6 841 - panda_tower6
862 - panda_hallway 842 - panda_hallway
863- pos: [1708, 171] 843- pos: [1731, 215]
864 room: The Artistic (Panda) 844 room: The Artistic (Panda)
865 door: Door to Lattice 845 door: Door to Lattice
866- pos: [1761, 169] 846- pos: [1794, 224]
867 paintings: 847 painting: boxes_painting2
868 - boxes_painting2 848 exits:
869 tags:
870 - lattice_directional 849 - lattice_directional
871- pos: [1762, 139] 850- pos: [1785, 161]
872 room: The Artistic (Lattice) 851 room: The Artistic (Lattice)
873 door: Door to Apple 852 door: Door to Apple
874- pos: [1761, 101] 853- pos: [1794, 117]
875 paintings: 854 painting: cherry_painting3
876 - cherry_painting3 855 exits:
877 tags:
878 - apple_tower6 856 - apple_tower6
879 - apple_directional 857 - apple_directional
880- pos: [1708, 107] 858- pos: [1731, 107]
881 room: The Artistic (Apple) 859 room: The Artistic (Apple)
882 door: Door to Smiley 860 door: Door to Smiley
883- pos: [370, 681] 861- pos: [370, 563]
884 room: Number Hunt 862 room: Number Hunt
885 door: Eights 863 door: Eights
886- pos: [411, 685] 864- pos: [637, 605]
887 paintings: 865 painting: eye_painting
888 - eye_painting_2 866 entrances:
889 - smile_painting_2 867 - crossroads_eyewall
890 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:
891 - smiley_theysee 878 - smiley_theysee
892- pos: [310, 750] 879- pos: [311, 750]
893 room: The Eyes They See 880 room: The Eyes They See
894 door: Exit 881 door: Exit
895- pos: [334, 798] 882- pos: [348, 803]
896 paintings: 883 painting: arrows_painting_5
897 - arrows_painting_5 884- pos: [370, 751]
898- pos: [370, 792]
899 room: Outside The Wondrous 885 room: Outside The Wondrous
900 door: Wondrous Entrance 886 door: Wondrous Entrance
901- pos: [367, 752] 887- pos: [428, 696]
902 paintings: 888 painting: symmetry_painting_a_1
903 - symmetry_painting_a_1 889 exits:
904 - symmetry_painting_b_1
905 - symmetry_painting_a_3
906 - symmetry_painting_a_5
907 - symmetry_painting_b_4
908 - symmetry_painting_a_2
909 - symmetry_painting_b_2
910 - symmetry_painting_a_6
911 - symmetry_painting_b_6
912 tags:
913 - symmetry_starting 890 - symmetry_starting
914- 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]
915 room: The Wondrous 921 room: The Wondrous
916 door: Exit 922 door: Exit
917 paintings: 923- pos: [535, 647]
918 - arrows_painting_9 924 room: The Wondrous
919- pos: [449, 755] 925 door: Exit
920 paintings: 926 painting: arrows_painting_9
921 - flower_painting_6 927- pos: [610, 674]
922 tags: 928 painting: flower_painting_6
929 entrances:
923 - flower_arrow 930 - flower_arrow
924- pos: [1101, 222] 931- pos: [1156, 262]
925 paintings: 932 painting: panda_painting
926 - panda_painting 933 entrances:
927 tags:
928 - panda_hallway 934 - panda_hallway
929- pos: [1152, 209] 935- pos: [1152, 209]
930 room: Hallway Room (1) 936 room: Hallway Room (1)
931 door: Exit 937 door: Exit
932- pos: [1189, 170] 938- pos: [1190, 171]
933 room: Hallway Room (2) 939 room: Hallway Room (2)
934 door: Exit 940 door: Exit
935- pos: [1238, 124] 941- pos: [1238, 124]
@@ -944,16 +950,17 @@
944- pos: [1415, 140] 950- pos: [1415, 140]
945 room: Number Hunt 951 room: Number Hunt
946 door: Nines 952 door: Nines
947- pos: [1458, 133] 953- pos: [1424, 85]
948 paintings: 954 painting: south_afar
949 - south_afar
950- pos: [826, 1452] 955- pos: [826, 1452]
951 room: Outside The Wanderer 956 room: Outside The Wanderer
952 door: Wanderer Entrance 957 door: Wanderer Entrance
958 tilted: true
953- pos: [763, 1465] 959- pos: [763, 1465]
954 room: Outside The Wanderer 960 room: Outside The Wanderer
955 door: Tower Entrance 961 door: Tower Entrance
956- pos: [1655, 1151] 962 tilted: true
963- pos: [1656, 1152]
957 room: Number Hunt 964 room: Number Hunt
958 door: Eights 965 door: Eights
959- pos: [1623, 1044] 966- pos: [1623, 1044]
@@ -971,23 +978,26 @@
971- pos: [1511, 1119] 978- pos: [1511, 1119]
972 room: Art Gallery 979 room: Art Gallery
973 door: Exit 980 door: Exit
974- pos: [1654, 1116] 981- pos: [1730, 1162]
975 paintings: 982 painting: flower_painting_2
976 - smile_painting_3 983- pos: [1730, 1189]
977 - flower_painting_2 984 painting: map_painting
978 - scenery_painting_0a 985- pos: [1698, 1189]
979 - map_painting 986 painting: smile_painting_3
980 - fruitbowl_painting4 987 entrances:
981 tags:
982 - smiley_gallery 988 - smiley_gallery
989- pos: [1714, 1215]
990 painting: fruitbowl_painting4
991- pos: [1714, 1242]
992 painting: scenery_painting_0a
983- pos: [1120, 1286] 993- pos: [1120, 1286]
984 room: Rhyme Room (Smiley) 994 room: Rhyme Room (Smiley)
985 door: Door to Target 995 door: Door to Target
986- pos: [1120, 1315] 996- pos: [1120, 1315]
987 tags: 997 entrances: # this could be considered 2 way since the subway map has a one way gate at the exit anyway
988 - rhyme_smiley_target 998 - rhyme_smiley_target
989- pos: [792, 1137] 999- pos: [792, 1137]
990 tags: 1000 exits:
991 - rhyme_smiley_target 1001 - rhyme_smiley_target
992- pos: [895, 1217] 1002- pos: [895, 1217]
993 room: Number Hunt 1003 room: Number Hunt
@@ -998,9 +1008,8 @@
998- pos: [1120, 1195] 1008- pos: [1120, 1195]
999 room: Rhyme Room (Circle) 1009 room: Rhyme Room (Circle)
1000 door: Door to Smiley 1010 door: Door to Smiley
1001- pos: [1118, 1137] 1011- pos: [1130, 1124]
1002 paintings: 1012 painting: arrows_painting_3
1003 - arrows_painting_3
1004- pos: [1050, 1142] 1013- pos: [1050, 1142]
1005 room: Rhyme Room (Looped Square) 1014 room: Rhyme Room (Looped Square)
1006 door: Door to Circle 1015 door: Door to Circle
@@ -1013,29 +1022,41 @@
1013- pos: [852, 1200] 1022- pos: [852, 1200]
1014 room: Rhyme Room (Target) 1023 room: Rhyme Room (Target)
1015 door: Door to Cross 1024 door: Door to Cross
1016- pos: [850, 1138] 1025- pos: [862, 1124]
1017 paintings: 1026 painting: arrows_painting_4
1018 - arrows_painting_4
1019- pos: [1652, 1393]
1020 room: Room Room
1021 door: Excavation
1022- pos: [1592, 1442] 1027- pos: [1592, 1442]
1023 room: Room Room 1028 room: Room Room
1024 door: Cellar Exit 1029 door: Cellar Exit
1025- pos: [1570, 938] 1030- pos: [1623, 938]
1026 room: Outside The Wise 1031 room: Outside The Wise
1027 door: Wise Entrance 1032 door: Wise Entrance
1028- pos: [1653, 935] 1033- pos: [1665, 920]
1029 paintings: 1034 painting: clock_painting_3
1030 - clock_painting_3 1035- pos: [241, 348]
1031- pos: [369, 605]
1032 room: Outside The Scientific 1036 room: Outside The Scientific
1033 door: Scientific Entrance 1037 door: Scientific Entrance
1034- pos: [294, 602] 1038- pos: [176, 326]
1035 paintings: 1039 painting: hi_solved_painting4
1036 - hi_solved_painting4 1040 entrances:
1037 tags:
1038 - hi_scientific 1041 - hi_scientific
1039- pos: [814, 1001] 1042- pos: [815, 1002]
1040 room: Challenge Room 1043 room: Challenge Room
1041 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
25void AchievementsPane::UpdateIndicators() { 26void 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
12class AchievementsPane : public wxListView { 16class 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 f245c2b..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -4,11 +4,13 @@
4#define _WEBSOCKETPP_CPP11_STRICT_ 4#define _WEBSOCKETPP_CPP11_STRICT_
5#pragma comment(lib, "crypt32") 5#pragma comment(lib, "crypt32")
6 6
7#include <fmt/core.h>
7#include <hkutil/string.h> 8#include <hkutil/string.h>
8 9
9#include <any> 10#include <any>
10#include <apclient.hpp> 11#include <apclient.hpp>
11#include <apuuid.hpp> 12#include <apuuid.hpp>
13#include <bitset>
12#include <chrono> 14#include <chrono>
13#include <exception> 15#include <exception>
14#include <filesystem> 16#include <filesystem>
@@ -21,42 +23,73 @@
21#include <tuple> 23#include <tuple>
22 24
23#include "game_data.h" 25#include "game_data.h"
26#include "ipc_state.h"
27#include "logger.h"
24#include "tracker_frame.h" 28#include "tracker_frame.h"
25#include "tracker_state.h" 29#include "tracker_state.h"
26 30
27constexpr int AP_MAJOR = 0; 31constexpr int AP_MAJOR = 0;
28constexpr int AP_MINOR = 4; 32constexpr int AP_MINOR = 6;
29constexpr int AP_REVISION = 5; 33constexpr int AP_REVISION = 1;
30 34
31constexpr const char* CERT_STORE_PATH = "cacert.pem"; 35constexpr const char* CERT_STORE_PATH = "cacert.pem";
32constexpr int ITEM_HANDLING = 7; // <- all 36constexpr int ITEM_HANDLING = 7; // <- all
33 37
38constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds
39constexpr int CONNECTION_BACKOFF_INTERVAL = 100;
40
41constexpr int PANEL_COUNT = 803;
42constexpr int PANEL_BITFIELD_LENGTH = 48;
43constexpr int PANEL_BITFIELDS = 17;
44
34namespace { 45namespace {
35 46
36struct APState { 47const std::set<long> kNonProgressionItems = {
37 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};
38 58
59struct APState {
60 // Initialized on main thread
39 bool initialized = false; 61 bool initialized = false;
40
41 TrackerFrame* tracker_frame = nullptr; 62 TrackerFrame* tracker_frame = nullptr;
63 std::list<std::string> tracked_data_storage_keys;
42 64
43 bool client_active = false; 65 // Client
44 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.";
45 73
46 bool connected = false; 74 bool connected = false;
47 bool has_connection_result = false; 75 std::string connection_failure;
76 int remaining_loops = 0;
48 77
49 std::string data_storage_prefix; 78 std::string data_storage_prefix;
50 std::list<std::string> tracked_data_storage_keys;
51 std::string victory_data_storage_key; 79 std::string victory_data_storage_key;
52 80
81 std::string save_name;
82
53 std::map<int64_t, int> inventory; 83 std::map<int64_t, int> inventory;
54 std::set<int64_t> checked_locations; 84 std::set<int64_t> checked_locations;
55 std::map<std::string, std::any> data_storage; 85 std::map<std::string, std::any> data_storage;
56 std::optional<std::tuple<int, int>> player_pos; 86 std::optional<std::tuple<int, int>> player_pos;
87 std::bitset<PANEL_COUNT> solved_panels;
57 88
58 DoorShuffleMode door_shuffle_mode = kNO_DOORS; 89 DoorShuffleMode door_shuffle_mode = kNO_DOORS;
90 bool group_doors = false;
59 bool color_shuffle = false; 91 bool color_shuffle = false;
92 PanelShuffleMode panel_shuffle_mode = kNO_PANELS;
60 bool painting_shuffle = false; 93 bool painting_shuffle = false;
61 int mastery_requirement = 21; 94 int mastery_requirement = 21;
62 int level_2_requirement = 223; 95 int level_2_requirement = 223;
@@ -68,37 +101,197 @@ struct APState {
68 bool pilgrimage_allows_paintings = false; 101 bool pilgrimage_allows_paintings = false;
69 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; 102 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
70 bool sunwarp_shuffle = false; 103 bool sunwarp_shuffle = false;
104 bool postgame_shuffle = true;
71 105
72 std::map<std::string, std::string> painting_mapping; 106 std::map<std::string, std::string> painting_mapping;
107 std::set<std::string> painting_codomain;
73 std::map<int, SunwarpMapping> sunwarp_mapping; 108 std::map<int, SunwarpMapping> sunwarp_mapping;
74 109
75 void Connect(std::string server, std::string player, std::string password) { 110 void Connect(std::string server, std::string player, std::string password) {
76 if (!initialized) { 111 Initialize();
77 wxLogVerbose("Initializing APState...");
78 112
79 std::thread([this]() { 113 {
80 for (;;) { 114 std::lock_guard state_guard(state_mutex);
81 { 115 SetStatusMessage("Connecting to Archipelago server....");
82 std::lock_guard client_guard(client_mutex); 116 }
83 if (apclient) { 117 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
84 apclient->poll();
85 }
86 }
87 118
88 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 119 // Creating and setting up the client has to all be done while holding the
89 } 120 // client mutex, so that the other thread doesn't try to poll before we add
90 }).detach(); 121 // handlers, etc.
122 {
123 TrackerLog("Destroying old AP client...");
124
125 std::lock_guard client_guard(client_mutex);
91 126
92 for (int panel_id : GD_GetAchievementPanels()) { 127 if (apclient) {
93 tracked_data_storage_keys.push_back( 128 DestroyClient();
94 "Achievement|" + GD_GetPanel(panel_id).achievement_name);
95 } 129 }
96 130
97 for (const MapArea& map_area : GD_GetMapAreas()) { 131 std::string cert_store = "";
98 for (const Location& location : map_area.locations) { 132 if (std::filesystem::exists(CERT_STORE_PATH)) {
99 tracked_data_storage_keys.push_back( 133 cert_store = CERT_STORE_PATH;
100 "Hunt|" + std::to_string(location.ap_location_id)); 134 }
101 } 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() {
288 if (!initialized) {
289 TrackerLog("Initializing APState...");
290
291 std::thread([this]() { Thread(); }).detach();
292
293 for (int i = 0; i < PANEL_BITFIELDS; i++) {
294 tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i));
102 } 295 }
103 296
104 tracked_data_storage_keys.push_back("PlayerPos"); 297 tracked_data_storage_keys.push_back("PlayerPos");
@@ -106,125 +299,189 @@ struct APState {
106 299
107 initialized = true; 300 initialized = true;
108 } 301 }
302 }
109 303
110 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 304 void Thread() {
111 wxLogStatus("Connecting to Archipelago server (%s)...", server); 305 std::string display_error;
112 306
113 { 307 for (;;) {
114 wxLogVerbose("Destroying old AP client..."); 308 {
309 std::lock_guard client_guard(client_mutex);
310 if (apclient) {
311 apclient->poll();
115 312
116 std::lock_guard client_guard(client_mutex); 313 {
314 std::lock_guard state_guard(state_mutex);
117 315
118 if (apclient) { 316 if (!connected) {
119 DestroyClient(); 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 }
337 }
338 }
339 }
120 } 340 }
121 341
122 std::string cert_store = ""; 342 if (!display_error.empty()) {
123 if (std::filesystem::exists(CERT_STORE_PATH)) { 343 wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR);
124 cert_store = CERT_STORE_PATH; 344 display_error.clear();
125 } 345 }
126 346
127 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, 347 std::this_thread::sleep_for(std::chrono::milliseconds(100));
128 cert_store);
129 } 348 }
349 }
350
351 void OnRoomInfo(std::string player, std::string password) {
352 {
353 std::lock_guard state_guard(state_mutex);
130 354
131 inventory.clear();
132 checked_locations.clear();
133 data_storage.clear();
134 player_pos = std::nullopt;
135 victory_data_storage_key.clear();
136 door_shuffle_mode = kNO_DOORS;
137 color_shuffle = false;
138 painting_shuffle = false;
139 painting_mapping.clear();
140 mastery_requirement = 21;
141 level_2_requirement = 223;
142 location_checks = kNORMAL_LOCATIONS;
143 victory_condition = kTHE_END;
144 early_color_hallways = false;
145 pilgrimage_enabled = false;
146 pilgrimage_allows_roof_access = false;
147 pilgrimage_allows_paintings = false;
148 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
149 sunwarp_shuffle = false;
150 sunwarp_mapping.clear();
151
152 connected = false;
153 has_connection_result = false;
154
155 apclient->set_room_info_handler([this, player, password]() {
156 inventory.clear(); 355 inventory.clear();
157 356
158 wxLogStatus("Connected to Archipelago server. Authenticating as %s %s", 357 SetStatusMessage("Connected to Archipelago server. Authenticating...");
159 player, 358 }
160 (password.empty() ? "without password"
161 : "with password " + password));
162 tracker_frame->SetStatusMessage(
163 "Connected to Archipelago server. Authenticating...");
164
165 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
166 {AP_MAJOR, AP_MINOR, AP_REVISION});
167 });
168
169 apclient->set_location_checked_handler(
170 [this](const std::list<int64_t>& locations) {
171 for (const int64_t location_id : locations) {
172 checked_locations.insert(location_id);
173 wxLogVerbose("Location: %lld", location_id);
174 }
175 359
176 RefreshTracker(false); 360 TrackerLog(fmt::format(
177 }); 361 "Connected to Archipelago server. Authenticating as {} {}", player,
178 362 (password.empty() ? "without password" : "with password " + password)));
179 apclient->set_slot_disconnected_handler([this]() {
180 tracker_frame->SetStatusMessage(
181 "Disconnected from Archipelago. Attempting to reconnect...");
182 wxLogStatus(
183 "Slot disconnected from Archipelago. Attempting to reconnect...");
184 });
185
186 apclient->set_socket_disconnected_handler([this]() {
187 tracker_frame->SetStatusMessage(
188 "Disconnected from Archipelago. Attempting to reconnect...");
189 wxLogStatus(
190 "Socket disconnected from Archipelago. Attempting to reconnect...");
191 });
192
193 apclient->set_items_received_handler(
194 [this](const std::list<APClient::NetworkItem>& items) {
195 for (const APClient::NetworkItem& item : items) {
196 inventory[item.item]++;
197 wxLogVerbose("Item: %lld", item.item);
198 }
199 363
200 RefreshTracker(false); 364 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
201 }); 365 {AP_MAJOR, AP_MINOR, AP_REVISION});
366 }
202 367
203 apclient->set_retrieved_handler( 368 void OnLocationChecked(const std::list<int64_t>& locations) {
204 [this](const std::map<std::string, nlohmann::json>& data) { 369 {
205 for (const auto& [key, value] : data) { 370 std::lock_guard state_guard(state_mutex);
206 HandleDataStorage(key, value); 371
207 } 372 for (const int64_t location_id : locations) {
373 checked_locations.insert(location_id);
374 TrackerLog(fmt::format("Location: {}", location_id));
375 }
376 }
377
378 RefreshTracker(StateUpdate{.cleared_locations = true});
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 }
417 }
418
419 for (const auto& [item_id, item_index] : index_by_item) {
420 item_states.push_back(ItemState{.name = GD_GetItemName(item_id),
421 .amount = inventory[item_id],
422 .index = item_index});
423 }
424 }
425
426 RefreshTracker(StateUpdate{.items = item_states,
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 }
439 }
208 440
209 RefreshTracker(false); 441 RefreshTracker(state_update);
210 }); 442 }
443
444 void OnSetReply(const std::string& key, const nlohmann::json& value) {
445 StateUpdate state_update;
446
447 {
448 std::lock_guard state_guard(state_mutex);
449 HandleDataStorage(key, value, state_update);
450 }
451
452 RefreshTracker(state_update);
453 }
454
455 void OnSlotConnected(std::string player, std::string server,
456 const nlohmann::json& slot_data) {
457 IPC_SetTrackerSlot(server, player);
458
459 TrackerLog("Connected to Archipelago!");
211 460
212 apclient->set_set_reply_handler([this](const std::string& key, 461 {
213 const nlohmann::json& value, 462 std::lock_guard state_guard(state_mutex);
214 const nlohmann::json&) {
215 HandleDataStorage(key, value);
216 RefreshTracker(false);
217 });
218 463
219 apclient->set_slot_connected_handler([this]( 464 SetStatusMessage(
220 const nlohmann::json& slot_data) { 465 fmt::format("Connected to Archipelago! ({}@{}).", player, server));
221 tracker_frame->SetStatusMessage("Connected to Archipelago!");
222 wxLogStatus("Connected to Archipelago!");
223 466
467 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(),
468 apclient->get_player_number());
224 data_storage_prefix = 469 data_storage_prefix =
225 "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; 470 fmt::format("Lingo_{}_", apclient->get_player_number());
226 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 }
227 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>();
228 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 485 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
229 mastery_requirement = slot_data["mastery_achievements"].get<int>(); 486 mastery_requirement = slot_data["mastery_achievements"].get<int>();
230 level_2_requirement = slot_data["level_2_requirement"].get<int>(); 487 level_2_requirement = slot_data["level_2_requirement"].get<int>();
@@ -246,6 +503,9 @@ struct APState {
246 : kSUNWARP_ACCESS_NORMAL; 503 : kSUNWARP_ACCESS_NORMAL;
247 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && 504 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
248 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;
249 509
250 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { 510 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
251 painting_mapping.clear(); 511 painting_mapping.clear();
@@ -253,6 +513,7 @@ struct APState {
253 for (const auto& mapping_it : 513 for (const auto& mapping_it :
254 slot_data["painting_entrance_to_exit"].items()) { 514 slot_data["painting_entrance_to_exit"].items()) {
255 painting_mapping[mapping_it.key()] = mapping_it.value(); 515 painting_mapping[mapping_it.key()] = mapping_it.value();
516 painting_codomain.insert(mapping_it.value());
256 } 517 }
257 } 518 }
258 519
@@ -268,193 +529,165 @@ struct APState {
268 } 529 }
269 } 530 }
270 531
271 connected = true;
272 has_connection_result = true;
273
274 RefreshTracker(true);
275
276 std::list<std::string> corrected_keys; 532 std::list<std::string> corrected_keys;
277 for (const std::string& key : tracked_data_storage_keys) { 533 for (const std::string& key : tracked_data_storage_keys) {
278 corrected_keys.push_back(data_storage_prefix + key); 534 corrected_keys.push_back(data_storage_prefix + key);
279 } 535 }
280 536
281 { 537 victory_data_storage_key =
282 std::ostringstream vdsks; 538 fmt::format("_read_client_status_{}_{}", apclient->get_team_number(),
283 vdsks << "_read_client_status_" << apclient->get_team_number() << "_" 539 apclient->get_player_number());
284 << apclient->get_player_number();
285 victory_data_storage_key = vdsks.str();
286 }
287 540
288 corrected_keys.push_back(victory_data_storage_key); 541 corrected_keys.push_back(victory_data_storage_key);
289 542
290 apclient->Get(corrected_keys); 543 apclient->Get(corrected_keys);
291 apclient->SetNotify(corrected_keys); 544 apclient->SetNotify(corrected_keys);
292 });
293
294 apclient->set_slot_refused_handler(
295 [this](const std::list<std::string>& errors) {
296 connected = false;
297 has_connection_result = true;
298
299 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
300
301 std::vector<std::string> error_messages;
302 error_messages.push_back("Could not connect to Archipelago.");
303
304 for (const std::string& error : errors) {
305 if (error == "InvalidSlot") {
306 error_messages.push_back("Invalid player name.");
307 } else if (error == "InvalidGame") {
308 error_messages.push_back(
309 "The specified player is not playing Lingo.");
310 } else if (error == "IncompatibleVersion") {
311 error_messages.push_back(
312 "The Archipelago server is not the correct version for this "
313 "client.");
314 } else if (error == "InvalidPassword") {
315 error_messages.push_back("Incorrect password.");
316 } else if (error == "InvalidItemsHandling") {
317 error_messages.push_back(
318 "Invalid item handling flag. This is a bug with the tracker. "
319 "Please report it to the lingo-ap-tracker GitHub.");
320 } else {
321 error_messages.push_back("Unknown error.");
322 }
323 }
324
325 std::string full_message = hatkirby::implode(error_messages, " ");
326 wxLogError(wxString(full_message));
327
328 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
329 });
330
331 client_active = true;
332 545
333 int timeout = 5000; // 5 seconds 546 connected = true;
334 int interval = 100; 547 }
335 int remaining_loops = timeout / interval;
336 while (!has_connection_result) {
337 if (interval == 0) {
338 connected = false;
339 has_connection_result = true;
340 548
341 DestroyClient(); 549 ResetReachabilityRequirements();
550 RefreshTracker(std::nullopt);
551 }
342 552
343 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 553 void OnSlotRefused(const std::list<std::string>& errors) {
344 wxLogStatus("Timeout while connecting to Archipelago server."); 554 std::vector<std::string> error_messages;
345 wxMessageBox("Timeout while connecting to Archipelago server.", 555 error_messages.push_back("Could not connect to Archipelago.");
346 "Connection failed", wxOK | wxICON_ERROR); 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.");
347 } 574 }
575 }
348 576
349 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, " ");
350 580
351 interval--; 581 SetStatusMessage("Disconnected from Archipelago.");
352 } 582 }
583 }
353 584
354 if (connected) { 585 // Assumes state mutex is locked.
355 RefreshTracker(false); 586 void SetStatusMessage(std::string msg) {
356 } else { 587 status_message = std::move(msg);
357 client_active = false; 588
358 } 589 tracker_frame->UpdateStatusMessage();
359 } 590 }
360 591
361 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) {
362 if (value.is_boolean()) { 594 if (value.is_boolean()) {
363 data_storage[key] = value.get<bool>(); 595 data_storage[key] = value.get<bool>();
364 wxLogVerbose("Data storage %s retrieved as %s", key, 596 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
365 (value.get<bool>() ? "true" : "false")); 597 (value.get<bool>() ? "true" : "false")));
598
366 } else if (value.is_number()) { 599 } else if (value.is_number()) {
367 data_storage[key] = value.get<int>(); 600 data_storage[key] = value.get<int>();
368 wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>()); 601 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
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 }
369 } else if (value.is_object()) { 622 } else if (value.is_object()) {
370 if (key.ends_with("PlayerPos")) { 623 if (key.ends_with("PlayerPos")) {
371 auto map_value = value.get<std::map<std::string, int>>(); 624 auto map_value = value.get<std::map<std::string, int>>();
372 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;
373 } else { 627 } else {
374 data_storage[key] = value.get<std::map<std::string, int>>(); 628 data_storage[key] = value.get<std::map<std::string, int>>();
375 } 629 }
376 630
377 wxLogVerbose("Data storage %s retrieved as dictionary", key); 631 TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key));
378 } else if (value.is_null()) { 632 } else if (value.is_null()) {
379 if (key.ends_with("PlayerPos")) { 633 if (key.ends_with("PlayerPos")) {
380 player_pos = std::nullopt; 634 player_pos = std::nullopt;
635 state_update.player_position = true;
381 } else { 636 } else {
382 data_storage.erase(key); 637 data_storage.erase(key);
383 } 638 }
384 639
385 wxLogVerbose("Data storage %s retrieved as null", key); 640 TrackerLog(fmt::format("Data storage {} retrieved as null", key));
386 } else if (value.is_array()) { 641 } else if (value.is_array()) {
387 auto list_value = value.get<std::vector<std::string>>(); 642 auto list_value = value.get<std::vector<std::string>>();
388 643
389 if (key.ends_with("Paintings")) { 644 if (key.ends_with("Paintings")) {
390 data_storage[key] = 645 data_storage[key] =
391 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());
392 } else { 649 } else {
393 data_storage[key] = list_value; 650 data_storage[key] = list_value;
394 } 651 }
395 652
396 wxLogVerbose("Data storage %s retrieved as list: [%s]", key, 653 TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key,
397 hatkirby::implode(list_value, ", ")); 654 hatkirby::implode(list_value, ", ")));
398 } 655 }
399 } 656 }
400 657
401 bool HasCheckedGameLocation(int location_id) { 658 // State mutex should NOT be locked.
402 return checked_locations.count(location_id); 659 // nullopt state_update indicates a reset.
403 } 660 void RefreshTracker(std::optional<StateUpdate> state_update) {
661 TrackerLog("Refreshing display...");
404 662
405 bool HasCheckedHuntPanel(int location_id) { 663 if (!state_update || state_update->progression_items ||
406 std::string key = 664 !state_update->paintings.empty()) {
407 data_storage_prefix + "Hunt|" + std::to_string(location_id); 665 std::string prev_msg;
408 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 666 {
409 } 667 std::lock_guard state_guard(state_mutex);
410
411 bool HasItem(int item_id, int quantity) {
412 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
413 }
414
415 bool HasAchievement(const std::string& name) {
416 std::string key = data_storage_prefix + "Achievement|" + name;
417 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
418 }
419 668
420 const std::set<std::string>& GetCheckedPaintings() { 669 prev_msg = status_message;
421 std::string key = data_storage_prefix + "Paintings"; 670 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
422 if (!data_storage.count(key)) { 671 }
423 data_storage[key] = std::set<std::string>();
424 }
425 672
426 return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); 673 RecalculateReachability();
427 }
428 674
429 void RefreshTracker(bool reset) { 675 {
430 wxLogVerbose("Refreshing display..."); 676 std::lock_guard state_guard(state_mutex);
431 677
432 RecalculateReachability(); 678 SetStatusMessage(prev_msg);
679 }
680 }
681
433 682
434 if (reset) { 683 if (!state_update) {
435 tracker_frame->ResetIndicators(); 684 tracker_frame->ResetIndicators();
436 } else { 685 } else {
437 tracker_frame->UpdateIndicators(); 686 tracker_frame->UpdateIndicators(*state_update);
438 } 687 }
439 } 688 }
440 689
441 int64_t GetItemId(const std::string& item_name) {
442 int64_t ap_id = apclient->get_item_id(item_name);
443 if (ap_id == APClient::INVALID_NAME_ID) {
444 wxLogError("Could not find AP item ID for %s", item_name);
445 }
446
447 return ap_id;
448 }
449
450 bool HasReachedGoal() {
451 return data_storage.count(victory_data_storage_key) &&
452 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
453 30; // CLIENT_GOAL
454 }
455
456 void DestroyClient() { 690 void DestroyClient() {
457 client_active = false;
458 apclient->reset(); 691 apclient->reset();
459 apclient.reset(); 692 apclient.reset();
460 } 693 }
@@ -473,79 +706,179 @@ void AP_Connect(std::string server, std::string player, std::string password) {
473 GetState().Connect(server, player, password); 706 GetState().Connect(server, player, password);
474} 707}
475 708
476bool AP_HasCheckedGameLocation(int location_id) { 709std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); }
477 return GetState().HasCheckedGameLocation(location_id); 710
711std::string AP_GetSaveName() {
712 std::lock_guard state_guard(GetState().state_mutex);
713
714 return GetState().save_name;
478} 715}
479 716
480bool AP_HasCheckedHuntPanel(int location_id) { 717bool AP_HasCheckedGameLocation(int location_id) {
481 return GetState().HasCheckedHuntPanel(location_id); 718 return GetState().HasCheckedGameLocation(location_id);
482} 719}
483 720
484bool AP_HasItem(int item_id, int quantity) { 721bool AP_HasItem(int item_id, int quantity) {
485 return GetState().HasItem(item_id, quantity); 722 return GetState().HasItem(item_id, quantity);
486} 723}
487 724
488DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 725bool AP_HasItemSafe(int item_id, int quantity) {
726 return GetState().HasItemSafe(item_id, quantity);
727}
728
729DoorShuffleMode AP_GetDoorShuffleMode() {
730 std::lock_guard state_guard(GetState().state_mutex);
489 731
490bool AP_IsColorShuffle() { return GetState().color_shuffle; } 732 return GetState().door_shuffle_mode;
733}
734
735bool AP_AreDoorsGrouped() {
736 std::lock_guard state_guard(GetState().state_mutex);
737
738 return GetState().group_doors;
739}
740
741bool AP_IsColorShuffle() {
742 std::lock_guard state_guard(GetState().state_mutex);
491 743
492bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 744 return GetState().color_shuffle;
745}
746
747bool AP_IsPaintingShuffle() {
748 std::lock_guard state_guard(GetState().state_mutex);
749
750 return GetState().painting_shuffle;
751}
752
753std::map<std::string, std::string> AP_GetPaintingMapping() {
754 std::lock_guard state_guard(GetState().state_mutex);
493 755
494const std::map<std::string, std::string>& AP_GetPaintingMapping() {
495 return GetState().painting_mapping; 756 return GetState().painting_mapping;
496} 757}
497 758
498const std::set<std::string>& AP_GetCheckedPaintings() { 759bool AP_IsPaintingMappedTo(const std::string& painting_id) {
760 std::lock_guard state_guard(GetState().state_mutex);
761
762 return GetState().painting_codomain.count(painting_id);
763}
764
765std::set<std::string> AP_GetCheckedPaintings() {
499 return GetState().GetCheckedPaintings(); 766 return GetState().GetCheckedPaintings();
500} 767}
501 768
502int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 769bool AP_IsPaintingChecked(const std::string& painting_id) {
770 return GetState().IsPaintingChecked(painting_id);
771}
772
773void AP_RevealPaintings() { GetState().RevealPaintings(); }
774
775int AP_GetMasteryRequirement() {
776 std::lock_guard state_guard(GetState().state_mutex);
777
778 return GetState().mastery_requirement;
779}
780
781int AP_GetLevel2Requirement() {
782 std::lock_guard state_guard(GetState().state_mutex);
783
784 return GetState().level_2_requirement;
785}
503 786
504int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 787LocationChecks AP_GetLocationsChecks() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
790 return GetState().location_checks;
791}
505 792
506bool AP_IsLocationVisible(int classification) { 793bool AP_IsLocationVisible(int classification) {
794 std::lock_guard state_guard(GetState().state_mutex);
795
796 int world_state = 0;
797
507 switch (GetState().location_checks) { 798 switch (GetState().location_checks) {
508 case kNORMAL_LOCATIONS: 799 case kNORMAL_LOCATIONS:
509 return classification & kLOCATION_NORMAL; 800 world_state = kLOCATION_NORMAL;
801 break;
510 case kREDUCED_LOCATIONS: 802 case kREDUCED_LOCATIONS:
511 return classification & kLOCATION_REDUCED; 803 world_state = kLOCATION_REDUCED;
804 break;
512 case kPANELSANITY: 805 case kPANELSANITY:
513 return classification & kLOCATION_INSANITY; 806 world_state = kLOCATION_INSANITY;
807 break;
514 default: 808 default:
515 return false; 809 return false;
516 } 810 }
811
812 if (GetState().door_shuffle_mode == kDOORS_MODE &&
813 !GetState().early_color_hallways) {
814 world_state |= kLOCATION_SMALL_SPHERE_ONE;
815 }
816
817 return (world_state & classification);
818}
819
820PanelShuffleMode AP_GetPanelShuffleMode() {
821 std::lock_guard state_guard(GetState().state_mutex);
822
823 return GetState().panel_shuffle_mode;
517} 824}
518 825
519VictoryCondition AP_GetVictoryCondition() { 826VictoryCondition AP_GetVictoryCondition() {
827 std::lock_guard state_guard(GetState().state_mutex);
828
520 return GetState().victory_condition; 829 return GetState().victory_condition;
521} 830}
522 831
523bool AP_HasAchievement(const std::string& achievement_name) { 832bool AP_HasEarlyColorHallways() {
524 return GetState().HasAchievement(achievement_name); 833 std::lock_guard state_guard(GetState().state_mutex);
834
835 return GetState().early_color_hallways;
525} 836}
526 837
527bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } 838bool AP_IsPilgrimageEnabled() {
839 std::lock_guard state_guard(GetState().state_mutex);
528 840
529bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } 841 return GetState().pilgrimage_enabled;
842}
530 843
531bool AP_DoesPilgrimageAllowRoofAccess() { 844bool AP_DoesPilgrimageAllowRoofAccess() {
845 std::lock_guard state_guard(GetState().state_mutex);
846
532 return GetState().pilgrimage_allows_roof_access; 847 return GetState().pilgrimage_allows_roof_access;
533} 848}
534 849
535bool AP_DoesPilgrimageAllowPaintings() { 850bool AP_DoesPilgrimageAllowPaintings() {
851 std::lock_guard state_guard(GetState().state_mutex);
852
536 return GetState().pilgrimage_allows_paintings; 853 return GetState().pilgrimage_allows_paintings;
537} 854}
538 855
539SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } 856SunwarpAccess AP_GetSunwarpAccess() {
857 std::lock_guard state_guard(GetState().state_mutex);
540 858
541bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } 859 return GetState().sunwarp_access;
860}
861
862bool AP_IsSunwarpShuffle() {
863 std::lock_guard state_guard(GetState().state_mutex);
864
865 return GetState().sunwarp_shuffle;
866}
542 867
543const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { 868std::map<int, SunwarpMapping> AP_GetSunwarpMapping() {
544 return GetState().sunwarp_mapping; 869 return GetState().sunwarp_mapping;
545} 870}
546 871
872bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; }
873
547bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 874bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
548 875
549std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { 876std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
877 std::lock_guard state_guard(GetState().state_mutex);
878
550 return GetState().player_pos; 879 return GetState().player_pos;
551} 880}
881
882bool 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 5fbb720..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -11,7 +11,7 @@
11 11
12class TrackerFrame; 12class TrackerFrame;
13 13
14enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 }; 14enum DoorShuffleMode { kNO_DOORS = 0, kPANELS_MODE = 1, kDOORS_MODE = 2 };
15 15
16enum VictoryCondition { 16enum 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
29enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 };
30
29enum SunwarpAccess { 31enum SunwarpAccess {
30 kSUNWARP_ACCESS_NORMAL = 0, 32 kSUNWARP_ACCESS_NORMAL = 0,
31 kSUNWARP_ACCESS_DISABLED = 1, 33 kSUNWARP_ACCESS_DISABLED = 1,
@@ -39,35 +41,57 @@ struct SunwarpMapping {
39 int exit_index; 41 int exit_index;
40}; 42};
41 43
44struct ItemState {
45 std::string name;
46 int amount = 0;
47 int index = 0;
48};
49
42void AP_SetTrackerFrame(TrackerFrame* tracker_frame); 50void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
43 51
44void AP_Connect(std::string server, std::string player, std::string password); 52void AP_Connect(std::string server, std::string player, std::string password);
45 53
46bool AP_HasCheckedGameLocation(int location_id); 54std::string AP_GetStatusMessage();
47 55
48bool AP_HasCheckedHuntPanel(int location_id); 56std::string AP_GetSaveName();
49 57
58bool AP_HasCheckedGameLocation(int location_id);
59
60// This doesn't lock the state mutex, for speed, so it must ONLY be called from
61// RecalculateReachability, which is only called from the APState thread anyway.
50bool AP_HasItem(int item_id, int quantity = 1); 62bool AP_HasItem(int item_id, int quantity = 1);
51 63
64bool AP_HasItemSafe(int item_id, int quantity = 1);
65
52DoorShuffleMode AP_GetDoorShuffleMode(); 66DoorShuffleMode AP_GetDoorShuffleMode();
53 67
68bool AP_AreDoorsGrouped();
69
54bool AP_IsColorShuffle(); 70bool AP_IsColorShuffle();
55 71
56bool AP_IsPaintingShuffle(); 72bool AP_IsPaintingShuffle();
57 73
58const std::map<std::string, std::string>& AP_GetPaintingMapping(); 74std::map<std::string, std::string> AP_GetPaintingMapping();
59 75
60const std::set<std::string>& AP_GetCheckedPaintings(); 76bool AP_IsPaintingMappedTo(const std::string& painting_id);
77
78std::set<std::string> AP_GetCheckedPaintings();
79
80bool AP_IsPaintingChecked(const std::string& painting_id);
81
82void AP_RevealPaintings();
61 83
62int AP_GetMasteryRequirement(); 84int AP_GetMasteryRequirement();
63 85
64int AP_GetLevel2Requirement(); 86int AP_GetLevel2Requirement();
65 87
88LocationChecks AP_GetLocationsChecks();
89
66bool AP_IsLocationVisible(int classification); 90bool AP_IsLocationVisible(int classification);
67 91
68VictoryCondition AP_GetVictoryCondition(); 92PanelShuffleMode AP_GetPanelShuffleMode();
69 93
70bool AP_HasAchievement(const std::string& achievement_name); 94VictoryCondition AP_GetVictoryCondition();
71 95
72bool AP_HasEarlyColorHallways(); 96bool AP_HasEarlyColorHallways();
73 97
@@ -81,10 +105,14 @@ SunwarpAccess AP_GetSunwarpAccess();
81 105
82bool AP_IsSunwarpShuffle(); 106bool AP_IsSunwarpShuffle();
83 107
84const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); 108std::map<int, SunwarpMapping> AP_GetSunwarpMapping();
109
110bool AP_IsPostgameShuffle();
85 111
86bool AP_HasReachedGoal(); 112bool AP_HasReachedGoal();
87 113
88std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); 114std::optional<std::tuple<int, int>> AP_GetPlayerPosition();
89 115
116bool AP_IsPanelSolved(int solve_index);
117
90#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 6e70315..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -2,62 +2,68 @@
2 2
3#include <wx/dcbuffer.h> 3#include <wx/dcbuffer.h>
4 4
5#include <algorithm>
6
5#include "ap_state.h" 7#include "ap_state.h"
6#include "game_data.h" 8#include "game_data.h"
7#include "global.h" 9#include "global.h"
10#include "icons.h"
8#include "tracker_config.h" 11#include "tracker_config.h"
12#include "tracker_panel.h"
9#include "tracker_state.h" 13#include "tracker_state.h"
10 14
11AreaPopup::AreaPopup(wxWindow* parent, int area_id) 15AreaPopup::AreaPopup(wxWindow* parent, int area_id)
12 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 16 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
13 SetBackgroundStyle(wxBG_STYLE_PAINT); 17 SetBackgroundStyle(wxBG_STYLE_PAINT);
14 18
15 unchecked_eye_ = 19 LoadIcons();
16 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
17 wxBITMAP_TYPE_PNG)
18 .Scale(32, 32));
19 checked_eye_ = wxBitmap(
20 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
21 .Scale(32, 32));
22 20
21 // TODO: This is slow on high-DPI screens.
23 SetScrollRate(5, 5); 22 SetScrollRate(5, 5);
24 23
25 SetBackgroundColour(*wxBLACK); 24 SetBackgroundColour(*wxBLACK);
26 Hide(); 25 Hide();
27 26
28 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); 27 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this);
28 Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this);
29 29
30 UpdateIndicators(); 30 ResetIndicators();
31} 31}
32 32
33void AreaPopup::UpdateIndicators() { 33void AreaPopup::ResetIndicators() {
34 indicators_.clear();
35
34 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());
35 39
36 // Start calculating extents. 40 // Start calculating extents.
37 wxMemoryDC mem_dc; 41 wxMemoryDC mem_dc;
38 mem_dc.SetFont(GetFont().Bold()); 42 mem_dc.SetFont(the_font.Bold());
39 wxSize header_extent = mem_dc.GetTextExtent(map_area.name); 43 header_extent_ = mem_dc.GetTextExtent(map_area.name);
40 44
41 int acc_height = header_extent.GetHeight() + 20; 45 int acc_height = header_extent_.GetHeight() + FromDIP(20);
42 int col_width = 0; 46 int col_width = 0;
43 47
44 mem_dc.SetFont(GetFont()); 48 mem_dc.SetFont(the_font);
45
46 std::vector<int> real_locations;
47 49
48 for (int section_id = 0; section_id < map_area.locations.size(); 50 for (int section_id = 0; section_id < map_area.locations.size();
49 section_id++) { 51 section_id++) {
50 const Location& location = map_area.locations.at(section_id); 52 const Location& location = map_area.locations.at(section_id);
51 53 if ((!AP_IsLocationVisible(location.classification) ||
52 if (!AP_IsLocationVisible(location.classification) && 54 IsLocationPostgame(location.ap_location_id)) &&
53 !(location.hunt && GetTrackerConfig().show_hunt_panels)) { 55 !(location.hunt &&
56 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
57 !(location.single_panel &&
58 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) {
54 continue; 59 continue;
55 } 60 }
56 61
57 real_locations.push_back(section_id); 62 indicators_.emplace_back(section_id, kLOCATION, acc_height);
58 63
59 wxSize item_extent = mem_dc.GetTextExtent(location.name); 64 wxSize item_extent = mem_dc.GetTextExtent(location.name);
60 int item_height = std::max(32, item_extent.GetHeight()) + 10; 65 int item_height =
66 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
61 acc_height += item_height; 67 acc_height += item_height;
62 68
63 if (item_extent.GetWidth() > col_width) { 69 if (item_extent.GetWidth() > col_width) {
@@ -65,49 +71,107 @@ void AreaPopup::UpdateIndicators() {
65 } 71 }
66 } 72 }
67 73
68 int item_width = col_width + 10 + 32; 74 if (AP_IsPaintingShuffle()) {
69 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 75 for (int painting_id : map_area.paintings) {
70 76 if (IsPaintingPostgame(painting_id)) {
71 Fit(); 77 continue;
72 SetVirtualSize(full_width, acc_height); 78 }
73
74 rendered_ = wxBitmap(full_width, acc_height);
75 mem_dc.SelectObject(rendered_);
76 mem_dc.SetPen(*wxTRANSPARENT_PEN);
77 mem_dc.SetBrush(*wxBLACK_BRUSH);
78 mem_dc.DrawRectangle({0, 0}, {full_width, acc_height});
79 79
80 mem_dc.SetFont(GetFont().Bold()); 80 indicators_.emplace_back(painting_id, kPAINTING, acc_height);
81 mem_dc.SetTextForeground(*wxWHITE);
82 mem_dc.DrawText(map_area.name,
83 {(full_width - header_extent.GetWidth()) / 2, 10});
84 81
85 int cur_height = header_extent.GetHeight() + 20; 82 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
83 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
84 int item_height =
85 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
86 acc_height += item_height;
86 87
87 mem_dc.SetFont(GetFont()); 88 if (item_extent.GetWidth() > col_width) {
89 col_width = item_extent.GetWidth();
90 }
91 }
92 }
88 93
89 for (int section_id : real_locations) { 94 int item_width = col_width + FromDIP(10 + 32);
90 const Location& location = map_area.locations.at(section_id); 95 full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20);
96 full_height_ = acc_height;
91 97
92 bool checked = 98 Fit();
93 AP_HasCheckedGameLocation(location.ap_location_id) || 99 SetVirtualSize(full_width_, full_height_);
94 (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)) ||
95 (IsLocationWinCondition(location) && AP_HasReachedGoal());
96 100
97 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; 101 UpdateIndicators();
102}
98 103
99 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 104void 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());
100 108
101 bool reachable = IsLocationReachable(location.ap_location_id); 109 rendered_ = wxBitmap(full_width_, full_height_);
102 const wxColour* text_color = reachable ? wxWHITE : wxRED;
103 mem_dc.SetTextForeground(*text_color);
104 110
105 wxSize item_extent = mem_dc.GetTextExtent(location.name); 111 wxMemoryDC mem_dc;
106 mem_dc.DrawText( 112 mem_dc.SelectObject(rendered_);
107 location.name, 113 mem_dc.SetPen(*wxTRANSPARENT_PEN);
108 {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 114 mem_dc.SetBrush(*wxBLACK_BRUSH);
115 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
109 116
110 cur_height += 10 + 32; 117 mem_dc.SetFont(the_font.Bold());
118 mem_dc.SetTextForeground(*wxWHITE);
119 mem_dc.DrawText(map_area.name,
120 {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)});
121
122 mem_dc.SetFont(the_font);
123
124 for (const IndicatorInfo& indicator : indicators_) {
125 switch (indicator.type) {
126 case kLOCATION: {
127 const Location& location = map_area.locations.at(indicator.id);
128
129 bool checked = false;
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 }
138
139 const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_;
140
141 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
142
143 bool reachable = IsLocationReachable(location.ap_location_id);
144 const wxColour* text_color = reachable ? wxWHITE : wxRED;
145 mem_dc.SetTextForeground(*text_color);
146
147 wxSize item_extent = mem_dc.GetTextExtent(location.name);
148 mem_dc.DrawText(
149 location.name,
150 {FromDIP(10 + 32 + 10),
151 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
152
153 break;
154 }
155 case kPAINTING: {
156 const PaintingExit& painting = GD_GetPaintingExit(indicator.id);
157
158 bool reachable = IsPaintingReachable(indicator.id);
159 const wxColour* text_color = reachable ? wxWHITE : wxRED;
160 mem_dc.SetTextForeground(*text_color);
161
162 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id);
163 const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_;
164 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
165
166 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
167 mem_dc.DrawText(
168 painting.display_name,
169 {FromDIP(10 + 32 + 10),
170 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
171
172 break;
173 }
174 }
111 } 175 }
112} 176}
113 177
@@ -118,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) {
118 182
119 event.Skip(); 183 event.Skip();
120} 184}
185
186void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) {
187 LoadIcons();
188 ResetIndicators();
189
190 event.Skip();
191}
192
193void 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
10class AreaPopup : public wxScrolledCanvas { 12class 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
5ConnectionDialog::ConnectionDialog() 5ConnectionDialog::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
63void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { 70void 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 7c9564b..588ffc8 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -1,11 +1,6 @@
1#include "game_data.h" 1#include "game_data.h"
2 2
3#include <wx/wxprec.h> 3#include <fmt/core.h>
4
5#ifndef WX_PRECOMP
6#include <wx/wx.h>
7#endif
8
9#include <hkutil/string.h> 4#include <hkutil/string.h>
10#include <yaml-cpp/yaml.h> 5#include <yaml-cpp/yaml.h>
11 6
@@ -13,51 +8,31 @@
13#include <sstream> 8#include <sstream>
14 9
15#include "global.h" 10#include "global.h"
11#include "logger.h"
16 12
17namespace { 13namespace {
18 14
19LingoColor GetColorForString(const std::string &str) {
20 if (str == "black") {
21 return LingoColor::kBlack;
22 } else if (str == "red") {
23 return LingoColor::kRed;
24 } else if (str == "blue") {
25 return LingoColor::kBlue;
26 } else if (str == "yellow") {
27 return LingoColor::kYellow;
28 } else if (str == "orange") {
29 return LingoColor::kOrange;
30 } else if (str == "green") {
31 return LingoColor::kGreen;
32 } else if (str == "gray") {
33 return LingoColor::kGray;
34 } else if (str == "brown") {
35 return LingoColor::kBrown;
36 } else if (str == "purple") {
37 return LingoColor::kPurple;
38 } else {
39 wxLogError("Invalid color: %s", str);
40
41 return LingoColor::kNone;
42 }
43}
44
45struct GameData { 15struct GameData {
46 std::vector<Room> rooms_; 16 std::vector<Room> rooms_;
47 std::vector<Door> doors_; 17 std::vector<Door> doors_;
48 std::vector<Panel> panels_; 18 std::vector<Panel> panels_;
19 std::vector<PanelDoor> panel_doors_;
49 std::vector<MapArea> map_areas_; 20 std::vector<MapArea> map_areas_;
50 std::vector<SubwayItem> subway_items_; 21 std::vector<SubwayItem> subway_items_;
22 std::vector<PaintingExit> paintings_;
51 23
52 std::map<std::string, int> room_by_id_; 24 std::map<std::string, int> room_by_id_;
53 std::map<std::string, int> door_by_id_; 25 std::map<std::string, int> door_by_id_;
54 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_;
55 std::map<std::string, int> area_by_id_; 28 std::map<std::string, int> area_by_id_;
29 std::map<std::string, int> painting_by_id_;
56 30
57 std::vector<int> door_definition_order_; 31 std::vector<int> door_definition_order_;
58 32
59 std::map<std::string, int> room_by_painting_; 33 std::map<std::string, int> room_by_painting_;
60 std::map<int, int> room_by_sunwarp_; 34 std::map<int, int> room_by_sunwarp_;
35 std::map<int, int> panel_by_solve_index_;
61 36
62 std::vector<int> achievement_panels_; 37 std::vector<int> achievement_panels_;
63 38
@@ -68,6 +43,8 @@ struct GameData {
68 std::map<std::string, int> subway_item_by_painting_; 43 std::map<std::string, int> subway_item_by_painting_;
69 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; 44 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
70 45
46 std::map<int, std::string> item_by_ap_id_;
47
71 bool loaded_area_data_ = false; 48 bool loaded_area_data_ = false;
72 std::set<std::string> malconfigured_areas_; 49 std::set<std::string> malconfigured_areas_;
73 50
@@ -83,10 +60,10 @@ struct GameData {
83 ids_config["special_items"][color_name]) { 60 ids_config["special_items"][color_name]) {
84 std::string input_name = color_name; 61 std::string input_name = color_name;
85 input_name[0] = std::tolower(input_name[0]); 62 input_name[0] = std::tolower(input_name[0]);
86 ap_id_by_color_[GetColorForString(input_name)] = 63 ap_id_by_color_[GetLingoColorForString(input_name)] =
87 ids_config["special_items"][color_name].as<int>(); 64 ids_config["special_items"][color_name].as<int>();
88 } else { 65 } else {
89 wxLogError("Missing AP item ID for color %s", color_name); 66 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
90 } 67 }
91 }; 68 };
92 69
@@ -100,8 +77,18 @@ struct GameData {
100 init_color_id("Brown"); 77 init_color_id("Brown");
101 init_color_id("Gray"); 78 init_color_id("Gray");
102 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
103 rooms_.reserve(lingo_config.size() * 2); 88 rooms_.reserve(lingo_config.size() * 2);
104 89
90 std::vector<int> panel_location_ids;
91
105 for (const auto &room_it : lingo_config) { 92 for (const auto &room_it : lingo_config) {
106 int room_id = AddOrGetRoom(room_it.first.as<std::string>()); 93 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
107 94
@@ -111,6 +98,7 @@ struct GameData {
111 auto process_single_entrance = 98 auto process_single_entrance =
112 [this, room_id, from_room_id](const YAML::Node &option) { 99 [this, room_id, from_room_id](const YAML::Node &option) {
113 Exit exit_obj; 100 Exit exit_obj;
101 exit_obj.source_room = from_room_id;
114 exit_obj.destination_room = room_id; 102 exit_obj.destination_room = room_id;
115 103
116 if (option["door"]) { 104 if (option["door"]) {
@@ -139,13 +127,17 @@ struct GameData {
139 exit_obj.type = EntranceType::kCrossroadsRoofAccess; 127 exit_obj.type = EntranceType::kCrossroadsRoofAccess;
140 } 128 }
141 129
130 if (option["static_painting"] && option["static_painting"].as<bool>()) {
131 exit_obj.type = EntranceType::kStaticPainting;
132 }
133
142 rooms_[from_room_id].exits.push_back(exit_obj); 134 rooms_[from_room_id].exits.push_back(exit_obj);
143 }; 135 };
144 136
145 switch (entrance_it.second.Type()) { 137 switch (entrance_it.second.Type()) {
146 case YAML::NodeType::Scalar: { 138 case YAML::NodeType::Scalar: {
147 // This is just "true". 139 // This is just "true".
148 rooms_[from_room_id].exits.push_back({.destination_room = room_id}); 140 rooms_[from_room_id].exits.push_back({.source_room = from_room_id, .destination_room = room_id});
149 break; 141 break;
150 } 142 }
151 case YAML::NodeType::Map: { 143 case YAML::NodeType::Map: {
@@ -163,7 +155,8 @@ struct GameData {
163 // This shouldn't happen. 155 // This shouldn't happen.
164 std::ostringstream formatted; 156 std::ostringstream formatted;
165 formatted << entrance_it; 157 formatted << entrance_it;
166 wxLogError("Error reading game data: %s", formatted.str()); 158 TrackerLog(
159 fmt::format("Error reading game data: {}", formatted.str()));
167 break; 160 break;
168 } 161 }
169 } 162 }
@@ -177,12 +170,12 @@ struct GameData {
177 170
178 if (panel_it.second["colors"]) { 171 if (panel_it.second["colors"]) {
179 if (panel_it.second["colors"].IsScalar()) { 172 if (panel_it.second["colors"].IsScalar()) {
180 panels_[panel_id].colors.push_back(GetColorForString( 173 panels_[panel_id].colors.push_back(GetLingoColorForString(
181 panel_it.second["colors"].as<std::string>())); 174 panel_it.second["colors"].as<std::string>()));
182 } else { 175 } else {
183 for (const auto &color_node : panel_it.second["colors"]) { 176 for (const auto &color_node : panel_it.second["colors"]) {
184 panels_[panel_id].colors.push_back( 177 panels_[panel_id].colors.push_back(
185 GetColorForString(color_node.as<std::string>())); 178 GetLingoColorForString(color_node.as<std::string>()));
186 } 179 }
187 } 180 }
188 } 181 }
@@ -260,6 +253,16 @@ struct GameData {
260 achievement_panels_.push_back(panel_id); 253 achievement_panels_.push_back(panel_id);
261 } 254 }
262 255
256 if (panel_it.second["location_name"]) {
257 panels_[panel_id].location_name =
258 panel_it.second["location_name"].as<std::string>();
259 }
260
261 if (panel_it.second["id"]) {
262 panels_[panel_id].nodepath =
263 panel_it.second["id"].as<std::string>();
264 }
265
263 if (panel_it.second["hunt"]) { 266 if (panel_it.second["hunt"]) {
264 panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); 267 panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>();
265 } 268 }
@@ -278,13 +281,15 @@ struct GameData {
278 ids_config["panels"][rooms_[room_id].name] && 281 ids_config["panels"][rooms_[room_id].name] &&
279 ids_config["panels"][rooms_[room_id].name] 282 ids_config["panels"][rooms_[room_id].name]
280 [panels_[panel_id].name]) { 283 [panels_[panel_id].name]) {
281 panels_[panel_id].ap_location_id = 284 int location_id = ids_config["panels"][rooms_[room_id].name]
282 ids_config["panels"][rooms_[room_id].name] 285 [panels_[panel_id].name]
283 [panels_[panel_id].name] 286 .as<int>();
284 .as<int>(); 287 panels_[panel_id].ap_location_id = location_id;
288 panel_location_ids.push_back(location_id);
285 } else { 289 } else {
286 wxLogError("Missing AP location ID for panel %s - %s", 290 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
287 rooms_[room_id].name, panels_[panel_id].name); 291 rooms_[room_id].name,
292 panels_[panel_id].name));
288 } 293 }
289 } 294 }
290 } 295 }
@@ -346,9 +351,13 @@ struct GameData {
346 ids_config["doors"][rooms_[room_id].name] 351 ids_config["doors"][rooms_[room_id].name]
347 [doors_[door_id].name]["item"] 352 [doors_[door_id].name]["item"]
348 .as<int>(); 353 .as<int>();
354
355 item_by_ap_id_[doors_[door_id].ap_item_id] =
356 doors_[door_id].item_name;
349 } else { 357 } else {
350 wxLogError("Missing AP item ID for door %s - %s", 358 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
351 rooms_[room_id].name, doors_[door_id].name); 359 rooms_[room_id].name,
360 doors_[door_id].name));
352 } 361 }
353 } 362 }
354 363
@@ -361,9 +370,12 @@ struct GameData {
361 doors_[door_id].group_ap_item_id = 370 doors_[door_id].group_ap_item_id =
362 ids_config["door_groups"][doors_[door_id].group_name] 371 ids_config["door_groups"][doors_[door_id].group_name]
363 .as<int>(); 372 .as<int>();
373
374 item_by_ap_id_[doors_[door_id].group_ap_item_id] =
375 doors_[door_id].group_name;
364 } else { 376 } else {
365 wxLogError("Missing AP item ID for door group %s", 377 TrackerLog(fmt::format("Missing AP item ID for door group {}",
366 doors_[door_id].group_name); 378 doors_[door_id].group_name));
367 } 379 }
368 } 380 }
369 381
@@ -373,11 +385,11 @@ struct GameData {
373 } else if (!door_it.second["skip_location"] && 385 } else if (!door_it.second["skip_location"] &&
374 !door_it.second["event"]) { 386 !door_it.second["event"]) {
375 if (has_external_panels) { 387 if (has_external_panels) {
376 wxLogError( 388 TrackerLog(fmt::format(
377 "%s - %s has panels from other rooms but does not have an " 389 "{} - {} has panels from other rooms but does not have an "
378 "explicit location name and is not marked skip_location or " 390 "explicit location name and is not marked skip_location or "
379 "event", 391 "event",
380 rooms_[room_id].name, doors_[door_id].name); 392 rooms_[room_id].name, doors_[door_id].name));
381 } 393 }
382 394
383 doors_[door_id].location_name = 395 doors_[door_id].location_name =
@@ -397,8 +409,9 @@ struct GameData {
397 [doors_[door_id].name]["location"] 409 [doors_[door_id].name]["location"]
398 .as<int>(); 410 .as<int>();
399 } else { 411 } else {
400 wxLogError("Missing AP location ID for door %s - %s", 412 TrackerLog(fmt::format("Missing AP location ID for door {} - {}",
401 rooms_[room_id].name, doors_[door_id].name); 413 rooms_[room_id].name,
414 doors_[door_id].name));
402 } 415 }
403 } 416 }
404 417
@@ -417,14 +430,107 @@ struct GameData {
417 } 430 }
418 } 431 }
419 432
433 if (room_it.second["panel_doors"]) {
434 for (const auto &panel_door_it : room_it.second["panel_doors"]) {
435 std::string panel_door_name = panel_door_it.first.as<std::string>();
436 int panel_door_id =
437 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name);
438
439 std::map<std::string, std::vector<std::string>> panel_per_room;
440 int num_panels = 0;
441 for (const auto &panel_node : panel_door_it.second["panels"]) {
442 num_panels++;
443
444 int panel_id = -1;
445
446 if (panel_node.IsScalar()) {
447 panel_id = AddOrGetPanel(rooms_[room_id].name,
448 panel_node.as<std::string>());
449
450 panel_per_room[rooms_[room_id].name].push_back(
451 panel_node.as<std::string>());
452 } else {
453 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(),
454 panel_node["panel"].as<std::string>());
455
456 panel_per_room[panel_node["room"].as<std::string>()].push_back(
457 panel_node["panel"].as<std::string>());
458 }
459
460 Panel &panel = panels_[panel_id];
461 panel.panel_door = panel_door_id;
462 }
463
464 if (panel_door_it.second["item_name"]) {
465 panel_doors_[panel_door_id].item_name =
466 panel_door_it.second["item_name"].as<std::string>();
467 } else {
468 std::vector<std::string> room_strs;
469 for (const auto &[room_str, panels_str] : panel_per_room) {
470 room_strs.push_back(fmt::format(
471 "{} - {}", room_str, hatkirby::implode(panels_str, ", ")));
472 }
473
474 if (num_panels == 1) {
475 panel_doors_[panel_door_id].item_name =
476 fmt::format("{} (Panel)", room_strs[0]);
477 } else {
478 panel_doors_[panel_door_id].item_name = fmt::format(
479 "{} (Panels)", hatkirby::implode(room_strs, " and "));
480 }
481 }
482
483 if (ids_config["panel_doors"] &&
484 ids_config["panel_doors"][rooms_[room_id].name] &&
485 ids_config["panel_doors"][rooms_[room_id].name]
486 [panel_door_name]) {
487 panel_doors_[panel_door_id].ap_item_id =
488 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name]
489 .as<int>();
490
491 item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] =
492 panel_doors_[panel_door_id].item_name;
493 } else {
494 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}",
495 rooms_[room_id].name, panel_door_name));
496 }
497
498 if (panel_door_it.second["panel_group"]) {
499 std::string panel_group =
500 panel_door_it.second["panel_group"].as<std::string>();
501
502 if (ids_config["panel_groups"] &&
503 ids_config["panel_groups"][panel_group]) {
504 panel_doors_[panel_door_id].group_ap_item_id =
505 ids_config["panel_groups"][panel_group].as<int>();
506
507 item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] =
508 panel_group;
509 } else {
510 TrackerLog(fmt::format(
511 "Missing AP item ID for panel door group {}", panel_group));
512 }
513 }
514 }
515 }
516
420 if (room_it.second["paintings"]) { 517 if (room_it.second["paintings"]) {
421 for (const auto &painting : room_it.second["paintings"]) { 518 for (const auto &painting : room_it.second["paintings"]) {
422 std::string painting_id = painting["id"].as<std::string>(); 519 std::string internal_id = painting["id"].as<std::string>();
423 room_by_painting_[painting_id] = room_id; 520 int painting_id = AddOrGetPainting(internal_id);
521 PaintingExit &painting_exit = paintings_[painting_id];
522 painting_exit.room = room_id;
523
524 if (painting["display_name"]) {
525 painting_exit.display_name =
526 painting["display_name"].as<std::string>();
527 } else {
528 painting_exit.display_name = painting_exit.internal_id;
529 }
424 530
425 if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { 531 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
426 PaintingExit painting_exit; 532 (!painting["disable"] || !painting["disable"].as<bool>())) {
427 painting_exit.id = painting_id; 533 painting_exit.entrance = true;
428 534
429 if (painting["required_door"]) { 535 if (painting["required_door"]) {
430 std::string rd_room = rooms_[room_id].name; 536 std::string rd_room = rooms_[room_id].name;
@@ -435,9 +541,9 @@ struct GameData {
435 painting_exit.door = AddOrGetDoor( 541 painting_exit.door = AddOrGetDoor(
436 rd_room, painting["required_door"]["door"].as<std::string>()); 542 rd_room, painting["required_door"]["door"].as<std::string>());
437 } 543 }
438
439 rooms_[room_id].paintings.push_back(painting_exit);
440 } 544 }
545
546 rooms_[room_id].paintings.push_back(painting_exit.id);
441 } 547 }
442 } 548 }
443 549
@@ -463,33 +569,74 @@ struct GameData {
463 ids_config["progression"][progressive_item_name]) { 569 ids_config["progression"][progressive_item_name]) {
464 progressive_item_id = 570 progressive_item_id =
465 ids_config["progression"][progressive_item_name].as<int>(); 571 ids_config["progression"][progressive_item_name].as<int>();
572
573 item_by_ap_id_[progressive_item_id] = progressive_item_name;
466 } else { 574 } else {
467 wxLogError("Missing AP item ID for progressive item %s", 575 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
468 progressive_item_name); 576 progressive_item_name));
469 } 577 }
470 578
471 int index = 1; 579 if (progression_it.second["doors"]) {
472 for (const auto &stage : progression_it.second) { 580 int index = 1;
473 int door_id = -1; 581 for (const auto &stage : progression_it.second["doors"]) {
582 int door_id = -1;
583
584 if (stage.IsScalar()) {
585 door_id =
586 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>());
587 } else {
588 door_id = AddOrGetDoor(stage["room"].as<std::string>(),
589 stage["door"].as<std::string>());
590 }
474 591
475 if (stage.IsScalar()) { 592 doors_[door_id].progressives.push_back(
476 door_id = 593 {.item_name = progressive_item_name,
477 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); 594 .ap_item_id = progressive_item_id,
478 } else { 595 .quantity = index});
479 door_id = AddOrGetDoor(stage["room"].as<std::string>(), 596 index++;
480 stage["door"].as<std::string>());
481 } 597 }
598 }
482 599
483 doors_[door_id].progressives.push_back( 600 if (progression_it.second["panel_doors"]) {
484 {.item_name = progressive_item_name, 601 int index = 1;
485 .ap_item_id = progressive_item_id, 602 for (const auto &stage : progression_it.second["panel_doors"]) {
486 .quantity = index}); 603 int panel_door_id = -1;
487 index++; 604
605 if (stage.IsScalar()) {
606 panel_door_id = AddOrGetPanelDoor(rooms_[room_id].name,
607 stage.as<std::string>());
608 } else {
609 panel_door_id =
610 AddOrGetPanelDoor(stage["room"].as<std::string>(),
611 stage["panel_door"].as<std::string>());
612 }
613
614 panel_doors_[panel_door_id].progressives.push_back(
615 {.item_name = progressive_item_name,
616 .ap_item_id = progressive_item_id,
617 .quantity = index});
618 index++;
619 }
488 } 620 }
489 } 621 }
490 } 622 }
491 } 623 }
492 624
625 // Determine the panel solve indices from the sorted location IDs.
626 std::sort(panel_location_ids.begin(), panel_location_ids.end());
627
628 std::map<int, int> solve_index_by_location_id;
629 for (int i = 0; i < panel_location_ids.size(); i++) {
630 solve_index_by_location_id[panel_location_ids[i]] = i;
631 }
632
633 for (Panel &panel : panels_) {
634 if (panel.ap_location_id != -1) {
635 panel.solve_index = solve_index_by_location_id[panel.ap_location_id];
636 panel_by_solve_index_[panel.solve_index] = panel.id;
637 }
638 }
639
493 map_areas_.reserve(areas_config.size()); 640 map_areas_.reserve(areas_config.size());
494 641
495 std::map<std::string, int> fold_areas; 642 std::map<std::string, int> fold_areas;
@@ -510,13 +657,28 @@ struct GameData {
510 // Only locations for the panels are kept here. 657 // Only locations for the panels are kept here.
511 std::map<std::string, std::tuple<int, int>> locations_by_name; 658 std::map<std::string, std::tuple<int, int>> locations_by_name;
512 659
513 for (const Panel &panel : panels_) { 660 for (Panel &panel : panels_) {
514 int room_id = panel.room; 661 int room_id = panel.room;
515 std::string room_name = rooms_[room_id].name; 662 std::string room_name = rooms_[room_id].name;
516 663
517 std::string area_name = room_name; 664 std::string area_name = room_name;
518 if (fold_areas.count(room_name)) { 665 std::string section_name = panel.name;
519 int fold_area_id = fold_areas[room_name]; 666 std::string location_name = room_name + " - " + panel.name;
667
668 if (!panel.location_name.empty()) {
669 location_name = panel.location_name;
670
671 size_t divider_pos = location_name.find(" - ");
672 if (divider_pos != std::string::npos) {
673 area_name = location_name.substr(0, divider_pos);
674 section_name = location_name.substr(divider_pos + 3);
675 }
676 } else {
677 panel.location_name = location_name;
678 }
679
680 if (fold_areas.count(area_name)) {
681 int fold_area_id = fold_areas[area_name];
520 area_name = map_areas_[fold_area_id].name; 682 area_name = map_areas_[fold_area_id].name;
521 } 683 }
522 684
@@ -528,19 +690,23 @@ struct GameData {
528 } 690 }
529 } 691 }
530 692
693 if (room_name == "Starting Room") {
694 classification |= kLOCATION_SMALL_SPHERE_ONE;
695 }
696
531 int area_id = AddOrGetArea(area_name); 697 int area_id = AddOrGetArea(area_name);
532 MapArea &map_area = map_areas_[area_id]; 698 MapArea &map_area = map_areas_[area_id];
533 // room field should be the original room ID 699 // room field should be the original room ID
534 map_area.locations.push_back( 700 map_area.locations.push_back({.name = section_name,
535 {.name = panel.name, 701 .ap_location_name = location_name,
536 .ap_location_name = room_name + " - " + panel.name, 702 .ap_location_id = panel.ap_location_id,
537 .ap_location_id = panel.ap_location_id, 703 .room = panel.room,
538 .room = panel.room, 704 .panels = {panel.id},
539 .panels = {panel.id}, 705 .classification = classification,
540 .classification = classification, 706 .hunt = panel.hunt,
541 .hunt = panel.hunt}); 707 .single_panel = panel.id});
542 locations_by_name[map_area.locations.back().ap_location_name] = { 708 locations_by_name[location_name] = {area_id,
543 area_id, map_area.locations.size() - 1}; 709 map_area.locations.size() - 1};
544 } 710 }
545 711
546 for (int door_id : door_definition_order_) { 712 for (int door_id : door_definition_order_) {
@@ -591,14 +757,36 @@ struct GameData {
591 for (const Location &location : map_area.locations) { 757 for (const Location &location : map_area.locations) {
592 map_area.classification |= location.classification; 758 map_area.classification |= location.classification;
593 map_area.hunt |= location.hunt; 759 map_area.hunt |= location.hunt;
760 map_area.has_single_panel |= location.single_panel.has_value();
761 }
762 }
763
764 for (const Room &room : rooms_) {
765 std::string area_name = room.name;
766 if (fold_areas.count(room.name)) {
767 int fold_area_id = fold_areas[room.name];
768 area_name = map_areas_[fold_area_id].name;
769 }
770
771 if (!room.paintings.empty()) {
772 int area_id = AddOrGetArea(area_name);
773 MapArea &map_area = map_areas_[area_id];
774
775 for (int painting_id : room.paintings) {
776 PaintingExit &painting_obj = paintings_.at(painting_id);
777 painting_obj.map_area = area_id;
778 if (painting_obj.entrance) {
779 map_area.paintings.push_back(painting_id);
780 }
781 }
594 } 782 }
595 } 783 }
596 784
597 // Report errors. 785 // Report errors.
598 for (const std::string &area : malconfigured_areas_) { 786 for (const std::string &area : malconfigured_areas_) {
599 wxLogError("Area data not found for: %s", area); 787 TrackerLog(fmt::format("Area data not found for: {}", area));
600 } 788 }
601 789
602 // Read in subway items. 790 // Read in subway items.
603 YAML::Node subway_config = 791 YAML::Node subway_config =
604 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml")); 792 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml"));
@@ -613,13 +801,10 @@ struct GameData {
613 subway_it["door"].as<std::string>()); 801 subway_it["door"].as<std::string>());
614 } 802 }
615 803
616 if (subway_it["paintings"]) { 804 if (subway_it["painting"]) {
617 for (const auto &painting_it : subway_it["paintings"]) { 805 std::string painting_id = subway_it["painting"].as<std::string>();
618 std::string painting_id = painting_it.as<std::string>(); 806 subway_item.painting = painting_id;
619 807 subway_item_by_painting_[painting_id] = subway_item.id;
620 subway_item.paintings.push_back(painting_id);
621 subway_item_by_painting_[painting_id] = subway_item.id;
622 }
623 } 808 }
624 809
625 if (subway_it["tags"]) { 810 if (subway_it["tags"]) {
@@ -628,6 +813,18 @@ struct GameData {
628 } 813 }
629 } 814 }
630 815
816 if (subway_it["entrances"]) {
817 for (const auto &entrance_it : subway_it["entrances"]) {
818 subway_item.entrances.push_back(entrance_it.as<std::string>());
819 }
820 }
821
822 if (subway_it["exits"]) {
823 for (const auto &exit_it : subway_it["exits"]) {
824 subway_item.exits.push_back(exit_it.as<std::string>());
825 }
826 }
827
631 if (subway_it["sunwarp"]) { 828 if (subway_it["sunwarp"]) {
632 SubwaySunwarp sunwarp; 829 SubwaySunwarp sunwarp;
633 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); 830 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
@@ -654,6 +851,10 @@ struct GameData {
654 subway_item.special = subway_it["special"].as<std::string>(); 851 subway_item.special = subway_it["special"].as<std::string>();
655 } 852 }
656 853
854 if (subway_it["tilted"]) {
855 subway_item.tilted = subway_it["tilted"].as<bool>();
856 }
857
657 subway_items_.push_back(subway_item); 858 subway_items_.push_back(subway_item);
658 } 859 }
659 860
@@ -667,7 +868,7 @@ struct GameData {
667 868
668 for (const auto &[tag, items] : subway_tags) { 869 for (const auto &[tag, items] : subway_tags) {
669 if (items.size() == 1) { 870 if (items.size() == 1) {
670 wxLogWarning("Singleton subway item tag: %s", tag); 871 TrackerLog(fmt::format("Singleton subway item tag: {}", tag));
671 } 872 }
672 } 873 }
673 } 874 }
@@ -687,7 +888,8 @@ struct GameData {
687 if (!door_by_id_.count(full_name)) { 888 if (!door_by_id_.count(full_name)) {
688 int door_id = doors_.size(); 889 int door_id = doors_.size();
689 door_by_id_[full_name] = doors_.size(); 890 door_by_id_[full_name] = doors_.size();
690 doors_.push_back({.id = door_id, .room = AddOrGetRoom(room), .name = door}); 891 doors_.push_back(
892 {.id = door_id, .room = AddOrGetRoom(room), .name = door});
691 } 893 }
692 894
693 return door_by_id_[full_name]; 895 return door_by_id_[full_name];
@@ -706,6 +908,18 @@ struct GameData {
706 return panel_by_id_[full_name]; 908 return panel_by_id_[full_name];
707 } 909 }
708 910
911 int AddOrGetPanelDoor(std::string room, std::string panel) {
912 std::string full_name = room + " - " + panel;
913
914 if (!panel_doors_by_id_.count(full_name)) {
915 int panel_door_id = panel_doors_.size();
916 panel_doors_by_id_[full_name] = panel_door_id;
917 panel_doors_.push_back({});
918 }
919
920 return panel_doors_by_id_[full_name];
921 }
922
709 int AddOrGetArea(std::string area) { 923 int AddOrGetArea(std::string area) {
710 if (!area_by_id_.count(area)) { 924 if (!area_by_id_.count(area)) {
711 if (loaded_area_data_) { 925 if (loaded_area_data_) {
@@ -719,6 +933,16 @@ struct GameData {
719 933
720 return area_by_id_[area]; 934 return area_by_id_[area];
721 } 935 }
936
937 int AddOrGetPainting(std::string internal_id) {
938 if (!painting_by_id_.count(internal_id)) {
939 int painting_id = paintings_.size();
940 painting_by_id_[internal_id] = painting_id;
941 paintings_.push_back({.id = painting_id, .internal_id = internal_id});
942 }
943
944 return painting_by_id_[internal_id];
945 }
722}; 946};
723 947
724GameData &GetState() { 948GameData &GetState() {
@@ -728,7 +952,12 @@ GameData &GetState() {
728 952
729} // namespace 953} // namespace
730 954
731bool SubwaySunwarp::operator<(const SubwaySunwarp& rhs) const { 955bool SubwayItem::HasWarps() const {
956 return !(this->tags.empty() && this->entrances.empty() &&
957 this->exits.empty());
958}
959
960bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
732 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); 961 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
733} 962}
734 963
@@ -746,6 +975,10 @@ const std::vector<Door> &GD_GetDoors() { return GetState().doors_; }
746 975
747const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } 976const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); }
748 977
978const PanelDoor &GD_GetPanelDoor(int panel_door_id) {
979 return GetState().panel_doors_.at(panel_door_id);
980}
981
749int GD_GetDoorByName(const std::string &name) { 982int GD_GetDoorByName(const std::string &name) {
750 return GetState().door_by_id_.at(name); 983 return GetState().door_by_id_.at(name);
751} 984}
@@ -754,8 +987,20 @@ const Panel &GD_GetPanel(int panel_id) {
754 return GetState().panels_.at(panel_id); 987 return GetState().panels_.at(panel_id);
755} 988}
756 989
757int GD_GetRoomForPainting(const std::string &painting_id) { 990int GD_GetPanelBySolveIndex(int solve_index) {
758 return GetState().room_by_painting_.at(painting_id); 991 return GetState().panel_by_solve_index_.at(solve_index);
992}
993
994const std::vector<PaintingExit> &GD_GetPaintings() {
995 return GetState().paintings_;
996}
997
998const PaintingExit &GD_GetPaintingExit(int painting_id) {
999 return GetState().paintings_.at(painting_id);
1000}
1001
1002int GD_GetPaintingByName(const std::string &name) {
1003 return GetState().painting_by_id_.at(name);
759} 1004}
760 1005
761const std::vector<int> &GD_GetAchievementPanels() { 1006const std::vector<int> &GD_GetAchievementPanels() {
@@ -782,15 +1027,48 @@ const SubwayItem &GD_GetSubwayItem(int id) {
782 return GetState().subway_items_.at(id); 1027 return GetState().subway_items_.at(id);
783} 1028}
784 1029
785int GD_GetSubwayItemForPainting(const std::string& painting_id) { 1030std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
786#ifndef NDEBUG 1031 if (GetState().subway_item_by_painting_.count(painting_id)) {
787 if (!GetState().subway_item_by_painting_.count(painting_id)) { 1032 return GetState().subway_item_by_painting_.at(painting_id);
788 wxLogError("No subway item for painting %s", painting_id);
789 } 1033 }
790#endif 1034 return std::nullopt;
791 return GetState().subway_item_by_painting_.at(painting_id);
792} 1035}
793 1036
794int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 1037int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
795 return GetState().subway_item_by_sunwarp_.at(sunwarp); 1038 return GetState().subway_item_by_sunwarp_.at(sunwarp);
796} 1039}
1040
1041std::string GD_GetItemName(int id) {
1042 auto it = GetState().item_by_ap_id_.find(id);
1043 if (it != GetState().item_by_ap_id_.end()) {
1044 return it->second;
1045 } else {
1046 return "Unknown";
1047 }
1048}
1049
1050LingoColor GetLingoColorForString(const std::string &str) {
1051 if (str == "black") {
1052 return LingoColor::kBlack;
1053 } else if (str == "red") {
1054 return LingoColor::kRed;
1055 } else if (str == "blue") {
1056 return LingoColor::kBlue;
1057 } else if (str == "yellow") {
1058 return LingoColor::kYellow;
1059 } else if (str == "orange") {
1060 return LingoColor::kOrange;
1061 } else if (str == "green") {
1062 return LingoColor::kGreen;
1063 } else if (str == "gray") {
1064 return LingoColor::kGray;
1065 } else if (str == "brown") {
1066 return LingoColor::kBrown;
1067 } else if (str == "purple") {
1068 return LingoColor::kPurple;
1069 } else {
1070 TrackerLog(fmt::format("Invalid color: {}", str));
1071
1072 return LingoColor::kNone;
1073 }
1074}
diff --git a/src/game_data.h b/src/game_data.h index 3afaec3..8d3db4b 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -22,6 +22,7 @@ enum class LingoColor {
22constexpr int kLOCATION_NORMAL = 1; 22constexpr int kLOCATION_NORMAL = 1;
23constexpr int kLOCATION_REDUCED = 2; 23constexpr int kLOCATION_REDUCED = 2;
24constexpr int kLOCATION_INSANITY = 4; 24constexpr int kLOCATION_INSANITY = 4;
25constexpr int kLOCATION_SMALL_SPHERE_ONE = 8;
25 26
26enum class EntranceType { 27enum class EntranceType {
27 kNormal, 28 kNormal,
@@ -30,6 +31,7 @@ enum class EntranceType {
30 kWarp, 31 kWarp,
31 kPilgrimage, 32 kPilgrimage,
32 kCrossroadsRoofAccess, 33 kCrossroadsRoofAccess,
34 kStaticPainting,
33}; 35};
34 36
35enum class DoorType { 37enum class DoorType {
@@ -42,6 +44,7 @@ struct Panel {
42 int id; 44 int id;
43 int room; 45 int room;
44 std::string name; 46 std::string name;
47 std::string nodepath;
45 std::vector<LingoColor> colors; 48 std::vector<LingoColor> colors;
46 std::vector<int> required_rooms; 49 std::vector<int> required_rooms;
47 std::vector<int> required_doors; 50 std::vector<int> required_doors;
@@ -50,9 +53,12 @@ struct Panel {
50 bool exclude_reduce = false; 53 bool exclude_reduce = false;
51 bool achievement = false; 54 bool achievement = false;
52 std::string achievement_name; 55 std::string achievement_name;
56 std::string location_name;
53 bool non_counting = false; 57 bool non_counting = false;
54 int ap_location_id = -1; 58 int ap_location_id = -1;
55 bool hunt = false; 59 bool hunt = false;
60 int panel_door = -1;
61 int solve_index = -1;
56}; 62};
57 63
58struct ProgressiveRequirement { 64struct ProgressiveRequirement {
@@ -80,21 +86,34 @@ struct Door {
80 DoorType type = DoorType::kNormal; 86 DoorType type = DoorType::kNormal;
81}; 87};
82 88
89struct PanelDoor {
90 int ap_item_id = -1;
91 int group_ap_item_id = -1;
92 std::vector<ProgressiveRequirement> progressives;
93 std::string item_name;
94};
95
83struct Exit { 96struct Exit {
97 int source_room;
84 int destination_room; 98 int destination_room;
85 std::optional<int> door; 99 std::optional<int> door;
86 EntranceType type = EntranceType::kNormal; 100 EntranceType type = EntranceType::kNormal;
87}; 101};
88 102
89struct PaintingExit { 103struct PaintingExit {
90 std::string id; 104 int id;
105 int room;
106 std::string internal_id;
107 std::string display_name;
91 std::optional<int> door; 108 std::optional<int> door;
109 bool entrance = false;
110 int map_area;
92}; 111};
93 112
94struct Room { 113struct Room {
95 std::string name; 114 std::string name;
96 std::vector<Exit> exits; 115 std::vector<Exit> exits;
97 std::vector<PaintingExit> paintings; 116 std::vector<int> paintings;
98 std::vector<int> sunwarps; 117 std::vector<int> sunwarps;
99 std::vector<int> panels; 118 std::vector<int> panels;
100}; 119};
@@ -107,16 +126,19 @@ struct Location {
107 std::vector<int> panels; 126 std::vector<int> panels;
108 int classification = 0; 127 int classification = 0;
109 bool hunt = false; 128 bool hunt = false;
129 std::optional<int> single_panel;
110}; 130};
111 131
112struct MapArea { 132struct MapArea {
113 int id; 133 int id;
114 std::string name; 134 std::string name;
115 std::vector<Location> locations; 135 std::vector<Location> locations;
136 std::vector<int> paintings;
116 int map_x; 137 int map_x;
117 int map_y; 138 int map_y;
118 int classification = 0; 139 int classification = 0;
119 bool hunt = false; 140 bool hunt = false;
141 bool has_single_panel = false;
120}; 142};
121 143
122enum class SubwaySunwarpType { 144enum class SubwaySunwarpType {
@@ -136,11 +158,16 @@ struct SubwayItem {
136 int id; 158 int id;
137 int x; 159 int x;
138 int y; 160 int y;
161 bool tilted = false;
139 std::optional<int> door; 162 std::optional<int> door;
140 std::vector<std::string> paintings; 163 std::optional<std::string> painting;
141 std::vector<std::string> tags; 164 std::vector<std::string> tags; // 2-way teleports
165 std::vector<std::string> entrances; // teleport entrances
166 std::vector<std::string> exits; // teleport exits
142 std::optional<SubwaySunwarp> sunwarp; 167 std::optional<SubwaySunwarp> sunwarp;
143 std::optional<std::string> special; 168 std::optional<std::string> special;
169
170 bool HasWarps() const;
144}; 171};
145 172
146const std::vector<MapArea>& GD_GetMapAreas(); 173const std::vector<MapArea>& GD_GetMapAreas();
@@ -151,14 +178,21 @@ const std::vector<Door>& GD_GetDoors();
151const Door& GD_GetDoor(int door_id); 178const Door& GD_GetDoor(int door_id);
152int GD_GetDoorByName(const std::string& name); 179int GD_GetDoorByName(const std::string& name);
153const Panel& GD_GetPanel(int panel_id); 180const Panel& GD_GetPanel(int panel_id);
154int GD_GetRoomForPainting(const std::string& painting_id); 181int GD_GetPanelBySolveIndex(int solve_index);
182const PanelDoor& GD_GetPanelDoor(int panel_door_id);
183const std::vector<PaintingExit>& GD_GetPaintings();
184const PaintingExit& GD_GetPaintingExit(int painting_id);
185int GD_GetPaintingByName(const std::string& name);
155const std::vector<int>& GD_GetAchievementPanels(); 186const std::vector<int>& GD_GetAchievementPanels();
156int GD_GetItemIdForColor(LingoColor color); 187int GD_GetItemIdForColor(LingoColor color);
157const std::vector<int>& GD_GetSunwarpDoors(); 188const std::vector<int>& GD_GetSunwarpDoors();
158int GD_GetRoomForSunwarp(int index); 189int GD_GetRoomForSunwarp(int index);
159const std::vector<SubwayItem>& GD_GetSubwayItems(); 190const std::vector<SubwayItem>& GD_GetSubwayItems();
160const SubwayItem& GD_GetSubwayItem(int id); 191const SubwayItem& GD_GetSubwayItem(int id);
161int GD_GetSubwayItemForPainting(const std::string& painting_id); 192std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
162int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 193int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
194std::string GD_GetItemName(int id);
195
196LingoColor GetLingoColorForString(const std::string& str);
163 197
164#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 198#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
29bool IsLocationWinCondition(const Location& location) { 29std::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
42bool 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
11std::string GetAbsolutePath(std::string_view path); 11std::string GetAbsolutePath(std::string_view path);
12 12
13std::string GetWinCondition();
14
13bool IsLocationWinCondition(const Location& location); 15bool 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/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
5const 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
18static IconCache* ICON_CACHE_INSTANCE = nullptr;
19
20void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; }
21
22IconCache& 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
14class 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
22void SetTheIconCache(IconCache* instance);
23IconCache& 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
5constexpr const char* kDefaultIpcAddress = "ws://127.0.0.1:41253";
6
7IpcDialog::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
52void 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
12class 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
22namespace {
23
24struct 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
342IPCState& GetState() {
343 static IPCState* instance = new IPCState();
344 return *instance;
345}
346
347} // namespace
348
349void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) {
350 GetState().SetTrackerFrame(tracker_frame);
351}
352
353void IPC_Connect(std::string address) { GetState().Connect(address); }
354
355std::optional<std::string> IPC_GetStatusMessage() {
356 return GetState().GetStatusMessage();
357}
358
359void IPC_SetTrackerSlot(std::string server, std::string user) {
360 GetState().SetTrackerSlot(server, user);
361}
362
363bool IPC_IsConnected() { return GetState().IsConnected(); }
364
365std::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
9class TrackerFrame;
10
11void IPC_SetTrackerFrame(TrackerFrame* tracker_frame);
12
13void IPC_Connect(std::string address);
14
15std::optional<std::string> IPC_GetStatusMessage();
16
17void IPC_SetTrackerSlot(std::string server, std::string user);
18
19bool IPC_IsConnected();
20
21std::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
5namespace {
6
7enum 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
16inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) {
17 return static_cast<SortInstruction>(static_cast<int>(lhs) |
18 static_cast<int>(rhs));
19}
20
21template <typename T>
22int 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
32int 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
51ItemsPane::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
62void ItemsPane::ResetIndicators() {
63 DeleteAllItems();
64 items_.clear();
65}
66
67void 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
107void 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
118void 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
126void 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
17class 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
5wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent);
6
7LogDialog::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
26void LogDialog::LogMessage(const std::string& message) {
27 wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE);
28 event->SetString(message);
29 QueueEvent(event);
30}
31
32void 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
10wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent);
11
12class 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 new file mode 100644 index 0000000..8a08b58 --- /dev/null +++ b/src/logger.cpp
@@ -0,0 +1,64 @@
1#include "logger.h"
2
3#include <chrono>
4#include <fstream>
5#include <mutex>
6#include <sstream>
7
8#include "global.h"
9#include "log_dialog.h"
10
11namespace {
12
13class Logger {
14 public:
15 Logger() : logfile_(GetAbsolutePath("debug.log")) {}
16
17 void LogLine(const std::string& text) {
18 std::lock_guard guard(file_mutex_);
19 std::ostringstream line;
20 line << "[" << std::chrono::system_clock::now() << "] " << text;
21
22 logfile_ << line.str() << std::endl;
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;
43 }
44
45 private:
46 std::ofstream logfile_;
47 std::mutex file_mutex_;
48 LogDialog* log_dialog_ = nullptr;
49};
50
51Logger& GetLogger() {
52 static Logger* instance = new Logger();
53 return *instance;
54}
55
56} // namespace
57
58void TrackerLog(std::string text) { GetLogger().LogLine(text); }
59
60std::string TrackerReadPastLog() { return GetLogger().GetContents(); }
61
62void TrackerSetLogDialog(LogDialog* log_dialog) {
63 GetLogger().SetLogDialog(log_dialog);
64}
diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..f669790 --- /dev/null +++ b/src/logger.h
@@ -0,0 +1,14 @@
1#ifndef LOGGER_H_9BDD07EA
2#define LOGGER_H_9BDD07EA
3
4#include <string>
5
6class LogDialog;
7
8void TrackerLog(std::string message);
9
10std::string TrackerReadPastLog();
11
12void TrackerSetLogDialog(LogDialog* log_dialog);
13
14#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index 5b036ea..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -4,31 +4,29 @@
4#include <wx/wx.h> 4#include <wx/wx.h>
5#endif 5#endif
6 6
7#include <fstream>
8
9#include "global.h" 7#include "global.h"
10#include "tracker_config.h" 8#include "tracker_config.h"
11#include "tracker_frame.h" 9#include "tracker_frame.h"
12 10
13static std::ofstream* logfile;
14
15class TrackerApp : public wxApp { 11class TrackerApp : public wxApp {
16 public: 12 public:
17 virtual bool OnInit() { 13 virtual bool OnInit() override {
18 logfile = new std::ofstream(GetAbsolutePath("debug.log"));
19 wxLog::SetActiveTarget(new wxLogStream(logfile));
20
21#ifndef NDEBUG
22 wxLog::SetVerbose(true);
23 wxLog::SetActiveTarget(new wxLogWindow(nullptr, "Debug Log"));
24#endif
25
26 GetTrackerConfig().Load(); 14 GetTrackerConfig().Load();
27 15
28 TrackerFrame *frame = new TrackerFrame(); 16 TrackerFrame *frame = new TrackerFrame();
29 frame->Show(true); 17 frame->Show(true);
30 return true; 18 return true;
31 } 19 }
20
21 bool OnExceptionInMainLoop() override {
22 try {
23 throw;
24 } catch (const std::exception& ex) {
25 wxLogError(ex.what());
26 }
27
28 return false;
29 }
32}; 30};
33 31
34wxIMPLEMENT_APP(TrackerApp); 32wxIMPLEMENT_APP(TrackerApp);
diff --git a/src/network_set.cpp b/src/network_set.cpp index 6d2a098..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
7void NetworkSet::AddLink(int id1, int id2) { 7void 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,14 +16,37 @@ 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);
23}
24
25void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2, bool two_way) {
26 if (two_way && id2 > id1) {
27 std::swap(id1, id2);
28 }
29
30 if (!network_by_item_.count(network_id)) {
31 network_by_item_[network_id] = {};
32 }
33
34 NetworkNode node = {id1, id2, two_way};
35
36 network_by_item_[network_id].insert(node);
22} 37}
23 38
24bool NetworkSet::IsItemInNetwork(int id) const { 39bool NetworkSet::IsItemInNetwork(int id) const {
25 return network_by_item_.count(id); 40 return network_by_item_.count(id);
26} 41}
27 42
28const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const { 43const std::set<NetworkNode>& NetworkSet::GetNetworkGraph(int id) const {
29 return network_by_item_.at(id); 44 return network_by_item_.at(id);
30} 45}
46
47bool 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 e6f0c07..0f72052 100644 --- a/src/network_set.h +++ b/src/network_set.h
@@ -7,19 +7,29 @@
7#include <utility> 7#include <utility>
8#include <vector> 8#include <vector>
9 9
10struct NetworkNode {
11 int entry;
12 int exit;
13 bool two_way;
14
15 bool operator<(const NetworkNode& rhs) const;
16};
17
10class NetworkSet { 18class 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);
23
24 void AddLinkToNetwork(int network_id, int id1, int id2, bool two_way);
15 25
16 bool IsItemInNetwork(int id) const; 26 bool IsItemInNetwork(int id) const;
17 27
18 const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const; 28 const std::set<NetworkNode>& GetNetworkGraph(int id) const;
19 29
20 private: 30 private:
21 31
22 std::map<int, std::set<std::pair<int, int>>> network_by_item_; 32 std::map<int, std::set<NetworkNode>> network_by_item_;
23}; 33};
24 34
25#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
5namespace {
6
7const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"};
8const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"};
9const char* kPanelShuffleLabels[] = {"None", "Rearrange"};
10const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2",
11 "Pilgrimage"};
12const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock",
13 "Individual", "Progressive"};
14
15void 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
24OptionsPane::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
47void 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
12class 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
13namespace {
14
15std::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
24PaintingsPane::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
44void PaintingsPane::ResetIndicators() {
45 tree_ctrl_->DeleteAllItems();
46 reveal_btn_->Enable(AP_IsPaintingShuffle());
47}
48
49void 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
74void 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
86void 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
10class wxDataViewEvent;
11class wxDataViewTreeCtrl;
12
13class 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
12ReportPopup::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
28void ReportPopup::SetDoorId(int door_id) {
29 door_id_ = door_id;
30
31 ResetIndicators();
32}
33
34void ReportPopup::Reset() {
35 door_id_ = -1;
36}
37
38void 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
73void 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
109void 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
119void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) {
120 LoadIcons();
121 ResetIndicators();
122
123 event.Skip();
124}
125
126void 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
10class 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
5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { 5SettingsDialog::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
10class SettingsDialog : public wxDialog { 14class 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 6070fd5..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -1,22 +1,36 @@
1#include "subway_map.h" 1#include "subway_map.h"
2 2
3#include <fmt/core.h>
3#include <wx/dcbuffer.h> 4#include <wx/dcbuffer.h>
5#include <wx/dcgraph.h>
4 6
5#include <sstream> 7#include <sstream>
6 8
7#include "ap_state.h" 9#include "ap_state.h"
8#include "game_data.h" 10#include "game_data.h"
9#include "global.h" 11#include "global.h"
12#include "report_popup.h"
10#include "tracker_state.h" 13#include "tracker_state.h"
11 14
12constexpr int AREA_ACTUAL_SIZE = 21; 15constexpr int AREA_ACTUAL_SIZE = 21;
13constexpr int OWL_ACTUAL_SIZE = 32; 16constexpr int OWL_ACTUAL_SIZE = 32;
17constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11
18constexpr int PAINTING_EXIT_RADIUS = 6;
14 19
15enum class ItemDrawType { 20enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit };
16 kNone, 21
17 kBox, 22namespace {
18 kOwl 23
19}; 24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
25 if (subway_item.painting) {
26 return {subway_item.x, subway_item.y};
27 } else {
28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
29 subway_item.y + AREA_ACTUAL_SIZE / 2};
30 }
31}
32
33} // namespace
20 34
21SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 35SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
22 SetBackgroundStyle(wxBG_STYLE_PAINT); 36 SetBackgroundStyle(wxBG_STYLE_PAINT);
@@ -42,28 +56,109 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
42 56
43 Redraw(); 57 Redraw();
44 58
59 scroll_timer_ = new wxTimer(this);
60
45 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); 61 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
46 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); 62 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
63 Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
64 Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
67
68 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15}));
69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
70
71 help_button_ = new wxButton(this, wxID_ANY, "Help");
72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
47} 76}
48 77
49void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
50 networks_.Clear(); 79 networks_.Clear();
51 80
52 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;
53 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
54 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 85 if (AP_HasEarlyColorHallways() &&
86 subway_item.special == "early_color_hallways") {
87 entrances["early_ch"].push_back(subway_item.id);
88 }
89
90 if (AP_IsPaintingShuffle() && subway_item.painting) {
55 continue; 91 continue;
56 } 92 }
57 93
58 for (const std::string &tag : subway_item.tags) { 94 for (const std::string &tag : subway_item.tags) {
59 tagged[tag].push_back(subway_item.id); 95 tagged[tag].push_back(subway_item.id);
60 } 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 }
61 103
62 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { 104 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) {
63 std::ostringstream tag; 105 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots);
64 tag << "sunwarp" << subway_item.sunwarp->dots; 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 }
116 }
65 117
66 tagged[tag.str()].push_back(subway_item.id); 118 if (!AP_IsPilgrimageEnabled()) {
119 if (subway_item.special == "sun_painting") {
120 entrances["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 }
124 }
125 }
126
127 if (AP_IsSunwarpShuffle()) {
128 sunwarp_mapping_ = AP_GetSunwarpMapping();
129
130 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal};
131 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp);
132
133 for (const auto &[index, mapping] : sunwarp_mapping_) {
134 std::string tag = fmt::format("sunwarp{}", mapping.dots);
135
136 SubwaySunwarp fromWarp;
137 if (index < 6) {
138 fromWarp.dots = index + 1;
139 fromWarp.type = SubwaySunwarpType::kEnter;
140 } else {
141 fromWarp.dots = index - 5;
142 fromWarp.type = SubwaySunwarpType::kExit;
143 }
144
145 SubwaySunwarp toWarp;
146 if (mapping.exit_index < 6) {
147 toWarp.dots = mapping.exit_index + 1;
148 toWarp.type = SubwaySunwarpType::kEnter;
149 } else {
150 toWarp.dots = mapping.exit_index - 5;
151 toWarp.type = SubwaySunwarpType::kExit;
152 }
153
154 entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
155 exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
156
157 networks_.AddLinkToNetwork(
158 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp),
159 mapping.dots == 6 ? final_sunwarp_item
160 : GD_GetSubwayItemForSunwarp(toWarp),
161 false);
67 } 162 }
68 } 163 }
69 164
@@ -73,115 +168,243 @@ void SubwayMap::OnConnect() {
73 tag_it1++) { 168 tag_it1++) {
74 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();
75 tag_it2++) { 170 tag_it2++) {
76 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);
77 } 182 }
78 } 183 }
79 } 184 }
80 185
81 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
82} 189}
83 190
84void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
192 if (AP_IsSunwarpShuffle()) {
193 sunwarp_mapping_ = AP_GetSunwarpMapping();
194 }
195
85 if (AP_IsPaintingShuffle()) { 196 if (AP_IsPaintingShuffle()) {
86 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) {
87 if (!checked_paintings_.count(painting_id)) { 202 if (!checked_paintings_.count(painting_id)) {
88 checked_paintings_.insert(painting_id); 203 checked_paintings_.insert(painting_id);
89 204
90 if (AP_GetPaintingMapping().count(painting_id)) { 205 if (painting_mapping.count(painting_id)) {
91 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), 206 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
92 GD_GetSubwayItemForPainting( 207 std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id));
93 AP_GetPaintingMapping().at(painting_id))); 208
209 if (from_id && to_id) {
210 networks_.AddLink(*from_id, *to_id, false);
211 }
94 } 212 }
95 } 213 }
96 } 214 }
97 } 215 }
98 216
217 report_popup_->UpdateIndicators();
218
99 Redraw(); 219 Redraw();
100} 220}
101 221
102void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, 222void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
103 SubwaySunwarp to_sunwarp) { 223 SubwaySunwarp to_sunwarp) {
104 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), 224 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
105 GD_GetSubwayItemForSunwarp(to_sunwarp)); 225 GD_GetSubwayItemForSunwarp(to_sunwarp), false);
226}
227
228void SubwayMap::Zoom(bool in) {
229 wxPoint focus_point;
230
231 if (mouse_position_) {
232 focus_point = *mouse_position_;
233 } else {
234 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
235 }
236
237 if (in) {
238 if (zoom_ < 3.0) {
239 SetZoom(zoom_ + 0.25, focus_point);
240 }
241 } else {
242 if (zoom_ > 1.0) {
243 SetZoom(zoom_ - 0.25, focus_point);
244 }
245 }
106} 246}
107 247
108void SubwayMap::OnPaint(wxPaintEvent &event) { 248void SubwayMap::OnPaint(wxPaintEvent &event) {
109 if (GetSize() != rendered_.GetSize()) { 249 if (GetSize() != rendered_.GetSize()) {
110 Redraw(); 250 wxSize panel_size = GetSize();
111 } 251 wxSize image_size = map_image_.GetSize();
252
253 render_x_ = 0;
254 render_y_ = 0;
255 render_width_ = panel_size.GetWidth();
256 render_height_ = panel_size.GetHeight();
257
258 if (image_size.GetWidth() * panel_size.GetHeight() >
259 panel_size.GetWidth() * image_size.GetHeight()) {
260 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
261 image_size.GetWidth();
262 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
263 } else {
264 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
265 image_size.GetHeight();
266 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
267 }
112 268
113 wxBufferedPaintDC dc(this); 269 SetZoomPos({zoom_x_, zoom_y_});
114 dc.DrawBitmap(rendered_, 0, 0);
115 270
116 if (hovered_item_ && networks_.IsItemInNetwork(*hovered_item_)) { 271 SetUpHelpButton();
117 dc.SetBrush(*wxTRANSPARENT_BRUSH);
118 272
119 for (const auto &[item_id1, item_id2] : 273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
120 networks_.GetNetworkGraph(*hovered_item_)) { 274 wxDefaultCoord, wxSIZE_AUTO);
121 const SubwayItem &item1 = GD_GetSubwayItem(item_id1); 275 }
122 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
123 276
124 int item1_x = (item1.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; 277 wxBufferedPaintDC dc(this);
125 int item1_y = (item1.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; 278 dc.SetBackground(*wxWHITE_BRUSH);
279 dc.Clear();
280
281 {
282 wxMemoryDC rendered_dc;
283 rendered_dc.SelectObject(rendered_);
284
285 int dst_x;
286 int dst_y;
287 int dst_w;
288 int dst_h;
289 int src_x;
290 int src_y;
291 int src_w;
292 int src_h;
293
294 int zoomed_width = render_width_ * zoom_;
295 int zoomed_height = render_height_ * zoom_;
296
297 if (zoomed_width <= GetSize().GetWidth()) {
298 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
299 dst_w = zoomed_width;
300 src_x = 0;
301 src_w = map_image_.GetWidth();
302 } else {
303 dst_x = 0;
304 dst_w = GetSize().GetWidth();
305 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
306 src_w =
307 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
308 }
126 309
127 int item2_x = (item2.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; 310 if (zoomed_height <= GetSize().GetHeight()) {
128 int item2_y = (item2.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; 311 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
312 dst_h = zoomed_height;
313 src_y = 0;
314 src_h = map_image_.GetHeight();
315 } else {
316 dst_y = 0;
317 dst_h = GetSize().GetHeight();
318 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
319 src_h =
320 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
321 }
129 322
130 int left = std::min(item1_x, item2_x); 323 wxGCDC gcdc(dc);
131 int top = std::min(item1_y, item2_y); 324 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
132 int right = std::max(item1_x, item2_x); 325 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
133 int bottom = std::max(item1_y, item2_y); 326 src_w, src_h);
327 }
134 328
135 int halfwidth = right - left; 329 if (hovered_item_) {
136 int halfheight = bottom - top; 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
332
333 for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) {
334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
336
337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
339
340 int left = std::min(item1_pos.x, item2_pos.x);
341 int top = std::min(item1_pos.y, item2_pos.y);
342 int right = std::max(item1_pos.x, item2_pos.x);
343 int bottom = std::max(item1_pos.y, item2_pos.y);
344
345 int halfwidth = right - left;
346 int halfheight = bottom - top;
347
348 if (halfwidth < 4 || halfheight < 4) {
349 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
350 dc.DrawLine(item1_pos, item2_pos);
351 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
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 }
359 } else {
360 int ellipse_x;
361 int ellipse_y;
362 double start;
363 double end;
137 364
138 if (halfwidth < 4 || halfheight < 4) { 365 if (item1_pos.x > item2_pos.x) {
139 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); 366 ellipse_y = top;
140 dc.DrawLine(item1_x, item1_y, item2_x, item2_y);
141 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
142 dc.DrawLine(item1_x, item1_y, item2_x, item2_y);
143 } else {
144 int ellipse_x;
145 int ellipse_y;
146 double start;
147 double end;
148 367
149 if (item1_x > item2_x) { 368 if (item1_pos.y > item2_pos.y) {
150 ellipse_y = top; 369 ellipse_x = left - halfwidth;
151 370
152 if (item1_y > item2_y) { 371 start = 0;
153 ellipse_x = left - halfwidth; 372 end = 90;
373 } else {
374 ellipse_x = left;
154 375
155 start = 0; 376 start = 90;
156 end = 90; 377 end = 180;
378 }
157 } else { 379 } else {
158 ellipse_x = left; 380 ellipse_y = top - halfheight;
159 381
160 start = 90; 382 if (item1_pos.y > item2_pos.y) {
161 end = 180; 383 ellipse_x = left - halfwidth;
162 }
163 } else {
164 ellipse_y = top - halfheight;
165 384
166 if (item1_y > item2_y) { 385 start = 270;
167 ellipse_x = left - halfwidth; 386 end = 360;
387 } else {
388 ellipse_x = left;
168 389
169 start = 270; 390 start = 180;
170 end = 360; 391 end = 270;
171 } else { 392 }
172 ellipse_x = left; 393 }
173 394
174 start = 180; 395 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
175 end = 270; 396 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
397 halfheight * 2, start, end);
398 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
399 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
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);
176 } 406 }
177 } 407 }
178
179 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
180 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2,
181 start, end);
182 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
183 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2,
184 start, end);
185 } 408 }
186 } 409 }
187 } 410 }
@@ -190,137 +413,426 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
190} 413}
191 414
192void SubwayMap::OnMouseMove(wxMouseEvent &event) { 415void SubwayMap::OnMouseMove(wxMouseEvent &event) {
193 int mouse_x = std::clamp( 416 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
194 (event.GetX() - render_x_) * map_image_.GetWidth() / render_width_,
195 0, map_image_.GetWidth() - 1);
196 int mouse_y = std::clamp(
197 (event.GetY() - render_y_) * map_image_.GetWidth() / render_width_,
198 0, map_image_.GetHeight() - 1);
199 417
200 std::vector<int> hovered = tree_->query( 418 std::vector<int> hovered = tree_->query(
201 {static_cast<float>(mouse_x), static_cast<float>(mouse_y), 2, 2}); 419 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
202 std::optional<int> new_hovered_item;
203 if (!hovered.empty()) { 420 if (!hovered.empty()) {
204 new_hovered_item = hovered[0]; 421 actual_hover_ = hovered[0];
422 } else {
423 actual_hover_ = std::nullopt;
205 } 424 }
206 425
207 if (new_hovered_item != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
208 hovered_item_ = new_hovered_item; 427 EvaluateHover();
428 }
209 429
210 Refresh(); 430 if (scroll_mode_) {
431 EvaluateScroll(event.GetPosition());
211 } 432 }
212 433
434 mouse_position_ = event.GetPosition();
435
213 event.Skip(); 436 event.Skip();
214} 437}
215 438
216void SubwayMap::Redraw() { 439void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
217 wxSize panel_size = GetSize(); 440 double new_zoom = zoom_;
218 wxSize image_size = map_image_.GetSize(); 441 if (event.GetWheelRotation() > 0) {
219 442 new_zoom = std::min(3.0, zoom_ + 0.25);
220 render_x_ = 0;
221 render_y_ = 0;
222 render_width_ = panel_size.GetWidth();
223 render_height_ = panel_size.GetHeight();
224
225 if (image_size.GetWidth() * panel_size.GetHeight() >
226 panel_size.GetWidth() * image_size.GetHeight()) {
227 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
228 image_size.GetWidth();
229 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
230 } else { 443 } else {
231 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / 444 new_zoom = std::max(1.0, zoom_ - 0.25);
232 image_size.GetHeight(); 445 }
233 render_x_ = (panel_size.GetWidth() - render_width_) / 2; 446
447 if (zoom_ != new_zoom) {
448 SetZoom(new_zoom, event.GetPosition());
449 }
450
451 event.Skip();
452}
453
454void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
455 SetScrollSpeed(0, 0);
456 mouse_position_ = std::nullopt;
457}
458
459void SubwayMap::OnMouseClick(wxMouseEvent &event) {
460 bool finished = false;
461
462 if (actual_hover_) {
463 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
464 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
465
466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
467 networks_.IsItemInNetwork(*hovered_item_)) {
468 if (actual_hover_ != hovered_item_) {
469 EvaluateHover();
470
471 if (!hovered_item_) {
472 sticky_hover_ = false;
473 }
474 } else {
475 sticky_hover_ = !sticky_hover_;
476 }
477
478 finished = true;
479 }
480 }
481
482 if (!finished) {
483 if (scroll_mode_) {
484 scroll_mode_ = false;
485
486 SetScrollSpeed(0, 0);
487
488 SetCursor(wxCURSOR_ARROW);
489 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
490 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
491 event.GetPosition().y < GetSize().GetHeight() / 6 ||
492 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
493 scroll_mode_ = true;
494
495 EvaluateScroll(event.GetPosition());
496
497 SetCursor(wxCURSOR_CROSS);
498 } else {
499 sticky_hover_ = false;
500 }
501 }
502}
503
504void SubwayMap::OnTimer(wxTimerEvent &event) {
505 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
506 Refresh();
507}
508
509void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
510 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
511
512 if (new_zoom != zoom_) {
513 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
234 } 514 }
515}
235 516
236 rendered_ = wxBitmap( 517void SubwayMap::OnClickHelp(wxCommandEvent &event) {
237 map_image_ 518 wxMessageBox(
238 .Scale(render_width_, render_height_, wxIMAGE_QUALITY_BILINEAR) 519 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
239 .Size(panel_size, {render_x_, render_y_}, 255, 255, 255)); 520 "corner.\nClick on a side of the screen to start panning. It will follow "
521 "your mouse. Click again to stop.\nHover over a door to see the "
522 "requirements to open it.\nHover over a warp or active painting to see "
523 "what it is connected to.\nFor one-way connections, there will be a "
524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
525 "that the painting is locked by a door.\nA blue circle means painting "
526 "shuffle is enabled and the painting has not been checked yet.\nA black "
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.",
530 "Subway Map Help");
531}
532
533void SubwayMap::Redraw() {
534 rendered_ = wxBitmap(map_image_);
240 535
241 wxMemoryDC dc; 536 wxMemoryDC dc;
242 dc.SelectObject(rendered_); 537 dc.SelectObject(rendered_);
243 538
539 wxGCDC gcdc(dc);
540
541 std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping();
542
244 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
245 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
246 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
247 std::optional<wxColour> shade_color; 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
547
548 if (AP_HasEarlyColorHallways() &&
549 subway_item.special == "early_color_hallways") {
550 draw_type = ItemDrawType::kOwl;
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);
248 559
249 if (subway_item.door) {
250 draw_type = ItemDrawType::kBox; 560 draw_type = ItemDrawType::kBox;
251 561 if (AP_HasItemSafe(color_item_id)) {
252 if (IsDoorOpen(*subway_item.door)) { 562 brush_color = wxGREEN_BRUSH;
253 if (!subway_item.paintings.empty()) { 563 } else {
254 draw_type = ItemDrawType::kOwl; 564 brush_color = wxRED_BRUSH;
255 } else { 565 }
566 } else if (subway_item.special == "sun_painting") {
567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
569 if (IsDoorOpen(*subway_item.door)) {
256 brush_color = wxGREEN_BRUSH; 570 brush_color = wxGREEN_BRUSH;
571 } else {
572 brush_color = wxRED_BRUSH;
257 } 573 }
574 }
575 } else if (subway_item.sunwarp &&
576 subway_item.sunwarp->type == SubwaySunwarpType::kFinal &&
577 AP_IsPilgrimageEnabled()) {
578 draw_type = ItemDrawType::kBox;
579
580 if (IsPilgrimageDoable()) {
581 brush_color = wxGREEN_BRUSH;
258 } else { 582 } else {
259 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
260 } 584 }
261 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
262 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
263 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
264 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
265 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
266 590 if (!checked_paintings_.count(*subway_item.painting)) {
267 for (const std::string &painting_id : subway_item.paintings) { 591 draw_type = ItemDrawType::kOwl;
268 if (checked_paintings_.count(painting_id)) { 592 brush_color = wxBLUE_BRUSH;
269 has_checked_painting = true; 593 } else if (painting_mapping.count(*subway_item.painting)) {
270 594 draw_type = ItemDrawType::kOwl;
271 if (AP_GetPaintingMapping().count(painting_id)) { 595 brush_color = wxGREEN_BRUSH;
272 has_mapped_painting = true; 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
273 } 597 draw_type = ItemDrawType::kOwlExit;
274 } else { 598 brush_color = wxGREEN_BRUSH;
275 has_unchecked_painting = true;
276 }
277 } 599 }
278 600 } else if (subway_item.HasWarps()) {
279 if (has_unchecked_painting || has_mapped_painting) { 601 brush_color = wxGREEN_BRUSH;
602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
280 draw_type = ItemDrawType::kOwl; 605 draw_type = ItemDrawType::kOwl;
281
282 if (has_unchecked_painting) {
283 if (has_checked_painting) {
284 shade_color = wxColour(255, 255, 0, 100);
285 } else {
286 shade_color = wxColour(100, 100, 100, 100);
287 }
288 }
289 } 606 }
290 } else if (!subway_item.tags.empty()) { 607 }
291 draw_type = ItemDrawType::kOwl; 608 } else if (subway_door) {
609 draw_type = ItemDrawType::kBox;
610
611 if (IsDoorOpen(*subway_door)) {
612 brush_color = wxGREEN_BRUSH;
613 } else {
614 brush_color = wxRED_BRUSH;
292 } 615 }
293 } 616 }
294 617
295 int real_area_x = 618 wxPoint real_area_pos = {subway_item.x, subway_item.y};
296 render_x_ + subway_item.x * render_width_ / image_size.GetWidth();
297 int real_area_y =
298 render_y_ + subway_item.y * render_width_ / image_size.GetWidth();
299 619
300 int real_area_size = 620 int real_area_size =
301 render_width_ * 621 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
302 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE) /
303 image_size.GetWidth();
304 if (real_area_size == 0) {
305 real_area_size = 1;
306 }
307 622
308 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
309 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
310 dc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
311 dc.DrawRectangle({real_area_x, real_area_y}, 626
312 {real_area_size, real_area_size}); 627 if (subway_item.tilted) {
313 } else if (draw_type == ItemDrawType::kOwl) { 628 constexpr int AREA_TILTED_SIDE =
314 wxBitmap owl_bitmap = wxBitmap( 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
315 owl_image_.Scale(real_area_size, real_area_size, 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
316 wxIMAGE_QUALITY_BILINEAR)); 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
317 dc.DrawBitmap(owl_bitmap, {real_area_x, real_area_y}); 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
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 }
318 } 653 }
319 } 654 }
320} 655}
321 656
322quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int& id) const { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
660 help_button_->SetPosition({
661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
662 15,
663 });
664}
665
666void SubwayMap::EvaluateScroll(wxPoint pos) {
667 int scroll_x;
668 int scroll_y;
669 if (pos.x < GetSize().GetWidth() / 9) {
670 scroll_x = 20;
671 } else if (pos.x < GetSize().GetWidth() / 6) {
672 scroll_x = 5;
673 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
674 scroll_x = -20;
675 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
676 scroll_x = -5;
677 } else {
678 scroll_x = 0;
679 }
680 if (pos.y < GetSize().GetHeight() / 9) {
681 scroll_y = 20;
682 } else if (pos.y < GetSize().GetHeight() / 6) {
683 scroll_y = 5;
684 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
685 scroll_y = -20;
686 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
687 scroll_y = -5;
688 } else {
689 scroll_y = 0;
690 }
691
692 SetScrollSpeed(scroll_x, scroll_y);
693}
694
695void 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
740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
742 map_image_.GetSize().GetWidth() +
743 zoom_x_),
744 static_cast<int>(pos.y * render_width_ * zoom_ /
745 map_image_.GetSize().GetWidth() +
746 zoom_y_)};
747}
748
749wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
750 return {static_cast<int>(pos.x * render_width_ * zoom_ /
751 map_image_.GetSize().GetWidth()),
752 static_cast<int>(pos.y * render_width_ * zoom_ /
753 map_image_.GetSize().GetWidth())};
754}
755
756wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
757 return {
758 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
759 render_width_ / zoom_),
760 0, map_image_.GetWidth() - 1),
761 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
762 render_width_ / zoom_),
763 0, map_image_.GetHeight() - 1)};
764}
765
766void SubwayMap::SetZoomPos(wxPoint pos) {
767 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
768 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
769 } else {
770 zoom_x_ = std::clamp(
771 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
772 0);
773 }
774 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
775 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
776 } else {
777 zoom_y_ = std::clamp(
778 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
779 0);
780 }
781}
782
783void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
784 bool should_timer = (scroll_x != 0 || scroll_y != 0);
785 if (should_timer != scroll_timer_->IsRunning()) {
786 if (should_timer) {
787 scroll_timer_->Start(1000 / 60);
788 } else {
789 scroll_timer_->Stop();
790 }
791 }
792
793 scroll_x_ = scroll_x;
794 scroll_y_ = scroll_y;
795}
796
797void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
798 wxPoint map_pos = RenderPosToMapPos(static_point);
799 zoom_ = zoom;
800
801 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
802 SetZoomPos(-(virtual_pos - static_point));
803
804 Refresh();
805
806 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
807}
808
809std::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
827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
323 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
324 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
325 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 }
326} 838}
diff --git a/src/subway_map.h b/src/subway_map.h index e5f0bf6..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
22class ReportPopup;
23
21class SubwayMap : public wxPanel { 24class SubwayMap : public wxPanel {
22 public: 25 public:
23 SubwayMap(wxWindow *parent); 26 SubwayMap(wxWindow *parent);
@@ -25,12 +28,33 @@ class SubwayMap : public wxPanel {
25 void OnConnect(); 28 void OnConnect();
26 void UpdateIndicators(); 29 void UpdateIndicators();
27 void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp); 30 void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp);
31 void Zoom(bool in);
28 32
29 private: 33 private:
30 void OnPaint(wxPaintEvent &event); 34 void OnPaint(wxPaintEvent &event);
31 void OnMouseMove(wxMouseEvent &event); 35 void OnMouseMove(wxMouseEvent &event);
36 void OnMouseScroll(wxMouseEvent &event);
37 void OnMouseLeave(wxMouseEvent &event);
38 void OnMouseClick(wxMouseEvent &event);
39 void OnTimer(wxTimerEvent &event);
40 void OnZoomSlide(wxCommandEvent &event);
41 void OnClickHelp(wxCommandEvent &event);
32 42
33 void Redraw(); 43 void Redraw();
44 void SetUpHelpButton();
45
46 wxPoint MapPosToRenderPos(wxPoint pos) const;
47 wxPoint MapPosToVirtualPos(wxPoint pos) const;
48 wxPoint RenderPosToMapPos(wxPoint pos) const;
49
50 void EvaluateScroll(wxPoint pos);
51 void EvaluateHover();
52
53 void SetZoomPos(wxPoint pos);
54 void SetScrollSpeed(int scroll_x, int scroll_y);
55 void SetZoom(double zoom, wxPoint static_point);
56
57 std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item);
34 58
35 wxImage map_image_; 59 wxImage map_image_;
36 wxImage owl_image_; 60 wxImage owl_image_;
@@ -38,8 +62,23 @@ class SubwayMap : public wxPanel {
38 wxBitmap rendered_; 62 wxBitmap rendered_;
39 int render_x_ = 0; 63 int render_x_ = 0;
40 int render_y_ = 0; 64 int render_y_ = 0;
41 int render_width_ = 0; 65 int render_width_ = 1;
42 int render_height_ = 0; 66 int render_height_ = 1;
67
68 double zoom_ = 1.0;
69 int zoom_x_ = 0; // in render space
70 int zoom_y_ = 0;
71
72 bool scroll_mode_ = false;
73 wxTimer* scroll_timer_;
74 int scroll_x_ = 0;
75 int scroll_y_ = 0;
76
77 wxSlider *zoom_slider_;
78
79 wxButton *help_button_;
80
81 std::optional<wxPoint> mouse_position_;
43 82
44 struct GetItemBox { 83 struct GetItemBox {
45 quadtree::Box<float> operator()(const int &id) const; 84 quadtree::Box<float> operator()(const int &id) const;
@@ -47,9 +86,16 @@ class SubwayMap : public wxPanel {
47 86
48 std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_; 87 std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_;
49 std::optional<int> hovered_item_; 88 std::optional<int> hovered_item_;
89 std::optional<int> actual_hover_;
90 bool sticky_hover_ = false;
91
92 ReportPopup *report_popup_;
50 93
51 NetworkSet networks_; 94 NetworkSet networks_;
52 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_;
53}; 99};
54 100
55#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 e944704..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -1,30 +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>
6#include <wx/filedlg.h>
7#include <wx/notebook.h>
8#include <wx/splitter.h>
9#include <wx/stdpaths.h>
5#include <wx/webrequest.h> 10#include <wx/webrequest.h>
6 11
12#include <algorithm>
7#include <nlohmann/json.hpp> 13#include <nlohmann/json.hpp>
8#include <sstream> 14#include <sstream>
9 15
10#include "achievements_pane.h" 16#include "achievements_pane.h"
11#include "ap_state.h" 17#include "ap_state.h"
12#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"
13#include "settings_dialog.h" 26#include "settings_dialog.h"
14#include "subway_map.h" 27#include "subway_map.h"
15#include "tracker_config.h" 28#include "tracker_config.h"
16#include "tracker_panel.h" 29#include "tracker_panel.h"
17#include "version.h" 30#include "version.h"
18 31
32namespace {
33
34std::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
19enum TrackerFrameIds { 48enum TrackerFrameIds {
20 ID_CONNECT = 1, 49 ID_AP_CONNECT = 1,
21 ID_CHECK_FOR_UPDATES = 2, 50 ID_CHECK_FOR_UPDATES = 2,
22 ID_SETTINGS = 3 51 ID_SETTINGS = 3,
52 ID_ZOOM_IN = 4,
53 ID_ZOOM_OUT = 5,
54 ID_IPC_CONNECT = 7,
55 ID_LOG_DIALOG = 8,
23}; 56};
24 57
25wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); 58wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
26wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 59wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent);
27wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 60wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
61wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent);
28 62
29TrackerFrame::TrackerFrame() 63TrackerFrame::TrackerFrame()
30 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, 64 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
@@ -32,52 +66,87 @@ TrackerFrame::TrackerFrame()
32 ::wxInitAllImageHandlers(); 66 ::wxInitAllImageHandlers();
33 67
34 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();
35 75
36 wxMenu *menuFile = new wxMenu(); 76 wxMenu *menuFile = new wxMenu();
37 menuFile->Append(ID_CONNECT, "&Connect"); 77 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
78 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
38 menuFile->Append(ID_SETTINGS, "&Settings"); 79 menuFile->Append(ID_SETTINGS, "&Settings");
39 menuFile->Append(wxID_EXIT); 80 menuFile->Append(wxID_EXIT);
40 81
82 wxMenu *menuView = new wxMenu();
83 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\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");
87
88 zoom_in_menu_item_->Enable(false);
89 zoom_out_menu_item_->Enable(false);
90
41 wxMenu *menuHelp = new wxMenu(); 91 wxMenu *menuHelp = new wxMenu();
42 menuHelp->Append(wxID_ABOUT); 92 menuHelp->Append(wxID_ABOUT);
43 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates"); 93 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates");
44 94
45 wxMenuBar *menuBar = new wxMenuBar(); 95 wxMenuBar *menuBar = new wxMenuBar();
46 menuBar->Append(menuFile, "&File"); 96 menuBar->Append(menuFile, "&File");
97 menuBar->Append(menuView, "&View");
47 menuBar->Append(menuHelp, "&Help"); 98 menuBar->Append(menuHelp, "&Help");
48 99
49 SetMenuBar(menuBar); 100 SetMenuBar(menuBar);
50 101
51 CreateStatusBar(); 102 CreateStatusBar();
52 SetStatusText("Not connected to Archipelago.");
53 103
54 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); 104 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
55 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); 105 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
56 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);
57 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); 108 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
58 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, 109 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
59 ID_CHECK_FOR_UPDATES); 110 ID_CHECK_FOR_UPDATES);
111 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
112 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
113 Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG);
114 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
115 Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged,
116 this);
60 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); 117 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
61 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 118 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
62 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));
63 123
64 achievements_pane_ = new AchievementsPane(this); 124 splitter_window_ = new wxSplitterWindow(this, wxID_ANY);
125 splitter_window_->SetMinimumPaneSize(logicalSize.x / 5);
65 126
66 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); 127 wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY);
128
129 achievements_pane_ = new AchievementsPane(choicebook);
67 choicebook->AddPage(achievements_pane_, "Achievements"); 130 choicebook->AddPage(achievements_pane_, "Achievements");
68 131
69 wxNotebook *rightpane = new wxNotebook(this, wxID_ANY); 132 items_pane_ = new ItemsPane(choicebook);
70 tracker_panel_ = new TrackerPanel(rightpane); 133 choicebook->AddPage(items_pane_, "Items");
71 subway_map_ = new SubwayMap(rightpane); 134
72 rightpane->AddPage(tracker_panel_, "Map"); 135 options_pane_ = new OptionsPane(choicebook);
73 rightpane->AddPage(subway_map_, "Subway"); 136 choicebook->AddPage(options_pane_, "Options");
137
138 paintings_pane_ = new PaintingsPane(choicebook);
139 choicebook->AddPage(paintings_pane_, "Paintings");
74 140
75 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 141 notebook_ = new wxNotebook(splitter_window_, wxID_ANY);
76 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1)); 142 tracker_panel_ = new TrackerPanel(notebook_);
77 top_sizer->Add(rightpane, wxSizerFlags().Expand().Proportion(3)); 143 subway_map_ = new SubwayMap(notebook_);
144 notebook_->AddPage(tracker_panel_, "Map");
145 notebook_->AddPage(subway_map_, "Subway");
78 146
79 SetSizerAndFit(top_sizer); 147 splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4);
80 SetSize(1280, 728); 148
149 SetSize(logicalSize);
81 150
82 if (!GetTrackerConfig().asked_to_check_for_updates) { 151 if (!GetTrackerConfig().asked_to_check_for_updates) {
83 GetTrackerConfig().asked_to_check_for_updates = true; 152 GetTrackerConfig().asked_to_check_for_updates = true;
@@ -94,23 +163,28 @@ TrackerFrame::TrackerFrame()
94 } 163 }
95 164
96 if (GetTrackerConfig().should_check_for_updates) { 165 if (GetTrackerConfig().should_check_for_updates) {
97 CheckForUpdates(/*manual=*/false); 166 updater_->CheckForUpdates(/*invisible=*/true);
98 } 167 }
168
169 SetStatusText(GetStatusMessage());
99} 170}
100 171
101void TrackerFrame::SetStatusMessage(std::string message) { 172void TrackerFrame::ConnectToAp(std::string server, std::string user,
102 wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED); 173 std::string pass) {
103 event->SetString(message.c_str()); 174 QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server),
175 std::move(user), std::move(pass)));
176}
104 177
105 QueueEvent(event); 178void TrackerFrame::UpdateStatusMessage() {
179 QueueEvent(new wxCommandEvent(STATUS_CHANGED));
106} 180}
107 181
108void TrackerFrame::ResetIndicators() { 182void TrackerFrame::ResetIndicators() {
109 QueueEvent(new wxCommandEvent(STATE_RESET)); 183 QueueEvent(new wxCommandEvent(STATE_RESET));
110} 184}
111 185
112void TrackerFrame::UpdateIndicators() { 186void TrackerFrame::UpdateIndicators(StateUpdate state) {
113 QueueEvent(new wxCommandEvent(STATE_CHANGED)); 187 QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state)));
114} 188}
115 189
116void TrackerFrame::OnAbout(wxCommandEvent &event) { 190void TrackerFrame::OnAbout(wxCommandEvent &event) {
@@ -118,6 +192,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
118 about_info.SetName("Lingo Archipelago Tracker"); 192 about_info.SetName("Lingo Archipelago Tracker");
119 about_info.SetVersion(kTrackerVersion.ToString()); 193 about_info.SetVersion(kTrackerVersion.ToString());
120 about_info.AddDeveloper("hatkirby"); 194 about_info.AddDeveloper("hatkirby");
195 about_info.AddDeveloper("art0007i");
121 about_info.AddArtist("Brenton Wildes"); 196 about_info.AddArtist("Brenton Wildes");
122 about_info.AddArtist("kinrah"); 197 about_info.AddArtist("kinrah");
123 198
@@ -126,7 +201,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
126 201
127void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } 202void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
128 203
129void TrackerFrame::OnConnect(wxCommandEvent &event) { 204void TrackerFrame::OnApConnect(wxCommandEvent &event) {
130 ConnectionDialog dlg; 205 ConnectionDialog dlg;
131 206
132 if (dlg.ShowModal() == wxID_OK) { 207 if (dlg.ShowModal() == wxID_OK) {
@@ -144,7 +219,7 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
144 } 219 }
145 } 220 }
146 221
147 while (new_history.size() > 5) { 222 while (new_history.size() > 10) {
148 new_history.pop_back(); 223 new_history.pop_back();
149 } 224 }
150 225
@@ -156,6 +231,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
156 } 231 }
157} 232}
158 233
234void 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
159void TrackerFrame::OnSettings(wxCommandEvent &event) { 245void TrackerFrame::OnSettings(wxCommandEvent &event) {
160 SettingsDialog dlg; 246 SettingsDialog dlg;
161 247
@@ -163,83 +249,119 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
163 GetTrackerConfig().should_check_for_updates = 249 GetTrackerConfig().should_check_for_updates =
164 dlg.GetShouldCheckForUpdates(); 250 dlg.GetShouldCheckForUpdates();
165 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); 251 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
166 GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); 252 GetTrackerConfig().visible_panels = dlg.GetVisiblePanels();
253 GetTrackerConfig().track_position = dlg.GetTrackPosition();
167 GetTrackerConfig().Save(); 254 GetTrackerConfig().Save();
168 255
169 UpdateIndicators(); 256 UpdateIndicators(StateUpdate{.cleared_locations = true,
257 .player_position = true,
258 .changed_settings = true});
170 } 259 }
171} 260}
172 261
173void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 262void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
174 CheckForUpdates(/*manual=*/true); 263 updater_->CheckForUpdates(/*invisible=*/false);
175} 264}
176 265
177void TrackerFrame::OnStateReset(wxCommandEvent& event) { 266void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
178 tracker_panel_->UpdateIndicators(); 267 if (notebook_->GetSelection() == 1) {
179 achievements_pane_->UpdateIndicators(); 268 subway_map_->Zoom(true);
180 subway_map_->OnConnect(); 269 }
181 Refresh();
182} 270}
183 271
184void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 272void TrackerFrame::OnZoomOut(wxCommandEvent &event) {
185 tracker_panel_->UpdateIndicators(); 273 if (notebook_->GetSelection() == 1) {
186 achievements_pane_->UpdateIndicators(); 274 subway_map_->Zoom(false);
187 subway_map_->UpdateIndicators(); 275 }
188 Refresh();
189} 276}
190 277
191void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { 278void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) {
192 SetStatusText(event.GetString()); 279 if (log_dialog_ == nullptr) {
280 log_dialog_ = new LogDialog(this);
281 log_dialog_->Show();
282 TrackerSetLogDialog(log_dialog_);
283
284 log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow,
285 this);
286 } else {
287 log_dialog_->SetFocus();
288 }
193} 289}
194 290
195void TrackerFrame::CheckForUpdates(bool manual) { 291void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) {
196 wxWebRequest request = wxWebSession::GetDefault().CreateRequest( 292 TrackerSetLogDialog(nullptr);
197 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); 293 log_dialog_ = nullptr;
198 294
199 if (!request.IsOk()) { 295 event.Skip();
200 if (manual) { 296}
201 wxMessageBox("Could not check for updates.", "Error", 297
202 wxOK | wxICON_ERROR); 298void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
203 } else { 299 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
204 SetStatusText("Could not check for updates."); 300 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
301}
302
303void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) {
304 notebook_->Refresh();
305}
306
307void TrackerFrame::OnStateReset(wxCommandEvent &event) {
308 tracker_panel_->UpdateIndicators(/*reset=*/true);
309 achievements_pane_->UpdateIndicators();
310 items_pane_->ResetIndicators();
311 options_pane_->OnConnect();
312 paintings_pane_->ResetIndicators();
313 subway_map_->OnConnect();
314 Refresh();
315}
316
317void TrackerFrame::OnStateChanged(StateChangedEvent &event) {
318 const StateUpdate &state = event.GetState();
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();
205 } 341 }
342 }
206 343
207 return; 344 if (std::any_of(state.panels.begin(), state.panels.end(),
345 [](int solve_index) {
346 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index))
347 .achievement;
348 })) {
349 achievements_pane_->UpdateIndicators();
208 } 350 }
209 351
210 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { 352 if (!state.items.empty()) {
211 if (evt.GetState() == wxWebRequest::State_Completed) { 353 items_pane_->UpdateIndicators(state.items);
212 std::string response = evt.GetResponse().AsString().ToStdString(); 354 }
213 355
214 Version latest_version(response); 356 if (!state.paintings.empty()) {
215 if (kTrackerVersion < latest_version) { 357 paintings_pane_->UpdateIndicators(state.paintings);
216 std::ostringstream message_text; 358 }
217 message_text << "There is a newer version of Lingo AP Tracker " 359}
218 "available. You have " 360
219 << kTrackerVersion.ToString() 361void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
220 << ", and the latest version is " 362 SetStatusText(wxString::FromUTF8(GetStatusMessage()));
221 << latest_version.ToString() 363}
222 << ". Would you like to update?";
223
224 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
225 wxYES) {
226 wxLaunchDefaultBrowser(
227 "https://code.fourisland.com/lingo-ap-tracker/about/"
228 "CHANGELOG.md");
229 }
230 } else if (manual) {
231 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
232 wxOK);
233 }
234 } else if (evt.GetState() == wxWebRequest::State_Failed) {
235 if (manual) {
236 wxMessageBox("Could not check for updates.", "Error",
237 wxOK | wxICON_ERROR);
238 } else {
239 SetStatusText("Could not check for updates.");
240 }
241 }
242 });
243 364
244 request.Start(); 365void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
366 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
245} 367}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index f1d7171..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,39 +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
10class AchievementsPane; 17class AchievementsPane;
18class ItemsPane;
19class LogDialog;
20class OptionsPane;
21class PaintingsPane;
11class SubwayMap; 22class SubwayMap;
12class TrackerPanel; 23class TrackerPanel;
24class wxBookCtrlEvent;
25class wxNotebook;
26class wxSplitterEvent;
27class wxSplitterWindow;
28
29class 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
52struct 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
62class 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};
13 74
14wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); 75wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
15wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 76wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent);
16wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 77wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
78wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent);
17 79
18class TrackerFrame : public wxFrame { 80class TrackerFrame : public wxFrame {
19 public: 81 public:
20 TrackerFrame(); 82 TrackerFrame();
21 83
22 void SetStatusMessage(std::string message); 84 void ConnectToAp(std::string server, std::string user, std::string pass);
85 void UpdateStatusMessage();
23 86
24 void ResetIndicators(); 87 void ResetIndicators();
25 void UpdateIndicators(); 88 void UpdateIndicators(StateUpdate state);
26 89
27 private: 90 private:
28 void OnExit(wxCommandEvent &event); 91 void OnExit(wxCommandEvent &event);
29 void OnAbout(wxCommandEvent &event); 92 void OnAbout(wxCommandEvent &event);
30 void OnConnect(wxCommandEvent &event); 93 void OnApConnect(wxCommandEvent &event);
94 void OnIpcConnect(wxCommandEvent &event);
31 void OnSettings(wxCommandEvent &event); 95 void OnSettings(wxCommandEvent &event);
32 void OnCheckForUpdates(wxCommandEvent &event); 96 void OnCheckForUpdates(wxCommandEvent &event);
97 void OnZoomIn(wxCommandEvent &event);
98 void OnZoomOut(wxCommandEvent &event);
99 void OnOpenLogWindow(wxCommandEvent &event);
100 void OnCloseLogWindow(wxCloseEvent &event);
101 void OnChangePage(wxBookCtrlEvent &event);
102 void OnSashPositionChanged(wxSplitterEvent &event);
33 103
34 void OnStateReset(wxCommandEvent &event); 104 void OnStateReset(wxCommandEvent &event);
35 void OnStateChanged(wxCommandEvent &event); 105 void OnStateChanged(StateChangedEvent &event);
36 void OnStatusChanged(wxCommandEvent &event); 106 void OnStatusChanged(wxCommandEvent &event);
107 void OnConnectToAp(ApConnectEvent &event);
108
109 std::unique_ptr<Updater> updater_;
37 110
38 void CheckForUpdates(bool manual); 111 wxSplitterWindow *splitter_window_;
39 112 wxNotebook *notebook_;
40 TrackerPanel *tracker_panel_; 113 TrackerPanel *tracker_panel_;
41 AchievementsPane *achievements_pane_; 114 AchievementsPane *achievements_pane_;
115 ItemsPane *items_pane_;
116 OptionsPane *options_pane_;
117 PaintingsPane *paintings_pane_;
42 SubwayMap *subway_map_; 118 SubwayMap *subway_map_;
119 LogDialog *log_dialog_ = nullptr;
120
121 wxMenuItem *zoom_in_menu_item_;
122 wxMenuItem *zoom_out_menu_item_;
123
124 IconCache icons_;
43}; 125};
44 126
45#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 66bce81..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -1,11 +1,15 @@
1#include "tracker_panel.h" 1#include "tracker_panel.h"
2 2
3#include <fmt/core.h>
3#include <wx/dcbuffer.h> 4#include <wx/dcbuffer.h>
4 5
6#include <algorithm>
7
5#include "ap_state.h" 8#include "ap_state.h"
6#include "area_popup.h" 9#include "area_popup.h"
7#include "game_data.h" 10#include "game_data.h"
8#include "global.h" 11#include "global.h"
12#include "ipc_state.h"
9#include "tracker_config.h" 13#include "tracker_config.h"
10#include "tracker_state.h" 14#include "tracker_state.h"
11 15
@@ -39,15 +43,38 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
39 areas_.push_back(area); 43 areas_.push_back(area);
40 } 44 }
41 45
46 Resize();
42 Redraw(); 47 Redraw();
43 48
44 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); 49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
45 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); 50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
46} 51}
47 52
48void TrackerPanel::UpdateIndicators() { 53void TrackerPanel::UpdateIndicators(bool reset) {
49 for (AreaIndicator &area : areas_) { 54 if (reset) {
50 area.popup->UpdateIndicators(); 55 for (AreaIndicator &area : areas_) {
56 const MapArea &map_area = GD_GetMapArea(area.area_id);
57
58 if ((!AP_IsLocationVisible(map_area.classification) ||
59 IsAreaPostgame(area.area_id)) &&
60 !(map_area.hunt &&
61 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
62 !(map_area.has_single_panel &&
63 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) &&
64 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
65 area.active = false;
66 } else {
67 area.active = true;
68 }
69
70 area.popup->ResetIndicators();
71 }
72
73 Resize();
74 } else {
75 for (AreaIndicator &area : areas_) {
76 area.popup->UpdateIndicators();
77 }
51 } 78 }
52 79
53 Redraw(); 80 Redraw();
@@ -55,22 +82,35 @@ void TrackerPanel::UpdateIndicators() {
55 82
56void TrackerPanel::OnPaint(wxPaintEvent &event) { 83void TrackerPanel::OnPaint(wxPaintEvent &event) {
57 if (GetSize() != rendered_.GetSize()) { 84 if (GetSize() != rendered_.GetSize()) {
85 Resize();
58 Redraw(); 86 Redraw();
59 } 87 }
60 88
61 wxBufferedPaintDC dc(this); 89 wxBufferedPaintDC dc(this);
62 dc.DrawBitmap(rendered_, 0, 0); 90 dc.DrawBitmap(rendered_, 0, 0);
63 91
64 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()) {
65 // 1588, 1194 103 // 1588, 1194
66 // 14x14 -> 154x154 104 // 14x14 -> 154x154
67 double intended_x = 105 double intended_x =
68 1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 106 1588.0 + (std::get<0>(*player_position) * (154.0 / 14.0));
69 double intended_y = 107 double intended_y =
70 1194.0 + (std::get<1>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 108 1194.0 + (std::get<1>(*player_position) * (154.0 / 14.0));
71 109
72 int real_x = offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; 110 int real_x =
73 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;
74 114
75 dc.DrawBitmap(scaled_player_, real_x, real_y); 115 dc.DrawBitmap(scaled_player_, real_x, real_y);
76 } 116 }
@@ -92,8 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
92 event.Skip(); 132 event.Skip();
93} 133}
94 134
95void TrackerPanel::Redraw() { 135void TrackerPanel::Resize() {
96 wxSize panel_size = GetSize(); 136 wxSize panel_size = GetClientSize();
97 wxSize image_size = map_image_.GetSize(); 137 wxSize image_size = map_image_.GetSize();
98 138
99 int final_x = 0; 139 int final_x = 0;
@@ -112,7 +152,7 @@ void TrackerPanel::Redraw() {
112 final_x = (panel_size.GetWidth() - final_width) / 2; 152 final_x = (panel_size.GetWidth() - final_width) / 2;
113 } 153 }
114 154
115 rendered_ = wxBitmap( 155 scaled_map_ = wxBitmap(
116 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) 156 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
117 .Size(panel_size, {final_x, final_y}, 0, 0, 0)); 157 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
118 158
@@ -127,27 +167,64 @@ void TrackerPanel::Redraw() {
127 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, 167 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1,
128 player_height > 0 ? player_height : 1)); 168 player_height > 0 ? player_height : 1));
129 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
209void TrackerPanel::Redraw() {
210 rendered_ = scaled_map_;
211
130 wxMemoryDC dc; 212 wxMemoryDC dc;
131 dc.SelectObject(rendered_); 213 dc.SelectObject(rendered_);
132 214
133 int real_area_size =
134 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
135 int actual_border_size = 215 int actual_border_size =
136 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; 216 real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
137 const wxPoint upper_left_triangle[] = { 217 const wxPoint upper_left_triangle[] = {
138 {0, 0}, {0, real_area_size}, {real_area_size, 0}}; 218 {0, 0}, {0, real_area_size_}, {real_area_size_, 0}};
139 const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, 219 const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1},
140 {real_area_size - 1, 0}, 220 {real_area_size_ - 1, 0},
141 {real_area_size, real_area_size}}; 221 {real_area_size_, real_area_size_}};
142 222
143 for (AreaIndicator &area : areas_) { 223 for (AreaIndicator &area : areas_) {
144 const MapArea &map_area = GD_GetMapArea(area.area_id); 224 const MapArea &map_area = GD_GetMapArea(area.area_id);
145 if (!AP_IsLocationVisible(map_area.classification) && 225
146 !(map_area.hunt && GetTrackerConfig().show_hunt_panels)) { 226 if (!area.active) {
147 area.active = false;
148 continue; 227 continue;
149 } else {
150 area.active = true;
151 } 228 }
152 229
153 bool has_reachable_unchecked = false; 230 bool has_reachable_unchecked = false;
@@ -156,10 +233,15 @@ void TrackerPanel::Redraw() {
156 bool has_unchecked = false; 233 bool has_unchecked = false;
157 if (IsLocationWinCondition(section)) { 234 if (IsLocationWinCondition(section)) {
158 has_unchecked = !AP_HasReachedGoal(); 235 has_unchecked = !AP_HasReachedGoal();
159 } else if (AP_IsLocationVisible(section.classification)) { 236 } else if (AP_IsLocationVisible(section.classification) &&
237 !IsLocationPostgame(section.ap_location_id)) {
160 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); 238 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
161 } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { 239 } else if ((section.hunt && GetTrackerConfig().visible_panels ==
162 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);
163 } 245 }
164 246
165 if (has_unchecked) { 247 if (has_unchecked) {
@@ -171,10 +253,26 @@ void TrackerPanel::Redraw() {
171 } 253 }
172 } 254 }
173 255
174 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 256 if (AP_IsPaintingShuffle()) {
175 final_width / image_size.GetWidth(); 257 for (int painting_id : map_area.paintings) {
176 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * 258 if (IsPaintingPostgame(painting_id)) {
177 final_width / image_size.GetWidth(); 259 continue;
260 }
261
262 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
263 bool reachable = IsPaintingReachable(painting_id);
264 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) {
265 if (reachable) {
266 has_reachable_unchecked = true;
267 } else {
268 has_unreachable_unchecked = true;
269 }
270 }
271 }
272 }
273
274 int real_area_x = area.real_x1;
275 int real_area_y = area.real_y1;
178 276
179 if (has_reachable_unchecked && has_unreachable_unchecked && 277 if (has_reachable_unchecked && has_unreachable_unchecked &&
180 GetTrackerConfig().hybrid_areas) { 278 GetTrackerConfig().hybrid_areas) {
@@ -188,7 +286,7 @@ void TrackerPanel::Redraw() {
188 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
189 dc.SetBrush(*wxTRANSPARENT_BRUSH); 287 dc.SetBrush(*wxTRANSPARENT_BRUSH);
190 dc.DrawRectangle({real_area_x, real_area_y}, 288 dc.DrawRectangle({real_area_x, real_area_y},
191 {real_area_size, real_area_size}); 289 {real_area_size_, real_area_size_});
192 290
193 } else { 291 } else {
194 const wxBrush *brush_color = wxGREY_BRUSH; 292 const wxBrush *brush_color = wxGREY_BRUSH;
@@ -203,30 +301,7 @@ void TrackerPanel::Redraw() {
203 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 301 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
204 dc.SetBrush(*brush_color); 302 dc.SetBrush(*brush_color);
205 dc.DrawRectangle({real_area_x, real_area_y}, 303 dc.DrawRectangle({real_area_x, real_area_y},
206 {real_area_size, real_area_size}); 304 {real_area_size_, real_area_size_});
207 } 305 }
208
209 area.real_x1 = real_area_x;
210 area.real_x2 = real_area_x + real_area_size;
211 area.real_y1 = real_area_y;
212 area.real_y2 = real_area_y + real_area_size;
213
214 int popup_x =
215 final_x + map_area.map_x * final_width / image_size.GetWidth();
216 int popup_y =
217 final_y + map_area.map_y * final_width / image_size.GetWidth();
218
219 area.popup->SetClientSize(
220 area.popup->GetVirtualSize().GetWidth(),
221 std::min(panel_size.GetHeight(),
222 area.popup->GetVirtualSize().GetHeight()));
223
224 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
225 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
226 }
227 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
228 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
229 }
230 area.popup->SetPosition({popup_x, popup_y});
231 } 306 }
232} 307}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index 06ec7a0..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h
@@ -7,13 +7,17 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <optional>
11#include <set>
12#include <string>
13
10class AreaPopup; 14class AreaPopup;
11 15
12class TrackerPanel : public wxPanel { 16class TrackerPanel : public wxPanel {
13 public: 17 public:
14 TrackerPanel(wxWindow *parent); 18 TrackerPanel(wxWindow *parent);
15 19
16 void UpdateIndicators(); 20 void UpdateIndicators(bool reset);
17 21
18 private: 22 private:
19 struct AreaIndicator { 23 struct AreaIndicator {
@@ -29,10 +33,12 @@ class TrackerPanel : public wxPanel {
29 void OnPaint(wxPaintEvent &event); 33 void OnPaint(wxPaintEvent &event);
30 void OnMouseMove(wxMouseEvent &event); 34 void OnMouseMove(wxMouseEvent &event);
31 35
36 void Resize();
32 void Redraw(); 37 void Redraw();
33 38
34 wxImage map_image_; 39 wxImage map_image_;
35 wxImage player_image_; 40 wxImage player_image_;
41 wxBitmap scaled_map_;
36 wxBitmap rendered_; 42 wxBitmap rendered_;
37 wxBitmap scaled_player_; 43 wxBitmap scaled_player_;
38 44
@@ -40,6 +46,7 @@ class TrackerPanel : public wxPanel {
40 int offset_y_ = 0; 46 int offset_y_ = 0;
41 double scale_x_ = 0; 47 double scale_x_ = 0;
42 double scale_y_ = 0; 48 double scale_y_ = 0;
49 int real_area_size_ = 0;
43 50
44 std::vector<AreaIndicator> areas_; 51 std::vector<AreaIndicator> areas_;
45}; 52};
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 5588c7f..674f68a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -1,5 +1,8 @@
1#include "tracker_state.h" 1#include "tracker_state.h"
2 2
3#include <fmt/core.h>
4#include <hkutil/string.h>
5
3#include <list> 6#include <list>
4#include <map> 7#include <map>
5#include <mutex> 8#include <mutex>
@@ -9,13 +12,172 @@
9 12
10#include "ap_state.h" 13#include "ap_state.h"
11#include "game_data.h" 14#include "game_data.h"
15#include "global.h"
16#include "logger.h"
12 17
13namespace { 18namespace {
14 19
20struct Requirements {
21 bool disabled = false;
22
23 std::set<int> doors; // non-grouped, handles progressive
24 std::set<int> panel_doors; // non-grouped, handles progressive
25 std::set<int> items; // all other items
26 std::set<int> rooms; // maybe
27 bool mastery = false; // maybe
28 bool panel_hunt = false; // maybe
29 bool postgame = false;
30
31 void Merge(const Requirements& rhs) {
32 if (rhs.disabled) {
33 return;
34 }
35
36 for (int id : rhs.doors) {
37 doors.insert(id);
38 }
39 for (int id : rhs.panel_doors) {
40 panel_doors.insert(id);
41 }
42 for (int id : rhs.items) {
43 items.insert(id);
44 }
45 for (int id : rhs.rooms) {
46 rooms.insert(id);
47 }
48 mastery = mastery || rhs.mastery;
49 panel_hunt = panel_hunt || rhs.panel_hunt;
50 postgame = postgame || rhs.postgame;
51 }
52};
53
54class RequirementCalculator {
55 public:
56 void Reset() {
57 doors_.clear();
58 panels_.clear();
59 }
60
61 const Requirements& GetDoor(int door_id) {
62 if (!doors_.count(door_id)) {
63 Requirements requirements;
64 const Door& door_obj = GD_GetDoor(door_id);
65
66 if (door_obj.type == DoorType::kSunPainting) {
67 if (!AP_IsPilgrimageEnabled()) {
68 requirements.items.insert(door_obj.ap_item_id);
69 } else {
70 requirements.disabled = true;
71 }
72 } else if (door_obj.type == DoorType::kSunwarp) {
73 switch (AP_GetSunwarpAccess()) {
74 case kSUNWARP_ACCESS_NORMAL:
75 // Do nothing.
76 break;
77 case kSUNWARP_ACCESS_DISABLED:
78 requirements.disabled = true;
79 break;
80 case kSUNWARP_ACCESS_UNLOCK:
81 requirements.items.insert(door_obj.group_ap_item_id);
82 break;
83 case kSUNWARP_ACCESS_INDIVIDUAL:
84 case kSUNWARP_ACCESS_PROGRESSIVE:
85 requirements.doors.insert(door_obj.id);
86 break;
87 }
88 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) {
89 for (int panel_id : door_obj.panels) {
90 const Requirements& panel_reqs = GetPanel(panel_id);
91 requirements.Merge(panel_reqs);
92 }
93 } else if (AP_AreDoorsGrouped() && !door_obj.group_name.empty()) {
94 requirements.items.insert(door_obj.group_ap_item_id);
95 } else {
96 requirements.doors.insert(door_obj.id);
97 }
98
99 doors_[door_id] = requirements;
100 }
101
102 return doors_[door_id];
103 }
104
105 const Requirements& GetPanel(int panel_id) {
106 if (!panels_.count(panel_id)) {
107 Requirements requirements;
108 const Panel& panel_obj = GD_GetPanel(panel_id);
109
110 requirements.rooms.insert(panel_obj.room);
111
112 if (panel_obj.name == "THE MASTER") {
113 requirements.mastery = true;
114 }
115
116 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") &&
117 AP_GetLevel2Requirement() > 1) {
118 requirements.panel_hunt = true;
119 }
120
121 for (int room_id : panel_obj.required_rooms) {
122 requirements.rooms.insert(room_id);
123 }
124
125 for (int door_id : panel_obj.required_doors) {
126 const Requirements& door_reqs = GetDoor(door_id);
127 requirements.Merge(door_reqs);
128 }
129
130 for (int panel_id : panel_obj.required_panels) {
131 const Requirements& panel_reqs = GetPanel(panel_id);
132 requirements.Merge(panel_reqs);
133 }
134
135 if (AP_IsColorShuffle()) {
136 for (LingoColor color : panel_obj.colors) {
137 requirements.items.insert(GD_GetItemIdForColor(color));
138 }
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 }
155
156 panels_[panel_id] = requirements;
157 }
158
159 return panels_[panel_id];
160 }
161
162 private:
163 std::map<int, Requirements> doors_;
164 std::map<int, Requirements> panels_;
165};
166
15struct TrackerState { 167struct TrackerState {
16 std::map<int, bool> reachability; 168 std::map<int, bool> reachability;
17 std::set<int> reachable_doors; 169 std::set<int> reachable_doors;
170 std::set<int> solveable_panels;
171 std::set<int> reachable_paintings;
18 std::mutex reachability_mutex; 172 std::mutex reachability_mutex;
173 RequirementCalculator requirements;
174 std::map<int, std::map<std::string, bool>> door_reports;
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;
19}; 181};
20 182
21enum Decision { kYes, kNo, kMaybe }; 183enum Decision { kYes, kNo, kMaybe };
@@ -30,6 +192,11 @@ class StateCalculator;
30struct StateCalculatorOptions { 192struct StateCalculatorOptions {
31 int start; 193 int start;
32 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
33 StateCalculator* parent = nullptr; 200 StateCalculator* parent = nullptr;
34}; 201};
35 202
@@ -40,15 +207,33 @@ class StateCalculator {
40 explicit StateCalculator(StateCalculatorOptions options) 207 explicit StateCalculator(StateCalculatorOptions options)
41 : options_(options) {} 208 : options_(options) {}
42 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
43 void Calculate() { 220 void Calculate() {
221 painting_mapping_ = AP_GetPaintingMapping();
222 checked_paintings_ = AP_GetCheckedPaintings();
223 sunwarp_mapping_ = AP_GetSunwarpMapping();
224
44 std::list<int> panel_boundary; 225 std::list<int> panel_boundary;
226 std::list<int> painting_boundary;
45 std::list<Exit> flood_boundary; 227 std::list<Exit> flood_boundary;
46 flood_boundary.push_back({.destination_room = options_.start}); 228 flood_boundary.push_back(
229 {.source_room = -1, .destination_room = options_.start});
47 230
48 bool reachable_changed = true; 231 bool reachable_changed = true;
49 while (reachable_changed) { 232 while (reachable_changed) {
50 reachable_changed = false; 233 reachable_changed = false;
51 234
235 std::list<Exit> new_boundary;
236
52 std::list<int> new_panel_boundary; 237 std::list<int> new_panel_boundary;
53 for (int panel_id : panel_boundary) { 238 for (int panel_id : panel_boundary) {
54 if (solveable_panels_.count(panel_id)) { 239 if (solveable_panels_.count(panel_id)) {
@@ -64,7 +249,36 @@ class StateCalculator {
64 } 249 }
65 } 250 }
66 251
67 std::list<Exit> new_boundary; 252 std::list<int> new_painting_boundary;
253 for (int painting_id : painting_boundary) {
254 if (reachable_paintings_.count(painting_id)) {
255 continue;
256 }
257
258 Decision painting_reachable = IsPaintingReachable(painting_id);
259 if (painting_reachable == kYes) {
260 reachable_paintings_.insert(painting_id);
261 reachable_changed = true;
262
263 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
264 if (painting_mapping_.count(cur_painting.internal_id) &&
265 (checked_paintings_.count(cur_painting.internal_id) ||
266 options_.postgame_detection)) {
267 Exit painting_exit;
268 PaintingExit target_painting =
269 GD_GetPaintingExit(GD_GetPaintingByName(
270 painting_mapping_.at(cur_painting.internal_id)));
271 painting_exit.source_room = cur_painting.room;
272 painting_exit.destination_room = target_painting.room;
273 painting_exit.type = EntranceType::kPainting;
274
275 new_boundary.push_back(painting_exit);
276 }
277 } else if (painting_reachable == kMaybe) {
278 new_painting_boundary.push_back(painting_id);
279 }
280 }
281
68 for (const Exit& room_exit : flood_boundary) { 282 for (const Exit& room_exit : flood_boundary) {
69 if (reachable_rooms_.count(room_exit.destination_room)) { 283 if (reachable_rooms_.count(room_exit.destination_room)) {
70 continue; 284 continue;
@@ -83,6 +297,12 @@ class StateCalculator {
83 reachable_rooms_.insert(room_exit.destination_room); 297 reachable_rooms_.insert(room_exit.destination_room);
84 reachable_changed = true; 298 reachable_changed = true;
85 299
300#ifndef NDEBUG
301 std::list<int> room_path = paths_[room_exit.source_room];
302 room_path.push_back(room_exit.destination_room);
303 paths_[room_exit.destination_room] = room_path;
304#endif
305
86 const Room& room_obj = GD_GetRoom(room_exit.destination_room); 306 const Room& room_obj = GD_GetRoom(room_exit.destination_room);
87 for (const Exit& out_edge : room_obj.exits) { 307 for (const Exit& out_edge : room_obj.exits) {
88 if (out_edge.type == EntranceType::kPainting && 308 if (out_edge.type == EntranceType::kPainting &&
@@ -99,52 +319,56 @@ class StateCalculator {
99 } 319 }
100 320
101 if (AP_IsPaintingShuffle()) { 321 if (AP_IsPaintingShuffle()) {
102 for (const PaintingExit& out_edge : room_obj.paintings) { 322 for (int out_edge : room_obj.paintings) {
103 if (AP_GetPaintingMapping().count(out_edge.id)) { 323 new_painting_boundary.push_back(out_edge);
104 Exit painting_exit;
105 painting_exit.destination_room = GD_GetRoomForPainting(
106 AP_GetPaintingMapping().at(out_edge.id));
107 painting_exit.door = out_edge.door;
108
109 new_boundary.push_back(painting_exit);
110 }
111 } 324 }
112 } 325 }
113 326
114 if (AP_IsSunwarpShuffle()) { 327 if (AP_IsSunwarpShuffle()) {
115 for (int index : room_obj.sunwarps) { 328 for (int index : room_obj.sunwarps) {
116 if (AP_GetSunwarpMapping().count(index)) { 329 if (sunwarp_mapping_.count(index)) {
117 const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); 330 const SunwarpMapping& sm = sunwarp_mapping_.at(index);
118 331
119 Exit sunwarp_exit; 332 new_boundary.push_back(
120 sunwarp_exit.destination_room = 333 {.source_room = room_exit.destination_room,
121 GD_GetRoomForSunwarp(sm.exit_index); 334 .destination_room = GD_GetRoomForSunwarp(sm.exit_index),
122 sunwarp_exit.door = GD_GetSunwarpDoors().at(sm.dots - 1); 335 .door = GD_GetSunwarpDoors().at(sm.dots - 1),
123 336 .type = EntranceType::kSunwarp});
124 new_boundary.push_back(sunwarp_exit);
125 } 337 }
126 } 338 }
127 } 339 }
128 340
129 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { 341 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") {
130 new_boundary.push_back( 342 new_boundary.push_back(
131 {.destination_room = GD_GetRoomByName("Outside The Undeterred"), 343 {.source_room = room_exit.destination_room,
132 .type = EntranceType::kPainting}); 344 .destination_room = GD_GetRoomByName("Color Hallways"),
345 .type = EntranceType::kStaticPainting});
133 } 346 }
134 347
135 if (AP_IsPilgrimageEnabled()) { 348 if (AP_IsPilgrimageEnabled()) {
136 if (room_obj.name == "Hub Room") { 349 int pilgrimage_start_id = GD_GetRoomByName("Hub Room");
350 if (AP_IsSunwarpShuffle()) {
351 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
352 if (mapping.dots == 1) {
353 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index);
354 }
355 }
356 }
357
358 if (room_exit.destination_room == pilgrimage_start_id) {
137 new_boundary.push_back( 359 new_boundary.push_back(
138 {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), 360 {.source_room = room_exit.destination_room,
361 .destination_room = GD_GetRoomByName("Pilgrim Antechamber"),
139 .type = EntranceType::kPilgrimage}); 362 .type = EntranceType::kPilgrimage});
140 } 363 }
141 } else { 364 } else {
142 if (room_obj.name == "Starting Room") { 365 if (room_obj.name == "Starting Room") {
143 new_boundary.push_back( 366 new_boundary.push_back(
144 {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), 367 {.source_room = room_exit.destination_room,
368 .destination_room = GD_GetRoomByName("Pilgrim Antechamber"),
145 .door = 369 .door =
146 GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"), 370 GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"),
147 .type = EntranceType::kPainting}); 371 .type = EntranceType::kStaticPainting});
148 } 372 }
149 } 373 }
150 374
@@ -156,11 +380,17 @@ class StateCalculator {
156 380
157 flood_boundary = new_boundary; 381 flood_boundary = new_boundary;
158 panel_boundary = new_panel_boundary; 382 panel_boundary = new_panel_boundary;
383 painting_boundary = new_painting_boundary;
159 } 384 }
160 385
161 // Now that we know the full reachable area, let's make sure all doors are evaluated. 386 // Now that we know the full reachable area, let's make sure all doors are
387 // evaluated.
162 for (const Door& door : GD_GetDoors()) { 388 for (const Door& door : GD_GetDoors()) {
163 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]);
164 } 394 }
165 } 395 }
166 396
@@ -172,8 +402,32 @@ class StateCalculator {
172 402
173 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } 403 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; }
174 404
405 const std::set<int>& GetReachablePaintings() const {
406 return reachable_paintings_;
407 }
408
409 const std::map<int, std::map<std::string, bool>>& GetDoorReports() const {
410 return door_report_;
411 }
412
413 bool IsPilgrimageDoable() const { return pilgrimage_doable_; }
414
415 std::string GetPathToRoom(int room_id) const {
416 if (!paths_.count(room_id)) {
417 return "";
418 }
419
420 const std::list<int>& path = paths_.at(room_id);
421 std::vector<std::string> room_names;
422 for (int room_id : path) {
423 room_names.push_back(GD_GetRoom(room_id).name);
424 }
425 return hatkirby::implode(room_names, " -> ");
426 }
427
175 private: 428 private:
176 Decision IsNonGroupedDoorReachable(const Door& door_obj) { 429 template <typename T>
430 Decision IsNonGroupedDoorReachable(const T& door_obj) {
177 bool has_item = AP_HasItem(door_obj.ap_item_id); 431 bool has_item = AP_HasItem(door_obj.ap_item_id);
178 432
179 if (!has_item) { 433 if (!has_item) {
@@ -188,68 +442,71 @@ class StateCalculator {
188 return has_item ? kYes : kNo; 442 return has_item ? kYes : kNo;
189 } 443 }
190 444
191 Decision IsDoorReachable_Helper(int door_id) { 445 Decision AreRequirementsSatisfied(
192 const Door& door_obj = GD_GetDoor(door_id); 446 const Requirements& reqs, std::map<std::string, bool>* report = nullptr) {
193 447 if (reqs.disabled) {
194 if (!AP_IsPilgrimageEnabled() && door_obj.type == DoorType::kSunPainting) { 448 return kNo;
195 return AP_HasItem(door_obj.ap_item_id) ? kYes : kNo; 449 }
196 } else if (door_obj.type == DoorType::kSunwarp) { 450
197 switch (AP_GetSunwarpAccess()) { 451 if (reqs.postgame && options_.postgame_detection) {
198 case kSUNWARP_ACCESS_NORMAL: 452 return kNo;
199 return kYes; 453 }
200 case kSUNWARP_ACCESS_DISABLED: 454
201 return kNo; 455 Decision final_decision = kYes;
202 case kSUNWARP_ACCESS_UNLOCK: 456
203 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo; 457 if (!options_.postgame_detection) {
204 case kSUNWARP_ACCESS_INDIVIDUAL: 458 for (int door_id : reqs.doors) {
205 case kSUNWARP_ACCESS_PROGRESSIVE: 459 const Door& door_obj = GD_GetDoor(door_id);
206 return IsNonGroupedDoorReachable(door_obj); 460 Decision decision = IsNonGroupedDoorReachable(door_obj);
207 } 461
208 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) { 462 if (report) {
209 if (!reachable_rooms_.count(door_obj.room)) { 463 (*report)[door_obj.item_name] = (decision == kYes);
210 return kMaybe; 464 }
211 } 465
212 466 if (decision != kYes) {
213 for (int panel_id : door_obj.panels) { 467 final_decision = decision;
214 if (!solveable_panels_.count(panel_id)) {
215 return kMaybe;
216 } 468 }
217 } 469 }
218 470
219 return kYes; 471 for (int panel_door_id : reqs.panel_doors) {
220 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS && 472 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id);
221 !door_obj.group_name.empty()) { 473 Decision decision = IsNonGroupedDoorReachable(panel_door_obj);
222 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo;
223 } else {
224 return IsNonGroupedDoorReachable(door_obj);
225 }
226 }
227 474
228 Decision IsDoorReachable(int door_id) { 475 if (report) {
229 if (options_.parent) { 476 (*report)[panel_door_obj.item_name] = (decision == kYes);
230 return options_.parent->IsDoorReachable(door_id); 477 }
231 }
232 478
233 if (door_decisions_.count(door_id)) { 479 if (decision != kYes) {
234 return door_decisions_.at(door_id); 480 final_decision = decision;
235 } 481 }
482 }
236 483
237 Decision result = IsDoorReachable_Helper(door_id); 484 for (int item_id : reqs.items) {
238 if (result != kMaybe) { 485 bool has_item = AP_HasItem(item_id);
239 door_decisions_[door_id] = result; 486 if (report) {
487 (*report)[GD_GetItemName(item_id)] = has_item;
488 }
489
490 if (!has_item) {
491 final_decision = kNo;
492 }
493 }
240 } 494 }
241 495
242 return result; 496 for (int room_id : reqs.rooms) {
243 } 497 bool reachable = reachable_rooms_.count(room_id);
244 498
245 Decision IsPanelReachable(int panel_id) { 499 if (report) {
246 const Panel& panel_obj = GD_GetPanel(panel_id); 500 std::string report_name = "Reach \"" + GD_GetRoom(room_id).name + "\"";
501 (*report)[report_name] = reachable;
502 }
247 503
248 if (!reachable_rooms_.count(panel_obj.room)) { 504 if (!reachable && final_decision != kNo) {
249 return kMaybe; 505 final_decision = kMaybe;
506 }
250 } 507 }
251 508
252 if (panel_obj.name == "THE MASTER") { 509 if (reqs.mastery) {
253 int achievements_accessible = 0; 510 int achievements_accessible = 0;
254 511
255 for (int achieve_id : GD_GetAchievementPanels()) { 512 for (int achieve_id : GD_GetAchievementPanels()) {
@@ -262,12 +519,18 @@ class StateCalculator {
262 } 519 }
263 } 520 }
264 521
265 return (achievements_accessible >= AP_GetMasteryRequirement()) ? kYes 522 bool can_mastery =
266 : kMaybe; 523 (achievements_accessible >= AP_GetMasteryRequirement());
524 if (report) {
525 (*report)["Mastery"] = can_mastery;
526 }
527
528 if (!can_mastery && final_decision != kNo) {
529 final_decision = kMaybe;
530 }
267 } 531 }
268 532
269 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && 533 if (reqs.panel_hunt) {
270 AP_GetLevel2Requirement() > 1) {
271 int counting_panels_accessible = 0; 534 int counting_panels_accessible = 0;
272 535
273 for (int solved_panel_id : solveable_panels_) { 536 for (int solved_panel_id : solveable_panels_) {
@@ -278,41 +541,51 @@ class StateCalculator {
278 } 541 }
279 } 542 }
280 543
281 return (counting_panels_accessible >= AP_GetLevel2Requirement() - 1) 544 bool can_level2 =
282 ? kYes 545 (counting_panels_accessible >= AP_GetLevel2Requirement() - 1);
283 : kMaybe; 546 if (report) {
284 } 547 std::string report_name =
548 std::to_string(AP_GetLevel2Requirement()) + " Panels";
549 (*report)[report_name] = can_level2;
550 }
285 551
286 for (int room_id : panel_obj.required_rooms) { 552 if (!can_level2 && final_decision != kNo) {
287 if (!reachable_rooms_.count(room_id)) { 553 final_decision = kMaybe;
288 return kMaybe;
289 } 554 }
290 } 555 }
291 556
292 for (int door_id : panel_obj.required_doors) { 557 return final_decision;
293 Decision door_reachable = IsDoorReachable(door_id); 558 }
294 if (door_reachable == kNo) { 559
295 const Door& door_obj = GD_GetDoor(door_id); 560 Decision IsDoorReachable_Helper(int door_id) {
296 return (door_obj.is_event || AP_GetDoorShuffleMode() == kNO_DOORS) 561 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id));
297 ? kMaybe 562 }
298 : kNo; 563
299 } else if (door_reachable == kMaybe) { 564 Decision IsDoorReachable(int door_id) {
300 return kMaybe; 565 if (options_.parent) {
301 } 566 return options_.parent->IsDoorReachable(door_id);
302 } 567 }
303 568
304 for (int panel_id : panel_obj.required_panels) { 569 if (door_decisions_.count(door_id)) {
305 if (!solveable_panels_.count(panel_id)) { 570 return door_decisions_.at(door_id);
306 return kMaybe;
307 }
308 } 571 }
309 572
310 if (AP_IsColorShuffle()) { 573 Decision result = IsDoorReachable_Helper(door_id);
311 for (LingoColor color : panel_obj.colors) { 574 if (result != kMaybe) {
312 if (!AP_HasItem(GD_GetItemIdForColor(color))) { 575 door_decisions_[door_id] = result;
313 return kNo; 576 }
314 } 577
315 } 578 return result;
579 }
580
581 Decision IsPanelReachable(int panel_id) {
582 return AreRequirementsSatisfied(GetState().requirements.GetPanel(panel_id));
583 }
584
585 Decision IsPaintingReachable(int painting_id) {
586 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
587 if (painting.door) {
588 return IsDoorReachable(*painting.door);
316 } 589 }
317 590
318 return kYes; 591 return kYes;
@@ -337,7 +610,7 @@ class StateCalculator {
337 if (AP_IsSunwarpShuffle()) { 610 if (AP_IsSunwarpShuffle()) {
338 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); 611 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5);
339 612
340 for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { 613 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
341 if (mapping.dots > 1) { 614 if (mapping.dots > 1) {
342 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; 615 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index;
343 } 616 }
@@ -363,6 +636,8 @@ class StateCalculator {
363 } 636 }
364 } 637 }
365 638
639 pilgrimage_doable_ = true;
640
366 return kYes; 641 return kYes;
367 } 642 }
368 643
@@ -375,7 +650,8 @@ class StateCalculator {
375 !AP_DoesPilgrimageAllowRoofAccess()) { 650 !AP_DoesPilgrimageAllowRoofAccess()) {
376 return kNo; 651 return kNo;
377 } 652 }
378 if (room_exit.type == EntranceType::kPainting && 653 if ((room_exit.type == EntranceType::kPainting ||
654 room_exit.type == EntranceType::kStaticPainting) &&
379 !AP_DoesPilgrimageAllowPaintings()) { 655 !AP_DoesPilgrimageAllowPaintings()) {
380 return kNo; 656 return kNo;
381 } 657 }
@@ -401,16 +677,99 @@ class StateCalculator {
401 std::set<int> reachable_rooms_; 677 std::set<int> reachable_rooms_;
402 std::map<int, Decision> door_decisions_; 678 std::map<int, Decision> door_decisions_;
403 std::set<int> solveable_panels_; 679 std::set<int> solveable_panels_;
680 std::set<int> reachable_paintings_;
681 std::map<int, std::map<std::string, bool>> door_report_;
682 bool pilgrimage_doable_ = false;
683
684 std::map<int, std::list<int>> paths_;
685
686 std::map<std::string, std::string> painting_mapping_;
687 std::set<std::string> checked_paintings_;
688 std::map<int, SunwarpMapping> sunwarp_mapping_;
404}; 689};
405 690
406} // namespace 691} // namespace
407 692
693void ResetReachabilityRequirements() {
694 TrackerLog("Resetting tracker state...");
695
696 std::lock_guard reachability_guard(GetState().reachability_mutex);
697 GetState().requirements.Reset();
698 GetState().reachable_doors.clear();
699 GetState().solveable_panels.clear();
700
701 if (AP_IsPostgameShuffle()) {
702 GetState().non_postgame_areas.clear();
703 GetState().non_postgame_locations.clear();
704 GetState().non_postgame_paintings.clear();
705 } else {
706 StateCalculator postgame_calculator(
707 {.start = GD_GetRoomByName("Menu"), .postgame_detection = true});
708 postgame_calculator.Calculate();
709
710 std::set<int>& non_postgame_areas = GetState().non_postgame_areas;
711 non_postgame_areas.clear();
712
713 std::set<int>& non_postgame_locations = GetState().non_postgame_locations;
714 non_postgame_locations.clear();
715
716 const std::set<int>& reachable_rooms =
717 postgame_calculator.GetReachableRooms();
718 const std::set<int>& solveable_panels =
719 postgame_calculator.GetSolveablePanels();
720
721 for (const MapArea& map_area : GD_GetMapAreas()) {
722 bool area_reachable = false;
723
724 for (const Location& location_section : map_area.locations) {
725 bool reachable = reachable_rooms.count(location_section.room);
726 if (reachable) {
727 for (int panel_id : location_section.panels) {
728 reachable &= (solveable_panels.count(panel_id) == 1);
729 }
730 }
731
732 if (!reachable && IsLocationWinCondition(location_section)) {
733 reachable = true;
734 }
735
736 if (reachable) {
737 non_postgame_locations.insert(location_section.ap_location_id);
738 area_reachable = true;
739 }
740 }
741
742 for (int painting_id : map_area.paintings) {
743 if (postgame_calculator.GetReachablePaintings().count(painting_id)) {
744 area_reachable = true;
745 }
746 }
747
748 if (area_reachable) {
749 non_postgame_areas.insert(map_area.id);
750 }
751 }
752
753 GetState().non_postgame_paintings =
754 postgame_calculator.GetReachablePaintings();
755 }
756}
757
408void RecalculateReachability() { 758void RecalculateReachability() {
759 TrackerLog("Calculating reachability...");
760
761 std::lock_guard reachability_guard(GetState().reachability_mutex);
762
763 // Receiving items and checking paintings should never remove access to doors
764 // or panels, so we can preload any doors and panels we already know are
765 // accessible from previous runs, in order to reduce the work.
409 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 766 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
767 state_calculator.PreloadDoors(GetState().reachable_doors);
768 state_calculator.PreloadPanels(GetState().solveable_panels);
410 state_calculator.Calculate(); 769 state_calculator.Calculate();
411 770
412 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); 771 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms();
413 const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); 772 std::set<int> solveable_panels = state_calculator.GetSolveablePanels();
414 773
415 std::map<int, bool> new_reachability; 774 std::map<int, bool> new_reachability;
416 for (const MapArea& map_area : GD_GetMapAreas()) { 775 for (const MapArea& map_area : GD_GetMapAreas()) {
@@ -435,11 +794,16 @@ void RecalculateReachability() {
435 } 794 }
436 } 795 }
437 796
438 { 797 std::set<int> reachable_paintings = state_calculator.GetReachablePaintings();
439 std::lock_guard reachability_guard(GetState().reachability_mutex); 798 std::map<int, std::map<std::string, bool>> door_reports =
440 std::swap(GetState().reachability, new_reachability); 799 state_calculator.GetDoorReports();
441 std::swap(GetState().reachable_doors, new_reachable_doors); 800
442 } 801 std::swap(GetState().reachability, new_reachability);
802 std::swap(GetState().reachable_doors, new_reachable_doors);
803 std::swap(GetState().solveable_panels, solveable_panels);
804 std::swap(GetState().reachable_paintings, reachable_paintings);
805 std::swap(GetState().door_reports, door_reports);
806 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable();
443} 807}
444 808
445bool IsLocationReachable(int location_id) { 809bool IsLocationReachable(int location_id) {
@@ -457,3 +821,51 @@ bool IsDoorOpen(int door_id) {
457 821
458 return GetState().reachable_doors.count(door_id); 822 return GetState().reachable_doors.count(door_id);
459} 823}
824
825bool IsPaintingReachable(int painting_id) {
826 std::lock_guard reachability_guard(GetState().reachability_mutex);
827
828 return GetState().reachable_paintings.count(painting_id);
829}
830
831const std::map<std::string, bool>& GetDoorRequirements(int door_id) {
832 std::lock_guard reachability_guard(GetState().reachability_mutex);
833
834 return GetState().door_reports[door_id];
835}
836
837bool IsPilgrimageDoable() {
838 std::lock_guard reachability_guard(GetState().reachability_mutex);
839
840 return GetState().pilgrimage_doable;
841}
842
843bool IsAreaPostgame(int area_id) {
844 std::lock_guard reachability_guard(GetState().reachability_mutex);
845
846 if (GetState().non_postgame_areas.empty()) {
847 return false;
848 } else {
849 return !GetState().non_postgame_areas.count(area_id);
850 }
851}
852
853bool IsLocationPostgame(int location_id) {
854 std::lock_guard reachability_guard(GetState().reachability_mutex);
855
856 if (GetState().non_postgame_locations.empty()) {
857 return false;
858 } else {
859 return !GetState().non_postgame_locations.count(location_id);
860 }
861}
862
863bool IsPaintingPostgame(int painting_id) {
864 std::lock_guard reachability_guard(GetState().reachability_mutex);
865
866 if (GetState().non_postgame_paintings.empty()) {
867 return false;
868 } else {
869 return !GetState().non_postgame_paintings.count(painting_id);
870 }
871}
diff --git a/src/tracker_state.h b/src/tracker_state.h index 119b3b5..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h
@@ -1,10 +1,27 @@
1#ifndef TRACKER_STATE_H_8639BC90 1#ifndef TRACKER_STATE_H_8639BC90
2#define TRACKER_STATE_H_8639BC90 2#define TRACKER_STATE_H_8639BC90
3 3
4#include <map>
5#include <string>
6
7void ResetReachabilityRequirements();
8
4void RecalculateReachability(); 9void RecalculateReachability();
5 10
6bool IsLocationReachable(int location_id); 11bool IsLocationReachable(int location_id);
7 12
8bool IsDoorOpen(int door_id); 13bool IsDoorOpen(int door_id);
9 14
15bool IsPaintingReachable(int painting_id);
16
17const std::map<std::string, bool>& GetDoorRequirements(int door_id);
18
19bool IsPilgrimageDoable();
20
21bool IsAreaPostgame(int area_id);
22
23bool IsLocationPostgame(int location_id);
24
25bool IsPaintingPostgame(int painting_id);
26
10#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
22constexpr const char* kVersionFileUrl =
23 "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
24constexpr const char* kChangelogUrl =
25 "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
26
27namespace {
28
29std::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
47Updater::Updater(wxFrame* parent) : parent_(parent) {
48 Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
49}
50
51void 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
58void 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
81void 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
91void 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
163void 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
285bool 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
14class wxWebRequest;
15class wxWebRequestEvent;
16
17class 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
3std::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 36bd8c1..3439fda 100644 --- a/src/version.h +++ b/src/version.h
@@ -1,9 +1,10 @@
1#ifndef VERSION_H_C757E53C 1#ifndef VERSION_H_C757E53C
2#define VERSION_H_C757E53C 2#define VERSION_H_C757E53C
3 3
4#include <sstream>
5#include <regex> 4#include <regex>
6 5
6#include <fmt/core.h>
7
7struct Version { 8struct Version {
8 int major = 0; 9 int major = 0;
9 int minor = 0; 10 int minor = 0;
@@ -24,9 +25,7 @@ struct Version {
24 } 25 }
25 26
26 std::string ToString() const { 27 std::string ToString() const {
27 std::ostringstream output; 28 return fmt::format("v{}.{}.{}", major, minor, revision);
28 output << "v" << major << "." << minor << "." << revision;
29 return output.str();
30 } 29 }
31 30
32 bool operator<(const Version& rhs) const { 31 bool operator<(const Version& rhs) const {
@@ -37,6 +36,6 @@ struct Version {
37 } 36 }
38}; 37};
39 38
40constexpr const Version kTrackerVersion = Version(0, 9, 0); 39constexpr const Version kTrackerVersion = Version(2, 0, 2);
41 40
42#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 925212a..581a507 100644 --- a/vcpkg.json +++ b/vcpkg.json
@@ -1,8 +1,8 @@
1{ 1{
2 "dependencies": [ 2 "dependencies": [
3 "websocketpp",
4 "wxwidgets", 3 "wxwidgets",
5 "openssl", 4 "openssl",
6 "yaml-cpp" 5 "yaml-cpp",
6 "fmt"
7 ] 7 ]
8} 8}
diff --git a/vendor/apclientpp b/vendor/apclientpp
Subproject 8e54ae1bbef99fdbf9f674c79df416e38ca3cf6 Subproject 6866205eac2c1d1cbb7e64453d8d7150ffed858
diff --git a/vendor/asio b/vendor/asio
Subproject c465349fa5cd91a64bb369f5131ceacab2c0c1c Subproject 12e0ce9e0500bf0f247dbd1ae89427265645607
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
diff --git a/vendor/wswrap b/vendor/wswrap
Subproject 5c64761477bd1df380056e5ad3b20677616664b Subproject 9b5b11e8dad01c5f5e465b15dfee87dd4138a5f