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