about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md57
-rw-r--r--CMakeLists.txt9
-rw-r--r--README.md4
-rw-r--r--VERSION2
-rwxr-xr-xassets/areas.yaml12
-rw-r--r--assets/owl.pngbin0 -> 439 bytes
-rw-r--r--assets/subway.pngbin0 -> 184533 bytes
-rw-r--r--assets/subway.yaml1037
-rw-r--r--src/ap_state.cpp172
-rw-r--r--src/ap_state.h11
-rw-r--r--src/area_popup.cpp40
-rw-r--r--src/game_data.cpp238
-rw-r--r--src/game_data.h40
-rw-r--r--src/logger.cpp2
-rw-r--r--src/logger.h8
-rw-r--r--src/main.cpp11
-rw-r--r--src/network_set.cpp30
-rw-r--r--src/network_set.h25
-rw-r--r--src/subway_map.cpp724
-rw-r--r--src/subway_map.h92
-rw-r--r--src/tracker_frame.cpp82
-rw-r--r--src/tracker_frame.h14
-rw-r--r--src/tracker_panel.cpp24
-rw-r--r--src/tracker_state.cpp405
-rw-r--r--src/tracker_state.h11
-rw-r--r--src/version.h13
-rw-r--r--vcpkg.json3
m---------vendor/apclientpp0
m---------vendor/asio0
-rw-r--r--vendor/quadtree/Box.h67
-rw-r--r--vendor/quadtree/LICENSE21
-rw-r--r--vendor/quadtree/Quadtree.h315
-rw-r--r--vendor/quadtree/Vector2.h47
m---------vendor/wswrap0
34 files changed, 3279 insertions, 237 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f8a99..5173bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,62 @@
1# lingo-ap-tracker Releases 1# lingo-ap-tracker Releases
2 2
3## v0.10.3 - 2024-06-10
4
5- Fixed crash that occurred when the Eye Wall painting was in your painting
6 mapping.
7- Fixed getting bombarded with message boxes when the tracker crashes.
8- Moved early color hallways painting to match the new in-game location.
9
10Download:
11[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/>
12Source: [v0.10.3](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.3)
13
14## v0.10.2 - 2024-06-09
15
16- Fixed intermittent reachability detection issue.
17- Increased debug logging.
18
19Download:
20[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/>
21Source: [v0.10.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.2)
22
23## v0.10.1 - 2024-06-08
24
25- Fixed display not updating when game state changes.
26- Fixed broken reachability calculation.
27
28Download:
29[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/>
30Source: [v0.10.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.1)
31
32## v0.10.0 - 2024-06-06
33
34- Added a subway map tab to the tracker. This displays the topology of the game,
35 along with all of the doors, paintings, sunwarps, and regular warps. It can
36 show you whether doors are open or closed, and you can hover over them to see
37 what's needed to open them. You can also hover over paintings, sunwarps, and
38 regular warps to see what they're connected to. Thanks to Kinrah for creating
39 the image!
40- When playing on painting shuffle, the tracker will no longer automatically
41 take your painting mapping into consideration. Instead, unchecked paintings
42 will show up in the regular map tab as if they were unchecked locations.
43 Looking into a painting in-game marks it as checked in the tracker, and if the
44 painting goes somewhere, the tracker will record that and recalculate your
45 reachable areas accordingly. v3.1.0 or later of the client is required for
46 this to work.
47
48Download:
49[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/>
50Source: [v0.10.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.0)
51
52## v0.9.2 - 2024-06-04
53
54- Updated apclient and dependencies.
55
56Download:
57[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/>
58Source: [v0.9.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.9.2)
59
3## v0.9.1 - 2024-05-15 60## v0.9.1 - 2024-05-15
4 61
5- Fixed pilgrimage detection when sunwarp shuffle is on. 62- Fixed pilgrimage detection when sunwarp shuffle is on.
diff --git a/CMakeLists.txt b/CMakeLists.txt index a6f6342..f9f1117 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -10,6 +10,7 @@ find_package(wxWidgets CONFIG REQUIRED)
10find_package(OpenSSL REQUIRED) 10find_package(OpenSSL REQUIRED)
11find_package(yaml-cpp REQUIRED) 11find_package(yaml-cpp REQUIRED)
12find_package(websocketpp REQUIRED) 12find_package(websocketpp REQUIRED)
13find_package(fmt REQUIRED)
13 14
14include_directories( 15include_directories(
15 vendor/hkutil 16 vendor/hkutil
@@ -22,6 +23,8 @@ include_directories(
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}
27 vendor
25) 28)
26 29
27find_path(SYSTEM_INCLUDE_DIR zlib.h) 30find_path(SYSTEM_INCLUDE_DIR zlib.h)
@@ -39,12 +42,14 @@ add_executable(lingo_ap_tracker
39 "src/connection_dialog.cpp" 42 "src/connection_dialog.cpp"
40 "src/tracker_state.cpp" 43 "src/tracker_state.cpp"
41 "src/tracker_config.cpp" 44 "src/tracker_config.cpp"
42 "src/logger.cpp"
43 "src/achievements_pane.cpp" 45 "src/achievements_pane.cpp"
44 "src/settings_dialog.cpp" 46 "src/settings_dialog.cpp"
45 "src/global.cpp" 47 "src/global.cpp"
48 "src/subway_map.cpp"
49 "src/network_set.cpp"
50 "src/logger.cpp"
46 "vendor/whereami/whereami.c" 51 "vendor/whereami/whereami.c"
47) 52)
48set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) 53set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
49set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON) 54set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD_REQUIRED ON)
50target_link_libraries(lingo_ap_tracker PRIVATE OpenSSL::SSL OpenSSL::Crypto websocketpp::websocketpp wx::core wx::base wx::net yaml-cpp::yaml-cpp) 55target_link_libraries(lingo_ap_tracker PRIVATE fmt::fmt OpenSSL::SSL OpenSSL::Crypto websocketpp::websocketpp wx::core wx::base wx::net yaml-cpp::yaml-cpp)
diff --git a/README.md b/README.md index 0b928cd..83525dd 100644 --- a/README.md +++ b/README.md
@@ -6,3 +6,7 @@ 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
12Thanks to Kinrah for making the subway map image!
diff --git a/VERSION b/VERSION index 7f7306b..000aadb 100644 --- a/VERSION +++ b/VERSION
@@ -1 +1 @@
v0.9.1 \ No newline at end of file v0.10.3 \ No newline at end of file
diff --git a/assets/areas.yaml b/assets/areas.yaml index d38ceb8..cbcf23a 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):
diff --git a/assets/owl.png b/assets/owl.png new file mode 100644 index 0000000..0210303 --- /dev/null +++ b/assets/owl.png
Binary files differ
diff --git a/assets/subway.png b/assets/subway.png new file mode 100644 index 0000000..3860d2c --- /dev/null +++ b/assets/subway.png
Binary files differ
diff --git a/assets/subway.yaml b/assets/subway.yaml new file mode 100644 index 0000000..3d10ad3 --- /dev/null +++ b/assets/subway.yaml
@@ -0,0 +1,1037 @@
1---
2- pos: [1050, 954]
3 room: Starting Room
4 door: Back Right Door
5- pos: [986, 1034]
6 room: Starting Room
7 door: Rhyme Room Entrance
8- pos: [990, 956]
9 special: starting_room_paintings # Early Color Hallways painting is a hardcoded special case
10 paintings:
11 - arrows_painting
12- pos: [905, 841]
13 room: Hedge Maze
14 door: Painting Shortcut
15 paintings:
16 - garden_painting_tower2
17 tags:
18 - garden_starting
19- pos: [1066, 841]
20 room: Courtyard
21 door: Painting Shortcut
22 paintings:
23 - flower_painting_8
24 tags:
25 - flower_starting
26- pos: [905, 895]
27 room: The Wondrous (Doorknob)
28 door: Painting Shortcut
29 paintings:
30 - symmetry_painting_a_starter
31 tags:
32 - symmetry_starting
33- pos: [1066, 868]
34 room: Outside The Bold
35 door: Painting Shortcut
36 paintings:
37 - pencil_painting6
38 tags:
39 - pencil_starting
40- pos: [1066, 895]
41 room: Outside The Undeterred
42 door: Painting Shortcut
43 paintings:
44 - blueman_painting_3
45 tags:
46 - blueman_starting
47- pos: [905, 868]
48 room: Outside The Agreeable
49 door: Painting Shortcut
50 paintings:
51 - eyes_yellow_painting2
52 tags:
53 - street_starting
54- pos: [1211, 879]
55 room: Hidden Room
56 door: Dead End Door
57- pos: [1291, 906]
58 room: Hidden Room
59 door: Knight Night Entrance
60- pos: [1103, 980]
61 room: Hidden Room
62 door: Seeker Entrance
63- pos: [1173, 980]
64 room: Hidden Room
65 door: Rhyme Room Entrance
66- pos: [1116, 939]
67 paintings:
68 - owl_painting
69 tags:
70 - owl_hidden
71- pos: [986, 793]
72 room: Second Room
73 door: Exit Door
74- pos: [798, 584]
75 room: Hub Room
76 door: Crossroads Entrance
77- pos: [932, 665]
78 room: Hub Room
79 door: Tenacious Entrance
80- pos: [1361, 578]
81 room: Hub Room
82 door: Shortcut to Hedge Maze
83- pos: [1312, 841]
84 room: Hub Room
85 door: Near RAT Door
86- pos: [1371, 729]
87 room: Hub Room
88 door: Traveled Entrance
89- pos: [1313, 686]
90 paintings:
91 - maze_painting
92 tags:
93 - green_owl
94 - green_numbers
95- pos: [1172, 760]
96 sunwarp:
97 dots: 1
98 type: enter
99- pos: [1302, 638]
100 room: Outside The Undeterred
101 door: Fours
102- pos: [1243, 819]
103 room: Outside The Undeterred
104 door: Fours
105- pos: [1276, 819]
106 room: Number Hunt
107 door: Eights
108- pos: [1263, 867]
109 paintings:
110 - smile_painting_6
111 tags:
112 - smiley_deadend
113- pos: [1012, 1086]
114 sunwarp:
115 dots: 6
116 type: final
117- pos: [938, 1002]
118 room: Pilgrim Antechamber
119 door: Sun Painting
120 special: sun_painting
121- pos: [1053, 1090]
122 invisible: true
123 special: sun_painting_exit
124- pos: [1077, 1061]
125 room: Pilgrim Room
126 door: Shortcut to The Seeker
127- pos: [713, 359]
128 room: Number Hunt
129 door: Eights
130- pos: [932, 348]
131 room: Crossroads
132 door: Hollow Hallway
133- pos: [798, 290]
134 room: Crossroads
135 door: Tower Entrance
136- pos: [932, 477]
137 room: Crossroads
138 door: Tenacious Entrance
139- pos: [638, 477]
140 room: Crossroads
141 door: Discerning Entrance
142- pos: [905, 290]
143 room: Crossroads
144 door: Tower Back Entrance
145- pos: [894, 423]
146 room: Crossroads
147 door: Words Sword Door
148- pos: [632, 643]
149 room: Crossroads
150 door: Eye Wall
151- pos: [638, 520]
152 room: Crossroads
153 door: Roof Access
154- pos: [756, 400]
155 paintings:
156 - smile_painting_4
157 tags:
158 - smiley_crossroads
159- pos: [878, 509]
160 sunwarp:
161 dots: 1
162 type: exit
163- pos: [1056, 344]
164 room: Lost Area
165 door: Exit
166- pos: [954, 290]
167 room: Lost Area
168 door: Exit
169- pos: [986, 290]
170 room: Number Hunt
171 door: Eights
172- pos: [954, 247]
173 room: Amen Name Area
174 door: Exit
175- pos: [954, 222]
176 paintings:
177 - west_afar
178- pos: [986, 697]
179 room: The Tenacious
180 door: Shortcut to Hub Room
181- pos: [1173, 665]
182 room: Near Far Area
183 door: Door
184- pos: [1173, 622]
185 room: Warts Straw Area
186 door: Door
187- pos: [1173, 579]
188 room: Leaf Feel Area
189 door: Door
190- pos: [1173, 333]
191 room: Outside The Agreeable
192 door: Purple Barrier
193- pos: [1088, 289]
194 room: Outside The Undeterred
195 door: Fives
196- pos: [1088, 418]
197 room: Outside The Undeterred
198 door: Fives
199- pos: [1039, 477]
200 room: Outside The Agreeable
201 door: Tenacious Entrance
202- pos: [1147, 525]
203 room: Outside The Agreeable
204 door: Black Door
205- pos: [1216, 525]
206 room: Outside The Agreeable
207 door: Agreeable Entrance
208- pos: [1138, 287]
209 paintings:
210 - eyes_yellow_painting
211 tags:
212 - street_starting
213- pos: [1088, 385]
214 sunwarp:
215 dots: 6
216 type: enter
217- pos: [1195, 450]
218 room: Compass Room
219 door: Lookout Entrance
220- pos: [1214, 457]
221 paintings:
222 - pencil_painting7
223 tags:
224 - pencil_compass
225- pos: [1196, 417]
226 invisible: true
227 tags:
228 - agreeable_to_lookout
229- pos: [1657, 1392]
230 room: Room Room
231 door: Excavation
232- pos: [1725, 1441]
233 invisible: true
234 tags:
235 - agreeable_to_lookout
236- pos: [1040, 665]
237 room: Dread Hallway
238 door: Tenacious Entrance
239- pos: [1324, 525]
240 room: The Agreeable
241 door: Shortcut to Hedge Maze
242- pos: [1484, 392]
243 room: Hedge Maze
244 door: Perceptive Entrance
245- pos: [1441, 241]
246 room: Hedge Maze
247 door: Observant Entrance
248- pos: [1714, 434]
249 room: Hedge Maze
250 door: Observant Entrance
251- pos: [1477, 343]
252 paintings:
253 - garden_painting_tower
254 tags:
255 - garden_starting
256- pos: [1565, 311]
257 room: The Fearless (First Floor)
258 door: Second Floor
259- pos: [1597, 279]
260 room: The Fearless (Second Floor)
261 door: Third Floor
262- pos: [1414, 209]
263 room: The Observant
264 door: Backside Door
265- pos: [1624, 188]
266 room: The Observant
267 door: Stairs
268- pos: [1667, 686]
269 room: The Incomparable
270 door: Eight Door
271- pos: [1784, 569]
272 paintings:
273 - crown_painting
274 tags:
275 - crown_tower6
276- pos: [1653, 717]
277 paintings:
278 - eight_painting2
279 tags:
280 - eight_alcove
281- pos: [1653, 662]
282 paintings:
283 - eight_painting
284 tags:
285 - eight_alcove
286- pos: [697, 1471]
287 room: Orange Tower
288 door: Second Floor
289- pos: [633, 1406]
290 room: Orange Tower
291 door: Third Floor
292- pos: [570, 1343]
293 room: Orange Tower
294 door: Fourth Floor
295- pos: [504, 1279]
296 room: Orange Tower
297 door: Fifth Floor
298- pos: [440, 1215]
299 room: Orange Tower
300 door: Sixth Floor
301- pos: [379, 1153]
302 room: Orange Tower
303 door: Seventh Floor
304- pos: [905, 793]
305 room: Orange Tower First Floor
306 door: Shortcut to Hub Room
307- pos: [686, 820]
308 room: Orange Tower First Floor
309 door: Salt Pepper Door
310- pos: [755, 787]
311 sunwarp:
312 dots: 4
313 type: enter
314- pos: [719, 846]
315 tags:
316 - tower1_tower1
317- pos: [681, 1506]
318 tags:
319 - tower1_tower1
320- pos: [722, 1439]
321 tags:
322 - tower2_undeterred
323- pos: [533, 1375]
324 tags:
325 - tower3_tower3
326- pos: [662, 1375]
327 tags:
328 - tower3_gallery
329- pos: [483, 1307]
330 tags:
331 - tower4_room
332- pos: [598, 1307]
333 tags:
334 - tower4_tower4
335- pos: [598, 1287]
336 tags:
337 - tower4_courtyard
338- pos: [533, 1245]
339 tags:
340 - tower5_welcome
341- pos: [419, 1245]
342 tags:
343 - tower5_cellar
344- pos: [419, 1267]
345 tags:
346 - tower5_quadruple
347- pos: [203, 1014]
348 tags:
349 - tower2_undeterred
350- pos: [1325, 1191]
351 tags:
352 - tower3_tower3
353- pos: [1700, 1021]
354 tags:
355 - tower3_gallery
356- pos: [1653, 1318]
357 tags:
358 - tower4_room
359- pos: [918, 222]
360 tags:
361 - tower4_tower4
362- pos: [806, 222]
363 tags:
364 - tower4_courtyard
365- pos: [652, 951]
366 tags:
367 - tower5_welcome
368- pos: [1553, 1440]
369 tags:
370 - tower5_cellar
371- pos: [1459, 1119]
372 tags:
373 - tower5_quadruple
374- pos: [1216, 1280]
375 room: Orange Tower Third Floor
376 door: Red Barrier
377- pos: [1173, 1248]
378 room: Orange Tower Third Floor
379 door: Rhyme Room Entrance
380- pos: [1270, 1231]
381 paintings:
382 - arrows_painting_6
383 - flower_painting_5
384- pos: [1216, 1216]
385 sunwarp:
386 dots: 2
387 type: exit
388- pos: [1253, 1172]
389 sunwarp:
390 dots: 3
391 type: enter
392- pos: [852, 198]
393 room: Orange Tower Fourth Floor
394 door: Hot Crusts Door
395- pos: [830, 289]
396 sunwarp:
397 dots: 5
398 type: enter
399- pos: [877, 155]
400 room: Number Hunt
401 door: Eights
402- pos: [844, 134]
403 paintings:
404 - smile_painting_8
405 tags:
406 - smiley_hotcrusts
407- pos: [797, 155]
408 sunwarp:
409 dots: 2
410 type: enter
411- pos: [679, 985]
412 room: Number Hunt
413 door: Nines
414- pos: [723, 953]
415 room: Orange Tower Fifth Floor
416 door: Welcome Back
417- pos: [683, 944]
418 paintings:
419 - east_afar
420- pos: [548, 1221]
421 paintings:
422 - hi_solved_painting3
423- pos: [1574, 1425]
424 paintings:
425 - hi_solved_painting2
426- pos: [411, 1186]
427 paintings:
428 - arrows_painting_10
429 - owl_painting_3
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
442 - crown_tower6
443 - apple_tower6
444 - hi_scientific
445- pos: [349, 1124]
446 paintings:
447 - map_painting2
448- pos: [436, 1159]
449 room: Orange Tower Seventh Floor
450 door: Mastery
451- pos: [544, 1159]
452 paintings:
453 - arrows_painting_11
454- pos: [498, 284]
455 room: Courtyard
456 door: Green Barrier
457- pos: [556, 233]
458 paintings:
459 - flower_painting_7
460 tags:
461 - flower_starting
462 - flower_arrow
463- pos: [600, 332]
464 room: Number Hunt
465 door: Nines
466- pos: [579, 350]
467 paintings:
468 - blueman_painting
469 tags:
470 - blueman_courtyard
471- pos: [530, 310]
472 room: First Second Third Fourth
473 door: Backside Door
474- pos: [584, 107]
475 room: The Colorful (White)
476 door: Progress Door
477- pos: [622, 107]
478 room: The Colorful (Black)
479 door: Progress Door
480- pos: [659, 107]
481 room: The Colorful (Red)
482 door: Progress Door
483- pos: [697, 107]
484 room: The Colorful (Yellow)
485 door: Progress Door
486- pos: [734, 107]
487 room: The Colorful (Blue)
488 door: Progress Door
489- pos: [772, 107]
490 room: The Colorful (Purple)
491 door: Progress Door
492- pos: [809, 107]
493 room: The Colorful (Orange)
494 door: Progress Door
495- pos: [847, 107]
496 room: The Colorful (Green)
497 door: Progress Door
498- pos: [884, 107]
499 room: The Colorful (Brown)
500 door: Progress Door
501- pos: [922, 107]
502 room: The Colorful (Gray)
503 door: Progress Door
504- pos: [967, 107]
505 paintings:
506 - arrows_painting_12
507- pos: [878, 954]
508 room: Welcome Back Area
509 door: Shortcut to Starting Room
510- pos: [773, 954]
511 tags:
512 - hub_wb
513 - wondrous_wb
514 - undeterred_wb
515 - agreeable_wb
516 - wanderer_wb
517 - observant_wb
518 - gallery_wb
519 - scientific_wb
520 - cellar_wb
521- pos: [1107, 749]
522 tags:
523 - hub_wb
524- pos: [408, 817]
525 tags:
526 - wondrous_wb
527- pos: [281, 1017]
528 tags:
529 - undeterred_wb
530- pos: [1017, 289]
531 tags:
532 - agreeable_wb
533- pos: [907, 1385]
534 tags:
535 - wanderer_wb
536- pos: [1737, 1053]
537 tags:
538 - gallery_wb
539- pos: [1690, 268]
540 tags:
541 - observant_wb
542- pos: [250, 604]
543 tags:
544 - scientific_wb
545- pos: [1553, 1467]
546 tags:
547 - cellar_wb
548- pos: [1478, 498]
549 room: Owl Hallway
550 door: Shortcut to Hedge Maze
551- pos: [1480, 551]
552 paintings:
553 - arrows_painting_8
554 - maze_painting_2
555 - owl_painting_2
556 - clock_painting_4
557 tags:
558 - green_owl
559 - owl_hidden
560 - owl_tower6
561- pos: [1478, 938]
562 room: Number Hunt
563 door: Sevens
564- pos: [1580, 853]
565 room: Number Hunt
566 door: Sevens
567- pos: [1478, 905]
568 room: Number Hunt
569 door: Eights
570- pos: [1452, 841]
571 room: Number Hunt
572 door: Nines
573- pos: [1420, 841]
574 room: Outside The Initiated
575 door: Blue Barrier
576- pos: [1479, 1018]
577 room: Outside The Initiated
578 door: Orange Barrier
579- pos: [1360, 847]
580 room: Outside The Initiated
581 door: Shortcut to Hub Room
582- pos: [1511, 841]
583 room: Outside The Initiated
584 door: Initiated Entrance
585- pos: [1141, 1441]
586 room: Orange Tower Third Floor
587 door: Orange Barrier
588- pos: [1173, 1441]
589 room: Outside The Initiated
590 door: Green Barrier
591- pos: [1206, 1441]
592 room: Outside The Initiated
593 door: Purple Barrier
594- pos: [1189, 1355]
595 room: Outside The Initiated
596 door: Entrance
597- pos: [1580, 729]
598 room: Outside The Initiated
599 door: Eight Door
600- pos: [1530, 938]
601 paintings:
602 - clock_painting_5
603 tags:
604 - clock_initiated
605- pos: [1546, 938]
606 paintings:
607 - clock_painting_2
608 - arrows_painting_2
609 tags:
610 - clock_tower6
611 - clock_initiated
612- pos: [1579, 813]
613 sunwarp:
614 dots: 3
615 type: exit
616- pos: [1444, 896]
617 paintings:
618 - smile_painting_1
619 tags:
620 - smiley_initiated
621- pos: [1430, 691]
622 room: Outside The Undeterred
623 door: Fours
624- pos: [1468, 728]
625 room: The Traveled
626 door: Color Hallways Entrance
627- pos: [1533, 707]
628 tags:
629 - red_ch
630 - blue_ch
631 - yellow_ch
632 - green_ch
633 - early_ch
634- pos: [1567, 1264]
635 tags:
636 - red_ch
637- pos: [150, 808]
638 tags:
639 - blue_ch
640- pos: [626, 371]
641 tags:
642 - yellow_ch
643- pos: [1368, 241]
644 tags:
645 - green_ch
646- pos: [1543, 1307]
647 room: Number Hunt
648 door: Sixes
649- pos: [1543, 1388]
650 room: Number Hunt
651 door: Nines
652- pos: [1468, 1377]
653 room: Outside The Bold
654 door: Bold Entrance
655- pos: [1425, 1358]
656 paintings:
657 - pencil_painting2
658 - north_missing2
659 tags:
660 - pencil_compass
661 - pencil_starting
662 - pencil_directional
663- pos: [1334, 1419]
664 room: Outside The Bold
665 door: Steady Entrance
666- pos: [445, 1048]
667 tags:
668 - undeterred_artistic
669- pos: [279, 1071]
670 room: Number Hunt
671 door: Zero Door
672- pos: [338, 1071]
673 room: Outside The Undeterred
674 door: Twos
675- pos: [370, 1071]
676 room: Outside The Undeterred
677 door: Threes
678- pos: [402, 1071]
679 room: Outside The Undeterred
680 door: Fours
681- pos: [242, 1071]
682 room: Outside The Undeterred
683 door: Undeterred Entrance
684- pos: [60, 1017]
685 paintings:
686 - blueman_painting_2
687 tags:
688 - blueman_courtyard
689 - blueman_starting
690- pos: [402, 1012]
691 room: Outside The Undeterred
692 door: Green Painting
693 paintings:
694 - maze_painting_3
695 tags:
696 - green_numbers
697- pos: [134, 1007]
698 sunwarp:
699 dots: 4
700 type: exit
701- pos: [719, 1039]
702 room: Outside The Undeterred
703 door: Challenge Entrance
704- pos: [438, 1039]
705 room: Outside The Undeterred
706 door: Number Hunt
707- pos: [563, 1071]
708 room: Outside The Undeterred
709 door: Fives
710- pos: [596, 1071]
711 room: Number Hunt
712 door: Sixes
713- pos: [627, 1071]
714 room: Number Hunt
715 door: Sevens
716- pos: [659, 1071]
717 room: Number Hunt
718 door: Eights
719- pos: [692, 1071]
720 room: Number Hunt
721 door: Nines
722- pos: [525, 1002]
723 room: Number Hunt
724 door: Door to Directional Gallery
725- pos: [659, 1014]
726 room: Number Hunt
727 door: Eights
728 paintings:
729 - smile_painting_5
730 tags:
731 - smiley_numbers
732- pos: [557, 953]
733 room: Number Hunt
734 door: Sevens
735- pos: [391, 932]
736 room: Number Hunt
737 door: Sixes
738- pos: [311, 932]
739 room: Number Hunt
740 door: Nines
741- pos: [268, 868]
742 room: Number Hunt
743 door: Eights
744- pos: [268, 825]
745 room: Directional Gallery
746 door: Yellow Barrier
747- pos: [231, 681]
748 room: Number Hunt
749 door: Sixes
750- pos: [242, 980]
751 room: Directional Gallery
752 door: Shortcut to The Undeterred
753- pos: [351, 927]
754 paintings:
755 - boxes_painting
756 tags:
757 - lattice_directional
758- pos: [272, 927]
759 paintings:
760 - smile_painting_7
761 tags:
762 - smiley_directional
763- pos: [214, 822]
764 paintings:
765 - cherry_painting
766 tags:
767 - apple_directional
768- pos: [266, 735]
769 room: Number Hunt
770 door: Sixes
771 paintings:
772 - pencil_painting3
773 tags:
774 - pencil_directional
775- pos: [215, 735]
776 paintings:
777 - flower_painting_4
778- pos: [626, 851]
779 sunwarp:
780 dots: 6
781 type: exit
782- pos: [1141, 1441]
783 room: Orange Tower Third Floor
784 door: Orange Barrier
785- pos: [1174, 1441]
786 room: Outside The Initiated
787 door: Green Barrier
788- pos: [1205, 1441]
789 room: Outside The Initiated
790 door: Purple Barrier
791- pos: [1334, 1377]
792 room: Color Hunt
793 door: Shortcut to The Steady
794- pos: [1280, 1375]
795 paintings:
796 - arrows_painting_7
797- pos: [1233, 1321]
798 room: Outside The Initiated
799 door: Entrance
800 paintings:
801 - fruitbowl_painting3
802- pos: [1290, 1323]
803 sunwarp:
804 dots: 5
805 type: exit
806- pos: [1189, 1356]
807 room: Outside The Initiated
808 door: Entrance
809- pos: [1154, 1332]
810 paintings:
811 - colors_painting
812- pos: [1640, 1260]
813 room: The Bearer
814 door: Backside Door
815- pos: [1468, 1287]
816 room: The Bearer
817 door: Entrance
818- pos: [1430, 1232]
819 room: Number Hunt
820 door: Sixes
821- pos: [1388, 1152]
822 room: Bearer Side Area
823 door: Shortcut to Tower
824- pos: [1273, 1142]
825 paintings:
826 - pencil_painting5
827 - pencil_painting4
828- pos: [1355, 1092]
829 room: Knight Night (Final)
830 door: Exit
831- pos: [1382, 900]
832 room: Knight Night (Final)
833 door: Exit
834- pos: [1388, 1002]
835 # Complex case, because this is also blocked by Knight Night (Final) - Exit
836 room: Number Hunt
837 door: Sevens
838- pos: [1653, 101]
839 paintings:
840 - smile_painting_9
841 tags:
842 - smiley_crossroads
843 - smiley_deadend
844 - smiley_hotcrusts
845 - smiley_numbers
846 - smiley_directional
847 - smiley_initiated
848 - smiley_gallery
849 - smiley_theysee
850- pos: [1656, 139]
851 room: The Artistic (Smiley)
852 door: Door to Panda
853- pos: [1711, 140]
854 tags:
855 - undeterred_artistic
856- pos: [1653, 169]
857 paintings:
858 - panda_painting_3
859 tags:
860 - panda_tower6
861 - panda_hallway
862- pos: [1708, 171]
863 room: The Artistic (Panda)
864 door: Door to Lattice
865- pos: [1761, 169]
866 paintings:
867 - boxes_painting2
868 tags:
869 - lattice_directional
870- pos: [1762, 139]
871 room: The Artistic (Lattice)
872 door: Door to Apple
873- pos: [1761, 101]
874 paintings:
875 - cherry_painting3
876 tags:
877 - apple_tower6
878 - apple_directional
879- pos: [1708, 107]
880 room: The Artistic (Apple)
881 door: Door to Smiley
882- pos: [370, 681]
883 room: Number Hunt
884 door: Eights
885- pos: [411, 685]
886 paintings:
887 - eye_painting_2
888 - smile_painting_2
889 tags:
890 - smiley_theysee
891- pos: [310, 750]
892 room: The Eyes They See
893 door: Exit
894- pos: [334, 798]
895 paintings:
896 - arrows_painting_5
897- pos: [370, 792]
898 room: Outside The Wondrous
899 door: Wondrous Entrance
900- pos: [367, 752]
901 paintings:
902 - symmetry_painting_a_1
903 - symmetry_painting_b_1
904 - symmetry_painting_a_3
905 - symmetry_painting_a_5
906 - symmetry_painting_b_4
907 - symmetry_painting_a_2
908 - symmetry_painting_b_2
909 - symmetry_painting_a_6
910 - symmetry_painting_b_6
911 tags:
912 - symmetry_starting
913- pos: [407, 755]
914 room: The Wondrous
915 door: Exit
916 paintings:
917 - arrows_painting_9
918- pos: [449, 755]
919 paintings:
920 - flower_painting_6
921 tags:
922 - flower_arrow
923- pos: [1101, 222]
924 paintings:
925 - panda_painting
926 tags:
927 - panda_hallway
928- pos: [1152, 209]
929 room: Hallway Room (1)
930 door: Exit
931- pos: [1189, 170]
932 room: Hallway Room (2)
933 door: Exit
934- pos: [1238, 124]
935 room: Hallway Room (3)
936 door: Exit
937- pos: [1313, 108]
938 room: Hallway Room (4)
939 door: Exit
940- pos: [1528, 108]
941 room: Hallway Room (4)
942 door: Exit
943- pos: [1415, 140]
944 room: Number Hunt
945 door: Nines
946- pos: [1458, 133]
947 paintings:
948 - south_afar
949- pos: [826, 1452]
950 room: Outside The Wanderer
951 door: Wanderer Entrance
952- pos: [763, 1465]
953 room: Outside The Wanderer
954 door: Tower Entrance
955- pos: [1655, 1151]
956 room: Number Hunt
957 door: Eights
958- pos: [1623, 1044]
959 room: Art Gallery
960 door: Second Floor
961- pos: [1623, 1019]
962 room: Art Gallery
963 door: Third Floor
964- pos: [1623, 991]
965 room: Art Gallery
966 door: Fourth Floor
967- pos: [1623, 964]
968 room: Art Gallery
969 door: Fifth Floor
970- pos: [1511, 1119]
971 room: Art Gallery
972 door: Exit
973- pos: [1654, 1116]
974 paintings:
975 - smile_painting_3
976 - flower_painting_2
977 - scenery_painting_0a
978 - map_painting
979 - fruitbowl_painting4
980 tags:
981 - smiley_gallery
982- pos: [1120, 1286]
983 room: Rhyme Room (Smiley)
984 door: Door to Target
985- pos: [1120, 1315]
986 tags:
987 - rhyme_smiley_target
988- pos: [792, 1137]
989 tags:
990 - rhyme_smiley_target
991- pos: [895, 1217]
992 room: Number Hunt
993 door: Nines
994- pos: [938, 1296]
995 room: Rhyme Room (Cross)
996 door: Exit
997- pos: [1120, 1195]
998 room: Rhyme Room (Circle)
999 door: Door to Smiley
1000- pos: [1118, 1137]
1001 paintings:
1002 - arrows_painting_3
1003- pos: [1050, 1142]
1004 room: Rhyme Room (Looped Square)
1005 door: Door to Circle
1006- pos: [987, 1194]
1007 room: Rhyme Room (Looped Square)
1008 door: Door to Cross
1009- pos: [922, 1142]
1010 room: Rhyme Room (Looped Square)
1011 door: Door to Target
1012- pos: [852, 1200]
1013 room: Rhyme Room (Target)
1014 door: Door to Cross
1015- pos: [850, 1138]
1016 paintings:
1017 - arrows_painting_4
1018- pos: [1592, 1442]
1019 room: Room Room
1020 door: Cellar Exit
1021- pos: [1570, 938]
1022 room: Outside The Wise
1023 door: Wise Entrance
1024- pos: [1653, 935]
1025 paintings:
1026 - clock_painting_3
1027- pos: [369, 605]
1028 room: Outside The Scientific
1029 door: Scientific Entrance
1030- pos: [294, 602]
1031 paintings:
1032 - hi_solved_painting4
1033 tags:
1034 - hi_scientific
1035- pos: [814, 1001]
1036 room: Challenge Room
1037 door: Welcome Door
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index b0b4f0b..876fdd8 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -4,6 +4,7 @@
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>
@@ -71,6 +72,7 @@ struct APState {
71 bool sunwarp_shuffle = false; 72 bool sunwarp_shuffle = false;
72 73
73 std::map<std::string, std::string> painting_mapping; 74 std::map<std::string, std::string> painting_mapping;
75 std::set<std::string> painting_codomain;
74 std::map<int, SunwarpMapping> sunwarp_mapping; 76 std::map<int, SunwarpMapping> sunwarp_mapping;
75 77
76 void Connect(std::string server, std::string player, std::string password) { 78 void Connect(std::string server, std::string player, std::string password) {
@@ -91,24 +93,25 @@ struct APState {
91 }).detach(); 93 }).detach();
92 94
93 for (int panel_id : GD_GetAchievementPanels()) { 95 for (int panel_id : GD_GetAchievementPanels()) {
94 tracked_data_storage_keys.push_back( 96 tracked_data_storage_keys.push_back(fmt::format(
95 "Achievement|" + GD_GetPanel(panel_id).achievement_name); 97 "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
96 } 98 }
97 99
98 for (const MapArea& map_area : GD_GetMapAreas()) { 100 for (const MapArea& map_area : GD_GetMapAreas()) {
99 for (const Location& location : map_area.locations) { 101 for (const Location& location : map_area.locations) {
100 tracked_data_storage_keys.push_back( 102 tracked_data_storage_keys.push_back(
101 "Hunt|" + std::to_string(location.ap_location_id)); 103 fmt::format("Hunt|{}", location.ap_location_id));
102 } 104 }
103 } 105 }
104 106
105 tracked_data_storage_keys.push_back("PlayerPos"); 107 tracked_data_storage_keys.push_back("PlayerPos");
108 tracked_data_storage_keys.push_back("Paintings");
106 109
107 initialized = true; 110 initialized = true;
108 } 111 }
109 112
110 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 113 tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
111 TrackerLog("Connecting to Archipelago server (" + server + ")..."); 114 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
112 115
113 { 116 {
114 TrackerLog("Destroying old AP client..."); 117 TrackerLog("Destroying old AP client...");
@@ -137,6 +140,7 @@ struct APState {
137 color_shuffle = false; 140 color_shuffle = false;
138 painting_shuffle = false; 141 painting_shuffle = false;
139 painting_mapping.clear(); 142 painting_mapping.clear();
143 painting_codomain.clear();
140 mastery_requirement = 21; 144 mastery_requirement = 21;
141 level_2_requirement = 223; 145 level_2_requirement = 223;
142 location_checks = kNORMAL_LOCATIONS; 146 location_checks = kNORMAL_LOCATIONS;
@@ -149,16 +153,17 @@ struct APState {
149 sunwarp_shuffle = false; 153 sunwarp_shuffle = false;
150 sunwarp_mapping.clear(); 154 sunwarp_mapping.clear();
151 155
156 std::mutex connection_mutex;
152 connected = false; 157 connected = false;
153 has_connection_result = false; 158 has_connection_result = false;
154 159
155 apclient->set_room_info_handler([this, player, password]() { 160 apclient->set_room_info_handler([this, player, password]() {
156 inventory.clear(); 161 inventory.clear();
157 162
158 TrackerLog("Connected to Archipelago server. Authenticating as " + 163 TrackerLog(fmt::format(
159 player + 164 "Connected to Archipelago server. Authenticating as {} {}", player,
160 (password.empty() ? " without password" 165 (password.empty() ? "without password"
161 : " with password " + password)); 166 : "with password " + password)));
162 tracker_frame->SetStatusMessage( 167 tracker_frame->SetStatusMessage(
163 "Connected to Archipelago server. Authenticating..."); 168 "Connected to Archipelago server. Authenticating...");
164 169
@@ -170,10 +175,10 @@ struct APState {
170 [this](const std::list<int64_t>& locations) { 175 [this](const std::list<int64_t>& locations) {
171 for (const int64_t location_id : locations) { 176 for (const int64_t location_id : locations) {
172 checked_locations.insert(location_id); 177 checked_locations.insert(location_id);
173 TrackerLog("Location: " + std::to_string(location_id)); 178 TrackerLog(fmt::format("Location: {}", location_id));
174 } 179 }
175 180
176 RefreshTracker(); 181 RefreshTracker(false);
177 }); 182 });
178 183
179 apclient->set_slot_disconnected_handler([this]() { 184 apclient->set_slot_disconnected_handler([this]() {
@@ -194,10 +199,10 @@ struct APState {
194 [this](const std::list<APClient::NetworkItem>& items) { 199 [this](const std::list<APClient::NetworkItem>& items) {
195 for (const APClient::NetworkItem& item : items) { 200 for (const APClient::NetworkItem& item : items) {
196 inventory[item.item]++; 201 inventory[item.item]++;
197 TrackerLog("Item: " + std::to_string(item.item)); 202 TrackerLog(fmt::format("Item: {}", item.item));
198 } 203 }
199 204
200 RefreshTracker(); 205 RefreshTracker(false);
201 }); 206 });
202 207
203 apclient->set_retrieved_handler( 208 apclient->set_retrieved_handler(
@@ -206,23 +211,23 @@ struct APState {
206 HandleDataStorage(key, value); 211 HandleDataStorage(key, value);
207 } 212 }
208 213
209 RefreshTracker(); 214 RefreshTracker(false);
210 }); 215 });
211 216
212 apclient->set_set_reply_handler([this](const std::string& key, 217 apclient->set_set_reply_handler([this](const std::string& key,
213 const nlohmann::json& value, 218 const nlohmann::json& value,
214 const nlohmann::json&) { 219 const nlohmann::json&) {
215 HandleDataStorage(key, value); 220 HandleDataStorage(key, value);
216 RefreshTracker(); 221 RefreshTracker(false);
217 }); 222 });
218 223
219 apclient->set_slot_connected_handler([this]( 224 apclient->set_slot_connected_handler([this, &connection_mutex](
220 const nlohmann::json& slot_data) { 225 const nlohmann::json& slot_data) {
221 tracker_frame->SetStatusMessage("Connected to Archipelago!"); 226 tracker_frame->SetStatusMessage("Connected to Archipelago!");
222 TrackerLog("Connected to Archipelago!"); 227 TrackerLog("Connected to Archipelago!");
223 228
224 data_storage_prefix = 229 data_storage_prefix =
225 "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; 230 fmt::format("Lingo_{}_", apclient->get_player_number());
226 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); 231 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
227 color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; 232 color_shuffle = slot_data["shuffle_colors"].get<int>() == 1;
228 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 233 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
@@ -253,6 +258,7 @@ struct APState {
253 for (const auto& mapping_it : 258 for (const auto& mapping_it :
254 slot_data["painting_entrance_to_exit"].items()) { 259 slot_data["painting_entrance_to_exit"].items()) {
255 painting_mapping[mapping_it.key()] = mapping_it.value(); 260 painting_mapping[mapping_it.key()] = mapping_it.value();
261 painting_codomain.insert(mapping_it.value());
256 } 262 }
257 } 263 }
258 264
@@ -268,33 +274,39 @@ struct APState {
268 } 274 }
269 } 275 }
270 276
271 connected = true;
272 has_connection_result = true;
273
274 RefreshTracker();
275
276 std::list<std::string> corrected_keys; 277 std::list<std::string> corrected_keys;
277 for (const std::string& key : tracked_data_storage_keys) { 278 for (const std::string& key : tracked_data_storage_keys) {
278 corrected_keys.push_back(data_storage_prefix + key); 279 corrected_keys.push_back(data_storage_prefix + key);
279 } 280 }
280 281
281 { 282 victory_data_storage_key =
282 std::ostringstream vdsks; 283 fmt::format("_read_client_status_{}_{}", apclient->get_team_number(),
283 vdsks << "_read_client_status_" << apclient->get_team_number() << "_" 284 apclient->get_player_number());
284 << apclient->get_player_number();
285 victory_data_storage_key = vdsks.str();
286 }
287 285
288 corrected_keys.push_back(victory_data_storage_key); 286 corrected_keys.push_back(victory_data_storage_key);
289 287
290 apclient->Get(corrected_keys); 288 apclient->Get(corrected_keys);
291 apclient->SetNotify(corrected_keys); 289 apclient->SetNotify(corrected_keys);
290
291 ResetReachabilityRequirements();
292 RefreshTracker(true);
293
294 {
295 std::lock_guard connection_lock(connection_mutex);
296 if (!has_connection_result) {
297 connected = true;
298 has_connection_result = true;
299 }
300 }
292 }); 301 });
293 302
294 apclient->set_slot_refused_handler( 303 apclient->set_slot_refused_handler(
295 [this](const std::list<std::string>& errors) { 304 [this, &connection_mutex](const std::list<std::string>& errors) {
296 connected = false; 305 {
297 has_connection_result = true; 306 std::lock_guard connection_lock(connection_mutex);
307 connected = false;
308 has_connection_result = true;
309 }
298 310
299 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 311 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
300 312
@@ -333,18 +345,29 @@ struct APState {
333 int timeout = 5000; // 5 seconds 345 int timeout = 5000; // 5 seconds
334 int interval = 100; 346 int interval = 100;
335 int remaining_loops = timeout / interval; 347 int remaining_loops = timeout / interval;
336 while (!has_connection_result) { 348 while (true) {
337 if (interval == 0) { 349 {
338 connected = false; 350 std::lock_guard connection_lock(connection_mutex);
339 has_connection_result = true; 351 if (has_connection_result) {
352 break;
353 }
354 }
340 355
356 if (interval == 0) {
341 DestroyClient(); 357 DestroyClient();
342 358
343 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 359 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
344
345 TrackerLog("Timeout while connecting to Archipelago server."); 360 TrackerLog("Timeout while connecting to Archipelago server.");
346 wxMessageBox("Timeout while connecting to Archipelago server.", 361 wxMessageBox("Timeout while connecting to Archipelago server.",
347 "Connection failed", wxOK | wxICON_ERROR); 362 "Connection failed", wxOK | wxICON_ERROR);
363
364 {
365 std::lock_guard connection_lock(connection_mutex);
366 connected = false;
367 has_connection_result = true;
368 }
369
370 break;
348 } 371 }
349 372
350 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 373 std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -353,8 +376,6 @@ struct APState {
353 } 376 }
354 377
355 if (connected) { 378 if (connected) {
356 RefreshTracker();
357 } else {
358 client_active = false; 379 client_active = false;
359 } 380 }
360 } 381 }
@@ -362,12 +383,12 @@ struct APState {
362 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 383 void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
363 if (value.is_boolean()) { 384 if (value.is_boolean()) {
364 data_storage[key] = value.get<bool>(); 385 data_storage[key] = value.get<bool>();
365 TrackerLog("Data storage " + key + " retrieved as " + 386 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
366 (value.get<bool>() ? "true" : "false")); 387 (value.get<bool>() ? "true" : "false")));
367 } else if (value.is_number()) { 388 } else if (value.is_number()) {
368 data_storage[key] = value.get<int>(); 389 data_storage[key] = value.get<int>();
369 TrackerLog("Data storage " + key + " retrieved as " + 390 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
370 std::to_string(value.get<int>())); 391 value.get<int>()));
371 } else if (value.is_object()) { 392 } else if (value.is_object()) {
372 if (key.ends_with("PlayerPos")) { 393 if (key.ends_with("PlayerPos")) {
373 auto map_value = value.get<std::map<std::string, int>>(); 394 auto map_value = value.get<std::map<std::string, int>>();
@@ -376,7 +397,7 @@ struct APState {
376 data_storage[key] = value.get<std::map<std::string, int>>(); 397 data_storage[key] = value.get<std::map<std::string, int>>();
377 } 398 }
378 399
379 TrackerLog("Data storage " + key + " retrieved as dictionary"); 400 TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key));
380 } else if (value.is_null()) { 401 } else if (value.is_null()) {
381 if (key.ends_with("PlayerPos")) { 402 if (key.ends_with("PlayerPos")) {
382 player_pos = std::nullopt; 403 player_pos = std::nullopt;
@@ -384,7 +405,19 @@ struct APState {
384 data_storage.erase(key); 405 data_storage.erase(key);
385 } 406 }
386 407
387 TrackerLog("Data storage " + key + " retrieved as null"); 408 TrackerLog(fmt::format("Data storage {} retrieved as null", key));
409 } else if (value.is_array()) {
410 auto list_value = value.get<std::vector<std::string>>();
411
412 if (key.ends_with("Paintings")) {
413 data_storage[key] =
414 std::set<std::string>(list_value.begin(), list_value.end());
415 } else {
416 data_storage[key] = list_value;
417 }
418
419 TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key,
420 hatkirby::implode(list_value, ", ")));
388 } 421 }
389 } 422 }
390 423
@@ -394,7 +427,7 @@ struct APState {
394 427
395 bool HasCheckedHuntPanel(int location_id) { 428 bool HasCheckedHuntPanel(int location_id) {
396 std::string key = 429 std::string key =
397 data_storage_prefix + "Hunt|" + std::to_string(location_id); 430 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
398 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 431 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
399 } 432 }
400 433
@@ -403,26 +436,51 @@ struct APState {
403 } 436 }
404 437
405 bool HasAchievement(const std::string& name) { 438 bool HasAchievement(const std::string& name) {
406 std::string key = data_storage_prefix + "Achievement|" + name; 439 std::string key =
440 fmt::format("{}Achievement|{}", data_storage_prefix, name);
407 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 441 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
408 } 442 }
409 443
410 void RefreshTracker() { 444 const std::set<std::string>& GetCheckedPaintings() {
445 std::string key = fmt::format("{}Paintings", data_storage_prefix);
446 if (!data_storage.count(key)) {
447 data_storage[key] = std::set<std::string>();
448 }
449
450 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
451 }
452
453 bool IsPaintingChecked(const std::string& painting_id) {
454 const auto& checked_paintings = GetCheckedPaintings();
455
456 return checked_paintings.count(painting_id) ||
457 (painting_mapping.count(painting_id) &&
458 checked_paintings.count(painting_mapping.at(painting_id)));
459 }
460
461 void RefreshTracker(bool reset) {
411 TrackerLog("Refreshing display..."); 462 TrackerLog("Refreshing display...");
412 463
413 RecalculateReachability(); 464 RecalculateReachability();
414 tracker_frame->UpdateIndicators(); 465
466 if (reset) {
467 tracker_frame->ResetIndicators();
468 } else {
469 tracker_frame->UpdateIndicators();
470 }
415 } 471 }
416 472
417 int64_t GetItemId(const std::string& item_name) { 473 int64_t GetItemId(const std::string& item_name) {
418 int64_t ap_id = apclient->get_item_id(item_name); 474 int64_t ap_id = apclient->get_item_id(item_name);
419 if (ap_id == APClient::INVALID_NAME_ID) { 475 if (ap_id == APClient::INVALID_NAME_ID) {
420 TrackerLog("Could not find AP item ID for " + item_name); 476 TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
421 } 477 }
422 478
423 return ap_id; 479 return ap_id;
424 } 480 }
425 481
482 std::string GetItemName(int id) { return apclient->get_item_name(id); }
483
426 bool HasReachedGoal() { 484 bool HasReachedGoal() {
427 return data_storage.count(victory_data_storage_key) && 485 return data_storage.count(victory_data_storage_key) &&
428 std::any_cast<int>(data_storage.at(victory_data_storage_key)) == 486 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
@@ -461,16 +519,32 @@ bool AP_HasItem(int item_id, int quantity) {
461 return GetState().HasItem(item_id, quantity); 519 return GetState().HasItem(item_id, quantity);
462} 520}
463 521
522std::string AP_GetItemName(int item_id) {
523 return GetState().GetItemName(item_id);
524}
525
464DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 526DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; }
465 527
466bool AP_IsColorShuffle() { return GetState().color_shuffle; } 528bool AP_IsColorShuffle() { return GetState().color_shuffle; }
467 529
468bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 530bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; }
469 531
470const std::map<std::string, std::string> AP_GetPaintingMapping() { 532const std::map<std::string, std::string>& AP_GetPaintingMapping() {
471 return GetState().painting_mapping; 533 return GetState().painting_mapping;
472} 534}
473 535
536bool AP_IsPaintingMappedTo(const std::string& painting_id) {
537 return GetState().painting_codomain.count(painting_id);
538}
539
540const std::set<std::string>& AP_GetCheckedPaintings() {
541 return GetState().GetCheckedPaintings();
542}
543
544bool AP_IsPaintingChecked(const std::string& painting_id) {
545 return GetState().IsPaintingChecked(painting_id);
546}
547
474int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 548int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }
475 549
476int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 550int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }
diff --git a/src/ap_state.h b/src/ap_state.h index 6667e0d..7af7395 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -3,6 +3,7 @@
3 3
4#include <map> 4#include <map>
5#include <optional> 5#include <optional>
6#include <set>
6#include <string> 7#include <string>
7#include <tuple> 8#include <tuple>
8 9
@@ -48,13 +49,21 @@ bool AP_HasCheckedHuntPanel(int location_id);
48 49
49bool AP_HasItem(int item_id, int quantity = 1); 50bool AP_HasItem(int item_id, int quantity = 1);
50 51
52std::string AP_GetItemName(int item_id);
53
51DoorShuffleMode AP_GetDoorShuffleMode(); 54DoorShuffleMode AP_GetDoorShuffleMode();
52 55
53bool AP_IsColorShuffle(); 56bool AP_IsColorShuffle();
54 57
55bool AP_IsPaintingShuffle(); 58bool AP_IsPaintingShuffle();
56 59
57const std::map<std::string, std::string> AP_GetPaintingMapping(); 60const std::map<std::string, std::string>& AP_GetPaintingMapping();
61
62bool AP_IsPaintingMappedTo(const std::string& painting_id);
63
64const std::set<std::string>& AP_GetCheckedPaintings();
65
66bool AP_IsPaintingChecked(const std::string& painting_id);
58 67
59int AP_GetMasteryRequirement(); 68int AP_GetMasteryRequirement();
60 69
diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 3b5d8d4..58d8897 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -1,5 +1,7 @@
1#include "area_popup.h" 1#include "area_popup.h"
2 2
3#include <wx/dcbuffer.h>
4
3#include "ap_state.h" 5#include "ap_state.h"
4#include "game_data.h" 6#include "game_data.h"
5#include "global.h" 7#include "global.h"
@@ -8,6 +10,8 @@
8 10
9AreaPopup::AreaPopup(wxWindow* parent, int area_id) 11AreaPopup::AreaPopup(wxWindow* parent, int area_id)
10 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 12 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
13 SetBackgroundStyle(wxBG_STYLE_PAINT);
14
11 unchecked_eye_ = 15 unchecked_eye_ =
12 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), 16 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
13 wxBITMAP_TYPE_PNG) 17 wxBITMAP_TYPE_PNG)
@@ -61,6 +65,19 @@ void AreaPopup::UpdateIndicators() {
61 } 65 }
62 } 66 }
63 67
68 if (AP_IsPaintingShuffle()) {
69 for (int painting_id : map_area.paintings) {
70 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
71 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name.
72 int item_height = std::max(32, item_extent.GetHeight()) + 10;
73 acc_height += item_height;
74
75 if (item_extent.GetWidth() > col_width) {
76 col_width = item_extent.GetWidth();
77 }
78 }
79 }
80
64 int item_width = col_width + 10 + 32; 81 int item_width = col_width + 10 + 32;
65 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 82 int full_width = std::max(header_extent.GetWidth(), item_width) + 20;
66 83
@@ -105,10 +122,31 @@ void AreaPopup::UpdateIndicators() {
105 122
106 cur_height += 10 + 32; 123 cur_height += 10 + 32;
107 } 124 }
125
126 if (AP_IsPaintingShuffle()) {
127 for (int painting_id : map_area.paintings) {
128 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
129 bool checked = AP_IsPaintingChecked(painting.internal_id);
130 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
131
132 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height});
133
134 bool reachable = IsPaintingReachable(painting_id);
135 const wxColour* text_color = reachable ? wxWHITE : wxRED;
136 mem_dc.SetTextForeground(*text_color);
137
138 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name.
139 mem_dc.DrawText(painting.internal_id,
140 {10 + 32 + 10,
141 cur_height + (32 - mem_dc.GetFontMetrics().height) / 2});
142
143 cur_height += 10 + 32;
144 }
145 }
108} 146}
109 147
110void AreaPopup::OnPaint(wxPaintEvent& event) { 148void AreaPopup::OnPaint(wxPaintEvent& event) {
111 wxPaintDC dc(this); 149 wxBufferedPaintDC dc(this);
112 PrepareDC(dc); 150 PrepareDC(dc);
113 dc.DrawBitmap(rendered_, 0, 0); 151 dc.DrawBitmap(rendered_, 0, 0);
114 152
diff --git a/src/game_data.cpp b/src/game_data.cpp index bc4f41b..e75170e 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -1,5 +1,6 @@
1#include "game_data.h" 1#include "game_data.h"
2 2
3#include <fmt/core.h>
3#include <hkutil/string.h> 4#include <hkutil/string.h>
4#include <yaml-cpp/yaml.h> 5#include <yaml-cpp/yaml.h>
5 6
@@ -31,9 +32,7 @@ LingoColor GetColorForString(const std::string &str) {
31 } else if (str == "purple") { 32 } else if (str == "purple") {
32 return LingoColor::kPurple; 33 return LingoColor::kPurple;
33 } else { 34 } else {
34 std::ostringstream errmsg; 35 TrackerLog(fmt::format("Invalid color: {}", str));
35 errmsg << "Invalid color: " << str;
36 TrackerLog(errmsg.str());
37 36
38 return LingoColor::kNone; 37 return LingoColor::kNone;
39 } 38 }
@@ -44,11 +43,14 @@ struct GameData {
44 std::vector<Door> doors_; 43 std::vector<Door> doors_;
45 std::vector<Panel> panels_; 44 std::vector<Panel> panels_;
46 std::vector<MapArea> map_areas_; 45 std::vector<MapArea> map_areas_;
46 std::vector<SubwayItem> subway_items_;
47 std::vector<PaintingExit> paintings_;
47 48
48 std::map<std::string, int> room_by_id_; 49 std::map<std::string, int> room_by_id_;
49 std::map<std::string, int> door_by_id_; 50 std::map<std::string, int> door_by_id_;
50 std::map<std::string, int> panel_by_id_; 51 std::map<std::string, int> panel_by_id_;
51 std::map<std::string, int> area_by_id_; 52 std::map<std::string, int> area_by_id_;
53 std::map<std::string, int> painting_by_id_;
52 54
53 std::vector<int> door_definition_order_; 55 std::vector<int> door_definition_order_;
54 56
@@ -61,6 +63,9 @@ struct GameData {
61 63
62 std::vector<int> sunwarp_doors_; 64 std::vector<int> sunwarp_doors_;
63 65
66 std::map<std::string, int> subway_item_by_painting_;
67 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
68
64 bool loaded_area_data_ = false; 69 bool loaded_area_data_ = false;
65 std::set<std::string> malconfigured_areas_; 70 std::set<std::string> malconfigured_areas_;
66 71
@@ -79,9 +84,7 @@ struct GameData {
79 ap_id_by_color_[GetColorForString(input_name)] = 84 ap_id_by_color_[GetColorForString(input_name)] =
80 ids_config["special_items"][color_name].as<int>(); 85 ids_config["special_items"][color_name].as<int>();
81 } else { 86 } else {
82 std::ostringstream errmsg; 87 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
83 errmsg << "Missing AP item ID for color " << color_name;
84 TrackerLog(errmsg.str());
85 } 88 }
86 }; 89 };
87 90
@@ -156,8 +159,10 @@ struct GameData {
156 } 159 }
157 default: { 160 default: {
158 // This shouldn't happen. 161 // This shouldn't happen.
159 std::cout << "Error reading game data: " << entrance_it 162 std::ostringstream formatted;
160 << std::endl; 163 formatted << entrance_it;
164 TrackerLog(
165 fmt::format("Error reading game data: {}", formatted.str()));
161 break; 166 break;
162 } 167 }
163 } 168 }
@@ -282,10 +287,9 @@ struct GameData {
282 [panels_[panel_id].name] 287 [panels_[panel_id].name]
283 .as<int>(); 288 .as<int>();
284 } else { 289 } else {
285 std::ostringstream errmsg; 290 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
286 errmsg << "Missing AP location ID for panel " 291 rooms_[room_id].name,
287 << rooms_[room_id].name << " - " << panels_[panel_id].name; 292 panels_[panel_id].name));
288 TrackerLog(errmsg.str());
289 } 293 }
290 } 294 }
291 } 295 }
@@ -348,10 +352,9 @@ struct GameData {
348 [doors_[door_id].name]["item"] 352 [doors_[door_id].name]["item"]
349 .as<int>(); 353 .as<int>();
350 } else { 354 } else {
351 std::ostringstream errmsg; 355 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
352 errmsg << "Missing AP item ID for door " << rooms_[room_id].name 356 rooms_[room_id].name,
353 << " - " << doors_[door_id].name; 357 doors_[door_id].name));
354 TrackerLog(errmsg.str());
355 } 358 }
356 } 359 }
357 360
@@ -365,10 +368,8 @@ struct GameData {
365 ids_config["door_groups"][doors_[door_id].group_name] 368 ids_config["door_groups"][doors_[door_id].group_name]
366 .as<int>(); 369 .as<int>();
367 } else { 370 } else {
368 std::ostringstream errmsg; 371 TrackerLog(fmt::format("Missing AP item ID for door group {}",
369 errmsg << "Missing AP item ID for door group " 372 doors_[door_id].group_name));
370 << doors_[door_id].group_name;
371 TrackerLog(errmsg.str());
372 } 373 }
373 } 374 }
374 375
@@ -378,13 +379,11 @@ struct GameData {
378 } else if (!door_it.second["skip_location"] && 379 } else if (!door_it.second["skip_location"] &&
379 !door_it.second["event"]) { 380 !door_it.second["event"]) {
380 if (has_external_panels) { 381 if (has_external_panels) {
381 std::ostringstream errmsg; 382 TrackerLog(fmt::format(
382 errmsg 383 "{} - {} has panels from other rooms but does not have an "
383 << rooms_[room_id].name << " - " << doors_[door_id].name 384 "explicit location name and is not marked skip_location or "
384 << " has panels from other rooms but does not have an " 385 "event",
385 "explicit " 386 rooms_[room_id].name, doors_[door_id].name));
386 "location name and is not marked skip_location or event";
387 TrackerLog(errmsg.str());
388 } 387 }
389 388
390 doors_[door_id].location_name = 389 doors_[door_id].location_name =
@@ -404,10 +403,9 @@ struct GameData {
404 [doors_[door_id].name]["location"] 403 [doors_[door_id].name]["location"]
405 .as<int>(); 404 .as<int>();
406 } else { 405 } else {
407 std::ostringstream errmsg; 406 TrackerLog(fmt::format("Missing AP location ID for door {} - {}",
408 errmsg << "Missing AP location ID for door " 407 rooms_[room_id].name,
409 << rooms_[room_id].name << " - " << doors_[door_id].name; 408 doors_[door_id].name));
410 TrackerLog(errmsg.str());
411 } 409 }
412 } 410 }
413 411
@@ -428,12 +426,14 @@ struct GameData {
428 426
429 if (room_it.second["paintings"]) { 427 if (room_it.second["paintings"]) {
430 for (const auto &painting : room_it.second["paintings"]) { 428 for (const auto &painting : room_it.second["paintings"]) {
431 std::string painting_id = painting["id"].as<std::string>(); 429 std::string internal_id = painting["id"].as<std::string>();
432 room_by_painting_[painting_id] = room_id; 430 int painting_id = AddOrGetPainting(internal_id);
431 PaintingExit &painting_exit = paintings_[painting_id];
432 painting_exit.room = room_id;
433 433
434 if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { 434 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
435 PaintingExit painting_exit; 435 (!painting["disable"] || !painting["disable"].as<bool>())) {
436 painting_exit.id = painting_id; 436 painting_exit.entrance = true;
437 437
438 if (painting["required_door"]) { 438 if (painting["required_door"]) {
439 std::string rd_room = rooms_[room_id].name; 439 std::string rd_room = rooms_[room_id].name;
@@ -444,9 +444,9 @@ struct GameData {
444 painting_exit.door = AddOrGetDoor( 444 painting_exit.door = AddOrGetDoor(
445 rd_room, painting["required_door"]["door"].as<std::string>()); 445 rd_room, painting["required_door"]["door"].as<std::string>());
446 } 446 }
447
448 rooms_[room_id].paintings.push_back(painting_exit);
449 } 447 }
448
449 rooms_[room_id].paintings.push_back(painting_exit.id);
450 } 450 }
451 } 451 }
452 452
@@ -473,10 +473,8 @@ struct GameData {
473 progressive_item_id = 473 progressive_item_id =
474 ids_config["progression"][progressive_item_name].as<int>(); 474 ids_config["progression"][progressive_item_name].as<int>();
475 } else { 475 } else {
476 std::ostringstream errmsg; 476 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
477 errmsg << "Missing AP item ID for progressive item " 477 progressive_item_name));
478 << progressive_item_name;
479 TrackerLog(errmsg.str());
480 } 478 }
481 479
482 int index = 1; 480 int index = 1;
@@ -559,14 +557,13 @@ struct GameData {
559 int area_id = AddOrGetArea(area_name); 557 int area_id = AddOrGetArea(area_name);
560 MapArea &map_area = map_areas_[area_id]; 558 MapArea &map_area = map_areas_[area_id];
561 // room field should be the original room ID 559 // room field should be the original room ID
562 map_area.locations.push_back( 560 map_area.locations.push_back({.name = section_name,
563 {.name = section_name, 561 .ap_location_name = location_name,
564 .ap_location_name = location_name, 562 .ap_location_id = panel.ap_location_id,
565 .ap_location_id = panel.ap_location_id, 563 .room = panel.room,
566 .room = panel.room, 564 .panels = {panel.id},
567 .panels = {panel.id}, 565 .classification = classification,
568 .classification = classification, 566 .hunt = panel.hunt});
569 .hunt = panel.hunt});
570 locations_by_name[location_name] = {area_id, 567 locations_by_name[location_name] = {area_id,
571 map_area.locations.size() - 1}; 568 map_area.locations.size() - 1};
572 } 569 }
@@ -622,11 +619,101 @@ struct GameData {
622 } 619 }
623 } 620 }
624 621
622 for (const Room &room : rooms_) {
623 std::string area_name = room.name;
624 if (fold_areas.count(room.name)) {
625 int fold_area_id = fold_areas[room.name];
626 area_name = map_areas_[fold_area_id].name;
627 }
628
629 if (!room.paintings.empty()) {
630 int area_id = AddOrGetArea(area_name);
631 MapArea &map_area = map_areas_[area_id];
632
633 for (int painting_id : room.paintings) {
634 const PaintingExit &painting_obj = paintings_.at(painting_id);
635 if (painting_obj.entrance) {
636 map_area.paintings.push_back(painting_id);
637 }
638 }
639 }
640 }
641
625 // Report errors. 642 // Report errors.
626 for (const std::string &area : malconfigured_areas_) { 643 for (const std::string &area : malconfigured_areas_) {
627 std::ostringstream errstr; 644 TrackerLog(fmt::format("Area data not found for: {}", area));
628 errstr << "Area data not found for: " << area; 645 }
629 TrackerLog(errstr.str()); 646
647 // Read in subway items.
648 YAML::Node subway_config =
649 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml"));
650 for (const auto &subway_it : subway_config) {
651 SubwayItem subway_item;
652 subway_item.id = subway_items_.size();
653 subway_item.x = subway_it["pos"][0].as<int>();
654 subway_item.y = subway_it["pos"][1].as<int>();
655
656 if (subway_it["door"]) {
657 subway_item.door = AddOrGetDoor(subway_it["room"].as<std::string>(),
658 subway_it["door"].as<std::string>());
659 }
660
661 if (subway_it["paintings"]) {
662 for (const auto &painting_it : subway_it["paintings"]) {
663 std::string painting_id = painting_it.as<std::string>();
664
665 subway_item.paintings.push_back(painting_id);
666 subway_item_by_painting_[painting_id] = subway_item.id;
667 }
668 }
669
670 if (subway_it["tags"]) {
671 for (const auto &tag_it : subway_it["tags"]) {
672 subway_item.tags.push_back(tag_it.as<std::string>());
673 }
674 }
675
676 if (subway_it["sunwarp"]) {
677 SubwaySunwarp sunwarp;
678 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
679
680 std::string sunwarp_type =
681 subway_it["sunwarp"]["type"].as<std::string>();
682 if (sunwarp_type == "final") {
683 sunwarp.type = SubwaySunwarpType::kFinal;
684 } else if (sunwarp_type == "exit") {
685 sunwarp.type = SubwaySunwarpType::kExit;
686 } else {
687 sunwarp.type = SubwaySunwarpType::kEnter;
688 }
689
690 subway_item.sunwarp = sunwarp;
691
692 subway_item_by_sunwarp_[sunwarp] = subway_item.id;
693
694 subway_item.door =
695 AddOrGetDoor("Sunwarps", std::to_string(sunwarp.dots) + " Sunwarp");
696 }
697
698 if (subway_it["special"]) {
699 subway_item.special = subway_it["special"].as<std::string>();
700 }
701
702 subway_items_.push_back(subway_item);
703 }
704
705 // Find singleton subway tags.
706 std::map<std::string, std::set<int>> subway_tags;
707 for (const SubwayItem &subway_item : subway_items_) {
708 for (const std::string &tag : subway_item.tags) {
709 subway_tags[tag].insert(subway_item.id);
710 }
711 }
712
713 for (const auto &[tag, items] : subway_tags) {
714 if (items.size() == 1) {
715 TrackerLog(fmt::format("Singleton subway item tag: {}", tag));
716 }
630 } 717 }
631 } 718 }
632 719
@@ -643,8 +730,10 @@ struct GameData {
643 std::string full_name = room + " - " + door; 730 std::string full_name = room + " - " + door;
644 731
645 if (!door_by_id_.count(full_name)) { 732 if (!door_by_id_.count(full_name)) {
733 int door_id = doors_.size();
646 door_by_id_[full_name] = doors_.size(); 734 door_by_id_[full_name] = doors_.size();
647 doors_.push_back({.room = AddOrGetRoom(room), .name = door}); 735 doors_.push_back(
736 {.id = door_id, .room = AddOrGetRoom(room), .name = door});
648 } 737 }
649 738
650 return door_by_id_[full_name]; 739 return door_by_id_[full_name];
@@ -676,6 +765,16 @@ struct GameData {
676 765
677 return area_by_id_[area]; 766 return area_by_id_[area];
678 } 767 }
768
769 int AddOrGetPainting(std::string internal_id) {
770 if (!painting_by_id_.count(internal_id)) {
771 int painting_id = paintings_.size();
772 painting_by_id_[internal_id] = painting_id;
773 paintings_.push_back({.id = painting_id, .internal_id = internal_id});
774 }
775
776 return painting_by_id_[internal_id];
777 }
679}; 778};
680 779
681GameData &GetState() { 780GameData &GetState() {
@@ -685,6 +784,10 @@ GameData &GetState() {
685 784
686} // namespace 785} // namespace
687 786
787bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
788 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
789}
790
688const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; } 791const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; }
689 792
690const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); } 793const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); }
@@ -707,8 +810,12 @@ const Panel &GD_GetPanel(int panel_id) {
707 return GetState().panels_.at(panel_id); 810 return GetState().panels_.at(panel_id);
708} 811}
709 812
710int GD_GetRoomForPainting(const std::string &painting_id) { 813const PaintingExit &GD_GetPaintingExit(int painting_id) {
711 return GetState().room_by_painting_.at(painting_id); 814 return GetState().paintings_.at(painting_id);
815}
816
817int GD_GetPaintingByName(const std::string &name) {
818 return GetState().painting_by_id_.at(name);
712} 819}
713 820
714const std::vector<int> &GD_GetAchievementPanels() { 821const std::vector<int> &GD_GetAchievementPanels() {
@@ -726,3 +833,22 @@ const std::vector<int> &GD_GetSunwarpDoors() {
726int GD_GetRoomForSunwarp(int index) { 833int GD_GetRoomForSunwarp(int index) {
727 return GetState().room_by_sunwarp_.at(index); 834 return GetState().room_by_sunwarp_.at(index);
728} 835}
836
837const std::vector<SubwayItem> &GD_GetSubwayItems() {
838 return GetState().subway_items_;
839}
840
841const SubwayItem &GD_GetSubwayItem(int id) {
842 return GetState().subway_items_.at(id);
843}
844
845std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
846 if (GetState().subway_item_by_painting_.count(painting_id)) {
847 return GetState().subway_item_by_painting_.at(painting_id);
848 }
849 return std::nullopt;
850}
851
852int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
853 return GetState().subway_item_by_sunwarp_.at(sunwarp);
854}
diff --git a/src/game_data.h b/src/game_data.h index e30675a..b787e6f 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -64,6 +64,7 @@ struct ProgressiveRequirement {
64}; 64};
65 65
66struct Door { 66struct Door {
67 int id;
67 int room; 68 int room;
68 std::string name; 69 std::string name;
69 std::string location_name; 70 std::string location_name;
@@ -88,14 +89,17 @@ struct Exit {
88}; 89};
89 90
90struct PaintingExit { 91struct PaintingExit {
91 std::string id; 92 int id;
93 int room;
94 std::string internal_id;
92 std::optional<int> door; 95 std::optional<int> door;
96 bool entrance = false;
93}; 97};
94 98
95struct Room { 99struct Room {
96 std::string name; 100 std::string name;
97 std::vector<Exit> exits; 101 std::vector<Exit> exits;
98 std::vector<PaintingExit> paintings; 102 std::vector<int> paintings;
99 std::vector<int> sunwarps; 103 std::vector<int> sunwarps;
100 std::vector<int> panels; 104 std::vector<int> panels;
101}; 105};
@@ -114,12 +118,37 @@ struct MapArea {
114 int id; 118 int id;
115 std::string name; 119 std::string name;
116 std::vector<Location> locations; 120 std::vector<Location> locations;
121 std::vector<int> paintings;
117 int map_x; 122 int map_x;
118 int map_y; 123 int map_y;
119 int classification = 0; 124 int classification = 0;
120 bool hunt = false; 125 bool hunt = false;
121}; 126};
122 127
128enum class SubwaySunwarpType {
129 kEnter,
130 kExit,
131 kFinal
132};
133
134struct SubwaySunwarp {
135 int dots;
136 SubwaySunwarpType type;
137
138 bool operator<(const SubwaySunwarp& rhs) const;
139};
140
141struct SubwayItem {
142 int id;
143 int x;
144 int y;
145 std::optional<int> door;
146 std::vector<std::string> paintings;
147 std::vector<std::string> tags;
148 std::optional<SubwaySunwarp> sunwarp;
149 std::optional<std::string> special;
150};
151
123const std::vector<MapArea>& GD_GetMapAreas(); 152const std::vector<MapArea>& GD_GetMapAreas();
124const MapArea& GD_GetMapArea(int id); 153const MapArea& GD_GetMapArea(int id);
125int GD_GetRoomByName(const std::string& name); 154int GD_GetRoomByName(const std::string& name);
@@ -128,10 +157,15 @@ const std::vector<Door>& GD_GetDoors();
128const Door& GD_GetDoor(int door_id); 157const Door& GD_GetDoor(int door_id);
129int GD_GetDoorByName(const std::string& name); 158int GD_GetDoorByName(const std::string& name);
130const Panel& GD_GetPanel(int panel_id); 159const Panel& GD_GetPanel(int panel_id);
131int GD_GetRoomForPainting(const std::string& painting_id); 160const PaintingExit& GD_GetPaintingExit(int painting_id);
161int GD_GetPaintingByName(const std::string& name);
132const std::vector<int>& GD_GetAchievementPanels(); 162const std::vector<int>& GD_GetAchievementPanels();
133int GD_GetItemIdForColor(LingoColor color); 163int GD_GetItemIdForColor(LingoColor color);
134const std::vector<int>& GD_GetSunwarpDoors(); 164const std::vector<int>& GD_GetSunwarpDoors();
135int GD_GetRoomForSunwarp(int index); 165int GD_GetRoomForSunwarp(int index);
166const std::vector<SubwayItem>& GD_GetSubwayItems();
167const SubwayItem& GD_GetSubwayItem(int id);
168std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
169int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
136 170
137#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 171#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/logger.cpp b/src/logger.cpp index 4b722c8..09fc331 100644 --- a/src/logger.cpp +++ b/src/logger.cpp
@@ -26,7 +26,7 @@ class Logger {
26 26
27} // namespace 27} // namespace
28 28
29void TrackerLog(const std::string& text) { 29void TrackerLog(std::string text) {
30 static Logger* instance = new Logger(); 30 static Logger* instance = new Logger();
31 instance->LogLine(text); 31 instance->LogLine(text);
32} 32}
diff --git a/src/logger.h b/src/logger.h index db9bb49..a27839f 100644 --- a/src/logger.h +++ b/src/logger.h
@@ -1,8 +1,8 @@
1#ifndef LOGGER_H_6E7B9594 1#ifndef LOGGER_H_9BDD07EA
2#define LOGGER_H_6E7B9594 2#define LOGGER_H_9BDD07EA
3 3
4#include <string> 4#include <string>
5 5
6void TrackerLog(const std::string& text); 6void TrackerLog(std::string message);
7 7
8#endif /* end of include guard: LOGGER_H_6E7B9594 */ 8#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index fe9aceb..1d7cc9e 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -4,6 +4,7 @@
4#include <wx/wx.h> 4#include <wx/wx.h>
5#endif 5#endif
6 6
7#include "global.h"
7#include "tracker_config.h" 8#include "tracker_config.h"
8#include "tracker_frame.h" 9#include "tracker_frame.h"
9 10
@@ -16,6 +17,16 @@ class TrackerApp : public wxApp {
16 frame->Show(true); 17 frame->Show(true);
17 return true; 18 return true;
18 } 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 }
19}; 30};
20 31
21wxIMPLEMENT_APP(TrackerApp); 32wxIMPLEMENT_APP(TrackerApp);
diff --git a/src/network_set.cpp b/src/network_set.cpp new file mode 100644 index 0000000..6d2a098 --- /dev/null +++ b/src/network_set.cpp
@@ -0,0 +1,30 @@
1#include "network_set.h"
2
3void NetworkSet::Clear() {
4 network_by_item_.clear();
5}
6
7void NetworkSet::AddLink(int id1, int id2) {
8 if (id2 > id1) {
9 // Make sure id1 < id2
10 std::swap(id1, id2);
11 }
12
13 if (!network_by_item_.count(id1)) {
14 network_by_item_[id1] = {};
15 }
16 if (!network_by_item_.count(id2)) {
17 network_by_item_[id2] = {};
18 }
19
20 network_by_item_[id1].insert({id1, id2});
21 network_by_item_[id2].insert({id1, id2});
22}
23
24bool NetworkSet::IsItemInNetwork(int id) const {
25 return network_by_item_.count(id);
26}
27
28const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const {
29 return network_by_item_.at(id);
30}
diff --git a/src/network_set.h b/src/network_set.h new file mode 100644 index 0000000..e6f0c07 --- /dev/null +++ b/src/network_set.h
@@ -0,0 +1,25 @@
1#ifndef NETWORK_SET_H_3036B8E3
2#define NETWORK_SET_H_3036B8E3
3
4#include <map>
5#include <optional>
6#include <set>
7#include <utility>
8#include <vector>
9
10class NetworkSet {
11 public:
12 void Clear();
13
14 void AddLink(int id1, int id2);
15
16 bool IsItemInNetwork(int id) const;
17
18 const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const;
19
20 private:
21
22 std::map<int, std::set<std::pair<int, int>>> network_by_item_;
23};
24
25#endif /* end of include guard: NETWORK_SET_H_3036B8E3 */
diff --git a/src/subway_map.cpp b/src/subway_map.cpp new file mode 100644 index 0000000..e3b844d --- /dev/null +++ b/src/subway_map.cpp
@@ -0,0 +1,724 @@
1#include "subway_map.h"
2
3#include <fmt/core.h>
4#include <wx/dcbuffer.h>
5#include <wx/dcgraph.h>
6
7#include <sstream>
8
9#include "ap_state.h"
10#include "game_data.h"
11#include "global.h"
12#include "tracker_state.h"
13
14constexpr int AREA_ACTUAL_SIZE = 21;
15constexpr int OWL_ACTUAL_SIZE = 32;
16
17enum class ItemDrawType { kNone, kBox, kOwl };
18
19SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
20 SetBackgroundStyle(wxBG_STYLE_PAINT);
21
22 map_image_ =
23 wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG);
24 if (!map_image_.IsOk()) {
25 return;
26 }
27
28 owl_image_ =
29 wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG);
30 if (!owl_image_.IsOk()) {
31 return;
32 }
33
34 unchecked_eye_ =
35 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
36 wxBITMAP_TYPE_PNG)
37 .Scale(32, 32));
38 checked_eye_ = wxBitmap(
39 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
40 .Scale(32, 32));
41
42 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
43 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
44 static_cast<float>(map_image_.GetHeight())});
45 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
46 tree_->add(subway_item.id);
47 }
48
49 Redraw();
50
51 scroll_timer_ = new wxTimer(this);
52
53 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
54 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
55 Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
56 Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
57 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
58 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
59
60 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15});
61 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
62
63 help_button_ = new wxButton(this, wxID_ANY, "Help");
64 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
65 SetUpHelpButton();
66}
67
68void SubwayMap::OnConnect() {
69 networks_.Clear();
70
71 std::map<std::string, std::vector<int>> tagged;
72 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
73 if (AP_HasEarlyColorHallways() &&
74 subway_item.special == "starting_room_paintings") {
75 tagged["early_ch"].push_back(subway_item.id);
76 }
77
78 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) {
79 continue;
80 }
81
82 for (const std::string &tag : subway_item.tags) {
83 tagged[tag].push_back(subway_item.id);
84 }
85
86 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp &&
87 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
88 std::string tag = fmt::format("subway{}", subway_item.sunwarp->dots);
89 tagged[tag].push_back(subway_item.id);
90 }
91
92 if (!AP_IsPilgrimageEnabled() &&
93 (subway_item.special == "sun_painting" ||
94 subway_item.special == "sun_painting_exit")) {
95 tagged["sun_painting"].push_back(subway_item.id);
96 }
97 }
98
99 if (AP_IsSunwarpShuffle()) {
100 for (const auto &[index, mapping] : AP_GetSunwarpMapping()) {
101 std::string tag = fmt::format("sunwarp{}", mapping.dots);
102
103 SubwaySunwarp fromWarp;
104 if (index < 6) {
105 fromWarp.dots = index + 1;
106 fromWarp.type = SubwaySunwarpType::kEnter;
107 } else {
108 fromWarp.dots = index - 5;
109 fromWarp.type = SubwaySunwarpType::kExit;
110 }
111
112 SubwaySunwarp toWarp;
113 if (mapping.exit_index < 6) {
114 toWarp.dots = mapping.exit_index + 1;
115 toWarp.type = SubwaySunwarpType::kEnter;
116 } else {
117 toWarp.dots = mapping.exit_index - 5;
118 toWarp.type = SubwaySunwarpType::kExit;
119 }
120
121 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
122 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
123 }
124 }
125
126 for (const auto &[tag, items] : tagged) {
127 // Pairwise connect all items with the same tag.
128 for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end();
129 tag_it1++) {
130 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
131 tag_it2++) {
132 networks_.AddLink(*tag_it1, *tag_it2);
133 }
134 }
135 }
136
137 checked_paintings_.clear();
138}
139
140void SubwayMap::UpdateIndicators() {
141 if (AP_IsPaintingShuffle()) {
142 for (const std::string &painting_id : AP_GetCheckedPaintings()) {
143 if (!checked_paintings_.count(painting_id)) {
144 checked_paintings_.insert(painting_id);
145
146 if (AP_GetPaintingMapping().count(painting_id)) {
147 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
148 std::optional<int> to_id = GD_GetSubwayItemForPainting(
149 AP_GetPaintingMapping().at(painting_id));
150
151 if (from_id && to_id) {
152 networks_.AddLink(*from_id, *to_id);
153 }
154 }
155 }
156 }
157 }
158
159 Redraw();
160}
161
162void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
163 SubwaySunwarp to_sunwarp) {
164 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
165 GD_GetSubwayItemForSunwarp(to_sunwarp));
166}
167
168void SubwayMap::Zoom(bool in) {
169 wxPoint focus_point;
170
171 if (mouse_position_) {
172 focus_point = *mouse_position_;
173 } else {
174 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
175 }
176
177 if (in) {
178 if (zoom_ < 3.0) {
179 SetZoom(zoom_ + 0.25, focus_point);
180 }
181 } else {
182 if (zoom_ > 1.0) {
183 SetZoom(zoom_ - 0.25, focus_point);
184 }
185 }
186}
187
188void SubwayMap::OnPaint(wxPaintEvent &event) {
189 if (GetSize() != rendered_.GetSize()) {
190 wxSize panel_size = GetSize();
191 wxSize image_size = map_image_.GetSize();
192
193 render_x_ = 0;
194 render_y_ = 0;
195 render_width_ = panel_size.GetWidth();
196 render_height_ = panel_size.GetHeight();
197
198 if (image_size.GetWidth() * panel_size.GetHeight() >
199 panel_size.GetWidth() * image_size.GetHeight()) {
200 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
201 image_size.GetWidth();
202 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
203 } else {
204 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
205 image_size.GetHeight();
206 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
207 }
208
209 SetZoomPos({zoom_x_, zoom_y_});
210
211 SetUpHelpButton();
212 }
213
214 wxBufferedPaintDC dc(this);
215 dc.SetBackground(*wxWHITE_BRUSH);
216 dc.Clear();
217
218 {
219 wxMemoryDC rendered_dc;
220 rendered_dc.SelectObject(rendered_);
221
222 int dst_x;
223 int dst_y;
224 int dst_w;
225 int dst_h;
226 int src_x;
227 int src_y;
228 int src_w;
229 int src_h;
230
231 int zoomed_width = render_width_ * zoom_;
232 int zoomed_height = render_height_ * zoom_;
233
234 if (zoomed_width <= GetSize().GetWidth()) {
235 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
236 dst_w = zoomed_width;
237 src_x = 0;
238 src_w = map_image_.GetWidth();
239 } else {
240 dst_x = 0;
241 dst_w = GetSize().GetWidth();
242 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
243 src_w =
244 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
245 }
246
247 if (zoomed_height <= GetSize().GetHeight()) {
248 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
249 dst_h = zoomed_height;
250 src_y = 0;
251 src_h = map_image_.GetHeight();
252 } else {
253 dst_y = 0;
254 dst_h = GetSize().GetHeight();
255 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
256 src_h =
257 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
258 }
259
260 wxGCDC gcdc(dc);
261 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
262 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
263 src_w, src_h);
264 }
265
266 if (hovered_item_) {
267 // Note that these requirements are duplicated on OnMouseClick so that it
268 // knows when an item has a hover effect.
269 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
270 if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) {
271 const std::map<std::string, bool> &report =
272 GetDoorRequirements(*subway_item.door);
273
274 int acc_height = 10;
275 int col_width = 0;
276
277 for (const auto &[text, obtained] : report) {
278 wxSize item_extent = dc.GetTextExtent(text);
279 int item_height = std::max(32, item_extent.GetHeight()) + 10;
280 acc_height += item_height;
281
282 if (item_extent.GetWidth() > col_width) {
283 col_width = item_extent.GetWidth();
284 }
285 }
286
287 int item_width = col_width + 10 + 32;
288 int full_width = item_width + 20;
289
290 wxPoint popup_pos =
291 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
292 subway_item.y + AREA_ACTUAL_SIZE / 2});
293
294 if (popup_pos.x + full_width > GetSize().GetWidth()) {
295 popup_pos.x = GetSize().GetWidth() - full_width;
296 }
297 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
298 popup_pos.y = GetSize().GetHeight() - acc_height;
299 }
300
301 dc.SetPen(*wxTRANSPARENT_PEN);
302 dc.SetBrush(*wxBLACK_BRUSH);
303 dc.DrawRectangle(popup_pos, {full_width, acc_height});
304
305 dc.SetFont(GetFont());
306
307 int cur_height = 10;
308
309 for (const auto &[text, obtained] : report) {
310 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
311
312 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
313
314 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
315 wxSize item_extent = dc.GetTextExtent(text);
316 dc.DrawText(
317 text,
318 popup_pos +
319 wxPoint{10 + 32 + 10,
320 cur_height + (32 - dc.GetFontMetrics().height) / 2});
321
322 cur_height += 10 + 32;
323 }
324 }
325
326 if (networks_.IsItemInNetwork(*hovered_item_)) {
327 dc.SetBrush(*wxTRANSPARENT_BRUSH);
328
329 for (const auto &[item_id1, item_id2] :
330 networks_.GetNetworkGraph(*hovered_item_)) {
331 const SubwayItem &item1 = GD_GetSubwayItem(item_id1);
332 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
333
334 wxPoint item1_pos = MapPosToRenderPos(
335 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
336 wxPoint item2_pos = MapPosToRenderPos(
337 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
338
339 int left = std::min(item1_pos.x, item2_pos.x);
340 int top = std::min(item1_pos.y, item2_pos.y);
341 int right = std::max(item1_pos.x, item2_pos.x);
342 int bottom = std::max(item1_pos.y, item2_pos.y);
343
344 int halfwidth = right - left;
345 int halfheight = bottom - top;
346
347 if (halfwidth < 4 || halfheight < 4) {
348 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
349 dc.DrawLine(item1_pos, item2_pos);
350 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
351 dc.DrawLine(item1_pos, item2_pos);
352 } else {
353 int ellipse_x;
354 int ellipse_y;
355 double start;
356 double end;
357
358 if (item1_pos.x > item2_pos.x) {
359 ellipse_y = top;
360
361 if (item1_pos.y > item2_pos.y) {
362 ellipse_x = left - halfwidth;
363
364 start = 0;
365 end = 90;
366 } else {
367 ellipse_x = left;
368
369 start = 90;
370 end = 180;
371 }
372 } else {
373 ellipse_y = top - halfheight;
374
375 if (item1_pos.y > item2_pos.y) {
376 ellipse_x = left - halfwidth;
377
378 start = 270;
379 end = 360;
380 } else {
381 ellipse_x = left;
382
383 start = 180;
384 end = 270;
385 }
386 }
387
388 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
389 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
390 halfheight * 2, start, end);
391 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
392 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
393 halfheight * 2, start, end);
394 }
395 }
396 }
397 }
398
399 event.Skip();
400}
401
402void SubwayMap::OnMouseMove(wxMouseEvent &event) {
403 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
404
405 std::vector<int> hovered = tree_->query(
406 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
407 if (!hovered.empty()) {
408 actual_hover_ = hovered[0];
409 } else {
410 actual_hover_ = std::nullopt;
411 }
412
413 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
414 hovered_item_ = actual_hover_;
415
416 Refresh();
417 }
418
419 if (scroll_mode_) {
420 EvaluateScroll(event.GetPosition());
421 }
422
423 mouse_position_ = event.GetPosition();
424
425 event.Skip();
426}
427
428void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
429 double new_zoom = zoom_;
430 if (event.GetWheelRotation() > 0) {
431 new_zoom = std::min(3.0, zoom_ + 0.25);
432 } else {
433 new_zoom = std::max(1.0, zoom_ - 0.25);
434 }
435
436 if (zoom_ != new_zoom) {
437 SetZoom(new_zoom, event.GetPosition());
438 }
439
440 event.Skip();
441}
442
443void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
444 SetScrollSpeed(0, 0);
445 mouse_position_ = std::nullopt;
446}
447
448void SubwayMap::OnMouseClick(wxMouseEvent &event) {
449 bool finished = false;
450
451 if (actual_hover_) {
452 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
453 if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) ||
454 networks_.IsItemInNetwork(*hovered_item_)) {
455 if (actual_hover_ != hovered_item_) {
456 hovered_item_ = actual_hover_;
457
458 if (!hovered_item_) {
459 sticky_hover_ = false;
460 }
461
462 Refresh();
463 } else {
464 sticky_hover_ = !sticky_hover_;
465 }
466
467 finished = true;
468 }
469 }
470
471 if (!finished) {
472 if (scroll_mode_) {
473 scroll_mode_ = false;
474
475 SetScrollSpeed(0, 0);
476
477 SetCursor(wxCURSOR_ARROW);
478 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
479 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
480 event.GetPosition().y < GetSize().GetHeight() / 6 ||
481 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
482 scroll_mode_ = true;
483
484 EvaluateScroll(event.GetPosition());
485
486 SetCursor(wxCURSOR_CROSS);
487 } else {
488 sticky_hover_ = false;
489 }
490 }
491}
492
493void SubwayMap::OnTimer(wxTimerEvent &event) {
494 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
495 Refresh();
496}
497
498void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
499 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
500
501 if (new_zoom != zoom_) {
502 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
503 }
504}
505
506void SubwayMap::OnClickHelp(wxCommandEvent &event) {
507 wxMessageBox(
508 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
509 "corner.\nClick on a side of the screen to start panning. It will follow "
510 "your mouse. Click again to stop.\nHover over a door to see the "
511 "requirements to open it.\nHover over a warp or active painting to see "
512 "what it is connected to.\nIn painting shuffle, paintings that have not "
513 "yet been checked will not show their connections.\nA green shaded owl "
514 "means that there is a painting entrance there.\nA red shaded owl means "
515 "that there are only painting exits there.\nClick on a door or "
516 "warp to make the popup stick until you click again.",
517 "Subway Map Help");
518}
519
520void SubwayMap::Redraw() {
521 rendered_ = wxBitmap(map_image_);
522
523 wxMemoryDC dc;
524 dc.SelectObject(rendered_);
525
526 wxGCDC gcdc(dc);
527
528 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
529 ItemDrawType draw_type = ItemDrawType::kNone;
530 const wxBrush *brush_color = wxGREY_BRUSH;
531 std::optional<wxColour> shade_color;
532
533 if (AP_HasEarlyColorHallways() &&
534 subway_item.special == "starting_room_paintings") {
535 draw_type = ItemDrawType::kOwl;
536 shade_color = wxColour(0, 255, 0, 128);
537 } else if (subway_item.special == "sun_painting") {
538 if (!AP_IsPilgrimageEnabled()) {
539 if (IsDoorOpen(*subway_item.door)) {
540 draw_type = ItemDrawType::kOwl;
541 shade_color = wxColour(0, 255, 0, 128);
542 } else {
543 draw_type = ItemDrawType::kBox;
544 brush_color = wxRED_BRUSH;
545 }
546 }
547 } else if (!subway_item.paintings.empty()) {
548 if (AP_IsPaintingShuffle()) {
549 bool has_checked_painting = false;
550 bool has_unchecked_painting = false;
551 bool has_mapped_painting = false;
552 bool has_codomain_painting = false;
553
554 for (const std::string &painting_id : subway_item.paintings) {
555 if (checked_paintings_.count(painting_id)) {
556 has_checked_painting = true;
557
558 if (AP_GetPaintingMapping().count(painting_id)) {
559 has_mapped_painting = true;
560 } else if (AP_IsPaintingMappedTo(painting_id)) {
561 has_codomain_painting = true;
562 }
563 } else {
564 has_unchecked_painting = true;
565 }
566 }
567
568 if (has_unchecked_painting || has_mapped_painting ||
569 has_codomain_painting) {
570 draw_type = ItemDrawType::kOwl;
571
572 if (has_checked_painting) {
573 if (has_mapped_painting) {
574 shade_color = wxColour(0, 255, 0, 128);
575 } else {
576 shade_color = wxColour(255, 0, 0, 128);
577 }
578 }
579 }
580 } else if (!subway_item.tags.empty()) {
581 draw_type = ItemDrawType::kOwl;
582 }
583 } else if (subway_item.door) {
584 draw_type = ItemDrawType::kBox;
585
586 if (IsDoorOpen(*subway_item.door)) {
587 brush_color = wxGREEN_BRUSH;
588 } else {
589 brush_color = wxRED_BRUSH;
590 }
591 }
592
593 wxPoint real_area_pos = {subway_item.x, subway_item.y};
594
595 int real_area_size =
596 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
597
598 if (draw_type == ItemDrawType::kBox) {
599 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
600 gcdc.SetBrush(*brush_color);
601 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
602 } else if (draw_type == ItemDrawType::kOwl) {
603 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale(
604 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR));
605 gcdc.DrawBitmap(owl_bitmap, real_area_pos);
606
607 if (shade_color) {
608 gcdc.SetBrush(wxBrush(*shade_color));
609 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
610 }
611 }
612 }
613}
614
615void SubwayMap::SetUpHelpButton() {
616 help_button_->SetPosition({
617 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
618 15,
619 });
620}
621
622void SubwayMap::EvaluateScroll(wxPoint pos) {
623 int scroll_x;
624 int scroll_y;
625 if (pos.x < GetSize().GetWidth() / 9) {
626 scroll_x = 20;
627 } else if (pos.x < GetSize().GetWidth() / 6) {
628 scroll_x = 5;
629 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
630 scroll_x = -20;
631 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
632 scroll_x = -5;
633 } else {
634 scroll_x = 0;
635 }
636 if (pos.y < GetSize().GetHeight() / 9) {
637 scroll_y = 20;
638 } else if (pos.y < GetSize().GetHeight() / 6) {
639 scroll_y = 5;
640 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
641 scroll_y = -20;
642 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
643 scroll_y = -5;
644 } else {
645 scroll_y = 0;
646 }
647
648 SetScrollSpeed(scroll_x, scroll_y);
649}
650
651wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
652 return {static_cast<int>(pos.x * render_width_ * zoom_ /
653 map_image_.GetSize().GetWidth() +
654 zoom_x_),
655 static_cast<int>(pos.y * render_width_ * zoom_ /
656 map_image_.GetSize().GetWidth() +
657 zoom_y_)};
658}
659
660wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
661 return {static_cast<int>(pos.x * render_width_ * zoom_ /
662 map_image_.GetSize().GetWidth()),
663 static_cast<int>(pos.y * render_width_ * zoom_ /
664 map_image_.GetSize().GetWidth())};
665}
666
667wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
668 return {
669 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
670 render_width_ / zoom_),
671 0, map_image_.GetWidth() - 1),
672 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
673 render_width_ / zoom_),
674 0, map_image_.GetHeight() - 1)};
675}
676
677void SubwayMap::SetZoomPos(wxPoint pos) {
678 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
679 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
680 } else {
681 zoom_x_ = std::clamp(
682 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
683 0);
684 }
685 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
686 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
687 } else {
688 zoom_y_ = std::clamp(
689 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
690 0);
691 }
692}
693
694void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
695 bool should_timer = (scroll_x != 0 || scroll_y != 0);
696 if (should_timer != scroll_timer_->IsRunning()) {
697 if (should_timer) {
698 scroll_timer_->Start(1000 / 60);
699 } else {
700 scroll_timer_->Stop();
701 }
702 }
703
704 scroll_x_ = scroll_x;
705 scroll_y_ = scroll_y;
706}
707
708void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
709 wxPoint map_pos = RenderPosToMapPos(static_point);
710 zoom_ = zoom;
711
712 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
713 SetZoomPos(-(virtual_pos - static_point));
714
715 Refresh();
716
717 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
718}
719
720quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
721 const SubwayItem &subway_item = GD_GetSubwayItem(id);
722 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
723 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
724}
diff --git a/src/subway_map.h b/src/subway_map.h new file mode 100644 index 0000000..feee8ff --- /dev/null +++ b/src/subway_map.h
@@ -0,0 +1,92 @@
1#ifndef SUBWAY_MAP_H_BD2D843E
2#define SUBWAY_MAP_H_BD2D843E
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <memory>
11#include <optional>
12#include <set>
13#include <string>
14#include <vector>
15
16#include <quadtree/Quadtree.h>
17
18#include "game_data.h"
19#include "network_set.h"
20
21class SubwayMap : public wxPanel {
22 public:
23 SubwayMap(wxWindow *parent);
24
25 void OnConnect();
26 void UpdateIndicators();
27 void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp);
28 void Zoom(bool in);
29
30 private:
31 void OnPaint(wxPaintEvent &event);
32 void OnMouseMove(wxMouseEvent &event);
33 void OnMouseScroll(wxMouseEvent &event);
34 void OnMouseLeave(wxMouseEvent &event);
35 void OnMouseClick(wxMouseEvent &event);
36 void OnTimer(wxTimerEvent &event);
37 void OnZoomSlide(wxCommandEvent &event);
38 void OnClickHelp(wxCommandEvent &event);
39
40 void Redraw();
41 void SetUpHelpButton();
42
43 wxPoint MapPosToRenderPos(wxPoint pos) const;
44 wxPoint MapPosToVirtualPos(wxPoint pos) const;
45 wxPoint RenderPosToMapPos(wxPoint pos) const;
46
47 void EvaluateScroll(wxPoint pos);
48
49 void SetZoomPos(wxPoint pos);
50 void SetScrollSpeed(int scroll_x, int scroll_y);
51 void SetZoom(double zoom, wxPoint static_point);
52
53 wxImage map_image_;
54 wxImage owl_image_;
55 wxBitmap unchecked_eye_;
56 wxBitmap checked_eye_;
57
58 wxBitmap rendered_;
59 int render_x_ = 0;
60 int render_y_ = 0;
61 int render_width_ = 1;
62 int render_height_ = 1;
63
64 double zoom_ = 1.0;
65 int zoom_x_ = 0; // in render space
66 int zoom_y_ = 0;
67
68 bool scroll_mode_ = false;
69 wxTimer* scroll_timer_;
70 int scroll_x_ = 0;
71 int scroll_y_ = 0;
72
73 wxSlider *zoom_slider_;
74
75 wxButton *help_button_;
76
77 std::optional<wxPoint> mouse_position_;
78
79 struct GetItemBox {
80 quadtree::Box<float> operator()(const int &id) const;
81 };
82
83 std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_;
84 std::optional<int> hovered_item_;
85 std::optional<int> actual_hover_;
86 bool sticky_hover_ = false;
87
88 NetworkSet networks_;
89 std::set<std::string> checked_paintings_;
90};
91
92#endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index d64e0d3..107ae49 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -1,6 +1,8 @@
1#include "tracker_frame.h" 1#include "tracker_frame.h"
2 2
3#include <wx/aboutdlg.h>
3#include <wx/choicebk.h> 4#include <wx/choicebk.h>
5#include <wx/notebook.h>
4#include <wx/webrequest.h> 6#include <wx/webrequest.h>
5 7
6#include <nlohmann/json.hpp> 8#include <nlohmann/json.hpp>
@@ -10,6 +12,7 @@
10#include "ap_state.h" 12#include "ap_state.h"
11#include "connection_dialog.h" 13#include "connection_dialog.h"
12#include "settings_dialog.h" 14#include "settings_dialog.h"
15#include "subway_map.h"
13#include "tracker_config.h" 16#include "tracker_config.h"
14#include "tracker_panel.h" 17#include "tracker_panel.h"
15#include "version.h" 18#include "version.h"
@@ -17,9 +20,12 @@
17enum TrackerFrameIds { 20enum TrackerFrameIds {
18 ID_CONNECT = 1, 21 ID_CONNECT = 1,
19 ID_CHECK_FOR_UPDATES = 2, 22 ID_CHECK_FOR_UPDATES = 2,
20 ID_SETTINGS = 3 23 ID_SETTINGS = 3,
24 ID_ZOOM_IN = 4,
25 ID_ZOOM_OUT = 5,
21}; 26};
22 27
28wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
23wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 29wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent);
24wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 30wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
25 31
@@ -35,12 +41,20 @@ TrackerFrame::TrackerFrame()
35 menuFile->Append(ID_SETTINGS, "&Settings"); 41 menuFile->Append(ID_SETTINGS, "&Settings");
36 menuFile->Append(wxID_EXIT); 42 menuFile->Append(wxID_EXIT);
37 43
44 wxMenu *menuView = new wxMenu();
45 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+");
46 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--");
47
48 zoom_in_menu_item_->Enable(false);
49 zoom_out_menu_item_->Enable(false);
50
38 wxMenu *menuHelp = new wxMenu(); 51 wxMenu *menuHelp = new wxMenu();
39 menuHelp->Append(wxID_ABOUT); 52 menuHelp->Append(wxID_ABOUT);
40 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates"); 53 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates");
41 54
42 wxMenuBar *menuBar = new wxMenuBar(); 55 wxMenuBar *menuBar = new wxMenuBar();
43 menuBar->Append(menuFile, "&File"); 56 menuBar->Append(menuFile, "&File");
57 menuBar->Append(menuView, "&View");
44 menuBar->Append(menuHelp, "&Help"); 58 menuBar->Append(menuHelp, "&Help");
45 59
46 SetMenuBar(menuBar); 60 SetMenuBar(menuBar);
@@ -54,18 +68,26 @@ TrackerFrame::TrackerFrame()
54 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); 68 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
55 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, 69 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
56 ID_CHECK_FOR_UPDATES); 70 ID_CHECK_FOR_UPDATES);
71 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
72 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
73 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
74 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
57 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 75 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
58 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); 76 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
59 77
60 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); 78 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY);
61 achievements_pane_ = new AchievementsPane(this); 79 achievements_pane_ = new AchievementsPane(choicebook);
62 choicebook->AddPage(achievements_pane_, "Achievements"); 80 choicebook->AddPage(achievements_pane_, "Achievements");
63 81
64 tracker_panel_ = new TrackerPanel(this); 82 notebook_ = new wxNotebook(this, wxID_ANY);
83 tracker_panel_ = new TrackerPanel(notebook_);
84 subway_map_ = new SubwayMap(notebook_);
85 notebook_->AddPage(tracker_panel_, "Map");
86 notebook_->AddPage(subway_map_, "Subway");
65 87
66 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 88 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL);
67 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1)); 89 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
68 top_sizer->Add(tracker_panel_, wxSizerFlags().Expand().Proportion(3)); 90 top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3));
69 91
70 SetSizerAndFit(top_sizer); 92 SetSizerAndFit(top_sizer);
71 SetSize(1280, 728); 93 SetSize(1280, 728);
@@ -96,17 +118,23 @@ void TrackerFrame::SetStatusMessage(std::string message) {
96 QueueEvent(event); 118 QueueEvent(event);
97} 119}
98 120
121void TrackerFrame::ResetIndicators() {
122 QueueEvent(new wxCommandEvent(STATE_RESET));
123}
124
99void TrackerFrame::UpdateIndicators() { 125void TrackerFrame::UpdateIndicators() {
100 QueueEvent(new wxCommandEvent(STATE_CHANGED)); 126 QueueEvent(new wxCommandEvent(STATE_CHANGED));
101} 127}
102 128
103void TrackerFrame::OnAbout(wxCommandEvent &event) { 129void TrackerFrame::OnAbout(wxCommandEvent &event) {
104 std::ostringstream message_text; 130 wxAboutDialogInfo about_info;
105 message_text << "Lingo Archipelago Tracker " << kTrackerVersion 131 about_info.SetName("Lingo Archipelago Tracker");
106 << " by hatkirby"; 132 about_info.SetVersion(kTrackerVersion.ToString());
107 133 about_info.AddDeveloper("hatkirby");
108 wxMessageBox(message_text.str(), "About lingo-ap-tracker", 134 about_info.AddArtist("Brenton Wildes");
109 wxOK | wxICON_INFORMATION); 135 about_info.AddArtist("kinrah");
136
137 wxAboutBox(about_info);
110} 138}
111 139
112void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } 140void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
@@ -122,7 +150,8 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
122 std::deque<ConnectionDetails> new_history; 150 std::deque<ConnectionDetails> new_history;
123 new_history.push_back(GetTrackerConfig().connection_details); 151 new_history.push_back(GetTrackerConfig().connection_details);
124 152
125 for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { 153 for (const ConnectionDetails &details :
154 GetTrackerConfig().connection_history) {
126 if (details != GetTrackerConfig().connection_details) { 155 if (details != GetTrackerConfig().connection_details) {
127 new_history.push_back(details); 156 new_history.push_back(details);
128 } 157 }
@@ -158,9 +187,34 @@ void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
158 CheckForUpdates(/*manual=*/true); 187 CheckForUpdates(/*manual=*/true);
159} 188}
160 189
190void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
191 if (notebook_->GetSelection() == 1) {
192 subway_map_->Zoom(true);
193 }
194}
195
196void TrackerFrame::OnZoomOut(wxCommandEvent& event) {
197 if (notebook_->GetSelection() == 1) {
198 subway_map_->Zoom(false);
199 }
200}
201
202void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
203 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
204 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
205}
206
207void TrackerFrame::OnStateReset(wxCommandEvent& event) {
208 tracker_panel_->UpdateIndicators();
209 achievements_pane_->UpdateIndicators();
210 subway_map_->OnConnect();
211 Refresh();
212}
213
161void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 214void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
162 tracker_panel_->UpdateIndicators(); 215 tracker_panel_->UpdateIndicators();
163 achievements_pane_->UpdateIndicators(); 216 achievements_pane_->UpdateIndicators();
217 subway_map_->UpdateIndicators();
164 Refresh(); 218 Refresh();
165} 219}
166 220
@@ -192,8 +246,10 @@ void TrackerFrame::CheckForUpdates(bool manual) {
192 std::ostringstream message_text; 246 std::ostringstream message_text;
193 message_text << "There is a newer version of Lingo AP Tracker " 247 message_text << "There is a newer version of Lingo AP Tracker "
194 "available. You have " 248 "available. You have "
195 << kTrackerVersion << ", and the latest version is " 249 << kTrackerVersion.ToString()
196 << latest_version << ". Would you like to update?"; 250 << ", and the latest version is "
251 << latest_version.ToString()
252 << ". Would you like to update?";
197 253
198 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) == 254 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
199 wxYES) { 255 wxYES) {
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index e5bf97e..f7cb3f2 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -8,8 +8,12 @@
8#endif 8#endif
9 9
10class AchievementsPane; 10class AchievementsPane;
11class SubwayMap;
11class TrackerPanel; 12class TrackerPanel;
13class wxBookCtrlEvent;
14class wxNotebook;
12 15
16wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
13wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 17wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent);
14wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 18wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
15 19
@@ -19,6 +23,7 @@ class TrackerFrame : public wxFrame {
19 23
20 void SetStatusMessage(std::string message); 24 void SetStatusMessage(std::string message);
21 25
26 void ResetIndicators();
22 void UpdateIndicators(); 27 void UpdateIndicators();
23 28
24 private: 29 private:
@@ -27,14 +32,23 @@ class TrackerFrame : public wxFrame {
27 void OnConnect(wxCommandEvent &event); 32 void OnConnect(wxCommandEvent &event);
28 void OnSettings(wxCommandEvent &event); 33 void OnSettings(wxCommandEvent &event);
29 void OnCheckForUpdates(wxCommandEvent &event); 34 void OnCheckForUpdates(wxCommandEvent &event);
35 void OnZoomIn(wxCommandEvent &event);
36 void OnZoomOut(wxCommandEvent &event);
37 void OnChangePage(wxBookCtrlEvent &event);
30 38
39 void OnStateReset(wxCommandEvent &event);
31 void OnStateChanged(wxCommandEvent &event); 40 void OnStateChanged(wxCommandEvent &event);
32 void OnStatusChanged(wxCommandEvent &event); 41 void OnStatusChanged(wxCommandEvent &event);
33 42
34 void CheckForUpdates(bool manual); 43 void CheckForUpdates(bool manual);
35 44
45 wxNotebook *notebook_;
36 TrackerPanel *tracker_panel_; 46 TrackerPanel *tracker_panel_;
37 AchievementsPane *achievements_pane_; 47 AchievementsPane *achievements_pane_;
48 SubwayMap *subway_map_;
49
50 wxMenuItem *zoom_in_menu_item_;
51 wxMenuItem *zoom_out_menu_item_;
38}; 52};
39 53
40#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ 54#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */
diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index 5f9f8ea..d60c1b6 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -1,5 +1,7 @@
1#include "tracker_panel.h" 1#include "tracker_panel.h"
2 2
3#include <wx/dcbuffer.h>
4
3#include "ap_state.h" 5#include "ap_state.h"
4#include "area_popup.h" 6#include "area_popup.h"
5#include "game_data.h" 7#include "game_data.h"
@@ -13,6 +15,8 @@ constexpr int AREA_EFFECTIVE_SIZE = AREA_ACTUAL_SIZE + AREA_BORDER_SIZE * 2;
13constexpr int PLAYER_SIZE = 96; 15constexpr int PLAYER_SIZE = 96;
14 16
15TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 17TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
18 SetBackgroundStyle(wxBG_STYLE_PAINT);
19
16 map_image_ = wxImage(GetAbsolutePath("assets/lingo_map.png").c_str(), 20 map_image_ = wxImage(GetAbsolutePath("assets/lingo_map.png").c_str(),
17 wxBITMAP_TYPE_PNG); 21 wxBITMAP_TYPE_PNG);
18 if (!map_image_.IsOk()) { 22 if (!map_image_.IsOk()) {
@@ -54,7 +58,7 @@ void TrackerPanel::OnPaint(wxPaintEvent &event) {
54 Redraw(); 58 Redraw();
55 } 59 }
56 60
57 wxPaintDC dc(this); 61 wxBufferedPaintDC dc(this);
58 dc.DrawBitmap(rendered_, 0, 0); 62 dc.DrawBitmap(rendered_, 0, 0);
59 63
60 if (AP_GetPlayerPosition().has_value()) { 64 if (AP_GetPlayerPosition().has_value()) {
@@ -139,7 +143,8 @@ void TrackerPanel::Redraw() {
139 for (AreaIndicator &area : areas_) { 143 for (AreaIndicator &area : areas_) {
140 const MapArea &map_area = GD_GetMapArea(area.area_id); 144 const MapArea &map_area = GD_GetMapArea(area.area_id);
141 if (!AP_IsLocationVisible(map_area.classification) && 145 if (!AP_IsLocationVisible(map_area.classification) &&
142 !(map_area.hunt && GetTrackerConfig().show_hunt_panels)) { 146 !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
147 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
143 area.active = false; 148 area.active = false;
144 continue; 149 continue;
145 } else { 150 } else {
@@ -167,6 +172,21 @@ void TrackerPanel::Redraw() {
167 } 172 }
168 } 173 }
169 174
175 if (AP_IsPaintingShuffle()) {
176 for (int painting_id : map_area.paintings) {
177 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
178 if (!AP_IsPaintingChecked(painting.internal_id)) {
179 bool reachable = IsPaintingReachable(painting_id);
180
181 if (reachable) {
182 has_reachable_unchecked = true;
183 } else {
184 has_unreachable_unchecked = true;
185 }
186 }
187 }
188 }
189
170 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 190 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
171 final_width / image_size.GetWidth(); 191 final_width / image_size.GetWidth();
172 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * 192 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 640a159..2f25257 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -12,9 +12,142 @@
12 12
13namespace { 13namespace {
14 14
15struct Requirements {
16 bool disabled = false;
17
18 std::set<int> doors; // non-grouped, handles progressive
19 std::set<int> items; // all other items
20 std::set<int> rooms; // maybe
21 bool mastery = false; // maybe
22 bool panel_hunt = false; // maybe
23
24 void Merge(const Requirements& rhs) {
25 if (rhs.disabled) {
26 return;
27 }
28
29 for (int id : rhs.doors) {
30 doors.insert(id);
31 }
32 for (int id : rhs.items) {
33 items.insert(id);
34 }
35 for (int id : rhs.rooms) {
36 rooms.insert(id);
37 }
38 mastery = mastery || rhs.mastery;
39 panel_hunt = panel_hunt || rhs.panel_hunt;
40 }
41};
42
43class RequirementCalculator {
44 public:
45 void Reset() {
46 doors_.clear();
47 panels_.clear();
48 }
49
50 const Requirements& GetDoor(int door_id) {
51 if (!doors_.count(door_id)) {
52 Requirements requirements;
53 const Door& door_obj = GD_GetDoor(door_id);
54
55 if (door_obj.type == DoorType::kSunPainting) {
56 if (!AP_IsPilgrimageEnabled()) {
57 requirements.items.insert(door_obj.ap_item_id);
58 } else {
59 requirements.disabled = true;
60 }
61 } else if (door_obj.type == DoorType::kSunwarp) {
62 switch (AP_GetSunwarpAccess()) {
63 case kSUNWARP_ACCESS_NORMAL:
64 // Do nothing.
65 break;
66 case kSUNWARP_ACCESS_DISABLED:
67 requirements.disabled = true;
68 break;
69 case kSUNWARP_ACCESS_UNLOCK:
70 requirements.items.insert(door_obj.group_ap_item_id);
71 break;
72 case kSUNWARP_ACCESS_INDIVIDUAL:
73 case kSUNWARP_ACCESS_PROGRESSIVE:
74 requirements.doors.insert(door_obj.id);
75 break;
76 }
77 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
78 requirements.rooms.insert(door_obj.room);
79
80 for (int panel_id : door_obj.panels) {
81 const Requirements& panel_reqs = GetPanel(panel_id);
82 requirements.Merge(panel_reqs);
83 }
84 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
85 !door_obj.group_name.empty()) {
86 requirements.items.insert(door_obj.group_ap_item_id);
87 } else {
88 requirements.doors.insert(door_obj.id);
89 }
90
91 doors_[door_id] = requirements;
92 }
93
94 return doors_[door_id];
95 }
96
97 const Requirements& GetPanel(int panel_id) {
98 if (!panels_.count(panel_id)) {
99 Requirements requirements;
100 const Panel& panel_obj = GD_GetPanel(panel_id);
101
102 requirements.rooms.insert(panel_obj.room);
103
104 if (panel_obj.name == "THE MASTER") {
105 requirements.mastery = true;
106 }
107
108 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") &&
109 AP_GetLevel2Requirement() > 1) {
110 requirements.panel_hunt = true;
111 }
112
113 for (int room_id : panel_obj.required_rooms) {
114 requirements.rooms.insert(room_id);
115 }
116
117 for (int door_id : panel_obj.required_doors) {
118 const Requirements& door_reqs = GetDoor(door_id);
119 requirements.Merge(door_reqs);
120 }
121
122 for (int panel_id : panel_obj.required_panels) {
123 const Requirements& panel_reqs = GetPanel(panel_id);
124 requirements.Merge(panel_reqs);
125 }
126
127 if (AP_IsColorShuffle()) {
128 for (LingoColor color : panel_obj.colors) {
129 requirements.items.insert(GD_GetItemIdForColor(color));
130 }
131 }
132
133 panels_[panel_id] = requirements;
134 }
135
136 return panels_[panel_id];
137 }
138
139 private:
140 std::map<int, Requirements> doors_;
141 std::map<int, Requirements> panels_;
142};
143
15struct TrackerState { 144struct TrackerState {
16 std::map<int, bool> reachability; 145 std::map<int, bool> reachability;
146 std::set<int> reachable_doors;
147 std::set<int> reachable_paintings;
17 std::mutex reachability_mutex; 148 std::mutex reachability_mutex;
149 RequirementCalculator requirements;
150 std::map<int, std::map<std::string, bool>> door_reports;
18}; 151};
19 152
20enum Decision { kYes, kNo, kMaybe }; 153enum Decision { kYes, kNo, kMaybe };
@@ -41,6 +174,7 @@ class StateCalculator {
41 174
42 void Calculate() { 175 void Calculate() {
43 std::list<int> panel_boundary; 176 std::list<int> panel_boundary;
177 std::list<int> painting_boundary;
44 std::list<Exit> flood_boundary; 178 std::list<Exit> flood_boundary;
45 flood_boundary.push_back({.destination_room = options_.start}); 179 flood_boundary.push_back({.destination_room = options_.start});
46 180
@@ -48,6 +182,8 @@ class StateCalculator {
48 while (reachable_changed) { 182 while (reachable_changed) {
49 reachable_changed = false; 183 reachable_changed = false;
50 184
185 std::list<Exit> new_boundary;
186
51 std::list<int> new_panel_boundary; 187 std::list<int> new_panel_boundary;
52 for (int panel_id : panel_boundary) { 188 for (int panel_id : panel_boundary) {
53 if (solveable_panels_.count(panel_id)) { 189 if (solveable_panels_.count(panel_id)) {
@@ -63,7 +199,33 @@ class StateCalculator {
63 } 199 }
64 } 200 }
65 201
66 std::list<Exit> new_boundary; 202 std::list<int> new_painting_boundary;
203 for (int painting_id : painting_boundary) {
204 if (reachable_paintings_.count(painting_id)) {
205 continue;
206 }
207
208 Decision painting_reachable = IsPaintingReachable(painting_id);
209 if (painting_reachable == kYes) {
210 reachable_paintings_.insert(painting_id);
211 reachable_changed = true;
212
213 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
214 if (AP_GetPaintingMapping().count(cur_painting.internal_id) &&
215 AP_GetCheckedPaintings().count(cur_painting.internal_id)) {
216 Exit painting_exit;
217 PaintingExit target_painting =
218 GD_GetPaintingExit(GD_GetPaintingByName(
219 AP_GetPaintingMapping().at(cur_painting.internal_id)));
220 painting_exit.destination_room = target_painting.room;
221
222 new_boundary.push_back(painting_exit);
223 }
224 } else if (painting_reachable == kMaybe) {
225 new_painting_boundary.push_back(painting_id);
226 }
227 }
228
67 for (const Exit& room_exit : flood_boundary) { 229 for (const Exit& room_exit : flood_boundary) {
68 if (reachable_rooms_.count(room_exit.destination_room)) { 230 if (reachable_rooms_.count(room_exit.destination_room)) {
69 continue; 231 continue;
@@ -98,15 +260,8 @@ class StateCalculator {
98 } 260 }
99 261
100 if (AP_IsPaintingShuffle()) { 262 if (AP_IsPaintingShuffle()) {
101 for (const PaintingExit& out_edge : room_obj.paintings) { 263 for (int out_edge : room_obj.paintings) {
102 if (AP_GetPaintingMapping().count(out_edge.id)) { 264 new_painting_boundary.push_back(out_edge);
103 Exit painting_exit;
104 painting_exit.destination_room = GD_GetRoomForPainting(
105 AP_GetPaintingMapping().at(out_edge.id));
106 painting_exit.door = out_edge.door;
107
108 new_boundary.push_back(painting_exit);
109 }
110 } 265 }
111 } 266 }
112 267
@@ -155,6 +310,13 @@ class StateCalculator {
155 310
156 flood_boundary = new_boundary; 311 flood_boundary = new_boundary;
157 panel_boundary = new_panel_boundary; 312 panel_boundary = new_panel_boundary;
313 painting_boundary = new_painting_boundary;
314 }
315
316 // Now that we know the full reachable area, let's make sure all doors are
317 // evaluated.
318 for (const Door& door : GD_GetDoors()) {
319 int discard = IsDoorReachable(door.id);
158 } 320 }
159 } 321 }
160 322
@@ -166,6 +328,14 @@ class StateCalculator {
166 328
167 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } 329 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; }
168 330
331 const std::set<int>& GetReachablePaintings() const {
332 return reachable_paintings_;
333 }
334
335 const std::map<int, std::map<std::string, bool>>& GetDoorReports() const {
336 return door_report_;
337 }
338
169 private: 339 private:
170 Decision IsNonGroupedDoorReachable(const Door& door_obj) { 340 Decision IsNonGroupedDoorReachable(const Door& door_obj) {
171 bool has_item = AP_HasItem(door_obj.ap_item_id); 341 bool has_item = AP_HasItem(door_obj.ap_item_id);
@@ -182,68 +352,52 @@ class StateCalculator {
182 return has_item ? kYes : kNo; 352 return has_item ? kYes : kNo;
183 } 353 }
184 354
185 Decision IsDoorReachable_Helper(int door_id) { 355 Decision AreRequirementsSatisfied(
186 const Door& door_obj = GD_GetDoor(door_id); 356 const Requirements& reqs, std::map<std::string, bool>* report = nullptr) {
187 357 if (reqs.disabled) {
188 if (!AP_IsPilgrimageEnabled() && door_obj.type == DoorType::kSunPainting) { 358 return kNo;
189 return AP_HasItem(door_obj.ap_item_id) ? kYes : kNo;
190 } else if (door_obj.type == DoorType::kSunwarp) {
191 switch (AP_GetSunwarpAccess()) {
192 case kSUNWARP_ACCESS_NORMAL:
193 return kYes;
194 case kSUNWARP_ACCESS_DISABLED:
195 return kNo;
196 case kSUNWARP_ACCESS_UNLOCK:
197 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo;
198 case kSUNWARP_ACCESS_INDIVIDUAL:
199 case kSUNWARP_ACCESS_PROGRESSIVE:
200 return IsNonGroupedDoorReachable(door_obj);
201 }
202 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
203 if (!reachable_rooms_.count(door_obj.room)) {
204 return kMaybe;
205 }
206
207 for (int panel_id : door_obj.panels) {
208 if (!solveable_panels_.count(panel_id)) {
209 return kMaybe;
210 }
211 }
212
213 return kYes;
214 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
215 !door_obj.group_name.empty()) {
216 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo;
217 } else {
218 return IsNonGroupedDoorReachable(door_obj);
219 } 359 }
220 }
221 360
222 Decision IsDoorReachable(int door_id) { 361 Decision final_decision = kYes;
223 if (options_.parent) {
224 return options_.parent->IsDoorReachable(door_id);
225 }
226 362
227 if (door_decisions_.count(door_id)) { 363 for (int door_id : reqs.doors) {
228 return door_decisions_.at(door_id); 364 const Door& door_obj = GD_GetDoor(door_id);
365 Decision decision = IsNonGroupedDoorReachable(door_obj);
366
367 if (report) {
368 (*report)[door_obj.item_name] = (decision == kYes);
369 }
370
371 if (decision != kYes) {
372 final_decision = decision;
373 }
229 } 374 }
230 375
231 Decision result = IsDoorReachable_Helper(door_id); 376 for (int item_id : reqs.items) {
232 if (result != kMaybe) { 377 bool has_item = AP_HasItem(item_id);
233 door_decisions_[door_id] = result; 378 if (report) {
379 (*report)[AP_GetItemName(item_id)] = has_item;
380 }
381
382 if (!has_item) {
383 final_decision = kNo;
384 }
234 } 385 }
235 386
236 return result; 387 for (int room_id : reqs.rooms) {
237 } 388 bool reachable = reachable_rooms_.count(room_id);
238 389
239 Decision IsPanelReachable(int panel_id) { 390 if (report) {
240 const Panel& panel_obj = GD_GetPanel(panel_id); 391 std::string report_name = "Reach \"" + GD_GetRoom(room_id).name + "\"";
392 (*report)[report_name] = reachable;
393 }
241 394
242 if (!reachable_rooms_.count(panel_obj.room)) { 395 if (!reachable && final_decision != kNo) {
243 return kMaybe; 396 final_decision = kMaybe;
397 }
244 } 398 }
245 399
246 if (panel_obj.name == "THE MASTER") { 400 if (reqs.mastery) {
247 int achievements_accessible = 0; 401 int achievements_accessible = 0;
248 402
249 for (int achieve_id : GD_GetAchievementPanels()) { 403 for (int achieve_id : GD_GetAchievementPanels()) {
@@ -256,12 +410,18 @@ class StateCalculator {
256 } 410 }
257 } 411 }
258 412
259 return (achievements_accessible >= AP_GetMasteryRequirement()) ? kYes 413 bool can_mastery =
260 : kMaybe; 414 (achievements_accessible >= AP_GetMasteryRequirement());
415 if (report) {
416 (*report)["Mastery"] = can_mastery;
417 }
418
419 if (!can_mastery && final_decision != kNo) {
420 final_decision = kMaybe;
421 }
261 } 422 }
262 423
263 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && 424 if (reqs.panel_hunt) {
264 AP_GetLevel2Requirement() > 1) {
265 int counting_panels_accessible = 0; 425 int counting_panels_accessible = 0;
266 426
267 for (int solved_panel_id : solveable_panels_) { 427 for (int solved_panel_id : solveable_panels_) {
@@ -272,41 +432,58 @@ class StateCalculator {
272 } 432 }
273 } 433 }
274 434
275 return (counting_panels_accessible >= AP_GetLevel2Requirement() - 1) 435 bool can_level2 =
276 ? kYes 436 (counting_panels_accessible >= AP_GetLevel2Requirement() - 1);
277 : kMaybe; 437 if (report) {
278 } 438 std::string report_name =
439 std::to_string(AP_GetLevel2Requirement()) + " Panels";
440 (*report)[report_name] = can_level2;
441 }
279 442
280 for (int room_id : panel_obj.required_rooms) { 443 if (!can_level2 && final_decision != kNo) {
281 if (!reachable_rooms_.count(room_id)) { 444 final_decision = kMaybe;
282 return kMaybe;
283 } 445 }
284 } 446 }
285 447
286 for (int door_id : panel_obj.required_doors) { 448 return final_decision;
287 Decision door_reachable = IsDoorReachable(door_id); 449 }
288 if (door_reachable == kNo) { 450
289 const Door& door_obj = GD_GetDoor(door_id); 451 Decision IsDoorReachable_Helper(int door_id) {
290 return (door_obj.is_event || AP_GetDoorShuffleMode() == kNO_DOORS) 452 if (door_report_.count(door_id)) {
291 ? kMaybe 453 door_report_[door_id].clear();
292 : kNo; 454 } else {
293 } else if (door_reachable == kMaybe) { 455 door_report_[door_id] = {};
294 return kMaybe;
295 }
296 } 456 }
297 457
298 for (int panel_id : panel_obj.required_panels) { 458 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id),
299 if (!solveable_panels_.count(panel_id)) { 459 &door_report_[door_id]);
300 return kMaybe; 460 }
301 } 461
462 Decision IsDoorReachable(int door_id) {
463 if (options_.parent) {
464 return options_.parent->IsDoorReachable(door_id);
302 } 465 }
303 466
304 if (AP_IsColorShuffle()) { 467 if (door_decisions_.count(door_id)) {
305 for (LingoColor color : panel_obj.colors) { 468 return door_decisions_.at(door_id);
306 if (!AP_HasItem(GD_GetItemIdForColor(color))) { 469 }
307 return kNo; 470
308 } 471 Decision result = IsDoorReachable_Helper(door_id);
309 } 472 if (result != kMaybe) {
473 door_decisions_[door_id] = result;
474 }
475
476 return result;
477 }
478
479 Decision IsPanelReachable(int panel_id) {
480 return AreRequirementsSatisfied(GetState().requirements.GetPanel(panel_id));
481 }
482
483 Decision IsPaintingReachable(int painting_id) {
484 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
485 if (painting.door) {
486 return IsDoorReachable(*painting.door);
310 } 487 }
311 488
312 return kYes; 489 return kYes;
@@ -395,11 +572,20 @@ class StateCalculator {
395 std::set<int> reachable_rooms_; 572 std::set<int> reachable_rooms_;
396 std::map<int, Decision> door_decisions_; 573 std::map<int, Decision> door_decisions_;
397 std::set<int> solveable_panels_; 574 std::set<int> solveable_panels_;
575 std::set<int> reachable_paintings_;
576 std::map<int, std::map<std::string, bool>> door_report_;
398}; 577};
399 578
400} // namespace 579} // namespace
401 580
581void ResetReachabilityRequirements() {
582 std::lock_guard reachability_guard(GetState().reachability_mutex);
583 GetState().requirements.Reset();
584}
585
402void RecalculateReachability() { 586void RecalculateReachability() {
587 std::lock_guard reachability_guard(GetState().reachability_mutex);
588
403 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 589 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
404 state_calculator.Calculate(); 590 state_calculator.Calculate();
405 591
@@ -422,10 +608,21 @@ void RecalculateReachability() {
422 } 608 }
423 } 609 }
424 610
425 { 611 std::set<int> new_reachable_doors;
426 std::lock_guard reachability_guard(GetState().reachability_mutex); 612 for (const auto& [door_id, decision] : state_calculator.GetDoorDecisions()) {
427 std::swap(GetState().reachability, new_reachability); 613 if (decision == kYes) {
614 new_reachable_doors.insert(door_id);
615 }
428 } 616 }
617
618 std::set<int> reachable_paintings = state_calculator.GetReachablePaintings();
619 std::map<int, std::map<std::string, bool>> door_reports =
620 state_calculator.GetDoorReports();
621
622 std::swap(GetState().reachability, new_reachability);
623 std::swap(GetState().reachable_doors, new_reachable_doors);
624 std::swap(GetState().reachable_paintings, reachable_paintings);
625 std::swap(GetState().door_reports, door_reports);
429} 626}
430 627
431bool IsLocationReachable(int location_id) { 628bool IsLocationReachable(int location_id) {
@@ -437,3 +634,21 @@ bool IsLocationReachable(int location_id) {
437 return false; 634 return false;
438 } 635 }
439} 636}
637
638bool IsDoorOpen(int door_id) {
639 std::lock_guard reachability_guard(GetState().reachability_mutex);
640
641 return GetState().reachable_doors.count(door_id);
642}
643
644bool IsPaintingReachable(int painting_id) {
645 std::lock_guard reachability_guard(GetState().reachability_mutex);
646
647 return GetState().reachable_paintings.count(painting_id);
648}
649
650const std::map<std::string, bool>& GetDoorRequirements(int door_id) {
651 std::lock_guard reachability_guard(GetState().reachability_mutex);
652
653 return GetState().door_reports[door_id];
654}
diff --git a/src/tracker_state.h b/src/tracker_state.h index e73607f..c7857a0 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h
@@ -1,8 +1,19 @@
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
13bool IsDoorOpen(int door_id);
14
15bool IsPaintingReachable(int painting_id);
16
17const std::map<std::string, bool>& GetDoorRequirements(int door_id);
18
8#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ 19#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
diff --git a/src/version.h b/src/version.h index f127610..7c3d3ad 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 <iostream>
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;
@@ -23,6 +24,10 @@ struct Version {
23 } 24 }
24 } 25 }
25 26
27 std::string ToString() const {
28 return fmt::format("v{}.{}.{}", major, minor, revision);
29 }
30
26 bool operator<(const Version& rhs) const { 31 bool operator<(const Version& rhs) const {
27 return (major < rhs.major) || 32 return (major < rhs.major) ||
28 (major == rhs.major && 33 (major == rhs.major &&
@@ -31,10 +36,6 @@ struct Version {
31 } 36 }
32}; 37};
33 38
34std::ostream& operator<<(std::ostream& out, const Version& ver) { 39constexpr const Version kTrackerVersion = Version(0, 10, 3);
35 return out << "v" << ver.major << "." << ver.minor << "." << ver.revision;
36}
37
38constexpr const Version kTrackerVersion = Version(0, 9, 1);
39 40
40#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/vcpkg.json b/vcpkg.json index 925212a..e13d228 100644 --- a/vcpkg.json +++ b/vcpkg.json
@@ -3,6 +3,7 @@
3 "websocketpp", 3 "websocketpp",
4 "wxwidgets", 4 "wxwidgets",
5 "openssl", 5 "openssl",
6 "yaml-cpp" 6 "yaml-cpp",
7 "fmt"
7 ] 8 ]
8} 9}
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/quadtree/Box.h b/vendor/quadtree/Box.h new file mode 100644 index 0000000..65182bf --- /dev/null +++ b/vendor/quadtree/Box.h
@@ -0,0 +1,67 @@
1#pragma once
2
3#include "Vector2.h"
4
5namespace quadtree
6{
7
8template<typename T>
9class Box
10{
11public:
12 T left;
13 T top;
14 T width; // Must be positive
15 T height; // Must be positive
16
17 constexpr Box(T Left = 0, T Top = 0, T Width = 0, T Height = 0) noexcept :
18 left(Left), top(Top), width(Width), height(Height)
19 {
20
21 }
22
23 constexpr Box(const Vector2<T>& position, const Vector2<T>& size) noexcept :
24 left(position.x), top(position.y), width(size.x), height(size.y)
25 {
26
27 }
28
29 constexpr T getRight() const noexcept
30 {
31 return left + width;
32 }
33
34 constexpr T getBottom() const noexcept
35 {
36 return top + height;
37 }
38
39 constexpr Vector2<T> getTopLeft() const noexcept
40 {
41 return Vector2<T>(left, top);
42 }
43
44 constexpr Vector2<T> getCenter() const noexcept
45 {
46 return Vector2<T>(left + width / 2, top + height / 2);
47 }
48
49 constexpr Vector2<T> getSize() const noexcept
50 {
51 return Vector2<T>(width, height);
52 }
53
54 constexpr bool contains(const Box<T>& box) const noexcept
55 {
56 return left <= box.left && box.getRight() <= getRight() &&
57 top <= box.top && box.getBottom() <= getBottom();
58 }
59
60 constexpr bool intersects(const Box<T>& box) const noexcept
61 {
62 return !(left >= box.getRight() || getRight() <= box.left ||
63 top >= box.getBottom() || getBottom() <= box.top);
64 }
65};
66
67} \ No newline at end of file
diff --git a/vendor/quadtree/LICENSE b/vendor/quadtree/LICENSE new file mode 100644 index 0000000..1b226d3 --- /dev/null +++ b/vendor/quadtree/LICENSE
@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2019 Pierre Vigier
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
diff --git a/vendor/quadtree/Quadtree.h b/vendor/quadtree/Quadtree.h new file mode 100644 index 0000000..06097c5 --- /dev/null +++ b/vendor/quadtree/Quadtree.h
@@ -0,0 +1,315 @@
1#pragma once
2
3#include <cassert>
4#include <algorithm>
5#include <array>
6#include <memory>
7#include <type_traits>
8#include <vector>
9#include "Box.h"
10
11namespace quadtree
12{
13
14template<typename T, typename GetBox, typename Equal = std::equal_to<T>, typename Float = float>
15class Quadtree
16{
17 static_assert(std::is_convertible_v<std::invoke_result_t<GetBox, const T&>, Box<Float>>,
18 "GetBox must be a callable of signature Box<Float>(const T&)");
19 static_assert(std::is_convertible_v<std::invoke_result_t<Equal, const T&, const T&>, bool>,
20 "Equal must be a callable of signature bool(const T&, const T&)");
21 static_assert(std::is_arithmetic_v<Float>);
22
23public:
24 Quadtree(const Box<Float>& box, const GetBox& getBox = GetBox(),
25 const Equal& equal = Equal()) :
26 mBox(box), mRoot(std::make_unique<Node>()), mGetBox(getBox), mEqual(equal)
27 {
28
29 }
30
31 void add(const T& value)
32 {
33 add(mRoot.get(), 0, mBox, value);
34 }
35
36 void remove(const T& value)
37 {
38 remove(mRoot.get(), mBox, value);
39 }
40
41 std::vector<T> query(const Box<Float>& box) const
42 {
43 auto values = std::vector<T>();
44 query(mRoot.get(), mBox, box, values);
45 return values;
46 }
47
48 std::vector<std::pair<T, T>> findAllIntersections() const
49 {
50 auto intersections = std::vector<std::pair<T, T>>();
51 findAllIntersections(mRoot.get(), intersections);
52 return intersections;
53 }
54
55 Box<Float> getBox() const
56 {
57 return mBox;
58 }
59
60private:
61 static constexpr auto Threshold = std::size_t(16);
62 static constexpr auto MaxDepth = std::size_t(8);
63
64 struct Node
65 {
66 std::array<std::unique_ptr<Node>, 4> children;
67 std::vector<T> values;
68 };
69
70 Box<Float> mBox;
71 std::unique_ptr<Node> mRoot;
72 GetBox mGetBox;
73 Equal mEqual;
74
75 bool isLeaf(const Node* node) const
76 {
77 return !static_cast<bool>(node->children[0]);
78 }
79
80 Box<Float> computeBox(const Box<Float>& box, int i) const
81 {
82 auto origin = box.getTopLeft();
83 auto childSize = box.getSize() / static_cast<Float>(2);
84 switch (i)
85 {
86 // North West
87 case 0:
88 return Box<Float>(origin, childSize);
89 // Norst East
90 case 1:
91 return Box<Float>(Vector2<Float>(origin.x + childSize.x, origin.y), childSize);
92 // South West
93 case 2:
94 return Box<Float>(Vector2<Float>(origin.x, origin.y + childSize.y), childSize);
95 // South East
96 case 3:
97 return Box<Float>(origin + childSize, childSize);
98 default:
99 assert(false && "Invalid child index");
100 return Box<Float>();
101 }
102 }
103
104 int getQuadrant(const Box<Float>& nodeBox, const Box<Float>& valueBox) const
105 {
106 auto center = nodeBox.getCenter();
107 // West
108 if (valueBox.getRight() < center.x)
109 {
110 // North West
111 if (valueBox.getBottom() < center.y)
112 return 0;
113 // South West
114 else if (valueBox.top >= center.y)
115 return 2;
116 // Not contained in any quadrant
117 else
118 return -1;
119 }
120 // East
121 else if (valueBox.left >= center.x)
122 {
123 // North East
124 if (valueBox.getBottom() < center.y)
125 return 1;
126 // South East
127 else if (valueBox.top >= center.y)
128 return 3;
129 // Not contained in any quadrant
130 else
131 return -1;
132 }
133 // Not contained in any quadrant
134 else
135 return -1;
136 }
137
138 void add(Node* node, std::size_t depth, const Box<Float>& box, const T& value)
139 {
140 assert(node != nullptr);
141 assert(box.contains(mGetBox(value)));
142 if (isLeaf(node))
143 {
144 // Insert the value in this node if possible
145 if (depth >= MaxDepth || node->values.size() < Threshold)
146 node->values.push_back(value);
147 // Otherwise, we split and we try again
148 else
149 {
150 split(node, box);
151 add(node, depth, box, value);
152 }
153 }
154 else
155 {
156 auto i = getQuadrant(box, mGetBox(value));
157 // Add the value in a child if the value is entirely contained in it
158 if (i != -1)
159 add(node->children[static_cast<std::size_t>(i)].get(), depth + 1, computeBox(box, i), value);
160 // Otherwise, we add the value in the current node
161 else
162 node->values.push_back(value);
163 }
164 }
165
166 void split(Node* node, const Box<Float>& box)
167 {
168 assert(node != nullptr);
169 assert(isLeaf(node) && "Only leaves can be split");
170 // Create children
171 for (auto& child : node->children)
172 child = std::make_unique<Node>();
173 // Assign values to children
174 auto newValues = std::vector<T>(); // New values for this node
175 for (const auto& value : node->values)
176 {
177 auto i = getQuadrant(box, mGetBox(value));
178 if (i != -1)
179 node->children[static_cast<std::size_t>(i)]->values.push_back(value);
180 else
181 newValues.push_back(value);
182 }
183 node->values = std::move(newValues);
184 }
185
186 bool remove(Node* node, const Box<Float>& box, const T& value)
187 {
188 assert(node != nullptr);
189 assert(box.contains(mGetBox(value)));
190 if (isLeaf(node))
191 {
192 // Remove the value from node
193 removeValue(node, value);
194 return true;
195 }
196 else
197 {
198 // Remove the value in a child if the value is entirely contained in it
199 auto i = getQuadrant(box, mGetBox(value));
200 if (i != -1)
201 {
202 if (remove(node->children[static_cast<std::size_t>(i)].get(), computeBox(box, i), value))
203 return tryMerge(node);
204 }
205 // Otherwise, we remove the value from the current node
206 else
207 removeValue(node, value);
208 return false;
209 }
210 }
211
212 void removeValue(Node* node, const T& value)
213 {
214 // Find the value in node->values
215 auto it = std::find_if(std::begin(node->values), std::end(node->values),
216 [this, &value](const auto& rhs){ return mEqual(value, rhs); });
217 assert(it != std::end(node->values) && "Trying to remove a value that is not present in the node");
218 // Swap with the last element and pop back
219 *it = std::move(node->values.back());
220 node->values.pop_back();
221 }
222
223 bool tryMerge(Node* node)
224 {
225 assert(node != nullptr);
226 assert(!isLeaf(node) && "Only interior nodes can be merged");
227 auto nbValues = node->values.size();
228 for (const auto& child : node->children)
229 {
230 if (!isLeaf(child.get()))
231 return false;
232 nbValues += child->values.size();
233 }
234 if (nbValues <= Threshold)
235 {
236 node->values.reserve(nbValues);
237 // Merge the values of all the children
238 for (const auto& child : node->children)
239 {
240 for (const auto& value : child->values)
241 node->values.push_back(value);
242 }
243 // Remove the children
244 for (auto& child : node->children)
245 child.reset();
246 return true;
247 }
248 else
249 return false;
250 }
251
252 void query(Node* node, const Box<Float>& box, const Box<Float>& queryBox, std::vector<T>& values) const
253 {
254 assert(node != nullptr);
255 assert(queryBox.intersects(box));
256 for (const auto& value : node->values)
257 {
258 if (queryBox.intersects(mGetBox(value)))
259 values.push_back(value);
260 }
261 if (!isLeaf(node))
262 {
263 for (auto i = std::size_t(0); i < node->children.size(); ++i)
264 {
265 auto childBox = computeBox(box, static_cast<int>(i));
266 if (queryBox.intersects(childBox))
267 query(node->children[i].get(), childBox, queryBox, values);
268 }
269 }
270 }
271
272 void findAllIntersections(Node* node, std::vector<std::pair<T, T>>& intersections) const
273 {
274 // Find intersections between values stored in this node
275 // Make sure to not report the same intersection twice
276 for (auto i = std::size_t(0); i < node->values.size(); ++i)
277 {
278 for (auto j = std::size_t(0); j < i; ++j)
279 {
280 if (mGetBox(node->values[i]).intersects(mGetBox(node->values[j])))
281 intersections.emplace_back(node->values[i], node->values[j]);
282 }
283 }
284 if (!isLeaf(node))
285 {
286 // Values in this node can intersect values in descendants
287 for (const auto& child : node->children)
288 {
289 for (const auto& value : node->values)
290 findIntersectionsInDescendants(child.get(), value, intersections);
291 }
292 // Find intersections in children
293 for (const auto& child : node->children)
294 findAllIntersections(child.get(), intersections);
295 }
296 }
297
298 void findIntersectionsInDescendants(Node* node, const T& value, std::vector<std::pair<T, T>>& intersections) const
299 {
300 // Test against the values stored in this node
301 for (const auto& other : node->values)
302 {
303 if (mGetBox(value).intersects(mGetBox(other)))
304 intersections.emplace_back(value, other);
305 }
306 // Test against values stored into descendants of this node
307 if (!isLeaf(node))
308 {
309 for (const auto& child : node->children)
310 findIntersectionsInDescendants(child.get(), value, intersections);
311 }
312 }
313};
314
315}
diff --git a/vendor/quadtree/Vector2.h b/vendor/quadtree/Vector2.h new file mode 100644 index 0000000..302d73e --- /dev/null +++ b/vendor/quadtree/Vector2.h
@@ -0,0 +1,47 @@
1#pragma once
2
3namespace quadtree
4{
5
6template<typename T>
7class Vector2
8{
9public:
10 T x;
11 T y;
12
13 constexpr Vector2<T>(T X = 0, T Y = 0) noexcept : x(X), y(Y)
14 {
15
16 }
17
18 constexpr Vector2<T>& operator+=(const Vector2<T>& other) noexcept
19 {
20 x += other.x;
21 y += other.y;
22 return *this;
23 }
24
25 constexpr Vector2<T>& operator/=(T t) noexcept
26 {
27 x /= t;
28 y /= t;
29 return *this;
30 }
31};
32
33template<typename T>
34constexpr Vector2<T> operator+(Vector2<T> lhs, const Vector2<T>& rhs) noexcept
35{
36 lhs += rhs;
37 return lhs;
38}
39
40template<typename T>
41constexpr Vector2<T> operator/(Vector2<T> vec, T t) noexcept
42{
43 vec /= t;
44 return vec;
45}
46
47}
diff --git a/vendor/wswrap b/vendor/wswrap
Subproject 5c64761477bd1df380056e5ad3b20677616664b Subproject 9b5b11e8dad01c5f5e465b15dfee87dd4138a5f