about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2024-06-06 15:54:41 -0400
committerStar Rauchenberger <fefferburbia@gmail.com>2024-06-06 15:54:41 -0400
commit8ddab49cc13d809ca75dcd7f645661a3d3cb05c4 (patch)
treeba1e5f3237dbb7cdc939c35e193f5e6e46845a77
parentac38dd0a5c394eefc39b7a8cf7b96762f18c8b31 (diff)
parent6f5287b3921c843a6b322ccbdfcbef00a8f16980 (diff)
downloadlingo-ap-tracker-8ddab49cc13d809ca75dcd7f645661a3d3cb05c4.tar.gz
lingo-ap-tracker-8ddab49cc13d809ca75dcd7f645661a3d3cb05c4.tar.bz2
lingo-ap-tracker-8ddab49cc13d809ca75dcd7f645661a3d3cb05c4.zip
Merge branch 'subway'
-rw-r--r--CMakeLists.txt4
-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.yaml1041
-rw-r--r--src/ap_state.cpp120
-rw-r--r--src/ap_state.h11
-rw-r--r--src/area_popup.cpp40
-rw-r--r--src/game_data.cpp221
-rw-r--r--src/game_data.h39
-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.cpp700
-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.cpp400
-rw-r--r--src/tracker_state.h11
-rw-r--r--src/version.h12
-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
27 files changed, 3150 insertions, 241 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index a6f6342..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,10 +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/subway_map.cpp"
47 "src/network_set.cpp"
46 "vendor/whereami/whereami.c" 48 "vendor/whereami/whereami.c"
47) 49)
48set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20) 50set_property(TARGET lingo_ap_tracker PROPERTY CXX_STANDARD 20)
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..8f13f09 --- /dev/null +++ b/assets/subway.yaml
@@ -0,0 +1,1041 @@
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: [1652, 1393]
1020 room: Room Room
1021 door: Excavation
1022- pos: [1592, 1442]
1023 room: Room Room
1024 door: Cellar Exit
1025- pos: [1570, 938]
1026 room: Outside The Wise
1027 door: Wise Entrance
1028- pos: [1653, 935]
1029 paintings:
1030 - clock_painting_3
1031- pos: [369, 605]
1032 room: Outside The Scientific
1033 door: Scientific Entrance
1034- pos: [294, 602]
1035 paintings:
1036 - hi_solved_painting4
1037 tags:
1038 - hi_scientific
1039- pos: [814, 1001]
1040 room: Challenge Room
1041 door: Welcome Door
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index 8feb78b..0ce4582 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
@@ -71,11 +70,12 @@ struct APState {
71 bool sunwarp_shuffle = false; 70 bool sunwarp_shuffle = false;
72 71
73 std::map<std::string, std::string> painting_mapping; 72 std::map<std::string, std::string> painting_mapping;
73 std::set<std::string> painting_codomain;
74 std::map<int, SunwarpMapping> sunwarp_mapping; 74 std::map<int, SunwarpMapping> sunwarp_mapping;
75 75
76 void Connect(std::string server, std::string player, std::string password) { 76 void Connect(std::string server, std::string player, std::string password) {
77 if (!initialized) { 77 if (!initialized) {
78 TrackerLog("Initializing APState..."); 78 wxLogVerbose("Initializing APState...");
79 79
80 std::thread([this]() { 80 std::thread([this]() {
81 for (;;) { 81 for (;;) {
@@ -103,15 +103,16 @@ struct APState {
103 } 103 }
104 104
105 tracked_data_storage_keys.push_back("PlayerPos"); 105 tracked_data_storage_keys.push_back("PlayerPos");
106 tracked_data_storage_keys.push_back("Paintings");
106 107
107 initialized = true; 108 initialized = true;
108 } 109 }
109 110
110 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 111 tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
111 TrackerLog("Connecting to Archipelago server (" + server + ")..."); 112 wxLogStatus("Connecting to Archipelago server (%s)...", server);
112 113
113 { 114 {
114 TrackerLog("Destroying old AP client..."); 115 wxLogVerbose("Destroying old AP client...");
115 116
116 std::lock_guard client_guard(client_mutex); 117 std::lock_guard client_guard(client_mutex);
117 118
@@ -137,6 +138,7 @@ struct APState {
137 color_shuffle = false; 138 color_shuffle = false;
138 painting_shuffle = false; 139 painting_shuffle = false;
139 painting_mapping.clear(); 140 painting_mapping.clear();
141 painting_codomain.clear();
140 mastery_requirement = 21; 142 mastery_requirement = 21;
141 level_2_requirement = 223; 143 level_2_requirement = 223;
142 location_checks = kNORMAL_LOCATIONS; 144 location_checks = kNORMAL_LOCATIONS;
@@ -155,10 +157,10 @@ struct APState {
155 apclient->set_room_info_handler([this, player, password]() { 157 apclient->set_room_info_handler([this, player, password]() {
156 inventory.clear(); 158 inventory.clear();
157 159
158 TrackerLog("Connected to Archipelago server. Authenticating as " + 160 wxLogStatus("Connected to Archipelago server. Authenticating as %s %s",
159 player + 161 player,
160 (password.empty() ? " without password" 162 (password.empty() ? "without password"
161 : " with password " + password)); 163 : "with password " + password));
162 tracker_frame->SetStatusMessage( 164 tracker_frame->SetStatusMessage(
163 "Connected to Archipelago server. Authenticating..."); 165 "Connected to Archipelago server. Authenticating...");
164 166
@@ -170,23 +172,23 @@ struct APState {
170 [this](const std::list<int64_t>& locations) { 172 [this](const std::list<int64_t>& locations) {
171 for (const int64_t location_id : locations) { 173 for (const int64_t location_id : locations) {
172 checked_locations.insert(location_id); 174 checked_locations.insert(location_id);
173 TrackerLog("Location: " + std::to_string(location_id)); 175 wxLogVerbose("Location: %lld", location_id);
174 } 176 }
175 177
176 RefreshTracker(); 178 RefreshTracker(false);
177 }); 179 });
178 180
179 apclient->set_slot_disconnected_handler([this]() { 181 apclient->set_slot_disconnected_handler([this]() {
180 tracker_frame->SetStatusMessage( 182 tracker_frame->SetStatusMessage(
181 "Disconnected from Archipelago. Attempting to reconnect..."); 183 "Disconnected from Archipelago. Attempting to reconnect...");
182 TrackerLog( 184 wxLogStatus(
183 "Slot disconnected from Archipelago. Attempting to reconnect..."); 185 "Slot disconnected from Archipelago. Attempting to reconnect...");
184 }); 186 });
185 187
186 apclient->set_socket_disconnected_handler([this]() { 188 apclient->set_socket_disconnected_handler([this]() {
187 tracker_frame->SetStatusMessage( 189 tracker_frame->SetStatusMessage(
188 "Disconnected from Archipelago. Attempting to reconnect..."); 190 "Disconnected from Archipelago. Attempting to reconnect...");
189 TrackerLog( 191 wxLogStatus(
190 "Socket disconnected from Archipelago. Attempting to reconnect..."); 192 "Socket disconnected from Archipelago. Attempting to reconnect...");
191 }); 193 });
192 194
@@ -194,10 +196,10 @@ struct APState {
194 [this](const std::list<APClient::NetworkItem>& items) { 196 [this](const std::list<APClient::NetworkItem>& items) {
195 for (const APClient::NetworkItem& item : items) { 197 for (const APClient::NetworkItem& item : items) {
196 inventory[item.item]++; 198 inventory[item.item]++;
197 TrackerLog("Item: " + std::to_string(item.item)); 199 wxLogVerbose("Item: %lld", item.item);
198 } 200 }
199 201
200 RefreshTracker(); 202 RefreshTracker(false);
201 }); 203 });
202 204
203 apclient->set_retrieved_handler( 205 apclient->set_retrieved_handler(
@@ -206,20 +208,20 @@ struct APState {
206 HandleDataStorage(key, value); 208 HandleDataStorage(key, value);
207 } 209 }
208 210
209 RefreshTracker(); 211 RefreshTracker(false);
210 }); 212 });
211 213
212 apclient->set_set_reply_handler([this](const std::string& key, 214 apclient->set_set_reply_handler([this](const std::string& key,
213 const nlohmann::json& value, 215 const nlohmann::json& value,
214 const nlohmann::json&) { 216 const nlohmann::json&) {
215 HandleDataStorage(key, value); 217 HandleDataStorage(key, value);
216 RefreshTracker(); 218 RefreshTracker(false);
217 }); 219 });
218 220
219 apclient->set_slot_connected_handler([this]( 221 apclient->set_slot_connected_handler([this](
220 const nlohmann::json& slot_data) { 222 const nlohmann::json& slot_data) {
221 tracker_frame->SetStatusMessage("Connected to Archipelago!"); 223 tracker_frame->SetStatusMessage("Connected to Archipelago!");
222 TrackerLog("Connected to Archipelago!"); 224 wxLogStatus("Connected to Archipelago!");
223 225
224 data_storage_prefix = 226 data_storage_prefix =
225 "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; 227 "Lingo_" + std::to_string(apclient->get_player_number()) + "_";
@@ -253,6 +255,7 @@ struct APState {
253 for (const auto& mapping_it : 255 for (const auto& mapping_it :
254 slot_data["painting_entrance_to_exit"].items()) { 256 slot_data["painting_entrance_to_exit"].items()) {
255 painting_mapping[mapping_it.key()] = mapping_it.value(); 257 painting_mapping[mapping_it.key()] = mapping_it.value();
258 painting_codomain.insert(mapping_it.value());
256 } 259 }
257 } 260 }
258 261
@@ -271,7 +274,8 @@ struct APState {
271 connected = true; 274 connected = true;
272 has_connection_result = true; 275 has_connection_result = true;
273 276
274 RefreshTracker(); 277 ResetReachabilityRequirements();
278 RefreshTracker(true);
275 279
276 std::list<std::string> corrected_keys; 280 std::list<std::string> corrected_keys;
277 for (const std::string& key : tracked_data_storage_keys) { 281 for (const std::string& key : tracked_data_storage_keys) {
@@ -323,7 +327,7 @@ struct APState {
323 } 327 }
324 328
325 std::string full_message = hatkirby::implode(error_messages, " "); 329 std::string full_message = hatkirby::implode(error_messages, " ");
326 TrackerLog(full_message); 330 wxLogError(wxString(full_message));
327 331
328 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); 332 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
329 }); 333 });
@@ -341,8 +345,7 @@ struct APState {
341 DestroyClient(); 345 DestroyClient();
342 346
343 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 347 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
344 348 wxLogStatus("Timeout while connecting to Archipelago server.");
345 TrackerLog("Timeout while connecting to Archipelago server.");
346 wxMessageBox("Timeout while connecting to Archipelago server.", 349 wxMessageBox("Timeout while connecting to Archipelago server.",
347 "Connection failed", wxOK | wxICON_ERROR); 350 "Connection failed", wxOK | wxICON_ERROR);
348 } 351 }
@@ -353,7 +356,7 @@ struct APState {
353 } 356 }
354 357
355 if (connected) { 358 if (connected) {
356 RefreshTracker(); 359 RefreshTracker(false);
357 } else { 360 } else {
358 client_active = false; 361 client_active = false;
359 } 362 }
@@ -362,12 +365,11 @@ struct APState {
362 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 365 void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
363 if (value.is_boolean()) { 366 if (value.is_boolean()) {
364 data_storage[key] = value.get<bool>(); 367 data_storage[key] = value.get<bool>();
365 TrackerLog("Data storage " + key + " retrieved as " + 368 wxLogVerbose("Data storage %s retrieved as %s", key,
366 (value.get<bool>() ? "true" : "false")); 369 (value.get<bool>() ? "true" : "false"));
367 } else if (value.is_number()) { 370 } else if (value.is_number()) {
368 data_storage[key] = value.get<int>(); 371 data_storage[key] = value.get<int>();
369 TrackerLog("Data storage " + key + " retrieved as " + 372 wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>());
370 std::to_string(value.get<int>()));
371 } else if (value.is_object()) { 373 } else if (value.is_object()) {
372 if (key.ends_with("PlayerPos")) { 374 if (key.ends_with("PlayerPos")) {
373 auto map_value = value.get<std::map<std::string, int>>(); 375 auto map_value = value.get<std::map<std::string, int>>();
@@ -376,7 +378,7 @@ struct APState {
376 data_storage[key] = value.get<std::map<std::string, int>>(); 378 data_storage[key] = value.get<std::map<std::string, int>>();
377 } 379 }
378 380
379 TrackerLog("Data storage " + key + " retrieved as dictionary"); 381 wxLogVerbose("Data storage %s retrieved as dictionary", key);
380 } else if (value.is_null()) { 382 } else if (value.is_null()) {
381 if (key.ends_with("PlayerPos")) { 383 if (key.ends_with("PlayerPos")) {
382 player_pos = std::nullopt; 384 player_pos = std::nullopt;
@@ -384,7 +386,19 @@ struct APState {
384 data_storage.erase(key); 386 data_storage.erase(key);
385 } 387 }
386 388
387 TrackerLog("Data storage " + key + " retrieved as null"); 389 wxLogVerbose("Data storage %s retrieved as null", key);
390 } else if (value.is_array()) {
391 auto list_value = value.get<std::vector<std::string>>();
392
393 if (key.ends_with("Paintings")) {
394 data_storage[key] =
395 std::set<std::string>(list_value.begin(), list_value.end());
396 } else {
397 data_storage[key] = list_value;
398 }
399
400 wxLogVerbose("Data storage %s retrieved as list: [%s]", key,
401 hatkirby::implode(list_value, ", "));
388 } 402 }
389 } 403 }
390 404
@@ -407,22 +421,46 @@ struct APState {
407 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 421 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
408 } 422 }
409 423
410 void RefreshTracker() { 424 const std::set<std::string>& GetCheckedPaintings() {
411 TrackerLog("Refreshing display..."); 425 std::string key = data_storage_prefix + "Paintings";
426 if (!data_storage.count(key)) {
427 data_storage[key] = std::set<std::string>();
428 }
429
430 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
431 }
432
433 bool IsPaintingChecked(const std::string& painting_id) {
434 const auto& checked_paintings = GetCheckedPaintings();
435
436 return checked_paintings.count(painting_id) ||
437 (painting_mapping.count(painting_id) &&
438 checked_paintings.count(painting_mapping.at(painting_id)));
439 }
440
441 void RefreshTracker(bool reset) {
442 wxLogVerbose("Refreshing display...");
412 443
413 RecalculateReachability(); 444 RecalculateReachability();
414 tracker_frame->UpdateIndicators(); 445
446 if (reset) {
447 tracker_frame->ResetIndicators();
448 } else {
449 tracker_frame->UpdateIndicators();
450 }
415 } 451 }
416 452
417 int64_t GetItemId(const std::string& item_name) { 453 int64_t GetItemId(const std::string& item_name) {
418 int64_t ap_id = apclient->get_item_id(item_name); 454 int64_t ap_id = apclient->get_item_id(item_name);
419 if (ap_id == APClient::INVALID_NAME_ID) { 455 if (ap_id == APClient::INVALID_NAME_ID) {
420 TrackerLog("Could not find AP item ID for " + item_name); 456 wxLogError("Could not find AP item ID for %s", item_name);
421 } 457 }
422 458
423 return ap_id; 459 return ap_id;
424 } 460 }
425 461
462 std::string GetItemName(int id) { return apclient->get_item_name(id); }
463
426 bool HasReachedGoal() { 464 bool HasReachedGoal() {
427 return data_storage.count(victory_data_storage_key) && 465 return data_storage.count(victory_data_storage_key) &&
428 std::any_cast<int>(data_storage.at(victory_data_storage_key)) == 466 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
@@ -461,16 +499,32 @@ bool AP_HasItem(int item_id, int quantity) {
461 return GetState().HasItem(item_id, quantity); 499 return GetState().HasItem(item_id, quantity);
462} 500}
463 501
502std::string AP_GetItemName(int item_id) {
503 return GetState().GetItemName(item_id);
504}
505
464DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 506DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; }
465 507
466bool AP_IsColorShuffle() { return GetState().color_shuffle; } 508bool AP_IsColorShuffle() { return GetState().color_shuffle; }
467 509
468bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 510bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; }
469 511
470const std::map<std::string, std::string> AP_GetPaintingMapping() { 512const std::map<std::string, std::string>& AP_GetPaintingMapping() {
471 return GetState().painting_mapping; 513 return GetState().painting_mapping;
472} 514}
473 515
516bool AP_IsPaintingMappedTo(const std::string& painting_id) {
517 return GetState().painting_codomain.count(painting_id);
518}
519
520const std::set<std::string>& AP_GetCheckedPaintings() {
521 return GetState().GetCheckedPaintings();
522}
523
524bool AP_IsPaintingChecked(const std::string& painting_id) {
525 return GetState().IsPaintingChecked(painting_id);
526}
527
474int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 528int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }
475 529
476int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 530int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }
diff --git a/src/ap_state.h b/src/ap_state.h index 6667e0d..7af7395 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -3,6 +3,7 @@
3 3
4#include <map> 4#include <map>
5#include <optional> 5#include <optional>
6#include <set>
6#include <string> 7#include <string>
7#include <tuple> 8#include <tuple>
8 9
@@ -48,13 +49,21 @@ bool AP_HasCheckedHuntPanel(int location_id);
48 49
49bool AP_HasItem(int item_id, int quantity = 1); 50bool AP_HasItem(int item_id, int quantity = 1);
50 51
52std::string AP_GetItemName(int item_id);
53
51DoorShuffleMode AP_GetDoorShuffleMode(); 54DoorShuffleMode AP_GetDoorShuffleMode();
52 55
53bool AP_IsColorShuffle(); 56bool AP_IsColorShuffle();
54 57
55bool AP_IsPaintingShuffle(); 58bool AP_IsPaintingShuffle();
56 59
57const std::map<std::string, std::string> AP_GetPaintingMapping(); 60const std::map<std::string, std::string>& AP_GetPaintingMapping();
61
62bool AP_IsPaintingMappedTo(const std::string& painting_id);
63
64const std::set<std::string>& AP_GetCheckedPaintings();
65
66bool AP_IsPaintingChecked(const std::string& painting_id);
58 67
59int AP_GetMasteryRequirement(); 68int AP_GetMasteryRequirement();
60 69
diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 3b5d8d4..58d8897 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -1,5 +1,7 @@
1#include "area_popup.h" 1#include "area_popup.h"
2 2
3#include <wx/dcbuffer.h>
4
3#include "ap_state.h" 5#include "ap_state.h"
4#include "game_data.h" 6#include "game_data.h"
5#include "global.h" 7#include "global.h"
@@ -8,6 +10,8 @@
8 10
9AreaPopup::AreaPopup(wxWindow* parent, int area_id) 11AreaPopup::AreaPopup(wxWindow* parent, int area_id)
10 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 12 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
13 SetBackgroundStyle(wxBG_STYLE_PAINT);
14
11 unchecked_eye_ = 15 unchecked_eye_ =
12 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), 16 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
13 wxBITMAP_TYPE_PNG) 17 wxBITMAP_TYPE_PNG)
@@ -61,6 +65,19 @@ void AreaPopup::UpdateIndicators() {
61 } 65 }
62 } 66 }
63 67
68 if (AP_IsPaintingShuffle()) {
69 for (int painting_id : map_area.paintings) {
70 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
71 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name.
72 int item_height = std::max(32, item_extent.GetHeight()) + 10;
73 acc_height += item_height;
74
75 if (item_extent.GetWidth() > col_width) {
76 col_width = item_extent.GetWidth();
77 }
78 }
79 }
80
64 int item_width = col_width + 10 + 32; 81 int item_width = col_width + 10 + 32;
65 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 82 int full_width = std::max(header_extent.GetWidth(), item_width) + 20;
66 83
@@ -105,10 +122,31 @@ void AreaPopup::UpdateIndicators() {
105 122
106 cur_height += 10 + 32; 123 cur_height += 10 + 32;
107 } 124 }
125
126 if (AP_IsPaintingShuffle()) {
127 for (int painting_id : map_area.paintings) {
128 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
129 bool checked = AP_IsPaintingChecked(painting.internal_id);
130 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
131
132 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height});
133
134 bool reachable = IsPaintingReachable(painting_id);
135 const wxColour* text_color = reachable ? wxWHITE : wxRED;
136 mem_dc.SetTextForeground(*text_color);
137
138 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name.
139 mem_dc.DrawText(painting.internal_id,
140 {10 + 32 + 10,
141 cur_height + (32 - mem_dc.GetFontMetrics().height) / 2});
142
143 cur_height += 10 + 32;
144 }
145 }
108} 146}
109 147
110void AreaPopup::OnPaint(wxPaintEvent& event) { 148void AreaPopup::OnPaint(wxPaintEvent& event) {
111 wxPaintDC dc(this); 149 wxBufferedPaintDC dc(this);
112 PrepareDC(dc); 150 PrepareDC(dc);
113 dc.DrawBitmap(rendered_, 0, 0); 151 dc.DrawBitmap(rendered_, 0, 0);
114 152
diff --git a/src/game_data.cpp b/src/game_data.cpp index be31b8f..5776c6c 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 }
@@ -44,11 +47,14 @@ struct GameData {
44 std::vector<Door> doors_; 47 std::vector<Door> doors_;
45 std::vector<Panel> panels_; 48 std::vector<Panel> panels_;
46 std::vector<MapArea> map_areas_; 49 std::vector<MapArea> map_areas_;
50 std::vector<SubwayItem> subway_items_;
51 std::vector<PaintingExit> paintings_;
47 52
48 std::map<std::string, int> room_by_id_; 53 std::map<std::string, int> room_by_id_;
49 std::map<std::string, int> door_by_id_; 54 std::map<std::string, int> door_by_id_;
50 std::map<std::string, int> panel_by_id_; 55 std::map<std::string, int> panel_by_id_;
51 std::map<std::string, int> area_by_id_; 56 std::map<std::string, int> area_by_id_;
57 std::map<std::string, int> painting_by_id_;
52 58
53 std::vector<int> door_definition_order_; 59 std::vector<int> door_definition_order_;
54 60
@@ -61,6 +67,9 @@ struct GameData {
61 67
62 std::vector<int> sunwarp_doors_; 68 std::vector<int> sunwarp_doors_;
63 69
70 std::map<std::string, int> subway_item_by_painting_;
71 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
72
64 bool loaded_area_data_ = false; 73 bool loaded_area_data_ = false;
65 std::set<std::string> malconfigured_areas_; 74 std::set<std::string> malconfigured_areas_;
66 75
@@ -79,9 +88,7 @@ struct GameData {
79 ap_id_by_color_[GetColorForString(input_name)] = 88 ap_id_by_color_[GetColorForString(input_name)] =
80 ids_config["special_items"][color_name].as<int>(); 89 ids_config["special_items"][color_name].as<int>();
81 } else { 90 } else {
82 std::ostringstream errmsg; 91 wxLogError("Missing AP item ID for color %s", color_name);
83 errmsg << "Missing AP item ID for color " << color_name;
84 TrackerLog(errmsg.str());
85 } 92 }
86 }; 93 };
87 94
@@ -156,8 +163,9 @@ struct GameData {
156 } 163 }
157 default: { 164 default: {
158 // This shouldn't happen. 165 // This shouldn't happen.
159 std::cout << "Error reading game data: " << entrance_it 166 std::ostringstream formatted;
160 << std::endl; 167 formatted << entrance_it;
168 wxLogError("Error reading game data: %s", formatted.str());
161 break; 169 break;
162 } 170 }
163 } 171 }
@@ -282,10 +290,8 @@ struct GameData {
282 [panels_[panel_id].name] 290 [panels_[panel_id].name]
283 .as<int>(); 291 .as<int>();
284 } else { 292 } else {
285 std::ostringstream errmsg; 293 wxLogError("Missing AP location ID for panel %s - %s",
286 errmsg << "Missing AP location ID for panel " 294 rooms_[room_id].name, panels_[panel_id].name);
287 << rooms_[room_id].name << " - " << panels_[panel_id].name;
288 TrackerLog(errmsg.str());
289 } 295 }
290 } 296 }
291 } 297 }
@@ -348,10 +354,8 @@ struct GameData {
348 [doors_[door_id].name]["item"] 354 [doors_[door_id].name]["item"]
349 .as<int>(); 355 .as<int>();
350 } else { 356 } else {
351 std::ostringstream errmsg; 357 wxLogError("Missing AP item ID for door %s - %s",
352 errmsg << "Missing AP item ID for door " << rooms_[room_id].name 358 rooms_[room_id].name, doors_[door_id].name);
353 << " - " << doors_[door_id].name;
354 TrackerLog(errmsg.str());
355 } 359 }
356 } 360 }
357 361
@@ -365,10 +369,8 @@ struct GameData {
365 ids_config["door_groups"][doors_[door_id].group_name] 369 ids_config["door_groups"][doors_[door_id].group_name]
366 .as<int>(); 370 .as<int>();
367 } else { 371 } else {
368 std::ostringstream errmsg; 372 wxLogError("Missing AP item ID for door group %s",
369 errmsg << "Missing AP item ID for door group " 373 doors_[door_id].group_name);
370 << doors_[door_id].group_name;
371 TrackerLog(errmsg.str());
372 } 374 }
373 } 375 }
374 376
@@ -378,13 +380,11 @@ struct GameData {
378 } else if (!door_it.second["skip_location"] && 380 } else if (!door_it.second["skip_location"] &&
379 !door_it.second["event"]) { 381 !door_it.second["event"]) {
380 if (has_external_panels) { 382 if (has_external_panels) {
381 std::ostringstream errmsg; 383 wxLogError(
382 errmsg 384 "%s - %s has panels from other rooms but does not have an "
383 << rooms_[room_id].name << " - " << doors_[door_id].name 385 "explicit location name and is not marked skip_location or "
384 << " has panels from other rooms but does not have an " 386 "event",
385 "explicit " 387 rooms_[room_id].name, doors_[door_id].name);
386 "location name and is not marked skip_location or event";
387 TrackerLog(errmsg.str());
388 } 388 }
389 389
390 doors_[door_id].location_name = 390 doors_[door_id].location_name =
@@ -404,10 +404,8 @@ struct GameData {
404 [doors_[door_id].name]["location"] 404 [doors_[door_id].name]["location"]
405 .as<int>(); 405 .as<int>();
406 } else { 406 } else {
407 std::ostringstream errmsg; 407 wxLogError("Missing AP location ID for door %s - %s",
408 errmsg << "Missing AP location ID for door " 408 rooms_[room_id].name, doors_[door_id].name);
409 << rooms_[room_id].name << " - " << doors_[door_id].name;
410 TrackerLog(errmsg.str());
411 } 409 }
412 } 410 }
413 411
@@ -428,12 +426,13 @@ struct GameData {
428 426
429 if (room_it.second["paintings"]) { 427 if (room_it.second["paintings"]) {
430 for (const auto &painting : room_it.second["paintings"]) { 428 for (const auto &painting : room_it.second["paintings"]) {
431 std::string painting_id = painting["id"].as<std::string>(); 429 std::string internal_id = painting["id"].as<std::string>();
432 room_by_painting_[painting_id] = room_id;
433 430
434 if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { 431 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
435 PaintingExit painting_exit; 432 (!painting["disable"] || !painting["disable"].as<bool>())) {
436 painting_exit.id = painting_id; 433 int painting_id = AddOrGetPainting(internal_id);
434 PaintingExit &painting_exit = paintings_[painting_id];
435 painting_exit.room = room_id;
437 436
438 if (painting["required_door"]) { 437 if (painting["required_door"]) {
439 std::string rd_room = rooms_[room_id].name; 438 std::string rd_room = rooms_[room_id].name;
@@ -445,7 +444,7 @@ struct GameData {
445 rd_room, painting["required_door"]["door"].as<std::string>()); 444 rd_room, painting["required_door"]["door"].as<std::string>());
446 } 445 }
447 446
448 rooms_[room_id].paintings.push_back(painting_exit); 447 rooms_[room_id].paintings.push_back(painting_exit.id);
449 } 448 }
450 } 449 }
451 } 450 }
@@ -473,10 +472,8 @@ struct GameData {
473 progressive_item_id = 472 progressive_item_id =
474 ids_config["progression"][progressive_item_name].as<int>(); 473 ids_config["progression"][progressive_item_name].as<int>();
475 } else { 474 } else {
476 std::ostringstream errmsg; 475 wxLogError("Missing AP item ID for progressive item %s",
477 errmsg << "Missing AP item ID for progressive item " 476 progressive_item_name);
478 << progressive_item_name;
479 TrackerLog(errmsg.str());
480 } 477 }
481 478
482 int index = 1; 479 int index = 1;
@@ -618,11 +615,98 @@ struct GameData {
618 } 615 }
619 } 616 }
620 617
618 for (const Room &room : rooms_) {
619 std::string area_name = room.name;
620 if (fold_areas.count(room.name)) {
621 int fold_area_id = fold_areas[room.name];
622 area_name = map_areas_[fold_area_id].name;
623 }
624
625 if (!room.paintings.empty()) {
626 int area_id = AddOrGetArea(area_name);
627 MapArea &map_area = map_areas_[area_id];
628
629 for (int painting_id : room.paintings) {
630 map_area.paintings.push_back(painting_id);
631 }
632 }
633 }
634
621 // Report errors. 635 // Report errors.
622 for (const std::string &area : malconfigured_areas_) { 636 for (const std::string &area : malconfigured_areas_) {
623 std::ostringstream errstr; 637 wxLogError("Area data not found for: %s", area);
624 errstr << "Area data not found for: " << area; 638 }
625 TrackerLog(errstr.str()); 639
640 // Read in subway items.
641 YAML::Node subway_config =
642 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml"));
643 for (const auto &subway_it : subway_config) {
644 SubwayItem subway_item;
645 subway_item.id = subway_items_.size();
646 subway_item.x = subway_it["pos"][0].as<int>();
647 subway_item.y = subway_it["pos"][1].as<int>();
648
649 if (subway_it["door"]) {
650 subway_item.door = AddOrGetDoor(subway_it["room"].as<std::string>(),
651 subway_it["door"].as<std::string>());
652 }
653
654 if (subway_it["paintings"]) {
655 for (const auto &painting_it : subway_it["paintings"]) {
656 std::string painting_id = painting_it.as<std::string>();
657
658 subway_item.paintings.push_back(painting_id);
659 subway_item_by_painting_[painting_id] = subway_item.id;
660 }
661 }
662
663 if (subway_it["tags"]) {
664 for (const auto &tag_it : subway_it["tags"]) {
665 subway_item.tags.push_back(tag_it.as<std::string>());
666 }
667 }
668
669 if (subway_it["sunwarp"]) {
670 SubwaySunwarp sunwarp;
671 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
672
673 std::string sunwarp_type =
674 subway_it["sunwarp"]["type"].as<std::string>();
675 if (sunwarp_type == "final") {
676 sunwarp.type = SubwaySunwarpType::kFinal;
677 } else if (sunwarp_type == "exit") {
678 sunwarp.type = SubwaySunwarpType::kExit;
679 } else {
680 sunwarp.type = SubwaySunwarpType::kEnter;
681 }
682
683 subway_item.sunwarp = sunwarp;
684
685 subway_item_by_sunwarp_[sunwarp] = subway_item.id;
686
687 subway_item.door =
688 AddOrGetDoor("Sunwarps", std::to_string(sunwarp.dots) + " Sunwarp");
689 }
690
691 if (subway_it["special"]) {
692 subway_item.special = subway_it["special"].as<std::string>();
693 }
694
695 subway_items_.push_back(subway_item);
696 }
697
698 // Find singleton subway tags.
699 std::map<std::string, std::set<int>> subway_tags;
700 for (const SubwayItem &subway_item : subway_items_) {
701 for (const std::string &tag : subway_item.tags) {
702 subway_tags[tag].insert(subway_item.id);
703 }
704 }
705
706 for (const auto &[tag, items] : subway_tags) {
707 if (items.size() == 1) {
708 wxLogWarning("Singleton subway item tag: %s", tag);
709 }
626 } 710 }
627 } 711 }
628 712
@@ -639,8 +723,10 @@ struct GameData {
639 std::string full_name = room + " - " + door; 723 std::string full_name = room + " - " + door;
640 724
641 if (!door_by_id_.count(full_name)) { 725 if (!door_by_id_.count(full_name)) {
726 int door_id = doors_.size();
642 door_by_id_[full_name] = doors_.size(); 727 door_by_id_[full_name] = doors_.size();
643 doors_.push_back({.room = AddOrGetRoom(room), .name = door}); 728 doors_.push_back(
729 {.id = door_id, .room = AddOrGetRoom(room), .name = door});
644 } 730 }
645 731
646 return door_by_id_[full_name]; 732 return door_by_id_[full_name];
@@ -672,6 +758,16 @@ struct GameData {
672 758
673 return area_by_id_[area]; 759 return area_by_id_[area];
674 } 760 }
761
762 int AddOrGetPainting(std::string internal_id) {
763 if (!painting_by_id_.count(internal_id)) {
764 int painting_id = paintings_.size();
765 painting_by_id_[internal_id] = painting_id;
766 paintings_.push_back({.id = painting_id, .internal_id = internal_id});
767 }
768
769 return painting_by_id_[internal_id];
770 }
675}; 771};
676 772
677GameData &GetState() { 773GameData &GetState() {
@@ -681,6 +777,10 @@ GameData &GetState() {
681 777
682} // namespace 778} // namespace
683 779
780bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
781 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
782}
783
684const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; } 784const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; }
685 785
686const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); } 786const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); }
@@ -703,8 +803,12 @@ const Panel &GD_GetPanel(int panel_id) {
703 return GetState().panels_.at(panel_id); 803 return GetState().panels_.at(panel_id);
704} 804}
705 805
706int GD_GetRoomForPainting(const std::string &painting_id) { 806const PaintingExit &GD_GetPaintingExit(int painting_id) {
707 return GetState().room_by_painting_.at(painting_id); 807 return GetState().paintings_.at(painting_id);
808}
809
810int GD_GetPaintingByName(const std::string &name) {
811 return GetState().painting_by_id_.at(name);
708} 812}
709 813
710const std::vector<int> &GD_GetAchievementPanels() { 814const std::vector<int> &GD_GetAchievementPanels() {
@@ -722,3 +826,24 @@ const std::vector<int> &GD_GetSunwarpDoors() {
722int GD_GetRoomForSunwarp(int index) { 826int GD_GetRoomForSunwarp(int index) {
723 return GetState().room_by_sunwarp_.at(index); 827 return GetState().room_by_sunwarp_.at(index);
724} 828}
829
830const std::vector<SubwayItem> &GD_GetSubwayItems() {
831 return GetState().subway_items_;
832}
833
834const SubwayItem &GD_GetSubwayItem(int id) {
835 return GetState().subway_items_.at(id);
836}
837
838int GD_GetSubwayItemForPainting(const std::string &painting_id) {
839#ifndef NDEBUG
840 if (!GetState().subway_item_by_painting_.count(painting_id)) {
841 wxLogError("No subway item for painting %s", painting_id);
842 }
843#endif
844 return GetState().subway_item_by_painting_.at(painting_id);
845}
846
847int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
848 return GetState().subway_item_by_sunwarp_.at(sunwarp);
849}
diff --git a/src/game_data.h b/src/game_data.h index f3edaa2..a5d5699 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -63,6 +63,7 @@ struct ProgressiveRequirement {
63}; 63};
64 64
65struct Door { 65struct Door {
66 int id;
66 int room; 67 int room;
67 std::string name; 68 std::string name;
68 std::string location_name; 69 std::string location_name;
@@ -87,14 +88,16 @@ struct Exit {
87}; 88};
88 89
89struct PaintingExit { 90struct PaintingExit {
90 std::string id; 91 int id;
92 int room;
93 std::string internal_id;
91 std::optional<int> door; 94 std::optional<int> door;
92}; 95};
93 96
94struct Room { 97struct Room {
95 std::string name; 98 std::string name;
96 std::vector<Exit> exits; 99 std::vector<Exit> exits;
97 std::vector<PaintingExit> paintings; 100 std::vector<int> paintings;
98 std::vector<int> sunwarps; 101 std::vector<int> sunwarps;
99 std::vector<int> panels; 102 std::vector<int> panels;
100}; 103};
@@ -113,12 +116,37 @@ struct MapArea {
113 int id; 116 int id;
114 std::string name; 117 std::string name;
115 std::vector<Location> locations; 118 std::vector<Location> locations;
119 std::vector<int> paintings;
116 int map_x; 120 int map_x;
117 int map_y; 121 int map_y;
118 int classification = 0; 122 int classification = 0;
119 bool hunt = false; 123 bool hunt = false;
120}; 124};
121 125
126enum class SubwaySunwarpType {
127 kEnter,
128 kExit,
129 kFinal
130};
131
132struct SubwaySunwarp {
133 int dots;
134 SubwaySunwarpType type;
135
136 bool operator<(const SubwaySunwarp& rhs) const;
137};
138
139struct SubwayItem {
140 int id;
141 int x;
142 int y;
143 std::optional<int> door;
144 std::vector<std::string> paintings;
145 std::vector<std::string> tags;
146 std::optional<SubwaySunwarp> sunwarp;
147 std::optional<std::string> special;
148};
149
122const std::vector<MapArea>& GD_GetMapAreas(); 150const std::vector<MapArea>& GD_GetMapAreas();
123const MapArea& GD_GetMapArea(int id); 151const MapArea& GD_GetMapArea(int id);
124int GD_GetRoomByName(const std::string& name); 152int GD_GetRoomByName(const std::string& name);
@@ -127,10 +155,15 @@ const std::vector<Door>& GD_GetDoors();
127const Door& GD_GetDoor(int door_id); 155const Door& GD_GetDoor(int door_id);
128int GD_GetDoorByName(const std::string& name); 156int GD_GetDoorByName(const std::string& name);
129const Panel& GD_GetPanel(int panel_id); 157const Panel& GD_GetPanel(int panel_id);
130int GD_GetRoomForPainting(const std::string& painting_id); 158const PaintingExit& GD_GetPaintingExit(int painting_id);
159int GD_GetPaintingByName(const std::string& name);
131const std::vector<int>& GD_GetAchievementPanels(); 160const std::vector<int>& GD_GetAchievementPanels();
132int GD_GetItemIdForColor(LingoColor color); 161int GD_GetItemIdForColor(LingoColor color);
133const std::vector<int>& GD_GetSunwarpDoors(); 162const std::vector<int>& GD_GetSunwarpDoors();
134int GD_GetRoomForSunwarp(int index); 163int GD_GetRoomForSunwarp(int index);
164const std::vector<SubwayItem>& GD_GetSubwayItems();
165const SubwayItem& GD_GetSubwayItem(int id);
166int GD_GetSubwayItemForPainting(const std::string& painting_id);
167int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
135 168
136#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 169#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..abe6626 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
21#ifndef NDEBUG
22 wxLog::SetVerbose(true);
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..8364714 --- /dev/null +++ b/src/subway_map.cpp
@@ -0,0 +1,700 @@
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 for (const auto &[tag, items] : tagged) {
102 // Pairwise connect all items with the same tag.
103 for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end();
104 tag_it1++) {
105 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
106 tag_it2++) {
107 networks_.AddLink(*tag_it1, *tag_it2);
108 }
109 }
110 }
111
112 checked_paintings_.clear();
113}
114
115void SubwayMap::UpdateIndicators() {
116 if (AP_IsPaintingShuffle()) {
117 for (const std::string &painting_id : AP_GetCheckedPaintings()) {
118 if (!checked_paintings_.count(painting_id)) {
119 checked_paintings_.insert(painting_id);
120
121 if (AP_GetPaintingMapping().count(painting_id)) {
122 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id),
123 GD_GetSubwayItemForPainting(
124 AP_GetPaintingMapping().at(painting_id)));
125 }
126 }
127 }
128 }
129
130 Redraw();
131}
132
133void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
134 SubwaySunwarp to_sunwarp) {
135 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
136 GD_GetSubwayItemForSunwarp(to_sunwarp));
137}
138
139void SubwayMap::Zoom(bool in) {
140 wxPoint focus_point;
141
142 if (mouse_position_) {
143 focus_point = *mouse_position_;
144 } else {
145 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
146 }
147
148 if (in) {
149 if (zoom_ < 3.0) {
150 SetZoom(zoom_ + 0.25, focus_point);
151 }
152 } else {
153 if (zoom_ > 1.0) {
154 SetZoom(zoom_ - 0.25, focus_point);
155 }
156 }
157}
158
159void SubwayMap::OnPaint(wxPaintEvent &event) {
160 if (GetSize() != rendered_.GetSize()) {
161 wxSize panel_size = GetSize();
162 wxSize image_size = map_image_.GetSize();
163
164 render_x_ = 0;
165 render_y_ = 0;
166 render_width_ = panel_size.GetWidth();
167 render_height_ = panel_size.GetHeight();
168
169 if (image_size.GetWidth() * panel_size.GetHeight() >
170 panel_size.GetWidth() * image_size.GetHeight()) {
171 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
172 image_size.GetWidth();
173 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
174 } else {
175 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
176 image_size.GetHeight();
177 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
178 }
179
180 SetZoomPos({zoom_x_, zoom_y_});
181
182 SetUpHelpButton();
183 }
184
185 wxBufferedPaintDC dc(this);
186 dc.SetBackground(*wxWHITE_BRUSH);
187 dc.Clear();
188
189 {
190 wxMemoryDC rendered_dc;
191 rendered_dc.SelectObject(rendered_);
192
193 int dst_x;
194 int dst_y;
195 int dst_w;
196 int dst_h;
197 int src_x;
198 int src_y;
199 int src_w;
200 int src_h;
201
202 int zoomed_width = render_width_ * zoom_;
203 int zoomed_height = render_height_ * zoom_;
204
205 if (zoomed_width <= GetSize().GetWidth()) {
206 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
207 dst_w = zoomed_width;
208 src_x = 0;
209 src_w = map_image_.GetWidth();
210 } else {
211 dst_x = 0;
212 dst_w = GetSize().GetWidth();
213 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
214 src_w =
215 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
216 }
217
218 if (zoomed_height <= GetSize().GetHeight()) {
219 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
220 dst_h = zoomed_height;
221 src_y = 0;
222 src_h = map_image_.GetHeight();
223 } else {
224 dst_y = 0;
225 dst_h = GetSize().GetHeight();
226 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
227 src_h =
228 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
229 }
230
231 wxGCDC gcdc(dc);
232 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
233 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
234 src_w, src_h);
235 }
236
237 if (hovered_item_) {
238 // Note that these requirements are duplicated on OnMouseClick so that it
239 // knows when an item has a hover effect.
240 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
241 if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) {
242 const std::map<std::string, bool> &report =
243 GetDoorRequirements(*subway_item.door);
244
245 int acc_height = 10;
246 int col_width = 0;
247
248 for (const auto &[text, obtained] : report) {
249 wxSize item_extent = dc.GetTextExtent(text);
250 int item_height = std::max(32, item_extent.GetHeight()) + 10;
251 acc_height += item_height;
252
253 if (item_extent.GetWidth() > col_width) {
254 col_width = item_extent.GetWidth();
255 }
256 }
257
258 int item_width = col_width + 10 + 32;
259 int full_width = item_width + 20;
260
261 wxPoint popup_pos =
262 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
263 subway_item.y + AREA_ACTUAL_SIZE / 2});
264
265 if (popup_pos.x + full_width > GetSize().GetWidth()) {
266 popup_pos.x = GetSize().GetWidth() - full_width;
267 }
268 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
269 popup_pos.y = GetSize().GetHeight() - acc_height;
270 }
271
272 dc.SetPen(*wxTRANSPARENT_PEN);
273 dc.SetBrush(*wxBLACK_BRUSH);
274 dc.DrawRectangle(popup_pos, {full_width, acc_height});
275
276 dc.SetFont(GetFont());
277
278 int cur_height = 10;
279
280 for (const auto &[text, obtained] : report) {
281 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
282
283 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
284
285 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
286 wxSize item_extent = dc.GetTextExtent(text);
287 dc.DrawText(
288 text,
289 popup_pos +
290 wxPoint{10 + 32 + 10,
291 cur_height + (32 - dc.GetFontMetrics().height) / 2});
292
293 cur_height += 10 + 32;
294 }
295 }
296
297 if (networks_.IsItemInNetwork(*hovered_item_)) {
298 dc.SetBrush(*wxTRANSPARENT_BRUSH);
299
300 for (const auto &[item_id1, item_id2] :
301 networks_.GetNetworkGraph(*hovered_item_)) {
302 const SubwayItem &item1 = GD_GetSubwayItem(item_id1);
303 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
304
305 wxPoint item1_pos = MapPosToRenderPos(
306 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
307 wxPoint item2_pos = MapPosToRenderPos(
308 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
309
310 int left = std::min(item1_pos.x, item2_pos.x);
311 int top = std::min(item1_pos.y, item2_pos.y);
312 int right = std::max(item1_pos.x, item2_pos.x);
313 int bottom = std::max(item1_pos.y, item2_pos.y);
314
315 int halfwidth = right - left;
316 int halfheight = bottom - top;
317
318 if (halfwidth < 4 || halfheight < 4) {
319 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
320 dc.DrawLine(item1_pos, item2_pos);
321 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
322 dc.DrawLine(item1_pos, item2_pos);
323 } else {
324 int ellipse_x;
325 int ellipse_y;
326 double start;
327 double end;
328
329 if (item1_pos.x > item2_pos.x) {
330 ellipse_y = top;
331
332 if (item1_pos.y > item2_pos.y) {
333 ellipse_x = left - halfwidth;
334
335 start = 0;
336 end = 90;
337 } else {
338 ellipse_x = left;
339
340 start = 90;
341 end = 180;
342 }
343 } else {
344 ellipse_y = top - halfheight;
345
346 if (item1_pos.y > item2_pos.y) {
347 ellipse_x = left - halfwidth;
348
349 start = 270;
350 end = 360;
351 } else {
352 ellipse_x = left;
353
354 start = 180;
355 end = 270;
356 }
357 }
358
359 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
360 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
361 halfheight * 2, start, end);
362 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
363 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
364 halfheight * 2, start, end);
365 }
366 }
367 }
368 }
369
370 event.Skip();
371}
372
373void SubwayMap::OnMouseMove(wxMouseEvent &event) {
374 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
375
376 std::vector<int> hovered = tree_->query(
377 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
378 if (!hovered.empty()) {
379 actual_hover_= hovered[0];
380 } else {
381 actual_hover_ = std::nullopt;
382 }
383
384 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
385 hovered_item_ = actual_hover_;
386
387 Refresh();
388 }
389
390 if (scroll_mode_) {
391 EvaluateScroll(event.GetPosition());
392 }
393
394 mouse_position_ = event.GetPosition();
395
396 event.Skip();
397}
398
399void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
400 double new_zoom = zoom_;
401 if (event.GetWheelRotation() > 0) {
402 new_zoom = std::min(3.0, zoom_ + 0.25);
403 } else {
404 new_zoom = std::max(1.0, zoom_ - 0.25);
405 }
406
407 if (zoom_ != new_zoom) {
408 SetZoom(new_zoom, event.GetPosition());
409 }
410
411 event.Skip();
412}
413
414void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
415 SetScrollSpeed(0, 0);
416 mouse_position_ = std::nullopt;
417}
418
419void SubwayMap::OnMouseClick(wxMouseEvent &event) {
420 bool finished = false;
421
422 if (actual_hover_) {
423 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
424 if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) ||
425 networks_.IsItemInNetwork(*hovered_item_)) {
426 if (actual_hover_ != hovered_item_) {
427 hovered_item_ = actual_hover_;
428
429 if (!hovered_item_) {
430 sticky_hover_ = false;
431 }
432
433 Refresh();
434 } else {
435 sticky_hover_ = !sticky_hover_;
436 }
437
438 finished = true;
439 }
440 }
441
442 if (!finished) {
443 if (scroll_mode_) {
444 scroll_mode_ = false;
445
446 SetScrollSpeed(0, 0);
447
448 SetCursor(wxCURSOR_ARROW);
449 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
450 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
451 event.GetPosition().y < GetSize().GetHeight() / 6 ||
452 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
453 scroll_mode_ = true;
454
455 EvaluateScroll(event.GetPosition());
456
457 SetCursor(wxCURSOR_CROSS);
458 } else {
459 sticky_hover_ = false;
460 }
461 }
462}
463
464void SubwayMap::OnTimer(wxTimerEvent &event) {
465 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
466 Refresh();
467}
468
469void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
470 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
471
472 if (new_zoom != zoom_) {
473 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
474 }
475}
476
477void SubwayMap::OnClickHelp(wxCommandEvent &event) {
478 wxMessageBox(
479 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
480 "corner.\nClick on a side of the screen to start panning. It will follow "
481 "your mouse. Click again to stop.\nHover over a door to see the "
482 "requirements to open it.\nHover over a warp or active painting to see "
483 "what it is connected to.\nIn painting shuffle, paintings that have not "
484 "yet been checked will not show their connections.\nA green shaded owl "
485 "means that there is a painting entrance there.\nA red shaded owl means "
486 "that there are only painting exits there.\nClick on a door or "
487 "warp to make the popup stick until you click again.",
488 "Subway Map Help");
489}
490
491void SubwayMap::Redraw() {
492 rendered_ = wxBitmap(map_image_);
493
494 wxMemoryDC dc;
495 dc.SelectObject(rendered_);
496
497 wxGCDC gcdc(dc);
498
499 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
500 ItemDrawType draw_type = ItemDrawType::kNone;
501 const wxBrush *brush_color = wxGREY_BRUSH;
502 std::optional<wxColour> shade_color;
503
504 if (AP_HasEarlyColorHallways() &&
505 (subway_item.special == "starting_room_paintings" ||
506 subway_item.special == "early_color_hallways")) {
507 draw_type = ItemDrawType::kOwl;
508
509 if (subway_item.special == "starting_room_paintings") {
510 shade_color = wxColour(0, 255, 0, 128);
511 } else {
512 shade_color = wxColour(255, 0, 0, 128);
513 }
514 } else if (subway_item.special == "sun_painting") {
515 if (!AP_IsPilgrimageEnabled()) {
516 if (IsDoorOpen(*subway_item.door)) {
517 draw_type = ItemDrawType::kOwl;
518 shade_color = wxColour(0, 255, 0, 128);
519 } else {
520 draw_type = ItemDrawType::kBox;
521 brush_color = wxRED_BRUSH;
522 }
523 }
524 } else if (!subway_item.paintings.empty()) {
525 if (AP_IsPaintingShuffle()) {
526 bool has_checked_painting = false;
527 bool has_unchecked_painting = false;
528 bool has_mapped_painting = false;
529 bool has_codomain_painting = false;
530
531 for (const std::string &painting_id : subway_item.paintings) {
532 if (checked_paintings_.count(painting_id)) {
533 has_checked_painting = true;
534
535 if (AP_GetPaintingMapping().count(painting_id)) {
536 has_mapped_painting = true;
537 } else if (AP_IsPaintingMappedTo(painting_id)) {
538 has_codomain_painting = true;
539 }
540 } else {
541 has_unchecked_painting = true;
542 }
543 }
544
545 if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) {
546 draw_type = ItemDrawType::kOwl;
547
548 if (has_checked_painting) {
549 if (has_mapped_painting) {
550 shade_color = wxColour(0, 255, 0, 128);
551 } else {
552 shade_color = wxColour(255, 0, 0, 128);
553 }
554 }
555 }
556 } else if (!subway_item.tags.empty()) {
557 draw_type = ItemDrawType::kOwl;
558 }
559 } else if (subway_item.door) {
560 draw_type = ItemDrawType::kBox;
561
562 if (IsDoorOpen(*subway_item.door)) {
563 brush_color = wxGREEN_BRUSH;
564 } else {
565 brush_color = wxRED_BRUSH;
566 }
567 }
568
569 wxPoint real_area_pos = {subway_item.x, subway_item.y};
570
571 int real_area_size =
572 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
573
574 if (draw_type == ItemDrawType::kBox) {
575 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
576 gcdc.SetBrush(*brush_color);
577 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
578 } else if (draw_type == ItemDrawType::kOwl) {
579 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale(
580 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR));
581 gcdc.DrawBitmap(owl_bitmap, real_area_pos);
582
583 if (shade_color) {
584 gcdc.SetBrush(wxBrush(*shade_color));
585 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
586 }
587 }
588 }
589}
590
591void SubwayMap::SetUpHelpButton() {
592 help_button_->SetPosition({
593 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
594 15,
595 });
596}
597
598void SubwayMap::EvaluateScroll(wxPoint pos) {
599 int scroll_x;
600 int scroll_y;
601 if (pos.x < GetSize().GetWidth() / 9) {
602 scroll_x = 20;
603 } else if (pos.x < GetSize().GetWidth() / 6) {
604 scroll_x = 5;
605 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
606 scroll_x = -20;
607 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
608 scroll_x = -5;
609 } else {
610 scroll_x = 0;
611 }
612 if (pos.y < GetSize().GetHeight() / 9) {
613 scroll_y = 20;
614 } else if (pos.y < GetSize().GetHeight() / 6) {
615 scroll_y = 5;
616 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
617 scroll_y = -20;
618 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
619 scroll_y = -5;
620 } else {
621 scroll_y = 0;
622 }
623
624 SetScrollSpeed(scroll_x, scroll_y);
625}
626
627wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
628 return {static_cast<int>(pos.x * render_width_ * zoom_ /
629 map_image_.GetSize().GetWidth() +
630 zoom_x_),
631 static_cast<int>(pos.y * render_width_ * zoom_ /
632 map_image_.GetSize().GetWidth() +
633 zoom_y_)};
634}
635
636wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
637 return {static_cast<int>(pos.x * render_width_ * zoom_ /
638 map_image_.GetSize().GetWidth()),
639 static_cast<int>(pos.y * render_width_ * zoom_ /
640 map_image_.GetSize().GetWidth())};
641}
642
643wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
644 return {
645 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
646 render_width_ / zoom_),
647 0, map_image_.GetWidth() - 1),
648 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
649 render_width_ / zoom_),
650 0, map_image_.GetHeight() - 1)};
651}
652
653void SubwayMap::SetZoomPos(wxPoint pos) {
654 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
655 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
656 } else {
657 zoom_x_ = std::clamp(
658 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
659 0);
660 }
661 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
662 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
663 } else {
664 zoom_y_ = std::clamp(
665 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
666 0);
667 }
668}
669
670void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
671 bool should_timer = (scroll_x != 0 || scroll_y != 0);
672 if (should_timer != scroll_timer_->IsRunning()) {
673 if (should_timer) {
674 scroll_timer_->Start(1000 / 60);
675 } else {
676 scroll_timer_->Stop();
677 }
678 }
679
680 scroll_x_ = scroll_x;
681 scroll_y_ = scroll_y;
682}
683
684void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
685 wxPoint map_pos = RenderPosToMapPos(static_point);
686 zoom_ = zoom;
687
688 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
689 SetZoomPos(-(virtual_pos - static_point));
690
691 Refresh();
692
693 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
694}
695
696quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
697 const SubwayItem &subway_item = GD_GetSubwayItem(id);
698 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
699 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
700}
diff --git a/src/subway_map.h b/src/subway_map.h new file mode 100644 index 0000000..feee8ff --- /dev/null +++ b/src/subway_map.h
@@ -0,0 +1,92 @@
1#ifndef SUBWAY_MAP_H_BD2D843E
2#define SUBWAY_MAP_H_BD2D843E
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <memory>
11#include <optional>
12#include <set>
13#include <string>
14#include <vector>
15
16#include <quadtree/Quadtree.h>
17
18#include "game_data.h"
19#include "network_set.h"
20
21class SubwayMap : public wxPanel {
22 public:
23 SubwayMap(wxWindow *parent);
24
25 void OnConnect();
26 void UpdateIndicators();
27 void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp);
28 void Zoom(bool in);
29
30 private:
31 void OnPaint(wxPaintEvent &event);
32 void OnMouseMove(wxMouseEvent &event);
33 void OnMouseScroll(wxMouseEvent &event);
34 void OnMouseLeave(wxMouseEvent &event);
35 void OnMouseClick(wxMouseEvent &event);
36 void OnTimer(wxTimerEvent &event);
37 void OnZoomSlide(wxCommandEvent &event);
38 void OnClickHelp(wxCommandEvent &event);
39
40 void Redraw();
41 void SetUpHelpButton();
42
43 wxPoint MapPosToRenderPos(wxPoint pos) const;
44 wxPoint MapPosToVirtualPos(wxPoint pos) const;
45 wxPoint RenderPosToMapPos(wxPoint pos) const;
46
47 void EvaluateScroll(wxPoint pos);
48
49 void SetZoomPos(wxPoint pos);
50 void SetScrollSpeed(int scroll_x, int scroll_y);
51 void SetZoom(double zoom, wxPoint static_point);
52
53 wxImage map_image_;
54 wxImage owl_image_;
55 wxBitmap unchecked_eye_;
56 wxBitmap checked_eye_;
57
58 wxBitmap rendered_;
59 int render_x_ = 0;
60 int render_y_ = 0;
61 int render_width_ = 1;
62 int render_height_ = 1;
63
64 double zoom_ = 1.0;
65 int zoom_x_ = 0; // in render space
66 int zoom_y_ = 0;
67
68 bool scroll_mode_ = false;
69 wxTimer* scroll_timer_;
70 int scroll_x_ = 0;
71 int scroll_y_ = 0;
72
73 wxSlider *zoom_slider_;
74
75 wxButton *help_button_;
76
77 std::optional<wxPoint> mouse_position_;
78
79 struct GetItemBox {
80 quadtree::Box<float> operator()(const int &id) const;
81 };
82
83 std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_;
84 std::optional<int> hovered_item_;
85 std::optional<int> actual_hover_;
86 bool sticky_hover_ = false;
87
88 NetworkSet networks_;
89 std::set<std::string> checked_paintings_;
90};
91
92#endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index d64e0d3..107ae49 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -1,6 +1,8 @@
1#include "tracker_frame.h" 1#include "tracker_frame.h"
2 2
3#include <wx/aboutdlg.h>
3#include <wx/choicebk.h> 4#include <wx/choicebk.h>
5#include <wx/notebook.h>
4#include <wx/webrequest.h> 6#include <wx/webrequest.h>
5 7
6#include <nlohmann/json.hpp> 8#include <nlohmann/json.hpp>
@@ -10,6 +12,7 @@
10#include "ap_state.h" 12#include "ap_state.h"
11#include "connection_dialog.h" 13#include "connection_dialog.h"
12#include "settings_dialog.h" 14#include "settings_dialog.h"
15#include "subway_map.h"
13#include "tracker_config.h" 16#include "tracker_config.h"
14#include "tracker_panel.h" 17#include "tracker_panel.h"
15#include "version.h" 18#include "version.h"
@@ -17,9 +20,12 @@
17enum TrackerFrameIds { 20enum TrackerFrameIds {
18 ID_CONNECT = 1, 21 ID_CONNECT = 1,
19 ID_CHECK_FOR_UPDATES = 2, 22 ID_CHECK_FOR_UPDATES = 2,
20 ID_SETTINGS = 3 23 ID_SETTINGS = 3,
24 ID_ZOOM_IN = 4,
25 ID_ZOOM_OUT = 5,
21}; 26};
22 27
28wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
23wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 29wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent);
24wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 30wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
25 31
@@ -35,12 +41,20 @@ TrackerFrame::TrackerFrame()
35 menuFile->Append(ID_SETTINGS, "&Settings"); 41 menuFile->Append(ID_SETTINGS, "&Settings");
36 menuFile->Append(wxID_EXIT); 42 menuFile->Append(wxID_EXIT);
37 43
44 wxMenu *menuView = new wxMenu();
45 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+");
46 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--");
47
48 zoom_in_menu_item_->Enable(false);
49 zoom_out_menu_item_->Enable(false);
50
38 wxMenu *menuHelp = new wxMenu(); 51 wxMenu *menuHelp = new wxMenu();
39 menuHelp->Append(wxID_ABOUT); 52 menuHelp->Append(wxID_ABOUT);
40 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates"); 53 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates");
41 54
42 wxMenuBar *menuBar = new wxMenuBar(); 55 wxMenuBar *menuBar = new wxMenuBar();
43 menuBar->Append(menuFile, "&File"); 56 menuBar->Append(menuFile, "&File");
57 menuBar->Append(menuView, "&View");
44 menuBar->Append(menuHelp, "&Help"); 58 menuBar->Append(menuHelp, "&Help");
45 59
46 SetMenuBar(menuBar); 60 SetMenuBar(menuBar);
@@ -54,18 +68,26 @@ TrackerFrame::TrackerFrame()
54 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); 68 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
55 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, 69 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
56 ID_CHECK_FOR_UPDATES); 70 ID_CHECK_FOR_UPDATES);
71 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
72 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
73 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
74 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
57 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 75 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
58 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); 76 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
59 77
60 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); 78 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY);
61 achievements_pane_ = new AchievementsPane(this); 79 achievements_pane_ = new AchievementsPane(choicebook);
62 choicebook->AddPage(achievements_pane_, "Achievements"); 80 choicebook->AddPage(achievements_pane_, "Achievements");
63 81
64 tracker_panel_ = new TrackerPanel(this); 82 notebook_ = new wxNotebook(this, wxID_ANY);
83 tracker_panel_ = new TrackerPanel(notebook_);
84 subway_map_ = new SubwayMap(notebook_);
85 notebook_->AddPage(tracker_panel_, "Map");
86 notebook_->AddPage(subway_map_, "Subway");
65 87
66 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 88 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL);
67 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1)); 89 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
68 top_sizer->Add(tracker_panel_, wxSizerFlags().Expand().Proportion(3)); 90 top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3));
69 91
70 SetSizerAndFit(top_sizer); 92 SetSizerAndFit(top_sizer);
71 SetSize(1280, 728); 93 SetSize(1280, 728);
@@ -96,17 +118,23 @@ void TrackerFrame::SetStatusMessage(std::string message) {
96 QueueEvent(event); 118 QueueEvent(event);
97} 119}
98 120
121void TrackerFrame::ResetIndicators() {
122 QueueEvent(new wxCommandEvent(STATE_RESET));
123}
124
99void TrackerFrame::UpdateIndicators() { 125void TrackerFrame::UpdateIndicators() {
100 QueueEvent(new wxCommandEvent(STATE_CHANGED)); 126 QueueEvent(new wxCommandEvent(STATE_CHANGED));
101} 127}
102 128
103void TrackerFrame::OnAbout(wxCommandEvent &event) { 129void TrackerFrame::OnAbout(wxCommandEvent &event) {
104 std::ostringstream message_text; 130 wxAboutDialogInfo about_info;
105 message_text << "Lingo Archipelago Tracker " << kTrackerVersion 131 about_info.SetName("Lingo Archipelago Tracker");
106 << " by hatkirby"; 132 about_info.SetVersion(kTrackerVersion.ToString());
107 133 about_info.AddDeveloper("hatkirby");
108 wxMessageBox(message_text.str(), "About lingo-ap-tracker", 134 about_info.AddArtist("Brenton Wildes");
109 wxOK | wxICON_INFORMATION); 135 about_info.AddArtist("kinrah");
136
137 wxAboutBox(about_info);
110} 138}
111 139
112void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } 140void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
@@ -122,7 +150,8 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
122 std::deque<ConnectionDetails> new_history; 150 std::deque<ConnectionDetails> new_history;
123 new_history.push_back(GetTrackerConfig().connection_details); 151 new_history.push_back(GetTrackerConfig().connection_details);
124 152
125 for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { 153 for (const ConnectionDetails &details :
154 GetTrackerConfig().connection_history) {
126 if (details != GetTrackerConfig().connection_details) { 155 if (details != GetTrackerConfig().connection_details) {
127 new_history.push_back(details); 156 new_history.push_back(details);
128 } 157 }
@@ -158,9 +187,34 @@ void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
158 CheckForUpdates(/*manual=*/true); 187 CheckForUpdates(/*manual=*/true);
159} 188}
160 189
190void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
191 if (notebook_->GetSelection() == 1) {
192 subway_map_->Zoom(true);
193 }
194}
195
196void TrackerFrame::OnZoomOut(wxCommandEvent& event) {
197 if (notebook_->GetSelection() == 1) {
198 subway_map_->Zoom(false);
199 }
200}
201
202void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
203 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
204 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
205}
206
207void TrackerFrame::OnStateReset(wxCommandEvent& event) {
208 tracker_panel_->UpdateIndicators();
209 achievements_pane_->UpdateIndicators();
210 subway_map_->OnConnect();
211 Refresh();
212}
213
161void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 214void TrackerFrame::OnStateChanged(wxCommandEvent &event) {
162 tracker_panel_->UpdateIndicators(); 215 tracker_panel_->UpdateIndicators();
163 achievements_pane_->UpdateIndicators(); 216 achievements_pane_->UpdateIndicators();
217 subway_map_->UpdateIndicators();
164 Refresh(); 218 Refresh();
165} 219}
166 220
@@ -192,8 +246,10 @@ void TrackerFrame::CheckForUpdates(bool manual) {
192 std::ostringstream message_text; 246 std::ostringstream message_text;
193 message_text << "There is a newer version of Lingo AP Tracker " 247 message_text << "There is a newer version of Lingo AP Tracker "
194 "available. You have " 248 "available. You have "
195 << kTrackerVersion << ", and the latest version is " 249 << kTrackerVersion.ToString()
196 << latest_version << ". Would you like to update?"; 250 << ", and the latest version is "
251 << latest_version.ToString()
252 << ". Would you like to update?";
197 253
198 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) == 254 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
199 wxYES) { 255 wxYES) {
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index e5bf97e..f7cb3f2 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -8,8 +8,12 @@
8#endif 8#endif
9 9
10class AchievementsPane; 10class AchievementsPane;
11class SubwayMap;
11class TrackerPanel; 12class TrackerPanel;
13class wxBookCtrlEvent;
14class wxNotebook;
12 15
16wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
13wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 17wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent);
14wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 18wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
15 19
@@ -19,6 +23,7 @@ class TrackerFrame : public wxFrame {
19 23
20 void SetStatusMessage(std::string message); 24 void SetStatusMessage(std::string message);
21 25
26 void ResetIndicators();
22 void UpdateIndicators(); 27 void UpdateIndicators();
23 28
24 private: 29 private:
@@ -27,14 +32,23 @@ class TrackerFrame : public wxFrame {
27 void OnConnect(wxCommandEvent &event); 32 void OnConnect(wxCommandEvent &event);
28 void OnSettings(wxCommandEvent &event); 33 void OnSettings(wxCommandEvent &event);
29 void OnCheckForUpdates(wxCommandEvent &event); 34 void OnCheckForUpdates(wxCommandEvent &event);
35 void OnZoomIn(wxCommandEvent &event);
36 void OnZoomOut(wxCommandEvent &event);
37 void OnChangePage(wxBookCtrlEvent &event);
30 38
39 void OnStateReset(wxCommandEvent &event);
31 void OnStateChanged(wxCommandEvent &event); 40 void OnStateChanged(wxCommandEvent &event);
32 void OnStatusChanged(wxCommandEvent &event); 41 void OnStatusChanged(wxCommandEvent &event);
33 42
34 void CheckForUpdates(bool manual); 43 void CheckForUpdates(bool manual);
35 44
45 wxNotebook *notebook_;
36 TrackerPanel *tracker_panel_; 46 TrackerPanel *tracker_panel_;
37 AchievementsPane *achievements_pane_; 47 AchievementsPane *achievements_pane_;
48 SubwayMap *subway_map_;
49
50 wxMenuItem *zoom_in_menu_item_;
51 wxMenuItem *zoom_out_menu_item_;
38}; 52};
39 53
40#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ 54#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */
diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index 5f9f8ea..d60c1b6 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -1,5 +1,7 @@
1#include "tracker_panel.h" 1#include "tracker_panel.h"
2 2
3#include <wx/dcbuffer.h>
4
3#include "ap_state.h" 5#include "ap_state.h"
4#include "area_popup.h" 6#include "area_popup.h"
5#include "game_data.h" 7#include "game_data.h"
@@ -13,6 +15,8 @@ constexpr int AREA_EFFECTIVE_SIZE = AREA_ACTUAL_SIZE + AREA_BORDER_SIZE * 2;
13constexpr int PLAYER_SIZE = 96; 15constexpr int PLAYER_SIZE = 96;
14 16
15TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 17TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
18 SetBackgroundStyle(wxBG_STYLE_PAINT);
19
16 map_image_ = wxImage(GetAbsolutePath("assets/lingo_map.png").c_str(), 20 map_image_ = wxImage(GetAbsolutePath("assets/lingo_map.png").c_str(),
17 wxBITMAP_TYPE_PNG); 21 wxBITMAP_TYPE_PNG);
18 if (!map_image_.IsOk()) { 22 if (!map_image_.IsOk()) {
@@ -54,7 +58,7 @@ void TrackerPanel::OnPaint(wxPaintEvent &event) {
54 Redraw(); 58 Redraw();
55 } 59 }
56 60
57 wxPaintDC dc(this); 61 wxBufferedPaintDC dc(this);
58 dc.DrawBitmap(rendered_, 0, 0); 62 dc.DrawBitmap(rendered_, 0, 0);
59 63
60 if (AP_GetPlayerPosition().has_value()) { 64 if (AP_GetPlayerPosition().has_value()) {
@@ -139,7 +143,8 @@ void TrackerPanel::Redraw() {
139 for (AreaIndicator &area : areas_) { 143 for (AreaIndicator &area : areas_) {
140 const MapArea &map_area = GD_GetMapArea(area.area_id); 144 const MapArea &map_area = GD_GetMapArea(area.area_id);
141 if (!AP_IsLocationVisible(map_area.classification) && 145 if (!AP_IsLocationVisible(map_area.classification) &&
142 !(map_area.hunt && GetTrackerConfig().show_hunt_panels)) { 146 !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
147 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
143 area.active = false; 148 area.active = false;
144 continue; 149 continue;
145 } else { 150 } else {
@@ -167,6 +172,21 @@ void TrackerPanel::Redraw() {
167 } 172 }
168 } 173 }
169 174
175 if (AP_IsPaintingShuffle()) {
176 for (int painting_id : map_area.paintings) {
177 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
178 if (!AP_IsPaintingChecked(painting.internal_id)) {
179 bool reachable = IsPaintingReachable(painting_id);
180
181 if (reachable) {
182 has_reachable_unchecked = true;
183 } else {
184 has_unreachable_unchecked = true;
185 }
186 }
187 }
188 }
189
170 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 190 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
171 final_width / image_size.GetWidth(); 191 final_width / image_size.GetWidth();
172 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * 192 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 640a159..66e7751 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -12,9 +12,142 @@
12 12
13namespace { 13namespace {
14 14
15struct Requirements {
16 bool disabled = false;
17
18 std::set<int> doors; // non-grouped, handles progressive
19 std::set<int> items; // all other items
20 std::set<int> rooms; // maybe
21 bool mastery = false; // maybe
22 bool panel_hunt = false; // maybe
23
24 void Merge(const Requirements& rhs) {
25 if (rhs.disabled) {
26 return;
27 }
28
29 for (int id : rhs.doors) {
30 doors.insert(id);
31 }
32 for (int id : rhs.items) {
33 items.insert(id);
34 }
35 for (int id : rhs.rooms) {
36 rooms.insert(id);
37 }
38 mastery = mastery || rhs.mastery;
39 panel_hunt = panel_hunt || rhs.panel_hunt;
40 }
41};
42
43class RequirementCalculator {
44 public:
45 void Reset() {
46 doors_.clear();
47 panels_.clear();
48 }
49
50 const Requirements& GetDoor(int door_id) {
51 if (!doors_.count(door_id)) {
52 Requirements requirements;
53 const Door& door_obj = GD_GetDoor(door_id);
54
55 if (door_obj.type == DoorType::kSunPainting) {
56 if (!AP_IsPilgrimageEnabled()) {
57 requirements.items.insert(door_obj.ap_item_id);
58 } else {
59 requirements.disabled = true;
60 }
61 } else if (door_obj.type == DoorType::kSunwarp) {
62 switch (AP_GetSunwarpAccess()) {
63 case kSUNWARP_ACCESS_NORMAL:
64 // Do nothing.
65 break;
66 case kSUNWARP_ACCESS_DISABLED:
67 requirements.disabled = true;
68 break;
69 case kSUNWARP_ACCESS_UNLOCK:
70 requirements.items.insert(door_obj.group_ap_item_id);
71 break;
72 case kSUNWARP_ACCESS_INDIVIDUAL:
73 case kSUNWARP_ACCESS_PROGRESSIVE:
74 requirements.doors.insert(door_obj.id);
75 break;
76 }
77 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
78 requirements.rooms.insert(door_obj.room);
79
80 for (int panel_id : door_obj.panels) {
81 const Requirements& panel_reqs = GetPanel(panel_id);
82 requirements.Merge(panel_reqs);
83 }
84 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
85 !door_obj.group_name.empty()) {
86 requirements.items.insert(door_obj.group_ap_item_id);
87 } else {
88 requirements.doors.insert(door_obj.id);
89 }
90
91 doors_[door_id] = requirements;
92 }
93
94 return doors_[door_id];
95 }
96
97 const Requirements& GetPanel(int panel_id) {
98 if (!panels_.count(panel_id)) {
99 Requirements requirements;
100 const Panel& panel_obj = GD_GetPanel(panel_id);
101
102 requirements.rooms.insert(panel_obj.room);
103
104 if (panel_obj.name == "THE MASTER") {
105 requirements.mastery = true;
106 }
107
108 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") &&
109 AP_GetLevel2Requirement() > 1) {
110 requirements.panel_hunt = true;
111 }
112
113 for (int room_id : panel_obj.required_rooms) {
114 requirements.rooms.insert(room_id);
115 }
116
117 for (int door_id : panel_obj.required_doors) {
118 const Requirements& door_reqs = GetDoor(door_id);
119 requirements.Merge(door_reqs);
120 }
121
122 for (int panel_id : panel_obj.required_panels) {
123 const Requirements& panel_reqs = GetPanel(panel_id);
124 requirements.Merge(panel_reqs);
125 }
126
127 if (AP_IsColorShuffle()) {
128 for (LingoColor color : panel_obj.colors) {
129 requirements.items.insert(GD_GetItemIdForColor(color));
130 }
131 }
132
133 panels_[panel_id] = requirements;
134 }
135
136 return panels_[panel_id];
137 }
138
139 private:
140 std::map<int, Requirements> doors_;
141 std::map<int, Requirements> panels_;
142};
143
15struct TrackerState { 144struct TrackerState {
16 std::map<int, bool> reachability; 145 std::map<int, bool> reachability;
146 std::set<int> reachable_doors;
147 std::set<int> reachable_paintings;
17 std::mutex reachability_mutex; 148 std::mutex reachability_mutex;
149 RequirementCalculator requirements;
150 std::map<int, std::map<std::string, bool>> door_reports;
18}; 151};
19 152
20enum Decision { kYes, kNo, kMaybe }; 153enum Decision { kYes, kNo, kMaybe };
@@ -41,6 +174,7 @@ class StateCalculator {
41 174
42 void Calculate() { 175 void Calculate() {
43 std::list<int> panel_boundary; 176 std::list<int> panel_boundary;
177 std::list<int> painting_boundary;
44 std::list<Exit> flood_boundary; 178 std::list<Exit> flood_boundary;
45 flood_boundary.push_back({.destination_room = options_.start}); 179 flood_boundary.push_back({.destination_room = options_.start});
46 180
@@ -48,6 +182,8 @@ class StateCalculator {
48 while (reachable_changed) { 182 while (reachable_changed) {
49 reachable_changed = false; 183 reachable_changed = false;
50 184
185 std::list<Exit> new_boundary;
186
51 std::list<int> new_panel_boundary; 187 std::list<int> new_panel_boundary;
52 for (int panel_id : panel_boundary) { 188 for (int panel_id : panel_boundary) {
53 if (solveable_panels_.count(panel_id)) { 189 if (solveable_panels_.count(panel_id)) {
@@ -63,7 +199,33 @@ class StateCalculator {
63 } 199 }
64 } 200 }
65 201
66 std::list<Exit> new_boundary; 202 std::list<int> new_painting_boundary;
203 for (int painting_id : painting_boundary) {
204 if (reachable_paintings_.count(painting_id)) {
205 continue;
206 }
207
208 Decision painting_reachable = IsPaintingReachable(painting_id);
209 if (painting_reachable == kYes) {
210 reachable_paintings_.insert(painting_id);
211 reachable_changed = true;
212
213 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
214 if (AP_GetPaintingMapping().count(cur_painting.internal_id) &&
215 AP_GetCheckedPaintings().count(cur_painting.internal_id)) {
216 Exit painting_exit;
217 PaintingExit target_painting =
218 GD_GetPaintingExit(GD_GetPaintingByName(
219 AP_GetPaintingMapping().at(cur_painting.internal_id)));
220 painting_exit.destination_room = target_painting.room;
221
222 new_boundary.push_back(painting_exit);
223 }
224 } else if (painting_reachable == kMaybe) {
225 new_painting_boundary.push_back(painting_id);
226 }
227 }
228
67 for (const Exit& room_exit : flood_boundary) { 229 for (const Exit& room_exit : flood_boundary) {
68 if (reachable_rooms_.count(room_exit.destination_room)) { 230 if (reachable_rooms_.count(room_exit.destination_room)) {
69 continue; 231 continue;
@@ -98,15 +260,8 @@ class StateCalculator {
98 } 260 }
99 261
100 if (AP_IsPaintingShuffle()) { 262 if (AP_IsPaintingShuffle()) {
101 for (const PaintingExit& out_edge : room_obj.paintings) { 263 for (int out_edge : room_obj.paintings) {
102 if (AP_GetPaintingMapping().count(out_edge.id)) { 264 new_painting_boundary.push_back(out_edge);
103 Exit painting_exit;
104 painting_exit.destination_room = GD_GetRoomForPainting(
105 AP_GetPaintingMapping().at(out_edge.id));
106 painting_exit.door = out_edge.door;
107
108 new_boundary.push_back(painting_exit);
109 }
110 } 265 }
111 } 266 }
112 267
@@ -155,6 +310,13 @@ class StateCalculator {
155 310
156 flood_boundary = new_boundary; 311 flood_boundary = new_boundary;
157 panel_boundary = new_panel_boundary; 312 panel_boundary = new_panel_boundary;
313 painting_boundary = new_painting_boundary;
314 }
315
316 // Now that we know the full reachable area, let's make sure all doors are
317 // evaluated.
318 for (const Door& door : GD_GetDoors()) {
319 int discard = IsDoorReachable(door.id);
158 } 320 }
159 } 321 }
160 322
@@ -166,6 +328,14 @@ class StateCalculator {
166 328
167 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } 329 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; }
168 330
331 const std::set<int>& GetReachablePaintings() const {
332 return reachable_paintings_;
333 }
334
335 const std::map<int, std::map<std::string, bool>>& GetDoorReports() const {
336 return door_report_;
337 }
338
169 private: 339 private:
170 Decision IsNonGroupedDoorReachable(const Door& door_obj) { 340 Decision IsNonGroupedDoorReachable(const Door& door_obj) {
171 bool has_item = AP_HasItem(door_obj.ap_item_id); 341 bool has_item = AP_HasItem(door_obj.ap_item_id);
@@ -182,68 +352,52 @@ class StateCalculator {
182 return has_item ? kYes : kNo; 352 return has_item ? kYes : kNo;
183 } 353 }
184 354
185 Decision IsDoorReachable_Helper(int door_id) { 355 Decision AreRequirementsSatisfied(
186 const Door& door_obj = GD_GetDoor(door_id); 356 const Requirements& reqs, std::map<std::string, bool>* report = nullptr) {
187 357 if (reqs.disabled) {
188 if (!AP_IsPilgrimageEnabled() && door_obj.type == DoorType::kSunPainting) { 358 return kNo;
189 return AP_HasItem(door_obj.ap_item_id) ? kYes : kNo;
190 } else if (door_obj.type == DoorType::kSunwarp) {
191 switch (AP_GetSunwarpAccess()) {
192 case kSUNWARP_ACCESS_NORMAL:
193 return kYes;
194 case kSUNWARP_ACCESS_DISABLED:
195 return kNo;
196 case kSUNWARP_ACCESS_UNLOCK:
197 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo;
198 case kSUNWARP_ACCESS_INDIVIDUAL:
199 case kSUNWARP_ACCESS_PROGRESSIVE:
200 return IsNonGroupedDoorReachable(door_obj);
201 }
202 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) {
203 if (!reachable_rooms_.count(door_obj.room)) {
204 return kMaybe;
205 }
206
207 for (int panel_id : door_obj.panels) {
208 if (!solveable_panels_.count(panel_id)) {
209 return kMaybe;
210 }
211 }
212
213 return kYes;
214 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS &&
215 !door_obj.group_name.empty()) {
216 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo;
217 } else {
218 return IsNonGroupedDoorReachable(door_obj);
219 } 359 }
220 }
221 360
222 Decision IsDoorReachable(int door_id) { 361 Decision final_decision = kYes;
223 if (options_.parent) {
224 return options_.parent->IsDoorReachable(door_id);
225 }
226 362
227 if (door_decisions_.count(door_id)) { 363 for (int door_id : reqs.doors) {
228 return door_decisions_.at(door_id); 364 const Door& door_obj = GD_GetDoor(door_id);
365 Decision decision = IsNonGroupedDoorReachable(door_obj);
366
367 if (report) {
368 (*report)[door_obj.item_name] = (decision == kYes);
369 }
370
371 if (decision != kYes) {
372 final_decision = decision;
373 }
229 } 374 }
230 375
231 Decision result = IsDoorReachable_Helper(door_id); 376 for (int item_id : reqs.items) {
232 if (result != kMaybe) { 377 bool has_item = AP_HasItem(item_id);
233 door_decisions_[door_id] = result; 378 if (report) {
379 (*report)[AP_GetItemName(item_id)] = has_item;
380 }
381
382 if (!has_item) {
383 final_decision = kNo;
384 }
234 } 385 }
235 386
236 return result; 387 for (int room_id : reqs.rooms) {
237 } 388 bool reachable = reachable_rooms_.count(room_id);
238 389
239 Decision IsPanelReachable(int panel_id) { 390 if (report) {
240 const Panel& panel_obj = GD_GetPanel(panel_id); 391 std::string report_name = "Reach \"" + GD_GetRoom(room_id).name + "\"";
392 (*report)[report_name] = reachable;
393 }
241 394
242 if (!reachable_rooms_.count(panel_obj.room)) { 395 if (!reachable && final_decision != kNo) {
243 return kMaybe; 396 final_decision = kMaybe;
397 }
244 } 398 }
245 399
246 if (panel_obj.name == "THE MASTER") { 400 if (reqs.mastery) {
247 int achievements_accessible = 0; 401 int achievements_accessible = 0;
248 402
249 for (int achieve_id : GD_GetAchievementPanels()) { 403 for (int achieve_id : GD_GetAchievementPanels()) {
@@ -256,12 +410,18 @@ class StateCalculator {
256 } 410 }
257 } 411 }
258 412
259 return (achievements_accessible >= AP_GetMasteryRequirement()) ? kYes 413 bool can_mastery =
260 : kMaybe; 414 (achievements_accessible >= AP_GetMasteryRequirement());
415 if (report) {
416 (*report)["Mastery"] = can_mastery;
417 }
418
419 if (!can_mastery && final_decision != kNo) {
420 final_decision = kMaybe;
421 }
261 } 422 }
262 423
263 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && 424 if (reqs.panel_hunt) {
264 AP_GetLevel2Requirement() > 1) {
265 int counting_panels_accessible = 0; 425 int counting_panels_accessible = 0;
266 426
267 for (int solved_panel_id : solveable_panels_) { 427 for (int solved_panel_id : solveable_panels_) {
@@ -272,41 +432,58 @@ class StateCalculator {
272 } 432 }
273 } 433 }
274 434
275 return (counting_panels_accessible >= AP_GetLevel2Requirement() - 1) 435 bool can_level2 =
276 ? kYes 436 (counting_panels_accessible >= AP_GetLevel2Requirement() - 1);
277 : kMaybe; 437 if (report) {
278 } 438 std::string report_name =
439 std::to_string(AP_GetLevel2Requirement()) + " Panels";
440 (*report)[report_name] = can_level2;
441 }
279 442
280 for (int room_id : panel_obj.required_rooms) { 443 if (!can_level2 && final_decision != kNo) {
281 if (!reachable_rooms_.count(room_id)) { 444 final_decision = kMaybe;
282 return kMaybe;
283 } 445 }
284 } 446 }
285 447
286 for (int door_id : panel_obj.required_doors) { 448 return final_decision;
287 Decision door_reachable = IsDoorReachable(door_id); 449 }
288 if (door_reachable == kNo) { 450
289 const Door& door_obj = GD_GetDoor(door_id); 451 Decision IsDoorReachable_Helper(int door_id) {
290 return (door_obj.is_event || AP_GetDoorShuffleMode() == kNO_DOORS) 452 if (door_report_.count(door_id)) {
291 ? kMaybe 453 door_report_[door_id].clear();
292 : kNo; 454 } else {
293 } else if (door_reachable == kMaybe) { 455 door_report_[door_id] = {};
294 return kMaybe;
295 }
296 } 456 }
297 457
298 for (int panel_id : panel_obj.required_panels) { 458 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id),
299 if (!solveable_panels_.count(panel_id)) { 459 &door_report_[door_id]);
300 return kMaybe; 460 }
301 } 461
462 Decision IsDoorReachable(int door_id) {
463 if (options_.parent) {
464 return options_.parent->IsDoorReachable(door_id);
302 } 465 }
303 466
304 if (AP_IsColorShuffle()) { 467 if (door_decisions_.count(door_id)) {
305 for (LingoColor color : panel_obj.colors) { 468 return door_decisions_.at(door_id);
306 if (!AP_HasItem(GD_GetItemIdForColor(color))) { 469 }
307 return kNo; 470
308 } 471 Decision result = IsDoorReachable_Helper(door_id);
309 } 472 if (result != kMaybe) {
473 door_decisions_[door_id] = result;
474 }
475
476 return result;
477 }
478
479 Decision IsPanelReachable(int panel_id) {
480 return AreRequirementsSatisfied(GetState().requirements.GetPanel(panel_id));
481 }
482
483 Decision IsPaintingReachable(int painting_id) {
484 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
485 if (painting.door) {
486 return IsDoorReachable(*painting.door);
310 } 487 }
311 488
312 return kYes; 489 return kYes;
@@ -395,10 +572,17 @@ class StateCalculator {
395 std::set<int> reachable_rooms_; 572 std::set<int> reachable_rooms_;
396 std::map<int, Decision> door_decisions_; 573 std::map<int, Decision> door_decisions_;
397 std::set<int> solveable_panels_; 574 std::set<int> solveable_panels_;
575 std::set<int> reachable_paintings_;
576 std::map<int, std::map<std::string, bool>> door_report_;
398}; 577};
399 578
400} // namespace 579} // namespace
401 580
581void ResetReachabilityRequirements() {
582 std::lock_guard reachability_guard(GetState().reachability_mutex);
583 GetState().requirements.Reset();
584}
585
402void RecalculateReachability() { 586void RecalculateReachability() {
403 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 587 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
404 state_calculator.Calculate(); 588 state_calculator.Calculate();
@@ -422,9 +606,23 @@ void RecalculateReachability() {
422 } 606 }
423 } 607 }
424 608
609 std::set<int> new_reachable_doors;
610 for (const auto& [door_id, decision] : state_calculator.GetDoorDecisions()) {
611 if (decision == kYes) {
612 new_reachable_doors.insert(door_id);
613 }
614 }
615
616 std::set<int> reachable_paintings = state_calculator.GetReachablePaintings();
617 std::map<int, std::map<std::string, bool>> door_reports =
618 state_calculator.GetDoorReports();
619
425 { 620 {
426 std::lock_guard reachability_guard(GetState().reachability_mutex); 621 std::lock_guard reachability_guard(GetState().reachability_mutex);
427 std::swap(GetState().reachability, new_reachability); 622 std::swap(GetState().reachability, new_reachability);
623 std::swap(GetState().reachable_doors, new_reachable_doors);
624 std::swap(GetState().reachable_paintings, reachable_paintings);
625 std::swap(GetState().door_reports, door_reports);
428 } 626 }
429} 627}
430 628
@@ -437,3 +635,21 @@ bool IsLocationReachable(int location_id) {
437 return false; 635 return false;
438 } 636 }
439} 637}
638
639bool IsDoorOpen(int door_id) {
640 std::lock_guard reachability_guard(GetState().reachability_mutex);
641
642 return GetState().reachable_doors.count(door_id);
643}
644
645bool IsPaintingReachable(int painting_id) {
646 std::lock_guard reachability_guard(GetState().reachability_mutex);
647
648 return GetState().reachable_paintings.count(painting_id);
649}
650
651const std::map<std::string, bool>& GetDoorRequirements(int door_id) {
652 std::lock_guard reachability_guard(GetState().reachability_mutex);
653
654 return GetState().door_reports[door_id];
655}
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 0ccd2c7..4b13d42 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,10 +37,6 @@ struct Version {
31 } 37 }
32}; 38};
33 39
34std::ostream& operator<<(std::ostream& out, const Version& ver) {
35 return out << "v" << ver.major << "." << ver.minor << "." << ver.revision;
36}
37
38constexpr const Version kTrackerVersion = Version(0, 9, 2); 40constexpr const Version kTrackerVersion = Version(0, 9, 2);
39 41
40#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/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}