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