about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md54
-rw-r--r--CMakeLists.txt5
-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.yaml1038
-rw-r--r--src/ap_state.cpp163
-rw-r--r--src/ap_state.h11
-rw-r--r--src/area_popup.cpp40
-rw-r--r--src/game_data.cpp257
-rw-r--r--src/game_data.h41
-rw-r--r--src/logger.cpp32
-rw-r--r--src/logger.h8
-rw-r--r--src/main.cpp23
-rw-r--r--src/network_set.cpp30
-rw-r--r--src/network_set.h25
-rw-r--r--src/subway_map.cpp728
-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.h12
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
33 files changed, 3299 insertions, 264 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee737e..37167d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,59 @@
1# lingo-ap-tracker Releases 1# lingo-ap-tracker Releases
2 2
3## v0.10.2 - 2024-06-09
4
5- Fixed intermittent reachability detection issue.
6- Increased debug logging.
7
8Download:
9[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/>
10Source: [v0.10.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.2)
11
12## v0.10.1 - 2024-06-08
13
14- Fixed display not updating when game state changes.
15- Fixed broken reachability calculation.
16
17Download:
18[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/>
19Source: [v0.10.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.1)
20
21## v0.10.0 - 2024-06-06
22
23- Added a subway map tab to the tracker. This displays the topology of the game,
24 along with all of the doors, paintings, sunwarps, and regular warps. It can
25 show you whether doors are open or closed, and you can hover over them to see
26 what's needed to open them. You can also hover over paintings, sunwarps, and
27 regular warps to see what they're connected to. Thanks to Kinrah for creating
28 the image!
29- When playing on painting shuffle, the tracker will no longer automatically
30 take your painting mapping into consideration. Instead, unchecked paintings
31 will show up in the regular map tab as if they were unchecked locations.
32 Looking into a painting in-game marks it as checked in the tracker, and if the
33 painting goes somewhere, the tracker will record that and recalculate your
34 reachable areas accordingly. v3.1.0 or later of the client is required for
35 this to work.
36
37Download:
38[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/>
39Source: [v0.10.0](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.10.0)
40
41## v0.9.2 - 2024-06-04
42
43- Updated apclient and dependencies.
44
45Download:
46[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/>
47Source: [v0.9.2](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.9.2)
48
49## v0.9.1 - 2024-05-15
50
51- Fixed pilgrimage detection when sunwarp shuffle is on.
52
53Download:
54[lingo-ap-tracker-v0.9.1-win64.zip](https://files.fourisland.com/releases/lingo-ap-tracker/lingo-ap-tracker-v0.9.1-win64.zip)<br/>
55Source: [v0.9.1](https://code.fourisland.com/lingo-ap-tracker/tag/?h=v0.9.1)
56
3## v0.9.0 - 2024-04-22 57## v0.9.0 - 2024-04-22
4 58
5- Compatibility update for Archipelago 0.4.6 59- Compatibility update for Archipelago 0.4.6
diff --git a/CMakeLists.txt b/CMakeLists.txt index f857096..cd62c55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -22,6 +22,7 @@ include_directories(
22 ${yaml-cpp_INCLUDE_DIRS} 22 ${yaml-cpp_INCLUDE_DIRS}
23 ${OpenSSL_INCLUDE_DIRS} 23 ${OpenSSL_INCLUDE_DIRS}
24 vendor/whereami 24 vendor/whereami
25 vendor
25) 26)
26 27
27find_path(SYSTEM_INCLUDE_DIR zlib.h) 28find_path(SYSTEM_INCLUDE_DIR zlib.h)
@@ -39,11 +40,11 @@ add_executable(lingo_ap_tracker
39 "src/connection_dialog.cpp" 40 "src/connection_dialog.cpp"
40 "src/tracker_state.cpp" 41 "src/tracker_state.cpp"
41 "src/tracker_config.cpp" 42 "src/tracker_config.cpp"
42 "src/logger.cpp"
43 "src/achievements_pane.cpp" 43 "src/achievements_pane.cpp"
44 "src/settings_dialog.cpp" 44 "src/settings_dialog.cpp"
45 "src/global.cpp" 45 "src/global.cpp"
46 "src/version.cpp" 46 "src/subway_map.cpp"
47 "src/network_set.cpp"
47 "vendor/whereami/whereami.c" 48 "vendor/whereami/whereami.c"
48) 49)
49set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) 50set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
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 7965b36..a30852f 100644 --- a/VERSION +++ b/VERSION
@@ -1 +1 @@
v0.9.0 \ No newline at end of file v0.10.2 \ 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..266b722 --- /dev/null +++ b/assets/subway.yaml
@@ -0,0 +1,1038 @@
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: Outside The Undeterred
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- pos: [1567, 1264]
634 tags:
635 - red_ch
636- pos: [150, 808]
637 tags:
638 - blue_ch
639- pos: [626, 371]
640 tags:
641 - yellow_ch
642- pos: [1368, 241]
643 tags:
644 - green_ch
645- pos: [1543, 1307]
646 room: Number Hunt
647 door: Sixes
648- pos: [1543, 1388]
649 room: Number Hunt
650 door: Nines
651- pos: [1468, 1377]
652 room: Outside The Bold
653 door: Bold Entrance
654- pos: [1425, 1358]
655 paintings:
656 - pencil_painting2
657 - north_missing2
658 tags:
659 - pencil_compass
660 - pencil_starting
661 - pencil_directional
662- pos: [1334, 1419]
663 room: Outside The Bold
664 door: Steady Entrance
665- pos: [445, 1048]
666 tags:
667 - undeterred_artistic
668- pos: [279, 1071]
669 room: Number Hunt
670 door: Zero Door
671- pos: [338, 1071]
672 room: Outside The Undeterred
673 door: Twos
674- pos: [370, 1071]
675 room: Outside The Undeterred
676 door: Threes
677- pos: [402, 1071]
678 room: Outside The Undeterred
679 door: Fours
680- pos: [242, 1071]
681 room: Outside The Undeterred
682 door: Undeterred Entrance
683- pos: [60, 1017]
684 paintings:
685 - blueman_painting_2
686 tags:
687 - blueman_courtyard
688 - blueman_starting
689- pos: [60, 970]
690 special: early_color_hallways
691- pos: [402, 1012]
692 room: Outside The Undeterred
693 door: Green Painting
694 paintings:
695 - maze_painting_3
696 tags:
697 - green_numbers
698- pos: [134, 1007]
699 sunwarp:
700 dots: 4
701 type: exit
702- pos: [719, 1039]
703 room: Outside The Undeterred
704 door: Challenge Entrance
705- pos: [438, 1039]
706 room: Outside The Undeterred
707 door: Number Hunt
708- pos: [563, 1071]
709 room: Outside The Undeterred
710 door: Fives
711- pos: [596, 1071]
712 room: Number Hunt
713 door: Sixes
714- pos: [627, 1071]
715 room: Number Hunt
716 door: Sevens
717- pos: [659, 1071]
718 room: Number Hunt
719 door: Eights
720- pos: [692, 1071]
721 room: Number Hunt
722 door: Nines
723- pos: [525, 1002]
724 room: Number Hunt
725 door: Door to Directional Gallery
726- pos: [659, 1014]
727 room: Number Hunt
728 door: Eights
729 paintings:
730 - smile_painting_5
731 tags:
732 - smiley_numbers
733- pos: [557, 953]
734 room: Number Hunt
735 door: Sevens
736- pos: [391, 932]
737 room: Number Hunt
738 door: Sixes
739- pos: [311, 932]
740 room: Number Hunt
741 door: Nines
742- pos: [268, 868]
743 room: Number Hunt
744 door: Eights
745- pos: [268, 825]
746 room: Directional Gallery
747 door: Yellow Barrier
748- pos: [231, 681]
749 room: Number Hunt
750 door: Sixes
751- pos: [242, 980]
752 room: Directional Gallery
753 door: Shortcut to The Undeterred
754- pos: [351, 927]
755 paintings:
756 - boxes_painting
757 tags:
758 - lattice_directional
759- pos: [272, 927]
760 paintings:
761 - smile_painting_7
762 tags:
763 - smiley_directional
764- pos: [214, 822]
765 paintings:
766 - cherry_painting
767 tags:
768 - apple_directional
769- pos: [266, 735]
770 room: Number Hunt
771 door: Sixes
772 paintings:
773 - pencil_painting3
774 tags:
775 - pencil_directional
776- pos: [215, 735]
777 paintings:
778 - flower_painting_4
779- pos: [626, 851]
780 sunwarp:
781 dots: 6
782 type: exit
783- pos: [1141, 1441]
784 room: Orange Tower Third Floor
785 door: Orange Barrier
786- pos: [1174, 1441]
787 room: Outside The Initiated
788 door: Green Barrier
789- pos: [1205, 1441]
790 room: Outside The Initiated
791 door: Purple Barrier
792- pos: [1334, 1377]
793 room: Color Hunt
794 door: Shortcut to The Steady
795- pos: [1280, 1375]
796 paintings:
797 - arrows_painting_7
798- pos: [1233, 1321]
799 room: Outside The Initiated
800 door: Entrance
801 paintings:
802 - fruitbowl_painting3
803- pos: [1290, 1323]
804 sunwarp:
805 dots: 5
806 type: exit
807- pos: [1189, 1356]
808 room: Outside The Initiated
809 door: Entrance
810- pos: [1154, 1332]
811 paintings:
812 - colors_painting
813- pos: [1640, 1260]
814 room: The Bearer
815 door: Backside Door
816- pos: [1468, 1287]
817 room: The Bearer
818 door: Entrance
819- pos: [1430, 1232]
820 room: Number Hunt
821 door: Sixes
822- pos: [1388, 1152]
823 room: Bearer Side Area
824 door: Shortcut to Tower
825- pos: [1273, 1142]
826 paintings:
827 - pencil_painting5
828 - pencil_painting4
829- pos: [1355, 1092]
830 room: Knight Night (Final)
831 door: Exit
832- pos: [1382, 900]
833 room: Knight Night (Final)
834 door: Exit
835- pos: [1388, 1002]
836 # Complex case, because this is also blocked by Knight Night (Final) - Exit
837 room: Number Hunt
838 door: Sevens
839- pos: [1653, 101]
840 paintings:
841 - smile_painting_9
842 tags:
843 - smiley_crossroads
844 - smiley_deadend
845 - smiley_hotcrusts
846 - smiley_numbers
847 - smiley_directional
848 - smiley_initiated
849 - smiley_gallery
850 - smiley_theysee
851- pos: [1656, 139]
852 room: The Artistic (Smiley)
853 door: Door to Panda
854- pos: [1711, 140]
855 tags:
856 - undeterred_artistic
857- pos: [1653, 169]
858 paintings:
859 - panda_painting_3
860 tags:
861 - panda_tower6
862 - panda_hallway
863- pos: [1708, 171]
864 room: The Artistic (Panda)
865 door: Door to Lattice
866- pos: [1761, 169]
867 paintings:
868 - boxes_painting2
869 tags:
870 - lattice_directional
871- pos: [1762, 139]
872 room: The Artistic (Lattice)
873 door: Door to Apple
874- pos: [1761, 101]
875 paintings:
876 - cherry_painting3
877 tags:
878 - apple_tower6
879 - apple_directional
880- pos: [1708, 107]
881 room: The Artistic (Apple)
882 door: Door to Smiley
883- pos: [370, 681]
884 room: Number Hunt
885 door: Eights
886- pos: [411, 685]
887 paintings:
888 - eye_painting_2
889 - smile_painting_2
890 tags:
891 - smiley_theysee
892- pos: [310, 750]
893 room: The Eyes They See
894 door: Exit
895- pos: [334, 798]
896 paintings:
897 - arrows_painting_5
898- pos: [370, 792]
899 room: Outside The Wondrous
900 door: Wondrous Entrance
901- pos: [367, 752]
902 paintings:
903 - symmetry_painting_a_1
904 - symmetry_painting_b_1
905 - symmetry_painting_a_3
906 - symmetry_painting_a_5
907 - symmetry_painting_b_4
908 - symmetry_painting_a_2
909 - symmetry_painting_b_2
910 - symmetry_painting_a_6
911 - symmetry_painting_b_6
912 tags:
913 - symmetry_starting
914- pos: [407, 755]
915 room: The Wondrous
916 door: Exit
917 paintings:
918 - arrows_painting_9
919- pos: [449, 755]
920 paintings:
921 - flower_painting_6
922 tags:
923 - flower_arrow
924- pos: [1101, 222]
925 paintings:
926 - panda_painting
927 tags:
928 - panda_hallway
929- pos: [1152, 209]
930 room: Hallway Room (1)
931 door: Exit
932- pos: [1189, 170]
933 room: Hallway Room (2)
934 door: Exit
935- pos: [1238, 124]
936 room: Hallway Room (3)
937 door: Exit
938- pos: [1313, 108]
939 room: Hallway Room (4)
940 door: Exit
941- pos: [1528, 108]
942 room: Hallway Room (4)
943 door: Exit
944- pos: [1415, 140]
945 room: Number Hunt
946 door: Nines
947- pos: [1458, 133]
948 paintings:
949 - south_afar
950- pos: [826, 1452]
951 room: Outside The Wanderer
952 door: Wanderer Entrance
953- pos: [763, 1465]
954 room: Outside The Wanderer
955 door: Tower Entrance
956- pos: [1655, 1151]
957 room: Number Hunt
958 door: Eights
959- pos: [1623, 1044]
960 room: Art Gallery
961 door: Second Floor
962- pos: [1623, 1019]
963 room: Art Gallery
964 door: Third Floor
965- pos: [1623, 991]
966 room: Art Gallery
967 door: Fourth Floor
968- pos: [1623, 964]
969 room: Art Gallery
970 door: Fifth Floor
971- pos: [1511, 1119]
972 room: Art Gallery
973 door: Exit
974- pos: [1654, 1116]
975 paintings:
976 - smile_painting_3
977 - flower_painting_2
978 - scenery_painting_0a
979 - map_painting
980 - fruitbowl_painting4
981 tags:
982 - smiley_gallery
983- pos: [1120, 1286]
984 room: Rhyme Room (Smiley)
985 door: Door to Target
986- pos: [1120, 1315]
987 tags:
988 - rhyme_smiley_target
989- pos: [792, 1137]
990 tags:
991 - rhyme_smiley_target
992- pos: [895, 1217]
993 room: Number Hunt
994 door: Nines
995- pos: [938, 1296]
996 room: Rhyme Room (Cross)
997 door: Exit
998- pos: [1120, 1195]
999 room: Rhyme Room (Circle)
1000 door: Door to Smiley
1001- pos: [1118, 1137]
1002 paintings:
1003 - arrows_painting_3
1004- pos: [1050, 1142]
1005 room: Rhyme Room (Looped Square)
1006 door: Door to Circle
1007- pos: [987, 1194]
1008 room: Rhyme Room (Looped Square)
1009 door: Door to Cross
1010- pos: [922, 1142]
1011 room: Rhyme Room (Looped Square)
1012 door: Door to Target
1013- pos: [852, 1200]
1014 room: Rhyme Room (Target)
1015 door: Door to Cross
1016- pos: [850, 1138]
1017 paintings:
1018 - arrows_painting_4
1019- pos: [1592, 1442]
1020 room: Room Room
1021 door: Cellar Exit
1022- pos: [1570, 938]
1023 room: Outside The Wise
1024 door: Wise Entrance
1025- pos: [1653, 935]
1026 paintings:
1027 - clock_painting_3
1028- pos: [369, 605]
1029 room: Outside The Scientific
1030 door: Scientific Entrance
1031- pos: [294, 602]
1032 paintings:
1033 - hi_solved_painting4
1034 tags:
1035 - hi_scientific
1036- pos: [814, 1001]
1037 room: Challenge Room
1038 door: Welcome Door
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index 4fd241a..a7565cf 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -21,7 +21,6 @@
21#include <tuple> 21#include <tuple>
22 22
23#include "game_data.h" 23#include "game_data.h"
24#include "logger.h"
25#include "tracker_frame.h" 24#include "tracker_frame.h"
26#include "tracker_state.h" 25#include "tracker_state.h"
27 26
@@ -72,11 +71,12 @@ struct APState {
72 bool sunwarp_shuffle = false; 71 bool sunwarp_shuffle = false;
73 72
74 std::map<std::string, std::string> painting_mapping; 73 std::map<std::string, std::string> painting_mapping;
74 std::set<std::string> painting_codomain;
75 std::map<int, SunwarpMapping> sunwarp_mapping; 75 std::map<int, SunwarpMapping> sunwarp_mapping;
76 76
77 void Connect(std::string server, std::string player, std::string password) { 77 void Connect(std::string server, std::string player, std::string password) {
78 if (!initialized) { 78 if (!initialized) {
79 TrackerLog("Initializing APState..."); 79 wxLogVerbose("Initializing APState...");
80 80
81 std::thread([this]() { 81 std::thread([this]() {
82 for (;;) { 82 for (;;) {
@@ -104,15 +104,16 @@ struct APState {
104 } 104 }
105 105
106 tracked_data_storage_keys.push_back("PlayerPos"); 106 tracked_data_storage_keys.push_back("PlayerPos");
107 tracked_data_storage_keys.push_back("Paintings");
107 108
108 initialized = true; 109 initialized = true;
109 } 110 }
110 111
111 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 112 tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
112 TrackerLog("Connecting to Archipelago server (" + server + ")..."); 113 wxLogStatus("Connecting to Archipelago server (%s)...", server);
113 114
114 { 115 {
115 TrackerLog("Destroying old AP client..."); 116 wxLogVerbose("Destroying old AP client...");
116 117
117 std::lock_guard client_guard(client_mutex); 118 std::lock_guard client_guard(client_mutex);
118 119
@@ -139,6 +140,7 @@ struct APState {
139 color_shuffle = false; 140 color_shuffle = false;
140 painting_shuffle = false; 141 painting_shuffle = false;
141 painting_mapping.clear(); 142 painting_mapping.clear();
143 painting_codomain.clear();
142 mastery_requirement = 21; 144 mastery_requirement = 21;
143 level_2_requirement = 223; 145 level_2_requirement = 223;
144 location_checks = kNORMAL_LOCATIONS; 146 location_checks = kNORMAL_LOCATIONS;
@@ -151,16 +153,17 @@ struct APState {
151 sunwarp_shuffle = false; 153 sunwarp_shuffle = false;
152 sunwarp_mapping.clear(); 154 sunwarp_mapping.clear();
153 155
156 std::mutex connection_mutex;
154 connected = false; 157 connected = false;
155 has_connection_result = false; 158 has_connection_result = false;
156 159
157 apclient->set_room_info_handler([this, player, password]() { 160 apclient->set_room_info_handler([this, player, password]() {
158 inventory.clear(); 161 inventory.clear();
159 162
160 TrackerLog("Connected to Archipelago server. Authenticating as " + 163 wxLogStatus("Connected to Archipelago server. Authenticating as %s %s",
161 player + 164 player,
162 (password.empty() ? " without password" 165 (password.empty() ? "without password"
163 : " with password " + password)); 166 : "with password " + password));
164 tracker_frame->SetStatusMessage( 167 tracker_frame->SetStatusMessage(
165 "Connected to Archipelago server. Authenticating..."); 168 "Connected to Archipelago server. Authenticating...");
166 169
@@ -172,23 +175,23 @@ struct APState {
172 [this](const std::list<int64_t>& locations) { 175 [this](const std::list<int64_t>& locations) {
173 for (const int64_t location_id : locations) { 176 for (const int64_t location_id : locations) {
174 checked_locations.insert(location_id); 177 checked_locations.insert(location_id);
175 TrackerLog("Location: " + std::to_string(location_id)); 178 wxLogVerbose("Location: %lld", location_id);
176 } 179 }
177 180
178 RefreshTracker(); 181 RefreshTracker(false);
179 }); 182 });
180 183
181 apclient->set_slot_disconnected_handler([this]() { 184 apclient->set_slot_disconnected_handler([this]() {
182 tracker_frame->SetStatusMessage( 185 tracker_frame->SetStatusMessage(
183 "Disconnected from Archipelago. Attempting to reconnect..."); 186 "Disconnected from Archipelago. Attempting to reconnect...");
184 TrackerLog( 187 wxLogStatus(
185 "Slot disconnected from Archipelago. Attempting to reconnect..."); 188 "Slot disconnected from Archipelago. Attempting to reconnect...");
186 }); 189 });
187 190
188 apclient->set_socket_disconnected_handler([this]() { 191 apclient->set_socket_disconnected_handler([this]() {
189 tracker_frame->SetStatusMessage( 192 tracker_frame->SetStatusMessage(
190 "Disconnected from Archipelago. Attempting to reconnect..."); 193 "Disconnected from Archipelago. Attempting to reconnect...");
191 TrackerLog( 194 wxLogStatus(
192 "Socket disconnected from Archipelago. Attempting to reconnect..."); 195 "Socket disconnected from Archipelago. Attempting to reconnect...");
193 }); 196 });
194 197
@@ -196,10 +199,10 @@ struct APState {
196 [this](const std::list<APClient::NetworkItem>& items) { 199 [this](const std::list<APClient::NetworkItem>& items) {
197 for (const APClient::NetworkItem& item : items) { 200 for (const APClient::NetworkItem& item : items) {
198 inventory[item.item]++; 201 inventory[item.item]++;
199 TrackerLog("Item: " + std::to_string(item.item)); 202 wxLogVerbose("Item: %lld", item.item);
200 } 203 }
201 204
202 RefreshTracker(); 205 RefreshTracker(false);
203 }); 206 });
204 207
205 apclient->set_retrieved_handler( 208 apclient->set_retrieved_handler(
@@ -208,20 +211,20 @@ struct APState {
208 HandleDataStorage(key, value); 211 HandleDataStorage(key, value);
209 } 212 }
210 213
211 RefreshTracker(); 214 RefreshTracker(false);
212 }); 215 });
213 216
214 apclient->set_set_reply_handler([this](const std::string& key, 217 apclient->set_set_reply_handler([this](const std::string& key,
215 const nlohmann::json& value, 218 const nlohmann::json& value,
216 const nlohmann::json&) { 219 const nlohmann::json&) {
217 HandleDataStorage(key, value); 220 HandleDataStorage(key, value);
218 RefreshTracker(); 221 RefreshTracker(false);
219 }); 222 });
220 223
221 apclient->set_slot_connected_handler([this]( 224 apclient->set_slot_connected_handler([this, &connection_mutex](
222 const nlohmann::json& slot_data) { 225 const nlohmann::json& slot_data) {
223 tracker_frame->SetStatusMessage("Connected to Archipelago!"); 226 tracker_frame->SetStatusMessage("Connected to Archipelago!");
224 TrackerLog("Connected to Archipelago!"); 227 wxLogStatus("Connected to Archipelago!");
225 228
226 data_storage_prefix = 229 data_storage_prefix =
227 "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; 230 "Lingo_" + std::to_string(apclient->get_player_number()) + "_";
@@ -266,6 +269,7 @@ struct APState {
266 for (const auto& mapping_it : 269 for (const auto& mapping_it :
267 slot_data["painting_entrance_to_exit"].items()) { 270 slot_data["painting_entrance_to_exit"].items()) {
268 painting_mapping[mapping_it.key()] = mapping_it.value(); 271 painting_mapping[mapping_it.key()] = mapping_it.value();
272 painting_codomain.insert(mapping_it.value());
269 } 273 }
270 } 274 }
271 275
@@ -281,11 +285,6 @@ struct APState {
281 } 285 }
282 } 286 }
283 287
284 connected = true;
285 has_connection_result = true;
286
287 RefreshTracker();
288
289 std::list<std::string> corrected_keys; 288 std::list<std::string> corrected_keys;
290 for (const std::string& key : tracked_data_storage_keys) { 289 for (const std::string& key : tracked_data_storage_keys) {
291 corrected_keys.push_back(data_storage_prefix + key); 290 corrected_keys.push_back(data_storage_prefix + key);
@@ -302,12 +301,23 @@ struct APState {
302 301
303 apclient->Get(corrected_keys); 302 apclient->Get(corrected_keys);
304 apclient->SetNotify(corrected_keys); 303 apclient->SetNotify(corrected_keys);
304
305 {
306 std::lock_guard connection_lock(connection_mutex);
307 if (!has_connection_result) {
308 connected = true;
309 has_connection_result = true;
310 }
311 }
305 }); 312 });
306 313
307 apclient->set_slot_refused_handler( 314 apclient->set_slot_refused_handler(
308 [this](const std::list<std::string>& errors) { 315 [this, &connection_mutex](const std::list<std::string>& errors) {
309 connected = false; 316 {
310 has_connection_result = true; 317 std::lock_guard connection_lock(connection_mutex);
318 connected = false;
319 has_connection_result = true;
320 }
311 321
312 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 322 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
313 323
@@ -336,7 +346,7 @@ struct APState {
336 } 346 }
337 347
338 std::string full_message = hatkirby::implode(error_messages, " "); 348 std::string full_message = hatkirby::implode(error_messages, " ");
339 TrackerLog(full_message); 349 wxLogError(wxString(full_message));
340 350
341 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); 351 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
342 }); 352 });
@@ -346,18 +356,29 @@ struct APState {
346 int timeout = 5000; // 5 seconds 356 int timeout = 5000; // 5 seconds
347 int interval = 100; 357 int interval = 100;
348 int remaining_loops = timeout / interval; 358 int remaining_loops = timeout / interval;
349 while (!has_connection_result) { 359 while (true) {
350 if (interval == 0) { 360 {
351 connected = false; 361 std::lock_guard connection_lock(connection_mutex);
352 has_connection_result = true; 362 if (has_connection_result) {
363 break;
364 }
365 }
353 366
367 if (interval == 0) {
354 DestroyClient(); 368 DestroyClient();
355 369
356 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 370 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
357 371 wxLogStatus("Timeout while connecting to Archipelago server.");
358 TrackerLog("Timeout while connecting to Archipelago server.");
359 wxMessageBox("Timeout while connecting to Archipelago server.", 372 wxMessageBox("Timeout while connecting to Archipelago server.",
360 "Connection failed", wxOK | wxICON_ERROR); 373 "Connection failed", wxOK | wxICON_ERROR);
374
375 {
376 std::lock_guard connection_lock(connection_mutex);
377 connected = false;
378 has_connection_result = true;
379 }
380
381 break;
361 } 382 }
362 383
363 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 384 std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -366,7 +387,8 @@ struct APState {
366 } 387 }
367 388
368 if (connected) { 389 if (connected) {
369 RefreshTracker(); 390 ResetReachabilityRequirements();
391 RefreshTracker(true);
370 } else { 392 } else {
371 client_active = false; 393 client_active = false;
372 } 394 }
@@ -375,12 +397,11 @@ struct APState {
375 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 397 void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
376 if (value.is_boolean()) { 398 if (value.is_boolean()) {
377 data_storage[key] = value.get<bool>(); 399 data_storage[key] = value.get<bool>();
378 TrackerLog("Data storage " + key + " retrieved as " + 400 wxLogVerbose("Data storage %s retrieved as %s", key,
379 (value.get<bool>() ? "true" : "false")); 401 (value.get<bool>() ? "true" : "false"));
380 } else if (value.is_number()) { 402 } else if (value.is_number()) {
381 data_storage[key] = value.get<int>(); 403 data_storage[key] = value.get<int>();
382 TrackerLog("Data storage " + key + " retrieved as " + 404 wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>());
383 std::to_string(value.get<int>()));
384 } else if (value.is_object()) { 405 } else if (value.is_object()) {
385 if (key.ends_with("PlayerPos")) { 406 if (key.ends_with("PlayerPos")) {
386 auto map_value = value.get<std::map<std::string, int>>(); 407 auto map_value = value.get<std::map<std::string, int>>();
@@ -389,7 +410,7 @@ struct APState {
389 data_storage[key] = value.get<std::map<std::string, int>>(); 410 data_storage[key] = value.get<std::map<std::string, int>>();
390 } 411 }
391 412
392 TrackerLog("Data storage " + key + " retrieved as dictionary"); 413 wxLogVerbose("Data storage %s retrieved as dictionary", key);
393 } else if (value.is_null()) { 414 } else if (value.is_null()) {
394 if (key.ends_with("PlayerPos")) { 415 if (key.ends_with("PlayerPos")) {
395 player_pos = std::nullopt; 416 player_pos = std::nullopt;
@@ -397,7 +418,19 @@ struct APState {
397 data_storage.erase(key); 418 data_storage.erase(key);
398 } 419 }
399 420
400 TrackerLog("Data storage " + key + " retrieved as null"); 421 wxLogVerbose("Data storage %s retrieved as null", key);
422 } else if (value.is_array()) {
423 auto list_value = value.get<std::vector<std::string>>();
424
425 if (key.ends_with("Paintings")) {
426 data_storage[key] =
427 std::set<std::string>(list_value.begin(), list_value.end());
428 } else {
429 data_storage[key] = list_value;
430 }
431
432 wxLogVerbose("Data storage %s retrieved as list: [%s]", key,
433 hatkirby::implode(list_value, ", "));
401 } 434 }
402 } 435 }
403 436
@@ -420,22 +453,46 @@ struct APState {
420 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 453 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
421 } 454 }
422 455
423 void RefreshTracker() { 456 const std::set<std::string>& GetCheckedPaintings() {
424 TrackerLog("Refreshing display..."); 457 std::string key = data_storage_prefix + "Paintings";
458 if (!data_storage.count(key)) {
459 data_storage[key] = std::set<std::string>();
460 }
461
462 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
463 }
464
465 bool IsPaintingChecked(const std::string& painting_id) {
466 const auto& checked_paintings = GetCheckedPaintings();
467
468 return checked_paintings.count(painting_id) ||
469 (painting_mapping.count(painting_id) &&
470 checked_paintings.count(painting_mapping.at(painting_id)));
471 }
472
473 void RefreshTracker(bool reset) {
474 wxLogVerbose("Refreshing display...");
425 475
426 RecalculateReachability(); 476 RecalculateReachability();
427 tracker_frame->UpdateIndicators(); 477
478 if (reset) {
479 tracker_frame->ResetIndicators();
480 } else {
481 tracker_frame->UpdateIndicators();
482 }
428 } 483 }
429 484
430 int64_t GetItemId(const std::string& item_name) { 485 int64_t GetItemId(const std::string& item_name) {
431 int64_t ap_id = apclient->get_item_id(item_name); 486 int64_t ap_id = apclient->get_item_id(item_name);
432 if (ap_id == APClient::INVALID_NAME_ID) { 487 if (ap_id == APClient::INVALID_NAME_ID) {
433 TrackerLog("Could not find AP item ID for " + item_name); 488 wxLogError("Could not find AP item ID for %s", item_name);
434 } 489 }
435 490
436 return ap_id; 491 return ap_id;
437 } 492 }
438 493
494 std::string GetItemName(int id) { return apclient->get_item_name(id); }
495
439 bool HasReachedGoal() { 496 bool HasReachedGoal() {
440 return data_storage.count(victory_data_storage_key) && 497 return data_storage.count(victory_data_storage_key) &&
441 std::any_cast<int>(data_storage.at(victory_data_storage_key)) == 498 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
@@ -474,6 +531,10 @@ bool AP_HasItem(int item_id, int quantity) {
474 return GetState().HasItem(item_id, quantity); 531 return GetState().HasItem(item_id, quantity);
475} 532}
476 533
534std::string AP_GetItemName(int item_id) {
535 return GetState().GetItemName(item_id);
536}
537
477DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 538DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; }
478 539
479bool AP_AreDoorsGrouped() { return GetState().group_doors; } 540bool AP_AreDoorsGrouped() { return GetState().group_doors; }
@@ -482,10 +543,22 @@ bool AP_IsColorShuffle() { return GetState().color_shuffle; }
482 543
483bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 544bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; }
484 545
485const std::map<std::string, std::string> AP_GetPaintingMapping() { 546const std::map<std::string, std::string>& AP_GetPaintingMapping() {
486 return GetState().painting_mapping; 547 return GetState().painting_mapping;
487} 548}
488 549
550bool AP_IsPaintingMappedTo(const std::string& painting_id) {
551 return GetState().painting_codomain.count(painting_id);
552}
553
554const std::set<std::string>& AP_GetCheckedPaintings() {
555 return GetState().GetCheckedPaintings();
556}
557
558bool AP_IsPaintingChecked(const std::string& painting_id) {
559 return GetState().IsPaintingChecked(painting_id);
560}
561
489int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 562int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }
490 563
491int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 564int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }
diff --git a/src/ap_state.h b/src/ap_state.h index c514489..190b21f 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,6 +49,8 @@ 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_AreDoorsGrouped(); 56bool AP_AreDoorsGrouped();
@@ -56,7 +59,13 @@ bool AP_IsColorShuffle();
56 59
57bool AP_IsPaintingShuffle(); 60bool AP_IsPaintingShuffle();
58 61
59const std::map<std::string, std::string> AP_GetPaintingMapping(); 62const std::map<std::string, std::string>& AP_GetPaintingMapping();
63
64bool AP_IsPaintingMappedTo(const std::string& painting_id);
65
66const std::set<std::string>& AP_GetCheckedPaintings();
67
68bool AP_IsPaintingChecked(const std::string& painting_id);
60 69
61int AP_GetMasteryRequirement(); 70int AP_GetMasteryRequirement();
62 71
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 0567623..1ccf511 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -1,5 +1,11 @@
1#include "game_data.h" 1#include "game_data.h"
2 2
3#include <wx/wxprec.h>
4
5#ifndef WX_PRECOMP
6#include <wx/wx.h>
7#endif
8
3#include <hkutil/string.h> 9#include <hkutil/string.h>
4#include <yaml-cpp/yaml.h> 10#include <yaml-cpp/yaml.h>
5 11
@@ -7,7 +13,6 @@
7#include <sstream> 13#include <sstream>
8 14
9#include "global.h" 15#include "global.h"
10#include "logger.h"
11 16
12namespace { 17namespace {
13 18
@@ -31,9 +36,7 @@ LingoColor GetColorForString(const std::string &str) {
31 } else if (str == "purple") { 36 } else if (str == "purple") {
32 return LingoColor::kPurple; 37 return LingoColor::kPurple;
33 } else { 38 } else {
34 std::ostringstream errmsg; 39 wxLogError("Invalid color: %s", str);
35 errmsg << "Invalid color: " << str;
36 TrackerLog(errmsg.str());
37 40
38 return LingoColor::kNone; 41 return LingoColor::kNone;
39 } 42 }
@@ -45,12 +48,15 @@ struct GameData {
45 std::vector<Panel> panels_; 48 std::vector<Panel> panels_;
46 std::vector<PanelDoor> panel_doors_; 49 std::vector<PanelDoor> panel_doors_;
47 std::vector<MapArea> map_areas_; 50 std::vector<MapArea> map_areas_;
51 std::vector<SubwayItem> subway_items_;
52 std::vector<PaintingExit> paintings_;
48 53
49 std::map<std::string, int> room_by_id_; 54 std::map<std::string, int> room_by_id_;
50 std::map<std::string, int> door_by_id_; 55 std::map<std::string, int> door_by_id_;
51 std::map<std::string, int> panel_by_id_; 56 std::map<std::string, int> panel_by_id_;
52 std::map<std::string, int> panel_doors_by_id_; 57 std::map<std::string, int> panel_doors_by_id_;
53 std::map<std::string, int> area_by_id_; 58 std::map<std::string, int> area_by_id_;
59 std::map<std::string, int> painting_by_id_;
54 60
55 std::vector<int> door_definition_order_; 61 std::vector<int> door_definition_order_;
56 62
@@ -63,6 +69,9 @@ struct GameData {
63 69
64 std::vector<int> sunwarp_doors_; 70 std::vector<int> sunwarp_doors_;
65 71
72 std::map<std::string, int> subway_item_by_painting_;
73 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
74
66 bool loaded_area_data_ = false; 75 bool loaded_area_data_ = false;
67 std::set<std::string> malconfigured_areas_; 76 std::set<std::string> malconfigured_areas_;
68 77
@@ -81,9 +90,7 @@ struct GameData {
81 ap_id_by_color_[GetColorForString(input_name)] = 90 ap_id_by_color_[GetColorForString(input_name)] =
82 ids_config["special_items"][color_name].as<int>(); 91 ids_config["special_items"][color_name].as<int>();
83 } else { 92 } else {
84 std::ostringstream errmsg; 93 wxLogError("Missing AP item ID for color %s", color_name);
85 errmsg << "Missing AP item ID for color " << color_name;
86 TrackerLog(errmsg.str());
87 } 94 }
88 }; 95 };
89 96
@@ -158,8 +165,9 @@ struct GameData {
158 } 165 }
159 default: { 166 default: {
160 // This shouldn't happen. 167 // This shouldn't happen.
161 std::cout << "Error reading game data: " << entrance_it 168 std::ostringstream formatted;
162 << std::endl; 169 formatted << entrance_it;
170 wxLogError("Error reading game data: %s", formatted.str());
163 break; 171 break;
164 } 172 }
165 } 173 }
@@ -256,6 +264,11 @@ struct GameData {
256 achievement_panels_.push_back(panel_id); 264 achievement_panels_.push_back(panel_id);
257 } 265 }
258 266
267 if (panel_it.second["location_name"]) {
268 panels_[panel_id].location_name =
269 panel_it.second["location_name"].as<std::string>();
270 }
271
259 if (panel_it.second["hunt"]) { 272 if (panel_it.second["hunt"]) {
260 panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); 273 panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>();
261 } 274 }
@@ -279,10 +292,8 @@ struct GameData {
279 [panels_[panel_id].name] 292 [panels_[panel_id].name]
280 .as<int>(); 293 .as<int>();
281 } else { 294 } else {
282 std::ostringstream errmsg; 295 wxLogError("Missing AP location ID for panel %s - %s",
283 errmsg << "Missing AP location ID for panel " 296 rooms_[room_id].name, panels_[panel_id].name);
284 << rooms_[room_id].name << " - " << panels_[panel_id].name;
285 TrackerLog(errmsg.str());
286 } 297 }
287 } 298 }
288 } 299 }
@@ -345,10 +356,8 @@ struct GameData {
345 [doors_[door_id].name]["item"] 356 [doors_[door_id].name]["item"]
346 .as<int>(); 357 .as<int>();
347 } else { 358 } else {
348 std::ostringstream errmsg; 359 wxLogError("Missing AP item ID for door %s - %s",
349 errmsg << "Missing AP item ID for door " << rooms_[room_id].name 360 rooms_[room_id].name, doors_[door_id].name);
350 << " - " << doors_[door_id].name;
351 TrackerLog(errmsg.str());
352 } 361 }
353 } 362 }
354 363
@@ -362,10 +371,8 @@ struct GameData {
362 ids_config["door_groups"][doors_[door_id].group_name] 371 ids_config["door_groups"][doors_[door_id].group_name]
363 .as<int>(); 372 .as<int>();
364 } else { 373 } else {
365 std::ostringstream errmsg; 374 wxLogError("Missing AP item ID for door group %s",
366 errmsg << "Missing AP item ID for door group " 375 doors_[door_id].group_name);
367 << doors_[door_id].group_name;
368 TrackerLog(errmsg.str());
369 } 376 }
370 } 377 }
371 378
@@ -375,13 +382,11 @@ struct GameData {
375 } else if (!door_it.second["skip_location"] && 382 } else if (!door_it.second["skip_location"] &&
376 !door_it.second["event"]) { 383 !door_it.second["event"]) {
377 if (has_external_panels) { 384 if (has_external_panels) {
378 std::ostringstream errmsg; 385 wxLogError(
379 errmsg 386 "%s - %s has panels from other rooms but does not have an "
380 << rooms_[room_id].name << " - " << doors_[door_id].name 387 "explicit location name and is not marked skip_location or "
381 << " has panels from other rooms but does not have an " 388 "event",
382 "explicit " 389 rooms_[room_id].name, doors_[door_id].name);
383 "location name and is not marked skip_location or event";
384 TrackerLog(errmsg.str());
385 } 390 }
386 391
387 doors_[door_id].location_name = 392 doors_[door_id].location_name =
@@ -401,10 +406,8 @@ struct GameData {
401 [doors_[door_id].name]["location"] 406 [doors_[door_id].name]["location"]
402 .as<int>(); 407 .as<int>();
403 } else { 408 } else {
404 std::ostringstream errmsg; 409 wxLogError("Missing AP location ID for door %s - %s",
405 errmsg << "Missing AP location ID for door " 410 rooms_[room_id].name, doors_[door_id].name);
406 << rooms_[room_id].name << " - " << doors_[door_id].name;
407 TrackerLog(errmsg.str());
408 } 411 }
409 } 412 }
410 413
@@ -478,12 +481,14 @@ struct GameData {
478 481
479 if (room_it.second["paintings"]) { 482 if (room_it.second["paintings"]) {
480 for (const auto &painting : room_it.second["paintings"]) { 483 for (const auto &painting : room_it.second["paintings"]) {
481 std::string painting_id = painting["id"].as<std::string>(); 484 std::string internal_id = painting["id"].as<std::string>();
482 room_by_painting_[painting_id] = room_id; 485 int painting_id = AddOrGetPainting(internal_id);
486 PaintingExit &painting_exit = paintings_[painting_id];
487 painting_exit.room = room_id;
483 488
484 if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { 489 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
485 PaintingExit painting_exit; 490 (!painting["disable"] || !painting["disable"].as<bool>())) {
486 painting_exit.id = painting_id; 491 painting_exit.entrance = true;
487 492
488 if (painting["required_door"]) { 493 if (painting["required_door"]) {
489 std::string rd_room = rooms_[room_id].name; 494 std::string rd_room = rooms_[room_id].name;
@@ -494,9 +499,9 @@ struct GameData {
494 painting_exit.door = AddOrGetDoor( 499 painting_exit.door = AddOrGetDoor(
495 rd_room, painting["required_door"]["door"].as<std::string>()); 500 rd_room, painting["required_door"]["door"].as<std::string>());
496 } 501 }
497
498 rooms_[room_id].paintings.push_back(painting_exit);
499 } 502 }
503
504 rooms_[room_id].paintings.push_back(painting_exit.id);
500 } 505 }
501 } 506 }
502 507
@@ -523,10 +528,8 @@ struct GameData {
523 progressive_item_id = 528 progressive_item_id =
524 ids_config["progression"][progressive_item_name].as<int>(); 529 ids_config["progression"][progressive_item_name].as<int>();
525 } else { 530 } else {
526 std::ostringstream errmsg; 531 wxLogError("Missing AP item ID for progressive item %s",
527 errmsg << "Missing AP item ID for progressive item " 532 progressive_item_name);
528 << progressive_item_name;
529 TrackerLog(errmsg.str());
530 } 533 }
531 534
532 if (progression_it.second["doors"]) { 535 if (progression_it.second["doors"]) {
@@ -600,8 +603,21 @@ struct GameData {
600 std::string room_name = rooms_[room_id].name; 603 std::string room_name = rooms_[room_id].name;
601 604
602 std::string area_name = room_name; 605 std::string area_name = room_name;
603 if (fold_areas.count(room_name)) { 606 std::string section_name = panel.name;
604 int fold_area_id = fold_areas[room_name]; 607 std::string location_name = room_name + " - " + panel.name;
608
609 if (!panel.location_name.empty()) {
610 location_name = panel.location_name;
611
612 size_t divider_pos = location_name.find(" - ");
613 if (divider_pos != std::string::npos) {
614 area_name = location_name.substr(0, divider_pos);
615 section_name = location_name.substr(divider_pos + 3);
616 }
617 }
618
619 if (fold_areas.count(area_name)) {
620 int fold_area_id = fold_areas[area_name];
605 area_name = map_areas_[fold_area_id].name; 621 area_name = map_areas_[fold_area_id].name;
606 } 622 }
607 623
@@ -617,15 +633,15 @@ struct GameData {
617 MapArea &map_area = map_areas_[area_id]; 633 MapArea &map_area = map_areas_[area_id];
618 // room field should be the original room ID 634 // room field should be the original room ID
619 map_area.locations.push_back( 635 map_area.locations.push_back(
620 {.name = panel.name, 636 {.name = section_name,
621 .ap_location_name = room_name + " - " + panel.name, 637 .ap_location_name = location_name,
622 .ap_location_id = panel.ap_location_id, 638 .ap_location_id = panel.ap_location_id,
623 .room = panel.room, 639 .room = panel.room,
624 .panels = {panel.id}, 640 .panels = {panel.id},
625 .classification = classification, 641 .classification = classification,
626 .hunt = panel.hunt}); 642 .hunt = panel.hunt});
627 locations_by_name[map_area.locations.back().ap_location_name] = { 643 locations_by_name[location_name] = {area_id,
628 area_id, map_area.locations.size() - 1}; 644 map_area.locations.size() - 1};
629 } 645 }
630 646
631 for (int door_id : door_definition_order_) { 647 for (int door_id : door_definition_order_) {
@@ -679,11 +695,101 @@ struct GameData {
679 } 695 }
680 } 696 }
681 697
698 for (const Room &room : rooms_) {
699 std::string area_name = room.name;
700 if (fold_areas.count(room.name)) {
701 int fold_area_id = fold_areas[room.name];
702 area_name = map_areas_[fold_area_id].name;
703 }
704
705 if (!room.paintings.empty()) {
706 int area_id = AddOrGetArea(area_name);
707 MapArea &map_area = map_areas_[area_id];
708
709 for (int painting_id : room.paintings) {
710 const PaintingExit &painting_obj = paintings_.at(painting_id);
711 if (painting_obj.entrance) {
712 map_area.paintings.push_back(painting_id);
713 }
714 }
715 }
716 }
717
682 // Report errors. 718 // Report errors.
683 for (const std::string &area : malconfigured_areas_) { 719 for (const std::string &area : malconfigured_areas_) {
684 std::ostringstream errstr; 720 wxLogError("Area data not found for: %s", area);
685 errstr << "Area data not found for: " << area; 721 }
686 TrackerLog(errstr.str()); 722
723 // Read in subway items.
724 YAML::Node subway_config =
725 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml"));
726 for (const auto &subway_it : subway_config) {
727 SubwayItem subway_item;
728 subway_item.id = subway_items_.size();
729 subway_item.x = subway_it["pos"][0].as<int>();
730 subway_item.y = subway_it["pos"][1].as<int>();
731
732 if (subway_it["door"]) {
733 subway_item.door = AddOrGetDoor(subway_it["room"].as<std::string>(),
734 subway_it["door"].as<std::string>());
735 }
736
737 if (subway_it["paintings"]) {
738 for (const auto &painting_it : subway_it["paintings"]) {
739 std::string painting_id = painting_it.as<std::string>();
740
741 subway_item.paintings.push_back(painting_id);
742 subway_item_by_painting_[painting_id] = subway_item.id;
743 }
744 }
745
746 if (subway_it["tags"]) {
747 for (const auto &tag_it : subway_it["tags"]) {
748 subway_item.tags.push_back(tag_it.as<std::string>());
749 }
750 }
751
752 if (subway_it["sunwarp"]) {
753 SubwaySunwarp sunwarp;
754 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
755
756 std::string sunwarp_type =
757 subway_it["sunwarp"]["type"].as<std::string>();
758 if (sunwarp_type == "final") {
759 sunwarp.type = SubwaySunwarpType::kFinal;
760 } else if (sunwarp_type == "exit") {
761 sunwarp.type = SubwaySunwarpType::kExit;
762 } else {
763 sunwarp.type = SubwaySunwarpType::kEnter;
764 }
765
766 subway_item.sunwarp = sunwarp;
767
768 subway_item_by_sunwarp_[sunwarp] = subway_item.id;
769
770 subway_item.door =
771 AddOrGetDoor("Sunwarps", std::to_string(sunwarp.dots) + " Sunwarp");
772 }
773
774 if (subway_it["special"]) {
775 subway_item.special = subway_it["special"].as<std::string>();
776 }
777
778 subway_items_.push_back(subway_item);
779 }
780
781 // Find singleton subway tags.
782 std::map<std::string, std::set<int>> subway_tags;
783 for (const SubwayItem &subway_item : subway_items_) {
784 for (const std::string &tag : subway_item.tags) {
785 subway_tags[tag].insert(subway_item.id);
786 }
787 }
788
789 for (const auto &[tag, items] : subway_tags) {
790 if (items.size() == 1) {
791 wxLogWarning("Singleton subway item tag: %s", tag);
792 }
687 } 793 }
688 } 794 }
689 795
@@ -700,8 +806,10 @@ struct GameData {
700 std::string full_name = room + " - " + door; 806 std::string full_name = room + " - " + door;
701 807
702 if (!door_by_id_.count(full_name)) { 808 if (!door_by_id_.count(full_name)) {
809 int door_id = doors_.size();
703 door_by_id_[full_name] = doors_.size(); 810 door_by_id_[full_name] = doors_.size();
704 doors_.push_back({.room = AddOrGetRoom(room), .name = door}); 811 doors_.push_back(
812 {.id = door_id, .room = AddOrGetRoom(room), .name = door});
705 } 813 }
706 814
707 return door_by_id_[full_name]; 815 return door_by_id_[full_name];
@@ -745,6 +853,16 @@ struct GameData {
745 853
746 return area_by_id_[area]; 854 return area_by_id_[area];
747 } 855 }
856
857 int AddOrGetPainting(std::string internal_id) {
858 if (!painting_by_id_.count(internal_id)) {
859 int painting_id = paintings_.size();
860 painting_by_id_[internal_id] = painting_id;
861 paintings_.push_back({.id = painting_id, .internal_id = internal_id});
862 }
863
864 return painting_by_id_[internal_id];
865 }
748}; 866};
749 867
750GameData &GetState() { 868GameData &GetState() {
@@ -754,6 +872,10 @@ GameData &GetState() {
754 872
755} // namespace 873} // namespace
756 874
875bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
876 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
877}
878
757const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; } 879const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; }
758 880
759const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); } 881const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); }
@@ -780,8 +902,12 @@ const Panel &GD_GetPanel(int panel_id) {
780 return GetState().panels_.at(panel_id); 902 return GetState().panels_.at(panel_id);
781} 903}
782 904
783int GD_GetRoomForPainting(const std::string &painting_id) { 905const PaintingExit &GD_GetPaintingExit(int painting_id) {
784 return GetState().room_by_painting_.at(painting_id); 906 return GetState().paintings_.at(painting_id);
907}
908
909int GD_GetPaintingByName(const std::string &name) {
910 return GetState().painting_by_id_.at(name);
785} 911}
786 912
787const std::vector<int> &GD_GetAchievementPanels() { 913const std::vector<int> &GD_GetAchievementPanels() {
@@ -799,3 +925,24 @@ const std::vector<int> &GD_GetSunwarpDoors() {
799int GD_GetRoomForSunwarp(int index) { 925int GD_GetRoomForSunwarp(int index) {
800 return GetState().room_by_sunwarp_.at(index); 926 return GetState().room_by_sunwarp_.at(index);
801} 927}
928
929const std::vector<SubwayItem> &GD_GetSubwayItems() {
930 return GetState().subway_items_;
931}
932
933const SubwayItem &GD_GetSubwayItem(int id) {
934 return GetState().subway_items_.at(id);
935}
936
937int GD_GetSubwayItemForPainting(const std::string &painting_id) {
938#ifndef NDEBUG
939 if (!GetState().subway_item_by_painting_.count(painting_id)) {
940 wxLogError("No subway item for painting %s", painting_id);
941 }
942#endif
943 return GetState().subway_item_by_painting_.at(painting_id);
944}
945
946int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
947 return GetState().subway_item_by_sunwarp_.at(sunwarp);
948}
diff --git a/src/game_data.h b/src/game_data.h index 09824d7..aca4c3d 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -50,6 +50,7 @@ struct Panel {
50 bool exclude_reduce = false; 50 bool exclude_reduce = false;
51 bool achievement = false; 51 bool achievement = false;
52 std::string achievement_name; 52 std::string achievement_name;
53 std::string location_name;
53 bool non_counting = false; 54 bool non_counting = false;
54 int ap_location_id = -1; 55 int ap_location_id = -1;
55 bool hunt = false; 56 bool hunt = false;
@@ -63,6 +64,7 @@ struct ProgressiveRequirement {
63}; 64};
64 65
65struct Door { 66struct Door {
67 int id;
66 int room; 68 int room;
67 std::string name; 69 std::string name;
68 std::string location_name; 70 std::string location_name;
@@ -93,14 +95,17 @@ struct Exit {
93}; 95};
94 96
95struct PaintingExit { 97struct PaintingExit {
96 std::string id; 98 int id;
99 int room;
100 std::string internal_id;
97 std::optional<int> door; 101 std::optional<int> door;
102 bool entrance = false;
98}; 103};
99 104
100struct Room { 105struct Room {
101 std::string name; 106 std::string name;
102 std::vector<Exit> exits; 107 std::vector<Exit> exits;
103 std::vector<PaintingExit> paintings; 108 std::vector<int> paintings;
104 std::vector<int> sunwarps; 109 std::vector<int> sunwarps;
105 std::vector<int> panels; 110 std::vector<int> panels;
106}; 111};
@@ -119,12 +124,37 @@ struct MapArea {
119 int id; 124 int id;
120 std::string name; 125 std::string name;
121 std::vector<Location> locations; 126 std::vector<Location> locations;
127 std::vector<int> paintings;
122 int map_x; 128 int map_x;
123 int map_y; 129 int map_y;
124 int classification = 0; 130 int classification = 0;
125 bool hunt = false; 131 bool hunt = false;
126}; 132};
127 133
134enum class SubwaySunwarpType {
135 kEnter,
136 kExit,
137 kFinal
138};
139
140struct SubwaySunwarp {
141 int dots;
142 SubwaySunwarpType type;
143
144 bool operator<(const SubwaySunwarp& rhs) const;
145};
146
147struct SubwayItem {
148 int id;
149 int x;
150 int y;
151 std::optional<int> door;
152 std::vector<std::string> paintings;
153 std::vector<std::string> tags;
154 std::optional<SubwaySunwarp> sunwarp;
155 std::optional<std::string> special;
156};
157
128const std::vector<MapArea>& GD_GetMapAreas(); 158const std::vector<MapArea>& GD_GetMapAreas();
129const MapArea& GD_GetMapArea(int id); 159const MapArea& GD_GetMapArea(int id);
130int GD_GetRoomByName(const std::string& name); 160int GD_GetRoomByName(const std::string& name);
@@ -134,10 +164,15 @@ const Door& GD_GetDoor(int door_id);
134int GD_GetDoorByName(const std::string& name); 164int GD_GetDoorByName(const std::string& name);
135const Panel& GD_GetPanel(int panel_id); 165const Panel& GD_GetPanel(int panel_id);
136const PanelDoor& GD_GetPanelDoor(int panel_door_id); 166const PanelDoor& GD_GetPanelDoor(int panel_door_id);
137int GD_GetRoomForPainting(const std::string& painting_id); 167const PaintingExit& GD_GetPaintingExit(int painting_id);
168int GD_GetPaintingByName(const std::string& name);
138const std::vector<int>& GD_GetAchievementPanels(); 169const std::vector<int>& GD_GetAchievementPanels();
139int GD_GetItemIdForColor(LingoColor color); 170int GD_GetItemIdForColor(LingoColor color);
140const std::vector<int>& GD_GetSunwarpDoors(); 171const std::vector<int>& GD_GetSunwarpDoors();
141int GD_GetRoomForSunwarp(int index); 172int GD_GetRoomForSunwarp(int index);
173const std::vector<SubwayItem>& GD_GetSubwayItems();
174const SubwayItem& GD_GetSubwayItem(int id);
175int GD_GetSubwayItemForPainting(const std::string& painting_id);
176int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
142 177
143#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 178#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/logger.cpp b/src/logger.cpp deleted file mode 100644 index 4b722c8..0000000 --- a/src/logger.cpp +++ /dev/null
@@ -1,32 +0,0 @@
1#include "logger.h"
2
3#include <chrono>
4#include <fstream>
5#include <mutex>
6
7#include "global.h"
8
9namespace {
10
11class Logger {
12 public:
13 Logger() : logfile_(GetAbsolutePath("debug.log")) {}
14
15 void LogLine(const std::string& text) {
16 std::lock_guard guard(file_mutex_);
17 logfile_ << "[" << std::chrono::system_clock::now() << "] " << text
18 << std::endl;
19 logfile_.flush();
20 }
21
22 private:
23 std::ofstream logfile_;
24 std::mutex file_mutex_;
25};
26
27} // namespace
28
29void TrackerLog(const std::string& text) {
30 static Logger* instance = new Logger();
31 instance->LogLine(text);
32}
diff --git a/src/logger.h b/src/logger.h deleted file mode 100644 index db9bb49..0000000 --- a/src/logger.h +++ /dev/null
@@ -1,8 +0,0 @@
1#ifndef LOGGER_H_6E7B9594
2#define LOGGER_H_6E7B9594
3
4#include <string>
5
6void TrackerLog(const std::string& text);
7
8#endif /* end of include guard: LOGGER_H_6E7B9594 */
diff --git a/src/main.cpp b/src/main.cpp index fe9aceb..b327b25 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -4,18 +4,41 @@
4#include <wx/wx.h> 4#include <wx/wx.h>
5#endif 5#endif
6 6
7#include <fstream>
8
9#include "global.h"
7#include "tracker_config.h" 10#include "tracker_config.h"
8#include "tracker_frame.h" 11#include "tracker_frame.h"
9 12
13static std::ofstream* logfile;
14
10class TrackerApp : public wxApp { 15class TrackerApp : public wxApp {
11 public: 16 public:
12 virtual bool OnInit() { 17 virtual bool OnInit() {
18 logfile = new std::ofstream(GetAbsolutePath("debug.log"));
19 wxLog::SetActiveTarget(new wxLogStream(logfile));
20 wxLog::SetVerbose(true);
21
22#ifndef NDEBUG
23 wxLog::SetActiveTarget(new wxLogWindow(nullptr, "Debug Log"));
24#endif
25
13 GetTrackerConfig().Load(); 26 GetTrackerConfig().Load();
14 27
15 TrackerFrame *frame = new TrackerFrame(); 28 TrackerFrame *frame = new TrackerFrame();
16 frame->Show(true); 29 frame->Show(true);
17 return true; 30 return true;
18 } 31 }
32
33 bool OnExceptionInMainLoop() override {
34 try {
35 throw;
36 } catch (const std::exception& ex) {
37 wxLogError(ex.what());
38 }
39
40 return false;
41 }
19}; 42};
20 43
21wxIMPLEMENT_APP(TrackerApp); 44wxIMPLEMENT_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..408c4f0 --- /dev/null +++ b/src/subway_map.cpp
@@ -0,0 +1,728 @@
1#include "subway_map.h"
2
3#include <wx/dcbuffer.h>
4#include <wx/dcgraph.h>
5
6#include <sstream>
7
8#include "ap_state.h"
9#include "game_data.h"
10#include "global.h"
11#include "tracker_state.h"
12
13constexpr int AREA_ACTUAL_SIZE = 21;
14constexpr int OWL_ACTUAL_SIZE = 32;
15
16enum class ItemDrawType { kNone, kBox, kOwl };
17
18SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
19 SetBackgroundStyle(wxBG_STYLE_PAINT);
20
21 map_image_ =
22 wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG);
23 if (!map_image_.IsOk()) {
24 return;
25 }
26
27 owl_image_ =
28 wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG);
29 if (!owl_image_.IsOk()) {
30 return;
31 }
32
33 unchecked_eye_ =
34 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
35 wxBITMAP_TYPE_PNG)
36 .Scale(32, 32));
37 checked_eye_ = wxBitmap(
38 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
39 .Scale(32, 32));
40
41 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
42 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
43 static_cast<float>(map_image_.GetHeight())});
44 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
45 tree_->add(subway_item.id);
46 }
47
48 Redraw();
49
50 scroll_timer_ = new wxTimer(this);
51
52 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
53 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
54 Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
55 Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
56 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
57 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
58
59 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15});
60 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
61
62 help_button_ = new wxButton(this, wxID_ANY, "Help");
63 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
64 SetUpHelpButton();
65}
66
67void SubwayMap::OnConnect() {
68 networks_.Clear();
69
70 std::map<std::string, std::vector<int>> tagged;
71 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
72 if (AP_HasEarlyColorHallways() &&
73 (subway_item.special == "starting_room_paintings" ||
74 subway_item.special == "early_color_hallways")) {
75 tagged["early_color_hallways"].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::ostringstream tag;
89 tag << "sunwarp" << subway_item.sunwarp->dots;
90
91 tagged[tag.str()].push_back(subway_item.id);
92 }
93
94 if (!AP_IsPilgrimageEnabled() &&
95 (subway_item.special == "sun_painting" ||
96 subway_item.special == "sun_painting_exit")) {
97 tagged["sun_painting"].push_back(subway_item.id);
98 }
99 }
100
101 if (AP_IsSunwarpShuffle()) {
102 for (const auto &[index, mapping] : AP_GetSunwarpMapping()) {
103 std::ostringstream tag;
104 tag << "sunwarp" << mapping.dots;
105
106 SubwaySunwarp fromWarp;
107 if (index < 6) {
108 fromWarp.dots = index + 1;
109 fromWarp.type = SubwaySunwarpType::kEnter;
110 } else {
111 fromWarp.dots = index - 5;
112 fromWarp.type = SubwaySunwarpType::kExit;
113 }
114
115 SubwaySunwarp toWarp;
116 if (mapping.exit_index < 6) {
117 toWarp.dots = mapping.exit_index + 1;
118 toWarp.type = SubwaySunwarpType::kEnter;
119 } else {
120 toWarp.dots = mapping.exit_index - 5;
121 toWarp.type = SubwaySunwarpType::kExit;
122 }
123
124 tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
125 tagged[tag.str()].push_back(GD_GetSubwayItemForSunwarp(toWarp));
126 }
127 }
128
129 for (const auto &[tag, items] : tagged) {
130 // Pairwise connect all items with the same tag.
131 for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end();
132 tag_it1++) {
133 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
134 tag_it2++) {
135 networks_.AddLink(*tag_it1, *tag_it2);
136 }
137 }
138 }
139
140 checked_paintings_.clear();
141}
142
143void SubwayMap::UpdateIndicators() {
144 if (AP_IsPaintingShuffle()) {
145 for (const std::string &painting_id : AP_GetCheckedPaintings()) {
146 if (!checked_paintings_.count(painting_id)) {
147 checked_paintings_.insert(painting_id);
148
149 if (AP_GetPaintingMapping().count(painting_id)) {
150 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id),
151 GD_GetSubwayItemForPainting(
152 AP_GetPaintingMapping().at(painting_id)));
153 }
154 }
155 }
156 }
157
158 Redraw();
159}
160
161void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
162 SubwaySunwarp to_sunwarp) {
163 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
164 GD_GetSubwayItemForSunwarp(to_sunwarp));
165}
166
167void SubwayMap::Zoom(bool in) {
168 wxPoint focus_point;
169
170 if (mouse_position_) {
171 focus_point = *mouse_position_;
172 } else {
173 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
174 }
175
176 if (in) {
177 if (zoom_ < 3.0) {
178 SetZoom(zoom_ + 0.25, focus_point);
179 }
180 } else {
181 if (zoom_ > 1.0) {
182 SetZoom(zoom_ - 0.25, focus_point);
183 }
184 }
185}
186
187void SubwayMap::OnPaint(wxPaintEvent &event) {
188 if (GetSize() != rendered_.GetSize()) {
189 wxSize panel_size = GetSize();
190 wxSize image_size = map_image_.GetSize();
191
192 render_x_ = 0;
193 render_y_ = 0;
194 render_width_ = panel_size.GetWidth();
195 render_height_ = panel_size.GetHeight();
196
197 if (image_size.GetWidth() * panel_size.GetHeight() >
198 panel_size.GetWidth() * image_size.GetHeight()) {
199 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
200 image_size.GetWidth();
201 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
202 } else {
203 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
204 image_size.GetHeight();
205 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
206 }
207
208 SetZoomPos({zoom_x_, zoom_y_});
209
210 SetUpHelpButton();
211 }
212
213 wxBufferedPaintDC dc(this);
214 dc.SetBackground(*wxWHITE_BRUSH);
215 dc.Clear();
216
217 {
218 wxMemoryDC rendered_dc;
219 rendered_dc.SelectObject(rendered_);
220
221 int dst_x;
222 int dst_y;
223 int dst_w;
224 int dst_h;
225 int src_x;
226 int src_y;
227 int src_w;
228 int src_h;
229
230 int zoomed_width = render_width_ * zoom_;
231 int zoomed_height = render_height_ * zoom_;
232
233 if (zoomed_width <= GetSize().GetWidth()) {
234 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
235 dst_w = zoomed_width;
236 src_x = 0;
237 src_w = map_image_.GetWidth();
238 } else {
239 dst_x = 0;
240 dst_w = GetSize().GetWidth();
241 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
242 src_w =
243 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
244 }
245
246 if (zoomed_height <= GetSize().GetHeight()) {
247 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
248 dst_h = zoomed_height;
249 src_y = 0;
250 src_h = map_image_.GetHeight();
251 } else {
252 dst_y = 0;
253 dst_h = GetSize().GetHeight();
254 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
255 src_h =
256 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
257 }
258
259 wxGCDC gcdc(dc);
260 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
261 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
262 src_w, src_h);
263 }
264
265 if (hovered_item_) {
266 // Note that these requirements are duplicated on OnMouseClick so that it
267 // knows when an item has a hover effect.
268 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
269 if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) {
270 const std::map<std::string, bool> &report =
271 GetDoorRequirements(*subway_item.door);
272
273 int acc_height = 10;
274 int col_width = 0;
275
276 for (const auto &[text, obtained] : report) {
277 wxSize item_extent = dc.GetTextExtent(text);
278 int item_height = std::max(32, item_extent.GetHeight()) + 10;
279 acc_height += item_height;
280
281 if (item_extent.GetWidth() > col_width) {
282 col_width = item_extent.GetWidth();
283 }
284 }
285
286 int item_width = col_width + 10 + 32;
287 int full_width = item_width + 20;
288
289 wxPoint popup_pos =
290 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
291 subway_item.y + AREA_ACTUAL_SIZE / 2});
292
293 if (popup_pos.x + full_width > GetSize().GetWidth()) {
294 popup_pos.x = GetSize().GetWidth() - full_width;
295 }
296 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
297 popup_pos.y = GetSize().GetHeight() - acc_height;
298 }
299
300 dc.SetPen(*wxTRANSPARENT_PEN);
301 dc.SetBrush(*wxBLACK_BRUSH);
302 dc.DrawRectangle(popup_pos, {full_width, acc_height});
303
304 dc.SetFont(GetFont());
305
306 int cur_height = 10;
307
308 for (const auto &[text, obtained] : report) {
309 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
310
311 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
312
313 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
314 wxSize item_extent = dc.GetTextExtent(text);
315 dc.DrawText(
316 text,
317 popup_pos +
318 wxPoint{10 + 32 + 10,
319 cur_height + (32 - dc.GetFontMetrics().height) / 2});
320
321 cur_height += 10 + 32;
322 }
323 }
324
325 if (networks_.IsItemInNetwork(*hovered_item_)) {
326 dc.SetBrush(*wxTRANSPARENT_BRUSH);
327
328 for (const auto &[item_id1, item_id2] :
329 networks_.GetNetworkGraph(*hovered_item_)) {
330 const SubwayItem &item1 = GD_GetSubwayItem(item_id1);
331 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
332
333 wxPoint item1_pos = MapPosToRenderPos(
334 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
335 wxPoint item2_pos = MapPosToRenderPos(
336 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
337
338 int left = std::min(item1_pos.x, item2_pos.x);
339 int top = std::min(item1_pos.y, item2_pos.y);
340 int right = std::max(item1_pos.x, item2_pos.x);
341 int bottom = std::max(item1_pos.y, item2_pos.y);
342
343 int halfwidth = right - left;
344 int halfheight = bottom - top;
345
346 if (halfwidth < 4 || halfheight < 4) {
347 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
348 dc.DrawLine(item1_pos, item2_pos);
349 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
350 dc.DrawLine(item1_pos, item2_pos);
351 } else {
352 int ellipse_x;
353 int ellipse_y;
354 double start;
355 double end;
356
357 if (item1_pos.x > item2_pos.x) {
358 ellipse_y = top;
359
360 if (item1_pos.y > item2_pos.y) {
361 ellipse_x = left - halfwidth;
362
363 start = 0;
364 end = 90;
365 } else {
366 ellipse_x = left;
367
368 start = 90;
369 end = 180;
370 }
371 } else {
372 ellipse_y = top - halfheight;
373
374 if (item1_pos.y > item2_pos.y) {
375 ellipse_x = left - halfwidth;
376
377 start = 270;
378 end = 360;
379 } else {
380 ellipse_x = left;
381
382 start = 180;
383 end = 270;
384 }
385 }
386
387 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
388 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
389 halfheight * 2, start, end);
390 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
391 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
392 halfheight * 2, start, end);
393 }
394 }
395 }
396 }
397
398 event.Skip();
399}
400
401void SubwayMap::OnMouseMove(wxMouseEvent &event) {
402 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
403
404 std::vector<int> hovered = tree_->query(
405 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
406 if (!hovered.empty()) {
407 actual_hover_= hovered[0];
408 } else {
409 actual_hover_ = std::nullopt;
410 }
411
412 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
413 hovered_item_ = actual_hover_;
414
415 Refresh();
416 }
417
418 if (scroll_mode_) {
419 EvaluateScroll(event.GetPosition());
420 }
421
422 mouse_position_ = event.GetPosition();
423
424 event.Skip();
425}
426
427void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
428 double new_zoom = zoom_;
429 if (event.GetWheelRotation() > 0) {
430 new_zoom = std::min(3.0, zoom_ + 0.25);
431 } else {
432 new_zoom = std::max(1.0, zoom_ - 0.25);
433 }
434
435 if (zoom_ != new_zoom) {
436 SetZoom(new_zoom, event.GetPosition());
437 }
438
439 event.Skip();
440}
441
442void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
443 SetScrollSpeed(0, 0);
444 mouse_position_ = std::nullopt;
445}
446
447void SubwayMap::OnMouseClick(wxMouseEvent &event) {
448 bool finished = false;
449
450 if (actual_hover_) {
451 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
452 if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) ||
453 networks_.IsItemInNetwork(*hovered_item_)) {
454 if (actual_hover_ != hovered_item_) {
455 hovered_item_ = actual_hover_;
456
457 if (!hovered_item_) {
458 sticky_hover_ = false;
459 }
460
461 Refresh();
462 } else {
463 sticky_hover_ = !sticky_hover_;
464 }
465
466 finished = true;
467 }
468 }
469
470 if (!finished) {
471 if (scroll_mode_) {
472 scroll_mode_ = false;
473
474 SetScrollSpeed(0, 0);
475
476 SetCursor(wxCURSOR_ARROW);
477 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
478 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
479 event.GetPosition().y < GetSize().GetHeight() / 6 ||
480 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
481 scroll_mode_ = true;
482
483 EvaluateScroll(event.GetPosition());
484
485 SetCursor(wxCURSOR_CROSS);
486 } else {
487 sticky_hover_ = false;
488 }
489 }
490}
491
492void SubwayMap::OnTimer(wxTimerEvent &event) {
493 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
494 Refresh();
495}
496
497void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
498 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
499
500 if (new_zoom != zoom_) {
501 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
502 }
503}
504
505void SubwayMap::OnClickHelp(wxCommandEvent &event) {
506 wxMessageBox(
507 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
508 "corner.\nClick on a side of the screen to start panning. It will follow "
509 "your mouse. Click again to stop.\nHover over a door to see the "
510 "requirements to open it.\nHover over a warp or active painting to see "
511 "what it is connected to.\nIn painting shuffle, paintings that have not "
512 "yet been checked will not show their connections.\nA green shaded owl "
513 "means that there is a painting entrance there.\nA red shaded owl means "
514 "that there are only painting exits there.\nClick on a door or "
515 "warp to make the popup stick until you click again.",
516 "Subway Map Help");
517}
518
519void SubwayMap::Redraw() {
520 rendered_ = wxBitmap(map_image_);
521
522 wxMemoryDC dc;
523 dc.SelectObject(rendered_);
524
525 wxGCDC gcdc(dc);
526
527 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
528 ItemDrawType draw_type = ItemDrawType::kNone;
529 const wxBrush *brush_color = wxGREY_BRUSH;
530 std::optional<wxColour> shade_color;
531
532 if (AP_HasEarlyColorHallways() &&
533 (subway_item.special == "starting_room_paintings" ||
534 subway_item.special == "early_color_hallways")) {
535 draw_type = ItemDrawType::kOwl;
536
537 if (subway_item.special == "starting_room_paintings") {
538 shade_color = wxColour(0, 255, 0, 128);
539 } else {
540 shade_color = wxColour(255, 0, 0, 128);
541 }
542 } else if (subway_item.special == "sun_painting") {
543 if (!AP_IsPilgrimageEnabled()) {
544 if (IsDoorOpen(*subway_item.door)) {
545 draw_type = ItemDrawType::kOwl;
546 shade_color = wxColour(0, 255, 0, 128);
547 } else {
548 draw_type = ItemDrawType::kBox;
549 brush_color = wxRED_BRUSH;
550 }
551 }
552 } else if (!subway_item.paintings.empty()) {
553 if (AP_IsPaintingShuffle()) {
554 bool has_checked_painting = false;
555 bool has_unchecked_painting = false;
556 bool has_mapped_painting = false;
557 bool has_codomain_painting = false;
558
559 for (const std::string &painting_id : subway_item.paintings) {
560 if (checked_paintings_.count(painting_id)) {
561 has_checked_painting = true;
562
563 if (AP_GetPaintingMapping().count(painting_id)) {
564 has_mapped_painting = true;
565 } else if (AP_IsPaintingMappedTo(painting_id)) {
566 has_codomain_painting = true;
567 }
568 } else {
569 has_unchecked_painting = true;
570 }
571 }
572
573 if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) {
574 draw_type = ItemDrawType::kOwl;
575
576 if (has_checked_painting) {
577 if (has_mapped_painting) {
578 shade_color = wxColour(0, 255, 0, 128);
579 } else {
580 shade_color = wxColour(255, 0, 0, 128);
581 }
582 }
583 }
584 } else if (!subway_item.tags.empty()) {
585 draw_type = ItemDrawType::kOwl;
586 }
587 } else if (subway_item.door) {
588 draw_type = ItemDrawType::kBox;
589
590 if (IsDoorOpen(*subway_item.door)) {
591 brush_color = wxGREEN_BRUSH;
592 } else {
593 brush_color = wxRED_BRUSH;
594 }
595 }
596
597 wxPoint real_area_pos = {subway_item.x, subway_item.y};
598
599 int real_area_size =
600 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
601
602 if (draw_type == ItemDrawType::kBox) {
603 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
604 gcdc.SetBrush(*brush_color);
605 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
606 } else if (draw_type == ItemDrawType::kOwl) {
607 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale(
608 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR));
609 gcdc.DrawBitmap(owl_bitmap, real_area_pos);
610
611 if (shade_color) {
612 gcdc.SetBrush(wxBrush(*shade_color));
613 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
614 }
615 }
616 }
617}
618
619void SubwayMap::SetUpHelpButton() {
620 help_button_->SetPosition({
621 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
622 15,
623 });
624}
625
626void SubwayMap::EvaluateScroll(wxPoint pos) {
627 int scroll_x;
628 int scroll_y;
629 if (pos.x < GetSize().GetWidth() / 9) {
630 scroll_x = 20;
631 } else if (pos.x < GetSize().GetWidth() / 6) {
632 scroll_x = 5;
633 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
634 scroll_x = -20;
635 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
636 scroll_x = -5;
637 } else {
638 scroll_x = 0;
639 }
640 if (pos.y < GetSize().GetHeight() / 9) {
641 scroll_y = 20;
642 } else if (pos.y < GetSize().GetHeight() / 6) {
643 scroll_y = 5;
644 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
645 scroll_y = -20;
646 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
647 scroll_y = -5;
648 } else {
649 scroll_y = 0;
650 }
651
652 SetScrollSpeed(scroll_x, scroll_y);
653}
654
655wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
656 return {static_cast<int>(pos.x * render_width_ * zoom_ /
657 map_image_.GetSize().GetWidth() +
658 zoom_x_),
659 static_cast<int>(pos.y * render_width_ * zoom_ /
660 map_image_.GetSize().GetWidth() +
661 zoom_y_)};
662}
663
664wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
665 return {static_cast<int>(pos.x * render_width_ * zoom_ /
666 map_image_.GetSize().GetWidth()),
667 static_cast<int>(pos.y * render_width_ * zoom_ /
668 map_image_.GetSize().GetWidth())};
669}
670
671wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
672 return {
673 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
674 render_width_ / zoom_),
675 0, map_image_.GetWidth() - 1),
676 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
677 render_width_ / zoom_),
678 0, map_image_.GetHeight() - 1)};
679}
680
681void SubwayMap::SetZoomPos(wxPoint pos) {
682 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
683 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
684 } else {
685 zoom_x_ = std::clamp(
686 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
687 0);
688 }
689 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
690 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
691 } else {
692 zoom_y_ = std::clamp(
693 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
694 0);
695 }
696}
697
698void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
699 bool should_timer = (scroll_x != 0 || scroll_y != 0);
700 if (should_timer != scroll_timer_->IsRunning()) {
701 if (should_timer) {
702 scroll_timer_->Start(1000 / 60);
703 } else {
704 scroll_timer_->Stop();
705 }
706 }
707
708 scroll_x_ = scroll_x;
709 scroll_y_ = scroll_y;
710}
711
712void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
713 wxPoint map_pos = RenderPosToMapPos(static_point);
714 zoom_ = zoom;
715
716 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
717 SetZoomPos(-(virtual_pos - static_point));
718
719 Refresh();
720
721 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
722}
723
724quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
725 const SubwayItem &subway_item = GD_GetSubwayItem(id);
726 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
727 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
728}
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 1881513..c475fb7 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() != kDOORS_MODE || 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() == kDOORS_MODE && AP_AreDoorsGrouped() &&
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 if (panel_obj.panel_door != -1 && AP_GetDoorShuffleMode() == kPANELS_MODE) { 489 if (panel_obj.panel_door != -1 && AP_GetDoorShuffleMode() == kPANELS_MODE) {
@@ -417,11 +594,20 @@ class StateCalculator {
417 std::set<int> reachable_rooms_; 594 std::set<int> reachable_rooms_;
418 std::map<int, Decision> door_decisions_; 595 std::map<int, Decision> door_decisions_;
419 std::set<int> solveable_panels_; 596 std::set<int> solveable_panels_;
597 std::set<int> reachable_paintings_;
598 std::map<int, std::map<std::string, bool>> door_report_;
420}; 599};
421 600
422} // namespace 601} // namespace
423 602
603void ResetReachabilityRequirements() {
604 std::lock_guard reachability_guard(GetState().reachability_mutex);
605 GetState().requirements.Reset();
606}
607
424void RecalculateReachability() { 608void RecalculateReachability() {
609 std::lock_guard reachability_guard(GetState().reachability_mutex);
610
425 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 611 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
426 state_calculator.Calculate(); 612 state_calculator.Calculate();
427 613
@@ -444,10 +630,21 @@ void RecalculateReachability() {
444 } 630 }
445 } 631 }
446 632
447 { 633 std::set<int> new_reachable_doors;
448 std::lock_guard reachability_guard(GetState().reachability_mutex); 634 for (const auto& [door_id, decision] : state_calculator.GetDoorDecisions()) {
449 std::swap(GetState().reachability, new_reachability); 635 if (decision == kYes) {
636 new_reachable_doors.insert(door_id);
637 }
450 } 638 }
639
640 std::set<int> reachable_paintings = state_calculator.GetReachablePaintings();
641 std::map<int, std::map<std::string, bool>> door_reports =
642 state_calculator.GetDoorReports();
643
644 std::swap(GetState().reachability, new_reachability);
645 std::swap(GetState().reachable_doors, new_reachable_doors);
646 std::swap(GetState().reachable_paintings, reachable_paintings);
647 std::swap(GetState().door_reports, door_reports);
451} 648}
452 649
453bool IsLocationReachable(int location_id) { 650bool IsLocationReachable(int location_id) {
@@ -459,3 +656,21 @@ bool IsLocationReachable(int location_id) {
459 return false; 656 return false;
460 } 657 }
461} 658}
659
660bool IsDoorOpen(int door_id) {
661 std::lock_guard reachability_guard(GetState().reachability_mutex);
662
663 return GetState().reachable_doors.count(door_id);
664}
665
666bool IsPaintingReachable(int painting_id) {
667 std::lock_guard reachability_guard(GetState().reachability_mutex);
668
669 return GetState().reachable_paintings.count(painting_id);
670}
671
672const std::map<std::string, bool>& GetDoorRequirements(int door_id) {
673 std::lock_guard reachability_guard(GetState().reachability_mutex);
674
675 return GetState().door_reports.at(door_id);
676}
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 db75351..7aab91b 100644 --- a/src/version.h +++ b/src/version.h
@@ -1,7 +1,7 @@
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> 4#include <sstream>
5#include <regex> 5#include <regex>
6 6
7struct Version { 7struct Version {
@@ -23,6 +23,12 @@ struct Version {
23 } 23 }
24 } 24 }
25 25
26 std::string ToString() const {
27 std::ostringstream output;
28 output << "v" << major << "." << minor << "." << revision;
29 return output.str();
30 }
31
26 bool operator<(const Version& rhs) const { 32 bool operator<(const Version& rhs) const {
27 return (major < rhs.major) || 33 return (major < rhs.major) ||
28 (major == rhs.major && 34 (major == rhs.major &&
@@ -31,8 +37,6 @@ struct Version {
31 } 37 }
32}; 38};
33 39
34std::ostream& operator<<(std::ostream& out, const Version& ver); 40constexpr const Version kTrackerVersion = Version(0, 10, 2);
35
36constexpr const Version kTrackerVersion = Version(0, 9, 0);
37 41
38#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file 42#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file
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