about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/achievements_pane.cpp15
-rw-r--r--src/achievements_pane.h6
-rw-r--r--src/ap_state.cpp873
-rw-r--r--src/ap_state.h44
-rw-r--r--src/area_popup.cpp188
-rw-r--r--src/area_popup.h38
-rw-r--r--src/connection_dialog.cpp44
-rw-r--r--src/connection_dialog.h6
-rw-r--r--src/game_data.cpp496
-rw-r--r--src/game_data.h46
-rw-r--r--src/global.cpp16
-rw-r--r--src/global.h2
-rw-r--r--src/icons.cpp22
-rw-r--r--src/icons.h25
-rw-r--r--src/ipc_dialog.cpp54
-rw-r--r--src/ipc_dialog.h24
-rw-r--r--src/ipc_state.cpp367
-rw-r--r--src/ipc_state.h23
-rw-r--r--src/items_pane.cpp145
-rw-r--r--src/items_pane.h33
-rw-r--r--src/log_dialog.cpp37
-rw-r--r--src/log_dialog.h24
-rw-r--r--src/logger.cpp64
-rw-r--r--src/logger.h14
-rw-r--r--src/main.cpp24
-rw-r--r--src/network_set.cpp34
-rw-r--r--src/network_set.h16
-rw-r--r--src/options_pane.cpp71
-rw-r--r--src/options_pane.h19
-rw-r--r--src/paintings_pane.cpp86
-rw-r--r--src/paintings_pane.h28
-rw-r--r--src/report_popup.cpp131
-rw-r--r--src/report_popup.h38
-rw-r--r--src/settings_dialog.cpp45
-rw-r--r--src/settings_dialog.h13
-rw-r--r--src/subway_map.cpp842
-rw-r--r--src/subway_map.h50
-rw-r--r--src/tracker_config.cpp14
-rw-r--r--src/tracker_config.h10
-rw-r--r--src/tracker_frame.cpp294
-rw-r--r--src/tracker_frame.h96
-rw-r--r--src/tracker_panel.cpp185
-rw-r--r--src/tracker_panel.h9
-rw-r--r--src/tracker_state.cpp646
-rw-r--r--src/tracker_state.h17
-rw-r--r--src/updater.cpp309
-rw-r--r--src/updater.h46
-rw-r--r--src/version.cpp5
-rw-r--r--src/version.h9
-rw-r--r--src/windows.rc3
50 files changed, 4681 insertions, 965 deletions
diff --git a/src/achievements_pane.cpp b/src/achievements_pane.cpp index 8ec3727..d23c434 100644 --- a/src/achievements_pane.cpp +++ b/src/achievements_pane.cpp
@@ -8,23 +8,24 @@ AchievementsPane::AchievementsPane(wxWindow* parent)
8 AppendColumn("Achievement"); 8 AppendColumn("Achievement");
9 9
10 for (int panel_id : GD_GetAchievementPanels()) { 10 for (int panel_id : GD_GetAchievementPanels()) {
11 achievement_names_.push_back(GD_GetPanel(panel_id).achievement_name); 11 const Panel& panel = GD_GetPanel(panel_id);
12 achievements_.emplace_back(panel.achievement_name, panel.solve_index);
12 } 13 }
13 14
14 std::sort(std::begin(achievement_names_), std::end(achievement_names_)); 15 std::sort(std::begin(achievements_), std::end(achievements_));
15 16
16 for (int i = 0; i < achievement_names_.size(); i++) { 17 for (int i = 0; i < achievements_.size(); i++) {
17 InsertItem(i, achievement_names_.at(i)); 18 InsertItem(i, std::get<0>(achievements_.at(i)));
18 } 19 }
19 20
20 SetColumnWidth(0, wxLIST_AUTOSIZE); 21 SetColumnWidth(0, wxLIST_AUTOSIZE_USEHEADER);
21 22
22 UpdateIndicators(); 23 UpdateIndicators();
23} 24}
24 25
25void AchievementsPane::UpdateIndicators() { 26void AchievementsPane::UpdateIndicators() {
26 for (int i = 0; i < achievement_names_.size(); i++) { 27 for (int i = 0; i < achievements_.size(); i++) {
27 if (AP_HasAchievement(achievement_names_.at(i))) { 28 if (AP_IsPanelSolved(std::get<1>(achievements_.at(i)))) {
28 SetItemTextColour(i, *wxBLACK); 29 SetItemTextColour(i, *wxBLACK);
29 } else { 30 } else {
30 SetItemTextColour(i, *wxRED); 31 SetItemTextColour(i, *wxRED);
diff --git a/src/achievements_pane.h b/src/achievements_pane.h index ac88cac..941b5e3 100644 --- a/src/achievements_pane.h +++ b/src/achievements_pane.h
@@ -9,6 +9,10 @@
9 9
10#include <wx/listctrl.h> 10#include <wx/listctrl.h>
11 11
12#include <string>
13#include <tuple>
14#include <vector>
15
12class AchievementsPane : public wxListView { 16class AchievementsPane : public wxListView {
13 public: 17 public:
14 explicit AchievementsPane(wxWindow* parent); 18 explicit AchievementsPane(wxWindow* parent);
@@ -16,7 +20,7 @@ class AchievementsPane : public wxListView {
16 void UpdateIndicators(); 20 void UpdateIndicators();
17 21
18 private: 22 private:
19 std::vector<std::string> achievement_names_; 23 std::vector<std::tuple<std::string, int>> achievements_; // name, solve index
20}; 24};
21 25
22#endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file 26#endif /* end of include guard: ACHIEVEMENTS_PANE_H_C320D0B8 */ \ No newline at end of file
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index f245c2b..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -4,11 +4,13 @@
4#define _WEBSOCKETPP_CPP11_STRICT_ 4#define _WEBSOCKETPP_CPP11_STRICT_
5#pragma comment(lib, "crypt32") 5#pragma comment(lib, "crypt32")
6 6
7#include <fmt/core.h>
7#include <hkutil/string.h> 8#include <hkutil/string.h>
8 9
9#include <any> 10#include <any>
10#include <apclient.hpp> 11#include <apclient.hpp>
11#include <apuuid.hpp> 12#include <apuuid.hpp>
13#include <bitset>
12#include <chrono> 14#include <chrono>
13#include <exception> 15#include <exception>
14#include <filesystem> 16#include <filesystem>
@@ -21,42 +23,73 @@
21#include <tuple> 23#include <tuple>
22 24
23#include "game_data.h" 25#include "game_data.h"
26#include "ipc_state.h"
27#include "logger.h"
24#include "tracker_frame.h" 28#include "tracker_frame.h"
25#include "tracker_state.h" 29#include "tracker_state.h"
26 30
27constexpr int AP_MAJOR = 0; 31constexpr int AP_MAJOR = 0;
28constexpr int AP_MINOR = 4; 32constexpr int AP_MINOR = 6;
29constexpr int AP_REVISION = 5; 33constexpr int AP_REVISION = 1;
30 34
31constexpr const char* CERT_STORE_PATH = "cacert.pem"; 35constexpr const char* CERT_STORE_PATH = "cacert.pem";
32constexpr int ITEM_HANDLING = 7; // <- all 36constexpr int ITEM_HANDLING = 7; // <- all
33 37
38constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds
39constexpr int CONNECTION_BACKOFF_INTERVAL = 100;
40
41constexpr int PANEL_COUNT = 803;
42constexpr int PANEL_BITFIELD_LENGTH = 48;
43constexpr int PANEL_BITFIELDS = 17;
44
34namespace { 45namespace {
35 46
36struct APState { 47const std::set<long> kNonProgressionItems = {
37 std::unique_ptr<APClient> apclient; 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};
38 58
59struct APState {
60 // Initialized on main thread
39 bool initialized = false; 61 bool initialized = false;
40
41 TrackerFrame* tracker_frame = nullptr; 62 TrackerFrame* tracker_frame = nullptr;
63 std::list<std::string> tracked_data_storage_keys;
42 64
43 bool client_active = false; 65 // Client
44 std::mutex client_mutex; 66 std::mutex client_mutex;
67 std::unique_ptr<APClient> apclient;
68
69 // Protected state
70 std::mutex state_mutex;
71
72 std::string status_message = "Not connected to Archipelago.";
45 73
46 bool connected = false; 74 bool connected = false;
47 bool has_connection_result = false; 75 std::string connection_failure;
76 int remaining_loops = 0;
48 77
49 std::string data_storage_prefix; 78 std::string data_storage_prefix;
50 std::list<std::string> tracked_data_storage_keys;
51 std::string victory_data_storage_key; 79 std::string victory_data_storage_key;
52 80
81 std::string save_name;
82
53 std::map<int64_t, int> inventory; 83 std::map<int64_t, int> inventory;
54 std::set<int64_t> checked_locations; 84 std::set<int64_t> checked_locations;
55 std::map<std::string, std::any> data_storage; 85 std::map<std::string, std::any> data_storage;
56 std::optional<std::tuple<int, int>> player_pos; 86 std::optional<std::tuple<int, int>> player_pos;
87 std::bitset<PANEL_COUNT> solved_panels;
57 88
58 DoorShuffleMode door_shuffle_mode = kNO_DOORS; 89 DoorShuffleMode door_shuffle_mode = kNO_DOORS;
90 bool group_doors = false;
59 bool color_shuffle = false; 91 bool color_shuffle = false;
92 PanelShuffleMode panel_shuffle_mode = kNO_PANELS;
60 bool painting_shuffle = false; 93 bool painting_shuffle = false;
61 int mastery_requirement = 21; 94 int mastery_requirement = 21;
62 int level_2_requirement = 223; 95 int level_2_requirement = 223;
@@ -68,37 +101,197 @@ struct APState {
68 bool pilgrimage_allows_paintings = false; 101 bool pilgrimage_allows_paintings = false;
69 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; 102 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
70 bool sunwarp_shuffle = false; 103 bool sunwarp_shuffle = false;
104 bool postgame_shuffle = true;
71 105
72 std::map<std::string, std::string> painting_mapping; 106 std::map<std::string, std::string> painting_mapping;
107 std::set<std::string> painting_codomain;
73 std::map<int, SunwarpMapping> sunwarp_mapping; 108 std::map<int, SunwarpMapping> sunwarp_mapping;
74 109
75 void Connect(std::string server, std::string player, std::string password) { 110 void Connect(std::string server, std::string player, std::string password) {
76 if (!initialized) { 111 Initialize();
77 wxLogVerbose("Initializing APState...");
78 112
79 std::thread([this]() { 113 {
80 for (;;) { 114 std::lock_guard state_guard(state_mutex);
81 { 115 SetStatusMessage("Connecting to Archipelago server....");
82 std::lock_guard client_guard(client_mutex); 116 }
83 if (apclient) { 117 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
84 apclient->poll();
85 }
86 }
87 118
88 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 119 // Creating and setting up the client has to all be done while holding the
89 } 120 // client mutex, so that the other thread doesn't try to poll before we add
90 }).detach(); 121 // handlers, etc.
122 {
123 TrackerLog("Destroying old AP client...");
124
125 std::lock_guard client_guard(client_mutex);
91 126
92 for (int panel_id : GD_GetAchievementPanels()) { 127 if (apclient) {
93 tracked_data_storage_keys.push_back( 128 DestroyClient();
94 "Achievement|" + GD_GetPanel(panel_id).achievement_name);
95 } 129 }
96 130
97 for (const MapArea& map_area : GD_GetMapAreas()) { 131 std::string cert_store = "";
98 for (const Location& location : map_area.locations) { 132 if (std::filesystem::exists(CERT_STORE_PATH)) {
99 tracked_data_storage_keys.push_back( 133 cert_store = CERT_STORE_PATH;
100 "Hunt|" + std::to_string(location.ap_location_id)); 134 }
101 } 135
136 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server,
137 cert_store);
138
139 {
140 std::lock_guard state_guard(state_mutex);
141
142 connected = false;
143 connection_failure.clear();
144 remaining_loops = CONNECTION_TIMEOUT / CONNECTION_BACKOFF_INTERVAL;
145
146 save_name.clear();
147 inventory.clear();
148 checked_locations.clear();
149 data_storage.clear();
150 player_pos = std::nullopt;
151 solved_panels.reset();
152 victory_data_storage_key.clear();
153 door_shuffle_mode = kNO_DOORS;
154 group_doors = false;
155 color_shuffle = false;
156 panel_shuffle_mode = kNO_PANELS;
157 painting_shuffle = false;
158 painting_mapping.clear();
159 painting_codomain.clear();
160 mastery_requirement = 21;
161 level_2_requirement = 223;
162 location_checks = kNORMAL_LOCATIONS;
163 victory_condition = kTHE_END;
164 early_color_hallways = false;
165 pilgrimage_enabled = false;
166 pilgrimage_allows_roof_access = false;
167 pilgrimage_allows_paintings = false;
168 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
169 sunwarp_shuffle = false;
170 sunwarp_mapping.clear();
171 postgame_shuffle = true;
172 }
173
174 apclient->set_room_info_handler(
175 [this, player, password]() { OnRoomInfo(player, password); });
176
177 apclient->set_location_checked_handler(
178 [this](const std::list<int64_t>& locations) {
179 OnLocationChecked(locations);
180 });
181
182 apclient->set_slot_disconnected_handler(
183 [this]() { OnSlotDisconnected(); });
184
185 apclient->set_socket_disconnected_handler(
186 [this]() { OnSocketDisconnected(); });
187
188 apclient->set_items_received_handler(
189 [this](const std::list<APClient::NetworkItem>& items) {
190 OnItemsReceived(items);
191 });
192
193 apclient->set_retrieved_handler(
194 [this](const std::map<std::string, nlohmann::json>& data) {
195 OnRetrieved(data);
196 });
197
198 apclient->set_set_reply_handler(
199 [this](const std::string& key, const nlohmann::json& value,
200 const nlohmann::json&) { OnSetReply(key, value); });
201
202 apclient->set_slot_connected_handler(
203 [this, player, server](const nlohmann::json& slot_data) {
204 OnSlotConnected(player, server, slot_data);
205 });
206
207 apclient->set_slot_refused_handler(
208 [this](const std::list<std::string>& errors) {
209 OnSlotRefused(errors);
210 });
211 }
212 }
213
214 std::string GetStatusMessage() {
215 std::lock_guard state_guard(state_mutex);
216
217 return status_message;
218 }
219
220 bool HasCheckedGameLocation(int location_id) {
221 std::lock_guard state_guard(state_mutex);
222
223 return checked_locations.count(location_id);
224 }
225
226 bool HasItem(int item_id, int quantity) {
227 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
228 }
229
230 bool HasItemSafe(int item_id, int quantity) {
231 std::lock_guard state_guard(state_mutex);
232 return HasItem(item_id, quantity);
233 }
234
235 const std::set<std::string>& GetCheckedPaintings() {
236 std::lock_guard state_guard(state_mutex);
237
238 std::string key = fmt::format("{}Paintings", data_storage_prefix);
239 if (!data_storage.count(key)) {
240 data_storage[key] = std::set<std::string>();
241 }
242
243 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
244 }
245
246 bool IsPaintingChecked(const std::string& painting_id) {
247 const auto& checked_paintings = GetCheckedPaintings();
248
249 std::lock_guard state_guard(state_mutex);
250
251 return checked_paintings.count(painting_id) ||
252 (painting_mapping.count(painting_id) &&
253 checked_paintings.count(painting_mapping.at(painting_id)));
254 }
255
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 }
271
272 bool HasReachedGoal() {
273 std::lock_guard state_guard(state_mutex);
274
275 return data_storage.count(victory_data_storage_key) &&
276 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
277 30; // CLIENT_GOAL
278 }
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
286 private:
287 void Initialize() {
288 if (!initialized) {
289 TrackerLog("Initializing APState...");
290
291 std::thread([this]() { Thread(); }).detach();
292
293 for (int i = 0; i < PANEL_BITFIELDS; i++) {
294 tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i));
102 } 295 }
103 296
104 tracked_data_storage_keys.push_back("PlayerPos"); 297 tracked_data_storage_keys.push_back("PlayerPos");
@@ -106,125 +299,189 @@ struct APState {
106 299
107 initialized = true; 300 initialized = true;
108 } 301 }
302 }
109 303
110 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 304 void Thread() {
111 wxLogStatus("Connecting to Archipelago server (%s)...", server); 305 std::string display_error;
112 306
113 { 307 for (;;) {
114 wxLogVerbose("Destroying old AP client..."); 308 {
309 std::lock_guard client_guard(client_mutex);
310 if (apclient) {
311 apclient->poll();
115 312
116 std::lock_guard client_guard(client_mutex); 313 {
314 std::lock_guard state_guard(state_mutex);
117 315
118 if (apclient) { 316 if (!connected) {
119 DestroyClient(); 317 if (!connection_failure.empty()) {
318 TrackerLog(connection_failure);
319
320 display_error = connection_failure;
321 connection_failure.clear();
322
323 DestroyClient();
324 } else {
325 remaining_loops--;
326
327 if (remaining_loops <= 0) {
328 DestroyClient();
329
330 SetStatusMessage("Disconnected from Archipelago.");
331 TrackerLog("Timeout while connecting to Archipelago server.");
332
333 display_error =
334 "Timeout while connecting to Archipelago server.";
335 }
336 }
337 }
338 }
339 }
120 } 340 }
121 341
122 std::string cert_store = ""; 342 if (!display_error.empty()) {
123 if (std::filesystem::exists(CERT_STORE_PATH)) { 343 wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR);
124 cert_store = CERT_STORE_PATH; 344 display_error.clear();
125 } 345 }
126 346
127 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, 347 std::this_thread::sleep_for(std::chrono::milliseconds(100));
128 cert_store);
129 } 348 }
349 }
350
351 void OnRoomInfo(std::string player, std::string password) {
352 {
353 std::lock_guard state_guard(state_mutex);
130 354
131 inventory.clear();
132 checked_locations.clear();
133 data_storage.clear();
134 player_pos = std::nullopt;
135 victory_data_storage_key.clear();
136 door_shuffle_mode = kNO_DOORS;
137 color_shuffle = false;
138 painting_shuffle = false;
139 painting_mapping.clear();
140 mastery_requirement = 21;
141 level_2_requirement = 223;
142 location_checks = kNORMAL_LOCATIONS;
143 victory_condition = kTHE_END;
144 early_color_hallways = false;
145 pilgrimage_enabled = false;
146 pilgrimage_allows_roof_access = false;
147 pilgrimage_allows_paintings = false;
148 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
149 sunwarp_shuffle = false;
150 sunwarp_mapping.clear();
151
152 connected = false;
153 has_connection_result = false;
154
155 apclient->set_room_info_handler([this, player, password]() {
156 inventory.clear(); 355 inventory.clear();
157 356
158 wxLogStatus("Connected to Archipelago server. Authenticating as %s %s", 357 SetStatusMessage("Connected to Archipelago server. Authenticating...");
159 player, 358 }
160 (password.empty() ? "without password"
161 : "with password " + password));
162 tracker_frame->SetStatusMessage(
163 "Connected to Archipelago server. Authenticating...");
164
165 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
166 {AP_MAJOR, AP_MINOR, AP_REVISION});
167 });
168
169 apclient->set_location_checked_handler(
170 [this](const std::list<int64_t>& locations) {
171 for (const int64_t location_id : locations) {
172 checked_locations.insert(location_id);
173 wxLogVerbose("Location: %lld", location_id);
174 }
175 359
176 RefreshTracker(false); 360 TrackerLog(fmt::format(
177 }); 361 "Connected to Archipelago server. Authenticating as {} {}", player,
178 362 (password.empty() ? "without password" : "with password " + password)));
179 apclient->set_slot_disconnected_handler([this]() {
180 tracker_frame->SetStatusMessage(
181 "Disconnected from Archipelago. Attempting to reconnect...");
182 wxLogStatus(
183 "Slot disconnected from Archipelago. Attempting to reconnect...");
184 });
185
186 apclient->set_socket_disconnected_handler([this]() {
187 tracker_frame->SetStatusMessage(
188 "Disconnected from Archipelago. Attempting to reconnect...");
189 wxLogStatus(
190 "Socket disconnected from Archipelago. Attempting to reconnect...");
191 });
192
193 apclient->set_items_received_handler(
194 [this](const std::list<APClient::NetworkItem>& items) {
195 for (const APClient::NetworkItem& item : items) {
196 inventory[item.item]++;
197 wxLogVerbose("Item: %lld", item.item);
198 }
199 363
200 RefreshTracker(false); 364 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
201 }); 365 {AP_MAJOR, AP_MINOR, AP_REVISION});
366 }
202 367
203 apclient->set_retrieved_handler( 368 void OnLocationChecked(const std::list<int64_t>& locations) {
204 [this](const std::map<std::string, nlohmann::json>& data) { 369 {
205 for (const auto& [key, value] : data) { 370 std::lock_guard state_guard(state_mutex);
206 HandleDataStorage(key, value); 371
207 } 372 for (const int64_t location_id : locations) {
373 checked_locations.insert(location_id);
374 TrackerLog(fmt::format("Location: {}", location_id));
375 }
376 }
377
378 RefreshTracker(StateUpdate{.cleared_locations = true});
379 }
380
381 void OnSlotDisconnected() {
382 std::lock_guard state_guard(state_mutex);
383
384 SetStatusMessage(
385 "Disconnected from Archipelago. Attempting to reconnect...");
386 TrackerLog(
387 "Slot disconnected from Archipelago. Attempting to reconnect...");
388 }
389
390 void OnSocketDisconnected() {
391 std::lock_guard state_guard(state_mutex);
392
393 SetStatusMessage(
394 "Disconnected from Archipelago. Attempting to reconnect...");
395 TrackerLog(
396 "Socket disconnected from Archipelago. Attempting to reconnect...");
397 }
398
399 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) {
400 std::vector<ItemState> item_states;
401 bool progression_items = false;
402
403 {
404 std::lock_guard state_guard(state_mutex);
405
406 std::map<int64_t, int> index_by_item;
407
408 for (const APClient::NetworkItem& item : items) {
409 inventory[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});
423 }
424 }
425
426 RefreshTracker(StateUpdate{.items = item_states,
427 .progression_items = progression_items});
428 }
429
430 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) {
431 StateUpdate state_update;
432
433 {
434 std::lock_guard state_guard(state_mutex);
435
436 for (const auto& [key, value] : data) {
437 HandleDataStorage(key, value, state_update);
438 }
439 }
208 440
209 RefreshTracker(false); 441 RefreshTracker(state_update);
210 }); 442 }
443
444 void OnSetReply(const std::string& key, const nlohmann::json& value) {
445 StateUpdate state_update;
446
447 {
448 std::lock_guard state_guard(state_mutex);
449 HandleDataStorage(key, value, state_update);
450 }
451
452 RefreshTracker(state_update);
453 }
454
455 void OnSlotConnected(std::string player, std::string server,
456 const nlohmann::json& slot_data) {
457 IPC_SetTrackerSlot(server, player);
458
459 TrackerLog("Connected to Archipelago!");
211 460
212 apclient->set_set_reply_handler([this](const std::string& key, 461 {
213 const nlohmann::json& value, 462 std::lock_guard state_guard(state_mutex);
214 const nlohmann::json&) {
215 HandleDataStorage(key, value);
216 RefreshTracker(false);
217 });
218 463
219 apclient->set_slot_connected_handler([this]( 464 SetStatusMessage(
220 const nlohmann::json& slot_data) { 465 fmt::format("Connected to Archipelago! ({}@{}).", player, server));
221 tracker_frame->SetStatusMessage("Connected to Archipelago!");
222 wxLogStatus("Connected to Archipelago!");
223 466
467 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(),
468 apclient->get_player_number());
224 data_storage_prefix = 469 data_storage_prefix =
225 "Lingo_" + std::to_string(apclient->get_player_number()) + "_"; 470 fmt::format("Lingo_{}_", apclient->get_player_number());
226 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>(); 471 door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
472 if (slot_data.contains("group_doors")) {
473 group_doors = slot_data.contains("group_doors") &&
474 slot_data["group_doors"].get<int>() == 1;
475 } else {
476 // If group_doors doesn't exist yet, that means kPANELS_MODE is
477 // actually kSIMPLE_DOORS.
478 if (door_shuffle_mode == kPANELS_MODE) {
479 door_shuffle_mode = kDOORS_MODE;
480 group_doors = true;
481 }
482 }
227 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>();
228 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 485 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
229 mastery_requirement = slot_data["mastery_achievements"].get<int>(); 486 mastery_requirement = slot_data["mastery_achievements"].get<int>();
230 level_2_requirement = slot_data["level_2_requirement"].get<int>(); 487 level_2_requirement = slot_data["level_2_requirement"].get<int>();
@@ -246,6 +503,9 @@ struct APState {
246 : kSUNWARP_ACCESS_NORMAL; 503 : kSUNWARP_ACCESS_NORMAL;
247 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && 504 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
248 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;
249 509
250 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { 510 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
251 painting_mapping.clear(); 511 painting_mapping.clear();
@@ -253,6 +513,7 @@ struct APState {
253 for (const auto& mapping_it : 513 for (const auto& mapping_it :
254 slot_data["painting_entrance_to_exit"].items()) { 514 slot_data["painting_entrance_to_exit"].items()) {
255 painting_mapping[mapping_it.key()] = mapping_it.value(); 515 painting_mapping[mapping_it.key()] = mapping_it.value();
516 painting_codomain.insert(mapping_it.value());
256 } 517 }
257 } 518 }
258 519
@@ -268,193 +529,165 @@ struct APState {
268 } 529 }
269 } 530 }
270 531
271 connected = true;
272 has_connection_result = true;
273
274 RefreshTracker(true);
275
276 std::list<std::string> corrected_keys; 532 std::list<std::string> corrected_keys;
277 for (const std::string& key : tracked_data_storage_keys) { 533 for (const std::string& key : tracked_data_storage_keys) {
278 corrected_keys.push_back(data_storage_prefix + key); 534 corrected_keys.push_back(data_storage_prefix + key);
279 } 535 }
280 536
281 { 537 victory_data_storage_key =
282 std::ostringstream vdsks; 538 fmt::format("_read_client_status_{}_{}", apclient->get_team_number(),
283 vdsks << "_read_client_status_" << apclient->get_team_number() << "_" 539 apclient->get_player_number());
284 << apclient->get_player_number();
285 victory_data_storage_key = vdsks.str();
286 }
287 540
288 corrected_keys.push_back(victory_data_storage_key); 541 corrected_keys.push_back(victory_data_storage_key);
289 542
290 apclient->Get(corrected_keys); 543 apclient->Get(corrected_keys);
291 apclient->SetNotify(corrected_keys); 544 apclient->SetNotify(corrected_keys);
292 });
293
294 apclient->set_slot_refused_handler(
295 [this](const std::list<std::string>& errors) {
296 connected = false;
297 has_connection_result = true;
298
299 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
300
301 std::vector<std::string> error_messages;
302 error_messages.push_back("Could not connect to Archipelago.");
303
304 for (const std::string& error : errors) {
305 if (error == "InvalidSlot") {
306 error_messages.push_back("Invalid player name.");
307 } else if (error == "InvalidGame") {
308 error_messages.push_back(
309 "The specified player is not playing Lingo.");
310 } else if (error == "IncompatibleVersion") {
311 error_messages.push_back(
312 "The Archipelago server is not the correct version for this "
313 "client.");
314 } else if (error == "InvalidPassword") {
315 error_messages.push_back("Incorrect password.");
316 } else if (error == "InvalidItemsHandling") {
317 error_messages.push_back(
318 "Invalid item handling flag. This is a bug with the tracker. "
319 "Please report it to the lingo-ap-tracker GitHub.");
320 } else {
321 error_messages.push_back("Unknown error.");
322 }
323 }
324
325 std::string full_message = hatkirby::implode(error_messages, " ");
326 wxLogError(wxString(full_message));
327
328 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
329 });
330
331 client_active = true;
332 545
333 int timeout = 5000; // 5 seconds 546 connected = true;
334 int interval = 100; 547 }
335 int remaining_loops = timeout / interval;
336 while (!has_connection_result) {
337 if (interval == 0) {
338 connected = false;
339 has_connection_result = true;
340 548
341 DestroyClient(); 549 ResetReachabilityRequirements();
550 RefreshTracker(std::nullopt);
551 }
342 552
343 tracker_frame->SetStatusMessage("Disconnected from Archipelago."); 553 void OnSlotRefused(const std::list<std::string>& errors) {
344 wxLogStatus("Timeout while connecting to Archipelago server."); 554 std::vector<std::string> error_messages;
345 wxMessageBox("Timeout while connecting to Archipelago server.", 555 error_messages.push_back("Could not connect to Archipelago.");
346 "Connection failed", wxOK | wxICON_ERROR); 556
557 for (const std::string& error : errors) {
558 if (error == "InvalidSlot") {
559 error_messages.push_back("Invalid player name.");
560 } else if (error == "InvalidGame") {
561 error_messages.push_back("The specified player is not playing Lingo.");
562 } else if (error == "IncompatibleVersion") {
563 error_messages.push_back(
564 "The Archipelago server is not the correct version for this "
565 "client.");
566 } else if (error == "InvalidPassword") {
567 error_messages.push_back("Incorrect password.");
568 } else if (error == "InvalidItemsHandling") {
569 error_messages.push_back(
570 "Invalid item handling flag. This is a bug with the tracker. "
571 "Please report it to the lingo-ap-tracker GitHub.");
572 } else {
573 error_messages.push_back("Unknown error.");
347 } 574 }
575 }
348 576
349 std::this_thread::sleep_for(std::chrono::milliseconds(100)); 577 {
578 std::lock_guard state_guard(state_mutex);
579 connection_failure = hatkirby::implode(error_messages, " ");
350 580
351 interval--; 581 SetStatusMessage("Disconnected from Archipelago.");
352 } 582 }
583 }
353 584
354 if (connected) { 585 // Assumes state mutex is locked.
355 RefreshTracker(false); 586 void SetStatusMessage(std::string msg) {
356 } else { 587 status_message = std::move(msg);
357 client_active = false; 588
358 } 589 tracker_frame->UpdateStatusMessage();
359 } 590 }
360 591
361 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 592 // Assumes state mutex is locked.
593 void HandleDataStorage(const std::string& key, const nlohmann::json& value, StateUpdate& state_update) {
362 if (value.is_boolean()) { 594 if (value.is_boolean()) {
363 data_storage[key] = value.get<bool>(); 595 data_storage[key] = value.get<bool>();
364 wxLogVerbose("Data storage %s retrieved as %s", key, 596 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
365 (value.get<bool>() ? "true" : "false")); 597 (value.get<bool>() ? "true" : "false")));
598
366 } else if (value.is_number()) { 599 } else if (value.is_number()) {
367 data_storage[key] = value.get<int>(); 600 data_storage[key] = value.get<int>();
368 wxLogVerbose("Data storage %s retrieved as %d", key, value.get<int>()); 601 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
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 }
369 } else if (value.is_object()) { 622 } else if (value.is_object()) {
370 if (key.ends_with("PlayerPos")) { 623 if (key.ends_with("PlayerPos")) {
371 auto map_value = value.get<std::map<std::string, int>>(); 624 auto map_value = value.get<std::map<std::string, int>>();
372 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;
373 } else { 627 } else {
374 data_storage[key] = value.get<std::map<std::string, int>>(); 628 data_storage[key] = value.get<std::map<std::string, int>>();
375 } 629 }
376 630
377 wxLogVerbose("Data storage %s retrieved as dictionary", key); 631 TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key));
378 } else if (value.is_null()) { 632 } else if (value.is_null()) {
379 if (key.ends_with("PlayerPos")) { 633 if (key.ends_with("PlayerPos")) {
380 player_pos = std::nullopt; 634 player_pos = std::nullopt;
635 state_update.player_position = true;
381 } else { 636 } else {
382 data_storage.erase(key); 637 data_storage.erase(key);
383 } 638 }
384 639
385 wxLogVerbose("Data storage %s retrieved as null", key); 640 TrackerLog(fmt::format("Data storage {} retrieved as null", key));
386 } else if (value.is_array()) { 641 } else if (value.is_array()) {
387 auto list_value = value.get<std::vector<std::string>>(); 642 auto list_value = value.get<std::vector<std::string>>();
388 643
389 if (key.ends_with("Paintings")) { 644 if (key.ends_with("Paintings")) {
390 data_storage[key] = 645 data_storage[key] =
391 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());
392 } else { 649 } else {
393 data_storage[key] = list_value; 650 data_storage[key] = list_value;
394 } 651 }
395 652
396 wxLogVerbose("Data storage %s retrieved as list: [%s]", key, 653 TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key,
397 hatkirby::implode(list_value, ", ")); 654 hatkirby::implode(list_value, ", ")));
398 } 655 }
399 } 656 }
400 657
401 bool HasCheckedGameLocation(int location_id) { 658 // State mutex should NOT be locked.
402 return checked_locations.count(location_id); 659 // nullopt state_update indicates a reset.
403 } 660 void RefreshTracker(std::optional<StateUpdate> state_update) {
661 TrackerLog("Refreshing display...");
404 662
405 bool HasCheckedHuntPanel(int location_id) { 663 if (!state_update || state_update->progression_items ||
406 std::string key = 664 !state_update->paintings.empty()) {
407 data_storage_prefix + "Hunt|" + std::to_string(location_id); 665 std::string prev_msg;
408 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key)); 666 {
409 } 667 std::lock_guard state_guard(state_mutex);
410
411 bool HasItem(int item_id, int quantity) {
412 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
413 }
414
415 bool HasAchievement(const std::string& name) {
416 std::string key = data_storage_prefix + "Achievement|" + name;
417 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
418 }
419 668
420 const std::set<std::string>& GetCheckedPaintings() { 669 prev_msg = status_message;
421 std::string key = data_storage_prefix + "Paintings"; 670 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
422 if (!data_storage.count(key)) { 671 }
423 data_storage[key] = std::set<std::string>();
424 }
425 672
426 return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); 673 RecalculateReachability();
427 }
428 674
429 void RefreshTracker(bool reset) { 675 {
430 wxLogVerbose("Refreshing display..."); 676 std::lock_guard state_guard(state_mutex);
431 677
432 RecalculateReachability(); 678 SetStatusMessage(prev_msg);
679 }
680 }
681
433 682
434 if (reset) { 683 if (!state_update) {
435 tracker_frame->ResetIndicators(); 684 tracker_frame->ResetIndicators();
436 } else { 685 } else {
437 tracker_frame->UpdateIndicators(); 686 tracker_frame->UpdateIndicators(*state_update);
438 } 687 }
439 } 688 }
440 689
441 int64_t GetItemId(const std::string& item_name) {
442 int64_t ap_id = apclient->get_item_id(item_name);
443 if (ap_id == APClient::INVALID_NAME_ID) {
444 wxLogError("Could not find AP item ID for %s", item_name);
445 }
446
447 return ap_id;
448 }
449
450 bool HasReachedGoal() {
451 return data_storage.count(victory_data_storage_key) &&
452 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
453 30; // CLIENT_GOAL
454 }
455
456 void DestroyClient() { 690 void DestroyClient() {
457 client_active = false;
458 apclient->reset(); 691 apclient->reset();
459 apclient.reset(); 692 apclient.reset();
460 } 693 }
@@ -473,79 +706,179 @@ void AP_Connect(std::string server, std::string player, std::string password) {
473 GetState().Connect(server, player, password); 706 GetState().Connect(server, player, password);
474} 707}
475 708
476bool AP_HasCheckedGameLocation(int location_id) { 709std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); }
477 return GetState().HasCheckedGameLocation(location_id); 710
711std::string AP_GetSaveName() {
712 std::lock_guard state_guard(GetState().state_mutex);
713
714 return GetState().save_name;
478} 715}
479 716
480bool AP_HasCheckedHuntPanel(int location_id) { 717bool AP_HasCheckedGameLocation(int location_id) {
481 return GetState().HasCheckedHuntPanel(location_id); 718 return GetState().HasCheckedGameLocation(location_id);
482} 719}
483 720
484bool AP_HasItem(int item_id, int quantity) { 721bool AP_HasItem(int item_id, int quantity) {
485 return GetState().HasItem(item_id, quantity); 722 return GetState().HasItem(item_id, quantity);
486} 723}
487 724
488DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 725bool AP_HasItemSafe(int item_id, int quantity) {
726 return GetState().HasItemSafe(item_id, quantity);
727}
728
729DoorShuffleMode AP_GetDoorShuffleMode() {
730 std::lock_guard state_guard(GetState().state_mutex);
489 731
490bool AP_IsColorShuffle() { return GetState().color_shuffle; } 732 return GetState().door_shuffle_mode;
733}
734
735bool AP_AreDoorsGrouped() {
736 std::lock_guard state_guard(GetState().state_mutex);
737
738 return GetState().group_doors;
739}
740
741bool AP_IsColorShuffle() {
742 std::lock_guard state_guard(GetState().state_mutex);
491 743
492bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 744 return GetState().color_shuffle;
745}
746
747bool AP_IsPaintingShuffle() {
748 std::lock_guard state_guard(GetState().state_mutex);
749
750 return GetState().painting_shuffle;
751}
752
753std::map<std::string, std::string> AP_GetPaintingMapping() {
754 std::lock_guard state_guard(GetState().state_mutex);
493 755
494const std::map<std::string, std::string>& AP_GetPaintingMapping() {
495 return GetState().painting_mapping; 756 return GetState().painting_mapping;
496} 757}
497 758
498const std::set<std::string>& AP_GetCheckedPaintings() { 759bool AP_IsPaintingMappedTo(const std::string& painting_id) {
760 std::lock_guard state_guard(GetState().state_mutex);
761
762 return GetState().painting_codomain.count(painting_id);
763}
764
765std::set<std::string> AP_GetCheckedPaintings() {
499 return GetState().GetCheckedPaintings(); 766 return GetState().GetCheckedPaintings();
500} 767}
501 768
502int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 769bool AP_IsPaintingChecked(const std::string& painting_id) {
770 return GetState().IsPaintingChecked(painting_id);
771}
772
773void AP_RevealPaintings() { GetState().RevealPaintings(); }
774
775int AP_GetMasteryRequirement() {
776 std::lock_guard state_guard(GetState().state_mutex);
777
778 return GetState().mastery_requirement;
779}
780
781int AP_GetLevel2Requirement() {
782 std::lock_guard state_guard(GetState().state_mutex);
783
784 return GetState().level_2_requirement;
785}
503 786
504int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 787LocationChecks AP_GetLocationsChecks() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
790 return GetState().location_checks;
791}
505 792
506bool AP_IsLocationVisible(int classification) { 793bool AP_IsLocationVisible(int classification) {
794 std::lock_guard state_guard(GetState().state_mutex);
795
796 int world_state = 0;
797
507 switch (GetState().location_checks) { 798 switch (GetState().location_checks) {
508 case kNORMAL_LOCATIONS: 799 case kNORMAL_LOCATIONS:
509 return classification & kLOCATION_NORMAL; 800 world_state = kLOCATION_NORMAL;
801 break;
510 case kREDUCED_LOCATIONS: 802 case kREDUCED_LOCATIONS:
511 return classification & kLOCATION_REDUCED; 803 world_state = kLOCATION_REDUCED;
804 break;
512 case kPANELSANITY: 805 case kPANELSANITY:
513 return classification & kLOCATION_INSANITY; 806 world_state = kLOCATION_INSANITY;
807 break;
514 default: 808 default:
515 return false; 809 return false;
516 } 810 }
811
812 if (GetState().door_shuffle_mode == kDOORS_MODE &&
813 !GetState().early_color_hallways) {
814 world_state |= kLOCATION_SMALL_SPHERE_ONE;
815 }
816
817 return (world_state & classification);
818}
819
820PanelShuffleMode AP_GetPanelShuffleMode() {
821 std::lock_guard state_guard(GetState().state_mutex);
822
823 return GetState().panel_shuffle_mode;
517} 824}
518 825
519VictoryCondition AP_GetVictoryCondition() { 826VictoryCondition AP_GetVictoryCondition() {
827 std::lock_guard state_guard(GetState().state_mutex);
828
520 return GetState().victory_condition; 829 return GetState().victory_condition;
521} 830}
522 831
523bool AP_HasAchievement(const std::string& achievement_name) { 832bool AP_HasEarlyColorHallways() {
524 return GetState().HasAchievement(achievement_name); 833 std::lock_guard state_guard(GetState().state_mutex);
834
835 return GetState().early_color_hallways;
525} 836}
526 837
527bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } 838bool AP_IsPilgrimageEnabled() {
839 std::lock_guard state_guard(GetState().state_mutex);
528 840
529bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } 841 return GetState().pilgrimage_enabled;
842}
530 843
531bool AP_DoesPilgrimageAllowRoofAccess() { 844bool AP_DoesPilgrimageAllowRoofAccess() {
845 std::lock_guard state_guard(GetState().state_mutex);
846
532 return GetState().pilgrimage_allows_roof_access; 847 return GetState().pilgrimage_allows_roof_access;
533} 848}
534 849
535bool AP_DoesPilgrimageAllowPaintings() { 850bool AP_DoesPilgrimageAllowPaintings() {
851 std::lock_guard state_guard(GetState().state_mutex);
852
536 return GetState().pilgrimage_allows_paintings; 853 return GetState().pilgrimage_allows_paintings;
537} 854}
538 855
539SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } 856SunwarpAccess AP_GetSunwarpAccess() {
857 std::lock_guard state_guard(GetState().state_mutex);
540 858
541bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } 859 return GetState().sunwarp_access;
860}
861
862bool AP_IsSunwarpShuffle() {
863 std::lock_guard state_guard(GetState().state_mutex);
864
865 return GetState().sunwarp_shuffle;
866}
542 867
543const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { 868std::map<int, SunwarpMapping> AP_GetSunwarpMapping() {
544 return GetState().sunwarp_mapping; 869 return GetState().sunwarp_mapping;
545} 870}
546 871
872bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; }
873
547bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 874bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
548 875
549std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { 876std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
877 std::lock_guard state_guard(GetState().state_mutex);
878
550 return GetState().player_pos; 879 return GetState().player_pos;
551} 880}
881
882bool AP_IsPanelSolved(int solve_index) {
883 return GetState().IsPanelSolved(solve_index);
884}
diff --git a/src/ap_state.h b/src/ap_state.h index 5fbb720..a757d89 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -11,7 +11,7 @@
11 11
12class TrackerFrame; 12class TrackerFrame;
13 13
14enum DoorShuffleMode { kNO_DOORS = 0, kSIMPLE_DOORS = 1, kCOMPLEX_DOORS = 2 }; 14enum DoorShuffleMode { kNO_DOORS = 0, kPANELS_MODE = 1, kDOORS_MODE = 2 };
15 15
16enum VictoryCondition { 16enum VictoryCondition {
17 kTHE_END = 0, 17 kTHE_END = 0,
@@ -26,6 +26,8 @@ enum LocationChecks {
26 kPANELSANITY = 2 26 kPANELSANITY = 2
27}; 27};
28 28
29enum PanelShuffleMode { kNO_PANELS = 0, kREARRANGE_PANELS = 1 };
30
29enum SunwarpAccess { 31enum SunwarpAccess {
30 kSUNWARP_ACCESS_NORMAL = 0, 32 kSUNWARP_ACCESS_NORMAL = 0,
31 kSUNWARP_ACCESS_DISABLED = 1, 33 kSUNWARP_ACCESS_DISABLED = 1,
@@ -39,35 +41,57 @@ struct SunwarpMapping {
39 int exit_index; 41 int exit_index;
40}; 42};
41 43
44struct ItemState {
45 std::string name;
46 int amount = 0;
47 int index = 0;
48};
49
42void AP_SetTrackerFrame(TrackerFrame* tracker_frame); 50void AP_SetTrackerFrame(TrackerFrame* tracker_frame);
43 51
44void AP_Connect(std::string server, std::string player, std::string password); 52void AP_Connect(std::string server, std::string player, std::string password);
45 53
46bool AP_HasCheckedGameLocation(int location_id); 54std::string AP_GetStatusMessage();
47 55
48bool AP_HasCheckedHuntPanel(int location_id); 56std::string AP_GetSaveName();
49 57
58bool AP_HasCheckedGameLocation(int location_id);
59
60// This doesn't lock the state mutex, for speed, so it must ONLY be called from
61// RecalculateReachability, which is only called from the APState thread anyway.
50bool AP_HasItem(int item_id, int quantity = 1); 62bool AP_HasItem(int item_id, int quantity = 1);
51 63
64bool AP_HasItemSafe(int item_id, int quantity = 1);
65
52DoorShuffleMode AP_GetDoorShuffleMode(); 66DoorShuffleMode AP_GetDoorShuffleMode();
53 67
68bool AP_AreDoorsGrouped();
69
54bool AP_IsColorShuffle(); 70bool AP_IsColorShuffle();
55 71
56bool AP_IsPaintingShuffle(); 72bool AP_IsPaintingShuffle();
57 73
58const std::map<std::string, std::string>& AP_GetPaintingMapping(); 74std::map<std::string, std::string> AP_GetPaintingMapping();
59 75
60const std::set<std::string>& AP_GetCheckedPaintings(); 76bool AP_IsPaintingMappedTo(const std::string& painting_id);
77
78std::set<std::string> AP_GetCheckedPaintings();
79
80bool AP_IsPaintingChecked(const std::string& painting_id);
81
82void AP_RevealPaintings();
61 83
62int AP_GetMasteryRequirement(); 84int AP_GetMasteryRequirement();
63 85
64int AP_GetLevel2Requirement(); 86int AP_GetLevel2Requirement();
65 87
88LocationChecks AP_GetLocationsChecks();
89
66bool AP_IsLocationVisible(int classification); 90bool AP_IsLocationVisible(int classification);
67 91
68VictoryCondition AP_GetVictoryCondition(); 92PanelShuffleMode AP_GetPanelShuffleMode();
69 93
70bool AP_HasAchievement(const std::string& achievement_name); 94VictoryCondition AP_GetVictoryCondition();
71 95
72bool AP_HasEarlyColorHallways(); 96bool AP_HasEarlyColorHallways();
73 97
@@ -81,10 +105,14 @@ SunwarpAccess AP_GetSunwarpAccess();
81 105
82bool AP_IsSunwarpShuffle(); 106bool AP_IsSunwarpShuffle();
83 107
84const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); 108std::map<int, SunwarpMapping> AP_GetSunwarpMapping();
109
110bool AP_IsPostgameShuffle();
85 111
86bool AP_HasReachedGoal(); 112bool AP_HasReachedGoal();
87 113
88std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); 114std::optional<std::tuple<int, int>> AP_GetPlayerPosition();
89 115
116bool AP_IsPanelSolved(int solve_index);
117
90#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 6e70315..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -2,62 +2,68 @@
2 2
3#include <wx/dcbuffer.h> 3#include <wx/dcbuffer.h>
4 4
5#include <algorithm>
6
5#include "ap_state.h" 7#include "ap_state.h"
6#include "game_data.h" 8#include "game_data.h"
7#include "global.h" 9#include "global.h"
10#include "icons.h"
8#include "tracker_config.h" 11#include "tracker_config.h"
12#include "tracker_panel.h"
9#include "tracker_state.h" 13#include "tracker_state.h"
10 14
11AreaPopup::AreaPopup(wxWindow* parent, int area_id) 15AreaPopup::AreaPopup(wxWindow* parent, int area_id)
12 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 16 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
13 SetBackgroundStyle(wxBG_STYLE_PAINT); 17 SetBackgroundStyle(wxBG_STYLE_PAINT);
14 18
15 unchecked_eye_ = 19 LoadIcons();
16 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
17 wxBITMAP_TYPE_PNG)
18 .Scale(32, 32));
19 checked_eye_ = wxBitmap(
20 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
21 .Scale(32, 32));
22 20
21 // TODO: This is slow on high-DPI screens.
23 SetScrollRate(5, 5); 22 SetScrollRate(5, 5);
24 23
25 SetBackgroundColour(*wxBLACK); 24 SetBackgroundColour(*wxBLACK);
26 Hide(); 25 Hide();
27 26
28 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); 27 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this);
28 Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this);
29 29
30 UpdateIndicators(); 30 ResetIndicators();
31} 31}
32 32
33void AreaPopup::UpdateIndicators() { 33void AreaPopup::ResetIndicators() {
34 indicators_.clear();
35
34 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());
35 39
36 // Start calculating extents. 40 // Start calculating extents.
37 wxMemoryDC mem_dc; 41 wxMemoryDC mem_dc;
38 mem_dc.SetFont(GetFont().Bold()); 42 mem_dc.SetFont(the_font.Bold());
39 wxSize header_extent = mem_dc.GetTextExtent(map_area.name); 43 header_extent_ = mem_dc.GetTextExtent(map_area.name);
40 44
41 int acc_height = header_extent.GetHeight() + 20; 45 int acc_height = header_extent_.GetHeight() + FromDIP(20);
42 int col_width = 0; 46 int col_width = 0;
43 47
44 mem_dc.SetFont(GetFont()); 48 mem_dc.SetFont(the_font);
45
46 std::vector<int> real_locations;
47 49
48 for (int section_id = 0; section_id < map_area.locations.size(); 50 for (int section_id = 0; section_id < map_area.locations.size();
49 section_id++) { 51 section_id++) {
50 const Location& location = map_area.locations.at(section_id); 52 const Location& location = map_area.locations.at(section_id);
51 53 if ((!AP_IsLocationVisible(location.classification) ||
52 if (!AP_IsLocationVisible(location.classification) && 54 IsLocationPostgame(location.ap_location_id)) &&
53 !(location.hunt && GetTrackerConfig().show_hunt_panels)) { 55 !(location.hunt &&
56 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
57 !(location.single_panel &&
58 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) {
54 continue; 59 continue;
55 } 60 }
56 61
57 real_locations.push_back(section_id); 62 indicators_.emplace_back(section_id, kLOCATION, acc_height);
58 63
59 wxSize item_extent = mem_dc.GetTextExtent(location.name); 64 wxSize item_extent = mem_dc.GetTextExtent(location.name);
60 int item_height = std::max(32, item_extent.GetHeight()) + 10; 65 int item_height =
66 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
61 acc_height += item_height; 67 acc_height += item_height;
62 68
63 if (item_extent.GetWidth() > col_width) { 69 if (item_extent.GetWidth() > col_width) {
@@ -65,49 +71,107 @@ void AreaPopup::UpdateIndicators() {
65 } 71 }
66 } 72 }
67 73
68 int item_width = col_width + 10 + 32; 74 if (AP_IsPaintingShuffle()) {
69 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 75 for (int painting_id : map_area.paintings) {
70 76 if (IsPaintingPostgame(painting_id)) {
71 Fit(); 77 continue;
72 SetVirtualSize(full_width, acc_height); 78 }
73
74 rendered_ = wxBitmap(full_width, acc_height);
75 mem_dc.SelectObject(rendered_);
76 mem_dc.SetPen(*wxTRANSPARENT_PEN);
77 mem_dc.SetBrush(*wxBLACK_BRUSH);
78 mem_dc.DrawRectangle({0, 0}, {full_width, acc_height});
79 79
80 mem_dc.SetFont(GetFont().Bold()); 80 indicators_.emplace_back(painting_id, kPAINTING, acc_height);
81 mem_dc.SetTextForeground(*wxWHITE);
82 mem_dc.DrawText(map_area.name,
83 {(full_width - header_extent.GetWidth()) / 2, 10});
84 81
85 int cur_height = header_extent.GetHeight() + 20; 82 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
83 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
84 int item_height =
85 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
86 acc_height += item_height;
86 87
87 mem_dc.SetFont(GetFont()); 88 if (item_extent.GetWidth() > col_width) {
89 col_width = item_extent.GetWidth();
90 }
91 }
92 }
88 93
89 for (int section_id : real_locations) { 94 int item_width = col_width + FromDIP(10 + 32);
90 const Location& location = map_area.locations.at(section_id); 95 full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20);
96 full_height_ = acc_height;
91 97
92 bool checked = 98 Fit();
93 AP_HasCheckedGameLocation(location.ap_location_id) || 99 SetVirtualSize(full_width_, full_height_);
94 (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id)) ||
95 (IsLocationWinCondition(location) && AP_HasReachedGoal());
96 100
97 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; 101 UpdateIndicators();
102}
98 103
99 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 104void AreaPopup::UpdateIndicators() {
105 const MapArea& map_area = GD_GetMapArea(area_id_);
106 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
107 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
100 108
101 bool reachable = IsLocationReachable(location.ap_location_id); 109 rendered_ = wxBitmap(full_width_, full_height_);
102 const wxColour* text_color = reachable ? wxWHITE : wxRED;
103 mem_dc.SetTextForeground(*text_color);
104 110
105 wxSize item_extent = mem_dc.GetTextExtent(location.name); 111 wxMemoryDC mem_dc;
106 mem_dc.DrawText( 112 mem_dc.SelectObject(rendered_);
107 location.name, 113 mem_dc.SetPen(*wxTRANSPARENT_PEN);
108 {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 114 mem_dc.SetBrush(*wxBLACK_BRUSH);
115 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
109 116
110 cur_height += 10 + 32; 117 mem_dc.SetFont(the_font.Bold());
118 mem_dc.SetTextForeground(*wxWHITE);
119 mem_dc.DrawText(map_area.name,
120 {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)});
121
122 mem_dc.SetFont(the_font);
123
124 for (const IndicatorInfo& indicator : indicators_) {
125 switch (indicator.type) {
126 case kLOCATION: {
127 const Location& location = map_area.locations.at(indicator.id);
128
129 bool checked = false;
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 }
138
139 const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_;
140
141 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
142
143 bool reachable = IsLocationReachable(location.ap_location_id);
144 const wxColour* text_color = reachable ? wxWHITE : wxRED;
145 mem_dc.SetTextForeground(*text_color);
146
147 wxSize item_extent = mem_dc.GetTextExtent(location.name);
148 mem_dc.DrawText(
149 location.name,
150 {FromDIP(10 + 32 + 10),
151 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
152
153 break;
154 }
155 case kPAINTING: {
156 const PaintingExit& painting = GD_GetPaintingExit(indicator.id);
157
158 bool reachable = IsPaintingReachable(indicator.id);
159 const wxColour* text_color = reachable ? wxWHITE : wxRED;
160 mem_dc.SetTextForeground(*text_color);
161
162 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id);
163 const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_;
164 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
165
166 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
167 mem_dc.DrawText(
168 painting.display_name,
169 {FromDIP(10 + 32 + 10),
170 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
171
172 break;
173 }
174 }
111 } 175 }
112} 176}
113 177
@@ -118,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) {
118 182
119 event.Skip(); 183 event.Skip();
120} 184}
185
186void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) {
187 LoadIcons();
188 ResetIndicators();
189
190 event.Skip();
191}
192
193void AreaPopup::LoadIcons() {
194 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
195 FromDIP(wxSize{32, 32}));
196 checked_eye_ =
197 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
198 unchecked_owl_ =
199 GetTheIconCache().GetIcon("assets/owl.png", FromDIP(wxSize{32, 32}));
200 checked_owl_ = GetTheIconCache().GetIcon("assets/checked_owl.png",
201 FromDIP(wxSize{32, 32}));
202}
diff --git a/src/area_popup.h b/src/area_popup.h index 00c644d..f8a2355 100644 --- a/src/area_popup.h +++ b/src/area_popup.h
@@ -7,19 +7,53 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <vector>
11
10class AreaPopup : public wxScrolledCanvas { 12class AreaPopup : public wxScrolledCanvas {
11 public: 13 public:
12 AreaPopup(wxWindow* parent, int area_id); 14 AreaPopup(wxWindow* parent, int area_id);
13 15
16 void ResetIndicators();
14 void UpdateIndicators(); 17 void UpdateIndicators();
15 18
19 int GetFullWidth() const { return full_width_; }
20 int GetFullHeight() const { return full_height_; }
21
16 private: 22 private:
23 enum IndicatorType {
24 kLOCATION,
25 kPAINTING,
26 };
27
28 struct IndicatorInfo {
29 // For locations, the id is an index into the map area's locations list.
30 // For paintings, it is a real painting id.
31 int id;
32 IndicatorType type;
33 int y;
34
35 IndicatorInfo(int id, IndicatorType type, int y)
36 : id(id), type(type), y(y) {}
37 };
38
17 void OnPaint(wxPaintEvent& event); 39 void OnPaint(wxPaintEvent& event);
40 void OnDPIChanged(wxDPIChangedEvent& event);
41
42 void LoadIcons();
18 43
19 int area_id_; 44 int area_id_;
20 45
21 wxBitmap unchecked_eye_; 46 const wxBitmap* unchecked_eye_;
22 wxBitmap checked_eye_; 47 const wxBitmap* checked_eye_;
48 const wxBitmap* unchecked_owl_;
49 const wxBitmap* checked_owl_;
50
51 int full_width_ = 0;
52 int full_height_ = 0;
53 wxSize header_extent_;
54
55 std::vector<IndicatorInfo> indicators_;
56
23 wxBitmap rendered_; 57 wxBitmap rendered_;
24}; 58};
25 59
diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp index 64fee98..b55a138 100644 --- a/src/connection_dialog.cpp +++ b/src/connection_dialog.cpp
@@ -4,17 +4,21 @@
4 4
5ConnectionDialog::ConnectionDialog() 5ConnectionDialog::ConnectionDialog()
6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { 6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
7 server_box_ = 7 server_box_ = new wxTextCtrl(
8 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_server, 8 this, -1,
9 wxDefaultPosition, {300, -1}); 9 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server),
10 player_box_ = 10 wxDefaultPosition, FromDIP(wxSize{300, -1}));
11 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_player, 11 player_box_ = new wxTextCtrl(
12 wxDefaultPosition, {300, -1}); 12 this, -1,
13 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_player),
14 wxDefaultPosition, FromDIP(wxSize{300, -1}));
13 password_box_ = new wxTextCtrl( 15 password_box_ = new wxTextCtrl(
14 this, -1, GetTrackerConfig().connection_details.ap_password, 16 this, -1,
15 wxDefaultPosition, {300, -1}); 17 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_password),
18 wxDefaultPosition, FromDIP(wxSize{300, -1}));
16 19
17 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10); 20 wxFlexGridSizer* form_sizer =
21 new wxFlexGridSizer(2, FromDIP(10), FromDIP(10));
18 22
19 form_sizer->Add( 23 form_sizer->Add(
20 new wxStaticText(this, -1, "Server:"), 24 new wxStaticText(this, -1, "Server:"),
@@ -30,17 +34,19 @@ ConnectionDialog::ConnectionDialog()
30 form_sizer->Add(password_box_, wxSizerFlags().Expand()); 34 form_sizer->Add(password_box_, wxSizerFlags().Expand());
31 35
32 history_list_ = new wxListBox(this, -1); 36 history_list_ = new wxListBox(this, -1);
33 for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { 37 for (const ConnectionDetails& details :
38 GetTrackerConfig().connection_history) {
34 wxString display_text; 39 wxString display_text;
35 display_text << details.ap_player; 40 display_text << wxString::FromUTF8(details.ap_player);
36 display_text << " ("; 41 display_text << " (";
37 display_text << details.ap_server; 42 display_text << wxString::FromUTF8(details.ap_server);
38 display_text << ")"; 43 display_text << ")";
39 44
40 history_list_->Append(display_text); 45 history_list_->Append(display_text);
41 } 46 }
42 47
43 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, this); 48 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen,
49 this);
44 50
45 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); 51 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL);
46 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); 52 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand());
@@ -52,7 +58,8 @@ ConnectionDialog::ConnectionDialog()
52 this, -1, "Enter the details to connect to Archipelago."), 58 this, -1, "Enter the details to connect to Archipelago."),
53 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); 59 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
54 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); 60 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand());
55 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Border().Center()); 61 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
62 wxSizerFlags().Border().Center());
56 63
57 SetSizerAndFit(top_sizer); 64 SetSizerAndFit(top_sizer);
58 65
@@ -62,9 +69,10 @@ ConnectionDialog::ConnectionDialog()
62 69
63void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { 70void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) {
64 if (e.IsSelection()) { 71 if (e.IsSelection()) {
65 const ConnectionDetails& details = GetTrackerConfig().connection_history.at(e.GetSelection()); 72 const ConnectionDetails& details =
66 server_box_->SetValue(details.ap_server); 73 GetTrackerConfig().connection_history.at(e.GetSelection());
67 player_box_->SetValue(details.ap_player); 74 server_box_->SetValue(wxString::FromUTF8(details.ap_server));
68 password_box_->SetValue(details.ap_password); 75 player_box_->SetValue(wxString::FromUTF8(details.ap_player));
76 password_box_->SetValue(wxString::FromUTF8(details.ap_password));
69 } 77 }
70} 78}
diff --git a/src/connection_dialog.h b/src/connection_dialog.h index 9fe62fd..ec2ee72 100644 --- a/src/connection_dialog.h +++ b/src/connection_dialog.h
@@ -14,12 +14,12 @@ class ConnectionDialog : public wxDialog {
14 public: 14 public:
15 ConnectionDialog(); 15 ConnectionDialog();
16 16
17 std::string GetServerValue() { return server_box_->GetValue().ToStdString(); } 17 std::string GetServerValue() { return server_box_->GetValue().utf8_string(); }
18 18
19 std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); } 19 std::string GetPlayerValue() { return player_box_->GetValue().utf8_string(); }
20 20
21 std::string GetPasswordValue() { 21 std::string GetPasswordValue() {
22 return password_box_->GetValue().ToStdString(); 22 return password_box_->GetValue().utf8_string();
23 } 23 }
24 24
25 private: 25 private:
diff --git a/src/game_data.cpp b/src/game_data.cpp index 7c9564b..588ffc8 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -1,11 +1,6 @@
1#include "game_data.h" 1#include "game_data.h"
2 2
3#include <wx/wxprec.h> 3#include <fmt/core.h>
4
5#ifndef WX_PRECOMP
6#include <wx/wx.h>
7#endif
8
9#include <hkutil/string.h> 4#include <hkutil/string.h>
10#include <yaml-cpp/yaml.h> 5#include <yaml-cpp/yaml.h>
11 6
@@ -13,51 +8,31 @@
13#include <sstream> 8#include <sstream>
14 9
15#include "global.h" 10#include "global.h"
11#include "logger.h"
16 12
17namespace { 13namespace {
18 14
19LingoColor GetColorForString(const std::string &str) {
20 if (str == "black") {
21 return LingoColor::kBlack;
22 } else if (str == "red") {
23 return LingoColor::kRed;
24 } else if (str == "blue") {
25 return LingoColor::kBlue;
26 } else if (str == "yellow") {
27 return LingoColor::kYellow;
28 } else if (str == "orange") {
29 return LingoColor::kOrange;
30 } else if (str == "green") {
31 return LingoColor::kGreen;
32 } else if (str == "gray") {
33 return LingoColor::kGray;
34 } else if (str == "brown") {
35 return LingoColor::kBrown;
36 } else if (str == "purple") {
37 return LingoColor::kPurple;
38 } else {
39 wxLogError("Invalid color: %s", str);
40
41 return LingoColor::kNone;
42 }
43}
44
45struct GameData { 15struct GameData {
46 std::vector<Room> rooms_; 16 std::vector<Room> rooms_;
47 std::vector<Door> doors_; 17 std::vector<Door> doors_;
48 std::vector<Panel> panels_; 18 std::vector<Panel> panels_;
19 std::vector<PanelDoor> panel_doors_;
49 std::vector<MapArea> map_areas_; 20 std::vector<MapArea> map_areas_;
50 std::vector<SubwayItem> subway_items_; 21 std::vector<SubwayItem> subway_items_;
22 std::vector<PaintingExit> paintings_;
51 23
52 std::map<std::string, int> room_by_id_; 24 std::map<std::string, int> room_by_id_;
53 std::map<std::string, int> door_by_id_; 25 std::map<std::string, int> door_by_id_;
54 std::map<std::string, int> panel_by_id_; 26 std::map<std::string, int> panel_by_id_;
27 std::map<std::string, int> panel_doors_by_id_;
55 std::map<std::string, int> area_by_id_; 28 std::map<std::string, int> area_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 32
59 std::map<std::string, int> room_by_painting_; 33 std::map<std::string, int> room_by_painting_;
60 std::map<int, int> room_by_sunwarp_; 34 std::map<int, int> room_by_sunwarp_;
35 std::map<int, int> panel_by_solve_index_;
61 36
62 std::vector<int> achievement_panels_; 37 std::vector<int> achievement_panels_;
63 38
@@ -68,6 +43,8 @@ struct GameData {
68 std::map<std::string, int> subway_item_by_painting_; 43 std::map<std::string, int> subway_item_by_painting_;
69 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; 44 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
70 45
46 std::map<int, std::string> item_by_ap_id_;
47
71 bool loaded_area_data_ = false; 48 bool loaded_area_data_ = false;
72 std::set<std::string> malconfigured_areas_; 49 std::set<std::string> malconfigured_areas_;
73 50
@@ -83,10 +60,10 @@ struct GameData {
83 ids_config["special_items"][color_name]) { 60 ids_config["special_items"][color_name]) {
84 std::string input_name = color_name; 61 std::string input_name = color_name;
85 input_name[0] = std::tolower(input_name[0]); 62 input_name[0] = std::tolower(input_name[0]);
86 ap_id_by_color_[GetColorForString(input_name)] = 63 ap_id_by_color_[GetLingoColorForString(input_name)] =
87 ids_config["special_items"][color_name].as<int>(); 64 ids_config["special_items"][color_name].as<int>();
88 } else { 65 } else {
89 wxLogError("Missing AP item ID for color %s", color_name); 66 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
90 } 67 }
91 }; 68 };
92 69
@@ -100,8 +77,18 @@ struct GameData {
100 init_color_id("Brown"); 77 init_color_id("Brown");
101 init_color_id("Gray"); 78 init_color_id("Gray");
102 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
103 rooms_.reserve(lingo_config.size() * 2); 88 rooms_.reserve(lingo_config.size() * 2);
104 89
90 std::vector<int> panel_location_ids;
91
105 for (const auto &room_it : lingo_config) { 92 for (const auto &room_it : lingo_config) {
106 int room_id = AddOrGetRoom(room_it.first.as<std::string>()); 93 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
107 94
@@ -111,6 +98,7 @@ struct GameData {
111 auto process_single_entrance = 98 auto process_single_entrance =
112 [this, room_id, from_room_id](const YAML::Node &option) { 99 [this, room_id, from_room_id](const YAML::Node &option) {
113 Exit exit_obj; 100 Exit exit_obj;
101 exit_obj.source_room = from_room_id;
114 exit_obj.destination_room = room_id; 102 exit_obj.destination_room = room_id;
115 103
116 if (option["door"]) { 104 if (option["door"]) {
@@ -139,13 +127,17 @@ struct GameData {
139 exit_obj.type = EntranceType::kCrossroadsRoofAccess; 127 exit_obj.type = EntranceType::kCrossroadsRoofAccess;
140 } 128 }
141 129
130 if (option["static_painting"] && option["static_painting"].as<bool>()) {
131 exit_obj.type = EntranceType::kStaticPainting;
132 }
133
142 rooms_[from_room_id].exits.push_back(exit_obj); 134 rooms_[from_room_id].exits.push_back(exit_obj);
143 }; 135 };
144 136
145 switch (entrance_it.second.Type()) { 137 switch (entrance_it.second.Type()) {
146 case YAML::NodeType::Scalar: { 138 case YAML::NodeType::Scalar: {
147 // This is just "true". 139 // This is just "true".
148 rooms_[from_room_id].exits.push_back({.destination_room = room_id}); 140 rooms_[from_room_id].exits.push_back({.source_room = from_room_id, .destination_room = room_id});
149 break; 141 break;
150 } 142 }
151 case YAML::NodeType::Map: { 143 case YAML::NodeType::Map: {
@@ -163,7 +155,8 @@ struct GameData {
163 // This shouldn't happen. 155 // This shouldn't happen.
164 std::ostringstream formatted; 156 std::ostringstream formatted;
165 formatted << entrance_it; 157 formatted << entrance_it;
166 wxLogError("Error reading game data: %s", formatted.str()); 158 TrackerLog(
159 fmt::format("Error reading game data: {}", formatted.str()));
167 break; 160 break;
168 } 161 }
169 } 162 }
@@ -177,12 +170,12 @@ struct GameData {
177 170
178 if (panel_it.second["colors"]) { 171 if (panel_it.second["colors"]) {
179 if (panel_it.second["colors"].IsScalar()) { 172 if (panel_it.second["colors"].IsScalar()) {
180 panels_[panel_id].colors.push_back(GetColorForString( 173 panels_[panel_id].colors.push_back(GetLingoColorForString(
181 panel_it.second["colors"].as<std::string>())); 174 panel_it.second["colors"].as<std::string>()));
182 } else { 175 } else {
183 for (const auto &color_node : panel_it.second["colors"]) { 176 for (const auto &color_node : panel_it.second["colors"]) {
184 panels_[panel_id].colors.push_back( 177 panels_[panel_id].colors.push_back(
185 GetColorForString(color_node.as<std::string>())); 178 GetLingoColorForString(color_node.as<std::string>()));
186 } 179 }
187 } 180 }
188 } 181 }
@@ -260,6 +253,16 @@ struct GameData {
260 achievement_panels_.push_back(panel_id); 253 achievement_panels_.push_back(panel_id);
261 } 254 }
262 255
256 if (panel_it.second["location_name"]) {
257 panels_[panel_id].location_name =
258 panel_it.second["location_name"].as<std::string>();
259 }
260
261 if (panel_it.second["id"]) {
262 panels_[panel_id].nodepath =
263 panel_it.second["id"].as<std::string>();
264 }
265
263 if (panel_it.second["hunt"]) { 266 if (panel_it.second["hunt"]) {
264 panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>(); 267 panels_[panel_id].hunt = panel_it.second["hunt"].as<bool>();
265 } 268 }
@@ -278,13 +281,15 @@ struct GameData {
278 ids_config["panels"][rooms_[room_id].name] && 281 ids_config["panels"][rooms_[room_id].name] &&
279 ids_config["panels"][rooms_[room_id].name] 282 ids_config["panels"][rooms_[room_id].name]
280 [panels_[panel_id].name]) { 283 [panels_[panel_id].name]) {
281 panels_[panel_id].ap_location_id = 284 int location_id = ids_config["panels"][rooms_[room_id].name]
282 ids_config["panels"][rooms_[room_id].name] 285 [panels_[panel_id].name]
283 [panels_[panel_id].name] 286 .as<int>();
284 .as<int>(); 287 panels_[panel_id].ap_location_id = location_id;
288 panel_location_ids.push_back(location_id);
285 } else { 289 } else {
286 wxLogError("Missing AP location ID for panel %s - %s", 290 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
287 rooms_[room_id].name, panels_[panel_id].name); 291 rooms_[room_id].name,
292 panels_[panel_id].name));
288 } 293 }
289 } 294 }
290 } 295 }
@@ -346,9 +351,13 @@ struct GameData {
346 ids_config["doors"][rooms_[room_id].name] 351 ids_config["doors"][rooms_[room_id].name]
347 [doors_[door_id].name]["item"] 352 [doors_[door_id].name]["item"]
348 .as<int>(); 353 .as<int>();
354
355 item_by_ap_id_[doors_[door_id].ap_item_id] =
356 doors_[door_id].item_name;
349 } else { 357 } else {
350 wxLogError("Missing AP item ID for door %s - %s", 358 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
351 rooms_[room_id].name, doors_[door_id].name); 359 rooms_[room_id].name,
360 doors_[door_id].name));
352 } 361 }
353 } 362 }
354 363
@@ -361,9 +370,12 @@ struct GameData {
361 doors_[door_id].group_ap_item_id = 370 doors_[door_id].group_ap_item_id =
362 ids_config["door_groups"][doors_[door_id].group_name] 371 ids_config["door_groups"][doors_[door_id].group_name]
363 .as<int>(); 372 .as<int>();
373
374 item_by_ap_id_[doors_[door_id].group_ap_item_id] =
375 doors_[door_id].group_name;
364 } else { 376 } else {
365 wxLogError("Missing AP item ID for door group %s", 377 TrackerLog(fmt::format("Missing AP item ID for door group {}",
366 doors_[door_id].group_name); 378 doors_[door_id].group_name));
367 } 379 }
368 } 380 }
369 381
@@ -373,11 +385,11 @@ struct GameData {
373 } else if (!door_it.second["skip_location"] && 385 } else if (!door_it.second["skip_location"] &&
374 !door_it.second["event"]) { 386 !door_it.second["event"]) {
375 if (has_external_panels) { 387 if (has_external_panels) {
376 wxLogError( 388 TrackerLog(fmt::format(
377 "%s - %s has panels from other rooms but does not have an " 389 "{} - {} has panels from other rooms but does not have an "
378 "explicit location name and is not marked skip_location or " 390 "explicit location name and is not marked skip_location or "
379 "event", 391 "event",
380 rooms_[room_id].name, doors_[door_id].name); 392 rooms_[room_id].name, doors_[door_id].name));
381 } 393 }
382 394
383 doors_[door_id].location_name = 395 doors_[door_id].location_name =
@@ -397,8 +409,9 @@ struct GameData {
397 [doors_[door_id].name]["location"] 409 [doors_[door_id].name]["location"]
398 .as<int>(); 410 .as<int>();
399 } else { 411 } else {
400 wxLogError("Missing AP location ID for door %s - %s", 412 TrackerLog(fmt::format("Missing AP location ID for door {} - {}",
401 rooms_[room_id].name, doors_[door_id].name); 413 rooms_[room_id].name,
414 doors_[door_id].name));
402 } 415 }
403 } 416 }
404 417
@@ -417,14 +430,107 @@ struct GameData {
417 } 430 }
418 } 431 }
419 432
433 if (room_it.second["panel_doors"]) {
434 for (const auto &panel_door_it : room_it.second["panel_doors"]) {
435 std::string panel_door_name = panel_door_it.first.as<std::string>();
436 int panel_door_id =
437 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name);
438
439 std::map<std::string, std::vector<std::string>> panel_per_room;
440 int num_panels = 0;
441 for (const auto &panel_node : panel_door_it.second["panels"]) {
442 num_panels++;
443
444 int panel_id = -1;
445
446 if (panel_node.IsScalar()) {
447 panel_id = AddOrGetPanel(rooms_[room_id].name,
448 panel_node.as<std::string>());
449
450 panel_per_room[rooms_[room_id].name].push_back(
451 panel_node.as<std::string>());
452 } else {
453 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(),
454 panel_node["panel"].as<std::string>());
455
456 panel_per_room[panel_node["room"].as<std::string>()].push_back(
457 panel_node["panel"].as<std::string>());
458 }
459
460 Panel &panel = panels_[panel_id];
461 panel.panel_door = panel_door_id;
462 }
463
464 if (panel_door_it.second["item_name"]) {
465 panel_doors_[panel_door_id].item_name =
466 panel_door_it.second["item_name"].as<std::string>();
467 } else {
468 std::vector<std::string> room_strs;
469 for (const auto &[room_str, panels_str] : panel_per_room) {
470 room_strs.push_back(fmt::format(
471 "{} - {}", room_str, hatkirby::implode(panels_str, ", ")));
472 }
473
474 if (num_panels == 1) {
475 panel_doors_[panel_door_id].item_name =
476 fmt::format("{} (Panel)", room_strs[0]);
477 } else {
478 panel_doors_[panel_door_id].item_name = fmt::format(
479 "{} (Panels)", hatkirby::implode(room_strs, " and "));
480 }
481 }
482
483 if (ids_config["panel_doors"] &&
484 ids_config["panel_doors"][rooms_[room_id].name] &&
485 ids_config["panel_doors"][rooms_[room_id].name]
486 [panel_door_name]) {
487 panel_doors_[panel_door_id].ap_item_id =
488 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name]
489 .as<int>();
490
491 item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] =
492 panel_doors_[panel_door_id].item_name;
493 } else {
494 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}",
495 rooms_[room_id].name, panel_door_name));
496 }
497
498 if (panel_door_it.second["panel_group"]) {
499 std::string panel_group =
500 panel_door_it.second["panel_group"].as<std::string>();
501
502 if (ids_config["panel_groups"] &&
503 ids_config["panel_groups"][panel_group]) {
504 panel_doors_[panel_door_id].group_ap_item_id =
505 ids_config["panel_groups"][panel_group].as<int>();
506
507 item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] =
508 panel_group;
509 } else {
510 TrackerLog(fmt::format(
511 "Missing AP item ID for panel door group {}", panel_group));
512 }
513 }
514 }
515 }
516
420 if (room_it.second["paintings"]) { 517 if (room_it.second["paintings"]) {
421 for (const auto &painting : room_it.second["paintings"]) { 518 for (const auto &painting : room_it.second["paintings"]) {
422 std::string painting_id = painting["id"].as<std::string>(); 519 std::string internal_id = painting["id"].as<std::string>();
423 room_by_painting_[painting_id] = room_id; 520 int painting_id = AddOrGetPainting(internal_id);
521 PaintingExit &painting_exit = paintings_[painting_id];
522 painting_exit.room = room_id;
523
524 if (painting["display_name"]) {
525 painting_exit.display_name =
526 painting["display_name"].as<std::string>();
527 } else {
528 painting_exit.display_name = painting_exit.internal_id;
529 }
424 530
425 if (!painting["exit_only"] || !painting["exit_only"].as<bool>()) { 531 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
426 PaintingExit painting_exit; 532 (!painting["disable"] || !painting["disable"].as<bool>())) {
427 painting_exit.id = painting_id; 533 painting_exit.entrance = true;
428 534
429 if (painting["required_door"]) { 535 if (painting["required_door"]) {
430 std::string rd_room = rooms_[room_id].name; 536 std::string rd_room = rooms_[room_id].name;
@@ -435,9 +541,9 @@ struct GameData {
435 painting_exit.door = AddOrGetDoor( 541 painting_exit.door = AddOrGetDoor(
436 rd_room, painting["required_door"]["door"].as<std::string>()); 542 rd_room, painting["required_door"]["door"].as<std::string>());
437 } 543 }
438
439 rooms_[room_id].paintings.push_back(painting_exit);
440 } 544 }
545
546 rooms_[room_id].paintings.push_back(painting_exit.id);
441 } 547 }
442 } 548 }
443 549
@@ -463,33 +569,74 @@ struct GameData {
463 ids_config["progression"][progressive_item_name]) { 569 ids_config["progression"][progressive_item_name]) {
464 progressive_item_id = 570 progressive_item_id =
465 ids_config["progression"][progressive_item_name].as<int>(); 571 ids_config["progression"][progressive_item_name].as<int>();
572
573 item_by_ap_id_[progressive_item_id] = progressive_item_name;
466 } else { 574 } else {
467 wxLogError("Missing AP item ID for progressive item %s", 575 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
468 progressive_item_name); 576 progressive_item_name));
469 } 577 }
470 578
471 int index = 1; 579 if (progression_it.second["doors"]) {
472 for (const auto &stage : progression_it.second) { 580 int index = 1;
473 int door_id = -1; 581 for (const auto &stage : progression_it.second["doors"]) {
582 int door_id = -1;
583
584 if (stage.IsScalar()) {
585 door_id =
586 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>());
587 } else {
588 door_id = AddOrGetDoor(stage["room"].as<std::string>(),
589 stage["door"].as<std::string>());
590 }
474 591
475 if (stage.IsScalar()) { 592 doors_[door_id].progressives.push_back(
476 door_id = 593 {.item_name = progressive_item_name,
477 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); 594 .ap_item_id = progressive_item_id,
478 } else { 595 .quantity = index});
479 door_id = AddOrGetDoor(stage["room"].as<std::string>(), 596 index++;
480 stage["door"].as<std::string>());
481 } 597 }
598 }
482 599
483 doors_[door_id].progressives.push_back( 600 if (progression_it.second["panel_doors"]) {
484 {.item_name = progressive_item_name, 601 int index = 1;
485 .ap_item_id = progressive_item_id, 602 for (const auto &stage : progression_it.second["panel_doors"]) {
486 .quantity = index}); 603 int panel_door_id = -1;
487 index++; 604
605 if (stage.IsScalar()) {
606 panel_door_id = AddOrGetPanelDoor(rooms_[room_id].name,
607 stage.as<std::string>());
608 } else {
609 panel_door_id =
610 AddOrGetPanelDoor(stage["room"].as<std::string>(),
611 stage["panel_door"].as<std::string>());
612 }
613
614 panel_doors_[panel_door_id].progressives.push_back(
615 {.item_name = progressive_item_name,
616 .ap_item_id = progressive_item_id,
617 .quantity = index});
618 index++;
619 }
488 } 620 }
489 } 621 }
490 } 622 }
491 } 623 }
492 624
625 // Determine the panel solve indices from the sorted location IDs.
626 std::sort(panel_location_ids.begin(), panel_location_ids.end());
627
628 std::map<int, int> solve_index_by_location_id;
629 for (int i = 0; i < panel_location_ids.size(); i++) {
630 solve_index_by_location_id[panel_location_ids[i]] = i;
631 }
632
633 for (Panel &panel : panels_) {
634 if (panel.ap_location_id != -1) {
635 panel.solve_index = solve_index_by_location_id[panel.ap_location_id];
636 panel_by_solve_index_[panel.solve_index] = panel.id;
637 }
638 }
639
493 map_areas_.reserve(areas_config.size()); 640 map_areas_.reserve(areas_config.size());
494 641
495 std::map<std::string, int> fold_areas; 642 std::map<std::string, int> fold_areas;
@@ -510,13 +657,28 @@ struct GameData {
510 // Only locations for the panels are kept here. 657 // Only locations for the panels are kept here.
511 std::map<std::string, std::tuple<int, int>> locations_by_name; 658 std::map<std::string, std::tuple<int, int>> locations_by_name;
512 659
513 for (const Panel &panel : panels_) { 660 for (Panel &panel : panels_) {
514 int room_id = panel.room; 661 int room_id = panel.room;
515 std::string room_name = rooms_[room_id].name; 662 std::string room_name = rooms_[room_id].name;
516 663
517 std::string area_name = room_name; 664 std::string area_name = room_name;
518 if (fold_areas.count(room_name)) { 665 std::string section_name = panel.name;
519 int fold_area_id = fold_areas[room_name]; 666 std::string location_name = room_name + " - " + panel.name;
667
668 if (!panel.location_name.empty()) {
669 location_name = panel.location_name;
670
671 size_t divider_pos = location_name.find(" - ");
672 if (divider_pos != std::string::npos) {
673 area_name = location_name.substr(0, divider_pos);
674 section_name = location_name.substr(divider_pos + 3);
675 }
676 } else {
677 panel.location_name = location_name;
678 }
679
680 if (fold_areas.count(area_name)) {
681 int fold_area_id = fold_areas[area_name];
520 area_name = map_areas_[fold_area_id].name; 682 area_name = map_areas_[fold_area_id].name;
521 } 683 }
522 684
@@ -528,19 +690,23 @@ struct GameData {
528 } 690 }
529 } 691 }
530 692
693 if (room_name == "Starting Room") {
694 classification |= kLOCATION_SMALL_SPHERE_ONE;
695 }
696
531 int area_id = AddOrGetArea(area_name); 697 int area_id = AddOrGetArea(area_name);
532 MapArea &map_area = map_areas_[area_id]; 698 MapArea &map_area = map_areas_[area_id];
533 // room field should be the original room ID 699 // room field should be the original room ID
534 map_area.locations.push_back( 700 map_area.locations.push_back({.name = section_name,
535 {.name = panel.name, 701 .ap_location_name = location_name,
536 .ap_location_name = room_name + " - " + panel.name, 702 .ap_location_id = panel.ap_location_id,
537 .ap_location_id = panel.ap_location_id, 703 .room = panel.room,
538 .room = panel.room, 704 .panels = {panel.id},
539 .panels = {panel.id}, 705 .classification = classification,
540 .classification = classification, 706 .hunt = panel.hunt,
541 .hunt = panel.hunt}); 707 .single_panel = panel.id});
542 locations_by_name[map_area.locations.back().ap_location_name] = { 708 locations_by_name[location_name] = {area_id,
543 area_id, map_area.locations.size() - 1}; 709 map_area.locations.size() - 1};
544 } 710 }
545 711
546 for (int door_id : door_definition_order_) { 712 for (int door_id : door_definition_order_) {
@@ -591,14 +757,36 @@ struct GameData {
591 for (const Location &location : map_area.locations) { 757 for (const Location &location : map_area.locations) {
592 map_area.classification |= location.classification; 758 map_area.classification |= location.classification;
593 map_area.hunt |= location.hunt; 759 map_area.hunt |= location.hunt;
760 map_area.has_single_panel |= location.single_panel.has_value();
761 }
762 }
763
764 for (const Room &room : rooms_) {
765 std::string area_name = room.name;
766 if (fold_areas.count(room.name)) {
767 int fold_area_id = fold_areas[room.name];
768 area_name = map_areas_[fold_area_id].name;
769 }
770
771 if (!room.paintings.empty()) {
772 int area_id = AddOrGetArea(area_name);
773 MapArea &map_area = map_areas_[area_id];
774
775 for (int painting_id : room.paintings) {
776 PaintingExit &painting_obj = paintings_.at(painting_id);
777 painting_obj.map_area = area_id;
778 if (painting_obj.entrance) {
779 map_area.paintings.push_back(painting_id);
780 }
781 }
594 } 782 }
595 } 783 }
596 784
597 // Report errors. 785 // Report errors.
598 for (const std::string &area : malconfigured_areas_) { 786 for (const std::string &area : malconfigured_areas_) {
599 wxLogError("Area data not found for: %s", area); 787 TrackerLog(fmt::format("Area data not found for: {}", area));
600 } 788 }
601 789
602 // Read in subway items. 790 // Read in subway items.
603 YAML::Node subway_config = 791 YAML::Node subway_config =
604 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml")); 792 YAML::LoadFile(GetAbsolutePath("assets/subway.yaml"));
@@ -613,13 +801,10 @@ struct GameData {
613 subway_it["door"].as<std::string>()); 801 subway_it["door"].as<std::string>());
614 } 802 }
615 803
616 if (subway_it["paintings"]) { 804 if (subway_it["painting"]) {
617 for (const auto &painting_it : subway_it["paintings"]) { 805 std::string painting_id = subway_it["painting"].as<std::string>();
618 std::string painting_id = painting_it.as<std::string>(); 806 subway_item.painting = painting_id;
619 807 subway_item_by_painting_[painting_id] = subway_item.id;
620 subway_item.paintings.push_back(painting_id);
621 subway_item_by_painting_[painting_id] = subway_item.id;
622 }
623 } 808 }
624 809
625 if (subway_it["tags"]) { 810 if (subway_it["tags"]) {
@@ -628,6 +813,18 @@ struct GameData {
628 } 813 }
629 } 814 }
630 815
816 if (subway_it["entrances"]) {
817 for (const auto &entrance_it : subway_it["entrances"]) {
818 subway_item.entrances.push_back(entrance_it.as<std::string>());
819 }
820 }
821
822 if (subway_it["exits"]) {
823 for (const auto &exit_it : subway_it["exits"]) {
824 subway_item.exits.push_back(exit_it.as<std::string>());
825 }
826 }
827
631 if (subway_it["sunwarp"]) { 828 if (subway_it["sunwarp"]) {
632 SubwaySunwarp sunwarp; 829 SubwaySunwarp sunwarp;
633 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); 830 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
@@ -654,6 +851,10 @@ struct GameData {
654 subway_item.special = subway_it["special"].as<std::string>(); 851 subway_item.special = subway_it["special"].as<std::string>();
655 } 852 }
656 853
854 if (subway_it["tilted"]) {
855 subway_item.tilted = subway_it["tilted"].as<bool>();
856 }
857
657 subway_items_.push_back(subway_item); 858 subway_items_.push_back(subway_item);
658 } 859 }
659 860
@@ -667,7 +868,7 @@ struct GameData {
667 868
668 for (const auto &[tag, items] : subway_tags) { 869 for (const auto &[tag, items] : subway_tags) {
669 if (items.size() == 1) { 870 if (items.size() == 1) {
670 wxLogWarning("Singleton subway item tag: %s", tag); 871 TrackerLog(fmt::format("Singleton subway item tag: {}", tag));
671 } 872 }
672 } 873 }
673 } 874 }
@@ -687,7 +888,8 @@ struct GameData {
687 if (!door_by_id_.count(full_name)) { 888 if (!door_by_id_.count(full_name)) {
688 int door_id = doors_.size(); 889 int door_id = doors_.size();
689 door_by_id_[full_name] = doors_.size(); 890 door_by_id_[full_name] = doors_.size();
690 doors_.push_back({.id = door_id, .room = AddOrGetRoom(room), .name = door}); 891 doors_.push_back(
892 {.id = door_id, .room = AddOrGetRoom(room), .name = door});
691 } 893 }
692 894
693 return door_by_id_[full_name]; 895 return door_by_id_[full_name];
@@ -706,6 +908,18 @@ struct GameData {
706 return panel_by_id_[full_name]; 908 return panel_by_id_[full_name];
707 } 909 }
708 910
911 int AddOrGetPanelDoor(std::string room, std::string panel) {
912 std::string full_name = room + " - " + panel;
913
914 if (!panel_doors_by_id_.count(full_name)) {
915 int panel_door_id = panel_doors_.size();
916 panel_doors_by_id_[full_name] = panel_door_id;
917 panel_doors_.push_back({});
918 }
919
920 return panel_doors_by_id_[full_name];
921 }
922
709 int AddOrGetArea(std::string area) { 923 int AddOrGetArea(std::string area) {
710 if (!area_by_id_.count(area)) { 924 if (!area_by_id_.count(area)) {
711 if (loaded_area_data_) { 925 if (loaded_area_data_) {
@@ -719,6 +933,16 @@ struct GameData {
719 933
720 return area_by_id_[area]; 934 return area_by_id_[area];
721 } 935 }
936
937 int AddOrGetPainting(std::string internal_id) {
938 if (!painting_by_id_.count(internal_id)) {
939 int painting_id = paintings_.size();
940 painting_by_id_[internal_id] = painting_id;
941 paintings_.push_back({.id = painting_id, .internal_id = internal_id});
942 }
943
944 return painting_by_id_[internal_id];
945 }
722}; 946};
723 947
724GameData &GetState() { 948GameData &GetState() {
@@ -728,7 +952,12 @@ GameData &GetState() {
728 952
729} // namespace 953} // namespace
730 954
731bool SubwaySunwarp::operator<(const SubwaySunwarp& rhs) const { 955bool SubwayItem::HasWarps() const {
956 return !(this->tags.empty() && this->entrances.empty() &&
957 this->exits.empty());
958}
959
960bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
732 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); 961 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
733} 962}
734 963
@@ -746,6 +975,10 @@ const std::vector<Door> &GD_GetDoors() { return GetState().doors_; }
746 975
747const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } 976const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); }
748 977
978const PanelDoor &GD_GetPanelDoor(int panel_door_id) {
979 return GetState().panel_doors_.at(panel_door_id);
980}
981
749int GD_GetDoorByName(const std::string &name) { 982int GD_GetDoorByName(const std::string &name) {
750 return GetState().door_by_id_.at(name); 983 return GetState().door_by_id_.at(name);
751} 984}
@@ -754,8 +987,20 @@ const Panel &GD_GetPanel(int panel_id) {
754 return GetState().panels_.at(panel_id); 987 return GetState().panels_.at(panel_id);
755} 988}
756 989
757int GD_GetRoomForPainting(const std::string &painting_id) { 990int GD_GetPanelBySolveIndex(int solve_index) {
758 return GetState().room_by_painting_.at(painting_id); 991 return GetState().panel_by_solve_index_.at(solve_index);
992}
993
994const std::vector<PaintingExit> &GD_GetPaintings() {
995 return GetState().paintings_;
996}
997
998const PaintingExit &GD_GetPaintingExit(int painting_id) {
999 return GetState().paintings_.at(painting_id);
1000}
1001
1002int GD_GetPaintingByName(const std::string &name) {
1003 return GetState().painting_by_id_.at(name);
759} 1004}
760 1005
761const std::vector<int> &GD_GetAchievementPanels() { 1006const std::vector<int> &GD_GetAchievementPanels() {
@@ -782,15 +1027,48 @@ const SubwayItem &GD_GetSubwayItem(int id) {
782 return GetState().subway_items_.at(id); 1027 return GetState().subway_items_.at(id);
783} 1028}
784 1029
785int GD_GetSubwayItemForPainting(const std::string& painting_id) { 1030std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
786#ifndef NDEBUG 1031 if (GetState().subway_item_by_painting_.count(painting_id)) {
787 if (!GetState().subway_item_by_painting_.count(painting_id)) { 1032 return GetState().subway_item_by_painting_.at(painting_id);
788 wxLogError("No subway item for painting %s", painting_id);
789 } 1033 }
790#endif 1034 return std::nullopt;
791 return GetState().subway_item_by_painting_.at(painting_id);
792} 1035}
793 1036
794int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 1037int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
795 return GetState().subway_item_by_sunwarp_.at(sunwarp); 1038 return GetState().subway_item_by_sunwarp_.at(sunwarp);
796} 1039}
1040
1041std::string GD_GetItemName(int id) {
1042 auto it = GetState().item_by_ap_id_.find(id);
1043 if (it != GetState().item_by_ap_id_.end()) {
1044 return it->second;
1045 } else {
1046 return "Unknown";
1047 }
1048}
1049
1050LingoColor GetLingoColorForString(const std::string &str) {
1051 if (str == "black") {
1052 return LingoColor::kBlack;
1053 } else if (str == "red") {
1054 return LingoColor::kRed;
1055 } else if (str == "blue") {
1056 return LingoColor::kBlue;
1057 } else if (str == "yellow") {
1058 return LingoColor::kYellow;
1059 } else if (str == "orange") {
1060 return LingoColor::kOrange;
1061 } else if (str == "green") {
1062 return LingoColor::kGreen;
1063 } else if (str == "gray") {
1064 return LingoColor::kGray;
1065 } else if (str == "brown") {
1066 return LingoColor::kBrown;
1067 } else if (str == "purple") {
1068 return LingoColor::kPurple;
1069 } else {
1070 TrackerLog(fmt::format("Invalid color: {}", str));
1071
1072 return LingoColor::kNone;
1073 }
1074}
diff --git a/src/game_data.h b/src/game_data.h index 3afaec3..8d3db4b 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -22,6 +22,7 @@ enum class LingoColor {
22constexpr int kLOCATION_NORMAL = 1; 22constexpr int kLOCATION_NORMAL = 1;
23constexpr int kLOCATION_REDUCED = 2; 23constexpr int kLOCATION_REDUCED = 2;
24constexpr int kLOCATION_INSANITY = 4; 24constexpr int kLOCATION_INSANITY = 4;
25constexpr int kLOCATION_SMALL_SPHERE_ONE = 8;
25 26
26enum class EntranceType { 27enum class EntranceType {
27 kNormal, 28 kNormal,
@@ -30,6 +31,7 @@ enum class EntranceType {
30 kWarp, 31 kWarp,
31 kPilgrimage, 32 kPilgrimage,
32 kCrossroadsRoofAccess, 33 kCrossroadsRoofAccess,
34 kStaticPainting,
33}; 35};
34 36
35enum class DoorType { 37enum class DoorType {
@@ -42,6 +44,7 @@ struct Panel {
42 int id; 44 int id;
43 int room; 45 int room;
44 std::string name; 46 std::string name;
47 std::string nodepath;
45 std::vector<LingoColor> colors; 48 std::vector<LingoColor> colors;
46 std::vector<int> required_rooms; 49 std::vector<int> required_rooms;
47 std::vector<int> required_doors; 50 std::vector<int> required_doors;
@@ -50,9 +53,12 @@ struct Panel {
50 bool exclude_reduce = false; 53 bool exclude_reduce = false;
51 bool achievement = false; 54 bool achievement = false;
52 std::string achievement_name; 55 std::string achievement_name;
56 std::string location_name;
53 bool non_counting = false; 57 bool non_counting = false;
54 int ap_location_id = -1; 58 int ap_location_id = -1;
55 bool hunt = false; 59 bool hunt = false;
60 int panel_door = -1;
61 int solve_index = -1;
56}; 62};
57 63
58struct ProgressiveRequirement { 64struct ProgressiveRequirement {
@@ -80,21 +86,34 @@ struct Door {
80 DoorType type = DoorType::kNormal; 86 DoorType type = DoorType::kNormal;
81}; 87};
82 88
89struct PanelDoor {
90 int ap_item_id = -1;
91 int group_ap_item_id = -1;
92 std::vector<ProgressiveRequirement> progressives;
93 std::string item_name;
94};
95
83struct Exit { 96struct Exit {
97 int source_room;
84 int destination_room; 98 int destination_room;
85 std::optional<int> door; 99 std::optional<int> door;
86 EntranceType type = EntranceType::kNormal; 100 EntranceType type = EntranceType::kNormal;
87}; 101};
88 102
89struct PaintingExit { 103struct PaintingExit {
90 std::string id; 104 int id;
105 int room;
106 std::string internal_id;
107 std::string display_name;
91 std::optional<int> door; 108 std::optional<int> door;
109 bool entrance = false;
110 int map_area;
92}; 111};
93 112
94struct Room { 113struct Room {
95 std::string name; 114 std::string name;
96 std::vector<Exit> exits; 115 std::vector<Exit> exits;
97 std::vector<PaintingExit> paintings; 116 std::vector<int> paintings;
98 std::vector<int> sunwarps; 117 std::vector<int> sunwarps;
99 std::vector<int> panels; 118 std::vector<int> panels;
100}; 119};
@@ -107,16 +126,19 @@ struct Location {
107 std::vector<int> panels; 126 std::vector<int> panels;
108 int classification = 0; 127 int classification = 0;
109 bool hunt = false; 128 bool hunt = false;
129 std::optional<int> single_panel;
110}; 130};
111 131
112struct MapArea { 132struct MapArea {
113 int id; 133 int id;
114 std::string name; 134 std::string name;
115 std::vector<Location> locations; 135 std::vector<Location> locations;
136 std::vector<int> paintings;
116 int map_x; 137 int map_x;
117 int map_y; 138 int map_y;
118 int classification = 0; 139 int classification = 0;
119 bool hunt = false; 140 bool hunt = false;
141 bool has_single_panel = false;
120}; 142};
121 143
122enum class SubwaySunwarpType { 144enum class SubwaySunwarpType {
@@ -136,11 +158,16 @@ struct SubwayItem {
136 int id; 158 int id;
137 int x; 159 int x;
138 int y; 160 int y;
161 bool tilted = false;
139 std::optional<int> door; 162 std::optional<int> door;
140 std::vector<std::string> paintings; 163 std::optional<std::string> painting;
141 std::vector<std::string> tags; 164 std::vector<std::string> tags; // 2-way teleports
165 std::vector<std::string> entrances; // teleport entrances
166 std::vector<std::string> exits; // teleport exits
142 std::optional<SubwaySunwarp> sunwarp; 167 std::optional<SubwaySunwarp> sunwarp;
143 std::optional<std::string> special; 168 std::optional<std::string> special;
169
170 bool HasWarps() const;
144}; 171};
145 172
146const std::vector<MapArea>& GD_GetMapAreas(); 173const std::vector<MapArea>& GD_GetMapAreas();
@@ -151,14 +178,21 @@ const std::vector<Door>& GD_GetDoors();
151const Door& GD_GetDoor(int door_id); 178const Door& GD_GetDoor(int door_id);
152int GD_GetDoorByName(const std::string& name); 179int GD_GetDoorByName(const std::string& name);
153const Panel& GD_GetPanel(int panel_id); 180const Panel& GD_GetPanel(int panel_id);
154int GD_GetRoomForPainting(const std::string& painting_id); 181int GD_GetPanelBySolveIndex(int solve_index);
182const PanelDoor& GD_GetPanelDoor(int panel_door_id);
183const std::vector<PaintingExit>& GD_GetPaintings();
184const PaintingExit& GD_GetPaintingExit(int painting_id);
185int GD_GetPaintingByName(const std::string& name);
155const std::vector<int>& GD_GetAchievementPanels(); 186const std::vector<int>& GD_GetAchievementPanels();
156int GD_GetItemIdForColor(LingoColor color); 187int GD_GetItemIdForColor(LingoColor color);
157const std::vector<int>& GD_GetSunwarpDoors(); 188const std::vector<int>& GD_GetSunwarpDoors();
158int GD_GetRoomForSunwarp(int index); 189int GD_GetRoomForSunwarp(int index);
159const std::vector<SubwayItem>& GD_GetSubwayItems(); 190const std::vector<SubwayItem>& GD_GetSubwayItems();
160const SubwayItem& GD_GetSubwayItem(int id); 191const SubwayItem& GD_GetSubwayItem(int id);
161int GD_GetSubwayItemForPainting(const std::string& painting_id); 192std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
162int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 193int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
194std::string GD_GetItemName(int id);
195
196LingoColor GetLingoColorForString(const std::string& str);
163 197
164#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 198#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/global.cpp b/src/global.cpp index 1eb3f8d..63f4a19 100644 --- a/src/global.cpp +++ b/src/global.cpp
@@ -26,17 +26,19 @@ std::string GetAbsolutePath(std::string_view path) {
26 return (GetExecutableDirectory() / path).string(); 26 return (GetExecutableDirectory() / path).string();
27} 27}
28 28
29bool IsLocationWinCondition(const Location& location) { 29std::string GetWinCondition() {
30 switch (AP_GetVictoryCondition()) { 30 switch (AP_GetVictoryCondition()) {
31 case kTHE_END: 31 case kTHE_END:
32 return location.ap_location_name == 32 return "Orange Tower Seventh Floor - THE END";
33 "Orange Tower Seventh Floor - THE END";
34 case kTHE_MASTER: 33 case kTHE_MASTER:
35 return location.ap_location_name == 34 return "Orange Tower Seventh Floor - THE MASTER";
36 "Orange Tower Seventh Floor - THE MASTER";
37 case kLEVEL_2: 35 case kLEVEL_2:
38 return location.ap_location_name == "Second Room - LEVEL 2"; 36 return "Second Room - LEVEL 2";
39 case kPILGRIMAGE: 37 case kPILGRIMAGE:
40 return location.ap_location_name == "Pilgrim Antechamber - PILGRIM"; 38 return "Pilgrim Antechamber - PILGRIM";
41 } 39 }
42} 40}
41
42bool IsLocationWinCondition(const Location& location) {
43 return location.ap_location_name == GetWinCondition();
44}
diff --git a/src/global.h b/src/global.h index 31ebde3..bdfa7ae 100644 --- a/src/global.h +++ b/src/global.h
@@ -10,6 +10,8 @@ const std::filesystem::path& GetExecutableDirectory();
10 10
11std::string GetAbsolutePath(std::string_view path); 11std::string GetAbsolutePath(std::string_view path);
12 12
13std::string GetWinCondition();
14
13bool IsLocationWinCondition(const Location& location); 15bool IsLocationWinCondition(const Location& location);
14 16
15#endif /* end of include guard: GLOBAL_H_44945DBA */ 17#endif /* end of include guard: GLOBAL_H_44945DBA */
diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..87ba037 --- /dev/null +++ b/src/icons.cpp
@@ -0,0 +1,22 @@
1#include "icons.h"
2
3#include "global.h"
4
5const wxBitmap* IconCache::GetIcon(const std::string& filename, wxSize size) {
6 std::tuple<std::string, int, int> key = {filename, size.x, size.y};
7
8 if (!icons_.count(key)) {
9 icons_[key] =
10 wxBitmap(wxImage(GetAbsolutePath(filename).c_str(),
11 wxBITMAP_TYPE_PNG)
12 .Scale(size.x, size.y));
13 }
14
15 return &icons_.at(key);
16}
17
18static IconCache* ICON_CACHE_INSTANCE = nullptr;
19
20void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; }
21
22IconCache& GetTheIconCache() { return *ICON_CACHE_INSTANCE; }
diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 0000000..23dca2a --- /dev/null +++ b/src/icons.h
@@ -0,0 +1,25 @@
1#ifndef ICONS_H_B95159A6
2#define ICONS_H_B95159A6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <map>
11#include <string>
12#include <tuple>
13
14class IconCache {
15 public:
16 const wxBitmap* GetIcon(const std::string& filename, wxSize size);
17
18 private:
19 std::map<std::tuple<std::string, int, int>, wxBitmap> icons_;
20};
21
22void SetTheIconCache(IconCache* instance);
23IconCache& GetTheIconCache();
24
25#endif /* end of include guard: ICONS_H_B95159A6 */
diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp new file mode 100644 index 0000000..6763b7f --- /dev/null +++ b/src/ipc_dialog.cpp
@@ -0,0 +1,54 @@
1#include "ipc_dialog.h"
2
3#include "tracker_config.h"
4
5constexpr const char* kDefaultIpcAddress = "ws://127.0.0.1:41253";
6
7IpcDialog::IpcDialog() : wxDialog(nullptr, wxID_ANY, "Connect to game") {
8 std::string address_value;
9 if (GetTrackerConfig().ipc_address.empty()) {
10 address_value = kDefaultIpcAddress;
11 } else {
12 address_value = GetTrackerConfig().ipc_address;
13 }
14
15 address_box_ = new wxTextCtrl(this, -1, wxString::FromUTF8(address_value),
16 wxDefaultPosition, FromDIP(wxSize{300, -1}));
17
18 wxButton* reset_button = new wxButton(this, -1, "Use Default");
19 reset_button->Bind(wxEVT_BUTTON, &IpcDialog::OnResetClicked, this);
20
21 wxFlexGridSizer* form_sizer =
22 new wxFlexGridSizer(3, FromDIP(10), FromDIP(10));
23 form_sizer->Add(
24 new wxStaticText(this, -1, "Address:"),
25 wxSizerFlags().Align(wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT));
26 form_sizer->Add(address_box_, wxSizerFlags().Expand());
27 form_sizer->Add(reset_button);
28
29 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
30 wxStaticText* top_text = new wxStaticText(this, -1, "");
31 top_sizer->Add(top_text, wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder().Expand());
32 top_sizer->Add(form_sizer, wxSizerFlags().DoubleBorder().Expand());
33 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
34 wxSizerFlags().Border().Center());
35
36 SetSizer(top_sizer);
37 Layout();
38 Fit();
39
40 int width = top_text->GetClientSize().GetWidth();
41 top_text->SetLabel(
42 "This allows you to connect to a running Lingo game and track non-multiworld "
43 "state, such as the player's position and what panels are solved. Unless "
44 "you are doing something weird, the default value for the address is "
45 "probably correct.");
46 top_text->Wrap(width);
47
48 Fit();
49 Center();
50}
51
52void IpcDialog::OnResetClicked(wxCommandEvent& event) {
53 address_box_->SetValue(kDefaultIpcAddress);
54}
diff --git a/src/ipc_dialog.h b/src/ipc_dialog.h new file mode 100644 index 0000000..a8c4512 --- /dev/null +++ b/src/ipc_dialog.h
@@ -0,0 +1,24 @@
1#ifndef IPC_DIALOG_H_F4C5680C
2#define IPC_DIALOG_H_F4C5680C
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <string>
11
12class IpcDialog : public wxDialog {
13 public:
14 IpcDialog();
15
16 std::string GetIpcAddress() { return address_box_->GetValue().utf8_string(); }
17
18 private:
19 void OnResetClicked(wxCommandEvent& event);
20
21 wxTextCtrl* address_box_;
22};
23
24#endif /* end of include guard: IPC_DIALOG_H_F4C5680C */
diff --git a/src/ipc_state.cpp b/src/ipc_state.cpp new file mode 100644 index 0000000..6e2a440 --- /dev/null +++ b/src/ipc_state.cpp
@@ -0,0 +1,367 @@
1#include "ipc_state.h"
2
3#define _WEBSOCKETPP_CPP11_STRICT_
4
5#include <fmt/core.h>
6
7#include <chrono>
8#include <memory>
9#include <mutex>
10#include <nlohmann/json.hpp>
11#include <optional>
12#include <set>
13#include <string>
14#include <thread>
15#include <tuple>
16#include <wswrap.hpp>
17
18#include "ap_state.h"
19#include "logger.h"
20#include "tracker_frame.h"
21
22namespace {
23
24struct IPCState {
25 std::mutex state_mutex;
26 TrackerFrame* tracker_frame = nullptr;
27
28 // Protected state
29 bool initialized = false;
30 std::string address;
31 bool should_disconnect = false;
32
33 std::optional<std::string> status_message;
34
35 bool slot_matches = false;
36 std::string tracker_ap_server;
37 std::string tracker_ap_user;
38 std::string game_ap_server;
39 std::string game_ap_user;
40
41 std::optional<std::tuple<int, int>> player_position;
42
43 // Thread state
44 std::unique_ptr<wswrap::WS> ws;
45 bool connected = false;
46
47 void SetTrackerFrame(TrackerFrame* frame) { tracker_frame = frame; }
48
49 void Connect(std::string a) {
50 // This is the main concurrency concern, as it mutates protected state in an
51 // important way. Thread() is documented with how it interacts with this
52 // function.
53 std::lock_guard state_guard(state_mutex);
54
55 if (!initialized) {
56 std::thread([this]() { Thread(); }).detach();
57
58 initialized = true;
59 } else if (address != a) {
60 should_disconnect = true;
61 }
62
63 address = a;
64 }
65
66 std::optional<std::string> GetStatusMessage() {
67 std::lock_guard state_guard(state_mutex);
68
69 return status_message;
70 }
71
72 void SetTrackerSlot(std::string server, std::string user) {
73 // This function is called from the APState thread, not the main thread, and
74 // it mutates protected state. It only really competes with OnMessage(), when
75 // a "Connect" message is received. If this is called right before, and the
76 // tracker slot does not match the old game slot, it will initiate a
77 // disconnect, and then the OnMessage() handler will see should_disconnect
78 // and stop processing the "Connect" message. If this is called right after
79 // and the slot does not match, IPC will disconnect, which is tolerable.
80 std::lock_guard state_guard(state_mutex);
81
82 tracker_ap_server = std::move(server);
83 tracker_ap_user = std::move(user);
84
85 CheckIfSlotMatches();
86
87 if (!slot_matches) {
88 should_disconnect = true;
89 address.clear();
90 }
91 }
92
93 bool IsConnected() {
94 std::lock_guard state_guard(state_mutex);
95
96 return slot_matches;
97 }
98
99 std::optional<std::tuple<int, int>> GetPlayerPosition() {
100 std::lock_guard state_guard(state_mutex);
101
102 return player_position;
103 }
104
105 private:
106 void Thread() {
107 for (;;) {
108 // initialized is definitely true because it is set to true when the thread
109 // is created and only set to false within this block, when the thread is
110 // killed. Thus, a call to Connect would always at most set
111 // should_disconnect and address. If this happens before this block, it is
112 // as if we are starting from a new thread anyway because should_disconnect
113 // is immediately reset. If a call to Connect happens after this block,
114 // then a connection attempt will be made to the wrong address, but the
115 // thread will grab the mutex right after this and back out the wrong
116 // connection.
117 std::string ipc_address;
118 {
119 std::lock_guard state_guard(state_mutex);
120
121 SetStatusMessage("Disconnected from game.");
122
123 should_disconnect = false;
124
125 slot_matches = false;
126 game_ap_server.clear();
127 game_ap_user.clear();
128
129 player_position = std::nullopt;
130
131 if (address.empty()) {
132 initialized = false;
133 return;
134 }
135
136 ipc_address = address;
137
138 SetStatusMessage("Connecting to game...");
139 }
140
141 int backoff_amount = 0;
142
143 TrackerLog(fmt::format("Looking for game over IPC ({})...", ipc_address));
144
145 while (!connected) {
146 if (TryConnect(ipc_address)) {
147 int backoff_limit = (backoff_amount + 1) * 10;
148
149 for (int i = 0; i < backoff_limit && !connected; i++) {
150 // If Connect is called right before this block, we will see and
151 // handle should_disconnect. If it is called right after, we will do
152 // one bad poll, one sleep, and then grab the mutex again right
153 // after.
154 {
155 std::lock_guard state_guard(state_mutex);
156 if (should_disconnect) {
157 break;
158 }
159 }
160
161 ws->poll();
162
163 // Back off
164 std::this_thread::sleep_for(std::chrono::milliseconds(100));
165 }
166
167 backoff_amount++;
168 } else {
169 std::lock_guard state_guard(state_mutex);
170
171 if (!should_disconnect) {
172 should_disconnect = true;
173 address.clear();
174
175 SetStatusMessage("Disconnected from game.");
176 }
177
178 break;
179 }
180
181 // If Connect is called right before this block, we will see and handle
182 // should_disconnect. If it is called right after, and the connection
183 // was unsuccessful, we will grab the mutex after one bad connection
184 // attempt. If the connection was successful, we grab the mutex right
185 // after exiting the loop.
186 bool show_error = false;
187 {
188 std::lock_guard state_guard(state_mutex);
189
190 if (should_disconnect) {
191 break;
192 } else if (!connected) {
193 if (backoff_amount >= 10) {
194 should_disconnect = true;
195 address.clear();
196
197 SetStatusMessage("Disconnected from game.");
198
199 show_error = true;
200 } else {
201 TrackerLog(fmt::format("Retrying IPC in {} second(s)...",
202 backoff_amount + 1));
203 }
204 }
205 }
206
207 // We do this after giving up the mutex because otherwise we could
208 // deadlock with the main thread.
209 if (show_error) {
210 TrackerLog("Giving up on IPC.");
211
212 wxMessageBox("Connection to Lingo timed out.", "Connection failed",
213 wxOK | wxICON_ERROR);
214 break;
215 }
216 }
217
218 // Pretty much every lock guard in the thread is the same. We check for
219 // should_disconnect, and if it gets set directly after the block, we do
220 // minimal bad work before checking for it again.
221 {
222 std::lock_guard state_guard(state_mutex);
223 if (should_disconnect) {
224 ws.reset();
225 continue;
226 }
227 }
228
229 while (connected) {
230 ws->poll();
231
232 std::this_thread::sleep_for(std::chrono::milliseconds(100));
233
234 {
235 std::lock_guard state_guard(state_mutex);
236 if (should_disconnect) {
237 ws.reset();
238 break;
239 }
240 }
241 }
242 }
243 }
244
245 bool TryConnect(std::string ipc_address) {
246 try {
247 ws = std::make_unique<wswrap::WS>(
248 ipc_address, [this]() { OnConnect(); }, [this]() { OnClose(); },
249 [this](const std::string& s) { OnMessage(s); },
250 [this](const std::string& s) { OnError(s); });
251 return true;
252 } catch (const std::exception& ex) {
253 TrackerLog(fmt::format("Error connecting to Lingo: {}", ex.what()));
254 wxMessageBox(ex.what(), "Error connecting to Lingo", wxOK | wxICON_ERROR);
255 ws.reset();
256 return false;
257 }
258 }
259
260 void OnConnect() {
261 connected = true;
262
263 {
264 std::lock_guard state_guard(state_mutex);
265
266 slot_matches = false;
267 player_position = std::nullopt;
268 }
269 }
270
271 void OnClose() {
272 connected = false;
273
274 {
275 std::lock_guard state_guard(state_mutex);
276
277 slot_matches = false;
278 }
279 }
280
281 void OnMessage(const std::string& s) {
282 TrackerLog(s);
283
284 auto msg = nlohmann::json::parse(s);
285
286 if (msg["cmd"] == "Connect") {
287 std::lock_guard state_guard(state_mutex);
288 if (should_disconnect) {
289 return;
290 }
291
292 game_ap_server = msg["slot"]["server"];
293 game_ap_user = msg["slot"]["player"];
294
295 CheckIfSlotMatches();
296
297 if (!slot_matches) {
298 tracker_frame->ConnectToAp(game_ap_server, game_ap_user,
299 msg["slot"]["password"]);
300 }
301 } else if (msg["cmd"] == "UpdatePosition") {
302 std::lock_guard state_guard(state_mutex);
303
304 player_position =
305 std::make_tuple<int, int>(msg["position"]["x"], msg["position"]["z"]);
306
307 tracker_frame->UpdateIndicators(StateUpdate{.player_position = true});
308 }
309 }
310
311 void OnError(const std::string& s) {}
312
313 // Assumes mutex is locked.
314 void CheckIfSlotMatches() {
315 slot_matches = (tracker_ap_server == game_ap_server &&
316 tracker_ap_user == game_ap_user);
317
318 if (slot_matches) {
319 SetStatusMessage("Connected to game.");
320
321 Sync();
322 } else if (connected) {
323 SetStatusMessage("Local game doesn't match AP slot.");
324 }
325 }
326
327 // Assumes mutex is locked.
328 void SetStatusMessage(std::optional<std::string> msg) {
329 status_message = msg;
330
331 tracker_frame->UpdateStatusMessage();
332 }
333
334 void Sync() {
335 nlohmann::json msg;
336 msg["cmd"] = "Sync";
337
338 ws->send_text(msg.dump());
339 }
340};
341
342IPCState& GetState() {
343 static IPCState* instance = new IPCState();
344 return *instance;
345}
346
347} // namespace
348
349void IPC_SetTrackerFrame(TrackerFrame* tracker_frame) {
350 GetState().SetTrackerFrame(tracker_frame);
351}
352
353void IPC_Connect(std::string address) { GetState().Connect(address); }
354
355std::optional<std::string> IPC_GetStatusMessage() {
356 return GetState().GetStatusMessage();
357}
358
359void IPC_SetTrackerSlot(std::string server, std::string user) {
360 GetState().SetTrackerSlot(server, user);
361}
362
363bool IPC_IsConnected() { return GetState().IsConnected(); }
364
365std::optional<std::tuple<int, int>> IPC_GetPlayerPosition() {
366 return GetState().GetPlayerPosition();
367}
diff --git a/src/ipc_state.h b/src/ipc_state.h new file mode 100644 index 0000000..0e6fa51 --- /dev/null +++ b/src/ipc_state.h
@@ -0,0 +1,23 @@
1#ifndef IPC_STATE_H_6B3B0958
2#define IPC_STATE_H_6B3B0958
3
4#include <optional>
5#include <set>
6#include <string>
7#include <tuple>
8
9class TrackerFrame;
10
11void IPC_SetTrackerFrame(TrackerFrame* tracker_frame);
12
13void IPC_Connect(std::string address);
14
15std::optional<std::string> IPC_GetStatusMessage();
16
17void IPC_SetTrackerSlot(std::string server, std::string user);
18
19bool IPC_IsConnected();
20
21std::optional<std::tuple<int, int>> IPC_GetPlayerPosition();
22
23#endif /* end of include guard: IPC_STATE_H_6B3B0958 */
diff --git a/src/items_pane.cpp b/src/items_pane.cpp new file mode 100644 index 0000000..055eec0 --- /dev/null +++ b/src/items_pane.cpp
@@ -0,0 +1,145 @@
1#include "items_pane.h"
2
3#include <map>
4
5namespace {
6
7enum SortInstruction {
8 SI_NONE = 0,
9 SI_ASC = 1 << 0,
10 SI_DESC = 1 << 1,
11 SI_NAME = 1 << 2,
12 SI_AMOUNT = 1 << 3,
13 SI_ORDER = 1 << 4,
14};
15
16inline SortInstruction operator|(SortInstruction lhs, SortInstruction rhs) {
17 return static_cast<SortInstruction>(static_cast<int>(lhs) |
18 static_cast<int>(rhs));
19}
20
21template <typename T>
22int ItemCompare(const T& lhs, const T& rhs, bool ascending) {
23 if (lhs < rhs) {
24 return ascending ? -1 : 1;
25 } else if (lhs > rhs) {
26 return ascending ? 1 : -1;
27 } else {
28 return 0;
29 }
30}
31
32int wxCALLBACK RowCompare(wxIntPtr item1, wxIntPtr item2, wxIntPtr sortData) {
33 const ItemState& lhs = *reinterpret_cast<const ItemState*>(item1);
34 const ItemState& rhs = *reinterpret_cast<const ItemState*>(item2);
35 SortInstruction instruction = static_cast<SortInstruction>(sortData);
36
37 bool ascending = (instruction & SI_ASC) != 0;
38 if ((instruction & SI_NAME) != 0) {
39 return ItemCompare(lhs.name, rhs.name, ascending);
40 } else if ((instruction & SI_AMOUNT) != 0) {
41 return ItemCompare(lhs.amount, rhs.amount, ascending);
42 } else if ((instruction & SI_ORDER) != 0) {
43 return ItemCompare(lhs.index, rhs.index, ascending);
44 } else {
45 return 0;
46 }
47}
48
49} // namespace
50
51ItemsPane::ItemsPane(wxWindow* parent)
52 : wxListView(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
53 wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_HRULES) {
54 AppendColumn("Item", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
55 AppendColumn("Amount", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
56 AppendColumn("Order", wxLIST_FORMAT_LEFT, wxLIST_AUTOSIZE_USEHEADER);
57
58 Bind(wxEVT_LIST_COL_CLICK, &ItemsPane::OnColClick, this);
59 Bind(wxEVT_DPI_CHANGED, &ItemsPane::OnDPIChanged, this);
60}
61
62void ItemsPane::ResetIndicators() {
63 DeleteAllItems();
64 items_.clear();
65}
66
67void ItemsPane::UpdateIndicators(const std::vector<ItemState>& items) {
68 std::map<std::string, ItemState> items_by_name;
69
70 for (const ItemState& item : items) {
71 items_by_name[item.name] = item;
72 }
73
74 for (int i = 0; i < GetItemCount(); i++) {
75 std::string item_name = GetItemText(i).utf8_string();
76 auto it = items_by_name.find(item_name);
77
78 if (it != items_by_name.end()) {
79 SetItem(i, 1, std::to_string(it->second.amount));
80 SetItem(i, 2, std::to_string(it->second.index));
81
82 *reinterpret_cast<ItemState*>(GetItemData(i)) = it->second;
83
84 items_by_name.erase(item_name);
85 }
86 }
87
88 for (const auto& [name, item] : items_by_name) {
89 int i = InsertItem(GetItemCount(), name);
90 SetItem(i, 1, std::to_string(item.amount));
91 SetItem(i, 2, std::to_string(item.index));
92
93 auto item_ptr = std::make_unique<ItemState>(item);
94 SetItemPtrData(i, reinterpret_cast<wxUIntPtr>(item_ptr.get()));
95 items_.push_back(std::move(item_ptr));
96 }
97
98 SetColumnWidth(0, wxLIST_AUTOSIZE);
99 SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
100 SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER);
101
102 if (GetSortIndicator() != -1) {
103 DoSort(GetSortIndicator(), IsAscendingSortIndicator());
104 }
105}
106
107void ItemsPane::OnColClick(wxListEvent& event) {
108 int col = event.GetColumn();
109 if (col == -1) {
110 return;
111 }
112
113 bool ascending = GetUpdatedAscendingSortIndicator(col);
114
115 DoSort(col, ascending);
116}
117
118void ItemsPane::OnDPIChanged(wxDPIChangedEvent& event) {
119 SetColumnWidth(0, wxLIST_AUTOSIZE);
120 SetColumnWidth(1, wxLIST_AUTOSIZE_USEHEADER);
121 SetColumnWidth(2, wxLIST_AUTOSIZE_USEHEADER);
122
123 event.Skip();
124}
125
126void ItemsPane::DoSort(int col, bool ascending) {
127 SortInstruction instruction = SI_NONE;
128 if (ascending) {
129 instruction = instruction | SI_ASC;
130 } else {
131 instruction = instruction | SI_DESC;
132 }
133
134 if (col == 0) {
135 instruction = instruction | SI_NAME;
136 } else if (col == 1) {
137 instruction = instruction | SI_AMOUNT;
138 } else if (col == 2) {
139 instruction = instruction | SI_ORDER;
140 }
141
142 if (SortItems(RowCompare, instruction)) {
143 ShowSortIndicator(col, ascending);
144 }
145}
diff --git a/src/items_pane.h b/src/items_pane.h new file mode 100644 index 0000000..aa09c49 --- /dev/null +++ b/src/items_pane.h
@@ -0,0 +1,33 @@
1#ifndef ITEMS_PANE_H_EB637EE3
2#define ITEMS_PANE_H_EB637EE3
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <wx/listctrl.h>
11
12#include <memory>
13#include <vector>
14
15#include "ap_state.h"
16
17class ItemsPane : public wxListView {
18 public:
19 explicit ItemsPane(wxWindow* parent);
20
21 void ResetIndicators();
22 void UpdateIndicators(const std::vector<ItemState>& items);
23
24 private:
25 void OnColClick(wxListEvent& event);
26 void OnDPIChanged(wxDPIChangedEvent& event);
27
28 void DoSort(int col, bool ascending);
29
30 std::vector<std::unique_ptr<ItemState>> items_;
31};
32
33#endif /* end of include guard: ITEMS_PANE_H_EB637EE3 */
diff --git a/src/log_dialog.cpp b/src/log_dialog.cpp new file mode 100644 index 0000000..3f0a8ad --- /dev/null +++ b/src/log_dialog.cpp
@@ -0,0 +1,37 @@
1#include "log_dialog.h"
2
3#include "logger.h"
4
5wxDEFINE_EVENT(LOG_MESSAGE, wxCommandEvent);
6
7LogDialog::LogDialog(wxWindow* parent)
8 : wxDialog(parent, wxID_ANY, "Debug Log", wxDefaultPosition, wxDefaultSize,
9 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) {
10 SetSize(FromDIP(wxSize{512, 280}));
11
12 text_area_ =
13 new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize,
14 wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP);
15 text_area_->SetValue(TrackerReadPastLog());
16
17 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
18 top_sizer->Add(text_area_,
19 wxSizerFlags().DoubleBorder().Expand().Proportion(1));
20
21 SetSizer(top_sizer);
22
23 Bind(LOG_MESSAGE, &LogDialog::OnLogMessage, this);
24}
25
26void LogDialog::LogMessage(const std::string& message) {
27 wxCommandEvent* event = new wxCommandEvent(LOG_MESSAGE);
28 event->SetString(message);
29 QueueEvent(event);
30}
31
32void LogDialog::OnLogMessage(wxCommandEvent& event) {
33 if (!text_area_->IsEmpty()) {
34 text_area_->AppendText("\n");
35 }
36 text_area_->AppendText(event.GetString());
37}
diff --git a/src/log_dialog.h b/src/log_dialog.h new file mode 100644 index 0000000..c29251f --- /dev/null +++ b/src/log_dialog.h
@@ -0,0 +1,24 @@
1#ifndef LOG_DIALOG_H_EEFD45B6
2#define LOG_DIALOG_H_EEFD45B6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10wxDECLARE_EVENT(LOG_MESSAGE, wxCommandEvent);
11
12class LogDialog : public wxDialog {
13 public:
14 explicit LogDialog(wxWindow* parent);
15
16 void LogMessage(const std::string& message);
17
18 private:
19 void OnLogMessage(wxCommandEvent& event);
20
21 wxTextCtrl* text_area_;
22};
23
24#endif /* end of include guard: LOG_DIALOG_H_EEFD45B6 */
diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..8a08b58 --- /dev/null +++ b/src/logger.cpp
@@ -0,0 +1,64 @@
1#include "logger.h"
2
3#include <chrono>
4#include <fstream>
5#include <mutex>
6#include <sstream>
7
8#include "global.h"
9#include "log_dialog.h"
10
11namespace {
12
13class Logger {
14 public:
15 Logger() : logfile_(GetAbsolutePath("debug.log")) {}
16
17 void LogLine(const std::string& text) {
18 std::lock_guard guard(file_mutex_);
19 std::ostringstream line;
20 line << "[" << std::chrono::system_clock::now() << "] " << text;
21
22 logfile_ << line.str() << std::endl;
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;
43 }
44
45 private:
46 std::ofstream logfile_;
47 std::mutex file_mutex_;
48 LogDialog* log_dialog_ = nullptr;
49};
50
51Logger& GetLogger() {
52 static Logger* instance = new Logger();
53 return *instance;
54}
55
56} // namespace
57
58void TrackerLog(std::string text) { GetLogger().LogLine(text); }
59
60std::string TrackerReadPastLog() { return GetLogger().GetContents(); }
61
62void TrackerSetLogDialog(LogDialog* log_dialog) {
63 GetLogger().SetLogDialog(log_dialog);
64}
diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..f669790 --- /dev/null +++ b/src/logger.h
@@ -0,0 +1,14 @@
1#ifndef LOGGER_H_9BDD07EA
2#define LOGGER_H_9BDD07EA
3
4#include <string>
5
6class LogDialog;
7
8void TrackerLog(std::string message);
9
10std::string TrackerReadPastLog();
11
12void TrackerSetLogDialog(LogDialog* log_dialog);
13
14#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index 5b036ea..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -4,31 +4,29 @@
4#include <wx/wx.h> 4#include <wx/wx.h>
5#endif 5#endif
6 6
7#include <fstream>
8
9#include "global.h" 7#include "global.h"
10#include "tracker_config.h" 8#include "tracker_config.h"
11#include "tracker_frame.h" 9#include "tracker_frame.h"
12 10
13static std::ofstream* logfile;
14
15class TrackerApp : public wxApp { 11class TrackerApp : public wxApp {
16 public: 12 public:
17 virtual bool OnInit() { 13 virtual bool OnInit() override {
18 logfile = new std::ofstream(GetAbsolutePath("debug.log"));
19 wxLog::SetActiveTarget(new wxLogStream(logfile));
20
21#ifndef NDEBUG
22 wxLog::SetVerbose(true);
23 wxLog::SetActiveTarget(new wxLogWindow(nullptr, "Debug Log"));
24#endif
25
26 GetTrackerConfig().Load(); 14 GetTrackerConfig().Load();
27 15
28 TrackerFrame *frame = new TrackerFrame(); 16 TrackerFrame *frame = new TrackerFrame();
29 frame->Show(true); 17 frame->Show(true);
30 return true; 18 return true;
31 } 19 }
20
21 bool OnExceptionInMainLoop() override {
22 try {
23 throw;
24 } catch (const std::exception& ex) {
25 wxLogError(ex.what());
26 }
27
28 return false;
29 }
32}; 30};
33 31
34wxIMPLEMENT_APP(TrackerApp); 32wxIMPLEMENT_APP(TrackerApp);
diff --git a/src/network_set.cpp b/src/network_set.cpp index 6d2a098..45911e3 100644 --- a/src/network_set.cpp +++ b/src/network_set.cpp
@@ -4,9 +4,8 @@ void NetworkSet::Clear() {
4 network_by_item_.clear(); 4 network_by_item_.clear();
5} 5}
6 6
7void NetworkSet::AddLink(int id1, int id2) { 7void NetworkSet::AddLink(int id1, int id2, bool two_way) {
8 if (id2 > id1) { 8 if (two_way && id2 > id1) {
9 // Make sure id1 < id2
10 std::swap(id1, id2); 9 std::swap(id1, id2);
11 } 10 }
12 11
@@ -17,14 +16,37 @@ void NetworkSet::AddLink(int id1, int id2) {
17 network_by_item_[id2] = {}; 16 network_by_item_[id2] = {};
18 } 17 }
19 18
20 network_by_item_[id1].insert({id1, id2}); 19 NetworkNode node = {id1, id2, two_way};
21 network_by_item_[id2].insert({id1, id2}); 20
21 network_by_item_[id1].insert(node);
22 network_by_item_[id2].insert(node);
23}
24
25void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2, bool two_way) {
26 if (two_way && id2 > id1) {
27 std::swap(id1, id2);
28 }
29
30 if (!network_by_item_.count(network_id)) {
31 network_by_item_[network_id] = {};
32 }
33
34 NetworkNode node = {id1, id2, two_way};
35
36 network_by_item_[network_id].insert(node);
22} 37}
23 38
24bool NetworkSet::IsItemInNetwork(int id) const { 39bool NetworkSet::IsItemInNetwork(int id) const {
25 return network_by_item_.count(id); 40 return network_by_item_.count(id);
26} 41}
27 42
28const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const { 43const std::set<NetworkNode>& NetworkSet::GetNetworkGraph(int id) const {
29 return network_by_item_.at(id); 44 return network_by_item_.at(id);
30} 45}
46
47bool NetworkNode::operator<(const NetworkNode& rhs) const {
48 if (entry != rhs.entry) return entry < rhs.entry;
49 if (exit != rhs.exit) return exit < rhs.exit;
50 if (two_way != rhs.two_way) return two_way < rhs.two_way;
51 return false;
52}
diff --git a/src/network_set.h b/src/network_set.h index e6f0c07..0f72052 100644 --- a/src/network_set.h +++ b/src/network_set.h
@@ -7,19 +7,29 @@
7#include <utility> 7#include <utility>
8#include <vector> 8#include <vector>
9 9
10struct NetworkNode {
11 int entry;
12 int exit;
13 bool two_way;
14
15 bool operator<(const NetworkNode& rhs) const;
16};
17
10class NetworkSet { 18class NetworkSet {
11 public: 19 public:
12 void Clear(); 20 void Clear();
13 21
14 void AddLink(int id1, int id2); 22 void AddLink(int id1, int id2, bool two_way);
23
24 void AddLinkToNetwork(int network_id, int id1, int id2, bool two_way);
15 25
16 bool IsItemInNetwork(int id) const; 26 bool IsItemInNetwork(int id) const;
17 27
18 const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const; 28 const std::set<NetworkNode>& GetNetworkGraph(int id) const;
19 29
20 private: 30 private:
21 31
22 std::map<int, std::set<std::pair<int, int>>> network_by_item_; 32 std::map<int, std::set<NetworkNode>> network_by_item_;
23}; 33};
24 34
25#endif /* end of include guard: NETWORK_SET_H_3036B8E3 */ 35#endif /* end of include guard: NETWORK_SET_H_3036B8E3 */
diff --git a/src/options_pane.cpp b/src/options_pane.cpp new file mode 100644 index 0000000..844e145 --- /dev/null +++ b/src/options_pane.cpp
@@ -0,0 +1,71 @@
1#include "options_pane.h"
2
3#include "ap_state.h"
4
5namespace {
6
7const char* kDoorShuffleLabels[] = {"None", "Panels", "Doors"};
8const char* kLocationChecksLabels[] = {"Normal", "Reduced", "Insanity"};
9const char* kPanelShuffleLabels[] = {"None", "Rearrange"};
10const char* kVictoryConditionLabels[] = {"The End", "The Master", "Level 2",
11 "Pilgrimage"};
12const char* kSunwarpAccessLabels[] = {"Normal", "Disabled", "Unlock",
13 "Individual", "Progressive"};
14
15void AddRow(wxDataViewListCtrl* list, const std::string& text) {
16 wxVector<wxVariant> data;
17 data.push_back(wxVariant{text + ": "});
18 data.push_back(wxVariant{""});
19 list->AppendItem(data);
20}
21
22} // namespace
23
24OptionsPane::OptionsPane(wxWindow* parent)
25 : wxDataViewListCtrl(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
26 wxDV_ROW_LINES) {
27 AppendTextColumn("Name", wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE);
28 AppendTextColumn("Value");
29 AddRow(this, "Shuffle Doors");
30 AddRow(this, "Group Doors");
31 AddRow(this, "Location Checks");
32 AddRow(this, "Shuffle Colors");
33 AddRow(this, "Shuffle Panels");
34 AddRow(this, "Shuffle Paintings");
35 AddRow(this, "Victory Condition");
36 AddRow(this, "Early Color Hallways");
37 AddRow(this, "Shuffle Postgame");
38 AddRow(this, "Enable Pilgrimage");
39 AddRow(this, "Pilgrimage Roof Access");
40 AddRow(this, "Pilgrimage Paintings");
41 AddRow(this, "Sunwarp Access");
42 AddRow(this, "Shuffle Sunwarps");
43 AddRow(this, "Mastery Achievements");
44 AddRow(this, "Level 2 Requirement");
45}
46
47void OptionsPane::OnConnect() {
48 SetTextValue(kDoorShuffleLabels[static_cast<size_t>(AP_GetDoorShuffleMode())],
49 0, 1);
50 SetTextValue(AP_AreDoorsGrouped() ? "Yes" : "No", 1, 1);
51 SetTextValue(
52 kLocationChecksLabels[static_cast<size_t>(AP_GetLocationsChecks())], 2,
53 1);
54 SetTextValue(AP_IsColorShuffle() ? "Yes" : "No", 3, 1);
55 SetTextValue(
56 kPanelShuffleLabels[static_cast<size_t>(AP_GetPanelShuffleMode())], 4, 1);
57 SetTextValue(AP_IsPaintingShuffle() ? "Yes" : "No", 5, 1);
58 SetTextValue(
59 kVictoryConditionLabels[static_cast<size_t>(AP_GetVictoryCondition())], 6,
60 1);
61 SetTextValue(AP_HasEarlyColorHallways() ? "Yes" : "No", 7, 1);
62 SetTextValue(AP_IsPostgameShuffle() ? "Yes" : "No", 8, 1);
63 SetTextValue(AP_IsPilgrimageEnabled() ? "Yes" : "No", 9, 1);
64 SetTextValue(AP_DoesPilgrimageAllowRoofAccess() ? "Yes" : "No", 10, 1);
65 SetTextValue(AP_DoesPilgrimageAllowPaintings() ? "Yes" : "No", 11, 1);
66 SetTextValue(kSunwarpAccessLabels[static_cast<size_t>(AP_GetSunwarpAccess())],
67 12, 1);
68 SetTextValue(AP_IsSunwarpShuffle() ? "Yes" : "No", 13, 1);
69 SetTextValue(std::to_string(AP_GetMasteryRequirement()), 14, 1);
70 SetTextValue(std::to_string(AP_GetLevel2Requirement()), 15, 1);
71}
diff --git a/src/options_pane.h b/src/options_pane.h new file mode 100644 index 0000000..e9df9f0 --- /dev/null +++ b/src/options_pane.h
@@ -0,0 +1,19 @@
1#ifndef OPTIONS_PANE_H_026A0EC0
2#define OPTIONS_PANE_H_026A0EC0
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <wx/dataview.h>
11
12class OptionsPane : public wxDataViewListCtrl {
13 public:
14 explicit OptionsPane(wxWindow* parent);
15
16 void OnConnect();
17};
18
19#endif /* end of include guard: OPTIONS_PANE_H_026A0EC0 */
diff --git a/src/paintings_pane.cpp b/src/paintings_pane.cpp new file mode 100644 index 0000000..bf5d71b --- /dev/null +++ b/src/paintings_pane.cpp
@@ -0,0 +1,86 @@
1#include "paintings_pane.h"
2
3#include <fmt/core.h>
4#include <wx/dataview.h>
5
6#include <map>
7#include <set>
8
9#include "ap_state.h"
10#include "game_data.h"
11#include "tracker_state.h"
12
13namespace {
14
15std::string GetPaintingDisplayName(const std::string& id) {
16 const PaintingExit& painting = GD_GetPaintingExit(GD_GetPaintingByName(id));
17 const MapArea& map_area = GD_GetMapArea(painting.map_area);
18
19 return fmt::format("{} - {}", map_area.name, painting.display_name);
20}
21
22} // namespace
23
24PaintingsPane::PaintingsPane(wxWindow* parent) : wxPanel(parent, wxID_ANY) {
25 wxStaticText* label = new wxStaticText(
26 this, wxID_ANY, "Shuffled paintings grouped by destination:");
27 tree_ctrl_ = new wxDataViewTreeCtrl(this, wxID_ANY);
28
29 reveal_btn_ = new wxButton(this, wxID_ANY, "Reveal shuffled paintings");
30 reveal_btn_->Disable();
31
32 wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
33 top_sizer->Add(label, wxSizerFlags().Border());
34 top_sizer->Add(tree_ctrl_, wxSizerFlags().Expand().Proportion(1));
35 top_sizer->Add(reveal_btn_, wxSizerFlags().Border().Expand());
36
37 SetSizerAndFit(top_sizer);
38
39 tree_ctrl_->Bind(wxEVT_DATAVIEW_ITEM_START_EDITING,
40 &PaintingsPane::OnStartEditingCell, this);
41 reveal_btn_->Bind(wxEVT_BUTTON, &PaintingsPane::OnClickRevealPaintings, this);
42}
43
44void PaintingsPane::ResetIndicators() {
45 tree_ctrl_->DeleteAllItems();
46 reveal_btn_->Enable(AP_IsPaintingShuffle());
47}
48
49void PaintingsPane::UpdateIndicators(const std::vector<std::string>&) {
50 // TODO: Optimize this by using the paintings delta.
51
52 tree_ctrl_->DeleteAllItems();
53
54 std::map<std::string, std::set<std::string>> grouped_paintings;
55
56 for (const auto& [from, to] : AP_GetPaintingMapping()) {
57 if (IsPaintingReachable(GD_GetPaintingByName(from)) &&
58 AP_IsPaintingChecked(from)) {
59 grouped_paintings[GetPaintingDisplayName(to)].insert(
60 GetPaintingDisplayName(from));
61 }
62 }
63
64 for (const auto& [to, froms] : grouped_paintings) {
65 wxDataViewItem tree_branch =
66 tree_ctrl_->AppendContainer(wxDataViewItem(0), to);
67
68 for (const std::string& from : froms) {
69 tree_ctrl_->AppendItem(tree_branch, from);
70 }
71 }
72}
73
74void PaintingsPane::OnClickRevealPaintings(wxCommandEvent& event) {
75 if (wxMessageBox("Clicking yes will reveal the mapping between all shuffled "
76 "paintings. This is usually considered a spoiler, and is "
77 "likely not allowed during competitions. This action is not "
78 "reversible. Are you sure you want to proceed?",
79 "Warning", wxYES_NO | wxICON_WARNING) == wxNO) {
80 return;
81 }
82
83 AP_RevealPaintings();
84}
85
86void PaintingsPane::OnStartEditingCell(wxDataViewEvent& event) { event.Veto(); }
diff --git a/src/paintings_pane.h b/src/paintings_pane.h new file mode 100644 index 0000000..1d14510 --- /dev/null +++ b/src/paintings_pane.h
@@ -0,0 +1,28 @@
1#ifndef PAINTINGS_PANE_H_815370D2
2#define PAINTINGS_PANE_H_815370D2
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class wxDataViewEvent;
11class wxDataViewTreeCtrl;
12
13class PaintingsPane : public wxPanel {
14 public:
15 explicit PaintingsPane(wxWindow* parent);
16
17 void ResetIndicators();
18 void UpdateIndicators(const std::vector<std::string>& paintings);
19
20 private:
21 void OnClickRevealPaintings(wxCommandEvent& event);
22 void OnStartEditingCell(wxDataViewEvent& event);
23
24 wxDataViewTreeCtrl* tree_ctrl_;
25 wxButton* reveal_btn_;
26};
27
28#endif /* end of include guard: PAINTINGS_PANE_H_815370D2 */
diff --git a/src/report_popup.cpp b/src/report_popup.cpp new file mode 100644 index 0000000..703e87f --- /dev/null +++ b/src/report_popup.cpp
@@ -0,0 +1,131 @@
1#include "report_popup.h"
2
3#include <wx/dcbuffer.h>
4
5#include <map>
6#include <string>
7
8#include "global.h"
9#include "icons.h"
10#include "tracker_state.h"
11
12ReportPopup::ReportPopup(wxWindow* parent)
13 : wxScrolledCanvas(parent, wxID_ANY) {
14 SetBackgroundStyle(wxBG_STYLE_PAINT);
15
16 LoadIcons();
17
18 // TODO: This is slow on high-DPI screens.
19 SetScrollRate(5, 5);
20
21 SetBackgroundColour(*wxBLACK);
22 Hide();
23
24 Bind(wxEVT_PAINT, &ReportPopup::OnPaint, this);
25 Bind(wxEVT_DPI_CHANGED, &ReportPopup::OnDPIChanged, this);
26}
27
28void ReportPopup::SetDoorId(int door_id) {
29 door_id_ = door_id;
30
31 ResetIndicators();
32}
33
34void ReportPopup::Reset() {
35 door_id_ = -1;
36}
37
38void ReportPopup::ResetIndicators() {
39 if (door_id_ == -1) {
40 return;
41 }
42
43 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
44 const std::map<std::string, bool>& report = GetDoorRequirements(door_id_);
45
46 wxMemoryDC mem_dc;
47 mem_dc.SetFont(the_font);
48
49 int acc_height = FromDIP(10);
50 int col_width = 0;
51
52 for (const auto& [text, obtained] : report) {
53 wxSize item_extent = mem_dc.GetTextExtent(text);
54 int item_height =
55 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
56 acc_height += item_height;
57
58 if (item_extent.GetWidth() > col_width) {
59 col_width = item_extent.GetWidth();
60 }
61 }
62
63 int item_width = col_width + FromDIP(10 + 32);
64 full_width_ = item_width + FromDIP(20);
65 full_height_ = acc_height;
66
67 Fit();
68 SetVirtualSize(full_width_, full_height_);
69
70 UpdateIndicators();
71}
72
73void ReportPopup::UpdateIndicators() {
74 if (door_id_ == -1) {
75 return;
76 }
77
78 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
79 const std::map<std::string, bool>& report = GetDoorRequirements(door_id_);
80
81 rendered_ = wxBitmap(full_width_, full_height_);
82
83 wxMemoryDC mem_dc;
84 mem_dc.SelectObject(rendered_);
85 mem_dc.SetPen(*wxTRANSPARENT_PEN);
86 mem_dc.SetBrush(*wxBLACK_BRUSH);
87 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
88
89 mem_dc.SetFont(the_font);
90
91 int cur_height = FromDIP(10);
92
93 for (const auto& [text, obtained] : report) {
94 const wxBitmap* eye_ptr = obtained ? checked_eye_ : unchecked_eye_;
95
96 mem_dc.DrawBitmap(*eye_ptr, wxPoint{FromDIP(10), cur_height});
97
98 mem_dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
99 wxSize item_extent = mem_dc.GetTextExtent(text);
100 mem_dc.DrawText(
101 text, wxPoint{FromDIP(10 + 32 + 10),
102 cur_height +
103 (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
104
105 cur_height += FromDIP(10 + 32);
106 }
107}
108
109void ReportPopup::OnPaint(wxPaintEvent& event) {
110 if (door_id_ != -1) {
111 wxBufferedPaintDC dc(this);
112 PrepareDC(dc);
113 dc.DrawBitmap(rendered_, 0, 0);
114 }
115
116 event.Skip();
117}
118
119void ReportPopup::OnDPIChanged(wxDPIChangedEvent& event) {
120 LoadIcons();
121 ResetIndicators();
122
123 event.Skip();
124}
125
126void ReportPopup::LoadIcons() {
127 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
128 FromDIP(wxSize{32, 32}));
129 checked_eye_ =
130 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
131}
diff --git a/src/report_popup.h b/src/report_popup.h new file mode 100644 index 0000000..bbb0bef --- /dev/null +++ b/src/report_popup.h
@@ -0,0 +1,38 @@
1#ifndef REPORT_POPUP_H_E065BED4
2#define REPORT_POPUP_H_E065BED4
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10class ReportPopup : public wxScrolledCanvas {
11 public:
12 explicit ReportPopup(wxWindow* parent);
13
14 void SetDoorId(int door_id);
15
16 void Reset();
17
18 void ResetIndicators();
19 void UpdateIndicators();
20
21 private:
22 void OnPaint(wxPaintEvent& event);
23 void OnDPIChanged(wxDPIChangedEvent& event);
24
25 void LoadIcons();
26
27 int door_id_ = -1;
28
29 const wxBitmap* unchecked_eye_;
30 const wxBitmap* checked_eye_;
31
32 int full_width_ = 0;
33 int full_height_ = 0;
34
35 wxBitmap rendered_;
36};
37
38#endif /* end of include guard: REPORT_POPUP_H_E065BED4 */
diff --git a/src/settings_dialog.cpp b/src/settings_dialog.cpp index 0321b5a..95df577 100644 --- a/src/settings_dialog.cpp +++ b/src/settings_dialog.cpp
@@ -3,30 +3,43 @@
3#include "tracker_config.h" 3#include "tracker_config.h"
4 4
5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") { 5SettingsDialog::SettingsDialog() : wxDialog(nullptr, wxID_ANY, "Settings") {
6 should_check_for_updates_box_ = new wxCheckBox( 6 wxStaticBoxSizer* main_box =
7 this, wxID_ANY, "Check for updates when the tracker opens"); 7 new wxStaticBoxSizer(wxVERTICAL, this, "General settings");
8
9 should_check_for_updates_box_ =
10 new wxCheckBox(main_box->GetStaticBox(), wxID_ANY,
11 "Check for updates when the tracker opens");
8 hybrid_areas_box_ = new wxCheckBox( 12 hybrid_areas_box_ = new wxCheckBox(
9 this, wxID_ANY, 13 main_box->GetStaticBox(), wxID_ANY,
10 "Use two colors to show that an area has partial availability"); 14 "Use two colors to show that an area has partial availability");
11 show_hunt_panels_box_ = new wxCheckBox(this, wxID_ANY, "Show hunt panels"); 15 track_position_box_ = new wxCheckBox(main_box->GetStaticBox(), wxID_ANY,
16 "Track player position");
12 17
13 should_check_for_updates_box_->SetValue( 18 should_check_for_updates_box_->SetValue(
14 GetTrackerConfig().should_check_for_updates); 19 GetTrackerConfig().should_check_for_updates);
15 hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas); 20 hybrid_areas_box_->SetValue(GetTrackerConfig().hybrid_areas);
16 show_hunt_panels_box_->SetValue(GetTrackerConfig().show_hunt_panels); 21 track_position_box_->SetValue(GetTrackerConfig().track_position);
22
23 main_box->Add(should_check_for_updates_box_, wxSizerFlags().Border());
24 main_box->AddSpacer(2);
25 main_box->Add(hybrid_areas_box_, wxSizerFlags().Border());
26 main_box->AddSpacer(2);
27 main_box->Add(track_position_box_, wxSizerFlags().Border());
28
29 const wxString visible_panels_choices[] = {"Only show locations",
30 "Show locations and hunt panels",
31 "Show all panels"};
32 visible_panels_box_ =
33 new wxRadioBox(this, wxID_ANY, "Visible panels", wxDefaultPosition,
34 wxDefaultSize, 3, visible_panels_choices, 1);
35 visible_panels_box_->SetSelection(
36 static_cast<int>(GetTrackerConfig().visible_panels));
17 37
18 wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL); 38 wxBoxSizer* form_sizer = new wxBoxSizer(wxVERTICAL);
19 39 form_sizer->Add(main_box, wxSizerFlags().Border().Expand());
20 form_sizer->Add(should_check_for_updates_box_, wxSizerFlags().HorzBorder()); 40 form_sizer->Add(visible_panels_box_, wxSizerFlags().Border().Expand());
21 form_sizer->AddSpacer(2); 41 form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
22 42 wxSizerFlags().Center().Border());
23 form_sizer->Add(hybrid_areas_box_, wxSizerFlags().HorzBorder());
24 form_sizer->AddSpacer(2);
25
26 form_sizer->Add(show_hunt_panels_box_, wxSizerFlags().HorzBorder());
27 form_sizer->AddSpacer(2);
28
29 form_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Center());
30 43
31 SetSizerAndFit(form_sizer); 44 SetSizerAndFit(form_sizer);
32 45
diff --git a/src/settings_dialog.h b/src/settings_dialog.h index d7c1ed3..c4dacfa 100644 --- a/src/settings_dialog.h +++ b/src/settings_dialog.h
@@ -7,6 +7,10 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <wx/radiobox.h>
11
12#include "tracker_config.h"
13
10class SettingsDialog : public wxDialog { 14class SettingsDialog : public wxDialog {
11 public: 15 public:
12 SettingsDialog(); 16 SettingsDialog();
@@ -15,12 +19,17 @@ class SettingsDialog : public wxDialog {
15 return should_check_for_updates_box_->GetValue(); 19 return should_check_for_updates_box_->GetValue();
16 } 20 }
17 bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); } 21 bool GetHybridAreas() const { return hybrid_areas_box_->GetValue(); }
18 bool GetShowHuntPanels() const { return show_hunt_panels_box_->GetValue(); } 22 TrackerConfig::VisiblePanels GetVisiblePanels() const {
23 return static_cast<TrackerConfig::VisiblePanels>(
24 visible_panels_box_->GetSelection());
25 }
26 bool GetTrackPosition() const { return track_position_box_->GetValue(); }
19 27
20 private: 28 private:
21 wxCheckBox* should_check_for_updates_box_; 29 wxCheckBox* should_check_for_updates_box_;
22 wxCheckBox* hybrid_areas_box_; 30 wxCheckBox* hybrid_areas_box_;
23 wxCheckBox* show_hunt_panels_box_; 31 wxRadioBox* visible_panels_box_;
32 wxCheckBox* track_position_box_;
24}; 33};
25 34
26#endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */ 35#endif /* end of include guard: SETTINGS_DIALOG_H_D8635719 */
diff --git a/src/subway_map.cpp b/src/subway_map.cpp index 6070fd5..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -1,22 +1,36 @@
1#include "subway_map.h" 1#include "subway_map.h"
2 2
3#include <fmt/core.h>
3#include <wx/dcbuffer.h> 4#include <wx/dcbuffer.h>
5#include <wx/dcgraph.h>
4 6
5#include <sstream> 7#include <sstream>
6 8
7#include "ap_state.h" 9#include "ap_state.h"
8#include "game_data.h" 10#include "game_data.h"
9#include "global.h" 11#include "global.h"
12#include "report_popup.h"
10#include "tracker_state.h" 13#include "tracker_state.h"
11 14
12constexpr int AREA_ACTUAL_SIZE = 21; 15constexpr int AREA_ACTUAL_SIZE = 21;
13constexpr int OWL_ACTUAL_SIZE = 32; 16constexpr int OWL_ACTUAL_SIZE = 32;
17constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11
18constexpr int PAINTING_EXIT_RADIUS = 6;
14 19
15enum class ItemDrawType { 20enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit };
16 kNone, 21
17 kBox, 22namespace {
18 kOwl 23
19}; 24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
25 if (subway_item.painting) {
26 return {subway_item.x, subway_item.y};
27 } else {
28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
29 subway_item.y + AREA_ACTUAL_SIZE / 2};
30 }
31}
32
33} // namespace
20 34
21SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) { 35SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
22 SetBackgroundStyle(wxBG_STYLE_PAINT); 36 SetBackgroundStyle(wxBG_STYLE_PAINT);
@@ -42,28 +56,109 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
42 56
43 Redraw(); 57 Redraw();
44 58
59 scroll_timer_ = new wxTimer(this);
60
45 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this); 61 Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
46 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this); 62 Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
63 Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
64 Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
67
68 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15}));
69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
70
71 help_button_ = new wxButton(this, wxID_ANY, "Help");
72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
47} 76}
48 77
49void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
50 networks_.Clear(); 79 networks_.Clear();
51 80
52 std::map<std::string, std::vector<int>> tagged; 81 std::map<std::string, std::vector<int>> tagged;
82 std::map<std::string, std::vector<int>> entrances;
83 std::map<std::string, std::vector<int>> exits;
53 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
54 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 85 if (AP_HasEarlyColorHallways() &&
86 subway_item.special == "early_color_hallways") {
87 entrances["early_ch"].push_back(subway_item.id);
88 }
89
90 if (AP_IsPaintingShuffle() && subway_item.painting) {
55 continue; 91 continue;
56 } 92 }
57 93
58 for (const std::string &tag : subway_item.tags) { 94 for (const std::string &tag : subway_item.tags) {
59 tagged[tag].push_back(subway_item.id); 95 tagged[tag].push_back(subway_item.id);
60 } 96 }
97 for (const std::string &tag : subway_item.entrances) {
98 entrances[tag].push_back(subway_item.id);
99 }
100 for (const std::string &tag : subway_item.exits) {
101 exits[tag].push_back(subway_item.id);
102 }
61 103
62 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { 104 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) {
63 std::ostringstream tag; 105 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots);
64 tag << "sunwarp" << subway_item.sunwarp->dots; 106 switch (subway_item.sunwarp->type) {
107 case SubwaySunwarpType::kEnter:
108 entrances[tag].push_back(subway_item.id);
109 break;
110 case SubwaySunwarpType::kExit:
111 exits[tag].push_back(subway_item.id);
112 break;
113 default:
114 break;
115 }
116 }
65 117
66 tagged[tag.str()].push_back(subway_item.id); 118 if (!AP_IsPilgrimageEnabled()) {
119 if (subway_item.special == "sun_painting") {
120 entrances["sun_painting"].push_back(subway_item.id);
121 } else if (subway_item.special == "sun_painting_exit") {
122 exits["sun_painting"].push_back(subway_item.id);
123 }
124 }
125 }
126
127 if (AP_IsSunwarpShuffle()) {
128 sunwarp_mapping_ = AP_GetSunwarpMapping();
129
130 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal};
131 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp);
132
133 for (const auto &[index, mapping] : sunwarp_mapping_) {
134 std::string tag = fmt::format("sunwarp{}", mapping.dots);
135
136 SubwaySunwarp fromWarp;
137 if (index < 6) {
138 fromWarp.dots = index + 1;
139 fromWarp.type = SubwaySunwarpType::kEnter;
140 } else {
141 fromWarp.dots = index - 5;
142 fromWarp.type = SubwaySunwarpType::kExit;
143 }
144
145 SubwaySunwarp toWarp;
146 if (mapping.exit_index < 6) {
147 toWarp.dots = mapping.exit_index + 1;
148 toWarp.type = SubwaySunwarpType::kEnter;
149 } else {
150 toWarp.dots = mapping.exit_index - 5;
151 toWarp.type = SubwaySunwarpType::kExit;
152 }
153
154 entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
155 exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
156
157 networks_.AddLinkToNetwork(
158 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp),
159 mapping.dots == 6 ? final_sunwarp_item
160 : GD_GetSubwayItemForSunwarp(toWarp),
161 false);
67 } 162 }
68 } 163 }
69 164
@@ -73,115 +168,243 @@ void SubwayMap::OnConnect() {
73 tag_it1++) { 168 tag_it1++) {
74 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end(); 169 for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
75 tag_it2++) { 170 tag_it2++) {
76 networks_.AddLink(*tag_it1, *tag_it2); 171 // two links because tags are bi-directional
172 networks_.AddLink(*tag_it1, *tag_it2, true);
173 }
174 }
175 }
176
177 for (const auto &[tag, items] : entrances) {
178 if (!exits.contains(tag)) continue;
179 for (auto exit : exits[tag]) {
180 for (auto entrance : items) {
181 networks_.AddLink(entrance, exit, false);
77 } 182 }
78 } 183 }
79 } 184 }
80 185
81 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
82} 189}
83 190
84void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
192 if (AP_IsSunwarpShuffle()) {
193 sunwarp_mapping_ = AP_GetSunwarpMapping();
194 }
195
85 if (AP_IsPaintingShuffle()) { 196 if (AP_IsPaintingShuffle()) {
86 for (const std::string &painting_id : AP_GetCheckedPaintings()) { 197 std::map<std::string, std::string> painting_mapping =
198 AP_GetPaintingMapping();
199 std::set<std::string> remote_checked_paintings = AP_GetCheckedPaintings();
200
201 for (const std::string &painting_id : remote_checked_paintings) {
87 if (!checked_paintings_.count(painting_id)) { 202 if (!checked_paintings_.count(painting_id)) {
88 checked_paintings_.insert(painting_id); 203 checked_paintings_.insert(painting_id);
89 204
90 if (AP_GetPaintingMapping().count(painting_id)) { 205 if (painting_mapping.count(painting_id)) {
91 networks_.AddLink(GD_GetSubwayItemForPainting(painting_id), 206 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
92 GD_GetSubwayItemForPainting( 207 std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id));
93 AP_GetPaintingMapping().at(painting_id))); 208
209 if (from_id && to_id) {
210 networks_.AddLink(*from_id, *to_id, false);
211 }
94 } 212 }
95 } 213 }
96 } 214 }
97 } 215 }
98 216
217 report_popup_->UpdateIndicators();
218
99 Redraw(); 219 Redraw();
100} 220}
101 221
102void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, 222void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
103 SubwaySunwarp to_sunwarp) { 223 SubwaySunwarp to_sunwarp) {
104 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), 224 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
105 GD_GetSubwayItemForSunwarp(to_sunwarp)); 225 GD_GetSubwayItemForSunwarp(to_sunwarp), false);
226}
227
228void SubwayMap::Zoom(bool in) {
229 wxPoint focus_point;
230
231 if (mouse_position_) {
232 focus_point = *mouse_position_;
233 } else {
234 focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
235 }
236
237 if (in) {
238 if (zoom_ < 3.0) {
239 SetZoom(zoom_ + 0.25, focus_point);
240 }
241 } else {
242 if (zoom_ > 1.0) {
243 SetZoom(zoom_ - 0.25, focus_point);
244 }
245 }
106} 246}
107 247
108void SubwayMap::OnPaint(wxPaintEvent &event) { 248void SubwayMap::OnPaint(wxPaintEvent &event) {
109 if (GetSize() != rendered_.GetSize()) { 249 if (GetSize() != rendered_.GetSize()) {
110 Redraw(); 250 wxSize panel_size = GetSize();
111 } 251 wxSize image_size = map_image_.GetSize();
252
253 render_x_ = 0;
254 render_y_ = 0;
255 render_width_ = panel_size.GetWidth();
256 render_height_ = panel_size.GetHeight();
257
258 if (image_size.GetWidth() * panel_size.GetHeight() >
259 panel_size.GetWidth() * image_size.GetHeight()) {
260 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
261 image_size.GetWidth();
262 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
263 } else {
264 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
265 image_size.GetHeight();
266 render_x_ = (panel_size.GetWidth() - render_width_) / 2;
267 }
112 268
113 wxBufferedPaintDC dc(this); 269 SetZoomPos({zoom_x_, zoom_y_});
114 dc.DrawBitmap(rendered_, 0, 0);
115 270
116 if (hovered_item_ && networks_.IsItemInNetwork(*hovered_item_)) { 271 SetUpHelpButton();
117 dc.SetBrush(*wxTRANSPARENT_BRUSH);
118 272
119 for (const auto &[item_id1, item_id2] : 273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
120 networks_.GetNetworkGraph(*hovered_item_)) { 274 wxDefaultCoord, wxSIZE_AUTO);
121 const SubwayItem &item1 = GD_GetSubwayItem(item_id1); 275 }
122 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
123 276
124 int item1_x = (item1.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; 277 wxBufferedPaintDC dc(this);
125 int item1_y = (item1.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; 278 dc.SetBackground(*wxWHITE_BRUSH);
279 dc.Clear();
280
281 {
282 wxMemoryDC rendered_dc;
283 rendered_dc.SelectObject(rendered_);
284
285 int dst_x;
286 int dst_y;
287 int dst_w;
288 int dst_h;
289 int src_x;
290 int src_y;
291 int src_w;
292 int src_h;
293
294 int zoomed_width = render_width_ * zoom_;
295 int zoomed_height = render_height_ * zoom_;
296
297 if (zoomed_width <= GetSize().GetWidth()) {
298 dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
299 dst_w = zoomed_width;
300 src_x = 0;
301 src_w = map_image_.GetWidth();
302 } else {
303 dst_x = 0;
304 dst_w = GetSize().GetWidth();
305 src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
306 src_w =
307 GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
308 }
126 309
127 int item2_x = (item2.x + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_x_; 310 if (zoomed_height <= GetSize().GetHeight()) {
128 int item2_y = (item2.y + AREA_ACTUAL_SIZE / 2) * render_width_ / map_image_.GetWidth() + render_y_; 311 dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
312 dst_h = zoomed_height;
313 src_y = 0;
314 src_h = map_image_.GetHeight();
315 } else {
316 dst_y = 0;
317 dst_h = GetSize().GetHeight();
318 src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
319 src_h =
320 GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
321 }
129 322
130 int left = std::min(item1_x, item2_x); 323 wxGCDC gcdc(dc);
131 int top = std::min(item1_y, item2_y); 324 gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
132 int right = std::max(item1_x, item2_x); 325 gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
133 int bottom = std::max(item1_y, item2_y); 326 src_w, src_h);
327 }
134 328
135 int halfwidth = right - left; 329 if (hovered_item_) {
136 int halfheight = bottom - top; 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
332
333 for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) {
334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
336
337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
339
340 int left = std::min(item1_pos.x, item2_pos.x);
341 int top = std::min(item1_pos.y, item2_pos.y);
342 int right = std::max(item1_pos.x, item2_pos.x);
343 int bottom = std::max(item1_pos.y, item2_pos.y);
344
345 int halfwidth = right - left;
346 int halfheight = bottom - top;
347
348 if (halfwidth < 4 || halfheight < 4) {
349 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
350 dc.DrawLine(item1_pos, item2_pos);
351 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
352 dc.DrawLine(item1_pos, item2_pos);
353 if (!node.two_way) {
354 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
355 dc.SetBrush(*wxCYAN_BRUSH);
356 dc.DrawCircle(item2_pos, 4);
357 dc.SetBrush(*wxTRANSPARENT_BRUSH);
358 }
359 } else {
360 int ellipse_x;
361 int ellipse_y;
362 double start;
363 double end;
137 364
138 if (halfwidth < 4 || halfheight < 4) { 365 if (item1_pos.x > item2_pos.x) {
139 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4)); 366 ellipse_y = top;
140 dc.DrawLine(item1_x, item1_y, item2_x, item2_y);
141 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
142 dc.DrawLine(item1_x, item1_y, item2_x, item2_y);
143 } else {
144 int ellipse_x;
145 int ellipse_y;
146 double start;
147 double end;
148 367
149 if (item1_x > item2_x) { 368 if (item1_pos.y > item2_pos.y) {
150 ellipse_y = top; 369 ellipse_x = left - halfwidth;
151 370
152 if (item1_y > item2_y) { 371 start = 0;
153 ellipse_x = left - halfwidth; 372 end = 90;
373 } else {
374 ellipse_x = left;
154 375
155 start = 0; 376 start = 90;
156 end = 90; 377 end = 180;
378 }
157 } else { 379 } else {
158 ellipse_x = left; 380 ellipse_y = top - halfheight;
159 381
160 start = 90; 382 if (item1_pos.y > item2_pos.y) {
161 end = 180; 383 ellipse_x = left - halfwidth;
162 }
163 } else {
164 ellipse_y = top - halfheight;
165 384
166 if (item1_y > item2_y) { 385 start = 270;
167 ellipse_x = left - halfwidth; 386 end = 360;
387 } else {
388 ellipse_x = left;
168 389
169 start = 270; 390 start = 180;
170 end = 360; 391 end = 270;
171 } else { 392 }
172 ellipse_x = left; 393 }
173 394
174 start = 180; 395 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
175 end = 270; 396 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
397 halfheight * 2, start, end);
398 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
399 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
400 halfheight * 2, start, end);
401 if (!node.two_way) {
402 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
403 dc.SetBrush(*wxCYAN_BRUSH);
404 dc.DrawCircle(item2_pos, 4);
405 dc.SetBrush(*wxTRANSPARENT_BRUSH);
176 } 406 }
177 } 407 }
178
179 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
180 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2,
181 start, end);
182 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
183 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, halfheight * 2,
184 start, end);
185 } 408 }
186 } 409 }
187 } 410 }
@@ -190,137 +413,426 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
190} 413}
191 414
192void SubwayMap::OnMouseMove(wxMouseEvent &event) { 415void SubwayMap::OnMouseMove(wxMouseEvent &event) {
193 int mouse_x = std::clamp( 416 wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());
194 (event.GetX() - render_x_) * map_image_.GetWidth() / render_width_,
195 0, map_image_.GetWidth() - 1);
196 int mouse_y = std::clamp(
197 (event.GetY() - render_y_) * map_image_.GetWidth() / render_width_,
198 0, map_image_.GetHeight() - 1);
199 417
200 std::vector<int> hovered = tree_->query( 418 std::vector<int> hovered = tree_->query(
201 {static_cast<float>(mouse_x), static_cast<float>(mouse_y), 2, 2}); 419 {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
202 std::optional<int> new_hovered_item;
203 if (!hovered.empty()) { 420 if (!hovered.empty()) {
204 new_hovered_item = hovered[0]; 421 actual_hover_ = hovered[0];
422 } else {
423 actual_hover_ = std::nullopt;
205 } 424 }
206 425
207 if (new_hovered_item != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
208 hovered_item_ = new_hovered_item; 427 EvaluateHover();
428 }
209 429
210 Refresh(); 430 if (scroll_mode_) {
431 EvaluateScroll(event.GetPosition());
211 } 432 }
212 433
434 mouse_position_ = event.GetPosition();
435
213 event.Skip(); 436 event.Skip();
214} 437}
215 438
216void SubwayMap::Redraw() { 439void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
217 wxSize panel_size = GetSize(); 440 double new_zoom = zoom_;
218 wxSize image_size = map_image_.GetSize(); 441 if (event.GetWheelRotation() > 0) {
219 442 new_zoom = std::min(3.0, zoom_ + 0.25);
220 render_x_ = 0;
221 render_y_ = 0;
222 render_width_ = panel_size.GetWidth();
223 render_height_ = panel_size.GetHeight();
224
225 if (image_size.GetWidth() * panel_size.GetHeight() >
226 panel_size.GetWidth() * image_size.GetHeight()) {
227 render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
228 image_size.GetWidth();
229 render_y_ = (panel_size.GetHeight() - render_height_) / 2;
230 } else { 443 } else {
231 render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) / 444 new_zoom = std::max(1.0, zoom_ - 0.25);
232 image_size.GetHeight(); 445 }
233 render_x_ = (panel_size.GetWidth() - render_width_) / 2; 446
447 if (zoom_ != new_zoom) {
448 SetZoom(new_zoom, event.GetPosition());
449 }
450
451 event.Skip();
452}
453
454void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
455 SetScrollSpeed(0, 0);
456 mouse_position_ = std::nullopt;
457}
458
459void SubwayMap::OnMouseClick(wxMouseEvent &event) {
460 bool finished = false;
461
462 if (actual_hover_) {
463 const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
464 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
465
466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
467 networks_.IsItemInNetwork(*hovered_item_)) {
468 if (actual_hover_ != hovered_item_) {
469 EvaluateHover();
470
471 if (!hovered_item_) {
472 sticky_hover_ = false;
473 }
474 } else {
475 sticky_hover_ = !sticky_hover_;
476 }
477
478 finished = true;
479 }
480 }
481
482 if (!finished) {
483 if (scroll_mode_) {
484 scroll_mode_ = false;
485
486 SetScrollSpeed(0, 0);
487
488 SetCursor(wxCURSOR_ARROW);
489 } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
490 event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
491 event.GetPosition().y < GetSize().GetHeight() / 6 ||
492 event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
493 scroll_mode_ = true;
494
495 EvaluateScroll(event.GetPosition());
496
497 SetCursor(wxCURSOR_CROSS);
498 } else {
499 sticky_hover_ = false;
500 }
501 }
502}
503
504void SubwayMap::OnTimer(wxTimerEvent &event) {
505 SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
506 Refresh();
507}
508
509void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
510 double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();
511
512 if (new_zoom != zoom_) {
513 SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
234 } 514 }
515}
235 516
236 rendered_ = wxBitmap( 517void SubwayMap::OnClickHelp(wxCommandEvent &event) {
237 map_image_ 518 wxMessageBox(
238 .Scale(render_width_, render_height_, wxIMAGE_QUALITY_BILINEAR) 519 "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
239 .Size(panel_size, {render_x_, render_y_}, 255, 255, 255)); 520 "corner.\nClick on a side of the screen to start panning. It will follow "
521 "your mouse. Click again to stop.\nHover over a door to see the "
522 "requirements to open it.\nHover over a warp or active painting to see "
523 "what it is connected to.\nFor one-way connections, there will be a "
524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
525 "that the painting is locked by a door.\nA blue circle means painting "
526 "shuffle is enabled and the painting has not been checked yet.\nA black "
527 "circle means the painting is not a warp.\nA green circle means that the "
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.",
530 "Subway Map Help");
531}
532
533void SubwayMap::Redraw() {
534 rendered_ = wxBitmap(map_image_);
240 535
241 wxMemoryDC dc; 536 wxMemoryDC dc;
242 dc.SelectObject(rendered_); 537 dc.SelectObject(rendered_);
243 538
539 wxGCDC gcdc(dc);
540
541 std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping();
542
244 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
245 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
246 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
247 std::optional<wxColour> shade_color; 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
547
548 if (AP_HasEarlyColorHallways() &&
549 subway_item.special == "early_color_hallways") {
550 draw_type = ItemDrawType::kOwl;
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);
248 559
249 if (subway_item.door) {
250 draw_type = ItemDrawType::kBox; 560 draw_type = ItemDrawType::kBox;
251 561 if (AP_HasItemSafe(color_item_id)) {
252 if (IsDoorOpen(*subway_item.door)) { 562 brush_color = wxGREEN_BRUSH;
253 if (!subway_item.paintings.empty()) { 563 } else {
254 draw_type = ItemDrawType::kOwl; 564 brush_color = wxRED_BRUSH;
255 } else { 565 }
566 } else if (subway_item.special == "sun_painting") {
567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
569 if (IsDoorOpen(*subway_item.door)) {
256 brush_color = wxGREEN_BRUSH; 570 brush_color = wxGREEN_BRUSH;
571 } else {
572 brush_color = wxRED_BRUSH;
257 } 573 }
574 }
575 } else if (subway_item.sunwarp &&
576 subway_item.sunwarp->type == SubwaySunwarpType::kFinal &&
577 AP_IsPilgrimageEnabled()) {
578 draw_type = ItemDrawType::kBox;
579
580 if (IsPilgrimageDoable()) {
581 brush_color = wxGREEN_BRUSH;
258 } else { 582 } else {
259 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
260 } 584 }
261 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
262 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
263 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
264 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
265 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
266 590 if (!checked_paintings_.count(*subway_item.painting)) {
267 for (const std::string &painting_id : subway_item.paintings) { 591 draw_type = ItemDrawType::kOwl;
268 if (checked_paintings_.count(painting_id)) { 592 brush_color = wxBLUE_BRUSH;
269 has_checked_painting = true; 593 } else if (painting_mapping.count(*subway_item.painting)) {
270 594 draw_type = ItemDrawType::kOwl;
271 if (AP_GetPaintingMapping().count(painting_id)) { 595 brush_color = wxGREEN_BRUSH;
272 has_mapped_painting = true; 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
273 } 597 draw_type = ItemDrawType::kOwlExit;
274 } else { 598 brush_color = wxGREEN_BRUSH;
275 has_unchecked_painting = true;
276 }
277 } 599 }
278 600 } else if (subway_item.HasWarps()) {
279 if (has_unchecked_painting || has_mapped_painting) { 601 brush_color = wxGREEN_BRUSH;
602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
280 draw_type = ItemDrawType::kOwl; 605 draw_type = ItemDrawType::kOwl;
281
282 if (has_unchecked_painting) {
283 if (has_checked_painting) {
284 shade_color = wxColour(255, 255, 0, 100);
285 } else {
286 shade_color = wxColour(100, 100, 100, 100);
287 }
288 }
289 } 606 }
290 } else if (!subway_item.tags.empty()) { 607 }
291 draw_type = ItemDrawType::kOwl; 608 } else if (subway_door) {
609 draw_type = ItemDrawType::kBox;
610
611 if (IsDoorOpen(*subway_door)) {
612 brush_color = wxGREEN_BRUSH;
613 } else {
614 brush_color = wxRED_BRUSH;
292 } 615 }
293 } 616 }
294 617
295 int real_area_x = 618 wxPoint real_area_pos = {subway_item.x, subway_item.y};
296 render_x_ + subway_item.x * render_width_ / image_size.GetWidth();
297 int real_area_y =
298 render_y_ + subway_item.y * render_width_ / image_size.GetWidth();
299 619
300 int real_area_size = 620 int real_area_size =
301 render_width_ * 621 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);
302 (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE) /
303 image_size.GetWidth();
304 if (real_area_size == 0) {
305 real_area_size = 1;
306 }
307 622
308 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
309 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
310 dc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
311 dc.DrawRectangle({real_area_x, real_area_y}, 626
312 {real_area_size, real_area_size}); 627 if (subway_item.tilted) {
313 } else if (draw_type == ItemDrawType::kOwl) { 628 constexpr int AREA_TILTED_SIDE =
314 wxBitmap owl_bitmap = wxBitmap( 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
315 owl_image_.Scale(real_area_size, real_area_size, 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
316 wxIMAGE_QUALITY_BILINEAR)); 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
317 dc.DrawBitmap(owl_bitmap, {real_area_x, real_area_y}); 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
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 }
318 } 653 }
319 } 654 }
320} 655}
321 656
322quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int& id) const { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
660 help_button_->SetPosition({
661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
662 15,
663 });
664}
665
666void SubwayMap::EvaluateScroll(wxPoint pos) {
667 int scroll_x;
668 int scroll_y;
669 if (pos.x < GetSize().GetWidth() / 9) {
670 scroll_x = 20;
671 } else if (pos.x < GetSize().GetWidth() / 6) {
672 scroll_x = 5;
673 } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
674 scroll_x = -20;
675 } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
676 scroll_x = -5;
677 } else {
678 scroll_x = 0;
679 }
680 if (pos.y < GetSize().GetHeight() / 9) {
681 scroll_y = 20;
682 } else if (pos.y < GetSize().GetHeight() / 6) {
683 scroll_y = 5;
684 } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
685 scroll_y = -20;
686 } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
687 scroll_y = -5;
688 } else {
689 scroll_y = 0;
690 }
691
692 SetScrollSpeed(scroll_x, scroll_y);
693}
694
695void SubwayMap::EvaluateHover() {
696 hovered_item_ = actual_hover_;
697
698 if (hovered_item_) {
699 // Note that these requirements are duplicated on OnMouseClick so that it
700 // knows when an item has a hover effect.
701 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
702 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
703
704 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
705 report_popup_->SetDoorId(*subway_door);
706
707 wxPoint popupPos =
708 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
709 subway_item.y + AREA_ACTUAL_SIZE / 2});
710
711 report_popup_->SetClientSize(
712 report_popup_->GetVirtualSize().GetWidth(),
713 std::min(GetSize().GetHeight(),
714 report_popup_->GetVirtualSize().GetHeight()));
715
716 if (popupPos.x + report_popup_->GetSize().GetWidth() >
717 GetSize().GetWidth()) {
718 popupPos.x = GetSize().GetWidth() - report_popup_->GetSize().GetWidth();
719 }
720 if (popupPos.y + report_popup_->GetSize().GetHeight() >
721 GetSize().GetHeight()) {
722 popupPos.y =
723 GetSize().GetHeight() - report_popup_->GetSize().GetHeight();
724 }
725 report_popup_->SetPosition(popupPos);
726
727 report_popup_->Show();
728 } else {
729 report_popup_->Reset();
730 report_popup_->Hide();
731 }
732 } else {
733 report_popup_->Reset();
734 report_popup_->Hide();
735 }
736
737 Refresh();
738}
739
740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
742 map_image_.GetSize().GetWidth() +
743 zoom_x_),
744 static_cast<int>(pos.y * render_width_ * zoom_ /
745 map_image_.GetSize().GetWidth() +
746 zoom_y_)};
747}
748
749wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
750 return {static_cast<int>(pos.x * render_width_ * zoom_ /
751 map_image_.GetSize().GetWidth()),
752 static_cast<int>(pos.y * render_width_ * zoom_ /
753 map_image_.GetSize().GetWidth())};
754}
755
756wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
757 return {
758 std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
759 render_width_ / zoom_),
760 0, map_image_.GetWidth() - 1),
761 std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
762 render_width_ / zoom_),
763 0, map_image_.GetHeight() - 1)};
764}
765
766void SubwayMap::SetZoomPos(wxPoint pos) {
767 if (render_width_ * zoom_ <= GetSize().GetWidth()) {
768 zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
769 } else {
770 zoom_x_ = std::clamp(
771 pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
772 0);
773 }
774 if (render_height_ * zoom_ <= GetSize().GetHeight()) {
775 zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
776 } else {
777 zoom_y_ = std::clamp(
778 pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
779 0);
780 }
781}
782
783void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
784 bool should_timer = (scroll_x != 0 || scroll_y != 0);
785 if (should_timer != scroll_timer_->IsRunning()) {
786 if (should_timer) {
787 scroll_timer_->Start(1000 / 60);
788 } else {
789 scroll_timer_->Stop();
790 }
791 }
792
793 scroll_x_ = scroll_x;
794 scroll_y_ = scroll_y;
795}
796
797void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
798 wxPoint map_pos = RenderPosToMapPos(static_point);
799 zoom_ = zoom;
800
801 wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
802 SetZoomPos(-(virtual_pos - static_point));
803
804 Refresh();
805
806 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
807}
808
809std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) {
810 if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
811 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
812 int sunwarp_index = subway_item.sunwarp->dots - 1;
813 if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) {
814 sunwarp_index += 6;
815 }
816
817 for (const auto &[start_index, mapping] : sunwarp_mapping_) {
818 if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
819 return GD_GetSunwarpDoors().at(mapping.dots - 1);
820 }
821 }
822 }
823
824 return subway_item.door;
825}
826
827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
323 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
324 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
325 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 }
326} 838}
diff --git a/src/subway_map.h b/src/subway_map.h index e5f0bf6..b04c2fd 100644 --- a/src/subway_map.h +++ b/src/subway_map.h
@@ -15,9 +15,12 @@
15 15
16#include <quadtree/Quadtree.h> 16#include <quadtree/Quadtree.h>
17 17
18#include "ap_state.h"
18#include "game_data.h" 19#include "game_data.h"
19#include "network_set.h" 20#include "network_set.h"
20 21
22class ReportPopup;
23
21class SubwayMap : public wxPanel { 24class SubwayMap : public wxPanel {
22 public: 25 public:
23 SubwayMap(wxWindow *parent); 26 SubwayMap(wxWindow *parent);
@@ -25,12 +28,33 @@ class SubwayMap : public wxPanel {
25 void OnConnect(); 28 void OnConnect();
26 void UpdateIndicators(); 29 void UpdateIndicators();
27 void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp); 30 void UpdateSunwarp(SubwaySunwarp from_sunwarp, SubwaySunwarp to_sunwarp);
31 void Zoom(bool in);
28 32
29 private: 33 private:
30 void OnPaint(wxPaintEvent &event); 34 void OnPaint(wxPaintEvent &event);
31 void OnMouseMove(wxMouseEvent &event); 35 void OnMouseMove(wxMouseEvent &event);
36 void OnMouseScroll(wxMouseEvent &event);
37 void OnMouseLeave(wxMouseEvent &event);
38 void OnMouseClick(wxMouseEvent &event);
39 void OnTimer(wxTimerEvent &event);
40 void OnZoomSlide(wxCommandEvent &event);
41 void OnClickHelp(wxCommandEvent &event);
32 42
33 void Redraw(); 43 void Redraw();
44 void SetUpHelpButton();
45
46 wxPoint MapPosToRenderPos(wxPoint pos) const;
47 wxPoint MapPosToVirtualPos(wxPoint pos) const;
48 wxPoint RenderPosToMapPos(wxPoint pos) const;
49
50 void EvaluateScroll(wxPoint pos);
51 void EvaluateHover();
52
53 void SetZoomPos(wxPoint pos);
54 void SetScrollSpeed(int scroll_x, int scroll_y);
55 void SetZoom(double zoom, wxPoint static_point);
56
57 std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item);
34 58
35 wxImage map_image_; 59 wxImage map_image_;
36 wxImage owl_image_; 60 wxImage owl_image_;
@@ -38,8 +62,23 @@ class SubwayMap : public wxPanel {
38 wxBitmap rendered_; 62 wxBitmap rendered_;
39 int render_x_ = 0; 63 int render_x_ = 0;
40 int render_y_ = 0; 64 int render_y_ = 0;
41 int render_width_ = 0; 65 int render_width_ = 1;
42 int render_height_ = 0; 66 int render_height_ = 1;
67
68 double zoom_ = 1.0;
69 int zoom_x_ = 0; // in render space
70 int zoom_y_ = 0;
71
72 bool scroll_mode_ = false;
73 wxTimer* scroll_timer_;
74 int scroll_x_ = 0;
75 int scroll_y_ = 0;
76
77 wxSlider *zoom_slider_;
78
79 wxButton *help_button_;
80
81 std::optional<wxPoint> mouse_position_;
43 82
44 struct GetItemBox { 83 struct GetItemBox {
45 quadtree::Box<float> operator()(const int &id) const; 84 quadtree::Box<float> operator()(const int &id) const;
@@ -47,9 +86,16 @@ class SubwayMap : public wxPanel {
47 86
48 std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_; 87 std::unique_ptr<quadtree::Quadtree<int, GetItemBox>> tree_;
49 std::optional<int> hovered_item_; 88 std::optional<int> hovered_item_;
89 std::optional<int> actual_hover_;
90 bool sticky_hover_ = false;
91
92 ReportPopup *report_popup_;
50 93
51 NetworkSet networks_; 94 NetworkSet networks_;
52 std::set<std::string> checked_paintings_; 95 std::set<std::string> checked_paintings_;
96
97 // Cached from APState.
98 std::map<int, SunwarpMapping> sunwarp_mapping_;
53}; 99};
54 100
55#endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */ 101#endif /* end of include guard: SUBWAY_MAP_H_BD2D843E */
diff --git a/src/tracker_config.cpp b/src/tracker_config.cpp index 85164d5..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"]) {
@@ -27,6 +29,11 @@ void TrackerConfig::Load() {
27 }); 29 });
28 } 30 }
29 } 31 }
32
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>());
30 } catch (const std::exception&) { 37 } catch (const std::exception&) {
31 // It's fine if the file can't be loaded. 38 // It's fine if the file can't be loaded.
32 } 39 }
@@ -40,7 +47,6 @@ void TrackerConfig::Save() {
40 output["asked_to_check_for_updates"] = asked_to_check_for_updates; 47 output["asked_to_check_for_updates"] = asked_to_check_for_updates;
41 output["should_check_for_updates"] = should_check_for_updates; 48 output["should_check_for_updates"] = should_check_for_updates;
42 output["hybrid_areas"] = hybrid_areas; 49 output["hybrid_areas"] = hybrid_areas;
43 output["show_hunt_panels"] = show_hunt_panels;
44 50
45 output.remove("connection_history"); 51 output.remove("connection_history");
46 for (const ConnectionDetails& details : connection_history) { 52 for (const ConnectionDetails& details : connection_history) {
@@ -52,6 +58,10 @@ void TrackerConfig::Save() {
52 output["connection_history"].push_back(connection); 58 output["connection_history"].push_back(connection);
53 } 59 }
54 60
61 output["ipc_address"] = ipc_address;
62 output["track_position"] = track_position;
63 output["visible_panels"] = static_cast<int>(visible_panels);
64
55 std::ofstream filewriter(filename_); 65 std::ofstream filewriter(filename_);
56 filewriter << output; 66 filewriter << output;
57} 67}
diff --git a/src/tracker_config.h b/src/tracker_config.h index a1a6c1d..df4105d 100644 --- a/src/tracker_config.h +++ b/src/tracker_config.h
@@ -23,12 +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;
37 std::string ipc_address;
38 bool track_position = true;
39 VisiblePanels visible_panels = kLOCATIONS_ONLY;
32 40
33 private: 41 private:
34 std::string filename_; 42 std::string filename_;
diff --git a/src/tracker_frame.cpp b/src/tracker_frame.cpp index e944704..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -1,30 +1,64 @@
1#include "tracker_frame.h" 1#include "tracker_frame.h"
2 2
3#include <fmt/core.h>
3#include <wx/aboutdlg.h> 4#include <wx/aboutdlg.h>
4#include <wx/choicebk.h> 5#include <wx/choicebk.h>
6#include <wx/filedlg.h>
7#include <wx/notebook.h>
8#include <wx/splitter.h>
9#include <wx/stdpaths.h>
5#include <wx/webrequest.h> 10#include <wx/webrequest.h>
6 11
12#include <algorithm>
7#include <nlohmann/json.hpp> 13#include <nlohmann/json.hpp>
8#include <sstream> 14#include <sstream>
9 15
10#include "achievements_pane.h" 16#include "achievements_pane.h"
11#include "ap_state.h" 17#include "ap_state.h"
12#include "connection_dialog.h" 18#include "connection_dialog.h"
19#include "ipc_dialog.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"
13#include "settings_dialog.h" 26#include "settings_dialog.h"
14#include "subway_map.h" 27#include "subway_map.h"
15#include "tracker_config.h" 28#include "tracker_config.h"
16#include "tracker_panel.h" 29#include "tracker_panel.h"
17#include "version.h" 30#include "version.h"
18 31
32namespace {
33
34std::string GetStatusMessage() {
35 std::string msg = AP_GetStatusMessage();
36
37 std::optional<std::string> ipc_msg = IPC_GetStatusMessage();
38 if (ipc_msg) {
39 msg += " ";
40 msg += *ipc_msg;
41 }
42
43 return msg;
44}
45
46} // namespace
47
19enum TrackerFrameIds { 48enum TrackerFrameIds {
20 ID_CONNECT = 1, 49 ID_AP_CONNECT = 1,
21 ID_CHECK_FOR_UPDATES = 2, 50 ID_CHECK_FOR_UPDATES = 2,
22 ID_SETTINGS = 3 51 ID_SETTINGS = 3,
52 ID_ZOOM_IN = 4,
53 ID_ZOOM_OUT = 5,
54 ID_IPC_CONNECT = 7,
55 ID_LOG_DIALOG = 8,
23}; 56};
24 57
25wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); 58wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
26wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 59wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent);
27wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 60wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
61wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent);
28 62
29TrackerFrame::TrackerFrame() 63TrackerFrame::TrackerFrame()
30 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, 64 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
@@ -32,52 +66,87 @@ TrackerFrame::TrackerFrame()
32 ::wxInitAllImageHandlers(); 66 ::wxInitAllImageHandlers();
33 67
34 AP_SetTrackerFrame(this); 68 AP_SetTrackerFrame(this);
69 IPC_SetTrackerFrame(this);
70
71 SetTheIconCache(&icons_);
72
73 updater_ = std::make_unique<Updater>(this);
74 updater_->Cleanup();
35 75
36 wxMenu *menuFile = new wxMenu(); 76 wxMenu *menuFile = new wxMenu();
37 menuFile->Append(ID_CONNECT, "&Connect"); 77 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
78 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
38 menuFile->Append(ID_SETTINGS, "&Settings"); 79 menuFile->Append(ID_SETTINGS, "&Settings");
39 menuFile->Append(wxID_EXIT); 80 menuFile->Append(wxID_EXIT);
40 81
82 wxMenu *menuView = new wxMenu();
83 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\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");
87
88 zoom_in_menu_item_->Enable(false);
89 zoom_out_menu_item_->Enable(false);
90
41 wxMenu *menuHelp = new wxMenu(); 91 wxMenu *menuHelp = new wxMenu();
42 menuHelp->Append(wxID_ABOUT); 92 menuHelp->Append(wxID_ABOUT);
43 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates"); 93 menuHelp->Append(ID_CHECK_FOR_UPDATES, "Check for Updates");
44 94
45 wxMenuBar *menuBar = new wxMenuBar(); 95 wxMenuBar *menuBar = new wxMenuBar();
46 menuBar->Append(menuFile, "&File"); 96 menuBar->Append(menuFile, "&File");
97 menuBar->Append(menuView, "&View");
47 menuBar->Append(menuHelp, "&Help"); 98 menuBar->Append(menuHelp, "&Help");
48 99
49 SetMenuBar(menuBar); 100 SetMenuBar(menuBar);
50 101
51 CreateStatusBar(); 102 CreateStatusBar();
52 SetStatusText("Not connected to Archipelago.");
53 103
54 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); 104 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
55 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); 105 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
56 Bind(wxEVT_MENU, &TrackerFrame::OnConnect, this, ID_CONNECT); 106 Bind(wxEVT_MENU, &TrackerFrame::OnApConnect, this, ID_AP_CONNECT);
107 Bind(wxEVT_MENU, &TrackerFrame::OnIpcConnect, this, ID_IPC_CONNECT);
57 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); 108 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
58 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, 109 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
59 ID_CHECK_FOR_UPDATES); 110 ID_CHECK_FOR_UPDATES);
111 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
112 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
113 Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG);
114 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
115 Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged,
116 this);
60 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); 117 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
61 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 118 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
62 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this); 119 Bind(STATUS_CHANGED, &TrackerFrame::OnStatusChanged, this);
120 Bind(CONNECT_TO_AP, &TrackerFrame::OnConnectToAp, this);
121
122 wxSize logicalSize = FromDIP(wxSize(1280, 728));
63 123
64 achievements_pane_ = new AchievementsPane(this); 124 splitter_window_ = new wxSplitterWindow(this, wxID_ANY);
125 splitter_window_->SetMinimumPaneSize(logicalSize.x / 5);
65 126
66 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY); 127 wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY);
128
129 achievements_pane_ = new AchievementsPane(choicebook);
67 choicebook->AddPage(achievements_pane_, "Achievements"); 130 choicebook->AddPage(achievements_pane_, "Achievements");
68 131
69 wxNotebook *rightpane = new wxNotebook(this, wxID_ANY); 132 items_pane_ = new ItemsPane(choicebook);
70 tracker_panel_ = new TrackerPanel(rightpane); 133 choicebook->AddPage(items_pane_, "Items");
71 subway_map_ = new SubwayMap(rightpane); 134
72 rightpane->AddPage(tracker_panel_, "Map"); 135 options_pane_ = new OptionsPane(choicebook);
73 rightpane->AddPage(subway_map_, "Subway"); 136 choicebook->AddPage(options_pane_, "Options");
137
138 paintings_pane_ = new PaintingsPane(choicebook);
139 choicebook->AddPage(paintings_pane_, "Paintings");
74 140
75 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 141 notebook_ = new wxNotebook(splitter_window_, wxID_ANY);
76 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1)); 142 tracker_panel_ = new TrackerPanel(notebook_);
77 top_sizer->Add(rightpane, wxSizerFlags().Expand().Proportion(3)); 143 subway_map_ = new SubwayMap(notebook_);
144 notebook_->AddPage(tracker_panel_, "Map");
145 notebook_->AddPage(subway_map_, "Subway");
78 146
79 SetSizerAndFit(top_sizer); 147 splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4);
80 SetSize(1280, 728); 148
149 SetSize(logicalSize);
81 150
82 if (!GetTrackerConfig().asked_to_check_for_updates) { 151 if (!GetTrackerConfig().asked_to_check_for_updates) {
83 GetTrackerConfig().asked_to_check_for_updates = true; 152 GetTrackerConfig().asked_to_check_for_updates = true;
@@ -94,23 +163,28 @@ TrackerFrame::TrackerFrame()
94 } 163 }
95 164
96 if (GetTrackerConfig().should_check_for_updates) { 165 if (GetTrackerConfig().should_check_for_updates) {
97 CheckForUpdates(/*manual=*/false); 166 updater_->CheckForUpdates(/*invisible=*/true);
98 } 167 }
168
169 SetStatusText(GetStatusMessage());
99} 170}
100 171
101void TrackerFrame::SetStatusMessage(std::string message) { 172void TrackerFrame::ConnectToAp(std::string server, std::string user,
102 wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED); 173 std::string pass) {
103 event->SetString(message.c_str()); 174 QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server),
175 std::move(user), std::move(pass)));
176}
104 177
105 QueueEvent(event); 178void TrackerFrame::UpdateStatusMessage() {
179 QueueEvent(new wxCommandEvent(STATUS_CHANGED));
106} 180}
107 181
108void TrackerFrame::ResetIndicators() { 182void TrackerFrame::ResetIndicators() {
109 QueueEvent(new wxCommandEvent(STATE_RESET)); 183 QueueEvent(new wxCommandEvent(STATE_RESET));
110} 184}
111 185
112void TrackerFrame::UpdateIndicators() { 186void TrackerFrame::UpdateIndicators(StateUpdate state) {
113 QueueEvent(new wxCommandEvent(STATE_CHANGED)); 187 QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state)));
114} 188}
115 189
116void TrackerFrame::OnAbout(wxCommandEvent &event) { 190void TrackerFrame::OnAbout(wxCommandEvent &event) {
@@ -118,6 +192,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
118 about_info.SetName("Lingo Archipelago Tracker"); 192 about_info.SetName("Lingo Archipelago Tracker");
119 about_info.SetVersion(kTrackerVersion.ToString()); 193 about_info.SetVersion(kTrackerVersion.ToString());
120 about_info.AddDeveloper("hatkirby"); 194 about_info.AddDeveloper("hatkirby");
195 about_info.AddDeveloper("art0007i");
121 about_info.AddArtist("Brenton Wildes"); 196 about_info.AddArtist("Brenton Wildes");
122 about_info.AddArtist("kinrah"); 197 about_info.AddArtist("kinrah");
123 198
@@ -126,7 +201,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
126 201
127void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } 202void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
128 203
129void TrackerFrame::OnConnect(wxCommandEvent &event) { 204void TrackerFrame::OnApConnect(wxCommandEvent &event) {
130 ConnectionDialog dlg; 205 ConnectionDialog dlg;
131 206
132 if (dlg.ShowModal() == wxID_OK) { 207 if (dlg.ShowModal() == wxID_OK) {
@@ -144,7 +219,7 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
144 } 219 }
145 } 220 }
146 221
147 while (new_history.size() > 5) { 222 while (new_history.size() > 10) {
148 new_history.pop_back(); 223 new_history.pop_back();
149 } 224 }
150 225
@@ -156,6 +231,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
156 } 231 }
157} 232}
158 233
234void TrackerFrame::OnIpcConnect(wxCommandEvent &event) {
235 IpcDialog dlg;
236
237 if (dlg.ShowModal() == wxID_OK) {
238 GetTrackerConfig().ipc_address = dlg.GetIpcAddress();
239 GetTrackerConfig().Save();
240
241 IPC_Connect(dlg.GetIpcAddress());
242 }
243}
244
159void TrackerFrame::OnSettings(wxCommandEvent &event) { 245void TrackerFrame::OnSettings(wxCommandEvent &event) {
160 SettingsDialog dlg; 246 SettingsDialog dlg;
161 247
@@ -163,83 +249,119 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
163 GetTrackerConfig().should_check_for_updates = 249 GetTrackerConfig().should_check_for_updates =
164 dlg.GetShouldCheckForUpdates(); 250 dlg.GetShouldCheckForUpdates();
165 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); 251 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
166 GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); 252 GetTrackerConfig().visible_panels = dlg.GetVisiblePanels();
253 GetTrackerConfig().track_position = dlg.GetTrackPosition();
167 GetTrackerConfig().Save(); 254 GetTrackerConfig().Save();
168 255
169 UpdateIndicators(); 256 UpdateIndicators(StateUpdate{.cleared_locations = true,
257 .player_position = true,
258 .changed_settings = true});
170 } 259 }
171} 260}
172 261
173void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 262void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
174 CheckForUpdates(/*manual=*/true); 263 updater_->CheckForUpdates(/*invisible=*/false);
175} 264}
176 265
177void TrackerFrame::OnStateReset(wxCommandEvent& event) { 266void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
178 tracker_panel_->UpdateIndicators(); 267 if (notebook_->GetSelection() == 1) {
179 achievements_pane_->UpdateIndicators(); 268 subway_map_->Zoom(true);
180 subway_map_->OnConnect(); 269 }
181 Refresh();
182} 270}
183 271
184void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 272void TrackerFrame::OnZoomOut(wxCommandEvent &event) {
185 tracker_panel_->UpdateIndicators(); 273 if (notebook_->GetSelection() == 1) {
186 achievements_pane_->UpdateIndicators(); 274 subway_map_->Zoom(false);
187 subway_map_->UpdateIndicators(); 275 }
188 Refresh();
189} 276}
190 277
191void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { 278void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) {
192 SetStatusText(event.GetString()); 279 if (log_dialog_ == nullptr) {
280 log_dialog_ = new LogDialog(this);
281 log_dialog_->Show();
282 TrackerSetLogDialog(log_dialog_);
283
284 log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow,
285 this);
286 } else {
287 log_dialog_->SetFocus();
288 }
193} 289}
194 290
195void TrackerFrame::CheckForUpdates(bool manual) { 291void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) {
196 wxWebRequest request = wxWebSession::GetDefault().CreateRequest( 292 TrackerSetLogDialog(nullptr);
197 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); 293 log_dialog_ = nullptr;
198 294
199 if (!request.IsOk()) { 295 event.Skip();
200 if (manual) { 296}
201 wxMessageBox("Could not check for updates.", "Error", 297
202 wxOK | wxICON_ERROR); 298void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
203 } else { 299 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
204 SetStatusText("Could not check for updates."); 300 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
301}
302
303void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) {
304 notebook_->Refresh();
305}
306
307void TrackerFrame::OnStateReset(wxCommandEvent &event) {
308 tracker_panel_->UpdateIndicators(/*reset=*/true);
309 achievements_pane_->UpdateIndicators();
310 items_pane_->ResetIndicators();
311 options_pane_->OnConnect();
312 paintings_pane_->ResetIndicators();
313 subway_map_->OnConnect();
314 Refresh();
315}
316
317void TrackerFrame::OnStateChanged(StateChangedEvent &event) {
318 const StateUpdate &state = event.GetState();
319
320 bool hunt_panels = false;
321 if (GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) {
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);
336 subway_map_->UpdateIndicators();
337 Refresh();
338 } else if (state.player_position && GetTrackerConfig().track_position) {
339 if (notebook_->GetSelection() == 0) {
340 tracker_panel_->Refresh();
205 } 341 }
342 }
206 343
207 return; 344 if (std::any_of(state.panels.begin(), state.panels.end(),
345 [](int solve_index) {
346 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index))
347 .achievement;
348 })) {
349 achievements_pane_->UpdateIndicators();
208 } 350 }
209 351
210 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { 352 if (!state.items.empty()) {
211 if (evt.GetState() == wxWebRequest::State_Completed) { 353 items_pane_->UpdateIndicators(state.items);
212 std::string response = evt.GetResponse().AsString().ToStdString(); 354 }
213 355
214 Version latest_version(response); 356 if (!state.paintings.empty()) {
215 if (kTrackerVersion < latest_version) { 357 paintings_pane_->UpdateIndicators(state.paintings);
216 std::ostringstream message_text; 358 }
217 message_text << "There is a newer version of Lingo AP Tracker " 359}
218 "available. You have " 360
219 << kTrackerVersion.ToString() 361void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
220 << ", and the latest version is " 362 SetStatusText(wxString::FromUTF8(GetStatusMessage()));
221 << latest_version.ToString() 363}
222 << ". Would you like to update?";
223
224 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
225 wxYES) {
226 wxLaunchDefaultBrowser(
227 "https://code.fourisland.com/lingo-ap-tracker/about/"
228 "CHANGELOG.md");
229 }
230 } else if (manual) {
231 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
232 wxOK);
233 }
234 } else if (evt.GetState() == wxWebRequest::State_Failed) {
235 if (manual) {
236 wxMessageBox("Could not check for updates.", "Error",
237 wxOK | wxICON_ERROR);
238 } else {
239 SetStatusText("Could not check for updates.");
240 }
241 }
242 });
243 364
244 request.Start(); 365void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
366 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
245} 367}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index f1d7171..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,39 +7,121 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <memory>
11#include <set>
12
13#include "ap_state.h"
14#include "icons.h"
15#include "updater.h"
16
10class AchievementsPane; 17class AchievementsPane;
18class ItemsPane;
19class LogDialog;
20class OptionsPane;
21class PaintingsPane;
11class SubwayMap; 22class SubwayMap;
12class TrackerPanel; 23class TrackerPanel;
24class wxBookCtrlEvent;
25class wxNotebook;
26class wxSplitterEvent;
27class wxSplitterWindow;
28
29class ApConnectEvent : public wxEvent {
30 public:
31 ApConnectEvent(wxEventType eventType, int winid, std::string server,
32 std::string user, std::string pass)
33 : wxEvent(winid, eventType),
34 ap_server_(std::move(server)),
35 ap_user_(std::move(user)),
36 ap_pass_(std::move(pass)) {}
37
38 const std::string &GetServer() const { return ap_server_; }
39
40 const std::string &GetUser() const { return ap_user_; }
41
42 const std::string &GetPass() const { return ap_pass_; }
43
44 virtual wxEvent *Clone() const { return new ApConnectEvent(*this); }
45
46 private:
47 std::string ap_server_;
48 std::string ap_user_;
49 std::string ap_pass_;
50};
51
52struct StateUpdate {
53 std::vector<ItemState> items;
54 bool progression_items = false;
55 std::vector<std::string> paintings;
56 bool cleared_locations = false;
57 std::set<int> panels;
58 bool player_position = false;
59 bool changed_settings = false;
60};
61
62class StateChangedEvent : public wxEvent {
63 public:
64 StateChangedEvent(wxEventType eventType, int winid, StateUpdate state)
65 : wxEvent(winid, eventType), state_(std::move(state)) {}
66
67 const StateUpdate &GetState() const { return state_; }
68
69 virtual wxEvent *Clone() const { return new StateChangedEvent(*this); }
70
71 private:
72 StateUpdate state_;
73};
13 74
14wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); 75wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
15wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 76wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent);
16wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 77wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
78wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent);
17 79
18class TrackerFrame : public wxFrame { 80class TrackerFrame : public wxFrame {
19 public: 81 public:
20 TrackerFrame(); 82 TrackerFrame();
21 83
22 void SetStatusMessage(std::string message); 84 void ConnectToAp(std::string server, std::string user, std::string pass);
85 void UpdateStatusMessage();
23 86
24 void ResetIndicators(); 87 void ResetIndicators();
25 void UpdateIndicators(); 88 void UpdateIndicators(StateUpdate state);
26 89
27 private: 90 private:
28 void OnExit(wxCommandEvent &event); 91 void OnExit(wxCommandEvent &event);
29 void OnAbout(wxCommandEvent &event); 92 void OnAbout(wxCommandEvent &event);
30 void OnConnect(wxCommandEvent &event); 93 void OnApConnect(wxCommandEvent &event);
94 void OnIpcConnect(wxCommandEvent &event);
31 void OnSettings(wxCommandEvent &event); 95 void OnSettings(wxCommandEvent &event);
32 void OnCheckForUpdates(wxCommandEvent &event); 96 void OnCheckForUpdates(wxCommandEvent &event);
97 void OnZoomIn(wxCommandEvent &event);
98 void OnZoomOut(wxCommandEvent &event);
99 void OnOpenLogWindow(wxCommandEvent &event);
100 void OnCloseLogWindow(wxCloseEvent &event);
101 void OnChangePage(wxBookCtrlEvent &event);
102 void OnSashPositionChanged(wxSplitterEvent &event);
33 103
34 void OnStateReset(wxCommandEvent &event); 104 void OnStateReset(wxCommandEvent &event);
35 void OnStateChanged(wxCommandEvent &event); 105 void OnStateChanged(StateChangedEvent &event);
36 void OnStatusChanged(wxCommandEvent &event); 106 void OnStatusChanged(wxCommandEvent &event);
107 void OnConnectToAp(ApConnectEvent &event);
108
109 std::unique_ptr<Updater> updater_;
37 110
38 void CheckForUpdates(bool manual); 111 wxSplitterWindow *splitter_window_;
39 112 wxNotebook *notebook_;
40 TrackerPanel *tracker_panel_; 113 TrackerPanel *tracker_panel_;
41 AchievementsPane *achievements_pane_; 114 AchievementsPane *achievements_pane_;
115 ItemsPane *items_pane_;
116 OptionsPane *options_pane_;
117 PaintingsPane *paintings_pane_;
42 SubwayMap *subway_map_; 118 SubwayMap *subway_map_;
119 LogDialog *log_dialog_ = nullptr;
120
121 wxMenuItem *zoom_in_menu_item_;
122 wxMenuItem *zoom_out_menu_item_;
123
124 IconCache icons_;
43}; 125};
44 126
45#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 66bce81..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -1,11 +1,15 @@
1#include "tracker_panel.h" 1#include "tracker_panel.h"
2 2
3#include <fmt/core.h>
3#include <wx/dcbuffer.h> 4#include <wx/dcbuffer.h>
4 5
6#include <algorithm>
7
5#include "ap_state.h" 8#include "ap_state.h"
6#include "area_popup.h" 9#include "area_popup.h"
7#include "game_data.h" 10#include "game_data.h"
8#include "global.h" 11#include "global.h"
12#include "ipc_state.h"
9#include "tracker_config.h" 13#include "tracker_config.h"
10#include "tracker_state.h" 14#include "tracker_state.h"
11 15
@@ -39,15 +43,38 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
39 areas_.push_back(area); 43 areas_.push_back(area);
40 } 44 }
41 45
46 Resize();
42 Redraw(); 47 Redraw();
43 48
44 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); 49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
45 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); 50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
46} 51}
47 52
48void TrackerPanel::UpdateIndicators() { 53void TrackerPanel::UpdateIndicators(bool reset) {
49 for (AreaIndicator &area : areas_) { 54 if (reset) {
50 area.popup->UpdateIndicators(); 55 for (AreaIndicator &area : areas_) {
56 const MapArea &map_area = GD_GetMapArea(area.area_id);
57
58 if ((!AP_IsLocationVisible(map_area.classification) ||
59 IsAreaPostgame(area.area_id)) &&
60 !(map_area.hunt &&
61 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
62 !(map_area.has_single_panel &&
63 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) &&
64 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
65 area.active = false;
66 } else {
67 area.active = true;
68 }
69
70 area.popup->ResetIndicators();
71 }
72
73 Resize();
74 } else {
75 for (AreaIndicator &area : areas_) {
76 area.popup->UpdateIndicators();
77 }
51 } 78 }
52 79
53 Redraw(); 80 Redraw();
@@ -55,22 +82,35 @@ void TrackerPanel::UpdateIndicators() {
55 82
56void TrackerPanel::OnPaint(wxPaintEvent &event) { 83void TrackerPanel::OnPaint(wxPaintEvent &event) {
57 if (GetSize() != rendered_.GetSize()) { 84 if (GetSize() != rendered_.GetSize()) {
85 Resize();
58 Redraw(); 86 Redraw();
59 } 87 }
60 88
61 wxBufferedPaintDC dc(this); 89 wxBufferedPaintDC dc(this);
62 dc.DrawBitmap(rendered_, 0, 0); 90 dc.DrawBitmap(rendered_, 0, 0);
63 91
64 if (AP_GetPlayerPosition().has_value()) { 92 std::optional<std::tuple<int, int>> player_position;
93 if (GetTrackerConfig().track_position)
94 {
95 if (IPC_IsConnected()) {
96 player_position = IPC_GetPlayerPosition();
97 } else {
98 player_position = AP_GetPlayerPosition();
99 }
100 }
101
102 if (player_position.has_value()) {
65 // 1588, 1194 103 // 1588, 1194
66 // 14x14 -> 154x154 104 // 14x14 -> 154x154
67 double intended_x = 105 double intended_x =
68 1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 106 1588.0 + (std::get<0>(*player_position) * (154.0 / 14.0));
69 double intended_y = 107 double intended_y =
70 1194.0 + (std::get<1>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 108 1194.0 + (std::get<1>(*player_position) * (154.0 / 14.0));
71 109
72 int real_x = offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; 110 int real_x =
73 int real_y = offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2; 111 offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2;
112 int real_y =
113 offset_y_ + scale_y_ * intended_y - scaled_player_.GetHeight() / 2;
74 114
75 dc.DrawBitmap(scaled_player_, real_x, real_y); 115 dc.DrawBitmap(scaled_player_, real_x, real_y);
76 } 116 }
@@ -92,8 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
92 event.Skip(); 132 event.Skip();
93} 133}
94 134
95void TrackerPanel::Redraw() { 135void TrackerPanel::Resize() {
96 wxSize panel_size = GetSize(); 136 wxSize panel_size = GetClientSize();
97 wxSize image_size = map_image_.GetSize(); 137 wxSize image_size = map_image_.GetSize();
98 138
99 int final_x = 0; 139 int final_x = 0;
@@ -112,7 +152,7 @@ void TrackerPanel::Redraw() {
112 final_x = (panel_size.GetWidth() - final_width) / 2; 152 final_x = (panel_size.GetWidth() - final_width) / 2;
113 } 153 }
114 154
115 rendered_ = wxBitmap( 155 scaled_map_ = wxBitmap(
116 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) 156 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
117 .Size(panel_size, {final_x, final_y}, 0, 0, 0)); 157 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
118 158
@@ -127,27 +167,64 @@ void TrackerPanel::Redraw() {
127 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, 167 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1,
128 player_height > 0 ? player_height : 1)); 168 player_height > 0 ? player_height : 1));
129 169
170 real_area_size_ = final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
171
172 for (AreaIndicator &area : areas_) {
173 const MapArea &map_area = GD_GetMapArea(area.area_id);
174
175 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) *
176 final_width / image_size.GetWidth();
177 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
178 final_width / image_size.GetWidth();
179
180 area.real_x1 = real_area_x;
181 area.real_x2 = real_area_x + real_area_size_;
182 area.real_y1 = real_area_y;
183 area.real_y2 = real_area_y + real_area_size_;
184
185 int popup_x =
186 final_x + map_area.map_x * final_width / image_size.GetWidth();
187 int popup_y =
188 final_y + map_area.map_y * final_width / image_size.GetWidth();
189
190 area.popup->SetClientSize(
191 area.popup->GetFullWidth(),
192 std::min(panel_size.GetHeight(), area.popup->GetFullHeight()));
193
194 if (area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
195 area.popup->SetSize(area.popup->GetSize().GetWidth(),
196 panel_size.GetHeight());
197 }
198
199 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
200 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
201 }
202 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
203 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
204 }
205 area.popup->SetPosition({popup_x, popup_y});
206 }
207}
208
209void TrackerPanel::Redraw() {
210 rendered_ = scaled_map_;
211
130 wxMemoryDC dc; 212 wxMemoryDC dc;
131 dc.SelectObject(rendered_); 213 dc.SelectObject(rendered_);
132 214
133 int real_area_size =
134 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
135 int actual_border_size = 215 int actual_border_size =
136 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; 216 real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
137 const wxPoint upper_left_triangle[] = { 217 const wxPoint upper_left_triangle[] = {
138 {0, 0}, {0, real_area_size}, {real_area_size, 0}}; 218 {0, 0}, {0, real_area_size_}, {real_area_size_, 0}};
139 const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, 219 const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1},
140 {real_area_size - 1, 0}, 220 {real_area_size_ - 1, 0},
141 {real_area_size, real_area_size}}; 221 {real_area_size_, real_area_size_}};
142 222
143 for (AreaIndicator &area : areas_) { 223 for (AreaIndicator &area : areas_) {
144 const MapArea &map_area = GD_GetMapArea(area.area_id); 224 const MapArea &map_area = GD_GetMapArea(area.area_id);
145 if (!AP_IsLocationVisible(map_area.classification) && 225
146 !(map_area.hunt && GetTrackerConfig().show_hunt_panels)) { 226 if (!area.active) {
147 area.active = false;
148 continue; 227 continue;
149 } else {
150 area.active = true;
151 } 228 }
152 229
153 bool has_reachable_unchecked = false; 230 bool has_reachable_unchecked = false;
@@ -156,10 +233,15 @@ void TrackerPanel::Redraw() {
156 bool has_unchecked = false; 233 bool has_unchecked = false;
157 if (IsLocationWinCondition(section)) { 234 if (IsLocationWinCondition(section)) {
158 has_unchecked = !AP_HasReachedGoal(); 235 has_unchecked = !AP_HasReachedGoal();
159 } else if (AP_IsLocationVisible(section.classification)) { 236 } else if (AP_IsLocationVisible(section.classification) &&
237 !IsLocationPostgame(section.ap_location_id)) {
160 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); 238 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
161 } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { 239 } else if ((section.hunt && GetTrackerConfig().visible_panels ==
162 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);
163 } 245 }
164 246
165 if (has_unchecked) { 247 if (has_unchecked) {
@@ -171,10 +253,26 @@ void TrackerPanel::Redraw() {
171 } 253 }
172 } 254 }
173 255
174 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 256 if (AP_IsPaintingShuffle()) {
175 final_width / image_size.GetWidth(); 257 for (int painting_id : map_area.paintings) {
176 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) * 258 if (IsPaintingPostgame(painting_id)) {
177 final_width / image_size.GetWidth(); 259 continue;
260 }
261
262 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
263 bool reachable = IsPaintingReachable(painting_id);
264 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) {
265 if (reachable) {
266 has_reachable_unchecked = true;
267 } else {
268 has_unreachable_unchecked = true;
269 }
270 }
271 }
272 }
273
274 int real_area_x = area.real_x1;
275 int real_area_y = area.real_y1;
178 276
179 if (has_reachable_unchecked && has_unreachable_unchecked && 277 if (has_reachable_unchecked && has_unreachable_unchecked &&
180 GetTrackerConfig().hybrid_areas) { 278 GetTrackerConfig().hybrid_areas) {
@@ -188,7 +286,7 @@ void TrackerPanel::Redraw() {
188 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
189 dc.SetBrush(*wxTRANSPARENT_BRUSH); 287 dc.SetBrush(*wxTRANSPARENT_BRUSH);
190 dc.DrawRectangle({real_area_x, real_area_y}, 288 dc.DrawRectangle({real_area_x, real_area_y},
191 {real_area_size, real_area_size}); 289 {real_area_size_, real_area_size_});
192 290
193 } else { 291 } else {
194 const wxBrush *brush_color = wxGREY_BRUSH; 292 const wxBrush *brush_color = wxGREY_BRUSH;
@@ -203,30 +301,7 @@ void TrackerPanel::Redraw() {
203 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 301 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
204 dc.SetBrush(*brush_color); 302 dc.SetBrush(*brush_color);
205 dc.DrawRectangle({real_area_x, real_area_y}, 303 dc.DrawRectangle({real_area_x, real_area_y},
206 {real_area_size, real_area_size}); 304 {real_area_size_, real_area_size_});
207 } 305 }
208
209 area.real_x1 = real_area_x;
210 area.real_x2 = real_area_x + real_area_size;
211 area.real_y1 = real_area_y;
212 area.real_y2 = real_area_y + real_area_size;
213
214 int popup_x =
215 final_x + map_area.map_x * final_width / image_size.GetWidth();
216 int popup_y =
217 final_y + map_area.map_y * final_width / image_size.GetWidth();
218
219 area.popup->SetClientSize(
220 area.popup->GetVirtualSize().GetWidth(),
221 std::min(panel_size.GetHeight(),
222 area.popup->GetVirtualSize().GetHeight()));
223
224 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
225 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
226 }
227 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
228 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
229 }
230 area.popup->SetPosition({popup_x, popup_y});
231 } 306 }
232} 307}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index 06ec7a0..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h
@@ -7,13 +7,17 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <optional>
11#include <set>
12#include <string>
13
10class AreaPopup; 14class AreaPopup;
11 15
12class TrackerPanel : public wxPanel { 16class TrackerPanel : public wxPanel {
13 public: 17 public:
14 TrackerPanel(wxWindow *parent); 18 TrackerPanel(wxWindow *parent);
15 19
16 void UpdateIndicators(); 20 void UpdateIndicators(bool reset);
17 21
18 private: 22 private:
19 struct AreaIndicator { 23 struct AreaIndicator {
@@ -29,10 +33,12 @@ class TrackerPanel : public wxPanel {
29 void OnPaint(wxPaintEvent &event); 33 void OnPaint(wxPaintEvent &event);
30 void OnMouseMove(wxMouseEvent &event); 34 void OnMouseMove(wxMouseEvent &event);
31 35
36 void Resize();
32 void Redraw(); 37 void Redraw();
33 38
34 wxImage map_image_; 39 wxImage map_image_;
35 wxImage player_image_; 40 wxImage player_image_;
41 wxBitmap scaled_map_;
36 wxBitmap rendered_; 42 wxBitmap rendered_;
37 wxBitmap scaled_player_; 43 wxBitmap scaled_player_;
38 44
@@ -40,6 +46,7 @@ class TrackerPanel : public wxPanel {
40 int offset_y_ = 0; 46 int offset_y_ = 0;
41 double scale_x_ = 0; 47 double scale_x_ = 0;
42 double scale_y_ = 0; 48 double scale_y_ = 0;
49 int real_area_size_ = 0;
43 50
44 std::vector<AreaIndicator> areas_; 51 std::vector<AreaIndicator> areas_;
45}; 52};
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 5588c7f..674f68a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -1,5 +1,8 @@
1#include "tracker_state.h" 1#include "tracker_state.h"
2 2
3#include <fmt/core.h>
4#include <hkutil/string.h>
5
3#include <list> 6#include <list>
4#include <map> 7#include <map>
5#include <mutex> 8#include <mutex>
@@ -9,13 +12,172 @@
9 12
10#include "ap_state.h" 13#include "ap_state.h"
11#include "game_data.h" 14#include "game_data.h"
15#include "global.h"
16#include "logger.h"
12 17
13namespace { 18namespace {
14 19
20struct Requirements {
21 bool disabled = false;
22
23 std::set<int> doors; // non-grouped, handles progressive
24 std::set<int> panel_doors; // non-grouped, handles progressive
25 std::set<int> items; // all other items
26 std::set<int> rooms; // maybe
27 bool mastery = false; // maybe
28 bool panel_hunt = false; // maybe
29 bool postgame = false;
30
31 void Merge(const Requirements& rhs) {
32 if (rhs.disabled) {
33 return;
34 }
35
36 for (int id : rhs.doors) {
37 doors.insert(id);
38 }
39 for (int id : rhs.panel_doors) {
40 panel_doors.insert(id);
41 }
42 for (int id : rhs.items) {
43 items.insert(id);
44 }
45 for (int id : rhs.rooms) {
46 rooms.insert(id);
47 }
48 mastery = mastery || rhs.mastery;
49 panel_hunt = panel_hunt || rhs.panel_hunt;
50 postgame = postgame || rhs.postgame;
51 }
52};
53
54class RequirementCalculator {
55 public:
56 void Reset() {
57 doors_.clear();
58 panels_.clear();
59 }
60
61 const Requirements& GetDoor(int door_id) {
62 if (!doors_.count(door_id)) {
63 Requirements requirements;
64 const Door& door_obj = GD_GetDoor(door_id);
65
66 if (door_obj.type == DoorType::kSunPainting) {
67 if (!AP_IsPilgrimageEnabled()) {
68 requirements.items.insert(door_obj.ap_item_id);
69 } else {
70 requirements.disabled = true;
71 }
72 } else if (door_obj.type == DoorType::kSunwarp) {
73 switch (AP_GetSunwarpAccess()) {
74 case kSUNWARP_ACCESS_NORMAL:
75 // Do nothing.
76 break;
77 case kSUNWARP_ACCESS_DISABLED:
78 requirements.disabled = true;
79 break;
80 case kSUNWARP_ACCESS_UNLOCK:
81 requirements.items.insert(door_obj.group_ap_item_id);
82 break;
83 case kSUNWARP_ACCESS_INDIVIDUAL:
84 case kSUNWARP_ACCESS_PROGRESSIVE:
85 requirements.doors.insert(door_obj.id);
86 break;
87 }
88 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) {
89 for (int panel_id : door_obj.panels) {
90 const Requirements& panel_reqs = GetPanel(panel_id);
91 requirements.Merge(panel_reqs);
92 }
93 } else if (AP_AreDoorsGrouped() && !door_obj.group_name.empty()) {
94 requirements.items.insert(door_obj.group_ap_item_id);
95 } else {
96 requirements.doors.insert(door_obj.id);
97 }
98
99 doors_[door_id] = requirements;
100 }
101
102 return doors_[door_id];
103 }
104
105 const Requirements& GetPanel(int panel_id) {
106 if (!panels_.count(panel_id)) {
107 Requirements requirements;
108 const Panel& panel_obj = GD_GetPanel(panel_id);
109
110 requirements.rooms.insert(panel_obj.room);
111
112 if (panel_obj.name == "THE MASTER") {
113 requirements.mastery = true;
114 }
115
116 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") &&
117 AP_GetLevel2Requirement() > 1) {
118 requirements.panel_hunt = true;
119 }
120
121 for (int room_id : panel_obj.required_rooms) {
122 requirements.rooms.insert(room_id);
123 }
124
125 for (int door_id : panel_obj.required_doors) {
126 const Requirements& door_reqs = GetDoor(door_id);
127 requirements.Merge(door_reqs);
128 }
129
130 for (int panel_id : panel_obj.required_panels) {
131 const Requirements& panel_reqs = GetPanel(panel_id);
132 requirements.Merge(panel_reqs);
133 }
134
135 if (AP_IsColorShuffle()) {
136 for (LingoColor color : panel_obj.colors) {
137 requirements.items.insert(GD_GetItemIdForColor(color));
138 }
139 }
140
141 if (panel_obj.panel_door != -1 &&
142 AP_GetDoorShuffleMode() == kPANELS_MODE) {
143 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_obj.panel_door);
144
145 if (panel_door_obj.group_ap_item_id != -1 && AP_AreDoorsGrouped()) {
146 requirements.items.insert(panel_door_obj.group_ap_item_id);
147 } else {
148 requirements.panel_doors.insert(panel_obj.panel_door);
149 }
150 }
151
152 if (panel_obj.location_name == GetWinCondition()) {
153 requirements.postgame = true;
154 }
155
156 panels_[panel_id] = requirements;
157 }
158
159 return panels_[panel_id];
160 }
161
162 private:
163 std::map<int, Requirements> doors_;
164 std::map<int, Requirements> panels_;
165};
166
15struct TrackerState { 167struct TrackerState {
16 std::map<int, bool> reachability; 168 std::map<int, bool> reachability;
17 std::set<int> reachable_doors; 169 std::set<int> reachable_doors;
170 std::set<int> solveable_panels;
171 std::set<int> reachable_paintings;
18 std::mutex reachability_mutex; 172 std::mutex reachability_mutex;
173 RequirementCalculator requirements;
174 std::map<int, std::map<std::string, bool>> door_reports;
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;
19}; 181};
20 182
21enum Decision { kYes, kNo, kMaybe }; 183enum Decision { kYes, kNo, kMaybe };
@@ -30,6 +192,11 @@ class StateCalculator;
30struct StateCalculatorOptions { 192struct StateCalculatorOptions {
31 int start; 193 int start;
32 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
33 StateCalculator* parent = nullptr; 200 StateCalculator* parent = nullptr;
34}; 201};
35 202
@@ -40,15 +207,33 @@ class StateCalculator {
40 explicit StateCalculator(StateCalculatorOptions options) 207 explicit StateCalculator(StateCalculatorOptions options)
41 : options_(options) {} 208 : options_(options) {}
42 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
43 void Calculate() { 220 void Calculate() {
221 painting_mapping_ = AP_GetPaintingMapping();
222 checked_paintings_ = AP_GetCheckedPaintings();
223 sunwarp_mapping_ = AP_GetSunwarpMapping();
224
44 std::list<int> panel_boundary; 225 std::list<int> panel_boundary;
226 std::list<int> painting_boundary;
45 std::list<Exit> flood_boundary; 227 std::list<Exit> flood_boundary;
46 flood_boundary.push_back({.destination_room = options_.start}); 228 flood_boundary.push_back(
229 {.source_room = -1, .destination_room = options_.start});
47 230
48 bool reachable_changed = true; 231 bool reachable_changed = true;
49 while (reachable_changed) { 232 while (reachable_changed) {
50 reachable_changed = false; 233 reachable_changed = false;
51 234
235 std::list<Exit> new_boundary;
236
52 std::list<int> new_panel_boundary; 237 std::list<int> new_panel_boundary;
53 for (int panel_id : panel_boundary) { 238 for (int panel_id : panel_boundary) {
54 if (solveable_panels_.count(panel_id)) { 239 if (solveable_panels_.count(panel_id)) {
@@ -64,7 +249,36 @@ class StateCalculator {
64 } 249 }
65 } 250 }
66 251
67 std::list<Exit> new_boundary; 252 std::list<int> new_painting_boundary;
253 for (int painting_id : painting_boundary) {
254 if (reachable_paintings_.count(painting_id)) {
255 continue;
256 }
257
258 Decision painting_reachable = IsPaintingReachable(painting_id);
259 if (painting_reachable == kYes) {
260 reachable_paintings_.insert(painting_id);
261 reachable_changed = true;
262
263 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
264 if (painting_mapping_.count(cur_painting.internal_id) &&
265 (checked_paintings_.count(cur_painting.internal_id) ||
266 options_.postgame_detection)) {
267 Exit painting_exit;
268 PaintingExit target_painting =
269 GD_GetPaintingExit(GD_GetPaintingByName(
270 painting_mapping_.at(cur_painting.internal_id)));
271 painting_exit.source_room = cur_painting.room;
272 painting_exit.destination_room = target_painting.room;
273 painting_exit.type = EntranceType::kPainting;
274
275 new_boundary.push_back(painting_exit);
276 }
277 } else if (painting_reachable == kMaybe) {
278 new_painting_boundary.push_back(painting_id);
279 }
280 }
281
68 for (const Exit& room_exit : flood_boundary) { 282 for (const Exit& room_exit : flood_boundary) {
69 if (reachable_rooms_.count(room_exit.destination_room)) { 283 if (reachable_rooms_.count(room_exit.destination_room)) {
70 continue; 284 continue;
@@ -83,6 +297,12 @@ class StateCalculator {
83 reachable_rooms_.insert(room_exit.destination_room); 297 reachable_rooms_.insert(room_exit.destination_room);
84 reachable_changed = true; 298 reachable_changed = true;
85 299
300#ifndef NDEBUG
301 std::list<int> room_path = paths_[room_exit.source_room];
302 room_path.push_back(room_exit.destination_room);
303 paths_[room_exit.destination_room] = room_path;
304#endif
305
86 const Room& room_obj = GD_GetRoom(room_exit.destination_room); 306 const Room& room_obj = GD_GetRoom(room_exit.destination_room);
87 for (const Exit& out_edge : room_obj.exits) { 307 for (const Exit& out_edge : room_obj.exits) {
88 if (out_edge.type == EntranceType::kPainting && 308 if (out_edge.type == EntranceType::kPainting &&
@@ -99,52 +319,56 @@ class StateCalculator {
99 } 319 }
100 320
101 if (AP_IsPaintingShuffle()) { 321 if (AP_IsPaintingShuffle()) {
102 for (const PaintingExit& out_edge : room_obj.paintings) { 322 for (int out_edge : room_obj.paintings) {
103 if (AP_GetPaintingMapping().count(out_edge.id)) { 323 new_painting_boundary.push_back(out_edge);
104 Exit painting_exit;
105 painting_exit.destination_room = GD_GetRoomForPainting(
106 AP_GetPaintingMapping().at(out_edge.id));
107 painting_exit.door = out_edge.door;
108
109 new_boundary.push_back(painting_exit);
110 }
111 } 324 }
112 } 325 }
113 326
114 if (AP_IsSunwarpShuffle()) { 327 if (AP_IsSunwarpShuffle()) {
115 for (int index : room_obj.sunwarps) { 328 for (int index : room_obj.sunwarps) {
116 if (AP_GetSunwarpMapping().count(index)) { 329 if (sunwarp_mapping_.count(index)) {
117 const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); 330 const SunwarpMapping& sm = sunwarp_mapping_.at(index);
118 331
119 Exit sunwarp_exit; 332 new_boundary.push_back(
120 sunwarp_exit.destination_room = 333 {.source_room = room_exit.destination_room,
121 GD_GetRoomForSunwarp(sm.exit_index); 334 .destination_room = GD_GetRoomForSunwarp(sm.exit_index),
122 sunwarp_exit.door = GD_GetSunwarpDoors().at(sm.dots - 1); 335 .door = GD_GetSunwarpDoors().at(sm.dots - 1),
123 336 .type = EntranceType::kSunwarp});
124 new_boundary.push_back(sunwarp_exit);
125 } 337 }
126 } 338 }
127 } 339 }
128 340
129 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { 341 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") {
130 new_boundary.push_back( 342 new_boundary.push_back(
131 {.destination_room = GD_GetRoomByName("Outside The Undeterred"), 343 {.source_room = room_exit.destination_room,
132 .type = EntranceType::kPainting}); 344 .destination_room = GD_GetRoomByName("Color Hallways"),
345 .type = EntranceType::kStaticPainting});
133 } 346 }
134 347
135 if (AP_IsPilgrimageEnabled()) { 348 if (AP_IsPilgrimageEnabled()) {
136 if (room_obj.name == "Hub Room") { 349 int pilgrimage_start_id = GD_GetRoomByName("Hub Room");
350 if (AP_IsSunwarpShuffle()) {
351 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
352 if (mapping.dots == 1) {
353 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index);
354 }
355 }
356 }
357
358 if (room_exit.destination_room == pilgrimage_start_id) {
137 new_boundary.push_back( 359 new_boundary.push_back(
138 {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), 360 {.source_room = room_exit.destination_room,
361 .destination_room = GD_GetRoomByName("Pilgrim Antechamber"),
139 .type = EntranceType::kPilgrimage}); 362 .type = EntranceType::kPilgrimage});
140 } 363 }
141 } else { 364 } else {
142 if (room_obj.name == "Starting Room") { 365 if (room_obj.name == "Starting Room") {
143 new_boundary.push_back( 366 new_boundary.push_back(
144 {.destination_room = GD_GetRoomByName("Pilgrim Antechamber"), 367 {.source_room = room_exit.destination_room,
368 .destination_room = GD_GetRoomByName("Pilgrim Antechamber"),
145 .door = 369 .door =
146 GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"), 370 GD_GetDoorByName("Pilgrim Antechamber - Sun Painting"),
147 .type = EntranceType::kPainting}); 371 .type = EntranceType::kStaticPainting});
148 } 372 }
149 } 373 }
150 374
@@ -156,11 +380,17 @@ class StateCalculator {
156 380
157 flood_boundary = new_boundary; 381 flood_boundary = new_boundary;
158 panel_boundary = new_panel_boundary; 382 panel_boundary = new_panel_boundary;
383 painting_boundary = new_painting_boundary;
159 } 384 }
160 385
161 // Now that we know the full reachable area, let's make sure all doors are evaluated. 386 // Now that we know the full reachable area, let's make sure all doors are
387 // evaluated.
162 for (const Door& door : GD_GetDoors()) { 388 for (const Door& door : GD_GetDoors()) {
163 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]);
164 } 394 }
165 } 395 }
166 396
@@ -172,8 +402,32 @@ class StateCalculator {
172 402
173 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; } 403 const std::set<int>& GetSolveablePanels() const { return solveable_panels_; }
174 404
405 const std::set<int>& GetReachablePaintings() const {
406 return reachable_paintings_;
407 }
408
409 const std::map<int, std::map<std::string, bool>>& GetDoorReports() const {
410 return door_report_;
411 }
412
413 bool IsPilgrimageDoable() const { return pilgrimage_doable_; }
414
415 std::string GetPathToRoom(int room_id) const {
416 if (!paths_.count(room_id)) {
417 return "";
418 }
419
420 const std::list<int>& path = paths_.at(room_id);
421 std::vector<std::string> room_names;
422 for (int room_id : path) {
423 room_names.push_back(GD_GetRoom(room_id).name);
424 }
425 return hatkirby::implode(room_names, " -> ");
426 }
427
175 private: 428 private:
176 Decision IsNonGroupedDoorReachable(const Door& door_obj) { 429 template <typename T>
430 Decision IsNonGroupedDoorReachable(const T& door_obj) {
177 bool has_item = AP_HasItem(door_obj.ap_item_id); 431 bool has_item = AP_HasItem(door_obj.ap_item_id);
178 432
179 if (!has_item) { 433 if (!has_item) {
@@ -188,68 +442,71 @@ class StateCalculator {
188 return has_item ? kYes : kNo; 442 return has_item ? kYes : kNo;
189 } 443 }
190 444
191 Decision IsDoorReachable_Helper(int door_id) { 445 Decision AreRequirementsSatisfied(
192 const Door& door_obj = GD_GetDoor(door_id); 446 const Requirements& reqs, std::map<std::string, bool>* report = nullptr) {
193 447 if (reqs.disabled) {
194 if (!AP_IsPilgrimageEnabled() && door_obj.type == DoorType::kSunPainting) { 448 return kNo;
195 return AP_HasItem(door_obj.ap_item_id) ? kYes : kNo; 449 }
196 } else if (door_obj.type == DoorType::kSunwarp) { 450
197 switch (AP_GetSunwarpAccess()) { 451 if (reqs.postgame && options_.postgame_detection) {
198 case kSUNWARP_ACCESS_NORMAL: 452 return kNo;
199 return kYes; 453 }
200 case kSUNWARP_ACCESS_DISABLED: 454
201 return kNo; 455 Decision final_decision = kYes;
202 case kSUNWARP_ACCESS_UNLOCK: 456
203 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo; 457 if (!options_.postgame_detection) {
204 case kSUNWARP_ACCESS_INDIVIDUAL: 458 for (int door_id : reqs.doors) {
205 case kSUNWARP_ACCESS_PROGRESSIVE: 459 const Door& door_obj = GD_GetDoor(door_id);
206 return IsNonGroupedDoorReachable(door_obj); 460 Decision decision = IsNonGroupedDoorReachable(door_obj);
207 } 461
208 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) { 462 if (report) {
209 if (!reachable_rooms_.count(door_obj.room)) { 463 (*report)[door_obj.item_name] = (decision == kYes);
210 return kMaybe; 464 }
211 } 465
212 466 if (decision != kYes) {
213 for (int panel_id : door_obj.panels) { 467 final_decision = decision;
214 if (!solveable_panels_.count(panel_id)) {
215 return kMaybe;
216 } 468 }
217 } 469 }
218 470
219 return kYes; 471 for (int panel_door_id : reqs.panel_doors) {
220 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS && 472 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id);
221 !door_obj.group_name.empty()) { 473 Decision decision = IsNonGroupedDoorReachable(panel_door_obj);
222 return AP_HasItem(door_obj.group_ap_item_id) ? kYes : kNo;
223 } else {
224 return IsNonGroupedDoorReachable(door_obj);
225 }
226 }
227 474
228 Decision IsDoorReachable(int door_id) { 475 if (report) {
229 if (options_.parent) { 476 (*report)[panel_door_obj.item_name] = (decision == kYes);
230 return options_.parent->IsDoorReachable(door_id); 477 }
231 }
232 478
233 if (door_decisions_.count(door_id)) { 479 if (decision != kYes) {
234 return door_decisions_.at(door_id); 480 final_decision = decision;
235 } 481 }
482 }
236 483
237 Decision result = IsDoorReachable_Helper(door_id); 484 for (int item_id : reqs.items) {
238 if (result != kMaybe) { 485 bool has_item = AP_HasItem(item_id);
239 door_decisions_[door_id] = result; 486 if (report) {
487 (*report)[GD_GetItemName(item_id)] = has_item;
488 }
489
490 if (!has_item) {
491 final_decision = kNo;
492 }
493 }
240 } 494 }
241 495
242 return result; 496 for (int room_id : reqs.rooms) {
243 } 497 bool reachable = reachable_rooms_.count(room_id);
244 498
245 Decision IsPanelReachable(int panel_id) { 499 if (report) {
246 const Panel& panel_obj = GD_GetPanel(panel_id); 500 std::string report_name = "Reach \"" + GD_GetRoom(room_id).name + "\"";
501 (*report)[report_name] = reachable;
502 }
247 503
248 if (!reachable_rooms_.count(panel_obj.room)) { 504 if (!reachable && final_decision != kNo) {
249 return kMaybe; 505 final_decision = kMaybe;
506 }
250 } 507 }
251 508
252 if (panel_obj.name == "THE MASTER") { 509 if (reqs.mastery) {
253 int achievements_accessible = 0; 510 int achievements_accessible = 0;
254 511
255 for (int achieve_id : GD_GetAchievementPanels()) { 512 for (int achieve_id : GD_GetAchievementPanels()) {
@@ -262,12 +519,18 @@ class StateCalculator {
262 } 519 }
263 } 520 }
264 521
265 return (achievements_accessible >= AP_GetMasteryRequirement()) ? kYes 522 bool can_mastery =
266 : kMaybe; 523 (achievements_accessible >= AP_GetMasteryRequirement());
524 if (report) {
525 (*report)["Mastery"] = can_mastery;
526 }
527
528 if (!can_mastery && final_decision != kNo) {
529 final_decision = kMaybe;
530 }
267 } 531 }
268 532
269 if ((panel_obj.name == "ANOTHER TRY" || panel_obj.name == "LEVEL 2") && 533 if (reqs.panel_hunt) {
270 AP_GetLevel2Requirement() > 1) {
271 int counting_panels_accessible = 0; 534 int counting_panels_accessible = 0;
272 535
273 for (int solved_panel_id : solveable_panels_) { 536 for (int solved_panel_id : solveable_panels_) {
@@ -278,41 +541,51 @@ class StateCalculator {
278 } 541 }
279 } 542 }
280 543
281 return (counting_panels_accessible >= AP_GetLevel2Requirement() - 1) 544 bool can_level2 =
282 ? kYes 545 (counting_panels_accessible >= AP_GetLevel2Requirement() - 1);
283 : kMaybe; 546 if (report) {
284 } 547 std::string report_name =
548 std::to_string(AP_GetLevel2Requirement()) + " Panels";
549 (*report)[report_name] = can_level2;
550 }
285 551
286 for (int room_id : panel_obj.required_rooms) { 552 if (!can_level2 && final_decision != kNo) {
287 if (!reachable_rooms_.count(room_id)) { 553 final_decision = kMaybe;
288 return kMaybe;
289 } 554 }
290 } 555 }
291 556
292 for (int door_id : panel_obj.required_doors) { 557 return final_decision;
293 Decision door_reachable = IsDoorReachable(door_id); 558 }
294 if (door_reachable == kNo) { 559
295 const Door& door_obj = GD_GetDoor(door_id); 560 Decision IsDoorReachable_Helper(int door_id) {
296 return (door_obj.is_event || AP_GetDoorShuffleMode() == kNO_DOORS) 561 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id));
297 ? kMaybe 562 }
298 : kNo; 563
299 } else if (door_reachable == kMaybe) { 564 Decision IsDoorReachable(int door_id) {
300 return kMaybe; 565 if (options_.parent) {
301 } 566 return options_.parent->IsDoorReachable(door_id);
302 } 567 }
303 568
304 for (int panel_id : panel_obj.required_panels) { 569 if (door_decisions_.count(door_id)) {
305 if (!solveable_panels_.count(panel_id)) { 570 return door_decisions_.at(door_id);
306 return kMaybe;
307 }
308 } 571 }
309 572
310 if (AP_IsColorShuffle()) { 573 Decision result = IsDoorReachable_Helper(door_id);
311 for (LingoColor color : panel_obj.colors) { 574 if (result != kMaybe) {
312 if (!AP_HasItem(GD_GetItemIdForColor(color))) { 575 door_decisions_[door_id] = result;
313 return kNo; 576 }
314 } 577
315 } 578 return result;
579 }
580
581 Decision IsPanelReachable(int panel_id) {
582 return AreRequirementsSatisfied(GetState().requirements.GetPanel(panel_id));
583 }
584
585 Decision IsPaintingReachable(int painting_id) {
586 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
587 if (painting.door) {
588 return IsDoorReachable(*painting.door);
316 } 589 }
317 590
318 return kYes; 591 return kYes;
@@ -337,7 +610,7 @@ class StateCalculator {
337 if (AP_IsSunwarpShuffle()) { 610 if (AP_IsSunwarpShuffle()) {
338 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); 611 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5);
339 612
340 for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { 613 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
341 if (mapping.dots > 1) { 614 if (mapping.dots > 1) {
342 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; 615 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index;
343 } 616 }
@@ -363,6 +636,8 @@ class StateCalculator {
363 } 636 }
364 } 637 }
365 638
639 pilgrimage_doable_ = true;
640
366 return kYes; 641 return kYes;
367 } 642 }
368 643
@@ -375,7 +650,8 @@ class StateCalculator {
375 !AP_DoesPilgrimageAllowRoofAccess()) { 650 !AP_DoesPilgrimageAllowRoofAccess()) {
376 return kNo; 651 return kNo;
377 } 652 }
378 if (room_exit.type == EntranceType::kPainting && 653 if ((room_exit.type == EntranceType::kPainting ||
654 room_exit.type == EntranceType::kStaticPainting) &&
379 !AP_DoesPilgrimageAllowPaintings()) { 655 !AP_DoesPilgrimageAllowPaintings()) {
380 return kNo; 656 return kNo;
381 } 657 }
@@ -401,16 +677,99 @@ class StateCalculator {
401 std::set<int> reachable_rooms_; 677 std::set<int> reachable_rooms_;
402 std::map<int, Decision> door_decisions_; 678 std::map<int, Decision> door_decisions_;
403 std::set<int> solveable_panels_; 679 std::set<int> solveable_panels_;
680 std::set<int> reachable_paintings_;
681 std::map<int, std::map<std::string, bool>> door_report_;
682 bool pilgrimage_doable_ = false;
683
684 std::map<int, std::list<int>> paths_;
685
686 std::map<std::string, std::string> painting_mapping_;
687 std::set<std::string> checked_paintings_;
688 std::map<int, SunwarpMapping> sunwarp_mapping_;
404}; 689};
405 690
406} // namespace 691} // namespace
407 692
693void ResetReachabilityRequirements() {
694 TrackerLog("Resetting tracker state...");
695
696 std::lock_guard reachability_guard(GetState().reachability_mutex);
697 GetState().requirements.Reset();
698 GetState().reachable_doors.clear();
699 GetState().solveable_panels.clear();
700
701 if (AP_IsPostgameShuffle()) {
702 GetState().non_postgame_areas.clear();
703 GetState().non_postgame_locations.clear();
704 GetState().non_postgame_paintings.clear();
705 } else {
706 StateCalculator postgame_calculator(
707 {.start = GD_GetRoomByName("Menu"), .postgame_detection = true});
708 postgame_calculator.Calculate();
709
710 std::set<int>& non_postgame_areas = GetState().non_postgame_areas;
711 non_postgame_areas.clear();
712
713 std::set<int>& non_postgame_locations = GetState().non_postgame_locations;
714 non_postgame_locations.clear();
715
716 const std::set<int>& reachable_rooms =
717 postgame_calculator.GetReachableRooms();
718 const std::set<int>& solveable_panels =
719 postgame_calculator.GetSolveablePanels();
720
721 for (const MapArea& map_area : GD_GetMapAreas()) {
722 bool area_reachable = false;
723
724 for (const Location& location_section : map_area.locations) {
725 bool reachable = reachable_rooms.count(location_section.room);
726 if (reachable) {
727 for (int panel_id : location_section.panels) {
728 reachable &= (solveable_panels.count(panel_id) == 1);
729 }
730 }
731
732 if (!reachable && IsLocationWinCondition(location_section)) {
733 reachable = true;
734 }
735
736 if (reachable) {
737 non_postgame_locations.insert(location_section.ap_location_id);
738 area_reachable = true;
739 }
740 }
741
742 for (int painting_id : map_area.paintings) {
743 if (postgame_calculator.GetReachablePaintings().count(painting_id)) {
744 area_reachable = true;
745 }
746 }
747
748 if (area_reachable) {
749 non_postgame_areas.insert(map_area.id);
750 }
751 }
752
753 GetState().non_postgame_paintings =
754 postgame_calculator.GetReachablePaintings();
755 }
756}
757
408void RecalculateReachability() { 758void RecalculateReachability() {
759 TrackerLog("Calculating reachability...");
760
761 std::lock_guard reachability_guard(GetState().reachability_mutex);
762
763 // Receiving items and checking paintings should never remove access to doors
764 // or panels, so we can preload any doors and panels we already know are
765 // accessible from previous runs, in order to reduce the work.
409 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 766 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
767 state_calculator.PreloadDoors(GetState().reachable_doors);
768 state_calculator.PreloadPanels(GetState().solveable_panels);
410 state_calculator.Calculate(); 769 state_calculator.Calculate();
411 770
412 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); 771 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms();
413 const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); 772 std::set<int> solveable_panels = state_calculator.GetSolveablePanels();
414 773
415 std::map<int, bool> new_reachability; 774 std::map<int, bool> new_reachability;
416 for (const MapArea& map_area : GD_GetMapAreas()) { 775 for (const MapArea& map_area : GD_GetMapAreas()) {
@@ -435,11 +794,16 @@ void RecalculateReachability() {
435 } 794 }
436 } 795 }
437 796
438 { 797 std::set<int> reachable_paintings = state_calculator.GetReachablePaintings();
439 std::lock_guard reachability_guard(GetState().reachability_mutex); 798 std::map<int, std::map<std::string, bool>> door_reports =
440 std::swap(GetState().reachability, new_reachability); 799 state_calculator.GetDoorReports();
441 std::swap(GetState().reachable_doors, new_reachable_doors); 800
442 } 801 std::swap(GetState().reachability, new_reachability);
802 std::swap(GetState().reachable_doors, new_reachable_doors);
803 std::swap(GetState().solveable_panels, solveable_panels);
804 std::swap(GetState().reachable_paintings, reachable_paintings);
805 std::swap(GetState().door_reports, door_reports);
806 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable();
443} 807}
444 808
445bool IsLocationReachable(int location_id) { 809bool IsLocationReachable(int location_id) {
@@ -457,3 +821,51 @@ bool IsDoorOpen(int door_id) {
457 821
458 return GetState().reachable_doors.count(door_id); 822 return GetState().reachable_doors.count(door_id);
459} 823}
824
825bool IsPaintingReachable(int painting_id) {
826 std::lock_guard reachability_guard(GetState().reachability_mutex);
827
828 return GetState().reachable_paintings.count(painting_id);
829}
830
831const std::map<std::string, bool>& GetDoorRequirements(int door_id) {
832 std::lock_guard reachability_guard(GetState().reachability_mutex);
833
834 return GetState().door_reports[door_id];
835}
836
837bool IsPilgrimageDoable() {
838 std::lock_guard reachability_guard(GetState().reachability_mutex);
839
840 return GetState().pilgrimage_doable;
841}
842
843bool IsAreaPostgame(int area_id) {
844 std::lock_guard reachability_guard(GetState().reachability_mutex);
845
846 if (GetState().non_postgame_areas.empty()) {
847 return false;
848 } else {
849 return !GetState().non_postgame_areas.count(area_id);
850 }
851}
852
853bool IsLocationPostgame(int location_id) {
854 std::lock_guard reachability_guard(GetState().reachability_mutex);
855
856 if (GetState().non_postgame_locations.empty()) {
857 return false;
858 } else {
859 return !GetState().non_postgame_locations.count(location_id);
860 }
861}
862
863bool IsPaintingPostgame(int painting_id) {
864 std::lock_guard reachability_guard(GetState().reachability_mutex);
865
866 if (GetState().non_postgame_paintings.empty()) {
867 return false;
868 } else {
869 return !GetState().non_postgame_paintings.count(painting_id);
870 }
871}
diff --git a/src/tracker_state.h b/src/tracker_state.h index 119b3b5..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h
@@ -1,10 +1,27 @@
1#ifndef TRACKER_STATE_H_8639BC90 1#ifndef TRACKER_STATE_H_8639BC90
2#define TRACKER_STATE_H_8639BC90 2#define TRACKER_STATE_H_8639BC90
3 3
4#include <map>
5#include <string>
6
7void ResetReachabilityRequirements();
8
4void RecalculateReachability(); 9void RecalculateReachability();
5 10
6bool IsLocationReachable(int location_id); 11bool IsLocationReachable(int location_id);
7 12
8bool IsDoorOpen(int door_id); 13bool IsDoorOpen(int door_id);
9 14
15bool IsPaintingReachable(int painting_id);
16
17const std::map<std::string, bool>& GetDoorRequirements(int door_id);
18
19bool IsPilgrimageDoable();
20
21bool IsAreaPostgame(int area_id);
22
23bool IsLocationPostgame(int location_id);
24
25bool IsPaintingPostgame(int painting_id);
26
10#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ 27#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..2b05daf --- /dev/null +++ b/src/updater.cpp
@@ -0,0 +1,309 @@
1#include "updater.h"
2
3#include <fmt/core.h>
4#include <openssl/evp.h>
5#include <openssl/sha.h>
6#include <wx/evtloop.h>
7#include <wx/progdlg.h>
8#include <wx/webrequest.h>
9#include <wx/wfstream.h>
10#include <wx/zipstrm.h>
11#include <yaml-cpp/yaml.h>
12
13#include <cstdio>
14#include <deque>
15#include <filesystem>
16#include <fstream>
17
18#include "global.h"
19#include "logger.h"
20#include "version.h"
21
22constexpr const char* kVersionFileUrl =
23 "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
24constexpr const char* kChangelogUrl =
25 "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
26
27namespace {
28
29std::string CalculateStringSha256(const wxString& data) {
30 unsigned char hash[SHA256_DIGEST_LENGTH];
31 EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
32 EVP_DigestInit(sha256, EVP_sha256());
33 EVP_DigestUpdate(sha256, data.c_str(), data.length());
34 EVP_DigestFinal_ex(sha256, hash, nullptr);
35 EVP_MD_CTX_free(sha256);
36
37 char output[65] = {0};
38 for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
39 snprintf(output + (i * 2), 3, "%02x", hash[i]);
40 }
41
42 return std::string(output);
43}
44
45} // namespace
46
47Updater::Updater(wxFrame* parent) : parent_(parent) {
48 Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
49}
50
51void Updater::Cleanup() {
52 std::filesystem::path oldDir = GetExecutableDirectory() / "old";
53 if (std::filesystem::is_directory(oldDir)) {
54 std::filesystem::remove_all(oldDir);
55 }
56}
57
58void Updater::CheckForUpdates(bool invisible) {
59 wxWebRequest versionRequest =
60 wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);
61
62 if (invisible) {
63 update_state_ = UpdateState::GetVersionInvisible;
64
65 versionRequest.Start();
66 } else {
67 update_state_ = UpdateState::GetVersionManual;
68
69 if (DownloadWithProgress(versionRequest)) {
70 if (versionRequest.GetState() == wxWebRequest::State_Failed) {
71 wxMessageBox("Could not check for updates.", "Error",
72 wxOK | wxICON_ERROR);
73 } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
74 ProcessVersionFile(
75 versionRequest.GetResponse().AsString().utf8_string());
76 }
77 }
78 }
79}
80
81void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
82 if (update_state_ == UpdateState::GetVersionInvisible) {
83 if (evt.GetState() == wxWebRequest::State_Completed) {
84 ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
85 } else if (evt.GetState() == wxWebRequest::State_Failed) {
86 parent_->SetStatusText("Could not check for updates.");
87 }
88 }
89}
90
91void Updater::ProcessVersionFile(std::string data) {
92 try {
93 YAML::Node versionInfo = YAML::Load(data);
94 Version latestVersion(versionInfo["version"].as<std::string>());
95
96 if (kTrackerVersion < latestVersion) {
97 if (versionInfo["packages"]) {
98 std::string platformIdentifier;
99
100 if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
101 platformIdentifier = "win64";
102 }
103
104 if (!platformIdentifier.empty() &&
105 versionInfo["packages"][platformIdentifier]) {
106 wxMessageDialog dialog(
107 nullptr,
108 fmt::format("There is a newer version of Lingo AP Tracker "
109 "available. You have {}, and the latest version is "
110 "{}. Would you like to update?",
111 kTrackerVersion.ToString(), latestVersion.ToString()),
112 "Update available", wxYES_NO | wxCANCEL);
113 dialog.SetYesNoLabels("Install update", "Open changelog");
114
115 int dlgResult = dialog.ShowModal();
116 if (dlgResult == wxID_YES) {
117 const YAML::Node& packageInfo =
118 versionInfo["packages"][platformIdentifier];
119 std::string packageUrl = packageInfo["url"].as<std::string>();
120 std::string packageChecksum =
121 packageInfo["checksum"].as<std::string>();
122
123 std::vector<std::filesystem::path> packageFiles;
124 if (packageInfo["files"]) {
125 for (const YAML::Node& filename : packageInfo["files"]) {
126 packageFiles.push_back(filename.as<std::string>());
127 }
128 }
129
130 std::vector<std::filesystem::path> deletedFiles;
131 if (packageInfo["deleted_files"]) {
132 for (const YAML::Node& filename : packageInfo["deleted_files"]) {
133 deletedFiles.push_back(filename.as<std::string>());
134 }
135 }
136
137 InstallUpdate(packageUrl, packageChecksum, packageFiles,
138 deletedFiles);
139 } else if (dlgResult == wxID_NO) {
140 wxLaunchDefaultBrowser(kChangelogUrl);
141 }
142
143 return;
144 }
145 }
146
147 if (wxMessageBox(
148 fmt::format("There is a newer version of Lingo AP Tracker "
149 "available. You have {}, and the latest version is "
150 "{}. Would you like to update?",
151 kTrackerVersion.ToString(), latestVersion.ToString()),
152 "Update available", wxYES_NO) == wxYES) {
153 wxLaunchDefaultBrowser(kChangelogUrl);
154 }
155 } else if (update_state_ == UpdateState::GetVersionManual) {
156 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
157 }
158 } catch (const std::exception& ex) {
159 wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
160 }
161}
162
163void Updater::InstallUpdate(std::string url, std::string checksum,
164 std::vector<std::filesystem::path> files,
165 std::vector<std::filesystem::path> deletedFiles) {
166 update_state_ = UpdateState::GetPackage;
167
168 wxWebRequest packageRequest =
169 wxWebSession::GetDefault().CreateRequest(this, url);
170
171 if (!DownloadWithProgress(packageRequest)) {
172 return;
173 }
174
175 bool download_issue = false;
176
177 wxFileName package_path;
178 package_path.AssignTempFileName("");
179
180 if (!package_path.IsOk()) {
181 download_issue = true;
182 } else {
183 wxFileOutputStream writeOut(package_path.GetFullPath());
184 wxString fileData = packageRequest.GetResponse().AsString();
185 writeOut.WriteAll(fileData.c_str(), fileData.length());
186
187 std::string downloadedChecksum = CalculateStringSha256(fileData);
188 if (downloadedChecksum != checksum) {
189 download_issue = true;
190 }
191 }
192
193 if (download_issue) {
194 if (wxMessageBox("There was an issue downloading the update. Would you "
195 "like to manually download it instead?",
196 "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
197 wxLaunchDefaultBrowser(kChangelogUrl);
198 }
199 return;
200 }
201
202 std::filesystem::path newArea = GetExecutableDirectory();
203 std::filesystem::path oldArea = newArea / "old";
204 std::set<std::filesystem::path> folders;
205 std::set<std::filesystem::path> filesToMove;
206 for (const std::filesystem::path& existingFile : files) {
207 std::filesystem::path movedPath = oldArea / existingFile;
208 std::filesystem::path movedDir = movedPath;
209 movedDir.remove_filename();
210 folders.insert(movedDir);
211 filesToMove.insert(existingFile);
212 }
213 for (const std::filesystem::path& existingFile : deletedFiles) {
214 std::filesystem::path movedPath = oldArea / existingFile;
215 std::filesystem::path movedDir = movedPath;
216 movedDir.remove_filename();
217 folders.insert(movedDir);
218 }
219
220 for (const std::filesystem::path& newFolder : folders) {
221 TrackerLog(fmt::format("Creating directory {}", newFolder.string()));
222
223 std::filesystem::create_directories(newFolder);
224 }
225
226 for (const std::filesystem::path& existingFile : files) {
227 std::filesystem::path existingPath = newArea / existingFile;
228
229 if (std::filesystem::is_regular_file(existingPath)) {
230 std::filesystem::path movedPath = oldArea / existingFile;
231
232 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
233 movedPath.string()));
234
235 std::filesystem::rename(existingPath, movedPath);
236 }
237 }
238 for (const std::filesystem::path& existingFile : deletedFiles) {
239 std::filesystem::path existingPath = newArea / existingFile;
240
241 if (std::filesystem::is_regular_file(existingPath)) {
242 std::filesystem::path movedPath = oldArea / existingFile;
243
244 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
245 movedPath.string()));
246
247 std::filesystem::rename(existingPath, movedPath);
248 }
249 }
250
251 wxFileInputStream fileInputStream(package_path.GetFullPath());
252 wxZipInputStream zipStream(fileInputStream);
253 std::unique_ptr<wxZipEntry> zipEntry;
254 while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
255 nullptr) {
256 if (zipEntry->IsDir()) {
257 continue;
258 }
259
260 std::filesystem::path archivePath = zipEntry->GetName().utf8_string();
261
262 TrackerLog(fmt::format("Found {} in archive", archivePath.string()));
263
264 // Cut off the root folder name
265 std::filesystem::path subPath;
266 for (auto it = std::next(archivePath.begin()); it != archivePath.end();
267 it++) {
268 subPath /= *it;
269 }
270
271 std::filesystem::path pastePath = newArea / subPath;
272
273 wxFileOutputStream fileOutput(pastePath.string());
274 zipStream.Read(fileOutput);
275 }
276
277 if (wxMessageBox(
278 "Update installed! The tracker must be restarted for the changes to take "
279 "effect. Do you want to close the tracker?",
280 "Update installed", wxYES_NO) == wxYES) {
281 wxExit();
282 }
283}
284
285bool Updater::DownloadWithProgress(wxWebRequest& request) {
286 request.Start();
287
288 wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
289 100, nullptr,
290 wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
291 wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
292 while (request.GetState() != wxWebRequest::State_Completed &&
293 request.GetState() != wxWebRequest::State_Failed) {
294 if (request.GetBytesExpectedToReceive() == -1) {
295 if (!dialog.Pulse()) {
296 request.Cancel();
297 return false;
298 }
299 } else {
300 dialog.SetRange(request.GetBytesExpectedToReceive());
301 if (!dialog.Update(request.GetBytesReceived())) {
302 request.Cancel();
303 return false;
304 }
305 }
306 }
307
308 return true;
309}
diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..c604a49 --- /dev/null +++ b/src/updater.h
@@ -0,0 +1,46 @@
1#ifndef UPDATER_H_809E7381
2#define UPDATER_H_809E7381
3
4#include <filesystem>
5#include <set>
6#include <string>
7
8#include <wx/wxprec.h>
9
10#ifndef WX_PRECOMP
11#include <wx/wx.h>
12#endif
13
14class wxWebRequest;
15class wxWebRequestEvent;
16
17class Updater : public wxEvtHandler {
18 public:
19 explicit Updater(wxFrame* parent);
20
21 void Cleanup();
22
23 void CheckForUpdates(bool invisible);
24
25 private:
26 enum class UpdateState {
27 GetVersionInvisible,
28 GetVersionManual,
29 GetPackage,
30 };
31
32 void OnWebRequestState(wxWebRequestEvent& event);
33
34 void ProcessVersionFile(std::string data);
35
36 void InstallUpdate(std::string url, std::string checksum,
37 std::vector<std::filesystem::path> files,
38 std::vector<std::filesystem::path> deletedFiles);
39
40 bool DownloadWithProgress(wxWebRequest& request);
41
42 wxFrame* parent_;
43 UpdateState update_state_ = UpdateState::GetVersionInvisible;
44};
45
46#endif /* end of include guard: UPDATER_H_809E7381 */
diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..3b4d5f3 --- /dev/null +++ b/src/version.cpp
@@ -0,0 +1,5 @@
1#include "version.h"
2
3std::ostream& operator<<(std::ostream& out, const Version& ver) {
4 return out << "v" << ver.major << "." << ver.minor << "." << ver.revision;
5}
diff --git a/src/version.h b/src/version.h index 36bd8c1..3439fda 100644 --- a/src/version.h +++ b/src/version.h
@@ -1,9 +1,10 @@
1#ifndef VERSION_H_C757E53C 1#ifndef VERSION_H_C757E53C
2#define VERSION_H_C757E53C 2#define VERSION_H_C757E53C
3 3
4#include <sstream>
5#include <regex> 4#include <regex>
6 5
6#include <fmt/core.h>
7
7struct Version { 8struct Version {
8 int major = 0; 9 int major = 0;
9 int minor = 0; 10 int minor = 0;
@@ -24,9 +25,7 @@ struct Version {
24 } 25 }
25 26
26 std::string ToString() const { 27 std::string ToString() const {
27 std::ostringstream output; 28 return fmt::format("v{}.{}.{}", major, minor, revision);
28 output << "v" << major << "." << minor << "." << revision;
29 return output.str();
30 } 29 }
31 30
32 bool operator<(const Version& rhs) const { 31 bool operator<(const Version& rhs) const {
@@ -37,6 +36,6 @@ struct Version {
37 } 36 }
38}; 37};
39 38
40constexpr const Version kTrackerVersion = Version(0, 9, 0); 39constexpr const Version kTrackerVersion = Version(2, 0, 2);
41 40
42#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"