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