diff options
author | Star Rauchenberger <fefferburbia@gmail.com> | 2024-06-06 15:54:41 -0400 |
---|---|---|
committer | Star Rauchenberger <fefferburbia@gmail.com> | 2024-06-06 15:54:41 -0400 |
commit | 8ddab49cc13d809ca75dcd7f645661a3d3cb05c4 (patch) | |
tree | ba1e5f3237dbb7cdc939c35e193f5e6e46845a77 /src | |
parent | ac38dd0a5c394eefc39b7a8cf7b96762f18c8b31 (diff) | |
parent | 6f5287b3921c843a6b322ccbdfcbef00a8f16980 (diff) | |
download | lingo-ap-tracker-8ddab49cc13d809ca75dcd7f645661a3d3cb05c4.tar.gz lingo-ap-tracker-8ddab49cc13d809ca75dcd7f645661a3d3cb05c4.tar.bz2 lingo-ap-tracker-8ddab49cc13d809ca75dcd7f645661a3d3cb05c4.zip |
Merge branch 'subway'
Diffstat (limited to 'src')
-rw-r--r-- | src/ap_state.cpp | 120 | ||||
-rw-r--r-- | src/ap_state.h | 11 | ||||
-rw-r--r-- | src/area_popup.cpp | 40 | ||||
-rw-r--r-- | src/game_data.cpp | 221 | ||||
-rw-r--r-- | src/game_data.h | 39 | ||||
-rw-r--r-- | src/logger.cpp | 32 | ||||
-rw-r--r-- | src/logger.h | 8 | ||||
-rw-r--r-- | src/main.cpp | 23 | ||||
-rw-r--r-- | src/network_set.cpp | 30 | ||||
-rw-r--r-- | src/network_set.h | 25 | ||||
-rw-r--r-- | src/subway_map.cpp | 700 | ||||
-rw-r--r-- | src/subway_map.h | 92 | ||||
-rw-r--r-- | src/tracker_frame.cpp | 82 | ||||
-rw-r--r-- | src/tracker_frame.h | 14 | ||||
-rw-r--r-- | src/tracker_panel.cpp | 24 | ||||
-rw-r--r-- | src/tracker_state.cpp | 400 | ||||
-rw-r--r-- | src/tracker_state.h | 11 | ||||
-rw-r--r-- | src/version.h | 12 |
18 files changed, 1646 insertions, 238 deletions
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index 8feb78b..0ce4582 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp | |||
@@ -21,7 +21,6 @@ | |||
21 | #include <tuple> | 21 | #include <tuple> |
22 | 22 | ||
23 | #include "game_data.h" | 23 | #include "game_data.h" |
24 | #include "logger.h" | ||
25 | #include "tracker_frame.h" | 24 | #include "tracker_frame.h" |
26 | #include "tracker_state.h" | 25 | #include "tracker_state.h" |
27 | 26 | ||
@@ -71,11 +70,12 @@ struct APState { | |||
71 | bool sunwarp_shuffle = false; | 70 | bool sunwarp_shuffle = false; |
72 | 71 | ||
73 | std::map<std::string, std::string> painting_mapping; | 72 | std::map<std::string, std::string> painting_mapping; |
73 | std::set<std::string> painting_codomain; | ||
74 | std::map<int, SunwarpMapping> sunwarp_mapping; | 74 | std::map<int, SunwarpMapping> sunwarp_mapping; |
75 | 75 | ||
76 | void Connect(std::string server, std::string player, std::string password) { | 76 | void Connect(std::string server, std::string player, std::string password) { |
77 | if (!initialized) { | 77 | if (!initialized) { |
78 | TrackerLog("Initializing APState..."); | 78 | wxLogVerbose("Initializing APState..."); |
79 | 79 | ||
80 | std::thread([this]() { | 80 | std::thread([this]() { |
81 | for (;;) { | 81 | for (;;) { |
@@ -103,15 +103,16 @@ struct APState { | |||
103 | } | 103 | } |
104 | 104 | ||
105 | tracked_data_storage_keys.push_back("PlayerPos"); | 105 | tracked_data_storage_keys.push_back("PlayerPos"); |
106 | tracked_data_storage_keys.push_back("Paintings"); | ||
106 | 107 | ||
107 | initialized = true; | 108 | initialized = true; |
108 | } | 109 | } |
109 | 110 | ||
110 | tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); | 111 | tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); |
111 | TrackerLog("Connecting to Archipelago server (" + server + ")..."); | 112 | wxLogStatus("Connecting to Archipelago server (%s)...", server); |
112 | 113 | ||
113 | { | 114 | { |
114 | TrackerLog("Destroying old AP client..."); | 115 | wxLogVerbose("Destroying old AP client..."); |
115 | 116 | ||
116 | std::lock_guard client_guard(client_mutex); | 117 | std::lock_guard client_guard(client_mutex); |
117 | 118 | ||
@@ -137,6 +138,7 @@ struct APState { | |||
137 | color_shuffle = false; | 138 | color_shuffle = false; |
138 | painting_shuffle = false; | 139 | painting_shuffle = false; |
139 | painting_mapping.clear(); | 140 | painting_mapping.clear(); |
141 | painting_codomain.clear(); | ||
140 | mastery_requirement = 21; | 142 | mastery_requirement = 21; |
141 | level_2_requirement = 223; | 143 | level_2_requirement = 223; |
142 | location_checks = kNORMAL_LOCATIONS; | 144 | location_checks = kNORMAL_LOCATIONS; |
@@ -155,10 +157,10 @@ struct APState { | |||
155 | apclient->set_room_info_handler([this, player, password]() { | 157 | apclient->set_room_info_handler([this, player, password]() { |
156 | inventory.clear(); | 158 | inventory.clear(); |
157 | 159 | ||
158 | TrackerLog("Connected to Archipelago server. Authenticating as " + | 160 | wxLogStatus("Connected to Archipelago server. Authenticating as %s %s", |
159 | player + | 161 | player, |
160 | (password.empty() ? " without password" | 162 | (password.empty() ? "without password" |
161 | : " with password " + password)); | 163 | : "with password " + password)); |
162 | tracker_frame->SetStatusMessage( | 164 | tracker_frame->SetStatusMessage( |
163 | "Connected to Archipelago server. Authenticating..."); | 165 | "Connected to Archipelago server. Authenticating..."); |
164 | 166 | ||
@@ -170,23 +172,23 @@ struct APState { | |||
170 | [this](const std::list<int64_t>& locations) { | 172 | [this](const std::list<int64_t>& locations) { |
171 | for (const int64_t location_id : locations) { | 173 | for (const int64_t location_id : locations) { |
172 | checked_locations.insert(location_id); | 174 | checked_locations.insert(location_id); |
173 | TrackerLog("Location: " + std::to_string(location_id)); | 175 | wxLogVerbose("Location: %lld", location_id); |
174 | } | 176 | } |
175 | 177 | ||
176 | RefreshTracker(); | 178 | RefreshTracker(false); |
177 | }); | 179 | }); |
178 | 180 | ||
179 | apclient->set_slot_disconnected_handler([this]() { | 181 | apclient->set_slot_disconnected_handler([this]() { |
180 | tracker_frame->SetStatusMessage( | 182 | tracker_frame->SetStatusMessage( |
181 | "Disconnected from Archipelago. Attempting to reconnect..."); | 183 | "Disconnected from Archipelago. Attempting to reconnect..."); |
182 | TrackerLog( | 184 | wxLogStatus( |
183 | "Slot disconnected from Archipelago. Attempting to reconnect..."); | 185 | "Slot disconnected from Archipelago. Attempting to reconnect..."); |
184 | }); | 186 | }); |
185 | 187 | ||
186 | apclient->set_socket_disconnected_handler([this]() { | 188 | apclient->set_socket_disconnected_handler([this]() { |
187 | tracker_frame->SetStatusMessage( | 189 | tracker_frame->SetStatusMessage( |
188 | "Disconnected from Archipelago. Attempting to reconnect..."); | 190 | "Disconnected from Archipelago. Attempting to reconnect..."); |
189 | TrackerLog( | 191 | wxLogStatus( |
190 | "Socket disconnected from Archipelago. Attempting to reconnect..."); | 192 | "Socket disconnected from Archipelago. Attempting to reconnect..."); |
191 | }); | 193 | }); |
192 | 194 | ||
@@ -194,10 +196,10 @@ struct APState { | |||
194 | [this](const std::list<APClient::NetworkItem>& items) { | 196 | [this](const std::list<APClient::NetworkItem>& items) { |
195 | for (const APClient::NetworkItem& item : items) { | 197 | for (const APClient::NetworkItem& item : items) { |
196 | inventory[item.item]++; | 198 | inventory[item.item]++; |
197 | TrackerLog("Item: " + std::to_string(item.item)); | 199 | wxLogVerbose("Item: %lld", item.item); |
198 | } | 200 | } |
199 | 201 | ||
200 | RefreshTracker(); | 202 | RefreshTracker(false); |
201 | }); | 203 | }); |
202 | 204 | ||
203 | apclient->set_retrieved_handler( | 205 | apclient->set_retrieved_handler( |
@@ -206,20 +208,20 @@ struct APState { | |||
206 | HandleDataStorage(key, value); | 208 | HandleDataStorage(key, value); |
207 | } | 209 | } |
208 | 210 | ||
209 | RefreshTracker(); | 211 | RefreshTracker(false); |
210 | }); | 212 | }); |
211 | 213 | ||
212 | apclient->set_set_reply_handler([this](const std::string& key, | 214 | apclient->set_set_reply_handler([this](const std::string& key, |
213 | const nlohmann::json& value, | 215 | const nlohmann::json& value, |
214 | const nlohmann::json&) { | 216 | const nlohmann::json&) { |
215 | HandleDataStorage(key, value); | 217 | HandleDataStorage(key, value); |
216 | RefreshTracker(); | 218 | RefreshTracker(false); |
217 | }); | 219 | }); |
218 | 220 | ||
219 | apclient->set_slot_connected_handler([this]( | 221 | apclient->set_slot_connected_handler([this]( |
220 | const nlohmann::json& slot_data) { | 222 | const nlohmann::json& slot_data) { |
221 | tracker_frame->SetStatusMessage("Connected to Archipelago!"); | 223 | tracker_frame->SetStatusMessage("Connected to Archipelago!"); |
222 | TrackerLog("Connected to Archipelago!"); | 224 | wxLogStatus("Connected to Archipelago!"); |
223 | 225 | ||
224 | data_storage_prefix = | 226 | data_storage_prefix = |
225 | "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; | 227 | "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; |
@@ -253,6 +255,7 @@ struct APState { | |||
253 | for (const auto& mapping_it : | 255 | for (const auto& mapping_it : |
254 | slot_data["painting_entrance_to_exit"].items()) { | 256 | slot_data["painting_entrance_to_exit"].items()) { |
255 | painting_mapping[mapping_it.key()] = mapping_it.value(); | 257 | painting_mapping[mapping_it.key()] = mapping_it.value(); |
258 | painting_codomain.insert(mapping_it.value()); | ||
256 | } | 259 | } |
257 | } | 260 | } |
258 | 261 | ||
@@ -271,7 +274,8 @@ struct APState { | |||
271 | connected = true; | 274 | connected = true; |
272 | has_connection_result = true; | 275 | has_connection_result = true; |
273 | 276 | ||
274 | RefreshTracker(); | 277 | ResetReachabilityRequirements(); |
278 | RefreshTracker(true); | ||
275 | 279 | ||
276 | std::list<std::string> corrected_keys; | 280 | std::list<std::string> corrected_keys; |
277 | for (const std::string& key : tracked_data_storage_keys) { | 281 | for (const std::string& key : tracked_data_storage_keys) { |
@@ -323,7 +327,7 @@ struct APState { | |||
323 | } | 327 | } |
324 | 328 | ||
325 | std::string full_message = hatkirby::implode(error_messages, " "); | 329 | std::string full_message = hatkirby::implode(error_messages, " "); |
326 | TrackerLog(full_message); | 330 | wxLogError(wxString(full_message)); |
327 | 331 | ||
328 | wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); | 332 | wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR); |
329 | }); | 333 | }); |
@@ -341,8 +345,7 @@ struct APState { | |||
341 | DestroyClient(); | 345 | DestroyClient(); |
342 | 346 | ||
343 | tracker_frame->SetStatusMessage("Disconnected from Archipelago."); | 347 | tracker_frame->SetStatusMessage("Disconnected from Archipelago."); |
344 | 348 | wxLogStatus("Timeout while connecting to Archipelago server."); | |
345 | TrackerLog("Timeout while connecting to Archipelago server."); | ||
346 | wxMessageBox("Timeout while connecting to Archipelago server.", | 349 | wxMessageBox("Timeout while connecting to Archipelago server.", |
347 | "Connection failed", wxOK | wxICON_ERROR); | 350 | "Connection failed", wxOK | wxICON_ERROR); |
348 | } | 351 | } |
@@ -353,7 +356,7 @@ struct APState { | |||
353 | } | 356 | } |
354 | 357 | ||
355 | if (connected) { | 358 | if (connected) { |
356 | RefreshTracker(); | 359 | RefreshTracker(false); |
357 | } else { | 360 | } else { |
358 | client_active = false; | 361 | client_active = false; |
359 | } | 362 | } |
@@ -362,12 +365,11 @@ struct APState { | |||
362 | void HandleDataStorage(const std::string& key, const nlohmann::json& value) { | 365 | void HandleDataStorage(const std::string& key, const nlohmann::json& value) { |
363 | if (value.is_boolean()) { | 366 | if (value.is_boolean()) { |
364 | data_storage[key] = value.get<bool>(); | 367 | data_storage[key] = value.get<bool>(); |
365 | TrackerLog("Data storage " + key + " retrieved as " + | 368 | wxLogVerbose("Data storage %s retrieved as %s", key, |
366 | (value.get<bool>() ? "true" : "false")); | 369 | (value.get<bool>() ? "true" : "false")); |
367 | } else if (value.is_number()) { | 370 | } else if (value.is_number()) { |
368 | data_storage[key] = value.get<int>(); | 371 | data_storage[key] = value.get<int>(); |
369 | TrackerLog("Data storage " + key + " retrieved as " + | 372 | wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>()); |
370 | std::to_string(value.get<int>())); | ||
371 | } else if (value.is_object()) { | 373 | } else if (value.is_object()) { |
372 | if (key.ends_with("PlayerPos")) { | 374 | if (key.ends_with("PlayerPos")) { |
373 | auto map_value = value.get<std::map<std::string, int>>(); | 375 | auto map_value = value.get<std::map<std::string, int>>(); |
@@ -376,7 +378,7 @@ struct APState { | |||
376 | data_storage[key] = value.get<std::map<std::string, int>>(); | 378 | data_storage[key] = value.get<std::map<std::string, int>>(); |
377 | } | 379 | } |
378 | 380 | ||
379 | TrackerLog("Data storage " + key + " retrieved as dictionary"); | 381 | wxLogVerbose("Data storage %s retrieved as dictionary", key); |
380 | } else if (value.is_null()) { | 382 | } else if (value.is_null()) { |
381 | if (key.ends_with("PlayerPos")) { | 383 | if (key.ends_with("PlayerPos")) { |
382 | player_pos = std::nullopt; | 384 | player_pos = std::nullopt; |
@@ -384,7 +386,19 @@ struct APState { | |||
384 | data_storage.erase(key); | 386 | data_storage.erase(key); |
385 | } | 387 | } |
386 | 388 | ||
387 | TrackerLog("Data storage " + key + " retrieved as null"); | 389 | wxLogVerbose("Data storage %s retrieved as null", key); |
390 | } else if (value.is_array()) { | ||
391 | auto list_value = value.get<std::vector<std::string>>(); | ||
392 | |||
393 | if (key.ends_with("Paintings")) { | ||
394 | data_storage[key] = | ||
395 | std::set<std::string>(list_value.begin(), list_value.end()); | ||
396 | } else { | ||
397 | data_storage[key] = list_value; | ||
398 | } | ||
399 | |||
400 | wxLogVerbose("Data storage %s retrieved as list: [%s]", key, | ||
401 | hatkirby::implode(list_value, ", ")); | ||
388 | } | 402 | } |
389 | } | 403 | } |
390 | 404 | ||
@@ -407,22 +421,46 @@ struct APState { | |||
407 | return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); | 421 | return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); |
408 | } | 422 | } |
409 | 423 | ||
410 | void RefreshTracker() { | 424 | const std::set<std::string>& GetCheckedPaintings() { |
411 | TrackerLog("Refreshing display..."); | 425 | std::string key = data_storage_prefix + "Paintings"; |
426 | if (!data_storage.count(key)) { | ||
427 | data_storage[key] = std::set<std::string>(); | ||
428 | } | ||
429 | |||
430 | return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); | ||
431 | } | ||
432 | |||
433 | bool IsPaintingChecked(const std::string& painting_id) { | ||
434 | const auto& checked_paintings = GetCheckedPaintings(); | ||
435 | |||
436 | return checked_paintings.count(painting_id) || | ||
437 | (painting_mapping.count(painting_id) && | ||
438 | checked_paintings.count(painting_mapping.at(painting_id))); | ||
439 | } | ||
440 | |||
441 | void RefreshTracker(bool reset) { | ||
442 | wxLogVerbose("Refreshing display..."); | ||
412 | 443 | ||
413 | RecalculateReachability(); | 444 | RecalculateReachability(); |
414 | tracker_frame->UpdateIndicators(); | 445 | |
446 | if (reset) { | ||
447 | tracker_frame->ResetIndicators(); | ||
448 | } else { | ||
449 | tracker_frame->UpdateIndicators(); | ||
450 | } | ||
415 | } | 451 | } |
416 | 452 | ||
417 | int64_t GetItemId(const std::string& item_name) { | 453 | int64_t GetItemId(const std::string& item_name) { |
418 | int64_t ap_id = apclient->get_item_id(item_name); | 454 | int64_t ap_id = apclient->get_item_id(item_name); |
419 | if (ap_id == APClient::INVALID_NAME_ID) { | 455 | if (ap_id == APClient::INVALID_NAME_ID) { |
420 | TrackerLog("Could not find AP item ID for " + item_name); | 456 | wxLogError("Could not find AP item ID for %s", item_name); |
421 | } | 457 | } |
422 | 458 | ||
423 | return ap_id; | 459 | return ap_id; |
424 | } | 460 | } |
425 | 461 | ||
462 | std::string GetItemName(int id) { return apclient->get_item_name(id); } | ||
463 | |||
426 | bool HasReachedGoal() { | 464 | bool HasReachedGoal() { |
427 | return data_storage.count(victory_data_storage_key) && | 465 | return data_storage.count(victory_data_storage_key) && |
428 | std::any_cast<int>(data_storage.at(victory_data_storage_key)) == | 466 | std::any_cast<int>(data_storage.at(victory_data_storage_key)) == |
@@ -461,16 +499,32 @@ bool AP_HasItem(int item_id, int quantity) { | |||
461 | return GetState().HasItem(item_id, quantity); | 499 | return GetState().HasItem(item_id, quantity); |
462 | } | 500 | } |
463 | 501 | ||
502 | std::string AP_GetItemName(int item_id) { | ||
503 | return GetState().GetItemName(item_id); | ||
504 | } | ||
505 | |||
464 | DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } | 506 | DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } |
465 | 507 | ||
466 | bool AP_IsColorShuffle() { return GetState().color_shuffle; } | 508 | bool AP_IsColorShuffle() { return GetState().color_shuffle; } |
467 | 509 | ||
468 | bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } | 510 | bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } |
469 | 511 | ||
470 | const std::map<std::string, std::string> AP_GetPaintingMapping() { | 512 | const std::map<std::string, std::string>& AP_GetPaintingMapping() { |
471 | return GetState().painting_mapping; | 513 | return GetState().painting_mapping; |
472 | } | 514 | } |
473 | 515 | ||
516 | bool AP_IsPaintingMappedTo(const std::string& painting_id) { | ||
517 | return GetState().painting_codomain.count(painting_id); | ||
518 | } | ||
519 | |||
520 | const std::set<std::string>& AP_GetCheckedPaintings() { | ||
521 | return GetState().GetCheckedPaintings(); | ||
522 | } | ||
523 | |||
524 | bool AP_IsPaintingChecked(const std::string& painting_id) { | ||
525 | return GetState().IsPaintingChecked(painting_id); | ||
526 | } | ||
527 | |||
474 | int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } | 528 | int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } |
475 | 529 | ||
476 | int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } | 530 | 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 be31b8f..5776c6c 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp | |||
@@ -1,5 +1,11 @@ | |||
1 | #include "game_data.h" | 1 | #include "game_data.h" |
2 | 2 | ||
3 | #include <wx/wxprec.h> | ||
4 | |||
5 | #ifndef WX_PRECOMP | ||
6 | #include <wx/wx.h> | ||
7 | #endif | ||
8 | |||
3 | #include <hkutil/string.h> | 9 | #include <hkutil/string.h> |
4 | #include <yaml-cpp/yaml.h> | 10 | #include <yaml-cpp/yaml.h> |
5 | 11 | ||
@@ -7,7 +13,6 @@ | |||
7 | #include <sstream> | 13 | #include <sstream> |
8 | 14 | ||
9 | #include "global.h" | 15 | #include "global.h" |
10 | #include "logger.h" | ||
11 | 16 | ||
12 | namespace { | 17 | namespace { |
13 | 18 | ||
@@ -31,9 +36,7 @@ LingoColor GetColorForString(const std::string &str) { | |||
31 | } else if (str == "purple") { | 36 | } else if (str == "purple") { |
32 | return LingoColor::kPurple; | 37 | return LingoColor::kPurple; |
33 | } else { | 38 | } else { |
34 | std::ostringstream errmsg; | 39 | wxLogError("Invalid color: %s", str); |
35 | errmsg << "Invalid color: " << str; | ||
36 | TrackerLog(errmsg.str()); | ||
37 | 40 | ||
38 | return LingoColor::kNone; | 41 | return LingoColor::kNone; |
39 | } | 42 | } |
@@ -44,11 +47,14 @@ struct GameData { | |||
44 | std::vector<Door> doors_; | 47 | std::vector<Door> doors_; |
45 | std::vector<Panel> panels_; | 48 | std::vector<Panel> panels_; |
46 | std::vector<MapArea> map_areas_; | 49 | std::vector<MapArea> map_areas_; |
50 | std::vector<SubwayItem> subway_items_; | ||
51 | std::vector<PaintingExit> paintings_; | ||
47 | 52 | ||
48 | std::map<std::string, int> room_by_id_; | 53 | std::map<std::string, int> room_by_id_; |
49 | std::map<std::string, int> door_by_id_; | 54 | std::map<std::string, int> door_by_id_; |
50 | std::map<std::string, int> panel_by_id_; | 55 | std::map<std::string, int> panel_by_id_; |
51 | std::map<std::string, int> area_by_id_; | 56 | std::map<std::string, int> area_by_id_; |
57 | std::map<std::string, int> painting_by_id_; | ||
52 | 58 | ||
53 | std::vector<int> door_definition_order_; | 59 | std::vector<int> door_definition_order_; |
54 | 60 | ||
@@ -61,6 +67,9 @@ struct GameData { | |||
61 | 67 | ||
62 | std::vector<int> sunwarp_doors_; | 68 | std::vector<int> sunwarp_doors_; |
63 | 69 | ||
70 | std::map<std::string, int> subway_item_by_painting_; | ||
71 | std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; | ||
72 | |||
64 | bool loaded_area_data_ = false; | 73 | bool loaded_area_data_ = false; |
65 | std::set<std::string> malconfigured_areas_; | 74 | std::set<std::string> malconfigured_areas_; |
66 | 75 | ||
@@ -79,9 +88,7 @@ struct GameData { | |||
79 | ap_id_by_color_[GetColorForString(input_name)] = | 88 | ap_id_by_color_[GetColorForString(input_name)] = |
80 | ids_config["special_items"][color_name].as<int>(); | 89 | ids_config["special_items"][color_name].as<int>(); |
81 | } else { | 90 | } else { |
82 | std::ostringstream errmsg; | 91 | wxLogError("Missing AP item ID for color %s", color_name); |
83 | errmsg << "Missing AP item ID for color " << color_name; | ||
84 | TrackerLog(errmsg.str()); | ||
85 | } | 92 | } |
86 | }; | 93 | }; |
87 | 94 | ||
@@ -156,8 +163,9 @@ struct GameData { | |||
156 | } | 163 | } |
157 | default: { | 164 | default: { |
158 | // This shouldn't happen. | 165 | // This shouldn't happen. |
159 | std::cout << "Error reading game data: " << entrance_it | 166 | std::ostringstream formatted; |
160 | << std::endl; | 167 | formatted << entrance_it; |
168 | wxLogError("Error reading game data: %s", formatted.str()); | ||
161 | break; | 169 | break; |
162 | } | 170 | } |
163 | } | 171 | } |
@@ -282,10 +290,8 @@ struct GameData { | |||
282 | [panels_[panel_id].name] | 290 | [panels_[panel_id].name] |
283 | .as<int>(); | 291 | .as<int>(); |
284 | } else { | 292 | } else { |
285 | std::ostringstream errmsg; | 293 | wxLogError("Missing AP location ID for panel %s - %s", |
286 | errmsg << "Missing AP location ID for panel " | 294 | rooms_[room_id].name, panels_[panel_id].name); |
287 | << rooms_[room_id].name << " - " << panels_[panel_id].name; | ||
288 | TrackerLog(errmsg.str()); | ||
289 | } | 295 | } |
290 | } | 296 | } |
291 | } | 297 | } |
@@ -348,10 +354,8 @@ struct GameData { | |||
348 | [doors_[door_id].name]["item"] | 354 | [doors_[door_id].name]["item"] |
349 | .as<int>(); | 355 | .as<int>(); |
350 | } else { | 356 | } else { |
351 | std::ostringstream errmsg; | 357 | wxLogError("Missing AP item ID for door %s - %s", |
352 | errmsg << "Missing AP item ID for door " << rooms_[room_id].name | 358 | rooms_[room_id].name, doors_[door_id].name); |
353 | << " - " << doors_[door_id].name; | ||
354 | TrackerLog(errmsg.str()); | ||
355 | } | 359 | } |
356 | } | 360 | } |
357 | 361 | ||
@@ -365,10 +369,8 @@ struct GameData { | |||
365 | ids_config["door_groups"][doors_[door_id].group_name] | 369 | ids_config["door_groups"][doors_[door_id].group_name] |
366 | .as<int>(); | 370 | .as<int>(); |
367 | } else { | 371 | } else { |
368 | std::ostringstream errmsg; | 372 | wxLogError("Missing AP item ID for door group %s", |
369 | errmsg << "Missing AP item ID for door group " | 373 | doors_[door_id].group_name); |
370 | << doors_[door_id].group_name; | ||
371 | TrackerLog(errmsg.str()); | ||
372 | } | 374 | } |
373 | } | 375 | } |
374 | 376 | ||
@@ -378,13 +380,11 @@ struct GameData { | |||
378 | } else if (!door_it.second["skip_location"] && | 380 | } else if (!door_it.second["skip_location"] && |
379 | !door_it.second["event"]) { | 381 | !door_it.second["event"]) { |
380 | if (has_external_panels) { | 382 | if (has_external_panels) { |
381 | std::ostringstream errmsg; | 383 | wxLogError( |
382 | errmsg | 384 | "%s - %s has panels from other rooms but does not have an " |
383 | << rooms_[room_id].name << " - " << doors_[door_id].name | 385 | "explicit location name and is not marked skip_location or " |
384 | << " has panels from other rooms but does not have an " | 386 | "event", |
385 | "explicit " | 387 | rooms_[room_id].name, doors_[door_id].name); |
386 | "location name and is not marked skip_location or event"; | ||
387 | TrackerLog(errmsg.str()); | ||
388 | } | 388 | } |
389 | 389 | ||
390 | doors_[door_id].location_name = | 390 | doors_[door_id].location_name = |
@@ -404,10 +404,8 @@ struct GameData { | |||
404 | [doors_[door_id].name]["location"] | 404 | [doors_[door_id].name]["location"] |
405 | .as<int>(); | 405 | .as<int>(); |
406 | } else { | 406 | } else { |
407 | std::ostringstream errmsg; | 407 | wxLogError("Missing AP location ID for door %s - %s", |
408 | errmsg << "Missing AP location ID for door " | 408 | rooms_[room_id].name, doors_[door_id].name); |
409 | << rooms_[room_id].name << " - " << doors_[door_id].name; | ||
410 | TrackerLog(errmsg.str()); | ||
411 | } | 409 | } |
412 | } | 410 | } |
413 | 411 | ||
@@ -428,12 +426,13 @@ struct GameData { | |||
428 | 426 | ||
429 | if (room_it.second["paintings"]) { | 427 | if (room_it.second["paintings"]) { |
430 | for (const auto &painting : room_it.second["paintings"]) { | 428 | for (const auto &painting : room_it.second["paintings"]) { |
431 | std::string painting_id = painting["id"].as<std::string>(); | 429 | std::string internal_id = painting["id"].as<std::string>(); |
432 | room_by_painting_[painting_id] = room_id; | ||
433 | 430 | ||
434 | if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { | 431 | if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && |
435 | PaintingExit painting_exit; | 432 | (!painting["disable"] || !painting["disable"].as<bool>())) { |
436 | painting_exit.id = painting_id; | 433 | int painting_id = AddOrGetPainting(internal_id); |
434 | PaintingExit &painting_exit = paintings_[painting_id]; | ||
435 | painting_exit.room = room_id; | ||
437 | 436 | ||
438 | if (painting["required_door"]) { | 437 | if (painting["required_door"]) { |
439 | std::string rd_room = rooms_[room_id].name; | 438 | std::string rd_room = rooms_[room_id].name; |
@@ -445,7 +444,7 @@ struct GameData { | |||
445 | rd_room, painting["required_door"]["door"].as<std::string>()); | 444 | rd_room, painting["required_door"]["door"].as<std::string>()); |
446 | } | 445 | } |
447 | 446 | ||
448 | rooms_[room_id].paintings.push_back(painting_exit); | 447 | rooms_[room_id].paintings.push_back(painting_exit.id); |
449 | } | 448 | } |
450 | } | 449 | } |
451 | } | 450 | } |
@@ -473,10 +472,8 @@ struct GameData { | |||
473 | progressive_item_id = | 472 | progressive_item_id = |
474 | ids_config["progression"][progressive_item_name].as<int>(); | 473 | ids_config["progression"][progressive_item_name].as<int>(); |
475 | } else { | 474 | } else { |
476 | std::ostringstream errmsg; | 475 | wxLogError("Missing AP item ID for progressive item %s", |
477 | errmsg << "Missing AP item ID for progressive item " | 476 | progressive_item_name); |
478 | << progressive_item_name; | ||
479 | TrackerLog(errmsg.str()); | ||
480 | } | 477 | } |
481 | 478 | ||
482 | int index = 1; | 479 | int index = 1; |
@@ -618,11 +615,98 @@ struct GameData { | |||
618 | } | 615 | } |
619 | } | 616 | } |
620 | 617 | ||
618 | for (const Room &room : rooms_) { | ||
619 | std::string area_name = room.name; | ||
620 | if (fold_areas.count(room.name)) { | ||
621 | int fold_area_id = fold_areas[room.name]; | ||
622 | area_name = map_areas_[fold_area_id].name; | ||
623 | } | ||
624 | |||
625 | if (!room.paintings.empty()) { | ||
626 | int area_id = AddOrGetArea(area_name); | ||
627 | MapArea &map_area = map_areas_[area_id]; | ||
628 | |||
629 | for (int painting_id : room.paintings) { | ||
630 | map_area.paintings.push_back(painting_id); | ||
631 | } | ||
632 | } | ||
633 | } | ||
634 | |||
621 | // Report errors. | 635 | // Report errors. |
622 | for (const std::string &area : malconfigured_areas_) { | 636 | for (const std::string &area : malconfigured_areas_) { |
623 | std::ostringstream errstr; | 637 | wxLogError("Area data not found for: %s", area); |
624 | errstr << "Area data not found for: " << area; | 638 | } |
625 | TrackerLog(errstr.str()); | 639 | |
640 | // Read in subway items. | ||
641 | YAML::Node subway_config = | ||
642 | YAML::LoadFile(GetAbsolutePath("assets/subway.yaml")); | ||
643 | for (const auto &subway_it : subway_config) { | ||
644 | SubwayItem subway_item; | ||
645 | subway_item.id = subway_items_.size(); | ||
646 | subway_item.x = subway_it["pos"][0].as<int>(); | ||
647 | subway_item.y = subway_it["pos"][1].as<int>(); | ||
648 | |||
649 | if (subway_it["door"]) { | ||
650 | subway_item.door = AddOrGetDoor(subway_it["room"].as<std::string>(), | ||
651 | subway_it["door"].as<std::string>()); | ||
652 | } | ||
653 | |||
654 | if (subway_it["paintings"]) { | ||
655 | for (const auto &painting_it : subway_it["paintings"]) { | ||
656 | std::string painting_id = painting_it.as<std::string>(); | ||
657 | |||
658 | subway_item.paintings.push_back(painting_id); | ||
659 | subway_item_by_painting_[painting_id] = subway_item.id; | ||
660 | } | ||
661 | } | ||
662 | |||
663 | if (subway_it["tags"]) { | ||
664 | for (const auto &tag_it : subway_it["tags"]) { | ||
665 | subway_item.tags.push_back(tag_it.as<std::string>()); | ||
666 | } | ||
667 | } | ||
668 | |||
669 | if (subway_it["sunwarp"]) { | ||
670 | SubwaySunwarp sunwarp; | ||
671 | sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); | ||
672 | |||
673 | std::string sunwarp_type = | ||
674 | subway_it["sunwarp"]["type"].as<std::string>(); | ||
675 | if (sunwarp_type == "final") { | ||
676 | sunwarp.type = SubwaySunwarpType::kFinal; | ||
677 | } else if (sunwarp_type == "exit") { | ||
678 | sunwarp.type = SubwaySunwarpType::kExit; | ||
679 | } else { | ||
680 | sunwarp.type = SubwaySunwarpType::kEnter; | ||
681 | } | ||
682 | |||
683 | subway_item.sunwarp = sunwarp; | ||
684 | |||
685 | subway_item_by_sunwarp_[sunwarp] = subway_item.id; | ||
686 | |||
687 | subway_item.door = | ||
688 | AddOrGetDoor("Sunwarps", std::to_string(sunwarp.dots) + " Sunwarp"); | ||
689 | } | ||
690 | |||
691 | if (subway_it["special"]) { | ||
692 | subway_item.special = subway_it["special"].as<std::string>(); | ||
693 | } | ||
694 | |||
695 | subway_items_.push_back(subway_item); | ||
696 | } | ||
697 | |||
698 | // Find singleton subway tags. | ||
699 | std::map<std::string, std::set<int>> subway_tags; | ||
700 | for (const SubwayItem &subway_item : subway_items_) { | ||
701 | for (const std::string &tag : subway_item.tags) { | ||
702 | subway_tags[tag].insert(subway_item.id); | ||
703 | } | ||
704 | } | ||
705 | |||
706 | for (const auto &[tag, items] : subway_tags) { | ||
707 | if (items.size() == 1) { | ||
708 | wxLogWarning("Singleton subway item tag: %s", tag); | ||
709 | } | ||
626 | } | 710 | } |
627 | } | 711 | } |
628 | 712 | ||
@@ -639,8 +723,10 @@ struct GameData { | |||
639 | std::string full_name = room + " - " + door; | 723 | std::string full_name = room + " - " + door; |
640 | 724 | ||
641 | if (!door_by_id_.count(full_name)) { | 725 | if (!door_by_id_.count(full_name)) { |
726 | int door_id = doors_.size(); | ||
642 | door_by_id_[full_name] = doors_.size(); | 727 | door_by_id_[full_name] = doors_.size(); |
643 | doors_.push_back({.room = AddOrGetRoom(room), .name = door}); | 728 | doors_.push_back( |
729 | {.id = door_id, .room = AddOrGetRoom(room), .name = door}); | ||
644 | } | 730 | } |
645 | 731 | ||
646 | return door_by_id_[full_name]; | 732 | return door_by_id_[full_name]; |
@@ -672,6 +758,16 @@ struct GameData { | |||
672 | 758 | ||
673 | return area_by_id_[area]; | 759 | return area_by_id_[area]; |
674 | } | 760 | } |
761 | |||
762 | int AddOrGetPainting(std::string internal_id) { | ||
763 | if (!painting_by_id_.count(internal_id)) { | ||
764 | int painting_id = paintings_.size(); | ||
765 | painting_by_id_[internal_id] = painting_id; | ||
766 | paintings_.push_back({.id = painting_id, .internal_id = internal_id}); | ||
767 | } | ||
768 | |||
769 | return painting_by_id_[internal_id]; | ||
770 | } | ||
675 | }; | 771 | }; |
676 | 772 | ||
677 | GameData &GetState() { | 773 | GameData &GetState() { |
@@ -681,6 +777,10 @@ GameData &GetState() { | |||
681 | 777 | ||
682 | } // namespace | 778 | } // namespace |
683 | 779 | ||
780 | bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const { | ||
781 | return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); | ||
782 | } | ||
783 | |||
684 | const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; } | 784 | const std::vector<MapArea> &GD_GetMapAreas() { return GetState().map_areas_; } |
685 | 785 | ||
686 | const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); } | 786 | const MapArea &GD_GetMapArea(int id) { return GetState().map_areas_.at(id); } |
@@ -703,8 +803,12 @@ const Panel &GD_GetPanel(int panel_id) { | |||
703 | return GetState().panels_.at(panel_id); | 803 | return GetState().panels_.at(panel_id); |
704 | } | 804 | } |
705 | 805 | ||
706 | int GD_GetRoomForPainting(const std::string &painting_id) { | 806 | const PaintingExit &GD_GetPaintingExit(int painting_id) { |
707 | return GetState().room_by_painting_.at(painting_id); | 807 | return GetState().paintings_.at(painting_id); |
808 | } | ||
809 | |||
810 | int GD_GetPaintingByName(const std::string &name) { | ||
811 | return GetState().painting_by_id_.at(name); | ||
708 | } | 812 | } |
709 | 813 | ||
710 | const std::vector<int> &GD_GetAchievementPanels() { | 814 | const std::vector<int> &GD_GetAchievementPanels() { |
@@ -722,3 +826,24 @@ const std::vector<int> &GD_GetSunwarpDoors() { | |||
722 | int GD_GetRoomForSunwarp(int index) { | 826 | int GD_GetRoomForSunwarp(int index) { |
723 | return GetState().room_by_sunwarp_.at(index); | 827 | return GetState().room_by_sunwarp_.at(index); |
724 | } | 828 | } |
829 | |||
830 | const std::vector<SubwayItem> &GD_GetSubwayItems() { | ||
831 | return GetState().subway_items_; | ||
832 | } | ||
833 | |||
834 | const SubwayItem &GD_GetSubwayItem(int id) { | ||
835 | return GetState().subway_items_.at(id); | ||
836 | } | ||
837 | |||
838 | int GD_GetSubwayItemForPainting(const std::string &painting_id) { | ||
839 | #ifndef NDEBUG | ||
840 | if (!GetState().subway_item_by_painting_.count(painting_id)) { | ||
841 | wxLogError("No subway item for painting %s", painting_id); | ||
842 | } | ||
843 | #endif | ||
844 | return GetState().subway_item_by_painting_.at(painting_id); | ||
845 | } | ||
846 | |||
847 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { | ||
848 | return GetState().subway_item_by_sunwarp_.at(sunwarp); | ||
849 | } | ||
diff --git a/src/game_data.h b/src/game_data.h index f3edaa2..a5d5699 100644 --- a/src/game_data.h +++ b/src/game_data.h | |||
@@ -63,6 +63,7 @@ struct ProgressiveRequirement { | |||
63 | }; | 63 | }; |
64 | 64 | ||
65 | struct Door { | 65 | struct Door { |
66 | int id; | ||
66 | int room; | 67 | int room; |
67 | std::string name; | 68 | std::string name; |
68 | std::string location_name; | 69 | std::string location_name; |
@@ -87,14 +88,16 @@ struct Exit { | |||
87 | }; | 88 | }; |
88 | 89 | ||
89 | struct PaintingExit { | 90 | struct PaintingExit { |
90 | std::string id; | 91 | int id; |
92 | int room; | ||
93 | std::string internal_id; | ||
91 | std::optional<int> door; | 94 | std::optional<int> door; |
92 | }; | 95 | }; |
93 | 96 | ||
94 | struct Room { | 97 | struct Room { |
95 | std::string name; | 98 | std::string name; |
96 | std::vector<Exit> exits; | 99 | std::vector<Exit> exits; |
97 | std::vector<PaintingExit> paintings; | 100 | std::vector<int> paintings; |
98 | std::vector<int> sunwarps; | 101 | std::vector<int> sunwarps; |
99 | std::vector<int> panels; | 102 | std::vector<int> panels; |
100 | }; | 103 | }; |
@@ -113,12 +116,37 @@ struct MapArea { | |||
113 | int id; | 116 | int id; |
114 | std::string name; | 117 | std::string name; |
115 | std::vector<Location> locations; | 118 | std::vector<Location> locations; |
119 | std::vector<int> paintings; | ||
116 | int map_x; | 120 | int map_x; |
117 | int map_y; | 121 | int map_y; |
118 | int classification = 0; | 122 | int classification = 0; |
119 | bool hunt = false; | 123 | bool hunt = false; |
120 | }; | 124 | }; |
121 | 125 | ||
126 | enum class SubwaySunwarpType { | ||
127 | kEnter, | ||
128 | kExit, | ||
129 | kFinal | ||
130 | }; | ||
131 | |||
132 | struct SubwaySunwarp { | ||
133 | int dots; | ||
134 | SubwaySunwarpType type; | ||
135 | |||
136 | bool operator<(const SubwaySunwarp& rhs) const; | ||
137 | }; | ||
138 | |||
139 | struct SubwayItem { | ||
140 | int id; | ||
141 | int x; | ||
142 | int y; | ||
143 | std::optional<int> door; | ||
144 | std::vector<std::string> paintings; | ||
145 | std::vector<std::string> tags; | ||
146 | std::optional<SubwaySunwarp> sunwarp; | ||
147 | std::optional<std::string> special; | ||
148 | }; | ||
149 | |||
122 | const std::vector<MapArea>& GD_GetMapAreas(); | 150 | const std::vector<MapArea>& GD_GetMapAreas(); |
123 | const MapArea& GD_GetMapArea(int id); | 151 | const MapArea& GD_GetMapArea(int id); |
124 | int GD_GetRoomByName(const std::string& name); | 152 | int GD_GetRoomByName(const std::string& name); |
@@ -127,10 +155,15 @@ const std::vector<Door>& GD_GetDoors(); | |||
127 | const Door& GD_GetDoor(int door_id); | 155 | const Door& GD_GetDoor(int door_id); |
128 | int GD_GetDoorByName(const std::string& name); | 156 | int GD_GetDoorByName(const std::string& name); |
129 | const Panel& GD_GetPanel(int panel_id); | 157 | const Panel& GD_GetPanel(int panel_id); |
130 | int GD_GetRoomForPainting(const std::string& painting_id); | 158 | const PaintingExit& GD_GetPaintingExit(int painting_id); |
159 | int GD_GetPaintingByName(const std::string& name); | ||
131 | const std::vector<int>& GD_GetAchievementPanels(); | 160 | const std::vector<int>& GD_GetAchievementPanels(); |
132 | int GD_GetItemIdForColor(LingoColor color); | 161 | int GD_GetItemIdForColor(LingoColor color); |
133 | const std::vector<int>& GD_GetSunwarpDoors(); | 162 | const std::vector<int>& GD_GetSunwarpDoors(); |
134 | int GD_GetRoomForSunwarp(int index); | 163 | int GD_GetRoomForSunwarp(int index); |
164 | const std::vector<SubwayItem>& GD_GetSubwayItems(); | ||
165 | const SubwayItem& GD_GetSubwayItem(int id); | ||
166 | int GD_GetSubwayItemForPainting(const std::string& painting_id); | ||
167 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); | ||
135 | 168 | ||
136 | #endif /* end of include guard: GAME_DATA_H_9C42AC51 */ | 169 | #endif /* end of include guard: GAME_DATA_H_9C42AC51 */ |
diff --git a/src/logger.cpp b/src/logger.cpp deleted file mode 100644 index 4b722c8..0000000 --- a/src/logger.cpp +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | #include "logger.h" | ||
2 | |||
3 | #include <chrono> | ||
4 | #include <fstream> | ||
5 | #include <mutex> | ||
6 | |||
7 | #include "global.h" | ||
8 | |||
9 | namespace { | ||
10 | |||
11 | class Logger { | ||
12 | public: | ||
13 | Logger() : logfile_(GetAbsolutePath("debug.log")) {} | ||
14 | |||
15 | void LogLine(const std::string& text) { | ||
16 | std::lock_guard guard(file_mutex_); | ||
17 | logfile_ << "[" << std::chrono::system_clock::now() << "] " << text | ||
18 | << std::endl; | ||
19 | logfile_.flush(); | ||
20 | } | ||
21 | |||
22 | private: | ||
23 | std::ofstream logfile_; | ||
24 | std::mutex file_mutex_; | ||
25 | }; | ||
26 | |||
27 | } // namespace | ||
28 | |||
29 | void TrackerLog(const std::string& text) { | ||
30 | static Logger* instance = new Logger(); | ||
31 | instance->LogLine(text); | ||
32 | } | ||
diff --git a/src/logger.h b/src/logger.h deleted file mode 100644 index db9bb49..0000000 --- a/src/logger.h +++ /dev/null | |||
@@ -1,8 +0,0 @@ | |||
1 | #ifndef LOGGER_H_6E7B9594 | ||
2 | #define LOGGER_H_6E7B9594 | ||
3 | |||
4 | #include <string> | ||
5 | |||
6 | void TrackerLog(const std::string& text); | ||
7 | |||
8 | #endif /* end of include guard: LOGGER_H_6E7B9594 */ | ||
diff --git a/src/main.cpp b/src/main.cpp index fe9aceb..abe6626 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
@@ -4,18 +4,41 @@ | |||
4 | #include <wx/wx.h> | 4 | #include <wx/wx.h> |
5 | #endif | 5 | #endif |
6 | 6 | ||
7 | #include <fstream> | ||
8 | |||
9 | #include "global.h" | ||
7 | #include "tracker_config.h" | 10 | #include "tracker_config.h" |
8 | #include "tracker_frame.h" | 11 | #include "tracker_frame.h" |
9 | 12 | ||
13 | static std::ofstream* logfile; | ||
14 | |||
10 | class TrackerApp : public wxApp { | 15 | class TrackerApp : public wxApp { |
11 | public: | 16 | public: |
12 | virtual bool OnInit() { | 17 | virtual bool OnInit() { |
18 | logfile = new std::ofstream(GetAbsolutePath("debug.log")); | ||
19 | wxLog::SetActiveTarget(new wxLogStream(logfile)); | ||
20 | |||
21 | #ifndef NDEBUG | ||
22 | wxLog::SetVerbose(true); | ||
23 | wxLog::SetActiveTarget(new wxLogWindow(nullptr, "Debug Log")); | ||
24 | #endif | ||
25 | |||
13 | GetTrackerConfig().Load(); | 26 | GetTrackerConfig().Load(); |
14 | 27 | ||
15 | TrackerFrame *frame = new TrackerFrame(); | 28 | TrackerFrame *frame = new TrackerFrame(); |
16 | frame->Show(true); | 29 | frame->Show(true); |
17 | return true; | 30 | return true; |
18 | } | 31 | } |
32 | |||
33 | bool OnExceptionInMainLoop() override { | ||
34 | try { | ||
35 | throw; | ||
36 | } catch (const std::exception& ex) { | ||
37 | wxLogError(ex.what()); | ||
38 | } | ||
39 | |||
40 | return false; | ||
41 | } | ||
19 | }; | 42 | }; |
20 | 43 | ||
21 | wxIMPLEMENT_APP(TrackerApp); | 44 | 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..8364714 --- /dev/null +++ b/src/subway_map.cpp | |||
@@ -0,0 +1,700 @@ | |||
1 | #include "subway_map.h" | ||
2 | |||
3 | #include <wx/dcbuffer.h> | ||
4 | #include <wx/dcgraph.h> | ||
5 | |||
6 | #include <sstream> | ||
7 | |||
8 | #include "ap_state.h" | ||
9 | #include "game_data.h" | ||
10 | #include "global.h" | ||
11 | #include "tracker_state.h" | ||
12 | |||
13 | constexpr int AREA_ACTUAL_SIZE = 21; | ||
14 | constexpr int OWL_ACTUAL_SIZE = 32; | ||
15 | |||
16 | enum class ItemDrawType { kNone, kBox, kOwl }; | ||
17 | |||
18 | SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | ||
19 | SetBackgroundStyle(wxBG_STYLE_PAINT); | ||
20 | |||
21 | map_image_ = | ||
22 | wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG); | ||
23 | if (!map_image_.IsOk()) { | ||
24 | return; | ||
25 | } | ||
26 | |||
27 | owl_image_ = | ||
28 | wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG); | ||
29 | if (!owl_image_.IsOk()) { | ||
30 | return; | ||
31 | } | ||
32 | |||
33 | unchecked_eye_ = | ||
34 | wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(), | ||
35 | wxBITMAP_TYPE_PNG) | ||
36 | .Scale(32, 32)); | ||
37 | checked_eye_ = wxBitmap( | ||
38 | wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG) | ||
39 | .Scale(32, 32)); | ||
40 | |||
41 | tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( | ||
42 | quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), | ||
43 | static_cast<float>(map_image_.GetHeight())}); | ||
44 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | ||
45 | tree_->add(subway_item.id); | ||
46 | } | ||
47 | |||
48 | Redraw(); | ||
49 | |||
50 | scroll_timer_ = new wxTimer(this); | ||
51 | |||
52 | Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); | ||
53 | Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); | ||
54 | Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this); | ||
55 | Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this); | ||
56 | Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); | ||
57 | Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); | ||
58 | |||
59 | zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15}); | ||
60 | zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); | ||
61 | |||
62 | help_button_ = new wxButton(this, wxID_ANY, "Help"); | ||
63 | help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); | ||
64 | SetUpHelpButton(); | ||
65 | } | ||
66 | |||
67 | void SubwayMap::OnConnect() { | ||
68 | networks_.Clear(); | ||
69 | |||
70 | std::map<std::string, std::vector<int>> tagged; | ||
71 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | ||
72 | if (AP_HasEarlyColorHallways() && | ||
73 | (subway_item.special == "starting_room_paintings" || | ||
74 | subway_item.special == "early_color_hallways")) { | ||
75 | tagged["early_color_hallways"].push_back(subway_item.id); | ||
76 | } | ||
77 | |||
78 | if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { | ||
79 | continue; | ||
80 | } | ||
81 | |||
82 | for (const std::string &tag : subway_item.tags) { | ||
83 | tagged[tag].push_back(subway_item.id); | ||
84 | } | ||
85 | |||
86 | if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && | ||
87 | subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { | ||
88 | std::ostringstream tag; | ||
89 | tag << "sunwarp" << subway_item.sunwarp->dots; | ||
90 | |||
91 | tagged[tag.str()].push_back(subway_item.id); | ||
92 | } | ||
93 | |||
94 | if (!AP_IsPilgrimageEnabled() && | ||
95 | (subway_item.special == "sun_painting" || | ||
96 | subway_item.special == "sun_painting_exit")) { | ||
97 | tagged["sun_painting"].push_back(subway_item.id); | ||
98 | } | ||
99 | } | ||
100 | |||
101 | for (const auto &[tag, items] : tagged) { | ||
102 | // Pairwise connect all items with the same tag. | ||
103 | for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end(); | ||
104 | tag_it1++) { | ||
105 | for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); | ||
106 | tag_it2++) { | ||
107 | networks_.AddLink(*tag_it1, *tag_it2); | ||
108 | } | ||
109 | } | ||
110 | } | ||
111 | |||
112 | checked_paintings_.clear(); | ||
113 | } | ||
114 | |||
115 | void SubwayMap::UpdateIndicators() { | ||
116 | if (AP_IsPaintingShuffle()) { | ||
117 | for (const std::string &painting_id : AP_GetCheckedPaintings()) { | ||
118 | if (!checked_paintings_.count(painting_id)) { | ||
119 | checked_paintings_.insert(painting_id); | ||
120 | |||
121 | if (AP_GetPaintingMapping().count(painting_id)) { | ||
122 | networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), | ||
123 | GD_GetSubwayItemForPainting( | ||
124 | AP_GetPaintingMapping().at(painting_id))); | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | } | ||
129 | |||
130 | Redraw(); | ||
131 | } | ||
132 | |||
133 | void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, | ||
134 | SubwaySunwarp to_sunwarp) { | ||
135 | networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), | ||
136 | GD_GetSubwayItemForSunwarp(to_sunwarp)); | ||
137 | } | ||
138 | |||
139 | void SubwayMap::Zoom(bool in) { | ||
140 | wxPoint focus_point; | ||
141 | |||
142 | if (mouse_position_) { | ||
143 | focus_point = *mouse_position_; | ||
144 | } else { | ||
145 | focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2}; | ||
146 | } | ||
147 | |||
148 | if (in) { | ||
149 | if (zoom_ < 3.0) { | ||
150 | SetZoom(zoom_ + 0.25, focus_point); | ||
151 | } | ||
152 | } else { | ||
153 | if (zoom_ > 1.0) { | ||
154 | SetZoom(zoom_ - 0.25, focus_point); | ||
155 | } | ||
156 | } | ||
157 | } | ||
158 | |||
159 | void SubwayMap::OnPaint(wxPaintEvent &event) { | ||
160 | if (GetSize() != rendered_.GetSize()) { | ||
161 | wxSize panel_size = GetSize(); | ||
162 | wxSize image_size = map_image_.GetSize(); | ||
163 | |||
164 | render_x_ = 0; | ||
165 | render_y_ = 0; | ||
166 | render_width_ = panel_size.GetWidth(); | ||
167 | render_height_ = panel_size.GetHeight(); | ||
168 | |||
169 | if (image_size.GetWidth() * panel_size.GetHeight() > | ||
170 | panel_size.GetWidth() * image_size.GetHeight()) { | ||
171 | render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) / | ||
172 | image_size.GetWidth(); | ||
173 | render_y_ = (panel_size.GetHeight() - render_height_) / 2; | ||
174 | } else { | ||
175 | render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / | ||
176 | image_size.GetHeight(); | ||
177 | render_x_ = (panel_size.GetWidth() - render_width_) / 2; | ||
178 | } | ||
179 | |||
180 | SetZoomPos({zoom_x_, zoom_y_}); | ||
181 | |||
182 | SetUpHelpButton(); | ||
183 | } | ||
184 | |||
185 | wxBufferedPaintDC dc(this); | ||
186 | dc.SetBackground(*wxWHITE_BRUSH); | ||
187 | dc.Clear(); | ||
188 | |||
189 | { | ||
190 | wxMemoryDC rendered_dc; | ||
191 | rendered_dc.SelectObject(rendered_); | ||
192 | |||
193 | int dst_x; | ||
194 | int dst_y; | ||
195 | int dst_w; | ||
196 | int dst_h; | ||
197 | int src_x; | ||
198 | int src_y; | ||
199 | int src_w; | ||
200 | int src_h; | ||
201 | |||
202 | int zoomed_width = render_width_ * zoom_; | ||
203 | int zoomed_height = render_height_ * zoom_; | ||
204 | |||
205 | if (zoomed_width <= GetSize().GetWidth()) { | ||
206 | dst_x = (GetSize().GetWidth() - zoomed_width) / 2; | ||
207 | dst_w = zoomed_width; | ||
208 | src_x = 0; | ||
209 | src_w = map_image_.GetWidth(); | ||
210 | } else { | ||
211 | dst_x = 0; | ||
212 | dst_w = GetSize().GetWidth(); | ||
213 | src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_; | ||
214 | src_w = | ||
215 | GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_; | ||
216 | } | ||
217 | |||
218 | if (zoomed_height <= GetSize().GetHeight()) { | ||
219 | dst_y = (GetSize().GetHeight() - zoomed_height) / 2; | ||
220 | dst_h = zoomed_height; | ||
221 | src_y = 0; | ||
222 | src_h = map_image_.GetHeight(); | ||
223 | } else { | ||
224 | dst_y = 0; | ||
225 | dst_h = GetSize().GetHeight(); | ||
226 | src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_; | ||
227 | src_h = | ||
228 | GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_; | ||
229 | } | ||
230 | |||
231 | wxGCDC gcdc(dc); | ||
232 | gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD); | ||
233 | gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y, | ||
234 | src_w, src_h); | ||
235 | } | ||
236 | |||
237 | if (hovered_item_) { | ||
238 | // Note that these requirements are duplicated on OnMouseClick so that it | ||
239 | // knows when an item has a hover effect. | ||
240 | const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_); | ||
241 | if (subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) { | ||
242 | const std::map<std::string, bool> &report = | ||
243 | GetDoorRequirements(*subway_item.door); | ||
244 | |||
245 | int acc_height = 10; | ||
246 | int col_width = 0; | ||
247 | |||
248 | for (const auto &[text, obtained] : report) { | ||
249 | wxSize item_extent = dc.GetTextExtent(text); | ||
250 | int item_height = std::max(32, item_extent.GetHeight()) + 10; | ||
251 | acc_height += item_height; | ||
252 | |||
253 | if (item_extent.GetWidth() > col_width) { | ||
254 | col_width = item_extent.GetWidth(); | ||
255 | } | ||
256 | } | ||
257 | |||
258 | int item_width = col_width + 10 + 32; | ||
259 | int full_width = item_width + 20; | ||
260 | |||
261 | wxPoint popup_pos = | ||
262 | MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2, | ||
263 | subway_item.y + AREA_ACTUAL_SIZE / 2}); | ||
264 | |||
265 | if (popup_pos.x + full_width > GetSize().GetWidth()) { | ||
266 | popup_pos.x = GetSize().GetWidth() - full_width; | ||
267 | } | ||
268 | if (popup_pos.y + acc_height > GetSize().GetHeight()) { | ||
269 | popup_pos.y = GetSize().GetHeight() - acc_height; | ||
270 | } | ||
271 | |||
272 | dc.SetPen(*wxTRANSPARENT_PEN); | ||
273 | dc.SetBrush(*wxBLACK_BRUSH); | ||
274 | dc.DrawRectangle(popup_pos, {full_width, acc_height}); | ||
275 | |||
276 | dc.SetFont(GetFont()); | ||
277 | |||
278 | int cur_height = 10; | ||
279 | |||
280 | for (const auto &[text, obtained] : report) { | ||
281 | wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_; | ||
282 | |||
283 | dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height}); | ||
284 | |||
285 | dc.SetTextForeground(obtained ? *wxWHITE : *wxRED); | ||
286 | wxSize item_extent = dc.GetTextExtent(text); | ||
287 | dc.DrawText( | ||
288 | text, | ||
289 | popup_pos + | ||
290 | wxPoint{10 + 32 + 10, | ||
291 | cur_height + (32 - dc.GetFontMetrics().height) / 2}); | ||
292 | |||
293 | cur_height += 10 + 32; | ||
294 | } | ||
295 | } | ||
296 | |||
297 | if (networks_.IsItemInNetwork(*hovered_item_)) { | ||
298 | dc.SetBrush(*wxTRANSPARENT_BRUSH); | ||
299 | |||
300 | for (const auto &[item_id1, item_id2] : | ||
301 | networks_.GetNetworkGraph(*hovered_item_)) { | ||
302 | const SubwayItem &item1 = GD_GetSubwayItem(item_id1); | ||
303 | const SubwayItem &item2 = GD_GetSubwayItem(item_id2); | ||
304 | |||
305 | wxPoint item1_pos = MapPosToRenderPos( | ||
306 | {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); | ||
307 | wxPoint item2_pos = MapPosToRenderPos( | ||
308 | {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2}); | ||
309 | |||
310 | int left = std::min(item1_pos.x, item2_pos.x); | ||
311 | int top = std::min(item1_pos.y, item2_pos.y); | ||
312 | int right = std::max(item1_pos.x, item2_pos.x); | ||
313 | int bottom = std::max(item1_pos.y, item2_pos.y); | ||
314 | |||
315 | int halfwidth = right - left; | ||
316 | int halfheight = bottom - top; | ||
317 | |||
318 | if (halfwidth < 4 || halfheight < 4) { | ||
319 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | ||
320 | dc.DrawLine(item1_pos, item2_pos); | ||
321 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
322 | dc.DrawLine(item1_pos, item2_pos); | ||
323 | } else { | ||
324 | int ellipse_x; | ||
325 | int ellipse_y; | ||
326 | double start; | ||
327 | double end; | ||
328 | |||
329 | if (item1_pos.x > item2_pos.x) { | ||
330 | ellipse_y = top; | ||
331 | |||
332 | if (item1_pos.y > item2_pos.y) { | ||
333 | ellipse_x = left - halfwidth; | ||
334 | |||
335 | start = 0; | ||
336 | end = 90; | ||
337 | } else { | ||
338 | ellipse_x = left; | ||
339 | |||
340 | start = 90; | ||
341 | end = 180; | ||
342 | } | ||
343 | } else { | ||
344 | ellipse_y = top - halfheight; | ||
345 | |||
346 | if (item1_pos.y > item2_pos.y) { | ||
347 | ellipse_x = left - halfwidth; | ||
348 | |||
349 | start = 270; | ||
350 | end = 360; | ||
351 | } else { | ||
352 | ellipse_x = left; | ||
353 | |||
354 | start = 180; | ||
355 | end = 270; | ||
356 | } | ||
357 | } | ||
358 | |||
359 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); | ||
360 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, | ||
361 | halfheight * 2, start, end); | ||
362 | dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); | ||
363 | dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, | ||
364 | halfheight * 2, start, end); | ||
365 | } | ||
366 | } | ||
367 | } | ||
368 | } | ||
369 | |||
370 | event.Skip(); | ||
371 | } | ||
372 | |||
373 | void SubwayMap::OnMouseMove(wxMouseEvent &event) { | ||
374 | wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition()); | ||
375 | |||
376 | std::vector<int> hovered = tree_->query( | ||
377 | {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2}); | ||
378 | if (!hovered.empty()) { | ||
379 | actual_hover_= hovered[0]; | ||
380 | } else { | ||
381 | actual_hover_ = std::nullopt; | ||
382 | } | ||
383 | |||
384 | if (!sticky_hover_ && actual_hover_ != hovered_item_) { | ||
385 | hovered_item_ = actual_hover_; | ||
386 | |||
387 | Refresh(); | ||
388 | } | ||
389 | |||
390 | if (scroll_mode_) { | ||
391 | EvaluateScroll(event.GetPosition()); | ||
392 | } | ||
393 | |||
394 | mouse_position_ = event.GetPosition(); | ||
395 | |||
396 | event.Skip(); | ||
397 | } | ||
398 | |||
399 | void SubwayMap::OnMouseScroll(wxMouseEvent &event) { | ||
400 | double new_zoom = zoom_; | ||
401 | if (event.GetWheelRotation() > 0) { | ||
402 | new_zoom = std::min(3.0, zoom_ + 0.25); | ||
403 | } else { | ||
404 | new_zoom = std::max(1.0, zoom_ - 0.25); | ||
405 | } | ||
406 | |||
407 | if (zoom_ != new_zoom) { | ||
408 | SetZoom(new_zoom, event.GetPosition()); | ||
409 | } | ||
410 | |||
411 | event.Skip(); | ||
412 | } | ||
413 | |||
414 | void SubwayMap::OnMouseLeave(wxMouseEvent &event) { | ||
415 | SetScrollSpeed(0, 0); | ||
416 | mouse_position_ = std::nullopt; | ||
417 | } | ||
418 | |||
419 | void SubwayMap::OnMouseClick(wxMouseEvent &event) { | ||
420 | bool finished = false; | ||
421 | |||
422 | if (actual_hover_) { | ||
423 | const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_); | ||
424 | if ((subway_item.door && !GetDoorRequirements(*subway_item.door).empty()) || | ||
425 | networks_.IsItemInNetwork(*hovered_item_)) { | ||
426 | if (actual_hover_ != hovered_item_) { | ||
427 | hovered_item_ = actual_hover_; | ||
428 | |||
429 | if (!hovered_item_) { | ||
430 | sticky_hover_ = false; | ||
431 | } | ||
432 | |||
433 | Refresh(); | ||
434 | } else { | ||
435 | sticky_hover_ = !sticky_hover_; | ||
436 | } | ||
437 | |||
438 | finished = true; | ||
439 | } | ||
440 | } | ||
441 | |||
442 | if (!finished) { | ||
443 | if (scroll_mode_) { | ||
444 | scroll_mode_ = false; | ||
445 | |||
446 | SetScrollSpeed(0, 0); | ||
447 | |||
448 | SetCursor(wxCURSOR_ARROW); | ||
449 | } else if (event.GetPosition().x < GetSize().GetWidth() / 6 || | ||
450 | event.GetPosition().x > 5 * GetSize().GetWidth() / 6 || | ||
451 | event.GetPosition().y < GetSize().GetHeight() / 6 || | ||
452 | event.GetPosition().y > 5 * GetSize().GetHeight() / 6) { | ||
453 | scroll_mode_ = true; | ||
454 | |||
455 | EvaluateScroll(event.GetPosition()); | ||
456 | |||
457 | SetCursor(wxCURSOR_CROSS); | ||
458 | } else { | ||
459 | sticky_hover_ = false; | ||
460 | } | ||
461 | } | ||
462 | } | ||
463 | |||
464 | void SubwayMap::OnTimer(wxTimerEvent &event) { | ||
465 | SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_}); | ||
466 | Refresh(); | ||
467 | } | ||
468 | |||
469 | void SubwayMap::OnZoomSlide(wxCommandEvent &event) { | ||
470 | double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue(); | ||
471 | |||
472 | if (new_zoom != zoom_) { | ||
473 | SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2}); | ||
474 | } | ||
475 | } | ||
476 | |||
477 | void SubwayMap::OnClickHelp(wxCommandEvent &event) { | ||
478 | wxMessageBox( | ||
479 | "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the " | ||
480 | "corner.\nClick on a side of the screen to start panning. It will follow " | ||
481 | "your mouse. Click again to stop.\nHover over a door to see the " | ||
482 | "requirements to open it.\nHover over a warp or active painting to see " | ||
483 | "what it is connected to.\nIn painting shuffle, paintings that have not " | ||
484 | "yet been checked will not show their connections.\nA green shaded owl " | ||
485 | "means that there is a painting entrance there.\nA red shaded owl means " | ||
486 | "that there are only painting exits there.\nClick on a door or " | ||
487 | "warp to make the popup stick until you click again.", | ||
488 | "Subway Map Help"); | ||
489 | } | ||
490 | |||
491 | void SubwayMap::Redraw() { | ||
492 | rendered_ = wxBitmap(map_image_); | ||
493 | |||
494 | wxMemoryDC dc; | ||
495 | dc.SelectObject(rendered_); | ||
496 | |||
497 | wxGCDC gcdc(dc); | ||
498 | |||
499 | for (const SubwayItem &subway_item : GD_GetSubwayItems()) { | ||
500 | ItemDrawType draw_type = ItemDrawType::kNone; | ||
501 | const wxBrush *brush_color = wxGREY_BRUSH; | ||
502 | std::optional<wxColour> shade_color; | ||
503 | |||
504 | if (AP_HasEarlyColorHallways() && | ||
505 | (subway_item.special == "starting_room_paintings" || | ||
506 | subway_item.special == "early_color_hallways")) { | ||
507 | draw_type = ItemDrawType::kOwl; | ||
508 | |||
509 | if (subway_item.special == "starting_room_paintings") { | ||
510 | shade_color = wxColour(0, 255, 0, 128); | ||
511 | } else { | ||
512 | shade_color = wxColour(255, 0, 0, 128); | ||
513 | } | ||
514 | } else if (subway_item.special == "sun_painting") { | ||
515 | if (!AP_IsPilgrimageEnabled()) { | ||
516 | if (IsDoorOpen(*subway_item.door)) { | ||
517 | draw_type = ItemDrawType::kOwl; | ||
518 | shade_color = wxColour(0, 255, 0, 128); | ||
519 | } else { | ||
520 | draw_type = ItemDrawType::kBox; | ||
521 | brush_color = wxRED_BRUSH; | ||
522 | } | ||
523 | } | ||
524 | } else if (!subway_item.paintings.empty()) { | ||
525 | if (AP_IsPaintingShuffle()) { | ||
526 | bool has_checked_painting = false; | ||
527 | bool has_unchecked_painting = false; | ||
528 | bool has_mapped_painting = false; | ||
529 | bool has_codomain_painting = false; | ||
530 | |||
531 | for (const std::string &painting_id : subway_item.paintings) { | ||
532 | if (checked_paintings_.count(painting_id)) { | ||
533 | has_checked_painting = true; | ||
534 | |||
535 | if (AP_GetPaintingMapping().count(painting_id)) { | ||
536 | has_mapped_painting = true; | ||
537 | } else if (AP_IsPaintingMappedTo(painting_id)) { | ||
538 | has_codomain_painting = true; | ||
539 | } | ||
540 | } else { | ||
541 | has_unchecked_painting = true; | ||
542 | } | ||
543 | } | ||
544 | |||
545 | if (has_unchecked_painting || has_mapped_painting || has_codomain_painting) { | ||
546 | draw_type = ItemDrawType::kOwl; | ||
547 | |||
548 | if (has_checked_painting) { | ||
549 | if (has_mapped_painting) { | ||
550 | shade_color = wxColour(0, 255, 0, 128); | ||
551 | } else { | ||
552 | shade_color = wxColour(255, 0, 0, 128); | ||
553 | } | ||
554 | } | ||
555 | } | ||
556 | } else if (!subway_item.tags.empty()) { | ||
557 | draw_type = ItemDrawType::kOwl; | ||
558 | } | ||
559 | } else if (subway_item.door) { | ||
560 | draw_type = ItemDrawType::kBox; | ||
561 | |||
562 | if (IsDoorOpen(*subway_item.door)) { | ||
563 | brush_color = wxGREEN_BRUSH; | ||
564 | } else { | ||
565 | brush_color = wxRED_BRUSH; | ||
566 | } | ||
567 | } | ||
568 | |||
569 | wxPoint real_area_pos = {subway_item.x, subway_item.y}; | ||
570 | |||
571 | int real_area_size = | ||
572 | (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE); | ||
573 | |||
574 | if (draw_type == ItemDrawType::kBox) { | ||
575 | gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); | ||
576 | gcdc.SetBrush(*brush_color); | ||
577 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | ||
578 | } else if (draw_type == ItemDrawType::kOwl) { | ||
579 | wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( | ||
580 | real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); | ||
581 | gcdc.DrawBitmap(owl_bitmap, real_area_pos); | ||
582 | |||
583 | if (shade_color) { | ||
584 | gcdc.SetBrush(wxBrush(*shade_color)); | ||
585 | gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); | ||
586 | } | ||
587 | } | ||
588 | } | ||
589 | } | ||
590 | |||
591 | void SubwayMap::SetUpHelpButton() { | ||
592 | help_button_->SetPosition({ | ||
593 | GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, | ||
594 | 15, | ||
595 | }); | ||
596 | } | ||
597 | |||
598 | void SubwayMap::EvaluateScroll(wxPoint pos) { | ||
599 | int scroll_x; | ||
600 | int scroll_y; | ||
601 | if (pos.x < GetSize().GetWidth() / 9) { | ||
602 | scroll_x = 20; | ||
603 | } else if (pos.x < GetSize().GetWidth() / 6) { | ||
604 | scroll_x = 5; | ||
605 | } else if (pos.x > 8 * GetSize().GetWidth() / 9) { | ||
606 | scroll_x = -20; | ||
607 | } else if (pos.x > 5 * GetSize().GetWidth() / 6) { | ||
608 | scroll_x = -5; | ||
609 | } else { | ||
610 | scroll_x = 0; | ||
611 | } | ||
612 | if (pos.y < GetSize().GetHeight() / 9) { | ||
613 | scroll_y = 20; | ||
614 | } else if (pos.y < GetSize().GetHeight() / 6) { | ||
615 | scroll_y = 5; | ||
616 | } else if (pos.y > 8 * GetSize().GetHeight() / 9) { | ||
617 | scroll_y = -20; | ||
618 | } else if (pos.y > 5 * GetSize().GetHeight() / 6) { | ||
619 | scroll_y = -5; | ||
620 | } else { | ||
621 | scroll_y = 0; | ||
622 | } | ||
623 | |||
624 | SetScrollSpeed(scroll_x, scroll_y); | ||
625 | } | ||
626 | |||
627 | wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { | ||
628 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | ||
629 | map_image_.GetSize().GetWidth() + | ||
630 | zoom_x_), | ||
631 | static_cast<int>(pos.y * render_width_ * zoom_ / | ||
632 | map_image_.GetSize().GetWidth() + | ||
633 | zoom_y_)}; | ||
634 | } | ||
635 | |||
636 | wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const { | ||
637 | return {static_cast<int>(pos.x * render_width_ * zoom_ / | ||
638 | map_image_.GetSize().GetWidth()), | ||
639 | static_cast<int>(pos.y * render_width_ * zoom_ / | ||
640 | map_image_.GetSize().GetWidth())}; | ||
641 | } | ||
642 | |||
643 | wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const { | ||
644 | return { | ||
645 | std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() / | ||
646 | render_width_ / zoom_), | ||
647 | 0, map_image_.GetWidth() - 1), | ||
648 | std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() / | ||
649 | render_width_ / zoom_), | ||
650 | 0, map_image_.GetHeight() - 1)}; | ||
651 | } | ||
652 | |||
653 | void SubwayMap::SetZoomPos(wxPoint pos) { | ||
654 | if (render_width_ * zoom_ <= GetSize().GetWidth()) { | ||
655 | zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2; | ||
656 | } else { | ||
657 | zoom_x_ = std::clamp( | ||
658 | pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_), | ||
659 | 0); | ||
660 | } | ||
661 | if (render_height_ * zoom_ <= GetSize().GetHeight()) { | ||
662 | zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2; | ||
663 | } else { | ||
664 | zoom_y_ = std::clamp( | ||
665 | pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_), | ||
666 | 0); | ||
667 | } | ||
668 | } | ||
669 | |||
670 | void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) { | ||
671 | bool should_timer = (scroll_x != 0 || scroll_y != 0); | ||
672 | if (should_timer != scroll_timer_->IsRunning()) { | ||
673 | if (should_timer) { | ||
674 | scroll_timer_->Start(1000 / 60); | ||
675 | } else { | ||
676 | scroll_timer_->Stop(); | ||
677 | } | ||
678 | } | ||
679 | |||
680 | scroll_x_ = scroll_x; | ||
681 | scroll_y_ = scroll_y; | ||
682 | } | ||
683 | |||
684 | void SubwayMap::SetZoom(double zoom, wxPoint static_point) { | ||
685 | wxPoint map_pos = RenderPosToMapPos(static_point); | ||
686 | zoom_ = zoom; | ||
687 | |||
688 | wxPoint virtual_pos = MapPosToVirtualPos(map_pos); | ||
689 | SetZoomPos(-(virtual_pos - static_point)); | ||
690 | |||
691 | Refresh(); | ||
692 | |||
693 | zoom_slider_->SetValue((zoom - 1.0) / 0.25); | ||
694 | } | ||
695 | |||
696 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { | ||
697 | const SubwayItem &subway_item = GD_GetSubwayItem(id); | ||
698 | return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), | ||
699 | AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; | ||
700 | } | ||
diff --git a/src/subway_map.h b/src/subway_map.h new file mode 100644 index 0000000..feee8ff --- /dev/null +++ b/src/subway_map.h | |||
@@ -0,0 +1,92 @@ | |||
1 | #ifndef SUBWAY_MAP_H_BD2D843E | ||
2 | #define SUBWAY_MAP_H_BD2D843E | ||
3 | |||
4 | #include <wx/wxprec.h> | ||
5 | |||
6 | #ifndef WX_PRECOMP | ||
7 | #include <wx/wx.h> | ||
8 | #endif | ||
9 | |||
10 | #include <memory> | ||
11 | #include <optional> | ||
12 | #include <set> | ||
13 | #include <string> | ||
14 | #include <vector> | ||
15 | |||
16 | #include <quadtree/Quadtree.h> | ||
17 | |||
18 | #include "game_data.h" | ||
19 | #include "network_set.h" | ||
20 | |||
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..66e7751 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,10 +572,17 @@ class StateCalculator { | |||
395 | std::set<int> reachable_rooms_; | 572 | std::set<int> reachable_rooms_; |
396 | std::map<int, Decision> door_decisions_; | 573 | std::map<int, Decision> door_decisions_; |
397 | std::set<int> solveable_panels_; | 574 | std::set<int> solveable_panels_; |
575 | std::set<int> reachable_paintings_; | ||
576 | std::map<int, std::map<std::string, bool>> door_report_; | ||
398 | }; | 577 | }; |
399 | 578 | ||
400 | } // namespace | 579 | } // namespace |
401 | 580 | ||
581 | void ResetReachabilityRequirements() { | ||
582 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
583 | GetState().requirements.Reset(); | ||
584 | } | ||
585 | |||
402 | void RecalculateReachability() { | 586 | void RecalculateReachability() { |
403 | StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); | 587 | StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); |
404 | state_calculator.Calculate(); | 588 | state_calculator.Calculate(); |
@@ -422,9 +606,23 @@ void RecalculateReachability() { | |||
422 | } | 606 | } |
423 | } | 607 | } |
424 | 608 | ||
609 | std::set<int> new_reachable_doors; | ||
610 | for (const auto& [door_id, decision] : state_calculator.GetDoorDecisions()) { | ||
611 | if (decision == kYes) { | ||
612 | new_reachable_doors.insert(door_id); | ||
613 | } | ||
614 | } | ||
615 | |||
616 | std::set<int> reachable_paintings = state_calculator.GetReachablePaintings(); | ||
617 | std::map<int, std::map<std::string, bool>> door_reports = | ||
618 | state_calculator.GetDoorReports(); | ||
619 | |||
425 | { | 620 | { |
426 | std::lock_guard reachability_guard(GetState().reachability_mutex); | 621 | std::lock_guard reachability_guard(GetState().reachability_mutex); |
427 | std::swap(GetState().reachability, new_reachability); | 622 | std::swap(GetState().reachability, new_reachability); |
623 | std::swap(GetState().reachable_doors, new_reachable_doors); | ||
624 | std::swap(GetState().reachable_paintings, reachable_paintings); | ||
625 | std::swap(GetState().door_reports, door_reports); | ||
428 | } | 626 | } |
429 | } | 627 | } |
430 | 628 | ||
@@ -437,3 +635,21 @@ bool IsLocationReachable(int location_id) { | |||
437 | return false; | 635 | return false; |
438 | } | 636 | } |
439 | } | 637 | } |
638 | |||
639 | bool IsDoorOpen(int door_id) { | ||
640 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
641 | |||
642 | return GetState().reachable_doors.count(door_id); | ||
643 | } | ||
644 | |||
645 | bool IsPaintingReachable(int painting_id) { | ||
646 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
647 | |||
648 | return GetState().reachable_paintings.count(painting_id); | ||
649 | } | ||
650 | |||
651 | const std::map<std::string, bool>& GetDoorRequirements(int door_id) { | ||
652 | std::lock_guard reachability_guard(GetState().reachability_mutex); | ||
653 | |||
654 | return GetState().door_reports[door_id]; | ||
655 | } | ||
diff --git a/src/tracker_state.h b/src/tracker_state.h index e73607f..c7857a0 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h | |||
@@ -1,8 +1,19 @@ | |||
1 | #ifndef TRACKER_STATE_H_8639BC90 | 1 | #ifndef TRACKER_STATE_H_8639BC90 |
2 | #define TRACKER_STATE_H_8639BC90 | 2 | #define TRACKER_STATE_H_8639BC90 |
3 | 3 | ||
4 | #include <map> | ||
5 | #include <string> | ||
6 | |||
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 0ccd2c7..4b13d42 100644 --- a/src/version.h +++ b/src/version.h | |||
@@ -1,7 +1,7 @@ | |||
1 | #ifndef VERSION_H_C757E53C | 1 | #ifndef VERSION_H_C757E53C |
2 | #define VERSION_H_C757E53C | 2 | #define VERSION_H_C757E53C |
3 | 3 | ||
4 | #include <iostream> | 4 | #include <sstream> |
5 | #include <regex> | 5 | #include <regex> |
6 | 6 | ||
7 | struct Version { | 7 | struct Version { |
@@ -23,6 +23,12 @@ struct Version { | |||
23 | } | 23 | } |
24 | } | 24 | } |
25 | 25 | ||
26 | std::string ToString() const { | ||
27 | std::ostringstream output; | ||
28 | output << "v" << major << "." << minor << "." << revision; | ||
29 | return output.str(); | ||
30 | } | ||
31 | |||
26 | bool operator<(const Version& rhs) const { | 32 | bool operator<(const Version& rhs) const { |
27 | return (major < rhs.major) || | 33 | return (major < rhs.major) || |
28 | (major == rhs.major && | 34 | (major == rhs.major && |
@@ -31,10 +37,6 @@ struct Version { | |||
31 | } | 37 | } |
32 | }; | 38 | }; |
33 | 39 | ||
34 | std::ostream& operator<<(std::ostream& out, const Version& ver) { | ||
35 | return out << "v" << ver.major << "." << ver.minor << "." << ver.revision; | ||
36 | } | ||
37 | |||
38 | constexpr const Version kTrackerVersion = Version(0, 9, 2); | 40 | constexpr const Version kTrackerVersion = Version(0, 9, 2); |
39 | 41 | ||
40 | #endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file | 42 | #endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file |