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