about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/achievements_pane.cpp15
-rw-r--r--src/achievements_pane.h6
-rw-r--r--src/ap_state.cpp211
-rw-r--r--src/ap_state.h27
-rw-r--r--src/area_popup.cpp196
-rw-r--r--src/area_popup.h38
-rw-r--r--src/connection_dialog.cpp44
-rw-r--r--src/connection_dialog.h6
-rw-r--r--src/game_data.cpp213
-rw-r--r--src/game_data.h13
-rw-r--r--src/global.cpp16
-rw-r--r--src/global.h2
-rw-r--r--src/godot_variant.cpp84
-rw-r--r--src/godot_variant.h28
-rw-r--r--src/icons.cpp22
-rw-r--r--src/icons.h25
-rw-r--r--src/ipc_dialog.cpp7
-rw-r--r--src/ipc_dialog.h2
-rw-r--r--src/ipc_state.cpp23
-rw-r--r--src/ipc_state.h2
-rw-r--r--src/items_pane.cpp145
-rw-r--r--src/items_pane.h33
-rw-r--r--src/log_dialog.cpp37
-rw-r--r--src/log_dialog.h24
-rw-r--r--src/logger.cpp42
-rw-r--r--src/logger.h6
-rw-r--r--src/main.cpp2
-rw-r--r--src/options_pane.cpp71
-rw-r--r--src/options_pane.h19
-rw-r--r--src/paintings_pane.cpp86
-rw-r--r--src/paintings_pane.h28
-rw-r--r--src/report_popup.cpp131
-rw-r--r--src/report_popup.h38
-rw-r--r--src/settings_dialog.cpp45
-rw-r--r--src/settings_dialog.h13
-rw-r--r--src/subway_map.cpp293
-rw-r--r--src/subway_map.h7
-rw-r--r--src/tracker_config.cpp10
-rw-r--r--src/tracker_config.h9
-rw-r--r--src/tracker_frame.cpp242
-rw-r--r--src/tracker_frame.h66
-rw-r--r--src/tracker_panel.cpp206
-rw-r--r--src/tracker_panel.h22
-rw-r--r--src/tracker_state.cpp203
-rw-r--r--src/tracker_state.h6
-rw-r--r--src/updater.cpp309
-rw-r--r--src/updater.h46
-rw-r--r--src/version.h2
-rw-r--r--src/windows.rc3
49 files changed, 2247 insertions, 877 deletions
diff --git a/src/achievements_pane.cpp b/src/achievements_pane.cpp index 8ec3727..d23c434 100644 --- a/src/achievements_pane.cpp +++ b/src/achievements_pane.cpp
@@ -8,23 +8,24 @@ AchievementsPane::AchievementsPane(wxWindow* parent)
8 AppendColumn("Achievement"); 8 AppendColumn("Achievement");
9 9
10 for (int panel_id : GD_GetAchievementPanels()) { 10 for (int panel_id : GD_GetAchievementPanels()) {
11 achievement_names_.push_back(GD_GetPanel(panel_id).achievement_name); 11 const Panel& panel = GD_GetPanel(panel_id);
12 achievements_.emplace_back(panel.achievement_name, panel.solve_index);
12 } 13 }
13 14
14 std::sort(std::begin(achievement_names_), std::end(achievement_names_)); 15 std::sort(std::begin(achievements_), std::end(achievements_));
15 16
16 for (int i = 0; i < achievement_names_.size(); i++) { 17 for (int i = 0; i < achievements_.size(); i++) {
17 InsertItem(i, achievement_names_.at(i)); 18 InsertItem(i, std::get<0>(achievements_.at(i)));
18 } 19 }
19 20
20 SetColumnWidth(0, wxLIST_AUTOSIZE); 21 SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
21 22
22 UpdateIndicators(); 23 UpdateIndicators();
23} 24}
24 25
25void AchievementsPane::UpdateIndicators() { 26void AchievementsPane::UpdateIndicators() {
26 for (int i = 0; i < achievement_names_.size(); i++) { 27 for (int i = 0; i < achievements_.size(); i++) {
27 if (AP_HasAchievement(achievement_names_.at(i))) { 28 if (AP_IsPanelSolved(std::get<1>(achievements_.at(i)))) {
28 SetItemTextColour(i, *wxBLACK); 29 SetItemTextColour(i, *wxBLACK);
29 } else { 30 } else {
30 SetItemTextColour(i, *wxRED); 31 SetItemTextColour(i, *wxRED);
diff --git a/src/achievements_pane.h b/src/achievements_pane.h index ac88cac..941b5e3 100644 --- a/src/achievements_pane.h +++ b/src/achievements_pane.h
@@ -9,6 +9,10 @@
9 9
10#include <wx/listctrl.h> 10#include <wx/listctrl.h>
11 11
12#include <string>
13#include <tuple>
14#include <vector>
15
12class AchievementsPane : public wxListView { 16class AchievementsPane : public wxListView {
13 public: 17 public:
14 explicit AchievementsPane(wxWindow* parent); 18 explicit AchievementsPane(wxWindow* parent);
@@ -16,7 +20,7 @@ class AchievementsPane : public wxListView {
16 void UpdateIndicators(); 20 void UpdateIndicators();
17 21
18 private: 22 private:
19 std::vector<std::string> achievement_names_; 23 std::vector<std::tuple<std::string, int>> achievements_; // name, solve index
20}; 24};
21 25
22#endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file 26#endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index 4ac0cce..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -10,6 +10,7 @@
10#include <any> 10#include <any>
11#include <apclient.hpp> 11#include <apclient.hpp>
12#include <apuuid.hpp> 12#include <apuuid.hpp>
13#include <bitset>
13#include <chrono> 14#include <chrono>
14#include <exception> 15#include <exception>
15#include <filesystem> 16#include <filesystem>
@@ -28,8 +29,8 @@
28#include "tracker_state.h" 29#include "tracker_state.h"
29 30
30constexpr int AP_MAJOR = 0; 31constexpr int AP_MAJOR = 0;
31constexpr int AP_MINOR = 4; 32constexpr int AP_MINOR = 6;
32constexpr int AP_REVISION = 5; 33constexpr int AP_REVISION = 1;
33 34
34constexpr const char* CERT_STORE_PATH = "cacert.pem"; 35constexpr const char* CERT_STORE_PATH = "cacert.pem";
35constexpr int ITEM_HANDLING = 7; // <- all 36constexpr int ITEM_HANDLING = 7; // <- all
@@ -37,8 +38,24 @@ constexpr int ITEM_HANDLING = 7; // <- all
37constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds 38constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds
38constexpr int CONNECTION_BACKOFF_INTERVAL = 100; 39constexpr int CONNECTION_BACKOFF_INTERVAL = 100;
39 40
41constexpr int PANEL_COUNT = 803;
42constexpr int PANEL_BITFIELD_LENGTH = 48;
43constexpr int PANEL_BITFIELDS = 17;
44
40namespace { 45namespace {
41 46
47const std::set<long> kNonProgressionItems = {
48 444409, // :)
49 444575, // The Feeling of Being Lost
50 444576, // Wanderlust
51 444577, // Empty White Hallways
52 444410, // Slowness Trap
53 444411, // Iceland Trap
54 444412, // Atbash Trap
55 444413, // Puzzle Skip
56 444680, // Speed Boost
57};
58
42struct APState { 59struct APState {
43 // Initialized on main thread 60 // Initialized on main thread
44 bool initialized = false; 61 bool initialized = false;
@@ -67,10 +84,12 @@ struct APState {
67 std::set<int64_t> checked_locations; 84 std::set<int64_t> checked_locations;
68 std::map<std::string, std::any> data_storage; 85 std::map<std::string, std::any> data_storage;
69 std::optional<std::tuple<int, int>> player_pos; 86 std::optional<std::tuple<int, int>> player_pos;
87 std::bitset<PANEL_COUNT> solved_panels;
70 88
71 DoorShuffleMode door_shuffle_mode = kNO_DOORS; 89 DoorShuffleMode door_shuffle_mode = kNO_DOORS;
72 bool group_doors = false; 90 bool group_doors = false;
73 bool color_shuffle = false; 91 bool color_shuffle = false;
92 PanelShuffleMode panel_shuffle_mode = kNO_PANELS;
74 bool painting_shuffle = false; 93 bool painting_shuffle = false;
75 int mastery_requirement = 21; 94 int mastery_requirement = 21;
76 int level_2_requirement = 223; 95 int level_2_requirement = 223;
@@ -82,6 +101,7 @@ struct APState {
82 bool pilgrimage_allows_paintings = false; 101 bool pilgrimage_allows_paintings = false;
83 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; 102 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
84 bool sunwarp_shuffle = false; 103 bool sunwarp_shuffle = false;
104 bool postgame_shuffle = true;
85 105
86 std::map<std::string, std::string> painting_mapping; 106 std::map<std::string, std::string> painting_mapping;
87 std::set<std::string> painting_codomain; 107 std::set<std::string> painting_codomain;
@@ -128,10 +148,12 @@ struct APState {
128 checked_locations.clear(); 148 checked_locations.clear();
129 data_storage.clear(); 149 data_storage.clear();
130 player_pos = std::nullopt; 150 player_pos = std::nullopt;
151 solved_panels.reset();
131 victory_data_storage_key.clear(); 152 victory_data_storage_key.clear();
132 door_shuffle_mode = kNO_DOORS; 153 door_shuffle_mode = kNO_DOORS;
133 group_doors = false; 154 group_doors = false;
134 color_shuffle = false; 155 color_shuffle = false;
156 panel_shuffle_mode = kNO_PANELS;
135 painting_shuffle = false; 157 painting_shuffle = false;
136 painting_mapping.clear(); 158 painting_mapping.clear();
137 painting_codomain.clear(); 159 painting_codomain.clear();
@@ -146,6 +168,7 @@ struct APState {
146 sunwarp_access = kSUNWARP_ACCESS_NORMAL; 168 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
147 sunwarp_shuffle = false; 169 sunwarp_shuffle = false;
148 sunwarp_mapping.clear(); 170 sunwarp_mapping.clear();
171 postgame_shuffle = true;
149 } 172 }
150 173
151 apclient->set_room_info_handler( 174 apclient->set_room_info_handler(
@@ -200,24 +223,13 @@ struct APState {
200 return checked_locations.count(location_id); 223 return checked_locations.count(location_id);
201 } 224 }
202 225
203 bool HasCheckedHuntPanel(int location_id) {
204 std::lock_guard state_guard(state_mutex);
205
206 std::string key =
207 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
208 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
209 }
210
211 bool HasItem(int item_id, int quantity) { 226 bool HasItem(int item_id, int quantity) {
212 return inventory.count(item_id) && inventory.at(item_id) >= quantity; 227 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
213 } 228 }
214 229
215 bool HasAchievement(const std::string& name) { 230 bool HasItemSafe(int item_id, int quantity) {
216 std::lock_guard state_guard(state_mutex); 231 std::lock_guard state_guard(state_mutex);
217 232 return HasItem(item_id, quantity);
218 std::string key =
219 fmt::format("{}Achievement|{}", data_storage_prefix, name);
220 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
221 } 233 }
222 234
223 const std::set<std::string>& GetCheckedPaintings() { 235 const std::set<std::string>& GetCheckedPaintings() {
@@ -241,7 +253,21 @@ struct APState {
241 checked_paintings.count(painting_mapping.at(painting_id))); 253 checked_paintings.count(painting_mapping.at(painting_id)));
242 } 254 }
243 255
244 std::string GetItemName(int id) { return apclient->get_item_name(id); } 256 void RevealPaintings() {
257 std::lock_guard state_guard(state_mutex);
258
259 std::vector<std::string> paintings;
260 for (const PaintingExit& painting : GD_GetPaintings()) {
261 paintings.push_back(painting.internal_id);
262 }
263
264 APClient::DataStorageOperation operation;
265 operation.operation = "replace";
266 operation.value = paintings;
267
268 apclient->Set(fmt::format("{}Paintings", data_storage_prefix), "", true,
269 {operation});
270 }
245 271
246 bool HasReachedGoal() { 272 bool HasReachedGoal() {
247 std::lock_guard state_guard(state_mutex); 273 std::lock_guard state_guard(state_mutex);
@@ -251,6 +277,12 @@ struct APState {
251 30; // CLIENT_GOAL 277 30; // CLIENT_GOAL
252 } 278 }
253 279
280 bool IsPanelSolved(int solve_index) {
281 std::lock_guard state_guard(state_mutex);
282
283 return solved_panels.test(solve_index);
284 }
285
254 private: 286 private:
255 void Initialize() { 287 void Initialize() {
256 if (!initialized) { 288 if (!initialized) {
@@ -258,16 +290,8 @@ struct APState {
258 290
259 std::thread([this]() { Thread(); }).detach(); 291 std::thread([this]() { Thread(); }).detach();
260 292
261 for (int panel_id : GD_GetAchievementPanels()) { 293 for (int i = 0; i < PANEL_BITFIELDS; i++) {
262 tracked_data_storage_keys.push_back(fmt::format( 294 tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i));
263 "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
264 }
265
266 for (const MapArea& map_area : GD_GetMapAreas()) {
267 for (const Location& location : map_area.locations) {
268 tracked_data_storage_keys.push_back(
269 fmt::format("Hunt|{}", location.ap_location_id));
270 }
271 } 295 }
272 296
273 tracked_data_storage_keys.push_back("PlayerPos"); 297 tracked_data_storage_keys.push_back("PlayerPos");
@@ -351,7 +375,7 @@ struct APState {
351 } 375 }
352 } 376 }
353 377
354 RefreshTracker(false); 378 RefreshTracker(StateUpdate{.cleared_locations = true});
355 } 379 }
356 380
357 void OnSlotDisconnected() { 381 void OnSlotDisconnected() {
@@ -373,37 +397,59 @@ struct APState {
373 } 397 }
374 398
375 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) { 399 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) {
400 std::vector<ItemState> item_states;
401 bool progression_items = false;
402
376 { 403 {
377 std::lock_guard state_guard(state_mutex); 404 std::lock_guard state_guard(state_mutex);
378 405
406 std::map<int64_t, int> index_by_item;
407
379 for (const APClient::NetworkItem& item : items) { 408 for (const APClient::NetworkItem& item : items) {
380 inventory[item.item]++; 409 inventory[item.item]++;
381 TrackerLog(fmt::format("Item: {}", item.item)); 410 TrackerLog(fmt::format("Item: {}", item.item));
411
412 index_by_item[item.item] = item.index;
413
414 if (!kNonProgressionItems.count(item.item)) {
415 progression_items = true;
416 }
417 }
418
419 for (const auto& [item_id, item_index] : index_by_item) {
420 item_states.push_back(ItemState{.name = GD_GetItemName(item_id),
421 .amount = inventory[item_id],
422 .index = item_index});
382 } 423 }
383 } 424 }
384 425
385 RefreshTracker(false); 426 RefreshTracker(StateUpdate{.items = item_states,
427 .progression_items = progression_items});
386 } 428 }
387 429
388 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) { 430 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) {
431 StateUpdate state_update;
432
389 { 433 {
390 std::lock_guard state_guard(state_mutex); 434 std::lock_guard state_guard(state_mutex);
391 435
392 for (const auto& [key, value] : data) { 436 for (const auto& [key, value] : data) {
393 HandleDataStorage(key, value); 437 HandleDataStorage(key, value, state_update);
394 } 438 }
395 } 439 }
396 440
397 RefreshTracker(false); 441 RefreshTracker(state_update);
398 } 442 }
399 443
400 void OnSetReply(const std::string& key, const nlohmann::json& value) { 444 void OnSetReply(const std::string& key, const nlohmann::json& value) {
445 StateUpdate state_update;
446
401 { 447 {
402 std::lock_guard state_guard(state_mutex); 448 std::lock_guard state_guard(state_mutex);
403 HandleDataStorage(key, value); 449 HandleDataStorage(key, value, state_update);
404 } 450 }
405 451
406 RefreshTracker(false); 452 RefreshTracker(state_update);
407 } 453 }
408 454
409 void OnSlotConnected(std::string player, std::string server, 455 void OnSlotConnected(std::string player, std::string server,
@@ -435,6 +481,7 @@ struct APState {
435 } 481 }
436 } 482 }
437 color_shuffle = slot_data["shuffle_colors"].get<int>() == 1; 483 color_shuffle = slot_data["shuffle_colors"].get<int>() == 1;
484 panel_shuffle_mode = slot_data["shuffle_panels"].get<PanelShuffleMode>();
438 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 485 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
439 mastery_requirement = slot_data["mastery_achievements"].get<int>(); 486 mastery_requirement = slot_data["mastery_achievements"].get<int>();
440 level_2_requirement = slot_data["level_2_requirement"].get<int>(); 487 level_2_requirement = slot_data["level_2_requirement"].get<int>();
@@ -456,6 +503,9 @@ struct APState {
456 : kSUNWARP_ACCESS_NORMAL; 503 : kSUNWARP_ACCESS_NORMAL;
457 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && 504 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
458 slot_data["shuffle_sunwarps"].get<int>() == 1; 505 slot_data["shuffle_sunwarps"].get<int>() == 1;
506 postgame_shuffle = slot_data.contains("shuffle_postgame")
507 ? (slot_data["shuffle_postgame"].get<int>() == 1)
508 : true;
459 509
460 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { 510 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
461 painting_mapping.clear(); 511 painting_mapping.clear();
@@ -497,7 +547,7 @@ struct APState {
497 } 547 }
498 548
499 ResetReachabilityRequirements(); 549 ResetReachabilityRequirements();
500 RefreshTracker(true); 550 RefreshTracker(std::nullopt);
501 } 551 }
502 552
503 void OnSlotRefused(const std::list<std::string>& errors) { 553 void OnSlotRefused(const std::list<std::string>& errors) {
@@ -540,19 +590,40 @@ struct APState {
540 } 590 }
541 591
542 // Assumes state mutex is locked. 592 // Assumes state mutex is locked.
543 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 593 void HandleDataStorage(const std::string& key, const nlohmann::json& value, StateUpdate& state_update) {
544 if (value.is_boolean()) { 594 if (value.is_boolean()) {
545 data_storage[key] = value.get<bool>(); 595 data_storage[key] = value.get<bool>();
546 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 596 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
547 (value.get<bool>() ? "true" : "false"))); 597 (value.get<bool>() ? "true" : "false")));
598
548 } else if (value.is_number()) { 599 } else if (value.is_number()) {
549 data_storage[key] = value.get<int>(); 600 data_storage[key] = value.get<int>();
550 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 601 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
551 value.get<int>())); 602 value.get<int>()));
603
604 if (key == victory_data_storage_key) {
605 state_update.cleared_locations = true;
606 } else if (key.find("Panels_") != std::string::npos) {
607 int bitfield_num =
608 std::stoi(key.substr(data_storage_prefix.size() + 7));
609 uint64_t bitfield_value = value.get<uint64_t>();
610 for (int i = 0; i < PANEL_BITFIELD_LENGTH; i++) {
611 if ((bitfield_value & (1LL << i)) != 0) {
612 int solve_index = bitfield_num * PANEL_BITFIELD_LENGTH + i;
613
614 if (!solved_panels.test(solve_index)) {
615 state_update.panels.insert(solve_index);
616 }
617
618 solved_panels.set(solve_index);
619 }
620 }
621 }
552 } else if (value.is_object()) { 622 } else if (value.is_object()) {
553 if (key.ends_with("PlayerPos")) { 623 if (key.ends_with("PlayerPos")) {
554 auto map_value = value.get<std::map<std::string, int>>(); 624 auto map_value = value.get<std::map<std::string, int>>();
555 player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]); 625 player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]);
626 state_update.player_position = true;
556 } else { 627 } else {
557 data_storage[key] = value.get<std::map<std::string, int>>(); 628 data_storage[key] = value.get<std::map<std::string, int>>();
558 } 629 }
@@ -561,6 +632,7 @@ struct APState {
561 } else if (value.is_null()) { 632 } else if (value.is_null()) {
562 if (key.ends_with("PlayerPos")) { 633 if (key.ends_with("PlayerPos")) {
563 player_pos = std::nullopt; 634 player_pos = std::nullopt;
635 state_update.player_position = true;
564 } else { 636 } else {
565 data_storage.erase(key); 637 data_storage.erase(key);
566 } 638 }
@@ -572,6 +644,8 @@ struct APState {
572 if (key.ends_with("Paintings")) { 644 if (key.ends_with("Paintings")) {
573 data_storage[key] = 645 data_storage[key] =
574 std::set<std::string>(list_value.begin(), list_value.end()); 646 std::set<std::string>(list_value.begin(), list_value.end());
647 state_update.paintings =
648 std::vector<std::string>(list_value.begin(), list_value.end());
575 } else { 649 } else {
576 data_storage[key] = list_value; 650 data_storage[key] = list_value;
577 } 651 }
@@ -582,29 +656,34 @@ struct APState {
582 } 656 }
583 657
584 // State mutex should NOT be locked. 658 // State mutex should NOT be locked.
585 void RefreshTracker(bool reset) { 659 // nullopt state_update indicates a reset.
660 void RefreshTracker(std::optional<StateUpdate> state_update) {
586 TrackerLog("Refreshing display..."); 661 TrackerLog("Refreshing display...");
587 662
588 std::string prev_msg; 663 if (!state_update || state_update->progression_items ||
589 { 664 !state_update->paintings.empty()) {
590 std::lock_guard state_guard(state_mutex); 665 std::string prev_msg;
666 {
667 std::lock_guard state_guard(state_mutex);
591 668
592 prev_msg = status_message; 669 prev_msg = status_message;
593 SetStatusMessage(fmt::format("{} Recalculating...", status_message)); 670 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
594 } 671 }
595 672
596 RecalculateReachability(); 673 RecalculateReachability();
597 674
598 if (reset) { 675 {
599 tracker_frame->ResetIndicators(); 676 std::lock_guard state_guard(state_mutex);
600 } else {
601 tracker_frame->UpdateIndicators();
602 }
603 677
604 { 678 SetStatusMessage(prev_msg);
605 std::lock_guard state_guard(state_mutex); 679 }
680 }
681
606 682
607 SetStatusMessage(prev_msg); 683 if (!state_update) {
684 tracker_frame->ResetIndicators();
685 } else {
686 tracker_frame->UpdateIndicators(*state_update);
608 } 687 }
609 } 688 }
610 689
@@ -639,16 +718,12 @@ bool AP_HasCheckedGameLocation(int location_id) {
639 return GetState().HasCheckedGameLocation(location_id); 718 return GetState().HasCheckedGameLocation(location_id);
640} 719}
641 720
642bool AP_HasCheckedHuntPanel(int location_id) {
643 return GetState().HasCheckedHuntPanel(location_id);
644}
645
646bool AP_HasItem(int item_id, int quantity) { 721bool AP_HasItem(int item_id, int quantity) {
647 return GetState().HasItem(item_id, quantity); 722 return GetState().HasItem(item_id, quantity);
648} 723}
649 724
650std::string AP_GetItemName(int item_id) { 725bool AP_HasItemSafe(int item_id, int quantity) {
651 return GetState().GetItemName(item_id); 726 return GetState().HasItemSafe(item_id, quantity);
652} 727}
653 728
654DoorShuffleMode AP_GetDoorShuffleMode() { 729DoorShuffleMode AP_GetDoorShuffleMode() {
@@ -695,6 +770,8 @@ bool AP_IsPaintingChecked(const std::string& painting_id) {
695 return GetState().IsPaintingChecked(painting_id); 770 return GetState().IsPaintingChecked(painting_id);
696} 771}
697 772
773void AP_RevealPaintings() { GetState().RevealPaintings(); }
774
698int AP_GetMasteryRequirement() { 775int AP_GetMasteryRequirement() {
699 std::lock_guard state_guard(GetState().state_mutex); 776 std::lock_guard state_guard(GetState().state_mutex);
700 777
@@ -707,6 +784,12 @@ int AP_GetLevel2Requirement() {
707 return GetState().level_2_requirement; 784 return GetState().level_2_requirement;
708} 785}
709 786
787LocationChecks AP_GetLocationsChecks() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
790 return GetState().location_checks;
791}
792
710bool AP_IsLocationVisible(int classification) { 793bool AP_IsLocationVisible(int classification) {
711 std::lock_guard state_guard(GetState().state_mutex); 794 std::lock_guard state_guard(GetState().state_mutex);
712 795
@@ -734,14 +817,16 @@ bool AP_IsLocationVisible(int classification) {
734 return (world_state & classification); 817 return (world_state & classification);
735} 818}
736 819
737VictoryCondition AP_GetVictoryCondition() { 820PanelShuffleMode AP_GetPanelShuffleMode() {
738 std::lock_guard state_guard(GetState().state_mutex); 821 std::lock_guard state_guard(GetState().state_mutex);
739 822
740 return GetState().victory_condition; 823 return GetState().panel_shuffle_mode;
741} 824}
742 825
743bool AP_HasAchievement(const std::string& achievement_name) { 826VictoryCondition AP_GetVictoryCondition() {
744 return GetState().HasAchievement(achievement_name); 827 std::lock_guard state_guard(GetState().state_mutex);
828
829 return GetState().victory_condition;
745} 830}
746 831
747bool AP_HasEarlyColorHallways() { 832bool AP_HasEarlyColorHallways() {
@@ -784,6 +869,8 @@ std::map<int, SunwarpMapping> AP_GetSunwarpMapping() {
784 return GetState().sunwarp_mapping; 869 return GetState().sunwarp_mapping;
785} 870}
786 871
872bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; }
873
787bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 874bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
788 875
789std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { 876std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
@@ -791,3 +878,7 @@ std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
791 878
792 return GetState().player_pos; 879 return GetState().player_pos;
793} 880}
881
882bool AP_IsPanelSolved(int solve_index) {
883 return GetState().IsPanelSolved(solve_index);
884}
diff --git a/src/ap_state.h b/src/ap_state.h index 2da0b8e..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -26,6 +26,8 @@ enum LocationChecks {
26 kPANELSANITY = 2 26 kPANELSANITY = 2
27}; 27};
28 28
29enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 };
30
29enum SunwarpAccess { 31enum SunwarpAccess {
30 kSUNWARP_ACCESS_NORMAL = 0, 32 kSUNWARP_ACCESS_NORMAL = 0,
31 kSUNWARP_ACCESS_DISABLED = 1, 33 kSUNWARP_ACCESS_DISABLED = 1,
@@ -39,6 +41,12 @@ struct SunwarpMapping {
39 int exit_index; 41 int exit_index;
40}; 42};
41 43
44struct ItemState {
45 std::string name;
46 int amount = 0;
47 int index = 0;
48};
49
42void AP_SetTrackerFrame(TrackerFrame* tracker_frame); 50void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
43 51
44void AP_Connect(std::string server, std::string player, std::string password); 52void AP_Connect(std::string server, std::string player, std::string password);
@@ -49,16 +57,11 @@ std::string AP_GetSaveName();
49 57
50bool AP_HasCheckedGameLocation(int location_id); 58bool AP_HasCheckedGameLocation(int location_id);
51 59
52bool AP_HasCheckedHuntPanel(int location_id);
53
54// This doesn't lock the state mutex, for speed, so it must ONLY be called from 60// This doesn't lock the state mutex, for speed, so it must ONLY be called from
55// RecalculateReachability, which is only called from the APState thread anyway. 61// RecalculateReachability, which is only called from the APState thread anyway.
56bool AP_HasItem(int item_id, int quantity = 1); 62bool AP_HasItem(int item_id, int quantity = 1);
57 63
58// This doesn't lock the client mutex because it is ONLY to be called from 64bool AP_HasItemSafe(int item_id, int quantity = 1);
59// RecalculateReachability, which is only called from within a client callback
60// anyway.
61std::string AP_GetItemName(int item_id);
62 65
63DoorShuffleMode AP_GetDoorShuffleMode(); 66DoorShuffleMode AP_GetDoorShuffleMode();
64 67
@@ -76,15 +79,19 @@ std::set<std::string> AP_GetCheckedPaintings();
76 79
77bool AP_IsPaintingChecked(const std::string& painting_id); 80bool AP_IsPaintingChecked(const std::string& painting_id);
78 81
82void AP_RevealPaintings();
83
79int AP_GetMasteryRequirement(); 84int AP_GetMasteryRequirement();
80 85
81int AP_GetLevel2Requirement(); 86int AP_GetLevel2Requirement();
82 87
88LocationChecks AP_GetLocationsChecks();
89
83bool AP_IsLocationVisible(int classification); 90bool AP_IsLocationVisible(int classification);
84 91
85VictoryCondition AP_GetVictoryCondition(); 92PanelShuffleMode AP_GetPanelShuffleMode();
86 93
87bool AP_HasAchievement(const std::string& achievement_name); 94VictoryCondition AP_GetVictoryCondition();
88 95
89bool AP_HasEarlyColorHallways(); 96bool AP_HasEarlyColorHallways();
90 97
@@ -100,8 +107,12 @@ bool AP_IsSunwarpShuffle();
100 107
101std::map<int, SunwarpMapping> AP_GetSunwarpMapping(); 108std::map<int, SunwarpMapping> AP_GetSunwarpMapping();
102 109
110bool AP_IsPostgameShuffle();
111
103bool AP_HasReachedGoal(); 112bool AP_HasReachedGoal();
104 113
105std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); 114std::optional<std::tuple<int, int>> AP_GetPlayerPosition();
106 115
116bool AP_IsPanelSolved(int solve_index);
117
107#endif /* end of include guard: AP_STATE_H_664A4180 */ 118#endif /* end of include guard: AP_STATE_H_664A4180 */
diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 8d6487e..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -7,6 +7,7 @@
7#include "ap_state.h" 7#include "ap_state.h"
8#include "game_data.h" 8#include "game_data.h"
9#include "global.h" 9#include "global.h"
10#include "icons.h"
10#include "tracker_config.h" 11#include "tracker_config.h"
11#include "tracker_panel.h" 12#include "tracker_panel.h"
12#include "tracker_state.h" 13#include "tracker_state.h"
@@ -15,60 +16,54 @@ AreaPopup::AreaPopup(wxWindow* parent, int area_id)
15 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 16 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
16 SetBackgroundStyle(wxBG_STYLE_PAINT); 17 SetBackgroundStyle(wxBG_STYLE_PAINT);
17 18
18 unchecked_eye_ = 19 LoadIcons();
19 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
20 wxBITMAP_TYPE_PNG)
21 .Scale(32, 32));
22 checked_eye_ = wxBitmap(
23 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
24 .Scale(32, 32));
25 20
21 // TODO: This is slow on high-DPI screens.
26 SetScrollRate(5, 5); 22 SetScrollRate(5, 5);
27 23
28 SetBackgroundColour(*wxBLACK); 24 SetBackgroundColour(*wxBLACK);
29 Hide(); 25 Hide();
30 26
31 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); 27 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this);
28 Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this);
32 29
33 UpdateIndicators(); 30 ResetIndicators();
34} 31}
35 32
36void AreaPopup::UpdateIndicators() { 33void AreaPopup::ResetIndicators() {
34 indicators_.clear();
35
37 const MapArea& map_area = GD_GetMapArea(area_id_); 36 const MapArea& map_area = GD_GetMapArea(area_id_);
37 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
38 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
38 39
39 // Start calculating extents. 40 // Start calculating extents.
40 wxMemoryDC mem_dc; 41 wxMemoryDC mem_dc;
41 mem_dc.SetFont(GetFont().Bold()); 42 mem_dc.SetFont(the_font.Bold());
42 wxSize header_extent = mem_dc.GetTextExtent(map_area.name); 43 header_extent_ = mem_dc.GetTextExtent(map_area.name);
43 44
44 int acc_height = header_extent.GetHeight() + 20; 45 int acc_height = header_extent_.GetHeight() + FromDIP(20);
45 int col_width = 0; 46 int col_width = 0;
46 47
47 mem_dc.SetFont(GetFont()); 48 mem_dc.SetFont(the_font);
48
49 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
50
51 std::vector<int> real_locations;
52 49
53 for (int section_id = 0; section_id < map_area.locations.size(); 50 for (int section_id = 0; section_id < map_area.locations.size();
54 section_id++) { 51 section_id++) {
55 const Location& location = map_area.locations.at(section_id); 52 const Location& location = map_area.locations.at(section_id);
56 53 if ((!AP_IsLocationVisible(location.classification) ||
57 if (tracker_panel->IsPanelsMode()) { 54 IsLocationPostgame(location.ap_location_id)) &&
58 if (!location.single_panel) { 55 !(location.hunt &&
59 continue; 56 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
60 } 57 !(location.single_panel &&
61 } else { 58 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) {
62 if (!AP_IsLocationVisible(location.classification) && 59 continue;
63 !(location.hunt && GetTrackerConfig().show_hunt_panels)) {
64 continue;
65 }
66 } 60 }
67 61
68 real_locations.push_back(section_id); 62 indicators_.emplace_back(section_id, kLOCATION, acc_height);
69 63
70 wxSize item_extent = mem_dc.GetTextExtent(location.name); 64 wxSize item_extent = mem_dc.GetTextExtent(location.name);
71 int item_height = std::max(32, item_extent.GetHeight()) + 10; 65 int item_height =
66 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
72 acc_height += item_height; 67 acc_height += item_height;
73 68
74 if (item_extent.GetWidth() > col_width) { 69 if (item_extent.GetWidth() > col_width) {
@@ -76,11 +71,18 @@ void AreaPopup::UpdateIndicators() {
76 } 71 }
77 } 72 }
78 73
79 if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { 74 if (AP_IsPaintingShuffle()) {
80 for (int painting_id : map_area.paintings) { 75 for (int painting_id : map_area.paintings) {
76 if (IsPaintingPostgame(painting_id)) {
77 continue;
78 }
79
80 indicators_.emplace_back(painting_id, kPAINTING, acc_height);
81
81 const PaintingExit& painting = GD_GetPaintingExit(painting_id); 82 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
82 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name. 83 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
83 int item_height = std::max(32, item_extent.GetHeight()) + 10; 84 int item_height =
85 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
84 acc_height += item_height; 86 acc_height += item_height;
85 87
86 if (item_extent.GetWidth() > col_width) { 88 if (item_extent.GetWidth() > col_width) {
@@ -89,80 +91,86 @@ void AreaPopup::UpdateIndicators() {
89 } 91 }
90 } 92 }
91 93
92 int item_width = col_width + 10 + 32; 94 int item_width = col_width + FromDIP(10 + 32);
93 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 95 full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20);
96 full_height_ = acc_height;
94 97
95 Fit(); 98 Fit();
96 SetVirtualSize(full_width, acc_height); 99 SetVirtualSize(full_width_, full_height_);
100
101 UpdateIndicators();
102}
103
104void AreaPopup::UpdateIndicators() {
105 const MapArea& map_area = GD_GetMapArea(area_id_);
106 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
107 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
108
109 rendered_ = wxBitmap(full_width_, full_height_);
97 110
98 rendered_ = wxBitmap(full_width, acc_height); 111 wxMemoryDC mem_dc;
99 mem_dc.SelectObject(rendered_); 112 mem_dc.SelectObject(rendered_);
100 mem_dc.SetPen(*wxTRANSPARENT_PEN); 113 mem_dc.SetPen(*wxTRANSPARENT_PEN);
101 mem_dc.SetBrush(*wxBLACK_BRUSH); 114 mem_dc.SetBrush(*wxBLACK_BRUSH);
102 mem_dc.DrawRectangle({0, 0}, {full_width, acc_height}); 115 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
103 116
104 mem_dc.SetFont(GetFont().Bold()); 117 mem_dc.SetFont(the_font.Bold());
105 mem_dc.SetTextForeground(*wxWHITE); 118 mem_dc.SetTextForeground(*wxWHITE);
106 mem_dc.DrawText(map_area.name, 119 mem_dc.DrawText(map_area.name,
107 {(full_width - header_extent.GetWidth()) / 2, 10}); 120 {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)});
108 121
109 int cur_height = header_extent.GetHeight() + 20; 122 mem_dc.SetFont(the_font);
110 123
111 mem_dc.SetFont(GetFont()); 124 for (const IndicatorInfo& indicator : indicators_) {
125 switch (indicator.type) {
126 case kLOCATION: {
127 const Location& location = map_area.locations.at(indicator.id);
112 128
113 for (int section_id : real_locations) { 129 bool checked = false;
114 const Location& location = map_area.locations.at(section_id); 130 if (IsLocationWinCondition(location)) {
131 checked = AP_HasReachedGoal();
132 } else {
133 checked = AP_HasCheckedGameLocation(location.ap_location_id) ||
134 (location.single_panel &&
135 AP_IsPanelSolved(
136 GD_GetPanel(*location.single_panel).solve_index));
137 }
115 138
116 bool checked = false; 139 const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_;
117 if (IsLocationWinCondition(location)) {
118 checked = AP_HasReachedGoal();
119 } else if (tracker_panel->IsPanelsMode()) {
120 const Panel& panel = GD_GetPanel(*location.single_panel);
121 if (panel.non_counting) {
122 checked = AP_HasCheckedGameLocation(location.ap_location_id);
123 } else {
124 checked = tracker_panel->GetSolvedPanels().contains(panel.nodepath);
125 }
126 } else {
127 checked =
128 AP_HasCheckedGameLocation(location.ap_location_id) ||
129 (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id));
130 }
131
132 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
133 140
134 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 141 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
135 142
136 bool reachable = IsLocationReachable(location.ap_location_id); 143 bool reachable = IsLocationReachable(location.ap_location_id);
137 const wxColour* text_color = reachable ? wxWHITE : wxRED; 144 const wxColour* text_color = reachable ? wxWHITE : wxRED;
138 mem_dc.SetTextForeground(*text_color); 145 mem_dc.SetTextForeground(*text_color);
139 146
140 wxSize item_extent = mem_dc.GetTextExtent(location.name); 147 wxSize item_extent = mem_dc.GetTextExtent(location.name);
141 mem_dc.DrawText( 148 mem_dc.DrawText(
142 location.name, 149 location.name,
143 {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 150 {FromDIP(10 + 32 + 10),
144 151 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
145 cur_height += 10 + 32;
146 }
147 152
148 if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { 153 break;
149 for (int painting_id : map_area.paintings) { 154 }
150 const PaintingExit& painting = GD_GetPaintingExit(painting_id); 155 case kPAINTING: {
156 const PaintingExit& painting = GD_GetPaintingExit(indicator.id);
151 157
152 bool reachable = IsPaintingReachable(painting_id); 158 bool reachable = IsPaintingReachable(indicator.id);
153 const wxColour* text_color = reachable ? wxWHITE : wxRED; 159 const wxColour* text_color = reachable ? wxWHITE : wxRED;
154 mem_dc.SetTextForeground(*text_color); 160 mem_dc.SetTextForeground(*text_color);
155 161
156 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); 162 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id);
157 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; 163 const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_;
158 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 164 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
159 165
160 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name. 166 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
161 mem_dc.DrawText(painting.internal_id, 167 mem_dc.DrawText(
162 {10 + 32 + 10, 168 painting.display_name,
163 cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 169 {FromDIP(10 + 32 + 10),
170 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
164 171
165 cur_height += 10 + 32; 172 break;
173 }
166 } 174 }
167 } 175 }
168} 176}
@@ -174,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) {
174 182
175 event.Skip(); 183 event.Skip();
176} 184}
185
186void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) {
187 LoadIcons();
188 ResetIndicators();
189
190 event.Skip();
191}
192
193void AreaPopup::LoadIcons() {
194 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
195 FromDIP(wxSize{32, 32}));
196 checked_eye_ =
197 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
198 unchecked_owl_ =
199 GetTheIconCache().GetIcon("assets/owl.png", FromDIP(wxSize{32, 32}));
200 checked_owl_ = GetTheIconCache().GetIcon("assets/checked_owl.png",
201 FromDIP(wxSize{32, 32}));
202}
diff --git a/src/area_popup.h b/src/area_popup.h index 00c644d..f8a2355 100644 --- a/src/area_popup.h +++ b/src/area_popup.h
@@ -7,19 +7,53 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <vector>
11
10class AreaPopup : public wxScrolledCanvas { 12class AreaPopup : public wxScrolledCanvas {
11 public: 13 public:
12 AreaPopup(wxWindow* parent, int area_id); 14 AreaPopup(wxWindow* parent, int area_id);
13 15
16 void ResetIndicators();
14 void UpdateIndicators(); 17 void UpdateIndicators();
15 18
19 int GetFullWidth() const { return full_width_; }
20 int GetFullHeight() const { return full_height_; }
21
16 private: 22 private:
23 enum IndicatorType {
24 kLOCATION,
25 kPAINTING,
26 };
27
28 struct IndicatorInfo {
29 // For locations, the id is an index into the map area's locations list.
30 // For paintings, it is a real painting id.
31 int id;
32 IndicatorType type;
33 int y;
34
35 IndicatorInfo(int id, IndicatorType type, int y)
36 : id(id), type(type), y(y) {}
37 };
38
17 void OnPaint(wxPaintEvent& event); 39 void OnPaint(wxPaintEvent& event);
40 void OnDPIChanged(wxDPIChangedEvent& event);
41
42 void LoadIcons();
18 43
19 int area_id_; 44 int area_id_;
20 45
21 wxBitmap unchecked_eye_; 46 const wxBitmap* unchecked_eye_;
22 wxBitmap checked_eye_; 47 const wxBitmap* checked_eye_;
48 const wxBitmap* unchecked_owl_;
49 const wxBitmap* checked_owl_;
50
51 int full_width_ = 0;
52 int full_height_ = 0;
53 wxSize header_extent_;
54
55 std::vector<IndicatorInfo> indicators_;
56
23 wxBitmap rendered_; 57 wxBitmap rendered_;
24}; 58};
25 59
diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp index 64fee98..b55a138 100644 --- a/src/connection_dialog.cpp +++ b/src/connection_dialog.cpp
@@ -4,17 +4,21 @@
4 4
5ConnectionDialog::ConnectionDialog() 5ConnectionDialog::ConnectionDialog()
6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { 6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
7 server_box_ = 7 server_box_ = new wxTextCtrl(
8 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_server, 8 this, -1,
9 wxDefaultPosition, {300, -1}); 9 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server),
10 player_box_ = 10 wxDefaultPosition, FromDIP(wxSize{300, -1}));
11 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_player, 11 player_box_ = new wxTextCtrl(
12 wxDefaultPosition, {300, -1}); 12 this, -1,
13 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_player),
14 wxDefaultPosition, FromDIP(wxSize{300, -1}));
13 password_box_ = new wxTextCtrl( 15 password_box_ = new wxTextCtrl(
14 this, -1, GetTrackerConfig().connection_details.ap_password, 16 this, -1,
15 wxDefaultPosition, {300, -1}); 17 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_password),
18 wxDefaultPosition, FromDIP(wxSize{300, -1}));
16 19
17 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10); 20 wxFlexGridSizer* form_sizer =
21 new wxFlexGridSizer(2, FromDIP(10), FromDIP(10));
18 22
19 form_sizer->Add( 23 form_sizer->Add(
20 new wxStaticText(this, -1, "Server:"), 24 new wxStaticText(this, -1, "Server:"),
@@ -30,17 +34,19 @@ ConnectionDialog::ConnectionDialog()
30 form_sizer->Add(password_box_, wxSizerFlags().Expand()); 34 form_sizer->Add(password_box_, wxSizerFlags().Expand());
31 35
32 history_list_ = new wxListBox(this, -1); 36 history_list_ = new wxListBox(this, -1);
33 for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { 37 for (const ConnectionDetails& details :
38 GetTrackerConfig().connection_history) {
34 wxString display_text; 39 wxString display_text;
35 display_text << details.ap_player; 40 display_text << wxString::FromUTF8(details.ap_player);
36 display_text << " ("; 41 display_text << " (";
37 display_text << details.ap_server; 42 display_text << wxString::FromUTF8(details.ap_server);
38 display_text << ")"; 43 display_text << ")";
39 44
40 history_list_->Append(display_text); 45 history_list_->Append(display_text);
41 } 46 }
42 47
43 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, this); 48 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen,
49 this);
44 50
45 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); 51 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL);
46 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); 52 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand());
@@ -52,7 +58,8 @@ ConnectionDialog::ConnectionDialog()
52 this, -1, "Enter the details to connect to Archipelago."), 58 this, -1, "Enter the details to connect to Archipelago."),
53 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); 59 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
54 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); 60 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand());
55 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Border().Center()); 61 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
62 wxSizerFlags().Border().Center());
56 63
57 SetSizerAndFit(top_sizer); 64 SetSizerAndFit(top_sizer);
58 65
@@ -62,9 +69,10 @@ ConnectionDialog::ConnectionDialog()
62 69
63void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { 70void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) {
64 if (e.IsSelection()) { 71 if (e.IsSelection()) {
65 const ConnectionDetails& details = GetTrackerConfig().connection_history.at(e.GetSelection()); 72 const ConnectionDetails& details =
66 server_box_->SetValue(details.ap_server); 73 GetTrackerConfig().connection_history.at(e.GetSelection());
67 player_box_->SetValue(details.ap_player); 74 server_box_->SetValue(wxString::FromUTF8(details.ap_server));
68 password_box_->SetValue(details.ap_password); 75 player_box_->SetValue(wxString::FromUTF8(details.ap_player));
76 password_box_->SetValue(wxString::FromUTF8(details.ap_password));
69 } 77 }
70} 78}
diff --git a/src/connection_dialog.h b/src/connection_dialog.h index 9fe62fd..ec2ee72 100644 --- a/src/connection_dialog.h +++ b/src/connection_dialog.h
@@ -14,12 +14,12 @@ class ConnectionDialog : public wxDialog {
14 public: 14 public:
15 ConnectionDialog(); 15 ConnectionDialog();
16 16
17 std::string GetServerValue() { return server_box_->GetValue().ToStdString(); } 17 std::string GetServerValue() { return server_box_->GetValue().utf8_string(); }
18 18
19 std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); } 19 std::string GetPlayerValue() { return player_box_->GetValue().utf8_string(); }
20 20
21 std::string GetPasswordValue() { 21 std::string GetPasswordValue() {
22 return password_box_->GetValue().ToStdString(); 22 return password_box_->GetValue().utf8_string();
23 } 23 }
24 24
25 private: 25 private:
diff --git a/src/game_data.cpp b/src/game_data.cpp index 0ac77af..94b9888 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -12,32 +12,6 @@
12 12
13namespace { 13namespace {
14 14
15LingoColor GetColorForString(const std::string &str) {
16 if (str == "black") {
17 return LingoColor::kBlack;
18 } else if (str == "red") {
19 return LingoColor::kRed;
20 } else if (str == "blue") {
21 return LingoColor::kBlue;
22 } else if (str == "yellow") {
23 return LingoColor::kYellow;
24 } else if (str == "orange") {
25 return LingoColor::kOrange;
26 } else if (str == "green") {
27 return LingoColor::kGreen;
28 } else if (str == "gray") {
29 return LingoColor::kGray;
30 } else if (str == "brown") {
31 return LingoColor::kBrown;
32 } else if (str == "purple") {
33 return LingoColor::kPurple;
34 } else {
35 TrackerLog(fmt::format("Invalid color: {}", str));
36
37 return LingoColor::kNone;
38 }
39}
40
41struct GameData { 15struct GameData {
42 std::vector<Room> rooms_; 16 std::vector<Room> rooms_;
43 std::vector<Door> doors_; 17 std::vector<Door> doors_;
@@ -55,10 +29,10 @@ struct GameData {
55 std::map<std::string, int> painting_by_id_; 29 std::map<std::string, int> painting_by_id_;
56 30
57 std::vector<int> door_definition_order_; 31 std::vector<int> door_definition_order_;
58 std::vector<int> room_definition_order_;
59 32
60 std::map<std::string, int> room_by_painting_; 33 std::map<std::string, int> room_by_painting_;
61 std::map<int, int> room_by_sunwarp_; 34 std::map<int, int> room_by_sunwarp_;
35 std::map<int, int> panel_by_solve_index_;
62 36
63 std::vector<int> achievement_panels_; 37 std::vector<int> achievement_panels_;
64 38
@@ -69,6 +43,8 @@ struct GameData {
69 std::map<std::string, int> subway_item_by_painting_; 43 std::map<std::string, int> subway_item_by_painting_;
70 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; 44 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
71 45
46 std::map<int, std::string> item_by_ap_id_;
47
72 bool loaded_area_data_ = false; 48 bool loaded_area_data_ = false;
73 std::set<std::string> malconfigured_areas_; 49 std::set<std::string> malconfigured_areas_;
74 50
@@ -84,7 +60,7 @@ struct GameData {
84 ids_config["special_items"][color_name]) { 60 ids_config["special_items"][color_name]) {
85 std::string input_name = color_name; 61 std::string input_name = color_name;
86 input_name[0] = std::tolower(input_name[0]); 62 input_name[0] = std::tolower(input_name[0]);
87 ap_id_by_color_[GetColorForString(input_name)] = 63 ap_id_by_color_[GetLingoColorForString(input_name)] =
88 ids_config["special_items"][color_name].as<int>(); 64 ids_config["special_items"][color_name].as<int>();
89 } else { 65 } else {
90 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); 66 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
@@ -101,11 +77,20 @@ struct GameData {
101 init_color_id("Brown"); 77 init_color_id("Brown");
102 init_color_id("Gray"); 78 init_color_id("Gray");
103 79
80 if (ids_config["special_items"]) {
81 for (const auto& special_item_it : ids_config["special_items"])
82 {
83 item_by_ap_id_[special_item_it.second.as<int>()] =
84 special_item_it.first.as<std::string>();
85 }
86 }
87
104 rooms_.reserve(lingo_config.size() * 2); 88 rooms_.reserve(lingo_config.size() * 2);
105 89
90 std::vector<int> panel_location_ids;
91
106 for (const auto &room_it : lingo_config) { 92 for (const auto &room_it : lingo_config) {
107 int room_id = AddOrGetRoom(room_it.first.as<std::string>()); 93 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
108 room_definition_order_.push_back(room_id);
109 94
110 for (const auto &entrance_it : room_it.second["entrances"]) { 95 for (const auto &entrance_it : room_it.second["entrances"]) {
111 int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>()); 96 int from_room_id = AddOrGetRoom(entrance_it.first.as<std::string>());
@@ -181,12 +166,12 @@ struct GameData {
181 166
182 if (panel_it.second["colors"]) { 167 if (panel_it.second["colors"]) {
183 if (panel_it.second["colors"].IsScalar()) { 168 if (panel_it.second["colors"].IsScalar()) {
184 panels_[panel_id].colors.push_back(GetColorForString( 169 panels_[panel_id].colors.push_back(GetLingoColorForString(
185 panel_it.second["colors"].as<std::string>())); 170 panel_it.second["colors"].as<std::string>()));
186 } else { 171 } else {
187 for (const auto &color_node : panel_it.second["colors"]) { 172 for (const auto &color_node : panel_it.second["colors"]) {
188 panels_[panel_id].colors.push_back( 173 panels_[panel_id].colors.push_back(
189 GetColorForString(color_node.as<std::string>())); 174 GetLingoColorForString(color_node.as<std::string>()));
190 } 175 }
191 } 176 }
192 } 177 }
@@ -292,10 +277,11 @@ struct GameData {
292 ids_config["panels"][rooms_[room_id].name] && 277 ids_config["panels"][rooms_[room_id].name] &&
293 ids_config["panels"][rooms_[room_id].name] 278 ids_config["panels"][rooms_[room_id].name]
294 [panels_[panel_id].name]) { 279 [panels_[panel_id].name]) {
295 panels_[panel_id].ap_location_id = 280 int location_id = ids_config["panels"][rooms_[room_id].name]
296 ids_config["panels"][rooms_[room_id].name] 281 [panels_[panel_id].name]
297 [panels_[panel_id].name] 282 .as<int>();
298 .as<int>(); 283 panels_[panel_id].ap_location_id = location_id;
284 panel_location_ids.push_back(location_id);
299 } else { 285 } else {
300 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", 286 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
301 rooms_[room_id].name, 287 rooms_[room_id].name,
@@ -361,6 +347,9 @@ struct GameData {
361 ids_config["doors"][rooms_[room_id].name] 347 ids_config["doors"][rooms_[room_id].name]
362 [doors_[door_id].name]["item"] 348 [doors_[door_id].name]["item"]
363 .as<int>(); 349 .as<int>();
350
351 item_by_ap_id_[doors_[door_id].ap_item_id] =
352 doors_[door_id].item_name;
364 } else { 353 } else {
365 TrackerLog(fmt::format("Missing AP item ID for door {} - {}", 354 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
366 rooms_[room_id].name, 355 rooms_[room_id].name,
@@ -377,6 +366,9 @@ struct GameData {
377 doors_[door_id].group_ap_item_id = 366 doors_[door_id].group_ap_item_id =
378 ids_config["door_groups"][doors_[door_id].group_name] 367 ids_config["door_groups"][doors_[door_id].group_name]
379 .as<int>(); 368 .as<int>();
369
370 item_by_ap_id_[doors_[door_id].group_ap_item_id] =
371 doors_[door_id].group_name;
380 } else { 372 } else {
381 TrackerLog(fmt::format("Missing AP item ID for door group {}", 373 TrackerLog(fmt::format("Missing AP item ID for door group {}",
382 doors_[door_id].group_name)); 374 doors_[door_id].group_name));
@@ -440,21 +432,50 @@ struct GameData {
440 int panel_door_id = 432 int panel_door_id =
441 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name); 433 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name);
442 434
435 std::map<std::string, std::vector<std::string>> panel_per_room;
436 int num_panels = 0;
443 for (const auto &panel_node : panel_door_it.second["panels"]) { 437 for (const auto &panel_node : panel_door_it.second["panels"]) {
438 num_panels++;
439
444 int panel_id = -1; 440 int panel_id = -1;
445 441
446 if (panel_node.IsScalar()) { 442 if (panel_node.IsScalar()) {
447 panel_id = AddOrGetPanel(rooms_[room_id].name, 443 panel_id = AddOrGetPanel(rooms_[room_id].name,
448 panel_node.as<std::string>()); 444 panel_node.as<std::string>());
445
446 panel_per_room[rooms_[room_id].name].push_back(
447 panel_node.as<std::string>());
449 } else { 448 } else {
450 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(), 449 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(),
451 panel_node["panel"].as<std::string>()); 450 panel_node["panel"].as<std::string>());
451
452 panel_per_room[panel_node["room"].as<std::string>()].push_back(
453 panel_node["panel"].as<std::string>());
452 } 454 }
453 455
454 Panel &panel = panels_[panel_id]; 456 Panel &panel = panels_[panel_id];
455 panel.panel_door = panel_door_id; 457 panel.panel_door = panel_door_id;
456 } 458 }
457 459
460 if (panel_door_it.second["item_name"]) {
461 panel_doors_[panel_door_id].item_name =
462 panel_door_it.second["item_name"].as<std::string>();
463 } else {
464 std::vector<std::string> room_strs;
465 for (const auto &[room_str, panels_str] : panel_per_room) {
466 room_strs.push_back(fmt::format(
467 "{} - {}", room_str, hatkirby::implode(panels_str, ", ")));
468 }
469
470 if (num_panels == 1) {
471 panel_doors_[panel_door_id].item_name =
472 fmt::format("{} (Panel)", room_strs[0]);
473 } else {
474 panel_doors_[panel_door_id].item_name = fmt::format(
475 "{} (Panels)", hatkirby::implode(room_strs, " and "));
476 }
477 }
478
458 if (ids_config["panel_doors"] && 479 if (ids_config["panel_doors"] &&
459 ids_config["panel_doors"][rooms_[room_id].name] && 480 ids_config["panel_doors"][rooms_[room_id].name] &&
460 ids_config["panel_doors"][rooms_[room_id].name] 481 ids_config["panel_doors"][rooms_[room_id].name]
@@ -462,6 +483,9 @@ struct GameData {
462 panel_doors_[panel_door_id].ap_item_id = 483 panel_doors_[panel_door_id].ap_item_id =
463 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name] 484 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name]
464 .as<int>(); 485 .as<int>();
486
487 item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] =
488 panel_doors_[panel_door_id].item_name;
465 } else { 489 } else {
466 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}", 490 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}",
467 rooms_[room_id].name, panel_door_name)); 491 rooms_[room_id].name, panel_door_name));
@@ -475,6 +499,9 @@ struct GameData {
475 ids_config["panel_groups"][panel_group]) { 499 ids_config["panel_groups"][panel_group]) {
476 panel_doors_[panel_door_id].group_ap_item_id = 500 panel_doors_[panel_door_id].group_ap_item_id =
477 ids_config["panel_groups"][panel_group].as<int>(); 501 ids_config["panel_groups"][panel_group].as<int>();
502
503 item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] =
504 panel_group;
478 } else { 505 } else {
479 TrackerLog(fmt::format( 506 TrackerLog(fmt::format(
480 "Missing AP item ID for panel door group {}", panel_group)); 507 "Missing AP item ID for panel door group {}", panel_group));
@@ -490,6 +517,13 @@ struct GameData {
490 PaintingExit &painting_exit = paintings_[painting_id]; 517 PaintingExit &painting_exit = paintings_[painting_id];
491 painting_exit.room = room_id; 518 painting_exit.room = room_id;
492 519
520 if (painting["display_name"]) {
521 painting_exit.display_name =
522 painting["display_name"].as<std::string>();
523 } else {
524 painting_exit.display_name = painting_exit.internal_id;
525 }
526
493 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && 527 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
494 (!painting["disable"] || !painting["disable"].as<bool>())) { 528 (!painting["disable"] || !painting["disable"].as<bool>())) {
495 painting_exit.entrance = true; 529 painting_exit.entrance = true;
@@ -531,6 +565,8 @@ struct GameData {
531 ids_config["progression"][progressive_item_name]) { 565 ids_config["progression"][progressive_item_name]) {
532 progressive_item_id = 566 progressive_item_id =
533 ids_config["progression"][progressive_item_name].as<int>(); 567 ids_config["progression"][progressive_item_name].as<int>();
568
569 item_by_ap_id_[progressive_item_id] = progressive_item_name;
534 } else { 570 } else {
535 TrackerLog(fmt::format("Missing AP item ID for progressive item {}", 571 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
536 progressive_item_name)); 572 progressive_item_name));
@@ -582,6 +618,21 @@ struct GameData {
582 } 618 }
583 } 619 }
584 620
621 // Determine the panel solve indices from the sorted location IDs.
622 std::sort(panel_location_ids.begin(), panel_location_ids.end());
623
624 std::map<int, int> solve_index_by_location_id;
625 for (int i = 0; i < panel_location_ids.size(); i++) {
626 solve_index_by_location_id[panel_location_ids[i]] = i;
627 }
628
629 for (Panel &panel : panels_) {
630 if (panel.ap_location_id != -1) {
631 panel.solve_index = solve_index_by_location_id[panel.ap_location_id];
632 panel_by_solve_index_[panel.solve_index] = panel.id;
633 }
634 }
635
585 map_areas_.reserve(areas_config.size()); 636 map_areas_.reserve(areas_config.size());
586 637
587 std::map<std::string, int> fold_areas; 638 std::map<std::string, int> fold_areas;
@@ -602,7 +653,7 @@ struct GameData {
602 // Only locations for the panels are kept here. 653 // Only locations for the panels are kept here.
603 std::map<std::string, std::tuple<int, int>> locations_by_name; 654 std::map<std::string, std::tuple<int, int>> locations_by_name;
604 655
605 for (const Panel &panel : panels_) { 656 for (Panel &panel : panels_) {
606 int room_id = panel.room; 657 int room_id = panel.room;
607 std::string room_name = rooms_[room_id].name; 658 std::string room_name = rooms_[room_id].name;
608 659
@@ -618,6 +669,8 @@ struct GameData {
618 area_name = location_name.substr(0, divider_pos); 669 area_name = location_name.substr(0, divider_pos);
619 section_name = location_name.substr(divider_pos + 3); 670 section_name = location_name.substr(divider_pos + 3);
620 } 671 }
672 } else {
673 panel.location_name = location_name;
621 } 674 }
622 675
623 if (fold_areas.count(area_name)) { 676 if (fold_areas.count(area_name)) {
@@ -716,7 +769,8 @@ struct GameData {
716 MapArea &map_area = map_areas_[area_id]; 769 MapArea &map_area = map_areas_[area_id];
717 770
718 for (int painting_id : room.paintings) { 771 for (int painting_id : room.paintings) {
719 const PaintingExit &painting_obj = paintings_.at(painting_id); 772 PaintingExit &painting_obj = paintings_.at(painting_id);
773 painting_obj.map_area = area_id;
720 if (painting_obj.entrance) { 774 if (painting_obj.entrance) {
721 map_area.paintings.push_back(painting_id); 775 map_area.paintings.push_back(painting_id);
722 } 776 }
@@ -724,31 +778,6 @@ struct GameData {
724 } 778 }
725 } 779 }
726 780
727 // As a workaround for a generator bug in 0.5.1, we are going to remove the
728 // panel door requirement on panels that are defined earlier in the file than
729 // the panel door is. This results in logic that matches the generator, even
730 // if it is not true to how the game should work. This will be reverted once
731 // the logic bug is fixed and released.
732 // See: https://github.com/ArchipelagoMW/Archipelago/pull/4342
733 for (Panel& panel : panels_) {
734 if (panel.panel_door == -1) {
735 continue;
736 }
737 const PanelDoor &panel_door = panel_doors_[panel.panel_door];
738 for (int room_id : room_definition_order_) {
739 if (room_id == panel_door.room) {
740 // The panel door was defined first (or at the same time as the panel),
741 // so we're good.
742 break;
743 } else if (room_id == panel.room) {
744 // The panel was defined first, so we have to pretend the panel door is
745 // not required for this panel.
746 panel.panel_door = -1;
747 break;
748 }
749 }
750 }
751
752 // Report errors. 781 // Report errors.
753 for (const std::string &area : malconfigured_areas_) { 782 for (const std::string &area : malconfigured_areas_) {
754 TrackerLog(fmt::format("Area data not found for: {}", area)); 783 TrackerLog(fmt::format("Area data not found for: {}", area));
@@ -768,13 +797,10 @@ struct GameData {
768 subway_it["door"].as<std::string>()); 797 subway_it["door"].as<std::string>());
769 } 798 }
770 799
771 if (subway_it["paintings"]) { 800 if (subway_it["painting"]) {
772 for (const auto &painting_it : subway_it["paintings"]) { 801 std::string painting_id = subway_it["painting"].as<std::string>();
773 std::string painting_id = painting_it.as<std::string>(); 802 subway_item.painting = painting_id;
774 803 subway_item_by_painting_[painting_id] = subway_item.id;
775 subway_item.paintings.push_back(painting_id);
776 subway_item_by_painting_[painting_id] = subway_item.id;
777 }
778 } 804 }
779 805
780 if (subway_it["tags"]) { 806 if (subway_it["tags"]) {
@@ -821,6 +847,10 @@ struct GameData {
821 subway_item.special = subway_it["special"].as<std::string>(); 847 subway_item.special = subway_it["special"].as<std::string>();
822 } 848 }
823 849
850 if (subway_it["tilted"]) {
851 subway_item.tilted = subway_it["tilted"].as<bool>();
852 }
853
824 subway_items_.push_back(subway_item); 854 subway_items_.push_back(subway_item);
825 } 855 }
826 856
@@ -880,7 +910,7 @@ struct GameData {
880 if (!panel_doors_by_id_.count(full_name)) { 910 if (!panel_doors_by_id_.count(full_name)) {
881 int panel_door_id = panel_doors_.size(); 911 int panel_door_id = panel_doors_.size();
882 panel_doors_by_id_[full_name] = panel_door_id; 912 panel_doors_by_id_[full_name] = panel_door_id;
883 panel_doors_.push_back({.room = AddOrGetRoom(room)}); 913 panel_doors_.push_back({});
884 } 914 }
885 915
886 return panel_doors_by_id_[full_name]; 916 return panel_doors_by_id_[full_name];
@@ -953,6 +983,14 @@ const Panel &GD_GetPanel(int panel_id) {
953 return GetState().panels_.at(panel_id); 983 return GetState().panels_.at(panel_id);
954} 984}
955 985
986int GD_GetPanelBySolveIndex(int solve_index) {
987 return GetState().panel_by_solve_index_.at(solve_index);
988}
989
990const std::vector<PaintingExit> &GD_GetPaintings() {
991 return GetState().paintings_;
992}
993
956const PaintingExit &GD_GetPaintingExit(int painting_id) { 994const PaintingExit &GD_GetPaintingExit(int painting_id) {
957 return GetState().paintings_.at(painting_id); 995 return GetState().paintings_.at(painting_id);
958} 996}
@@ -995,3 +1033,38 @@ std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
995int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 1033int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
996 return GetState().subway_item_by_sunwarp_.at(sunwarp); 1034 return GetState().subway_item_by_sunwarp_.at(sunwarp);
997} 1035}
1036
1037std::string GD_GetItemName(int id) {
1038 auto it = GetState().item_by_ap_id_.find(id);
1039 if (it != GetState().item_by_ap_id_.end()) {
1040 return it->second;
1041 } else {
1042 return "Unknown";
1043 }
1044}
1045
1046LingoColor GetLingoColorForString(const std::string &str) {
1047 if (str == "black") {
1048 return LingoColor::kBlack;
1049 } else if (str == "red") {
1050 return LingoColor::kRed;
1051 } else if (str == "blue") {
1052 return LingoColor::kBlue;
1053 } else if (str == "yellow") {
1054 return LingoColor::kYellow;
1055 } else if (str == "orange") {
1056 return LingoColor::kOrange;
1057 } else if (str == "green") {
1058 return LingoColor::kGreen;
1059 } else if (str == "gray") {
1060 return LingoColor::kGray;
1061 } else if (str == "brown") {
1062 return LingoColor::kBrown;
1063 } else if (str == "purple") {
1064 return LingoColor::kPurple;
1065 } else {
1066 TrackerLog(fmt::format("Invalid color: {}", str));
1067
1068 return LingoColor::kNone;
1069 }
1070}
diff --git a/src/game_data.h b/src/game_data.h index 31a1e78..ac911e5 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -57,6 +57,7 @@ struct Panel {
57 int ap_location_id = -1; 57 int ap_location_id = -1;
58 bool hunt = false; 58 bool hunt = false;
59 int panel_door = -1; 59 int panel_door = -1;
60 int solve_index = -1;
60}; 61};
61 62
62struct ProgressiveRequirement { 63struct ProgressiveRequirement {
@@ -85,10 +86,10 @@ struct Door {
85}; 86};
86 87
87struct PanelDoor { 88struct PanelDoor {
88 int room;
89 int ap_item_id = -1; 89 int ap_item_id = -1;
90 int group_ap_item_id = -1; 90 int group_ap_item_id = -1;
91 std::vector<ProgressiveRequirement> progressives; 91 std::vector<ProgressiveRequirement> progressives;
92 std::string item_name;
92}; 93};
93 94
94struct Exit { 95struct Exit {
@@ -102,8 +103,10 @@ struct PaintingExit {
102 int id; 103 int id;
103 int room; 104 int room;
104 std::string internal_id; 105 std::string internal_id;
106 std::string display_name;
105 std::optional<int> door; 107 std::optional<int> door;
106 bool entrance = false; 108 bool entrance = false;
109 int map_area;
107}; 110};
108 111
109struct Room { 112struct Room {
@@ -154,8 +157,9 @@ struct SubwayItem {
154 int id; 157 int id;
155 int x; 158 int x;
156 int y; 159 int y;
160 bool tilted = false;
157 std::optional<int> door; 161 std::optional<int> door;
158 std::vector<std::string> paintings; 162 std::optional<std::string> painting;
159 std::vector<std::string> tags; // 2-way teleports 163 std::vector<std::string> tags; // 2-way teleports
160 std::vector<std::string> entrances; // teleport entrances 164 std::vector<std::string> entrances; // teleport entrances
161 std::vector<std::string> exits; // teleport exits 165 std::vector<std::string> exits; // teleport exits
@@ -173,7 +177,9 @@ const std::vector<Door>& GD_GetDoors();
173const Door& GD_GetDoor(int door_id); 177const Door& GD_GetDoor(int door_id);
174int GD_GetDoorByName(const std::string& name); 178int GD_GetDoorByName(const std::string& name);
175const Panel& GD_GetPanel(int panel_id); 179const Panel& GD_GetPanel(int panel_id);
180int GD_GetPanelBySolveIndex(int solve_index);
176const PanelDoor& GD_GetPanelDoor(int panel_door_id); 181const PanelDoor& GD_GetPanelDoor(int panel_door_id);
182const std::vector<PaintingExit>& GD_GetPaintings();
177const PaintingExit& GD_GetPaintingExit(int painting_id); 183const PaintingExit& GD_GetPaintingExit(int painting_id);
178int GD_GetPaintingByName(const std::string& name); 184int GD_GetPaintingByName(const std::string& name);
179const std::vector<int>& GD_GetAchievementPanels(); 185const std::vector<int>& GD_GetAchievementPanels();
@@ -184,5 +190,8 @@ const std::vector<SubwayItem>& GD_GetSubwayItems();
184const SubwayItem& GD_GetSubwayItem(int id); 190const SubwayItem& GD_GetSubwayItem(int id);
185std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); 191std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
186int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 192int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
193std::string GD_GetItemName(int id);
194
195LingoColor GetLingoColorForString(const std::string& str);
187 196
188#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 197#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/global.cpp b/src/global.cpp index 1eb3f8d..63f4a19 100644 --- a/src/global.cpp +++ b/src/global.cpp
@@ -26,17 +26,19 @@ std::string GetAbsolutePath(std::string_view path) {
26 return (GetExecutableDirectory() / path).string(); 26 return (GetExecutableDirectory() / path).string();
27} 27}
28 28
29bool IsLocationWinCondition(const Location& location) { 29std::string GetWinCondition() {
30 switch (AP_GetVictoryCondition()) { 30 switch (AP_GetVictoryCondition()) {
31 case kTHE_END: 31 case kTHE_END:
32 return location.ap_location_name == 32 return "Orange Tower Seventh Floor - THE END";
33 "Orange Tower Seventh Floor - THE END";
34 case kTHE_MASTER: 33 case kTHE_MASTER:
35 return location.ap_location_name == 34 return "Orange Tower Seventh Floor - THE MASTER";
36 "Orange Tower Seventh Floor - THE MASTER";
37 case kLEVEL_2: 35 case kLEVEL_2:
38 return location.ap_location_name == "Second Room - LEVEL 2"; 36 return "Second Room - LEVEL 2";
39 case kPILGRIMAGE: 37 case kPILGRIMAGE:
40 return location.ap_location_name == "Pilgrim Antechamber - PILGRIM"; 38 return "Pilgrim Antechamber - PILGRIM";
41 } 39 }
42} 40}
41
42bool IsLocationWinCondition(const Location& location) {
43 return location.ap_location_name == GetWinCondition();
44}
diff --git a/src/global.h b/src/global.h index 31ebde3..bdfa7ae 100644 --- a/src/global.h +++ b/src/global.h
@@ -10,6 +10,8 @@ const std::filesystem::path& GetExecutableDirectory();
10 10
11std::string GetAbsolutePath(std::string_view path); 11std::string GetAbsolutePath(std::string_view path);
12 12
13std::string GetWinCondition();
14
13bool IsLocationWinCondition(const Location& location); 15bool IsLocationWinCondition(const Location& location);
14 16
15#endif /* end of include guard: GLOBAL_H_44945DBA */ 17#endif /* end of include guard: GLOBAL_H_44945DBA */
diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp deleted file mode 100644 index 152b9ef..0000000 --- a/src/godot_variant.cpp +++ /dev/null
@@ -1,84 +0,0 @@
1// Godot save decoder algorithm by Chris Souvey.
2
3#include "godot_variant.h"
4
5#include <algorithm>
6#include <charconv>
7#include <cstddef>
8#include <cstdint>
9#include <fstream>
10#include <string>
11#include <tuple>
12#include <variant>
13#include <vector>
14
15namespace {
16
17uint16_t ReadUint16(std::basic_istream<char>& stream) {
18 uint16_t result;
19 stream.read(reinterpret_cast<char*>(&result), 2);
20 return result;
21}
22
23uint32_t ReadUint32(std::basic_istream<char>& stream) {
24 uint32_t result;
25 stream.read(reinterpret_cast<char*>(&result), 4);
26 return result;
27}
28
29GodotVariant ParseVariant(std::basic_istream<char>& stream) {
30 uint16_t type = ReadUint16(stream);
31 stream.ignore(2);
32
33 switch (type) {
34 case 1: {
35 // bool
36 bool boolval = (ReadUint32(stream) == 1);
37 return {boolval};
38 }
39 case 15: {
40 // nodepath
41 uint32_t name_length = ReadUint32(stream) & 0x7fffffff;
42 uint32_t subname_length = ReadUint32(stream) & 0x7fffffff;
43 uint32_t flags = ReadUint32(stream);
44
45 std::vector<std::string> result;
46 for (size_t i = 0; i < name_length + subname_length; i++) {
47 uint32_t char_length = ReadUint32(stream);
48 uint32_t padded_length = (char_length % 4 == 0)
49 ? char_length
50 : (char_length + 4 - (char_length % 4));
51 std::vector<char> next_bytes(padded_length);
52 stream.read(next_bytes.data(), padded_length);
53 std::string next_piece;
54 std::copy(next_bytes.begin(),
55 std::next(next_bytes.begin(), char_length),
56 std::back_inserter(next_piece));
57 result.push_back(next_piece);
58 }
59
60 return {result};
61 }
62 case 19: {
63 // array
64 uint32_t length = ReadUint32(stream) & 0x7fffffff;
65 std::vector<GodotVariant> result;
66 for (size_t i = 0; i < length; i++) {
67 result.push_back(ParseVariant(stream));
68 }
69 return {result};
70 }
71 default: {
72 // eh
73 return {std::monostate{}};
74 }
75 }
76}
77
78} // namespace
79
80GodotVariant ParseGodotFile(std::string filename) {
81 std::ifstream file_stream(filename, std::ios_base::binary);
82 file_stream.ignore(4);
83 return ParseVariant(file_stream);
84}
diff --git a/src/godot_variant.h b/src/godot_variant.h deleted file mode 100644 index 620e569..0000000 --- a/src/godot_variant.h +++ /dev/null
@@ -1,28 +0,0 @@
1#ifndef GODOT_VARIANT_H_ED7F2EB6
2#define GODOT_VARIANT_H_ED7F2EB6
3
4#include <string>
5#include <variant>
6#include <vector>
7
8struct GodotVariant {
9 using value_type = std::variant<std::monostate, bool, std::vector<std::string>, std::vector<GodotVariant>>;
10
11 value_type value;
12
13 GodotVariant(value_type v) : value(v) {}
14
15 bool AsBool() const { return std::get<bool>(value); }
16
17 const std::vector<std::string>& AsNodePath() const {
18 return std::get<std::vector<std::string>>(value);
19 }
20
21 const std::vector<GodotVariant>& AsArray() const {
22 return std::get<std::vector<GodotVariant>>(value);
23 }
24};
25
26GodotVariant ParseGodotFile(std::string filename);
27
28#endif /* end of include guard: GODOT_VARIANT_H_ED7F2EB6 */
diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..87ba037 --- /dev/null +++ b/src/icons.cpp
@@ -0,0 +1,22 @@
1#include "icons.h"
2
3#include "global.h"
4
5const wxBitmap* IconCache::GetIcon(const std::string& filename, wxSize size) {
6 std::tuple<std::string, int, int> key = {filename, size.x, size.y};
7
8 if (!icons_.count(key)) {
9 icons_[key] =
10 wxBitmap(wxImage(GetAbsolutePath(filename).c_str(),
11 wxBITMAP_TYPE_PNG)
12 .Scale(size.x, size.y));
13 }
14
15 return &icons_.at(key);
16}
17
18static IconCache* ICON_CACHE_INSTANCE = nullptr;
19
20void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; }
21
22IconCache& GetTheIconCache() { return *ICON_CACHE_INSTANCE; }
diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 0000000..23dca2a --- /dev/null +++ b/src/icons.h
@@ -0,0 +1,25 @@
1#ifndef ICONS_H_B95159A6
2#define ICONS_H_B95159A6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <map>
11#include <string>
12#include <tuple>
13
14class IconCache {
15 public:
16 const wxBitmap* GetIcon(const std::string& filename, wxSize size);
17
18 private:
19 std::map<std::tuple<std::string, int, int>, wxBitmap> icons_;
20};
21
22void SetTheIconCache(IconCache* instance);
23IconCache& GetTheIconCache();
24
25#endif /* end of include guard: ICONS_H_B95159A6 */
diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp index f17c2d8..6763b7f 100644 --- a/src/ipc_dialog.cpp +++ b/src/ipc_dialog.cpp
@@ -12,13 +12,14 @@ IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") {
12 address_value = GetTrackerConfig().ipc_address; 12 address_value = GetTrackerConfig().ipc_address;
13 } 13 }
14 14
15 address_box_ = 15 address_box_ = new wxTextCtrl(this, -1, wxString::FromUTF8(address_value),
16 new wxTextCtrl(this, -1, address_value, wxDefaultPosition, {300, -1}); 16 wxDefaultPosition, FromDIP(wxSize{300, -1}));
17 17
18 wxButton* reset_button = new wxButton(this, -1, "Use Default"); 18 wxButton* reset_button = new wxButton(this, -1, "Use Default");
19 reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this); 19 reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this);
20 20
21 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(3, 10, 10); 21 wxFlexGridSizer* form_sizer =
22 new wxFlexGridSizer(3, FromDIP(10), FromDIP(10));
22 form_sizer->Add( 23 form_sizer->Add(
23 new wxStaticText(this, -1, "Address:"), 24 new wxStaticText(this, -1, "Address:"),
24 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT)); 25 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h index 1caed01..a8c4512 100644 --- a/src/ipc_dialog.h +++ b/src/ipc_dialog.h
@@ -13,7 +13,7 @@ class IpcDialog : public wxDialog {
13 public: 13 public:
14 IpcDialog(); 14 IpcDialog();
15 15
16 std::string GetIpcAddress() { return address_box_->GetValue().ToStdString(); } 16 std::string GetIpcAddress() { return address_box_->GetValue().utf8_string(); }
17 17
18 private: 18 private:
19 void OnResetClicked(wxCommandEvent& event); 19 void OnResetClicked(wxCommandEvent& event);
diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp index 1f8d286..6e2a440 100644 --- a/src/ipc_state.cpp +++ b/src/ipc_state.cpp
@@ -39,7 +39,6 @@ struct IPCState {
39 std::string game_ap_user; 39 std::string game_ap_user;
40 40
41 std::optional<std::tuple<int, int>> player_position; 41 std::optional<std::tuple<int, int>> player_position;
42 std::set<std::string> solved_panels;
43 42
44 // Thread state 43 // Thread state
45 std::unique_ptr<wswrap::WS> ws; 44 std::unique_ptr<wswrap::WS> ws;
@@ -103,12 +102,6 @@ struct IPCState {
103 return player_position; 102 return player_position;
104 } 103 }
105 104
106 std::set<std::string> GetSolvedPanels() {
107 std::lock_guard state_guard(state_mutex);
108
109 return solved_panels;
110 }
111
112 private: 105 private:
113 void Thread() { 106 void Thread() {
114 for (;;) { 107 for (;;) {
@@ -134,7 +127,6 @@ struct IPCState {
134 game_ap_user.clear(); 127 game_ap_user.clear();
135 128
136 player_position = std::nullopt; 129 player_position = std::nullopt;
137 solved_panels.clear();
138 130
139 if (address.empty()) { 131 if (address.empty()) {
140 initialized = false; 132 initialized = false;
@@ -273,7 +265,6 @@ struct IPCState {
273 265
274 slot_matches = false; 266 slot_matches = false;
275 player_position = std::nullopt; 267 player_position = std::nullopt;
276 solved_panels.clear();
277 } 268 }
278 } 269 }
279 270
@@ -313,15 +304,7 @@ struct IPCState {
313 player_position = 304 player_position =
314 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]); 305 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);
315 306
316 tracker_frame->RedrawPosition(); 307 tracker_frame->UpdateIndicators(StateUpdate{.player_position = true});
317 } else if (msg["cmd"] == "SolvePanels") {
318 std::lock_guard state_guard(state_mutex);
319
320 for (std::string panel : msg["panels"]) {
321 solved_panels.insert(std::move(panel));
322 }
323
324 tracker_frame->UpdateIndicators(kUPDATE_ONLY_PANELS);
325 } 308 }
326 } 309 }
327 310
@@ -382,7 +365,3 @@ bool IPC_IsConnected() { return GetState().IsConnected(); }
382std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { 365std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
383 return GetState().GetPlayerPosition(); 366 return GetState().GetPlayerPosition();
384} 367}
385
386std::set<std::string> IPC_GetSolvedPanels() {
387 return GetState().GetSolvedPanels();
388}
diff --git a/src/ipc_state.h b/src/ipc_state.h index 7c9d68d..0e6fa51 100644 --- a/src/ipc_state.h +++ b/src/ipc_state.h
@@ -20,6 +20,4 @@ bool IPC_IsConnected();
20 20
21std::optional<std::tuple<int, int>> IPC_GetPlayerPosition(); 21std::optional<std::tuple<int, int>> IPC_GetPlayerPosition();
22 22
23std::set<std::string> IPC_GetSolvedPanels();
24
25#endif /* end of include guard: IPC_STATE_H_6B3B0958 */ 23#endif /* end of include guard: IPC_STATE_H_6B3B0958 */
diff --git a/src/items_pane.cpp b/src/items_pane.cpp new file mode 100644 index 0000000..055eec0 --- /dev/null +++ b/src/items_pane.cpp
@@ -0,0 +1,145 @@
1#include "items_pane.h"
2
3#include <map>
4
5namespace {
6
7enum SortInstruction {
8 SI_NONE = 0,
9 SI_ASC = 1 << 0,
10 SI_DESC = 1 << 1,
11 SI_NAME = 1 << 2,
12 SI_AMOUNT = 1 << 3,
13 SI_ORDER = 1 << 4,
14};
15
16inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) {
17 return static_cast<SortInstruction>(static_cast<int>(lhs) |
18 static_cast<int>(rhs));
19}
20
21template <typename T>
22int ItemCompare(const T& lhs, const T& rhs, bool ascending) {
23 if (lhs < rhs) {
24 return ascending ? -1 : 1;
25 } else if (lhs > rhs) {
26 return ascending ? 1 : -1;
27 } else {
28 return 0;
29 }
30}
31
32int wxCALLBACK RowCompare(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) {
33 const ItemState& lhs = *reinterpret_cast<const ItemState*>(item1);
34 const ItemState& rhs = *reinterpret_cast<const ItemState*>(item2);
35 SortInstruction instruction = static_cast<SortInstruction>(sortData);
36
37 bool ascending = (instruction & SI_ASC) != 0;
38 if ((instruction & SI_NAME) != 0) {
39 return ItemCompare(lhs.name, rhs.name, ascending);
40 } else if ((instruction & SI_AMOUNT) != 0) {
41 return ItemCompare(lhs.amount, rhs.amount, ascending);
42 } else if ((instruction & SI_ORDER) != 0) {
43 return ItemCompare(lhs.index, rhs.index, ascending);
44 } else {
45 return 0;
46 }
47}
48
49} // namespace
50
51ItemsPane::ItemsPane(wxWindow* parent)
52 : wxListView(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
53 wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_HRULES) {
54 AppendColumn("Item", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
55 AppendColumn("Amount", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
56 AppendColumn("Order", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
57
58 Bind(wxEVT_LIST_COL_CLICK, &ItemsPane::OnColClick, this);
59 Bind(wxEVT_DPI_CHANGED, &ItemsPane::OnDPIChanged, this);
60}
61
62void ItemsPane::ResetIndicators() {
63 DeleteAllItems();
64 items_.clear();
65}
66
67void ItemsPane::UpdateIndicators(const std::vector<ItemState>& items) {
68 std::map<std::string, ItemState> items_by_name;
69
70 for (const ItemState& item : items) {
71 items_by_name[item.name] = item;
72 }
73
74 for (int i = 0; i < GetItemCount(); i++) {
75 std::string item_name = GetItemText(i).utf8_string();
76 auto it = items_by_name.find(item_name);
77
78 if (it != items_by_name.end()) {
79 SetItem(i, 1, std::to_string(it->second.amount));
80 SetItem(i, 2, std::to_string(it->second.index));
81
82 *reinterpret_cast<ItemState*>(GetItemData(i)) = it->second;
83
84 items_by_name.erase(item_name);
85 }
86 }
87
88 for (const auto& [name, item] : items_by_name) {
89 int i = InsertItem(GetItemCount(), name);
90 SetItem(i, 1, std::to_string(item.amount));
91 SetItem(i, 2, std::to_string(item.index));
92
93 auto item_ptr = std::make_unique<ItemState>(item);
94 SetItemPtrData(i, reinterpret_cast<wxUIntPtr>(item_ptr.get()));
95 items_.push_back(std::move(item_ptr));
96 }
97
98 SetColumnWidth(0, wxLIST_AUTOSIZE);
99 SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
100 SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER);
101
102 if (GetSortIndicator() != -1) {
103 DoSort(GetSortIndicator(), IsAscendingSortIndicator());
104 }
105}
106
107void ItemsPane::OnColClick(wxListEvent& event) {
108 int col = event.GetColumn();
109 if (col == -1) {
110 return;
111 }
112
113 bool ascending = GetUpdatedAscendingSortIndicator(col);
114
115 DoSort(col, ascending);
116}
117
118void ItemsPane::OnDPIChanged(wxDPIChangedEvent& event) {
119 SetColumnWidth(0, wxLIST_AUTOSIZE);
120 SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
121 SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER);
122
123 event.Skip();
124}
125
126void ItemsPane::DoSort(int col, bool ascending) {
127 SortInstruction instruction = SI_NONE;
128 if (ascending) {
129 instruction = instruction | SI_ASC;
130 } else {
131 instruction = instruction | SI_DESC;
132 }
133
134 if (col == 0) {
135 instruction = instruction | SI_NAME;
136 } else if (col == 1) {
137 instruction = instruction | SI_AMOUNT;
138 } else if (col == 2) {
139 instruction = instruction | SI_ORDER;
140 }
141
142 if (SortItems(RowCompare, instruction)) {
143 ShowSortIndicator(col, ascending);
144 }
145}
diff --git a/src/items_pane.h b/src/items_pane.h new file mode 100644 index 0000000..aa09c49 --- /dev/null +++ b/src/items_pane.h
@@ -0,0 +1,33 @@
1#ifndef ITEMS_PANE_H_EB637EE3
2#define ITEMS_PANE_H_EB637EE3
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <wx/listctrl.h>
11
12#include <memory>
13#include <vector>
14
15#include "ap_state.h"
16
17class ItemsPane : public wxListView {
18 public:
19 explicit ItemsPane(wxWindow* parent);
20
21 void ResetIndicators();
22 void UpdateIndicators(const std::vector<ItemState>& items);
23
24 private:
25 void OnColClick(wxListEvent& event);
26 void OnDPIChanged(wxDPIChangedEvent& event);
27
28 void DoSort(int col, bool ascending);
29
30 std::vector<std::unique_ptr<ItemState>> items_;
31};
32
33#endif /* end of include guard: ITEMS_PANE_H_EB637EE3 */
diff --git a/src/log_dialog.cpp b/src/log_dialog.cpp new file mode 100644 index 0000000..3f0a8ad --- /dev/null +++ b/src/log_dialog.cpp
@@ -0,0 +1,37 @@
1#include "log_dialog.h"
2
3#include "logger.h"
4
5wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent);
6
7LogDialog::LogDialog(wxWindow* parent)
8 : wxDialog(parent, wxID_ANY, "Debug Log", wxDefaultPosition, wxDefaultSize,
9 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
10 SetSize(FromDIP(wxSize{512, 280}));
11
12 text_area_ =
13 new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize,
14 wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP);
15 text_area_->SetValue(TrackerReadPastLog());
16
17 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
18 top_sizer->Add(text_area_,
19 wxSizerFlags().DoubleBorder().Expand().Proportion(1));
20
21 SetSizer(top_sizer);
22
23 Bind(LOG_MESSAGE, &LogDialog::OnLogMessage, this);
24}
25
26void LogDialog::LogMessage(const std::string& message) {
27 wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE);
28 event->SetString(message);
29 QueueEvent(event);
30}
31
32void LogDialog::OnLogMessage(wxCommandEvent& event) {
33 if (!text_area_->IsEmpty()) {
34 text_area_->AppendText("\n");
35 }
36 text_area_->AppendText(event.GetString());
37}
diff --git a/src/log_dialog.h b/src/log_dialog.h new file mode 100644 index 0000000..c29251f --- /dev/null +++ b/src/log_dialog.h
@@ -0,0 +1,24 @@
1#ifndef LOG_DIALOG_H_EEFD45B6
2#define LOG_DIALOG_H_EEFD45B6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent);
11
12class LogDialog : public wxDialog {
13 public:
14 explicit LogDialog(wxWindow* parent);
15
16 void LogMessage(const std::string& message);
17
18 private:
19 void OnLogMessage(wxCommandEvent& event);
20
21 wxTextCtrl* text_area_;
22};
23
24#endif /* end of include guard: LOG_DIALOG_H_EEFD45B6 */
diff --git a/src/logger.cpp b/src/logger.cpp index 09fc331..8a08b58 100644 --- a/src/logger.cpp +++ b/src/logger.cpp
@@ -3,8 +3,10 @@
3#include <chrono> 3#include <chrono>
4#include <fstream> 4#include <fstream>
5#include <mutex> 5#include <mutex>
6#include <sstream>
6 7
7#include "global.h" 8#include "global.h"
9#include "log_dialog.h"
8 10
9namespace { 11namespace {
10 12
@@ -14,19 +16,49 @@ class Logger {
14 16
15 void LogLine(const std::string& text) { 17 void LogLine(const std::string& text) {
16 std::lock_guard guard(file_mutex_); 18 std::lock_guard guard(file_mutex_);
17 logfile_ << "[" << std::chrono::system_clock::now() << "] " << text 19 std::ostringstream line;
18 << std::endl; 20 line << "[" << std::chrono::system_clock::now() << "] " << text;
21
22 logfile_ << line.str() << std::endl;
19 logfile_.flush(); 23 logfile_.flush();
24
25 if (log_dialog_ != nullptr) {
26 log_dialog_->LogMessage(line.str());
27 }
28 }
29
30 std::string GetContents() {
31 std::lock_guard guard(file_mutex_);
32
33 std::ifstream file_in(GetAbsolutePath("debug.log"));
34 std::ostringstream buffer;
35 buffer << file_in.rdbuf();
36
37 return buffer.str();
38 }
39
40 void SetLogDialog(LogDialog* log_dialog) {
41 std::lock_guard guard(file_mutex_);
42 log_dialog_ = log_dialog;
20 } 43 }
21 44
22 private: 45 private:
23 std::ofstream logfile_; 46 std::ofstream logfile_;
24 std::mutex file_mutex_; 47 std::mutex file_mutex_;
48 LogDialog* log_dialog_ = nullptr;
25}; 49};
26 50
51Logger& GetLogger() {
52 static Logger* instance = new Logger();
53 return *instance;
54}
55
27} // namespace 56} // namespace
28 57
29void TrackerLog(std::string text) { 58void TrackerLog(std::string text) { GetLogger().LogLine(text); }
30 static Logger* instance = new Logger(); 59
31 instance->LogLine(text); 60std::string TrackerReadPastLog() { return GetLogger().GetContents(); }
61
62void TrackerSetLogDialog(LogDialog* log_dialog) {
63 GetLogger().SetLogDialog(log_dialog);
32} 64}
diff --git a/src/logger.h b/src/logger.h index a27839f..f669790 100644 --- a/src/logger.h +++ b/src/logger.h
@@ -3,6 +3,12 @@
3 3
4#include <string> 4#include <string>
5 5
6class LogDialog;
7
6void TrackerLog(std::string message); 8void TrackerLog(std::string message);
7 9
10std::string TrackerReadPastLog();
11
12void TrackerSetLogDialog(LogDialog* log_dialog);
13
8#endif /* end of include guard: LOGGER_H_9BDD07EA */ 14#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index 1d7cc9e..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -10,7 +10,7 @@
10 10
11class TrackerApp : public wxApp { 11class TrackerApp : public wxApp {
12 public: 12 public:
13 virtual bool OnInit() { 13 virtual bool OnInit() override {
14 GetTrackerConfig().Load(); 14 GetTrackerConfig().Load();
15 15
16 TrackerFrame *frame = new TrackerFrame(); 16 TrackerFrame *frame = new TrackerFrame();
diff --git a/src/options_pane.cpp b/src/options_pane.cpp new file mode 100644 index 0000000..844e145 --- /dev/null +++ b/src/options_pane.cpp
@@ -0,0 +1,71 @@
1#include "options_pane.h"
2
3#include "ap_state.h"
4
5namespace {
6
7const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"};
8const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"};
9const char* kPanelShuffleLabels[] = {"None", "Rearrange"};
10const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2",
11 "Pilgrimage"};
12const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock",
13 "Individual", "Progressive"};
14
15void AddRow(wxDataViewListCtrl* list, const std::string& text) {
16 wxVector<wxVariant> data;
17 data.push_back(wxVariant{text + ": "});
18 data.push_back(wxVariant{""});
19 list->AppendItem(data);
20}
21
22} // namespace
23
24OptionsPane::OptionsPane(wxWindow* parent)
25 : wxDataViewListCtrl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
26 wxDV_ROW_LINES) {
27 AppendTextColumn("Name", wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE);
28 AppendTextColumn("Value");
29 AddRow(this, "Shuffle Doors");
30 AddRow(this, "Group Doors");
31 AddRow(this, "Location Checks");
32 AddRow(this, "Shuffle Colors");
33 AddRow(this, "Shuffle Panels");
34 AddRow(this, "Shuffle Paintings");
35 AddRow(this, "Victory Condition");
36 AddRow(this, "Early Color Hallways");
37 AddRow(this, "Shuffle Postgame");
38 AddRow(this, "Enable Pilgrimage");
39 AddRow(this, "Pilgrimage Roof Access");
40 AddRow(this, "Pilgrimage Paintings");
41 AddRow(this, "Sunwarp Access");
42 AddRow(this, "Shuffle Sunwarps");
43 AddRow(this, "Mastery Achievements");
44 AddRow(this, "Level 2 Requirement");
45}
46
47void OptionsPane::OnConnect() {
48 SetTextValue(kDoorShuffleLabels[static_cast<size_t>(AP_GetDoorShuffleMode())],
49 0, 1);
50 SetTextValue(AP_AreDoorsGrouped() ? "Yes" : "No", 1, 1);
51 SetTextValue(
52 kLocationChecksLabels[static_cast<size_t>(AP_GetLocationsChecks())], 2,
53 1);
54 SetTextValue(AP_IsColorShuffle() ? "Yes" : "No", 3, 1);
55 SetTextValue(
56 kPanelShuffleLabels[static_cast<size_t>(AP_GetPanelShuffleMode())], 4, 1);
57 SetTextValue(AP_IsPaintingShuffle() ? "Yes" : "No", 5, 1);
58 SetTextValue(
59 kVictoryConditionLabels[static_cast<size_t>(AP_GetVictoryCondition())], 6,
60 1);
61 SetTextValue(AP_HasEarlyColorHallways() ? "Yes" : "No", 7, 1);
62 SetTextValue(AP_IsPostgameShuffle() ? "Yes" : "No", 8, 1);
63 SetTextValue(AP_IsPilgrimageEnabled() ? "Yes" : "No", 9, 1);
64 SetTextValue(AP_DoesPilgrimageAllowRoofAccess() ? "Yes" : "No", 10, 1);
65 SetTextValue(AP_DoesPilgrimageAllowPaintings() ? "Yes" : "No", 11, 1);
66 SetTextValue(kSunwarpAccessLabels[static_cast<size_t>(AP_GetSunwarpAccess())],
67 12, 1);
68 SetTextValue(AP_IsSunwarpShuffle() ? "Yes" : "No", 13, 1);
69 SetTextValue(std::to_string(AP_GetMasteryRequirement()), 14, 1);
70 SetTextValue(std::to_string(AP_GetLevel2Requirement()), 15, 1);
71}
diff --git a/src/options_pane.h b/src/options_pane.h new file mode 100644 index 0000000..e9df9f0 --- /dev/null +++ b/src/options_pane.h
@@ -0,0 +1,19 @@
1#ifndef OPTIONS_PANE_H_026A0EC0
2#define OPTIONS_PANE_H_026A0EC0
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <wx/dataview.h>
11
12class OptionsPane : public wxDataViewListCtrl {
13 public:
14 explicit OptionsPane(wxWindow* parent);
15
16 void OnConnect();
17};
18
19#endif /* end of include guard: OPTIONS_PANE_H_026A0EC0 */
diff --git a/src/paintings_pane.cpp b/src/paintings_pane.cpp new file mode 100644 index 0000000..bf5d71b --- /dev/null +++ b/src/paintings_pane.cpp
@@ -0,0 +1,86 @@
1#include "paintings_pane.h"
2
3#include <fmt/core.h>
4#include <wx/dataview.h>
5
6#include <map>
7#include <set>
8
9#include "ap_state.h"
10#include "game_data.h"
11#include "tracker_state.h"
12
13namespace {
14
15std::string GetPaintingDisplayName(const std::string& id) {
16 const PaintingExit& painting = GD_GetPaintingExit(GD_GetPaintingByName(id));
17 const MapArea& map_area = GD_GetMapArea(painting.map_area);
18
19 return fmt::format("{} - {}", map_area.name, painting.display_name);
20}
21
22} // namespace
23
24PaintingsPane::PaintingsPane(wxWindow* parent) : wxPanel(parent, wxID_ANY) {
25 wxStaticText* label = new wxStaticText(
26 this, wxID_ANY, "Shuffled paintings grouped by destination:");
27 tree_ctrl_ = new wxDataViewTreeCtrl(this, wxID_ANY);
28
29 reveal_btn_ = new wxButton(this, wxID_ANY, "Reveal shuffled paintings");
30 reveal_btn_->Disable();
31
32 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
33 top_sizer->Add(label, wxSizerFlags().Border());
34 top_sizer->Add(tree_ctrl_, wxSizerFlags().Expand().Proportion(1));
35 top_sizer->Add(reveal_btn_, wxSizerFlags().Border().Expand());
36
37 SetSizerAndFit(top_sizer);
38
39 tree_ctrl_->Bind(wxEVT_DATAVIEW_ITEM_START_EDITING,
40 &PaintingsPane::OnStartEditingCell, this);
41 reveal_btn_->Bind(wxEVT_BUTTON, &PaintingsPane::OnClickRevealPaintings, this);
42}
43
44void PaintingsPane::ResetIndicators() {
45 tree_ctrl_->DeleteAllItems();
46 reveal_btn_->Enable(AP_IsPaintingShuffle());
47}
48
49void PaintingsPane::UpdateIndicators(const std::vector<std::string>&) {
50 // TODO: Optimize this by using the paintings delta.
51
52 tree_ctrl_->DeleteAllItems();
53
54 std::map<std::string, std::set<std::string>> grouped_paintings;
55
56 for (const auto& [from, to] : AP_GetPaintingMapping()) {
57 if (IsPaintingReachable(GD_GetPaintingByName(from)) &&
58 AP_IsPaintingChecked(from)) {
59 grouped_paintings[GetPaintingDisplayName(to)].insert(
60 GetPaintingDisplayName(from));
61 }
62 }
63
64 for (const auto& [to, froms] : grouped_paintings) {
65 wxDataViewItem tree_branch =
66 tree_ctrl_->AppendContainer(wxDataViewItem(0), to);
67
68 for (const std::string& from : froms) {
69 tree_ctrl_->AppendItem(tree_branch, from);
70 }
71 }
72}
73
74void PaintingsPane::OnClickRevealPaintings(wxCommandEvent& event) {
75 if (wxMessageBox("Clicking yes will reveal the mapping between all shuffled "
76 "paintings. This is usually considered a spoiler, and is "
77 "likely not allowed during competitions. This action is not "
78 "reversible. Are you sure you want to proceed?",
79 "Warning", wxYES_NO | wxICON_WARNING) == wxNO) {
80 return;
81 }
82
83 AP_RevealPaintings();
84}
85
86void PaintingsPane::OnStartEditingCell(wxDataViewEvent& event) { event.Veto(); }
diff --git a/src/paintings_pane.h b/src/paintings_pane.h new file mode 100644 index 0000000..1d14510 --- /dev/null +++ b/src/paintings_pane.h
@@ -0,0 +1,28 @@
1#ifndef PAINTINGS_PANE_H_815370D2
2#define PAINTINGS_PANE_H_815370D2
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class wxDataViewEvent;
11class wxDataViewTreeCtrl;
12
13class PaintingsPane : public wxPanel {
14 public:
15 explicit PaintingsPane(wxWindow* parent);
16
17 void ResetIndicators();
18 void UpdateIndicators(const std::vector<std::string>& paintings);
19
20 private:
21 void OnClickRevealPaintings(wxCommandEvent& event);
22 void OnStartEditingCell(wxDataViewEvent& event);
23
24 wxDataViewTreeCtrl* tree_ctrl_;
25 wxButton* reveal_btn_;
26};
27
28#endif /* end of include guard: PAINTINGS_PANE_H_815370D2 */
diff --git a/src/report_popup.cpp b/src/report_popup.cpp new file mode 100644 index 0000000..703e87f --- /dev/null +++ b/src/report_popup.cpp
@@ -0,0 +1,131 @@
1#include "report_popup.h"
2
3#include <wx/dcbuffer.h>
4
5#include <map>
6#include <string>
7
8#include "global.h"
9#include "icons.h"
10#include "tracker_state.h"
11
12ReportPopup::ReportPopup(wxWindow* parent)
13 : wxScrolledCanvas(parent, wxID_ANY) {
14 SetBackgroundStyle(wxBG_STYLE_PAINT);
15
16 LoadIcons();
17
18 // TODO: This is slow on high-DPI screens.
19 SetScrollRate(5, 5);
20
21 SetBackgroundColour(*wxBLACK);
22 Hide();
23
24 Bind(wxEVT_PAINT, &ReportPopup::OnPaint, this);
25 Bind(wxEVT_DPI_CHANGED, &ReportPopup::OnDPIChanged, this);
26}
27
28void ReportPopup::SetDoorId(int door_id) {
29 door_id_ = door_id;
30
31 ResetIndicators();
32}
33
34void ReportPopup::Reset() {
35 door_id_ = -1;
36}
37
38void ReportPopup::ResetIndicators() {
39 if (door_id_ == -1) {
40 return;
41 }
42
43 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
44 const std::map<std::string, bool>& report = GetDoorRequirements(door_id_);
45
46 wxMemoryDC mem_dc;
47 mem_dc.SetFont(the_font);
48
49 int acc_height = FromDIP(10);
50 int col_width = 0;
51
52 for (const auto& [text, obtained] : report) {
53 wxSize item_extent = mem_dc.GetTextExtent(text);
54 int item_height =
55 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
56 acc_height += item_height;
57
58 if (item_extent.GetWidth() > col_width) {
59 col_width = item_extent.GetWidth();
60 }
61 }
62
63 int item_width = col_width + FromDIP(10 + 32);
64 full_width_ = item_width + FromDIP(20);
65 full_height_ = acc_height;
66
67 Fit();
68 SetVirtualSize(full_width_, full_height_);
69
70 UpdateIndicators();
71}
72
73void ReportPopup::UpdateIndicators() {
74 if (door_id_ == -1) {
75 return;
76 }
77
78 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
79 const std::map<std::string, bool>& report = GetDoorRequirements(door_id_);
80
81 rendered_ = wxBitmap(full_width_, full_height_);
82
83 wxMemoryDC mem_dc;
84 mem_dc.SelectObject(rendered_);
85 mem_dc.SetPen(*wxTRANSPARENT_PEN);
86 mem_dc.SetBrush(*wxBLACK_BRUSH);
87 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
88
89 mem_dc.SetFont(the_font);
90
91 int cur_height = FromDIP(10);
92
93 for (const auto& [text, obtained] : report) {
94 const wxBitmap* eye_ptr = obtained ? checked_eye_ : unchecked_eye_;
95
96 mem_dc.DrawBitmap(*eye_ptr, wxPoint{FromDIP(10), cur_height});
97
98 mem_dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
99 wxSize item_extent = mem_dc.GetTextExtent(text);
100 mem_dc.DrawText(
101 text, wxPoint{FromDIP(10 + 32 + 10),
102 cur_height +
103 (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
104
105 cur_height += FromDIP(10 + 32);
106 }
107}
108
109void ReportPopup::OnPaint(wxPaintEvent& event) {
110 if (door_id_ != -1) {
111 wxBufferedPaintDC dc(this);
112 PrepareDC(dc);
113 dc.DrawBitmap(rendered_, 0, 0);
114 }
115
116 event.Skip();
117}
118
119void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) {
120 LoadIcons();
121 ResetIndicators();
122
123 event.Skip();
124}
125
126void ReportPopup::LoadIcons() {
127 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
128 FromDIP(wxSize{32, 32}));
129 checked_eye_ =
130 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
131}
diff --git a/src/report_popup.h b/src/report_popup.h new file mode 100644 index 0000000..bbb0bef --- /dev/null +++ b/src/report_popup.h
@@ -0,0 +1,38 @@
1#ifndef REPORT_POPUP_H_E065BED4
2#define REPORT_POPUP_H_E065BED4
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class ReportPopup : public wxScrolledCanvas {
11 public:
12 explicit ReportPopup(wxWindow* parent);
13
14 void SetDoorId(int door_id);
15
16 void Reset();
17
18 void ResetIndicators();
19 void UpdateIndicators();
20
21 private:
22 void OnPaint(wxPaintEvent& event);
23 void OnDPIChanged(wxDPIChangedEvent& event);
24
25 void LoadIcons();
26
27 int door_id_ = -1;
28
29 const wxBitmap* unchecked_eye_;
30 const wxBitmap* checked_eye_;
31
32 int full_width_ = 0;
33 int full_height_ = 0;
34
35 wxBitmap rendered_;
36};
37
38#endif /* end of include guard: REPORT_POPUP_H_E065BED4 */
diff --git a/src/settings_dialog.cpp b/src/settings_dialog.cpp index 0321b5a..95df577 100644 --- a/src/settings_dialog.cpp +++ b/src/settings_dialog.cpp
@@ -3,30 +3,43 @@
3#include "tracker_config.h" 3#include "tracker_config.h"
4 4
5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { 5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") {
6 should_check_for_updates_box_ = new wxCheckBox( 6 wxStaticBoxSizer* main_box =
7 this, wxID_ANY, "Check for updates when the tracker opens"); 7 new wxStaticBoxSizer(wxVERTICAL, this, "General settings");
8
9 should_check_for_updates_box_ =
10 new wxCheckBox(main_box->GetStaticBox(), wxID_ANY,
11 "Check for updates when the tracker opens");
8 hybrid_areas_box_ = new wxCheckBox( 12 hybrid_areas_box_ = new wxCheckBox(
9 this, wxID_ANY, 13 main_box->GetStaticBox(), wxID_ANY,
10 "Use two colors to show that an area has partial availability"); 14 "Use two colors to show that an area has partial availability");
11 show_hunt_panels_box_ = new wxCheckBox(this, wxID_ANY, "Show hunt panels"); 15 track_position_box_ = new wxCheckBox(main_box->GetStaticBox(), wxID_ANY,
16 "Track player position");
12 17
13 should_check_for_updates_box_->SetValue( 18 should_check_for_updates_box_->SetValue(
14 GetTrackerConfig().should_check_for_updates); 19 GetTrackerConfig().should_check_for_updates);
15 hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); 20 hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas);
16 show_hunt_panels_box_->SetValue(GetTrackerConfig().show_hunt_panels); 21 track_position_box_->SetValue(GetTrackerConfig().track_position);
22
23 main_box->Add(should_check_for_updates_box_, wxSizerFlags().Border());
24 main_box->AddSpacer(2);
25 main_box->Add(hybrid_areas_box_, wxSizerFlags().Border());
26 main_box->AddSpacer(2);
27 main_box->Add(track_position_box_, wxSizerFlags().Border());
28
29 const wxString visible_panels_choices[] = {"Only show locations",
30 "Show locations and hunt panels",
31 "Show all panels"};
32 visible_panels_box_ =
33 new wxRadioBox(this, wxID_ANY, "Visible panels", wxDefaultPosition,
34 wxDefaultSize, 3, visible_panels_choices, 1);
35 visible_panels_box_->SetSelection(
36 static_cast<int>(GetTrackerConfig().visible_panels));
17 37
18 wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); 38 wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL);
19 39 form_sizer->Add(main_box, wxSizerFlags().Border().Expand());
20 form_sizer->Add(should_check_for_updates_box_, wxSizerFlags().HorzBorder()); 40 form_sizer->Add(visible_panels_box_, wxSizerFlags().Border().Expand());
21 form_sizer->AddSpacer(2); 41 form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
22 42 wxSizerFlags().Center().Border());
23 form_sizer->Add(hybrid_areas_box_, wxSizerFlags().HorzBorder());
24 form_sizer->AddSpacer(2);
25
26 form_sizer->Add(show_hunt_panels_box_, wxSizerFlags().HorzBorder());
27 form_sizer->AddSpacer(2);
28
29 form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center());
30 43
31 SetSizerAndFit(form_sizer); 44 SetSizerAndFit(form_sizer);
32 45
diff --git a/src/settings_dialog.h b/src/settings_dialog.h index d7c1ed3..c4dacfa 100644 --- a/src/settings_dialog.h +++ b/src/settings_dialog.h
@@ -7,6 +7,10 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <wx/radiobox.h>
11
12#include "tracker_config.h"
13
10class SettingsDialog : public wxDialog { 14class SettingsDialog : public wxDialog {
11 public: 15 public:
12 SettingsDialog(); 16 SettingsDialog();
@@ -15,12 +19,17 @@ class SettingsDialog : public wxDialog {
15 return should_check_for_updates_box_->GetValue(); 19 return should_check_for_updates_box_->GetValue();
16 } 20 }
17 bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } 21 bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); }
18 bool GetShowHuntPanels() const { return show_hunt_panels_box_->GetValue(); } 22 TrackerConfig::VisiblePanels GetVisiblePanels() const {
23 return static_cast<TrackerConfig::VisiblePanels>(
24 visible_panels_box_->GetSelection());
25 }
26 bool GetTrackPosition() const { return track_position_box_->GetValue(); }
19 27
20 private: 28 private:
21 wxCheckBox* should_check_for_updates_box_; 29 wxCheckBox* should_check_for_updates_box_;
22 wxCheckBox* hybrid_areas_box_; 30 wxCheckBox* hybrid_areas_box_;
23 wxCheckBox* show_hunt_panels_box_; 31 wxRadioBox* visible_panels_box_;
32 wxCheckBox* track_position_box_;
24}; 33};
25 34
26#endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ 35#endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */
diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 0a250fb..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -9,12 +9,28 @@
9#include "ap_state.h" 9#include "ap_state.h"
10#include "game_data.h" 10#include "game_data.h"
11#include "global.h" 11#include "global.h"
12#include "report_popup.h"
12#include "tracker_state.h" 13#include "tracker_state.h"
13 14
14constexpr int AREA_ACTUAL_SIZE = 21; 15constexpr int AREA_ACTUAL_SIZE = 21;
15constexpr int OWL_ACTUAL_SIZE = 32; 16constexpr int OWL_ACTUAL_SIZE = 32;
17constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11
18constexpr int PAINTING_EXIT_RADIUS = 6;
16 19
17enum class ItemDrawType { kNone, kBox, kOwl }; 20enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit };
21
22namespace {
23
24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
25 if (subway_item.painting) {
26 return {subway_item.x, subway_item.y};
27 } else {
28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
29 subway_item.y + AREA_ACTUAL_SIZE / 2};
30 }
31}
32
33} // namespace
18 34
19SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 35SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
20 SetBackgroundStyle(wxBG_STYLE_PAINT); 36 SetBackgroundStyle(wxBG_STYLE_PAINT);
@@ -31,14 +47,6 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
31 return; 47 return;
32 } 48 }
33 49
34 unchecked_eye_ =
35 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
36 wxBITMAP_TYPE_PNG)
37 .Scale(32, 32));
38 checked_eye_ = wxBitmap(
39 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
40 .Scale(32, 32));
41
42 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( 50 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
43 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), 51 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
44 static_cast<float>(map_image_.GetHeight())}); 52 static_cast<float>(map_image_.GetHeight())});
@@ -57,12 +65,14 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
57 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); 65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
58 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); 66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
59 67
60 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15}); 68 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15}));
61 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); 69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
62 70
63 help_button_ = new wxButton(this, wxID_ANY, "Help"); 71 help_button_ = new wxButton(this, wxID_ANY, "Help");
64 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); 72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
65 SetUpHelpButton(); 73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
66} 76}
67 77
68void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
@@ -73,11 +83,11 @@ void SubwayMap::OnConnect() {
73 std::map<std::string, std::vector<int>> exits; 83 std::map<std::string, std::vector<int>> exits;
74 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
75 if (AP_HasEarlyColorHallways() && 85 if (AP_HasEarlyColorHallways() &&
76 subway_item.special == "starting_room_paintings") { 86 subway_item.special == "early_color_hallways") {
77 entrances["early_ch"].push_back(subway_item.id); 87 entrances["early_ch"].push_back(subway_item.id);
78 } 88 }
79 89
80 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 90 if (AP_IsPaintingShuffle() && subway_item.painting) {
81 continue; 91 continue;
82 } 92 }
83 93
@@ -174,6 +184,8 @@ void SubwayMap::OnConnect() {
174 } 184 }
175 185
176 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
177} 189}
178 190
179void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
@@ -202,6 +214,8 @@ void SubwayMap::UpdateIndicators() {
202 } 214 }
203 } 215 }
204 216
217 report_popup_->UpdateIndicators();
218
205 Redraw(); 219 Redraw();
206} 220}
207 221
@@ -255,6 +269,9 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
255 SetZoomPos({zoom_x_, zoom_y_}); 269 SetZoomPos({zoom_x_, zoom_y_});
256 270
257 SetUpHelpButton(); 271 SetUpHelpButton();
272
273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
274 wxDefaultCoord, wxSIZE_AUTO);
258 } 275 }
259 276
260 wxBufferedPaintDC dc(this); 277 wxBufferedPaintDC dc(this);
@@ -310,67 +327,6 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
310 } 327 }
311 328
312 if (hovered_item_) { 329 if (hovered_item_) {
313 // Note that these requirements are duplicated on OnMouseClick so that it
314 // knows when an item has a hover effect.
315 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
316 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
317
318 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
319 const std::map<std::string, bool> &report =
320 GetDoorRequirements(*subway_door);
321
322 int acc_height = 10;
323 int col_width = 0;
324
325 for (const auto &[text, obtained] : report) {
326 wxSize item_extent = dc.GetTextExtent(text);
327 int item_height = std::max(32, item_extent.GetHeight()) + 10;
328 acc_height += item_height;
329
330 if (item_extent.GetWidth() > col_width) {
331 col_width = item_extent.GetWidth();
332 }
333 }
334
335 int item_width = col_width + 10 + 32;
336 int full_width = item_width + 20;
337
338 wxPoint popup_pos =
339 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
340 subway_item.y + AREA_ACTUAL_SIZE / 2});
341
342 if (popup_pos.x + full_width > GetSize().GetWidth()) {
343 popup_pos.x = GetSize().GetWidth() - full_width;
344 }
345 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
346 popup_pos.y = GetSize().GetHeight() - acc_height;
347 }
348
349 dc.SetPen(*wxTRANSPARENT_PEN);
350 dc.SetBrush(*wxBLACK_BRUSH);
351 dc.DrawRectangle(popup_pos, {full_width, acc_height});
352
353 dc.SetFont(GetFont());
354
355 int cur_height = 10;
356
357 for (const auto &[text, obtained] : report) {
358 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
359
360 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
361
362 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
363 wxSize item_extent = dc.GetTextExtent(text);
364 dc.DrawText(
365 text,
366 popup_pos +
367 wxPoint{10 + 32 + 10,
368 cur_height + (32 - dc.GetFontMetrics().height) / 2});
369
370 cur_height += 10 + 32;
371 }
372 }
373
374 if (networks_.IsItemInNetwork(*hovered_item_)) { 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
375 dc.SetBrush(*wxTRANSPARENT_BRUSH); 331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
376 332
@@ -378,10 +334,8 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
378 const SubwayItem &item1 = GD_GetSubwayItem(node.entry); 334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
379 const SubwayItem &item2 = GD_GetSubwayItem(node.exit); 335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
380 336
381 wxPoint item1_pos = MapPosToRenderPos( 337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
382 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); 338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
383 wxPoint item2_pos = MapPosToRenderPos(
384 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
385 339
386 int left = std::min(item1_pos.x, item2_pos.x); 340 int left = std::min(item1_pos.x, item2_pos.x);
387 int top = std::min(item1_pos.y, item2_pos.y); 341 int top = std::min(item1_pos.y, item2_pos.y);
@@ -470,9 +424,7 @@ void SubwayMap::OnMouseMove(wxMouseEvent &event) {
470 } 424 }
471 425
472 if (!sticky_hover_ && actual_hover_ != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
473 hovered_item_ = actual_hover_; 427 EvaluateHover();
474
475 Refresh();
476 } 428 }
477 429
478 if (scroll_mode_) { 430 if (scroll_mode_) {
@@ -514,13 +466,11 @@ void SubwayMap::OnMouseClick(wxMouseEvent &event) {
514 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || 466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
515 networks_.IsItemInNetwork(*hovered_item_)) { 467 networks_.IsItemInNetwork(*hovered_item_)) {
516 if (actual_hover_ != hovered_item_) { 468 if (actual_hover_ != hovered_item_) {
517 hovered_item_ = actual_hover_; 469 EvaluateHover();
518 470
519 if (!hovered_item_) { 471 if (!hovered_item_) {
520 sticky_hover_ = false; 472 sticky_hover_ = false;
521 } 473 }
522
523 Refresh();
524 } else { 474 } else {
525 sticky_hover_ = !sticky_hover_; 475 sticky_hover_ = !sticky_hover_;
526 } 476 }
@@ -571,11 +521,12 @@ void SubwayMap::OnClickHelp(wxCommandEvent &event) {
571 "your mouse. Click again to stop.\nHover over a door to see the " 521 "your mouse. Click again to stop.\nHover over a door to see the "
572 "requirements to open it.\nHover over a warp or active painting to see " 522 "requirements to open it.\nHover over a warp or active painting to see "
573 "what it is connected to.\nFor one-way connections, there will be a " 523 "what it is connected to.\nFor one-way connections, there will be a "
574 "circle at the exit.\nIn painting shuffle, paintings that have not " 524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
575 "yet been checked will not show their connections.\nA green shaded owl " 525 "that the painting is locked by a door.\nA blue circle means painting "
576 "means that there is a painting entrance there.\nA red shaded owl means " 526 "shuffle is enabled and the painting has not been checked yet.\nA black "
577 "that there are only painting exits there.\nClick on a door or " 527 "circle means the painting is not a warp.\nA green circle means that the "
578 "warp to make the popup stick until you click again.", 528 "painting is a warp.\nPainting exits will be indicated with an X.\nClick "
529 "on a door or warp to make the popup stick until you click again.",
579 "Subway Map Help"); 530 "Subway Map Help");
580} 531}
581 532
@@ -592,20 +543,32 @@ void SubwayMap::Redraw() {
592 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
593 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
594 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
595 std::optional<wxColour> shade_color;
596 std::optional<int> subway_door = GetRealSubwayDoor(subway_item); 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
597 547
598 if (AP_HasEarlyColorHallways() && 548 if (AP_HasEarlyColorHallways() &&
599 subway_item.special == "starting_room_paintings") { 549 subway_item.special == "early_color_hallways") {
600 draw_type = ItemDrawType::kOwl; 550 draw_type = ItemDrawType::kOwl;
601 shade_color = wxColour(0, 255, 0, 128); 551 brush_color = wxGREEN_BRUSH;
552 } else if (subway_item.special == "starting_room_overhead") {
553 // Do not draw.
554 } else if (AP_IsColorShuffle() && subway_item.special &&
555 subway_item.special->starts_with("color_")) {
556 std::string color_name = subway_item.special->substr(6);
557 LingoColor lingo_color = GetLingoColorForString(color_name);
558 int color_item_id = GD_GetItemIdForColor(lingo_color);
559
560 draw_type = ItemDrawType::kBox;
561 if (AP_HasItemSafe(color_item_id)) {
562 brush_color = wxGREEN_BRUSH;
563 } else {
564 brush_color = wxRED_BRUSH;
565 }
602 } else if (subway_item.special == "sun_painting") { 566 } else if (subway_item.special == "sun_painting") {
603 if (!AP_IsPilgrimageEnabled()) { 567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
604 if (IsDoorOpen(*subway_item.door)) { 569 if (IsDoorOpen(*subway_item.door)) {
605 draw_type = ItemDrawType::kOwl; 570 brush_color = wxGREEN_BRUSH;
606 shade_color = wxColour(0, 255, 0, 128);
607 } else { 571 } else {
608 draw_type = ItemDrawType::kBox;
609 brush_color = wxRED_BRUSH; 572 brush_color = wxRED_BRUSH;
610 } 573 }
611 } 574 }
@@ -619,41 +582,28 @@ void SubwayMap::Redraw() {
619 } else { 582 } else {
620 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
621 } 584 }
622 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
623 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
624 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
625 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
626 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
627 bool has_codomain_painting = false; 590 if (!checked_paintings_.count(*subway_item.painting)) {
628
629 for (const std::string &painting_id : subway_item.paintings) {
630 if (checked_paintings_.count(painting_id)) {
631 has_checked_painting = true;
632
633 if (painting_mapping.count(painting_id)) {
634 has_mapped_painting = true;
635 } else if (AP_IsPaintingMappedTo(painting_id)) {
636 has_codomain_painting = true;
637 }
638 } else {
639 has_unchecked_painting = true;
640 }
641 }
642
643 if (has_unchecked_painting || has_mapped_painting ||
644 has_codomain_painting) {
645 draw_type = ItemDrawType::kOwl; 591 draw_type = ItemDrawType::kOwl;
646 592 brush_color = wxBLUE_BRUSH;
647 if (has_checked_painting) { 593 } else if (painting_mapping.count(*subway_item.painting)) {
648 if (has_mapped_painting) { 594 draw_type = ItemDrawType::kOwl;
649 shade_color = wxColour(0, 255, 0, 128); 595 brush_color = wxGREEN_BRUSH;
650 } else { 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
651 shade_color = wxColour(255, 0, 0, 128); 597 draw_type = ItemDrawType::kOwlExit;
652 } 598 brush_color = wxGREEN_BRUSH;
653 }
654 } 599 }
655 } else if (subway_item.HasWarps()) { 600 } else if (subway_item.HasWarps()) {
656 draw_type = ItemDrawType::kOwl; 601 brush_color = wxGREEN_BRUSH;
602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
605 draw_type = ItemDrawType::kOwl;
606 }
657 } 607 }
658 } else if (subway_door) { 608 } else if (subway_door) {
659 draw_type = ItemDrawType::kBox; 609 draw_type = ItemDrawType::kBox;
@@ -673,21 +623,40 @@ void SubwayMap::Redraw() {
673 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
674 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
675 gcdc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
676 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 626
677 } else if (draw_type == ItemDrawType::kOwl) { 627 if (subway_item.tilted) {
678 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( 628 constexpr int AREA_TILTED_SIDE =
679 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
680 gcdc.DrawBitmap(owl_bitmap, real_area_pos); 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
681 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
682 if (shade_color) { 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
683 gcdc.SetBrush(wxBrush(*shade_color)); 633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
684 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
685 } 637 }
638 } else if (draw_type == ItemDrawType::kOwl || draw_type == ItemDrawType::kOwlExit) {
639 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
640 gcdc.SetBrush(*brush_color);
641 gcdc.DrawCircle(real_area_pos, PAINTING_RADIUS);
642
643 if (draw_type == ItemDrawType::kOwlExit) {
644 gcdc.DrawLine(subway_item.x - PAINTING_EXIT_RADIUS,
645 subway_item.y - PAINTING_EXIT_RADIUS,
646 subway_item.x + PAINTING_EXIT_RADIUS,
647 subway_item.y + PAINTING_EXIT_RADIUS);
648 gcdc.DrawLine(subway_item.x + PAINTING_EXIT_RADIUS,
649 subway_item.y - PAINTING_EXIT_RADIUS,
650 subway_item.x - PAINTING_EXIT_RADIUS,
651 subway_item.y + PAINTING_EXIT_RADIUS);
652 }
686 } 653 }
687 } 654 }
688} 655}
689 656
690void SubwayMap::SetUpHelpButton() { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
691 help_button_->SetPosition({ 660 help_button_->SetPosition({
692 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, 661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
693 15, 662 15,
@@ -723,6 +692,51 @@ void SubwayMap::EvaluateScroll(wxPoint pos) {
723 SetScrollSpeed(scroll_x, scroll_y); 692 SetScrollSpeed(scroll_x, scroll_y);
724} 693}
725 694
695void SubwayMap::EvaluateHover() {
696 hovered_item_ = actual_hover_;
697
698 if (hovered_item_) {
699 // Note that these requirements are duplicated on OnMouseClick so that it
700 // knows when an item has a hover effect.
701 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
702 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
703
704 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
705 report_popup_->SetDoorId(*subway_door);
706
707 wxPoint popupPos =
708 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
709 subway_item.y + AREA_ACTUAL_SIZE / 2});
710
711 report_popup_->SetClientSize(
712 report_popup_->GetVirtualSize().GetWidth(),
713 std::min(GetSize().GetHeight(),
714 report_popup_->GetVirtualSize().GetHeight()));
715
716 if (popupPos.x + report_popup_->GetSize().GetWidth() >
717 GetSize().GetWidth()) {
718 popupPos.x = GetSize().GetWidth() - report_popup_->GetSize().GetWidth();
719 }
720 if (popupPos.y + report_popup_->GetSize().GetHeight() >
721 GetSize().GetHeight()) {
722 popupPos.y =
723 GetSize().GetHeight() - report_popup_->GetSize().GetHeight();
724 }
725 report_popup_->SetPosition(popupPos);
726
727 report_popup_->Show();
728 } else {
729 report_popup_->Reset();
730 report_popup_->Hide();
731 }
732 } else {
733 report_popup_->Reset();
734 report_popup_->Hide();
735 }
736
737 Refresh();
738}
739
726wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { 740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
727 return {static_cast<int>(pos.x * render_width_ * zoom_ / 741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
728 map_image_.GetSize().GetWidth() + 742 map_image_.GetSize().GetWidth() +
@@ -812,6 +826,13 @@ std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) {
812 826
813quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { 827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
814 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
815 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
816 AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE}; 830 return {static_cast<float>(subway_item.x) - PAINTING_RADIUS,
831 static_cast<float>(subway_item.y) - PAINTING_RADIUS,
832 PAINTING_RADIUS * 2, PAINTING_RADIUS * 2};
833 } else {
834 return {static_cast<float>(subway_item.x),
835 static_cast<float>(subway_item.y), AREA_ACTUAL_SIZE,
836 AREA_ACTUAL_SIZE};
837 }
817} 838}
diff --git a/src/subway_map.h b/src/subway_map.h index 6aa31f5..b04c2fd 100644 --- a/src/subway_map.h +++ b/src/subway_map.h
@@ -19,6 +19,8 @@
19#include "game_data.h" 19#include "game_data.h"
20#include "network_set.h" 20#include "network_set.h"
21 21
22class ReportPopup;
23
22class SubwayMap : public wxPanel { 24class SubwayMap : public wxPanel {
23 public: 25 public:
24 SubwayMap(wxWindow *parent); 26 SubwayMap(wxWindow *parent);
@@ -46,6 +48,7 @@ class SubwayMap : public wxPanel {
46 wxPoint RenderPosToMapPos(wxPoint pos) const; 48 wxPoint RenderPosToMapPos(wxPoint pos) const;
47 49
48 void EvaluateScroll(wxPoint pos); 50 void EvaluateScroll(wxPoint pos);
51 void EvaluateHover();
49 52
50 void SetZoomPos(wxPoint pos); 53 void SetZoomPos(wxPoint pos);
51 void SetScrollSpeed(int scroll_x, int scroll_y); 54 void SetScrollSpeed(int scroll_x, int scroll_y);
@@ -55,8 +58,6 @@ class SubwayMap : public wxPanel {
55 58
56 wxImage map_image_; 59 wxImage map_image_;
57 wxImage owl_image_; 60 wxImage owl_image_;
58 wxBitmap unchecked_eye_;
59 wxBitmap checked_eye_;
60 61
61 wxBitmap rendered_; 62 wxBitmap rendered_;
62 int render_x_ = 0; 63 int render_x_ = 0;
@@ -88,6 +89,8 @@ class SubwayMap : public wxPanel {
88 std::optional<int> actual_hover_; 89 std::optional<int> actual_hover_;
89 bool sticky_hover_ = false; 90 bool sticky_hover_ = false;
90 91
92 ReportPopup *report_popup_;
93
91 NetworkSet networks_; 94 NetworkSet networks_;
92 std::set<std::string> checked_paintings_; 95 std::set<std::string> checked_paintings_;
93 96
diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 129dbbc..da5d60a 100644 --- a/src/tracker_config.cpp +++ b/src/tracker_config.cpp
@@ -16,7 +16,9 @@ void TrackerConfig::Load() {
16 asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>(); 16 asked_to_check_for_updates = file["asked_to_check_for_updates"].as<bool>();
17 should_check_for_updates = file["should_check_for_updates"].as<bool>(); 17 should_check_for_updates = file["should_check_for_updates"].as<bool>();
18 hybrid_areas = file["hybrid_areas"].as<bool>(); 18 hybrid_areas = file["hybrid_areas"].as<bool>();
19 show_hunt_panels = file["show_hunt_panels"].as<bool>(); 19 if (file["show_hunt_panels"] && file["show_hunt_panels"].as<bool>()) {
20 visible_panels = kHUNT_PANELS;
21 }
20 22
21 if (file["connection_history"]) { 23 if (file["connection_history"]) {
22 for (const auto& connection : file["connection_history"]) { 24 for (const auto& connection : file["connection_history"]) {
@@ -29,6 +31,9 @@ void TrackerConfig::Load() {
29 } 31 }
30 32
31 ipc_address = file["ipc_address"].as<std::string>(); 33 ipc_address = file["ipc_address"].as<std::string>();
34 track_position = file["track_position"].as<bool>();
35 visible_panels =
36 static_cast<VisiblePanels>(file["visible_panels"].as<int>());
32 } catch (const std::exception&) { 37 } catch (const std::exception&) {
33 // It's fine if the file can't be loaded. 38 // It's fine if the file can't be loaded.
34 } 39 }
@@ -42,7 +47,6 @@ void TrackerConfig::Save() {
42 output["asked_to_check_for_updates"] = asked_to_check_for_updates; 47 output["asked_to_check_for_updates"] = asked_to_check_for_updates;
43 output["should_check_for_updates"] = should_check_for_updates; 48 output["should_check_for_updates"] = should_check_for_updates;
44 output["hybrid_areas"] = hybrid_areas; 49 output["hybrid_areas"] = hybrid_areas;
45 output["show_hunt_panels"] = show_hunt_panels;
46 50
47 output.remove("connection_history"); 51 output.remove("connection_history");
48 for (const ConnectionDetails& details : connection_history) { 52 for (const ConnectionDetails& details : connection_history) {
@@ -55,6 +59,8 @@ void TrackerConfig::Save() {
55 } 59 }
56 60
57 output["ipc_address"] = ipc_address; 61 output["ipc_address"] = ipc_address;
62 output["track_position"] = track_position;
63 output["visible_panels"] = static_cast<int>(visible_panels);
58 64
59 std::ofstream filewriter(filename_); 65 std::ofstream filewriter(filename_);
60 filewriter << output; 66 filewriter << output;
diff --git a/src/tracker_config.h b/src/tracker_config.h index 9244b74..df4105d 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h
@@ -23,13 +23,20 @@ class TrackerConfig {
23 23
24 void Save(); 24 void Save();
25 25
26 enum VisiblePanels {
27 kLOCATIONS_ONLY,
28 kHUNT_PANELS,
29 kALL_PANELS,
30 };
31
26 ConnectionDetails connection_details; 32 ConnectionDetails connection_details;
27 bool asked_to_check_for_updates = false; 33 bool asked_to_check_for_updates = false;
28 bool should_check_for_updates = false; 34 bool should_check_for_updates = false;
29 bool hybrid_areas = false; 35 bool hybrid_areas = false;
30 bool show_hunt_panels = false;
31 std::deque<ConnectionDetails> connection_history; 36 std::deque<ConnectionDetails> connection_history;
32 std::string ipc_address; 37 std::string ipc_address;
38 bool track_position = true;
39 VisiblePanels visible_panels = kLOCATIONS_ONLY;
33 40
34 private: 41 private:
35 std::string filename_; 42 std::string filename_;
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index 587d87b..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -5,9 +5,11 @@
5#include <wx/choicebk.h> 5#include <wx/choicebk.h>
6#include <wx/filedlg.h> 6#include <wx/filedlg.h>
7#include <wx/notebook.h> 7#include <wx/notebook.h>
8#include <wx/splitter.h>
8#include <wx/stdpaths.h> 9#include <wx/stdpaths.h>
9#include <wx/webrequest.h> 10#include <wx/webrequest.h>
10 11
12#include <algorithm>
11#include <nlohmann/json.hpp> 13#include <nlohmann/json.hpp>
12#include <sstream> 14#include <sstream>
13 15
@@ -16,6 +18,11 @@
16#include "connection_dialog.h" 18#include "connection_dialog.h"
17#include "ipc_dialog.h" 19#include "ipc_dialog.h"
18#include "ipc_state.h" 20#include "ipc_state.h"
21#include "items_pane.h"
22#include "log_dialog.h"
23#include "logger.h"
24#include "options_pane.h"
25#include "paintings_pane.h"
19#include "settings_dialog.h" 26#include "settings_dialog.h"
20#include "subway_map.h" 27#include "subway_map.h"
21#include "tracker_config.h" 28#include "tracker_config.h"
@@ -44,14 +51,13 @@ enum TrackerFrameIds {
44 ID_SETTINGS = 3, 51 ID_SETTINGS = 3,
45 ID_ZOOM_IN = 4, 52 ID_ZOOM_IN = 4,
46 ID_ZOOM_OUT = 5, 53 ID_ZOOM_OUT = 5,
47 ID_OPEN_SAVE_FILE = 6,
48 ID_IPC_CONNECT = 7, 54 ID_IPC_CONNECT = 7,
55 ID_LOG_DIALOG = 8,
49}; 56};
50 57
51wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); 58wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
52wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 59wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent);
53wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 60wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
54wxDEFINE_EVENT(REDRAW_POSITION, wxCommandEvent);
55wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); 61wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent);
56 62
57TrackerFrame::TrackerFrame() 63TrackerFrame::TrackerFrame()
@@ -62,16 +68,22 @@ TrackerFrame::TrackerFrame()
62 AP_SetTrackerFrame(this); 68 AP_SetTrackerFrame(this);
63 IPC_SetTrackerFrame(this); 69 IPC_SetTrackerFrame(this);
64 70
71 SetTheIconCache(&icons_);
72
73 updater_ = std::make_unique<Updater>(this);
74 updater_->Cleanup();
75
65 wxMenu *menuFile = new wxMenu(); 76 wxMenu *menuFile = new wxMenu();
66 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago"); 77 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
67 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo"); 78 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
68 menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O");
69 menuFile->Append(ID_SETTINGS, "&Settings"); 79 menuFile->Append(ID_SETTINGS, "&Settings");
70 menuFile->Append(wxID_EXIT); 80 menuFile->Append(wxID_EXIT);
71 81
72 wxMenu *menuView = new wxMenu(); 82 wxMenu *menuView = new wxMenu();
73 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+"); 83 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+");
74 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--"); 84 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--");
85 menuView->AppendSeparator();
86 menuView->Append(ID_LOG_DIALOG, "Show Log Window\tCtrl-L");
75 87
76 zoom_in_menu_item_->Enable(false); 88 zoom_in_menu_item_->Enable(false);
77 zoom_out_menu_item_->Enable(false); 89 zoom_out_menu_item_->Enable(false);
@@ -98,30 +110,43 @@ TrackerFrame::TrackerFrame()
98 ID_CHECK_FOR_UPDATES); 110 ID_CHECK_FOR_UPDATES);
99 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); 111 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
100 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); 112 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
113 Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG);
101 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); 114 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
102 Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE); 115 Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged,
116 this);
103 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); 117 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
104 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 118 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
105 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); 119 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
106 Bind(REDRAW_POSITION, &TrackerFrame::OnRedrawPosition, this);
107 Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this); 120 Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this);
108 121
109 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); 122 wxSize logicalSize = FromDIP(wxSize(1280, 728));
123
124 splitter_window_ = new wxSplitterWindow(this, wxID_ANY);
125 splitter_window_->SetMinimumPaneSize(logicalSize.x / 5);
126
127 wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY);
128
110 achievements_pane_ = new AchievementsPane(choicebook); 129 achievements_pane_ = new AchievementsPane(choicebook);
111 choicebook->AddPage(achievements_pane_, "Achievements"); 130 choicebook->AddPage(achievements_pane_, "Achievements");
112 131
113 notebook_ = new wxNotebook(this, wxID_ANY); 132 items_pane_ = new ItemsPane(choicebook);
133 choicebook->AddPage(items_pane_, "Items");
134
135 options_pane_ = new OptionsPane(choicebook);
136 choicebook->AddPage(options_pane_, "Options");
137
138 paintings_pane_ = new PaintingsPane(choicebook);
139 choicebook->AddPage(paintings_pane_, "Paintings");
140
141 notebook_ = new wxNotebook(splitter_window_, wxID_ANY);
114 tracker_panel_ = new TrackerPanel(notebook_); 142 tracker_panel_ = new TrackerPanel(notebook_);
115 subway_map_ = new SubwayMap(notebook_); 143 subway_map_ = new SubwayMap(notebook_);
116 notebook_->AddPage(tracker_panel_, "Map"); 144 notebook_->AddPage(tracker_panel_, "Map");
117 notebook_->AddPage(subway_map_, "Subway"); 145 notebook_->AddPage(subway_map_, "Subway");
118 146
119 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 147 splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4);
120 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
121 top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3));
122 148
123 SetSizerAndFit(top_sizer); 149 SetSize(logicalSize);
124 SetSize(1280, 728);
125 150
126 if (!GetTrackerConfig().asked_to_check_for_updates) { 151 if (!GetTrackerConfig().asked_to_check_for_updates) {
127 GetTrackerConfig().asked_to_check_for_updates = true; 152 GetTrackerConfig().asked_to_check_for_updates = true;
@@ -138,7 +163,7 @@ TrackerFrame::TrackerFrame()
138 } 163 }
139 164
140 if (GetTrackerConfig().should_check_for_updates) { 165 if (GetTrackerConfig().should_check_for_updates) {
141 CheckForUpdates(/*manual=*/false); 166 updater_->CheckForUpdates(/*invisible=*/true);
142 } 167 }
143 168
144 SetStatusText(GetStatusMessage()); 169 SetStatusText(GetStatusMessage());
@@ -158,15 +183,8 @@ void TrackerFrame::ResetIndicators() {
158 QueueEvent(new wxCommandEvent(STATE_RESET)); 183 QueueEvent(new wxCommandEvent(STATE_RESET));
159} 184}
160 185
161void TrackerFrame::UpdateIndicators(UpdateIndicatorsMode mode) { 186void TrackerFrame::UpdateIndicators(StateUpdate state) {
162 auto evt = new wxCommandEvent(STATE_CHANGED); 187 QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state)));
163 evt->SetInt(static_cast<int>(mode));
164
165 QueueEvent(evt);
166}
167
168void TrackerFrame::RedrawPosition() {
169 QueueEvent(new wxCommandEvent(REDRAW_POSITION));
170} 188}
171 189
172void TrackerFrame::OnAbout(wxCommandEvent &event) { 190void TrackerFrame::OnAbout(wxCommandEvent &event) {
@@ -231,15 +249,18 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
231 GetTrackerConfig().should_check_for_updates = 249 GetTrackerConfig().should_check_for_updates =
232 dlg.GetShouldCheckForUpdates(); 250 dlg.GetShouldCheckForUpdates();
233 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); 251 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
234 GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); 252 GetTrackerConfig().visible_panels = dlg.GetVisiblePanels();
253 GetTrackerConfig().track_position = dlg.GetTrackPosition();
235 GetTrackerConfig().Save(); 254 GetTrackerConfig().Save();
236 255
237 UpdateIndicators(); 256 UpdateIndicators(StateUpdate{.cleared_locations = true,
257 .player_position = true,
258 .changed_settings = true});
238 } 259 }
239} 260}
240 261
241void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 262void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
242 CheckForUpdates(/*manual=*/true); 263 updater_->CheckForUpdates(/*invisible=*/false);
243} 264}
244 265
245void TrackerFrame::OnZoomIn(wxCommandEvent &event) { 266void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
@@ -254,132 +275,93 @@ void TrackerFrame::OnZoomOut(wxCommandEvent &event) {
254 } 275 }
255} 276}
256 277
257void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { 278void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) {
258 zoom_in_menu_item_->Enable(event.GetSelection() == 1); 279 if (log_dialog_ == nullptr) {
259 zoom_out_menu_item_->Enable(event.GetSelection() == 1); 280 log_dialog_ = new LogDialog(this);
260} 281 log_dialog_->Show();
282 TrackerSetLogDialog(log_dialog_);
261 283
262void TrackerFrame::OnOpenFile(wxCommandEvent &event) { 284 log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow,
263 wxFileDialog open_file_dialog( 285 this);
264 this, "Open Lingo Save File", 286 } else {
265 fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable", 287 log_dialog_->SetFocus();
266 wxStandardPaths::Get().GetUserConfigDir().ToStdString()),
267 AP_GetSaveName(), "Lingo save file (*.save)|*.save",
268 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
269 if (open_file_dialog.ShowModal() == wxID_CANCEL) {
270 return;
271 } 288 }
289}
272 290
273 std::string savedata_path = open_file_dialog.GetPath().ToStdString(); 291void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) {
292 TrackerSetLogDialog(nullptr);
293 log_dialog_ = nullptr;
274 294
275 if (panels_panel_ == nullptr) { 295 event.Skip();
276 panels_panel_ = new TrackerPanel(notebook_); 296}
277 notebook_->AddPage(panels_panel_, "Panels");
278 }
279 297
280 notebook_->SetSelection(notebook_->FindPage(panels_panel_)); 298void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
281 panels_panel_->SetSavedataPath(savedata_path); 299 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
300 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
301}
302
303void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) {
304 notebook_->Refresh();
282} 305}
283 306
284void TrackerFrame::OnStateReset(wxCommandEvent &event) { 307void TrackerFrame::OnStateReset(wxCommandEvent &event) {
285 tracker_panel_->UpdateIndicators(); 308 tracker_panel_->UpdateIndicators(/*reset=*/true);
286 achievements_pane_->UpdateIndicators(); 309 achievements_pane_->UpdateIndicators();
310 items_pane_->ResetIndicators();
311 options_pane_->OnConnect();
312 paintings_pane_->ResetIndicators();
287 subway_map_->OnConnect(); 313 subway_map_->OnConnect();
288 if (panels_panel_ != nullptr) {
289 notebook_->DeletePage(notebook_->FindPage(panels_panel_));
290 panels_panel_ = nullptr;
291 }
292 Refresh(); 314 Refresh();
293} 315}
294 316
295void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 317void TrackerFrame::OnStateChanged(StateChangedEvent &event) {
296 UpdateIndicatorsMode mode = static_cast<UpdateIndicatorsMode>(event.GetInt()); 318 const StateUpdate &state = event.GetState();
297 319
298 if (mode == kUPDATE_ALL_INDICATORS) { 320 bool hunt_panels = false;
299 tracker_panel_->UpdateIndicators(); 321 if (GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) {
300 achievements_pane_->UpdateIndicators(); 322 hunt_panels = std::any_of(
323 state.panels.begin(), state.panels.end(), [](int solve_index) {
324 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index)).hunt;
325 });
326 } else if (GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) {
327 hunt_panels = true;
328 }
329
330 if (!state.items.empty() || !state.paintings.empty() ||
331 state.cleared_locations || hunt_panels) {
332 // TODO: The only real reason to reset tracker_panel during an active
333 // connection is if the hunt panels setting changes. If we remove hunt
334 // panels later, we can get rid of this.
335 tracker_panel_->UpdateIndicators(/*reset=*/state.changed_settings);
301 subway_map_->UpdateIndicators(); 336 subway_map_->UpdateIndicators();
302 if (panels_panel_ != nullptr) {
303 panels_panel_->UpdateIndicators();
304 }
305 Refresh(); 337 Refresh();
306 } else if (mode == kUPDATE_ONLY_PANELS) { 338 } else if (state.player_position && GetTrackerConfig().track_position) {
307 if (panels_panel_ == nullptr) { 339 if (notebook_->GetSelection() == 0) {
308 panels_panel_ = new TrackerPanel(notebook_); 340 tracker_panel_->Refresh();
309 panels_panel_->SetPanelsMode();
310 notebook_->AddPage(panels_panel_, "Panels");
311 }
312 panels_panel_->UpdateIndicators();
313 if (notebook_->GetSelection() == 2) {
314 Refresh();
315 } 341 }
316 } 342 }
317}
318 343
319void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { 344 if (std::any_of(state.panels.begin(), state.panels.end(),
320 SetStatusText(GetStatusMessage()); 345 [](int solve_index) {
321} 346 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index))
322 347 .achievement;
323void TrackerFrame::OnRedrawPosition(wxCommandEvent &event) { 348 })) {
324 if (notebook_->GetSelection() == 0) { 349 achievements_pane_->UpdateIndicators();
325 tracker_panel_->Refresh();
326 } else if (notebook_->GetSelection() == 2) {
327 panels_panel_->Refresh();
328 } 350 }
329}
330
331void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
332 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
333}
334
335void TrackerFrame::CheckForUpdates(bool manual) {
336 wxWebRequest request = wxWebSession::GetDefault().CreateRequest(
337 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION");
338 351
339 if (!request.IsOk()) { 352 if (!state.items.empty()) {
340 if (manual) { 353 items_pane_->UpdateIndicators(state.items);
341 wxMessageBox("Could not check for updates.", "Error", 354 }
342 wxOK | wxICON_ERROR);
343 } else {
344 SetStatusText("Could not check for updates.");
345 }
346 355
347 return; 356 if (!state.paintings.empty()) {
357 paintings_pane_->UpdateIndicators(state.paintings);
348 } 358 }
359}
349 360
350 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { 361void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
351 if (evt.GetState() == wxWebRequest::State_Completed) { 362 SetStatusText(wxString::FromUTF8(GetStatusMessage()));
352 std::string response = evt.GetResponse().AsString().ToStdString(); 363}
353
354 Version latest_version(response);
355 if (kTrackerVersion < latest_version) {
356 std::ostringstream message_text;
357 message_text << "There is a newer version of Lingo AP Tracker "
358 "available. You have "
359 << kTrackerVersion.ToString()
360 << ", and the latest version is "
361 << latest_version.ToString()
362 << ". Would you like to update?";
363
364 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
365 wxYES) {
366 wxLaunchDefaultBrowser(
367 "https://code.fourisland.com/lingo-ap-tracker/about/"
368 "CHANGELOG.md");
369 }
370 } else if (manual) {
371 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
372 wxOK);
373 }
374 } else if (evt.GetState() == wxWebRequest::State_Failed) {
375 if (manual) {
376 wxMessageBox("Could not check for updates.", "Error",
377 wxOK | wxICON_ERROR);
378 } else {
379 SetStatusText("Could not check for updates.");
380 }
381 }
382 });
383 364
384 request.Start(); 365void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
366 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
385} 367}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index e9fec17..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,11 +7,24 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <memory>
11#include <set>
12
13#include "ap_state.h"
14#include "icons.h"
15#include "updater.h"
16
10class AchievementsPane; 17class AchievementsPane;
18class ItemsPane;
19class LogDialog;
20class OptionsPane;
21class PaintingsPane;
11class SubwayMap; 22class SubwayMap;
12class TrackerPanel; 23class TrackerPanel;
13class wxBookCtrlEvent; 24class wxBookCtrlEvent;
14class wxNotebook; 25class wxNotebook;
26class wxSplitterEvent;
27class wxSplitterWindow;
15 28
16class ApConnectEvent : public wxEvent { 29class ApConnectEvent : public wxEvent {
17 public: 30 public:
@@ -36,17 +49,34 @@ class ApConnectEvent : public wxEvent {
36 std::string ap_pass_; 49 std::string ap_pass_;
37}; 50};
38 51
52struct StateUpdate {
53 std::vector<ItemState> items;
54 bool progression_items = false;
55 std::vector<std::string> paintings;
56 bool cleared_locations = false;
57 std::set<int> panels;
58 bool player_position = false;
59 bool changed_settings = false;
60};
61
62class StateChangedEvent : public wxEvent {
63 public:
64 StateChangedEvent(wxEventType eventType, int winid, StateUpdate state)
65 : wxEvent(winid, eventType), state_(std::move(state)) {}
66
67 const StateUpdate &GetState() const { return state_; }
68
69 virtual wxEvent *Clone() const { return new StateChangedEvent(*this); }
70
71 private:
72 StateUpdate state_;
73};
74
39wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); 75wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
40wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 76wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent);
41wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 77wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
42wxDECLARE_EVENT(REDRAW_POSITION, wxCommandEvent);
43wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); 78wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent);
44 79
45enum UpdateIndicatorsMode {
46 kUPDATE_ALL_INDICATORS = 0,
47 kUPDATE_ONLY_PANELS = 1,
48};
49
50class TrackerFrame : public wxFrame { 80class TrackerFrame : public wxFrame {
51 public: 81 public:
52 TrackerFrame(); 82 TrackerFrame();
@@ -55,8 +85,7 @@ class TrackerFrame : public wxFrame {
55 void UpdateStatusMessage(); 85 void UpdateStatusMessage();
56 86
57 void ResetIndicators(); 87 void ResetIndicators();
58 void UpdateIndicators(UpdateIndicatorsMode mode = kUPDATE_ALL_INDICATORS); 88 void UpdateIndicators(StateUpdate state);
59 void RedrawPosition();
60 89
61 private: 90 private:
62 void OnExit(wxCommandEvent &event); 91 void OnExit(wxCommandEvent &event);
@@ -67,25 +96,32 @@ class TrackerFrame : public wxFrame {
67 void OnCheckForUpdates(wxCommandEvent &event); 96 void OnCheckForUpdates(wxCommandEvent &event);
68 void OnZoomIn(wxCommandEvent &event); 97 void OnZoomIn(wxCommandEvent &event);
69 void OnZoomOut(wxCommandEvent &event); 98 void OnZoomOut(wxCommandEvent &event);
99 void OnOpenLogWindow(wxCommandEvent &event);
100 void OnCloseLogWindow(wxCloseEvent &event);
70 void OnChangePage(wxBookCtrlEvent &event); 101 void OnChangePage(wxBookCtrlEvent &event);
71 void OnOpenFile(wxCommandEvent &event); 102 void OnSashPositionChanged(wxSplitterEvent &event);
72 103
73 void OnStateReset(wxCommandEvent &event); 104 void OnStateReset(wxCommandEvent &event);
74 void OnStateChanged(wxCommandEvent &event); 105 void OnStateChanged(StateChangedEvent &event);
75 void OnStatusChanged(wxCommandEvent &event); 106 void OnStatusChanged(wxCommandEvent &event);
76 void OnRedrawPosition(wxCommandEvent &event);
77 void OnConnectToAp(ApConnectEvent &event); 107 void OnConnectToAp(ApConnectEvent &event);
108
109 std::unique_ptr<Updater> updater_;
78 110
79 void CheckForUpdates(bool manual); 111 wxSplitterWindow *splitter_window_;
80
81 wxNotebook *notebook_; 112 wxNotebook *notebook_;
82 TrackerPanel *tracker_panel_; 113 TrackerPanel *tracker_panel_;
83 AchievementsPane *achievements_pane_; 114 AchievementsPane *achievements_pane_;
115 ItemsPane *items_pane_;
116 OptionsPane *options_pane_;
117 PaintingsPane *paintings_pane_;
84 SubwayMap *subway_map_; 118 SubwayMap *subway_map_;
85 TrackerPanel *panels_panel_ = nullptr; 119 LogDialog *log_dialog_ = nullptr;
86 120
87 wxMenuItem *zoom_in_menu_item_; 121 wxMenuItem *zoom_in_menu_item_;
88 wxMenuItem *zoom_out_menu_item_; 122 wxMenuItem *zoom_out_menu_item_;
123
124 IconCache icons_;
89}; 125};
90 126
91#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */ 127#endif /* end of include guard: TRACKER_FRAME_H_86BD8DFB */
diff --git a/src/tracker_panel.cpp b/src/tracker_panel.cpp index 04b970c..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -9,7 +9,6 @@
9#include "area_popup.h" 9#include "area_popup.h"
10#include "game_data.h" 10#include "game_data.h"
11#include "global.h" 11#include "global.h"
12#include "godot_variant.h"
13#include "ipc_state.h" 12#include "ipc_state.h"
14#include "tracker_config.h" 13#include "tracker_config.h"
15#include "tracker_state.h" 14#include "tracker_state.h"
@@ -44,58 +43,46 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
44 areas_.push_back(area); 43 areas_.push_back(area);
45 } 44 }
46 45
46 Resize();
47 Redraw(); 47 Redraw();
48 48
49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); 49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); 50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
51} 51}
52 52
53void TrackerPanel::UpdateIndicators() { 53void TrackerPanel::UpdateIndicators(bool reset) {
54 if (panels_mode_ && !savedata_path_) { 54 if (reset) {
55 solved_panels_ = IPC_GetSolvedPanels(); 55 for (AreaIndicator &area : areas_) {
56 } 56 const MapArea &map_area = GD_GetMapArea(area.area_id);
57 57
58 for (AreaIndicator &area : areas_) { 58 if ((!AP_IsLocationVisible(map_area.classification) ||
59 area.popup->UpdateIndicators(); 59 IsAreaPostgame(area.area_id)) &&
60 } 60 !(map_area.hunt &&
61 61 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
62 Redraw(); 62 !(map_area.has_single_panel &&
63} 63 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) &&
64 64 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
65void TrackerPanel::SetPanelsMode() { panels_mode_ = true; } 65 area.active = false;
66 66 } else {
67void TrackerPanel::SetSavedataPath(std::string savedata_path) { 67 area.active = true;
68 if (!savedata_path_) { 68 }
69 wxButton *refresh_button =
70 new wxButton(this, wxID_ANY, "Refresh", {15, 15});
71 refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this);
72 }
73
74 savedata_path_ = savedata_path;
75 panels_mode_ = true;
76
77 RefreshSavedata();
78}
79 69
80void TrackerPanel::RefreshSavedata() { 70 area.popup->ResetIndicators();
81 solved_panels_.clear(); 71 }
82 72
83 GodotVariant godot_variant = ParseGodotFile(*savedata_path_); 73 Resize();
84 for (const GodotVariant &panel_node : godot_variant.AsArray()) { 74 } else {
85 const std::vector<GodotVariant> &fields = panel_node.AsArray(); 75 for (AreaIndicator &area : areas_) {
86 if (fields[1].AsBool()) { 76 area.popup->UpdateIndicators();
87 const std::vector<std::string> &nodepath = fields[0].AsNodePath();
88 std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]);
89 solved_panels_.insert(key);
90 } 77 }
91 } 78 }
92 79
93 UpdateIndicators(); 80 Redraw();
94 Refresh();
95} 81}
96 82
97void TrackerPanel::OnPaint(wxPaintEvent &event) { 83void TrackerPanel::OnPaint(wxPaintEvent &event) {
98 if (GetSize() != rendered_.GetSize()) { 84 if (GetSize() != rendered_.GetSize()) {
85 Resize();
99 Redraw(); 86 Redraw();
100 } 87 }
101 88
@@ -103,10 +90,13 @@ void TrackerPanel::OnPaint(wxPaintEvent &event) {
103 dc.DrawBitmap(rendered_, 0, 0); 90 dc.DrawBitmap(rendered_, 0, 0);
104 91
105 std::optional<std::tuple<int, int>> player_position; 92 std::optional<std::tuple<int, int>> player_position;
106 if (IPC_IsConnected()) { 93 if (GetTrackerConfig().track_position)
107 player_position = IPC_GetPlayerPosition(); 94 {
108 } else { 95 if (IPC_IsConnected()) {
109 player_position = AP_GetPlayerPosition(); 96 player_position = IPC_GetPlayerPosition();
97 } else {
98 player_position = AP_GetPlayerPosition();
99 }
110 } 100 }
111 101
112 if (player_position.has_value()) { 102 if (player_position.has_value()) {
@@ -142,12 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
142 event.Skip(); 132 event.Skip();
143} 133}
144 134
145void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { 135void TrackerPanel::Resize() {
146 RefreshSavedata(); 136 wxSize panel_size = GetClientSize();
147}
148
149void TrackerPanel::Redraw() {
150 wxSize panel_size = GetSize();
151 wxSize image_size = map_image_.GetSize(); 137 wxSize image_size = map_image_.GetSize();
152 138
153 int final_x = 0; 139 int final_x = 0;
@@ -166,7 +152,7 @@ void TrackerPanel::Redraw() {
166 final_x = (panel_size.GetWidth() - final_width) / 2; 152 final_x = (panel_size.GetWidth() - final_width) / 2;
167 } 153 }
168 154
169 rendered_ = wxBitmap( 155 scaled_map_ = wxBitmap(
170 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) 156 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
171 .Size(panel_size, {final_x, final_y}, 0, 0, 0)); 157 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
172 158
@@ -181,30 +167,61 @@ void TrackerPanel::Redraw() {
181 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, 167 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1,
182 player_height > 0 ? player_height : 1)); 168 player_height > 0 ? player_height : 1));
183 169
170 real_area_size_ = final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
171
172 for (AreaIndicator &area : areas_) {
173 const MapArea &map_area = GD_GetMapArea(area.area_id);
174
175 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
176 final_width / image_size.GetWidth();
177 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
178 final_width / image_size.GetWidth();
179
180 area.real_x1 = real_area_x;
181 area.real_x2 = real_area_x + real_area_size_;
182 area.real_y1 = real_area_y;
183 area.real_y2 = real_area_y + real_area_size_;
184
185 int popup_x =
186 final_x + map_area.map_x * final_width / image_size.GetWidth();
187 int popup_y =
188 final_y + map_area.map_y * final_width / image_size.GetWidth();
189
190 area.popup->SetClientSize(
191 area.popup->GetFullWidth(),
192 std::min(panel_size.GetHeight(), area.popup->GetFullHeight()));
193
194 if (area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
195 area.popup->SetSize(area.popup->GetSize().GetWidth(),
196 panel_size.GetHeight());
197 }
198
199 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
200 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
201 }
202 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
203 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
204 }
205 area.popup->SetPosition({popup_x, popup_y});
206 }
207}
208
209void TrackerPanel::Redraw() {
210 rendered_ = scaled_map_;
211
184 wxMemoryDC dc; 212 wxMemoryDC dc;
185 dc.SelectObject(rendered_); 213 dc.SelectObject(rendered_);
186 214
187 int real_area_size =
188 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
189 int actual_border_size = 215 int actual_border_size =
190 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; 216 real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
191 const wxPoint upper_left_triangle[] = { 217 const wxPoint upper_left_triangle[] = {
192 {0, 0}, {0, real_area_size}, {real_area_size, 0}}; 218 {0, 0}, {0, real_area_size_}, {real_area_size_, 0}};
193 const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, 219 const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1},
194 {real_area_size - 1, 0}, 220 {real_area_size_ - 1, 0},
195 {real_area_size, real_area_size}}; 221 {real_area_size_, real_area_size_}};
196 222
197 for (AreaIndicator &area : areas_) { 223 for (AreaIndicator &area : areas_) {
198 const MapArea &map_area = GD_GetMapArea(area.area_id); 224 const MapArea &map_area = GD_GetMapArea(area.area_id);
199 if (panels_mode_) {
200 area.active = map_area.has_single_panel;
201 } else if (!AP_IsLocationVisible(map_area.classification) &&
202 !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
203 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
204 area.active = false;
205 } else {
206 area.active = true;
207 }
208 225
209 if (!area.active) { 226 if (!area.active) {
210 continue; 227 continue;
@@ -216,19 +233,15 @@ void TrackerPanel::Redraw() {
216 bool has_unchecked = false; 233 bool has_unchecked = false;
217 if (IsLocationWinCondition(section)) { 234 if (IsLocationWinCondition(section)) {
218 has_unchecked = !AP_HasReachedGoal(); 235 has_unchecked = !AP_HasReachedGoal();
219 } else if (panels_mode_) { 236 } else if (AP_IsLocationVisible(section.classification) &&
220 if (section.single_panel) { 237 !IsLocationPostgame(section.ap_location_id)) {
221 const Panel &panel = GD_GetPanel(*section.single_panel);
222 if (panel.non_counting) {
223 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
224 } else {
225 has_unchecked = !GetSolvedPanels().contains(panel.nodepath);
226 }
227 }
228 } else if (AP_IsLocationVisible(section.classification)) {
229 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); 238 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
230 } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { 239 } else if ((section.hunt && GetTrackerConfig().visible_panels ==
231 has_unchecked = !AP_HasCheckedHuntPanel(section.ap_location_id); 240 TrackerConfig::kHUNT_PANELS) ||
241 (section.single_panel && GetTrackerConfig().visible_panels ==
242 TrackerConfig::kALL_PANELS)) {
243 has_unchecked =
244 !AP_IsPanelSolved(GD_GetPanel(*section.single_panel).solve_index);
232 } 245 }
233 246
234 if (has_unchecked) { 247 if (has_unchecked) {
@@ -240,8 +253,12 @@ void TrackerPanel::Redraw() {
240 } 253 }
241 } 254 }
242 255
243 if (AP_IsPaintingShuffle() && !panels_mode_) { 256 if (AP_IsPaintingShuffle()) {
244 for (int painting_id : map_area.paintings) { 257 for (int painting_id : map_area.paintings) {
258 if (IsPaintingPostgame(painting_id)) {
259 continue;
260 }
261
245 const PaintingExit &painting = GD_GetPaintingExit(painting_id); 262 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
246 bool reachable = IsPaintingReachable(painting_id); 263 bool reachable = IsPaintingReachable(painting_id);
247 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { 264 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) {
@@ -254,10 +271,8 @@ void TrackerPanel::Redraw() {
254 } 271 }
255 } 272 }
256 273
257 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 274 int real_area_x = area.real_x1;
258 final_width / image_size.GetWidth(); 275 int real_area_y = area.real_y1;
259 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
260 final_width / image_size.GetWidth();
261 276
262 if (has_reachable_unchecked && has_unreachable_unchecked && 277 if (has_reachable_unchecked && has_unreachable_unchecked &&
263 GetTrackerConfig().hybrid_areas) { 278 GetTrackerConfig().hybrid_areas) {
@@ -271,7 +286,7 @@ void TrackerPanel::Redraw() {
271 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
272 dc.SetBrush(*wxTRANSPARENT_BRUSH); 287 dc.SetBrush(*wxTRANSPARENT_BRUSH);
273 dc.DrawRectangle({real_area_x, real_area_y}, 288 dc.DrawRectangle({real_area_x, real_area_y},
274 {real_area_size, real_area_size}); 289 {real_area_size_, real_area_size_});
275 290
276 } else { 291 } else {
277 const wxBrush *brush_color = wxGREY_BRUSH; 292 const wxBrush *brush_color = wxGREY_BRUSH;
@@ -286,30 +301,7 @@ void TrackerPanel::Redraw() {
286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 301 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
287 dc.SetBrush(*brush_color); 302 dc.SetBrush(*brush_color);
288 dc.DrawRectangle({real_area_x, real_area_y}, 303 dc.DrawRectangle({real_area_x, real_area_y},
289 {real_area_size, real_area_size}); 304 {real_area_size_, real_area_size_});
290 } 305 }
291
292 area.real_x1 = real_area_x;
293 area.real_x2 = real_area_x + real_area_size;
294 area.real_y1 = real_area_y;
295 area.real_y2 = real_area_y + real_area_size;
296
297 int popup_x =
298 final_x + map_area.map_x * final_width / image_size.GetWidth();
299 int popup_y =
300 final_y + map_area.map_y * final_width / image_size.GetWidth();
301
302 area.popup->SetClientSize(
303 area.popup->GetVirtualSize().GetWidth(),
304 std::min(panel_size.GetHeight(),
305 area.popup->GetVirtualSize().GetHeight()));
306
307 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
308 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
309 }
310 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
311 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
312 }
313 area.popup->SetPosition({popup_x, popup_y});
314 } 306 }
315} 307}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index 822d181..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h
@@ -17,17 +17,7 @@ class TrackerPanel : public wxPanel {
17 public: 17 public:
18 TrackerPanel(wxWindow *parent); 18 TrackerPanel(wxWindow *parent);
19 19
20 void UpdateIndicators(); 20 void UpdateIndicators(bool reset);
21
22 void SetPanelsMode();
23
24 void SetSavedataPath(std::string savedata_path);
25
26 bool IsPanelsMode() const { return panels_mode_; }
27
28 const std::set<std::string> &GetSolvedPanels() const {
29 return solved_panels_;
30 }
31 21
32 private: 22 private:
33 struct AreaIndicator { 23 struct AreaIndicator {
@@ -42,14 +32,13 @@ class TrackerPanel : public wxPanel {
42 32
43 void OnPaint(wxPaintEvent &event); 33 void OnPaint(wxPaintEvent &event);
44 void OnMouseMove(wxMouseEvent &event); 34 void OnMouseMove(wxMouseEvent &event);
45 void OnRefreshSavedata(wxCommandEvent &event);
46 35
36 void Resize();
47 void Redraw(); 37 void Redraw();
48 38
49 void RefreshSavedata();
50
51 wxImage map_image_; 39 wxImage map_image_;
52 wxImage player_image_; 40 wxImage player_image_;
41 wxBitmap scaled_map_;
53 wxBitmap rendered_; 42 wxBitmap rendered_;
54 wxBitmap scaled_player_; 43 wxBitmap scaled_player_;
55 44
@@ -57,12 +46,9 @@ class TrackerPanel : public wxPanel {
57 int offset_y_ = 0; 46 int offset_y_ = 0;
58 double scale_x_ = 0; 47 double scale_x_ = 0;
59 double scale_y_ = 0; 48 double scale_y_ = 0;
49 int real_area_size_ = 0;
60 50
61 std::vector<AreaIndicator> areas_; 51 std::vector<AreaIndicator> areas_;
62
63 bool panels_mode_ = false;
64 std::optional<std::string> savedata_path_;
65 std::set<std::string> solved_panels_;
66}; 52};
67 53
68#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ 54#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index eee43e4..bf2725a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -12,6 +12,7 @@
12 12
13#include "ap_state.h" 13#include "ap_state.h"
14#include "game_data.h" 14#include "game_data.h"
15#include "global.h"
15#include "logger.h" 16#include "logger.h"
16 17
17namespace { 18namespace {
@@ -25,6 +26,7 @@ struct Requirements {
25 std::set<int> rooms; // maybe 26 std::set<int> rooms; // maybe
26 bool mastery = false; // maybe 27 bool mastery = false; // maybe
27 bool panel_hunt = false; // maybe 28 bool panel_hunt = false; // maybe
29 bool postgame = false;
28 30
29 void Merge(const Requirements& rhs) { 31 void Merge(const Requirements& rhs) {
30 if (rhs.disabled) { 32 if (rhs.disabled) {
@@ -45,6 +47,7 @@ struct Requirements {
45 } 47 }
46 mastery = mastery || rhs.mastery; 48 mastery = mastery || rhs.mastery;
47 panel_hunt = panel_hunt || rhs.panel_hunt; 49 panel_hunt = panel_hunt || rhs.panel_hunt;
50 postgame = postgame || rhs.postgame;
48 } 51 }
49}; 52};
50 53
@@ -83,8 +86,6 @@ class RequirementCalculator {
83 break; 86 break;
84 } 87 }
85 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) { 88 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) {
86 requirements.rooms.insert(door_obj.room);
87
88 for (int panel_id : door_obj.panels) { 89 for (int panel_id : door_obj.panels) {
89 const Requirements& panel_reqs = GetPanel(panel_id); 90 const Requirements& panel_reqs = GetPanel(panel_id);
90 requirements.Merge(panel_reqs); 91 requirements.Merge(panel_reqs);
@@ -148,6 +149,10 @@ class RequirementCalculator {
148 } 149 }
149 } 150 }
150 151
152 if (panel_obj.location_name == GetWinCondition()) {
153 requirements.postgame = true;
154 }
155
151 panels_[panel_id] = requirements; 156 panels_[panel_id] = requirements;
152 } 157 }
153 158
@@ -162,11 +167,17 @@ class RequirementCalculator {
162struct TrackerState { 167struct TrackerState {
163 std::map<int, bool> reachability; 168 std::map<int, bool> reachability;
164 std::set<int> reachable_doors; 169 std::set<int> reachable_doors;
170 std::set<int> solveable_panels;
165 std::set<int> reachable_paintings; 171 std::set<int> reachable_paintings;
166 std::mutex reachability_mutex; 172 std::mutex reachability_mutex;
167 RequirementCalculator requirements; 173 RequirementCalculator requirements;
168 std::map<int, std::map<std::string, bool>> door_reports; 174 std::map<int, std::map<std::string, bool>> door_reports;
169 bool pilgrimage_doable = false; 175 bool pilgrimage_doable = false;
176
177 // If these are empty, it actually means everything is non-postgame.
178 std::set<int> non_postgame_areas;
179 std::set<int> non_postgame_locations;
180 std::set<int> non_postgame_paintings;
170}; 181};
171 182
172enum Decision { kYes, kNo, kMaybe }; 183enum Decision { kYes, kNo, kMaybe };
@@ -181,6 +192,11 @@ class StateCalculator;
181struct StateCalculatorOptions { 192struct StateCalculatorOptions {
182 int start; 193 int start;
183 bool pilgrimage = false; 194 bool pilgrimage = false;
195
196 // Treats all items as collected and all paintings as checked, but postgame
197 // areas cannot be reached.
198 bool postgame_detection = false;
199
184 StateCalculator* parent = nullptr; 200 StateCalculator* parent = nullptr;
185}; 201};
186 202
@@ -191,6 +207,16 @@ class StateCalculator {
191 explicit StateCalculator(StateCalculatorOptions options) 207 explicit StateCalculator(StateCalculatorOptions options)
192 : options_(options) {} 208 : options_(options) {}
193 209
210 void PreloadPanels(const std::set<int>& panels) {
211 solveable_panels_ = panels;
212 }
213
214 void PreloadDoors(const std::set<int>& doors) {
215 for (int door_id : doors) {
216 door_decisions_[door_id] = kYes;
217 }
218 }
219
194 void Calculate() { 220 void Calculate() {
195 painting_mapping_ = AP_GetPaintingMapping(); 221 painting_mapping_ = AP_GetPaintingMapping();
196 checked_paintings_ = AP_GetCheckedPaintings(); 222 checked_paintings_ = AP_GetCheckedPaintings();
@@ -236,7 +262,8 @@ class StateCalculator {
236 262
237 PaintingExit cur_painting = GD_GetPaintingExit(painting_id); 263 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
238 if (painting_mapping_.count(cur_painting.internal_id) && 264 if (painting_mapping_.count(cur_painting.internal_id) &&
239 checked_paintings_.count(cur_painting.internal_id)) { 265 (checked_paintings_.count(cur_painting.internal_id) ||
266 options_.postgame_detection)) {
240 Exit painting_exit; 267 Exit painting_exit;
241 PaintingExit target_painting = 268 PaintingExit target_painting =
242 GD_GetPaintingExit(GD_GetPaintingByName( 269 GD_GetPaintingExit(GD_GetPaintingByName(
@@ -360,6 +387,10 @@ class StateCalculator {
360 // evaluated. 387 // evaluated.
361 for (const Door& door : GD_GetDoors()) { 388 for (const Door& door : GD_GetDoors()) {
362 int discard = IsDoorReachable(door.id); 389 int discard = IsDoorReachable(door.id);
390
391 door_report_[door.id] = {};
392 discard = AreRequirementsSatisfied(
393 GetState().requirements.GetDoor(door.id), &door_report_[door.id]);
363 } 394 }
364 } 395 }
365 396
@@ -417,43 +448,48 @@ class StateCalculator {
417 return kNo; 448 return kNo;
418 } 449 }
419 450
451 if (reqs.postgame && options_.postgame_detection) {
452 return kNo;
453 }
454
420 Decision final_decision = kYes; 455 Decision final_decision = kYes;
421 456
422 for (int door_id : reqs.doors) { 457 if (!options_.postgame_detection) {
423 const Door& door_obj = GD_GetDoor(door_id); 458 for (int door_id : reqs.doors) {
424 Decision decision = IsNonGroupedDoorReachable(door_obj); 459 const Door& door_obj = GD_GetDoor(door_id);
460 Decision decision = IsNonGroupedDoorReachable(door_obj);
425 461
426 if (report) { 462 if (report) {
427 (*report)[door_obj.item_name] = (decision == kYes); 463 (*report)[door_obj.item_name] = (decision == kYes);
428 } 464 }
429 465
430 if (decision != kYes) { 466 if (decision != kYes) {
431 final_decision = decision; 467 final_decision = decision;
468 }
432 } 469 }
433 }
434 470
435 for (int panel_door_id : reqs.panel_doors) { 471 for (int panel_door_id : reqs.panel_doors) {
436 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id); 472 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id);
437 Decision decision = IsNonGroupedDoorReachable(panel_door_obj); 473 Decision decision = IsNonGroupedDoorReachable(panel_door_obj);
438 474
439 if (report) { 475 if (report) {
440 (*report)[AP_GetItemName(panel_door_obj.ap_item_id)] = 476 (*report)[panel_door_obj.item_name] = (decision == kYes);
441 (decision == kYes); 477 }
442 }
443 478
444 if (decision != kYes) { 479 if (decision != kYes) {
445 final_decision = decision; 480 final_decision = decision;
481 }
446 } 482 }
447 }
448 483
449 for (int item_id : reqs.items) { 484 for (int item_id : reqs.items) {
450 bool has_item = AP_HasItem(item_id); 485 bool has_item = AP_HasItem(item_id);
451 if (report) { 486 if (report) {
452 (*report)[AP_GetItemName(item_id)] = has_item; 487 (*report)[GD_GetItemName(item_id)] = has_item;
453 } 488 }
454 489
455 if (!has_item) { 490 if (!has_item) {
456 final_decision = kNo; 491 final_decision = kNo;
492 }
457 } 493 }
458 } 494 }
459 495
@@ -522,14 +558,7 @@ class StateCalculator {
522 } 558 }
523 559
524 Decision IsDoorReachable_Helper(int door_id) { 560 Decision IsDoorReachable_Helper(int door_id) {
525 if (door_report_.count(door_id)) { 561 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id));
526 door_report_[door_id].clear();
527 } else {
528 door_report_[door_id] = {};
529 }
530
531 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id),
532 &door_report_[door_id]);
533 } 562 }
534 563
535 Decision IsDoorReachable(int door_id) { 564 Decision IsDoorReachable(int door_id) {
@@ -661,18 +690,85 @@ class StateCalculator {
661} // namespace 690} // namespace
662 691
663void ResetReachabilityRequirements() { 692void ResetReachabilityRequirements() {
693 TrackerLog("Resetting tracker state...");
694
664 std::lock_guard reachability_guard(GetState().reachability_mutex); 695 std::lock_guard reachability_guard(GetState().reachability_mutex);
665 GetState().requirements.Reset(); 696 GetState().requirements.Reset();
697 GetState().reachable_doors.clear();
698 GetState().solveable_panels.clear();
699
700 if (AP_IsPostgameShuffle()) {
701 GetState().non_postgame_areas.clear();
702 GetState().non_postgame_locations.clear();
703 GetState().non_postgame_paintings.clear();
704 } else {
705 StateCalculator postgame_calculator(
706 {.start = GD_GetRoomByName("Menu"), .postgame_detection = true});
707 postgame_calculator.Calculate();
708
709 std::set<int>& non_postgame_areas = GetState().non_postgame_areas;
710 non_postgame_areas.clear();
711
712 std::set<int>& non_postgame_locations = GetState().non_postgame_locations;
713 non_postgame_locations.clear();
714
715 const std::set<int>& reachable_rooms =
716 postgame_calculator.GetReachableRooms();
717 const std::set<int>& solveable_panels =
718 postgame_calculator.GetSolveablePanels();
719
720 for (const MapArea& map_area : GD_GetMapAreas()) {
721 bool area_reachable = false;
722
723 for (const Location& location_section : map_area.locations) {
724 bool reachable = reachable_rooms.count(location_section.room);
725 if (reachable) {
726 for (int panel_id : location_section.panels) {
727 reachable &= (solveable_panels.count(panel_id) == 1);
728 }
729 }
730
731 if (!reachable && IsLocationWinCondition(location_section)) {
732 reachable = true;
733 }
734
735 if (reachable) {
736 non_postgame_locations.insert(location_section.ap_location_id);
737 area_reachable = true;
738 }
739 }
740
741 for (int painting_id : map_area.paintings) {
742 if (postgame_calculator.GetReachablePaintings().count(painting_id)) {
743 area_reachable = true;
744 }
745 }
746
747 if (area_reachable) {
748 non_postgame_areas.insert(map_area.id);
749 }
750 }
751
752 GetState().non_postgame_paintings =
753 postgame_calculator.GetReachablePaintings();
754 }
666} 755}
667 756
668void RecalculateReachability() { 757void RecalculateReachability() {
758 TrackerLog("Calculating reachability...");
759
669 std::lock_guard reachability_guard(GetState().reachability_mutex); 760 std::lock_guard reachability_guard(GetState().reachability_mutex);
670 761
762 // Receiving items and checking paintings should never remove access to doors
763 // or panels, so we can preload any doors and panels we already know are
764 // accessible from previous runs, in order to reduce the work.
671 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 765 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
766 state_calculator.PreloadDoors(GetState().reachable_doors);
767 state_calculator.PreloadPanels(GetState().solveable_panels);
672 state_calculator.Calculate(); 768 state_calculator.Calculate();
673 769
674 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); 770 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms();
675 const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); 771 std::set<int> solveable_panels = state_calculator.GetSolveablePanels();
676 772
677 std::map<int, bool> new_reachability; 773 std::map<int, bool> new_reachability;
678 for (const MapArea& map_area : GD_GetMapAreas()) { 774 for (const MapArea& map_area : GD_GetMapAreas()) {
@@ -703,6 +799,7 @@ void RecalculateReachability() {
703 799
704 std::swap(GetState().reachability, new_reachability); 800 std::swap(GetState().reachability, new_reachability);
705 std::swap(GetState().reachable_doors, new_reachable_doors); 801 std::swap(GetState().reachable_doors, new_reachable_doors);
802 std::swap(GetState().solveable_panels, solveable_panels);
706 std::swap(GetState().reachable_paintings, reachable_paintings); 803 std::swap(GetState().reachable_paintings, reachable_paintings);
707 std::swap(GetState().door_reports, door_reports); 804 std::swap(GetState().door_reports, door_reports);
708 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); 805 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable();
@@ -741,3 +838,33 @@ bool IsPilgrimageDoable() {
741 838
742 return GetState().pilgrimage_doable; 839 return GetState().pilgrimage_doable;
743} 840}
841
842bool IsAreaPostgame(int area_id) {
843 std::lock_guard reachability_guard(GetState().reachability_mutex);
844
845 if (GetState().non_postgame_areas.empty()) {
846 return false;
847 } else {
848 return !GetState().non_postgame_areas.count(area_id);
849 }
850}
851
852bool IsLocationPostgame(int location_id) {
853 std::lock_guard reachability_guard(GetState().reachability_mutex);
854
855 if (GetState().non_postgame_locations.empty()) {
856 return false;
857 } else {
858 return !GetState().non_postgame_locations.count(location_id);
859 }
860}
861
862bool IsPaintingPostgame(int painting_id) {
863 std::lock_guard reachability_guard(GetState().reachability_mutex);
864
865 if (GetState().non_postgame_paintings.empty()) {
866 return false;
867 } else {
868 return !GetState().non_postgame_paintings.count(painting_id);
869 }
870}
diff --git a/src/tracker_state.h b/src/tracker_state.h index a8f155d..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h
@@ -18,4 +18,10 @@ const std::map<std::string, bool>& GetDoorRequirements(int door_id);
18 18
19bool IsPilgrimageDoable(); 19bool IsPilgrimageDoable();
20 20
21bool IsAreaPostgame(int area_id);
22
23bool IsLocationPostgame(int location_id);
24
25bool IsPaintingPostgame(int painting_id);
26
21#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ 27#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..2b05daf --- /dev/null +++ b/src/updater.cpp
@@ -0,0 +1,309 @@
1#include "updater.h"
2
3#include <fmt/core.h>
4#include <openssl/evp.h>
5#include <openssl/sha.h>
6#include <wx/evtloop.h>
7#include <wx/progdlg.h>
8#include <wx/webrequest.h>
9#include <wx/wfstream.h>
10#include <wx/zipstrm.h>
11#include <yaml-cpp/yaml.h>
12
13#include <cstdio>
14#include <deque>
15#include <filesystem>
16#include <fstream>
17
18#include "global.h"
19#include "logger.h"
20#include "version.h"
21
22constexpr const char* kVersionFileUrl =
23 "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
24constexpr const char* kChangelogUrl =
25 "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
26
27namespace {
28
29std::string CalculateStringSha256(const wxString& data) {
30 unsigned char hash[SHA256_DIGEST_LENGTH];
31 EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
32 EVP_DigestInit(sha256, EVP_sha256());
33 EVP_DigestUpdate(sha256, data.c_str(), data.length());
34 EVP_DigestFinal_ex(sha256, hash, nullptr);
35 EVP_MD_CTX_free(sha256);
36
37 char output[65] = {0};
38 for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
39 snprintf(output + (i * 2), 3, "%02x", hash[i]);
40 }
41
42 return std::string(output);
43}
44
45} // namespace
46
47Updater::Updater(wxFrame* parent) : parent_(parent) {
48 Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
49}
50
51void Updater::Cleanup() {
52 std::filesystem::path oldDir = GetExecutableDirectory() / "old";
53 if (std::filesystem::is_directory(oldDir)) {
54 std::filesystem::remove_all(oldDir);
55 }
56}
57
58void Updater::CheckForUpdates(bool invisible) {
59 wxWebRequest versionRequest =
60 wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);
61
62 if (invisible) {
63 update_state_ = UpdateState::GetVersionInvisible;
64
65 versionRequest.Start();
66 } else {
67 update_state_ = UpdateState::GetVersionManual;
68
69 if (DownloadWithProgress(versionRequest)) {
70 if (versionRequest.GetState() == wxWebRequest::State_Failed) {
71 wxMessageBox("Could not check for updates.", "Error",
72 wxOK | wxICON_ERROR);
73 } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
74 ProcessVersionFile(
75 versionRequest.GetResponse().AsString().utf8_string());
76 }
77 }
78 }
79}
80
81void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
82 if (update_state_ == UpdateState::GetVersionInvisible) {
83 if (evt.GetState() == wxWebRequest::State_Completed) {
84 ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
85 } else if (evt.GetState() == wxWebRequest::State_Failed) {
86 parent_->SetStatusText("Could not check for updates.");
87 }
88 }
89}
90
91void Updater::ProcessVersionFile(std::string data) {
92 try {
93 YAML::Node versionInfo = YAML::Load(data);
94 Version latestVersion(versionInfo["version"].as<std::string>());
95
96 if (kTrackerVersion < latestVersion) {
97 if (versionInfo["packages"]) {
98 std::string platformIdentifier;
99
100 if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
101 platformIdentifier = "win64";
102 }
103
104 if (!platformIdentifier.empty() &&
105 versionInfo["packages"][platformIdentifier]) {
106 wxMessageDialog dialog(
107 nullptr,
108 fmt::format("There is a newer version of Lingo AP Tracker "
109 "available. You have {}, and the latest version is "
110 "{}. Would you like to update?",
111 kTrackerVersion.ToString(), latestVersion.ToString()),
112 "Update available", wxYES_NO | wxCANCEL);
113 dialog.SetYesNoLabels("Install update", "Open changelog");
114
115 int dlgResult = dialog.ShowModal();
116 if (dlgResult == wxID_YES) {
117 const YAML::Node& packageInfo =
118 versionInfo["packages"][platformIdentifier];
119 std::string packageUrl = packageInfo["url"].as<std::string>();
120 std::string packageChecksum =
121 packageInfo["checksum"].as<std::string>();
122
123 std::vector<std::filesystem::path> packageFiles;
124 if (packageInfo["files"]) {
125 for (const YAML::Node& filename : packageInfo["files"]) {
126 packageFiles.push_back(filename.as<std::string>());
127 }
128 }
129
130 std::vector<std::filesystem::path> deletedFiles;
131 if (packageInfo["deleted_files"]) {
132 for (const YAML::Node& filename : packageInfo["deleted_files"]) {
133 deletedFiles.push_back(filename.as<std::string>());
134 }
135 }
136
137 InstallUpdate(packageUrl, packageChecksum, packageFiles,
138 deletedFiles);
139 } else if (dlgResult == wxID_NO) {
140 wxLaunchDefaultBrowser(kChangelogUrl);
141 }
142
143 return;
144 }
145 }
146
147 if (wxMessageBox(
148 fmt::format("There is a newer version of Lingo AP Tracker "
149 "available. You have {}, and the latest version is "
150 "{}. Would you like to update?",
151 kTrackerVersion.ToString(), latestVersion.ToString()),
152 "Update available", wxYES_NO) == wxYES) {
153 wxLaunchDefaultBrowser(kChangelogUrl);
154 }
155 } else if (update_state_ == UpdateState::GetVersionManual) {
156 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
157 }
158 } catch (const std::exception& ex) {
159 wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
160 }
161}
162
163void Updater::InstallUpdate(std::string url, std::string checksum,
164 std::vector<std::filesystem::path> files,
165 std::vector<std::filesystem::path> deletedFiles) {
166 update_state_ = UpdateState::GetPackage;
167
168 wxWebRequest packageRequest =
169 wxWebSession::GetDefault().CreateRequest(this, url);
170
171 if (!DownloadWithProgress(packageRequest)) {
172 return;
173 }
174
175 bool download_issue = false;
176
177 wxFileName package_path;
178 package_path.AssignTempFileName("");
179
180 if (!package_path.IsOk()) {
181 download_issue = true;
182 } else {
183 wxFileOutputStream writeOut(package_path.GetFullPath());
184 wxString fileData = packageRequest.GetResponse().AsString();
185 writeOut.WriteAll(fileData.c_str(), fileData.length());
186
187 std::string downloadedChecksum = CalculateStringSha256(fileData);
188 if (downloadedChecksum != checksum) {
189 download_issue = true;
190 }
191 }
192
193 if (download_issue) {
194 if (wxMessageBox("There was an issue downloading the update. Would you "
195 "like to manually download it instead?",
196 "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
197 wxLaunchDefaultBrowser(kChangelogUrl);
198 }
199 return;
200 }
201
202 std::filesystem::path newArea = GetExecutableDirectory();
203 std::filesystem::path oldArea = newArea / "old";
204 std::set<std::filesystem::path> folders;
205 std::set<std::filesystem::path> filesToMove;
206 for (const std::filesystem::path& existingFile : files) {
207 std::filesystem::path movedPath = oldArea / existingFile;
208 std::filesystem::path movedDir = movedPath;
209 movedDir.remove_filename();
210 folders.insert(movedDir);
211 filesToMove.insert(existingFile);
212 }
213 for (const std::filesystem::path& existingFile : deletedFiles) {
214 std::filesystem::path movedPath = oldArea / existingFile;
215 std::filesystem::path movedDir = movedPath;
216 movedDir.remove_filename();
217 folders.insert(movedDir);
218 }
219
220 for (const std::filesystem::path& newFolder : folders) {
221 TrackerLog(fmt::format("Creating directory {}", newFolder.string()));
222
223 std::filesystem::create_directories(newFolder);
224 }
225
226 for (const std::filesystem::path& existingFile : files) {
227 std::filesystem::path existingPath = newArea / existingFile;
228
229 if (std::filesystem::is_regular_file(existingPath)) {
230 std::filesystem::path movedPath = oldArea / existingFile;
231
232 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
233 movedPath.string()));
234
235 std::filesystem::rename(existingPath, movedPath);
236 }
237 }
238 for (const std::filesystem::path& existingFile : deletedFiles) {
239 std::filesystem::path existingPath = newArea / existingFile;
240
241 if (std::filesystem::is_regular_file(existingPath)) {
242 std::filesystem::path movedPath = oldArea / existingFile;
243
244 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
245 movedPath.string()));
246
247 std::filesystem::rename(existingPath, movedPath);
248 }
249 }
250
251 wxFileInputStream fileInputStream(package_path.GetFullPath());
252 wxZipInputStream zipStream(fileInputStream);
253 std::unique_ptr<wxZipEntry> zipEntry;
254 while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
255 nullptr) {
256 if (zipEntry->IsDir()) {
257 continue;
258 }
259
260 std::filesystem::path archivePath = zipEntry->GetName().utf8_string();
261
262 TrackerLog(fmt::format("Found {} in archive", archivePath.string()));
263
264 // Cut off the root folder name
265 std::filesystem::path subPath;
266 for (auto it = std::next(archivePath.begin()); it != archivePath.end();
267 it++) {
268 subPath /= *it;
269 }
270
271 std::filesystem::path pastePath = newArea / subPath;
272
273 wxFileOutputStream fileOutput(pastePath.string());
274 zipStream.Read(fileOutput);
275 }
276
277 if (wxMessageBox(
278 "Update installed! The tracker must be restarted for the changes to take "
279 "effect. Do you want to close the tracker?",
280 "Update installed", wxYES_NO) == wxYES) {
281 wxExit();
282 }
283}
284
285bool Updater::DownloadWithProgress(wxWebRequest& request) {
286 request.Start();
287
288 wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
289 100, nullptr,
290 wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
291 wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
292 while (request.GetState() != wxWebRequest::State_Completed &&
293 request.GetState() != wxWebRequest::State_Failed) {
294 if (request.GetBytesExpectedToReceive() == -1) {
295 if (!dialog.Pulse()) {
296 request.Cancel();
297 return false;
298 }
299 } else {
300 dialog.SetRange(request.GetBytesExpectedToReceive());
301 if (!dialog.Update(request.GetBytesReceived())) {
302 request.Cancel();
303 return false;
304 }
305 }
306 }
307
308 return true;
309}
diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..c604a49 --- /dev/null +++ b/src/updater.h
@@ -0,0 +1,46 @@
1#ifndef UPDATER_H_809E7381
2#define UPDATER_H_809E7381
3
4#include <filesystem>
5#include <set>
6#include <string>
7
8#include <wx/wxprec.h>
9
10#ifndef WX_PRECOMP
11#include <wx/wx.h>
12#endif
13
14class wxWebRequest;
15class wxWebRequestEvent;
16
17class Updater : public wxEvtHandler {
18 public:
19 explicit Updater(wxFrame* parent);
20
21 void Cleanup();
22
23 void CheckForUpdates(bool invisible);
24
25 private:
26 enum class UpdateState {
27 GetVersionInvisible,
28 GetVersionManual,
29 GetPackage,
30 };
31
32 void OnWebRequestState(wxWebRequestEvent& event);
33
34 void ProcessVersionFile(std::string data);
35
36 void InstallUpdate(std::string url, std::string checksum,
37 std::vector<std::filesystem::path> files,
38 std::vector<std::filesystem::path> deletedFiles);
39
40 bool DownloadWithProgress(wxWebRequest& request);
41
42 wxFrame* parent_;
43 UpdateState update_state_ = UpdateState::GetVersionInvisible;
44};
45
46#endif /* end of include guard: UPDATER_H_809E7381 */
diff --git a/src/version.h b/src/version.h index f734f02..3439fda 100644 --- a/src/version.h +++ b/src/version.h
@@ -36,6 +36,6 @@ struct Version {
36 } 36 }
37}; 37};
38 38
39constexpr const Version kTrackerVersion = Version(0, 12, 0); 39constexpr const Version kTrackerVersion = Version(2, 0, 2);
40 40
41#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file 41#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file
diff --git a/src/windows.rc b/src/windows.rc new file mode 100644 index 0000000..8ba30ed --- /dev/null +++ b/src/windows.rc
@@ -0,0 +1,3 @@
1#define wxUSE_RC_MANIFEST 1
2#define wxUSE_DPI_AWARE_MANIFEST 2
3#include "wx/msw/wx.rc"