diff options
Diffstat (limited to 'src')
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 | ||
25 | void AchievementsPane::UpdateIndicators() { | 26 | void 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 | |||
12 | class AchievementsPane : public wxListView { | 16 | class 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 | ||
30 | constexpr int AP_MAJOR = 0; | 31 | constexpr int AP_MAJOR = 0; |
31 | constexpr int AP_MINOR = 4; | 32 | constexpr int AP_MINOR = 6; |
32 | constexpr int AP_REVISION = 5; | 33 | constexpr int AP_REVISION = 1; |
33 | 34 | ||
34 | constexpr const char* CERT_STORE_PATH = "cacert.pem"; | 35 | constexpr const char* CERT_STORE_PATH = "cacert.pem"; |
35 | constexpr int ITEM_HANDLING = 7; // <- all | 36 | constexpr int ITEM_HANDLING = 7; // <- all |
@@ -37,8 +38,24 @@ constexpr int ITEM_HANDLING = 7; // <- all | |||
37 | constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds | 38 | constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds |
38 | constexpr int CONNECTION_BACKOFF_INTERVAL = 100; | 39 | constexpr int CONNECTION_BACKOFF_INTERVAL = 100; |
39 | 40 | ||
41 | constexpr int PANEL_COUNT = 803; | ||
42 | constexpr int PANEL_BITFIELD_LENGTH = 48; | ||
43 | constexpr int PANEL_BITFIELDS = 17; | ||
44 | |||
40 | namespace { | 45 | namespace { |
41 | 46 | ||
47 | const 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 | |||
42 | struct APState { | 59 | struct 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 | ||
642 | bool AP_HasCheckedHuntPanel(int location_id) { | ||
643 | return GetState().HasCheckedHuntPanel(location_id); | ||
644 | } | ||
645 | |||
646 | bool AP_HasItem(int item_id, int quantity) { | 721 | bool 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 | ||
650 | std::string AP_GetItemName(int item_id) { | 725 | bool AP_HasItemSafe(int item_id, int quantity) { |
651 | return GetState().GetItemName(item_id); | 726 | return GetState().HasItemSafe(item_id, quantity); |
652 | } | 727 | } |
653 | 728 | ||
654 | DoorShuffleMode AP_GetDoorShuffleMode() { | 729 | DoorShuffleMode 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 | ||
773 | void AP_RevealPaintings() { GetState().RevealPaintings(); } | ||
774 | |||
698 | int AP_GetMasteryRequirement() { | 775 | int 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 | ||
787 | LocationChecks AP_GetLocationsChecks() { | ||
788 | std::lock_guard state_guard(GetState().state_mutex); | ||
789 | |||
790 | return GetState().location_checks; | ||
791 | } | ||
792 | |||
710 | bool AP_IsLocationVisible(int classification) { | 793 | bool 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 | ||
737 | VictoryCondition AP_GetVictoryCondition() { | 820 | PanelShuffleMode 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 | ||
743 | bool AP_HasAchievement(const std::string& achievement_name) { | 826 | VictoryCondition 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 | ||
747 | bool AP_HasEarlyColorHallways() { | 832 | bool 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 | ||
872 | bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; } | ||
873 | |||
787 | bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } | 874 | bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } |
788 | 875 | ||
789 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { | 876 | std::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 | |||
882 | bool 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 | ||
29 | enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 }; | ||
30 | |||
29 | enum SunwarpAccess { | 31 | enum 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 | ||
44 | struct ItemState { | ||
45 | std::string name; | ||
46 | int amount = 0; | ||
47 | int index = 0; | ||
48 | }; | ||
49 | |||
42 | void AP_SetTrackerFrame(TrackerFrame* tracker_frame); | 50 | void AP_SetTrackerFrame(TrackerFrame* tracker_frame); |
43 | 51 | ||
44 | void AP_Connect(std::string server, std::string player, std::string password); | 52 | void AP_Connect(std::string server, std::string player, std::string password); |
@@ -49,16 +57,11 @@ std::string AP_GetSaveName(); | |||
49 | 57 | ||
50 | bool AP_HasCheckedGameLocation(int location_id); | 58 | bool AP_HasCheckedGameLocation(int location_id); |
51 | 59 | ||
52 | bool 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. |
56 | bool AP_HasItem(int item_id, int quantity = 1); | 62 | bool 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 | 64 | bool AP_HasItemSafe(int item_id, int quantity = 1); |
59 | // RecalculateReachability, which is only called from within a client callback | ||
60 | // anyway. | ||
61 | std::string AP_GetItemName(int item_id); | ||
62 | 65 | ||
63 | DoorShuffleMode AP_GetDoorShuffleMode(); | 66 | DoorShuffleMode AP_GetDoorShuffleMode(); |
64 | 67 | ||
@@ -76,15 +79,19 @@ std::set<std::string> AP_GetCheckedPaintings(); | |||
76 | 79 | ||
77 | bool AP_IsPaintingChecked(const std::string& painting_id); | 80 | bool AP_IsPaintingChecked(const std::string& painting_id); |
78 | 81 | ||
82 | void AP_RevealPaintings(); | ||
83 | |||
79 | int AP_GetMasteryRequirement(); | 84 | int AP_GetMasteryRequirement(); |
80 | 85 | ||
81 | int AP_GetLevel2Requirement(); | 86 | int AP_GetLevel2Requirement(); |
82 | 87 | ||
88 | LocationChecks AP_GetLocationsChecks(); | ||
89 | |||
83 | bool AP_IsLocationVisible(int classification); | 90 | bool AP_IsLocationVisible(int classification); |
84 | 91 | ||
85 | VictoryCondition AP_GetVictoryCondition(); | 92 | PanelShuffleMode AP_GetPanelShuffleMode(); |
86 | 93 | ||
87 | bool AP_HasAchievement(const std::string& achievement_name); | 94 | VictoryCondition AP_GetVictoryCondition(); |
88 | 95 | ||
89 | bool AP_HasEarlyColorHallways(); | 96 | bool AP_HasEarlyColorHallways(); |
90 | 97 | ||
@@ -100,8 +107,12 @@ bool AP_IsSunwarpShuffle(); | |||
100 | 107 | ||
101 | std::map<int, SunwarpMapping> AP_GetSunwarpMapping(); | 108 | std::map<int, SunwarpMapping> AP_GetSunwarpMapping(); |
102 | 109 | ||
110 | bool AP_IsPostgameShuffle(); | ||
111 | |||
103 | bool AP_HasReachedGoal(); | 112 | bool AP_HasReachedGoal(); |
104 | 113 | ||
105 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); | 114 | std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); |
106 | 115 | ||
116 | bool 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 | ||
36 | void AreaPopup::UpdateIndicators() { | 33 | void 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 | |||
104 | void 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 | |||
186 | void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) { | ||
187 | LoadIcons(); | ||
188 | ResetIndicators(); | ||
189 | |||
190 | event.Skip(); | ||
191 | } | ||
192 | |||
193 | void 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 | |||
10 | class AreaPopup : public wxScrolledCanvas { | 12 | class 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 | ||
5 | ConnectionDialog::ConnectionDialog() | 5 | ConnectionDialog::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 | ||
63 | void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { | 70 | void 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 | ||
13 | namespace { | 13 | namespace { |
14 | 14 | ||
15 | LingoColor 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 | |||
41 | struct GameData { | 15 | struct 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 | ||
986 | int GD_GetPanelBySolveIndex(int solve_index) { | ||
987 | return GetState().panel_by_solve_index_.at(solve_index); | ||
988 | } | ||
989 | |||
990 | const std::vector<PaintingExit> &GD_GetPaintings() { | ||
991 | return GetState().paintings_; | ||
992 | } | ||
993 | |||
956 | const PaintingExit &GD_GetPaintingExit(int painting_id) { | 994 | const 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) { | |||
995 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { | 1033 | int 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 | |||
1037 | std::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 | |||
1046 | LingoColor 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 | ||
62 | struct ProgressiveRequirement { | 63 | struct ProgressiveRequirement { |
@@ -85,10 +86,10 @@ struct Door { | |||
85 | }; | 86 | }; |
86 | 87 | ||
87 | struct PanelDoor { | 88 | struct 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 | ||
94 | struct Exit { | 95 | struct 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 | ||
109 | struct Room { | 112 | struct 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(); | |||
173 | const Door& GD_GetDoor(int door_id); | 177 | const Door& GD_GetDoor(int door_id); |
174 | int GD_GetDoorByName(const std::string& name); | 178 | int GD_GetDoorByName(const std::string& name); |
175 | const Panel& GD_GetPanel(int panel_id); | 179 | const Panel& GD_GetPanel(int panel_id); |
180 | int GD_GetPanelBySolveIndex(int solve_index); | ||
176 | const PanelDoor& GD_GetPanelDoor(int panel_door_id); | 181 | const PanelDoor& GD_GetPanelDoor(int panel_door_id); |
182 | const std::vector<PaintingExit>& GD_GetPaintings(); | ||
177 | const PaintingExit& GD_GetPaintingExit(int painting_id); | 183 | const PaintingExit& GD_GetPaintingExit(int painting_id); |
178 | int GD_GetPaintingByName(const std::string& name); | 184 | int GD_GetPaintingByName(const std::string& name); |
179 | const std::vector<int>& GD_GetAchievementPanels(); | 185 | const std::vector<int>& GD_GetAchievementPanels(); |
@@ -184,5 +190,8 @@ const std::vector<SubwayItem>& GD_GetSubwayItems(); | |||
184 | const SubwayItem& GD_GetSubwayItem(int id); | 190 | const SubwayItem& GD_GetSubwayItem(int id); |
185 | std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); | 191 | std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); |
186 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); | 192 | int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); |
193 | std::string GD_GetItemName(int id); | ||
194 | |||
195 | LingoColor 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 | ||
29 | bool IsLocationWinCondition(const Location& location) { | 29 | std::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 | |||
42 | bool 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 | ||
11 | std::string GetAbsolutePath(std::string_view path); | 11 | std::string GetAbsolutePath(std::string_view path); |
12 | 12 | ||
13 | std::string GetWinCondition(); | ||
14 | |||
13 | bool IsLocationWinCondition(const Location& location); | 15 | bool 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 | |||
15 | namespace { | ||
16 | |||
17 | uint16_t ReadUint16(std::basic_istream<char>& stream) { | ||
18 | uint16_t result; | ||
19 | stream.read(reinterpret_cast<char*>(&result), 2); | ||
20 | return result; | ||
21 | } | ||
22 | |||
23 | uint32_t ReadUint32(std::basic_istream<char>& stream) { | ||
24 | uint32_t result; | ||
25 | stream.read(reinterpret_cast<char*>(&result), 4); | ||
26 | return result; | ||
27 | } | ||
28 | |||
29 | GodotVariant 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 | |||
80 | GodotVariant 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 | |||
8 | struct 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 | |||
26 | GodotVariant 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 | |||
5 | const 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 | |||
18 | static IconCache* ICON_CACHE_INSTANCE = nullptr; | ||
19 | |||
20 | void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; } | ||
21 | |||
22 | IconCache& 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 | |||
14 | class 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 | |||
22 | void SetTheIconCache(IconCache* instance); | ||
23 | IconCache& 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(); } | |||
382 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { | 365 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() { |
383 | return GetState().GetPlayerPosition(); | 366 | return GetState().GetPlayerPosition(); |
384 | } | 367 | } |
385 | |||
386 | std::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 | ||
21 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition(); | 21 | std::optional<std::tuple<int, int>> IPC_GetPlayerPosition(); |
22 | 22 | ||
23 | std::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 | |||
5 | namespace { | ||
6 | |||
7 | enum 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 | |||
16 | inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) { | ||
17 | return static_cast<SortInstruction>(static_cast<int>(lhs) | | ||
18 | static_cast<int>(rhs)); | ||
19 | } | ||
20 | |||
21 | template <typename T> | ||
22 | int 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 | |||
32 | int 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 | |||
51 | ItemsPane::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 | |||
62 | void ItemsPane::ResetIndicators() { | ||
63 | DeleteAllItems(); | ||
64 | items_.clear(); | ||
65 | } | ||
66 | |||
67 | void 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 | |||
107 | void 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 | |||
118 | void 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 | |||
126 | void 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 | |||
17 | class 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 | |||
5 | wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent); | ||
6 | |||
7 | LogDialog::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 | |||
26 | void LogDialog::LogMessage(const std::string& message) { | ||
27 | wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE); | ||
28 | event->SetString(message); | ||
29 | QueueEvent(event); | ||
30 | } | ||
31 | |||
32 | void 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 | |||
10 | wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent); | ||
11 | |||
12 | class 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 | ||
9 | namespace { | 11 | namespace { |
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 | ||
51 | Logger& GetLogger() { | ||
52 | static Logger* instance = new Logger(); | ||
53 | return *instance; | ||
54 | } | ||
55 | |||
27 | } // namespace | 56 | } // namespace |
28 | 57 | ||
29 | void TrackerLog(std::string text) { | 58 | void TrackerLog(std::string text) { GetLogger().LogLine(text); } |
30 | static Logger* instance = new Logger(); | 59 | |
31 | instance->LogLine(text); | 60 | std::string TrackerReadPastLog() { return GetLogger().GetContents(); } |
61 | |||
62 | void 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 | ||
6 | class LogDialog; | ||
7 | |||
6 | void TrackerLog(std::string message); | 8 | void TrackerLog(std::string message); |
7 | 9 | ||
10 | std::string TrackerReadPastLog(); | ||
11 | |||
12 | void 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 | ||
11 | class TrackerApp : public wxApp { | 11 | class 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 | |||
5 | namespace { | ||
6 | |||
7 | const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"}; | ||
8 | const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"}; | ||
9 | const char* kPanelShuffleLabels[] = {"None", "Rearrange"}; | ||
10 | const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2", | ||
11 | "Pilgrimage"}; | ||
12 | const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock", | ||
13 | "Individual", "Progressive"}; | ||
14 | |||
15 | void 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 | |||
24 | OptionsPane::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 | |||
47 | void 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 | |||
12 | class 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 | |||
13 | namespace { | ||
14 | |||
15 | std::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 | |||
24 | PaintingsPane::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 | |||
44 | void PaintingsPane::ResetIndicators() { | ||
45 | tree_ctrl_->DeleteAllItems(); | ||
46 | reveal_btn_->Enable(AP_IsPaintingShuffle()); | ||
47 | } | ||
48 | |||
49 | void 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 | |||
74 | void 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 | |||
86 | void 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 | |||
10 | class wxDataViewEvent; | ||
11 | class wxDataViewTreeCtrl; | ||
12 | |||
13 | class 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 | |||
12 | ReportPopup::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 | |||
28 | void ReportPopup::SetDoorId(int door_id) { | ||
29 | door_id_ = door_id; | ||
30 | |||
31 | ResetIndicators(); | ||
32 | } | ||
33 | |||
34 | void ReportPopup::Reset() { | ||
35 | door_id_ = -1; | ||
36 | } | ||
37 | |||
38 | void 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 | |||
73 | void 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 | |||
109 | void 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 | |||
119 | void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) { | ||
120 | LoadIcons(); | ||
121 | ResetIndicators(); | ||
122 | |||
123 | event.Skip(); | ||
124 | } | ||
125 | |||
126 | void 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 | |||
10 | class 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 | ||
5 | SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { | 5 | SettingsDialog::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 | |||
10 | class SettingsDialog : public wxDialog { | 14 | class 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 | ||
14 | constexpr int AREA_ACTUAL_SIZE = 21; | 15 | constexpr int AREA_ACTUAL_SIZE = 21; |
15 | constexpr int OWL_ACTUAL_SIZE = 32; | 16 | constexpr int OWL_ACTUAL_SIZE = 32; |
17 | constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11 | ||
18 | constexpr int PAINTING_EXIT_RADIUS = 6; | ||
16 | 19 | ||
17 | enum class ItemDrawType { kNone, kBox, kOwl }; | 20 | enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit }; |
21 | |||
22 | namespace { | ||
23 | |||
24 | wxPoint 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 | ||
19 | SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { | 35 | SubwayMap::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 | ||
68 | void SubwayMap::OnConnect() { | 78 | void 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 | ||
179 | void SubwayMap::UpdateIndicators() { | 191 | void 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 | ||
690 | void SubwayMap::SetUpHelpButton() { | 657 | void 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 | ||
695 | void 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 | |||
726 | wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { | 740 | wxPoint 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 | ||
813 | quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { | 827 | quadtree::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 | ||
22 | class ReportPopup; | ||
23 | |||
22 | class SubwayMap : public wxPanel { | 24 | class 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 | ||
51 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); | 58 | wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); |
52 | wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); | 59 | wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent); |
53 | wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); | 60 | wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); |
54 | wxDEFINE_EVENT(REDRAW_POSITION, wxCommandEvent); | ||
55 | wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); | 61 | wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent); |
56 | 62 | ||
57 | TrackerFrame::TrackerFrame() | 63 | TrackerFrame::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 | ||
161 | void TrackerFrame::UpdateIndicators(UpdateIndicatorsMode mode) { | 186 | void 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 | |||
168 | void TrackerFrame::RedrawPosition() { | ||
169 | QueueEvent(new wxCommandEvent(REDRAW_POSITION)); | ||
170 | } | 188 | } |
171 | 189 | ||
172 | void TrackerFrame::OnAbout(wxCommandEvent &event) { | 190 | void 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 | ||
241 | void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { | 262 | void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { |
242 | CheckForUpdates(/*manual=*/true); | 263 | updater_->CheckForUpdates(/*invisible=*/false); |
243 | } | 264 | } |
244 | 265 | ||
245 | void TrackerFrame::OnZoomIn(wxCommandEvent &event) { | 266 | void TrackerFrame::OnZoomIn(wxCommandEvent &event) { |
@@ -254,132 +275,93 @@ void TrackerFrame::OnZoomOut(wxCommandEvent &event) { | |||
254 | } | 275 | } |
255 | } | 276 | } |
256 | 277 | ||
257 | void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { | 278 | void 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 | ||
262 | void 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(); | 291 | void 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_)); | 298 | void 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 | |||
303 | void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) { | ||
304 | notebook_->Refresh(); | ||
282 | } | 305 | } |
283 | 306 | ||
284 | void TrackerFrame::OnStateReset(wxCommandEvent &event) { | 307 | void 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 | ||
295 | void TrackerFrame::OnStateChanged(wxCommandEvent &event) { | 317 | void 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 | ||
319 | void 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; | |
323 | void 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 | |||
331 | void TrackerFrame::OnConnectToAp(ApConnectEvent &event) { | ||
332 | AP_Connect(event.GetServer(), event.GetUser(), event.GetPass()); | ||
333 | } | ||
334 | |||
335 | void 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) { | 361 | void 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(); | 365 | void 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 | |||
10 | class AchievementsPane; | 17 | class AchievementsPane; |
18 | class ItemsPane; | ||
19 | class LogDialog; | ||
20 | class OptionsPane; | ||
21 | class PaintingsPane; | ||
11 | class SubwayMap; | 22 | class SubwayMap; |
12 | class TrackerPanel; | 23 | class TrackerPanel; |
13 | class wxBookCtrlEvent; | 24 | class wxBookCtrlEvent; |
14 | class wxNotebook; | 25 | class wxNotebook; |
26 | class wxSplitterEvent; | ||
27 | class wxSplitterWindow; | ||
15 | 28 | ||
16 | class ApConnectEvent : public wxEvent { | 29 | class 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 | ||
52 | struct 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 | |||
62 | class 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 | |||
39 | wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); | 75 | wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); |
40 | wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); | 76 | wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent); |
41 | wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); | 77 | wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); |
42 | wxDECLARE_EVENT(REDRAW_POSITION, wxCommandEvent); | ||
43 | wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); | 78 | wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent); |
44 | 79 | ||
45 | enum UpdateIndicatorsMode { | ||
46 | kUPDATE_ALL_INDICATORS = 0, | ||
47 | kUPDATE_ONLY_PANELS = 1, | ||
48 | }; | ||
49 | |||
50 | class TrackerFrame : public wxFrame { | 80 | class 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 | ||
53 | void TrackerPanel::UpdateIndicators() { | 53 | void 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())) { | |
65 | void TrackerPanel::SetPanelsMode() { panels_mode_ = true; } | 65 | area.active = false; |
66 | 66 | } else { | |
67 | void 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 | ||
80 | void 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 | ||
97 | void TrackerPanel::OnPaint(wxPaintEvent &event) { | 83 | void 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 | ||
145 | void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { | 135 | void TrackerPanel::Resize() { |
146 | RefreshSavedata(); | 136 | wxSize panel_size = GetClientSize(); |
147 | } | ||
148 | |||
149 | void 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 | |||
209 | void 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 | ||
17 | namespace { | 18 | namespace { |
@@ -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 { | |||
162 | struct TrackerState { | 167 | struct 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 | ||
172 | enum Decision { kYes, kNo, kMaybe }; | 183 | enum Decision { kYes, kNo, kMaybe }; |
@@ -181,6 +192,11 @@ class StateCalculator; | |||
181 | struct StateCalculatorOptions { | 192 | struct 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 | ||
663 | void ResetReachabilityRequirements() { | 692 | void 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 | ||
668 | void RecalculateReachability() { | 757 | void 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 | |||
842 | bool 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 | |||
852 | bool 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 | |||
862 | bool 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 | ||
19 | bool IsPilgrimageDoable(); | 19 | bool IsPilgrimageDoable(); |
20 | 20 | ||
21 | bool IsAreaPostgame(int area_id); | ||
22 | |||
23 | bool IsLocationPostgame(int location_id); | ||
24 | |||
25 | bool 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 | |||
22 | constexpr const char* kVersionFileUrl = | ||
23 | "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml"; | ||
24 | constexpr const char* kChangelogUrl = | ||
25 | "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md"; | ||
26 | |||
27 | namespace { | ||
28 | |||
29 | std::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 | |||
47 | Updater::Updater(wxFrame* parent) : parent_(parent) { | ||
48 | Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this); | ||
49 | } | ||
50 | |||
51 | void 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 | |||
58 | void 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 | |||
81 | void 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 | |||
91 | void 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 | |||
163 | void 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 | |||
285 | bool 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 | |||
14 | class wxWebRequest; | ||
15 | class wxWebRequestEvent; | ||
16 | |||
17 | class 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 | ||
39 | constexpr const Version kTrackerVersion = Version(0, 12, 0); | 39 | constexpr 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" | ||