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.cpp849
-rw-r--r--src/ap_state.h38
-rw-r--r--src/area_popup.cpp196
-rw-r--r--src/area_popup.h38
-rw-r--r--src/connection_dialog.cpp44
-rw-r--r--src/connection_dialog.h6
-rw-r--r--src/game_data.cpp320
-rw-r--r--src/game_data.h26
-rw-r--r--src/global.cpp16
-rw-r--r--src/global.h2
-rw-r--r--src/godot_variant.cpp83
-rw-r--r--src/godot_variant.h28
-rw-r--r--src/icons.cpp22
-rw-r--r--src/icons.h25
-rw-r--r--src/ipc_dialog.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.cpp42
-rw-r--r--src/logger.h6
-rw-r--r--src/main.cpp2
-rw-r--r--src/network_set.cpp29
-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.cpp419
-rw-r--r--src/subway_map.h13
-rw-r--r--src/tracker_config.cpp14
-rw-r--r--src/tracker_config.h10
-rw-r--r--src/tracker_frame.cpp279
-rw-r--r--src/tracker_frame.h89
-rw-r--r--src/tracker_panel.cpp211
-rw-r--r--src/tracker_panel.h20
-rw-r--r--src/tracker_state.cpp251
-rw-r--r--src/tracker_state.h6
-rw-r--r--src/updater.cpp309
-rw-r--r--src/updater.h46
-rw-r--r--src/version.cpp5
-rw-r--r--src/version.h2
-rw-r--r--src/windows.rc3
52 files changed, 3532 insertions, 1092 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 f8d4ee0..8438649 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -10,6 +10,7 @@
10#include <any> 10#include <any>
11#include <apclient.hpp> 11#include <apclient.hpp>
12#include <apuuid.hpp> 12#include <apuuid.hpp>
13#include <bitset>
13#include <chrono> 14#include <chrono>
14#include <exception> 15#include <exception>
15#include <filesystem> 16#include <filesystem>
@@ -22,34 +23,59 @@
22#include <tuple> 23#include <tuple>
23 24
24#include "game_data.h" 25#include "game_data.h"
26#include "ipc_state.h"
25#include "logger.h" 27#include "logger.h"
26#include "tracker_frame.h" 28#include "tracker_frame.h"
27#include "tracker_state.h" 29#include "tracker_state.h"
28 30
29constexpr int AP_MAJOR = 0; 31constexpr int AP_MAJOR = 0;
30constexpr int AP_MINOR = 4; 32constexpr int AP_MINOR = 6;
31constexpr int AP_REVISION = 5; 33constexpr int AP_REVISION = 1;
32 34
33constexpr const char* CERT_STORE_PATH = "cacert.pem"; 35constexpr const char* CERT_STORE_PATH = "cacert.pem";
34constexpr int ITEM_HANDLING = 7; // <- all 36constexpr int ITEM_HANDLING = 7; // <- all
35 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
36namespace { 45namespace {
37 46
38struct APState { 47const std::set<long> kNonProgressionItems = {
39 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};
40 58
59struct APState {
60 // Initialized on main thread
41 bool initialized = false; 61 bool initialized = false;
42
43 TrackerFrame* tracker_frame = nullptr; 62 TrackerFrame* tracker_frame = nullptr;
63 std::list<std::string> tracked_data_storage_keys;
44 64
45 bool client_active = false; 65 // Client
46 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.";
47 73
48 bool connected = false; 74 bool connected = false;
49 bool has_connection_result = false; 75 std::string connection_failure;
76 int remaining_loops = 0;
50 77
51 std::string data_storage_prefix; 78 std::string data_storage_prefix;
52 std::list<std::string> tracked_data_storage_keys;
53 std::string victory_data_storage_key; 79 std::string victory_data_storage_key;
54 80
55 std::string save_name; 81 std::string save_name;
@@ -58,9 +84,12 @@ struct APState {
58 std::set<int64_t> checked_locations; 84 std::set<int64_t> checked_locations;
59 std::map<std::string, std::any> data_storage; 85 std::map<std::string, std::any> data_storage;
60 std::optional<std::tuple<int, int>> player_pos; 86 std::optional<std::tuple<int, int>> player_pos;
87 std::bitset<PANEL_COUNT> solved_panels;
61 88
62 DoorShuffleMode door_shuffle_mode = kNO_DOORS; 89 DoorShuffleMode door_shuffle_mode = kNO_DOORS;
90 bool group_doors = false;
63 bool color_shuffle = false; 91 bool color_shuffle = false;
92 PanelShuffleMode panel_shuffle_mode = kNO_PANELS;
64 bool painting_shuffle = false; 93 bool painting_shuffle = false;
65 int mastery_requirement = 21; 94 int mastery_requirement = 21;
66 int level_2_requirement = 223; 95 int level_2_requirement = 223;
@@ -72,171 +101,387 @@ struct APState {
72 bool pilgrimage_allows_paintings = false; 101 bool pilgrimage_allows_paintings = false;
73 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; 102 SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL;
74 bool sunwarp_shuffle = false; 103 bool sunwarp_shuffle = false;
104 bool postgame_shuffle = true;
75 105
76 std::map<std::string, std::string> painting_mapping; 106 std::map<std::string, std::string> painting_mapping;
77 std::set<std::string> painting_codomain; 107 std::set<std::string> painting_codomain;
78 std::map<int, SunwarpMapping> sunwarp_mapping; 108 std::map<int, SunwarpMapping> sunwarp_mapping;
79 109
80 void Connect(std::string server, std::string player, std::string password) { 110 void Connect(std::string server, std::string player, std::string password) {
111 Initialize();
112
113 {
114 std::lock_guard state_guard(state_mutex);
115 SetStatusMessage("Connecting to Archipelago server....");
116 }
117 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
118
119 // Creating and setting up the client has to all be done while holding the
120 // client mutex, so that the other thread doesn't try to poll before we add
121 // handlers, etc.
122 {
123 TrackerLog("Destroying old AP client...");
124
125 std::lock_guard client_guard(client_mutex);
126
127 if (apclient) {
128 DestroyClient();
129 }
130
131 std::string cert_store = "";
132 if (std::filesystem::exists(CERT_STORE_PATH)) {
133 cert_store = CERT_STORE_PATH;
134 }
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() {
81 if (!initialized) { 288 if (!initialized) {
82 TrackerLog("Initializing APState..."); 289 TrackerLog("Initializing APState...");
83 290
84 std::thread([this]() { 291 std::thread([this]() { Thread(); }).detach();
85 for (;;) { 292
293 for (int i = 0; i < PANEL_BITFIELDS; i++) {
294 tracked_data_storage_keys.push_back(fmt::format("Panels_{}", i));
295 }
296
297 tracked_data_storage_keys.push_back("PlayerPos");
298 tracked_data_storage_keys.push_back("Paintings");
299
300 initialized = true;
301 }
302 }
303
304 void Thread() {
305 std::string display_error;
306
307 for (;;) {
308 {
309 std::lock_guard client_guard(client_mutex);
310 if (apclient) {
311 apclient->poll();
312
86 { 313 {
87 std::lock_guard client_guard(client_mutex); 314 std::lock_guard state_guard(state_mutex);
88 if (apclient) { 315
89 apclient->poll(); 316 if (!connected) {
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 }
90 } 337 }
91 } 338 }
92
93 std::this_thread::sleep_for(std::chrono::milliseconds(100));
94 } 339 }
95 }).detach();
96
97 for (int panel_id : GD_GetAchievementPanels()) {
98 tracked_data_storage_keys.push_back(fmt::format(
99 "Achievement|{}", GD_GetPanel(panel_id).achievement_name));
100 } 340 }
101 341
102 for (const MapArea& map_area : GD_GetMapAreas()) { 342 if (!display_error.empty()) {
103 for (const Location& location : map_area.locations) { 343 wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR);
104 tracked_data_storage_keys.push_back( 344 display_error.clear();
105 fmt::format("Hunt|{}", location.ap_location_id));
106 }
107 } 345 }
108 346
109 tracked_data_storage_keys.push_back("PlayerPos"); 347 std::this_thread::sleep_for(std::chrono::milliseconds(100));
110 tracked_data_storage_keys.push_back("Paintings"); 348 }
349 }
111 350
112 initialized = true; 351 void OnRoomInfo(std::string player, std::string password) {
352 {
353 std::lock_guard state_guard(state_mutex);
354
355 inventory.clear();
356
357 SetStatusMessage("Connected to Archipelago server. Authenticating...");
113 } 358 }
114 359
115 tracker_frame->SetStatusMessage("Connecting to Archipelago server...."); 360 TrackerLog(fmt::format(
116 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server)); 361 "Connected to Archipelago server. Authenticating as {} {}", player,
362 (password.empty() ? "without password" : "with password " + password)));
363
364 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
365 {AP_MAJOR, AP_MINOR, AP_REVISION});
366 }
117 367
368 void OnLocationChecked(const std::list<int64_t>& locations) {
118 { 369 {
119 TrackerLog("Destroying old AP client..."); 370 std::lock_guard state_guard(state_mutex);
120 371
121 std::lock_guard client_guard(client_mutex); 372 for (const int64_t location_id : locations) {
373 checked_locations.insert(location_id);
374 TrackerLog(fmt::format("Location: {}", location_id));
375 }
376 }
122 377
123 if (apclient) { 378 RefreshTracker(StateUpdate{.cleared_locations = true});
124 DestroyClient(); 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 }
125 } 417 }
126 418
127 std::string cert_store = ""; 419 for (const auto& [item_id, item_index] : index_by_item) {
128 if (std::filesystem::exists(CERT_STORE_PATH)) { 420 item_states.push_back(ItemState{.name = GD_GetItemName(item_id),
129 cert_store = CERT_STORE_PATH; 421 .amount = inventory[item_id],
422 .index = item_index});
130 } 423 }
424 }
131 425
132 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, 426 RefreshTracker(StateUpdate{.items = item_states,
133 cert_store); 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 }
134 } 439 }
135 440
136 save_name.clear(); 441 RefreshTracker(state_update);
137 inventory.clear(); 442 }
138 checked_locations.clear();
139 data_storage.clear();
140 player_pos = std::nullopt;
141 victory_data_storage_key.clear();
142 door_shuffle_mode = kNO_DOORS;
143 color_shuffle = false;
144 painting_shuffle = false;
145 painting_mapping.clear();
146 painting_codomain.clear();
147 mastery_requirement = 21;
148 level_2_requirement = 223;
149 location_checks = kNORMAL_LOCATIONS;
150 victory_condition = kTHE_END;
151 early_color_hallways = false;
152 pilgrimage_enabled = false;
153 pilgrimage_allows_roof_access = false;
154 pilgrimage_allows_paintings = false;
155 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
156 sunwarp_shuffle = false;
157 sunwarp_mapping.clear();
158
159 std::mutex connection_mutex;
160 connected = false;
161 has_connection_result = false;
162
163 apclient->set_room_info_handler([this, player, password]() {
164 inventory.clear();
165 443
166 TrackerLog(fmt::format( 444 void OnSetReply(const std::string& key, const nlohmann::json& value) {
167 "Connected to Archipelago server. Authenticating as {} {}", player, 445 StateUpdate state_update;
168 (password.empty() ? "without password"
169 : "with password " + password)));
170 tracker_frame->SetStatusMessage(
171 "Connected to Archipelago server. Authenticating...");
172
173 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
174 {AP_MAJOR, AP_MINOR, AP_REVISION});
175 });
176
177 apclient->set_location_checked_handler(
178 [this](const std::list<int64_t>& locations) {
179 for (const int64_t location_id : locations) {
180 checked_locations.insert(location_id);
181 TrackerLog(fmt::format("Location: {}", location_id));
182 }
183 446
184 RefreshTracker(false); 447 {
185 }); 448 std::lock_guard state_guard(state_mutex);
186 449 HandleDataStorage(key, value, state_update);
187 apclient->set_slot_disconnected_handler([this]() { 450 }
188 tracker_frame->SetStatusMessage(
189 "Disconnected from Archipelago. Attempting to reconnect...");
190 TrackerLog(
191 "Slot disconnected from Archipelago. Attempting to reconnect...");
192 });
193
194 apclient->set_socket_disconnected_handler([this]() {
195 tracker_frame->SetStatusMessage(
196 "Disconnected from Archipelago. Attempting to reconnect...");
197 TrackerLog(
198 "Socket disconnected from Archipelago. Attempting to reconnect...");
199 });
200
201 apclient->set_items_received_handler(
202 [this](const std::list<APClient::NetworkItem>& items) {
203 for (const APClient::NetworkItem& item : items) {
204 inventory[item.item]++;
205 TrackerLog(fmt::format("Item: {}", item.item));
206 }
207 451
208 RefreshTracker(false); 452 RefreshTracker(state_update);
209 }); 453 }
210 454
211 apclient->set_retrieved_handler( 455 void OnSlotConnected(std::string player, std::string server,
212 [this](const std::map<std::string, nlohmann::json>& data) { 456 const nlohmann::json& slot_data) {
213 for (const auto& [key, value] : data) { 457 IPC_SetTrackerSlot(server, player);
214 HandleDataStorage(key, value);
215 }
216 458
217 RefreshTracker(false); 459 TrackerLog("Connected to Archipelago!");
218 });
219 460
220 apclient->set_set_reply_handler([this](const std::string& key, 461 {
221 const nlohmann::json& value, 462 std::lock_guard state_guard(state_mutex);
222 const nlohmann::json&) {
223 HandleDataStorage(key, value);
224 RefreshTracker(false);
225 });
226 463
227 apclient->set_slot_connected_handler([this, player, server, 464 SetStatusMessage(
228 &connection_mutex]( 465 fmt::format("Connected to Archipelago! ({}@{}).", player, server));
229 const nlohmann::json& slot_data) {
230 tracker_frame->SetStatusMessage(
231 fmt::format("Connected to Archipelago! ({}@{})", player, server));
232 TrackerLog("Connected to Archipelago!");
233 466
234 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), 467 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(),
235 apclient->get_player_number()); 468 apclient->get_player_number());
236 data_storage_prefix = 469 data_storage_prefix =
237 fmt::format("Lingo_{}_", apclient->get_player_number()); 470 fmt::format("Lingo_{}_", apclient->get_player_number());
238 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 }
239 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>();
240 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1; 485 painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
241 mastery_requirement = slot_data["mastery_achievements"].get<int>(); 486 mastery_requirement = slot_data["mastery_achievements"].get<int>();
242 level_2_requirement = slot_data["level_2_requirement"].get<int>(); 487 level_2_requirement = slot_data["level_2_requirement"].get<int>();
@@ -258,6 +503,9 @@ struct APState {
258 : kSUNWARP_ACCESS_NORMAL; 503 : kSUNWARP_ACCESS_NORMAL;
259 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && 504 sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
260 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;
261 509
262 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { 510 if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) {
263 painting_mapping.clear(); 511 painting_mapping.clear();
@@ -295,111 +543,87 @@ struct APState {
295 apclient->Get(corrected_keys); 543 apclient->Get(corrected_keys);
296 apclient->SetNotify(corrected_keys); 544 apclient->SetNotify(corrected_keys);
297 545
298 ResetReachabilityRequirements(); 546 connected = true;
299 RefreshTracker(true); 547 }
300
301 {
302 std::lock_guard connection_lock(connection_mutex);
303 if (!has_connection_result) {
304 connected = true;
305 has_connection_result = true;
306 }
307 }
308 });
309
310 apclient->set_slot_refused_handler(
311 [this, &connection_mutex](const std::list<std::string>& errors) {
312 {
313 std::lock_guard connection_lock(connection_mutex);
314 connected = false;
315 has_connection_result = true;
316 }
317
318 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
319
320 std::vector<std::string> error_messages;
321 error_messages.push_back("Could not connect to Archipelago.");
322
323 for (const std::string& error : errors) {
324 if (error == "InvalidSlot") {
325 error_messages.push_back("Invalid player name.");
326 } else if (error == "InvalidGame") {
327 error_messages.push_back(
328 "The specified player is not playing Lingo.");
329 } else if (error == "IncompatibleVersion") {
330 error_messages.push_back(
331 "The Archipelago server is not the correct version for this "
332 "client.");
333 } else if (error == "InvalidPassword") {
334 error_messages.push_back("Incorrect password.");
335 } else if (error == "InvalidItemsHandling") {
336 error_messages.push_back(
337 "Invalid item handling flag. This is a bug with the tracker. "
338 "Please report it to the lingo-ap-tracker GitHub.");
339 } else {
340 error_messages.push_back("Unknown error.");
341 }
342 }
343
344 std::string full_message = hatkirby::implode(error_messages, " ");
345 TrackerLog(full_message);
346
347 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
348 });
349
350 client_active = true;
351
352 int timeout = 5000; // 5 seconds
353 int interval = 100;
354 int remaining_loops = timeout / interval;
355 while (true) {
356 {
357 std::lock_guard connection_lock(connection_mutex);
358 if (has_connection_result) {
359 break;
360 }
361 }
362
363 if (interval == 0) {
364 DestroyClient();
365
366 tracker_frame->SetStatusMessage("Disconnected from Archipelago.");
367 TrackerLog("Timeout while connecting to Archipelago server.");
368 wxMessageBox("Timeout while connecting to Archipelago server.",
369 "Connection failed", wxOK | wxICON_ERROR);
370 548
371 { 549 ResetReachabilityRequirements();
372 std::lock_guard connection_lock(connection_mutex); 550 RefreshTracker(std::nullopt);
373 connected = false; 551 }
374 has_connection_result = true;
375 }
376 552
377 break; 553 void OnSlotRefused(const std::list<std::string>& errors) {
554 std::vector<std::string> error_messages;
555 error_messages.push_back("Could not connect to Archipelago.");
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.");
378 } 574 }
575 }
379 576
380 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, " ");
381 580
382 interval--; 581 SetStatusMessage("Disconnected from Archipelago.");
383 } 582 }
583 }
384 584
385 if (connected) { 585 // Assumes state mutex is locked.
386 client_active = false; 586 void SetStatusMessage(std::string msg) {
387 } 587 status_message = std::move(msg);
588
589 tracker_frame->UpdateStatusMessage();
388 } 590 }
389 591
390 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) {
391 if (value.is_boolean()) { 594 if (value.is_boolean()) {
392 data_storage[key] = value.get<bool>(); 595 data_storage[key] = value.get<bool>();
393 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 596 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
394 (value.get<bool>() ? "true" : "false"))); 597 (value.get<bool>() ? "true" : "false")));
598
395 } else if (value.is_number()) { 599 } else if (value.is_number()) {
396 data_storage[key] = value.get<int>(); 600 data_storage[key] = value.get<int>();
397 TrackerLog(fmt::format("Data storage {} retrieved as {}", key, 601 TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
398 value.get<int>())); 602 value.get<int>()));
603
604 if (key == victory_data_storage_key) {
605 state_update.cleared_locations = true;
606 } else if (key.find("Panels_") != std::string::npos) {
607 int bitfield_num =
608 std::stoi(key.substr(data_storage_prefix.size() + 7));
609 uint64_t bitfield_value = value.get<uint64_t>();
610 for (int i = 0; i < PANEL_BITFIELD_LENGTH; i++) {
611 if ((bitfield_value & (1LL << i)) != 0) {
612 int solve_index = bitfield_num * PANEL_BITFIELD_LENGTH + i;
613
614 if (!solved_panels.test(solve_index)) {
615 state_update.panels.insert(solve_index);
616 }
617
618 solved_panels.set(solve_index);
619 }
620 }
621 }
399 } else if (value.is_object()) { 622 } else if (value.is_object()) {
400 if (key.ends_with("PlayerPos")) { 623 if (key.ends_with("PlayerPos")) {
401 auto map_value = value.get<std::map<std::string, int>>(); 624 auto map_value = value.get<std::map<std::string, int>>();
402 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;
403 } else { 627 } else {
404 data_storage[key] = value.get<std::map<std::string, int>>(); 628 data_storage[key] = value.get<std::map<std::string, int>>();
405 } 629 }
@@ -408,6 +632,7 @@ struct APState {
408 } else if (value.is_null()) { 632 } else if (value.is_null()) {
409 if (key.ends_with("PlayerPos")) { 633 if (key.ends_with("PlayerPos")) {
410 player_pos = std::nullopt; 634 player_pos = std::nullopt;
635 state_update.player_position = true;
411 } else { 636 } else {
412 data_storage.erase(key); 637 data_storage.erase(key);
413 } 638 }
@@ -419,6 +644,8 @@ struct APState {
419 if (key.ends_with("Paintings")) { 644 if (key.ends_with("Paintings")) {
420 data_storage[key] = 645 data_storage[key] =
421 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());
422 } else { 649 } else {
423 data_storage[key] = list_value; 650 data_storage[key] = list_value;
424 } 651 }
@@ -428,74 +655,39 @@ struct APState {
428 } 655 }
429 } 656 }
430 657
431 bool HasCheckedGameLocation(int location_id) { 658 // State mutex should NOT be locked.
432 return checked_locations.count(location_id); 659 // nullopt state_update indicates a reset.
433 } 660 void RefreshTracker(std::optional<StateUpdate> state_update) {
434 661 TrackerLog("Refreshing display...");
435 bool HasCheckedHuntPanel(int location_id) {
436 std::string key =
437 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
438 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
439 }
440
441 bool HasItem(int item_id, int quantity) {
442 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
443 }
444
445 bool HasAchievement(const std::string& name) {
446 std::string key =
447 fmt::format("{}Achievement|{}", data_storage_prefix, name);
448 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
449 }
450
451 const std::set<std::string>& GetCheckedPaintings() {
452 std::string key = fmt::format("{}Paintings", data_storage_prefix);
453 if (!data_storage.count(key)) {
454 data_storage[key] = std::set<std::string>();
455 }
456 662
457 return std::any_cast<const std::set<std::string>&>(data_storage.at(key)); 663 if (!state_update || state_update->progression_items ||
458 } 664 !state_update->paintings.empty()) {
665 std::string prev_msg;
666 {
667 std::lock_guard state_guard(state_mutex);
459 668
460 bool IsPaintingChecked(const std::string& painting_id) { 669 prev_msg = status_message;
461 const auto& checked_paintings = GetCheckedPaintings(); 670 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
671 }
462 672
463 return checked_paintings.count(painting_id) || 673 RecalculateReachability();
464 (painting_mapping.count(painting_id) &&
465 checked_paintings.count(painting_mapping.at(painting_id)));
466 }
467 674
468 void RefreshTracker(bool reset) { 675 {
469 TrackerLog("Refreshing display..."); 676 std::lock_guard state_guard(state_mutex);
470 677
471 RecalculateReachability(); 678 SetStatusMessage(prev_msg);
679 }
680 }
681
472 682
473 if (reset) { 683 if (!state_update) {
474 tracker_frame->ResetIndicators(); 684 tracker_frame->ResetIndicators();
475 } else { 685 } else {
476 tracker_frame->UpdateIndicators(); 686 tracker_frame->UpdateIndicators(*state_update);
477 }
478 }
479
480 int64_t GetItemId(const std::string& item_name) {
481 int64_t ap_id = apclient->get_item_id(item_name);
482 if (ap_id == APClient::INVALID_NAME_ID) {
483 TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
484 } 687 }
485
486 return ap_id;
487 }
488
489 std::string GetItemName(int id) { return apclient->get_item_name(id); }
490
491 bool HasReachedGoal() {
492 return data_storage.count(victory_data_storage_key) &&
493 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
494 30; // CLIENT_GOAL
495 } 688 }
496 689
497 void DestroyClient() { 690 void DestroyClient() {
498 client_active = false;
499 apclient->reset(); 691 apclient->reset();
500 apclient.reset(); 692 apclient.reset();
501 } 693 }
@@ -514,39 +706,63 @@ void AP_Connect(std::string server, std::string player, std::string password) {
514 GetState().Connect(server, player, password); 706 GetState().Connect(server, player, password);
515} 707}
516 708
517std::string AP_GetSaveName() { return GetState().save_name; } 709std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); }
518 710
519bool AP_HasCheckedGameLocation(int location_id) { 711std::string AP_GetSaveName() {
520 return GetState().HasCheckedGameLocation(location_id); 712 std::lock_guard state_guard(GetState().state_mutex);
713
714 return GetState().save_name;
521} 715}
522 716
523bool AP_HasCheckedHuntPanel(int location_id) { 717bool AP_HasCheckedGameLocation(int location_id) {
524 return GetState().HasCheckedHuntPanel(location_id); 718 return GetState().HasCheckedGameLocation(location_id);
525} 719}
526 720
527bool AP_HasItem(int item_id, int quantity) { 721bool AP_HasItem(int item_id, int quantity) {
528 return GetState().HasItem(item_id, quantity); 722 return GetState().HasItem(item_id, quantity);
529} 723}
530 724
531std::string AP_GetItemName(int item_id) { 725bool AP_HasItemSafe(int item_id, int quantity) {
532 return GetState().GetItemName(item_id); 726 return GetState().HasItemSafe(item_id, quantity);
727}
728
729DoorShuffleMode AP_GetDoorShuffleMode() {
730 std::lock_guard state_guard(GetState().state_mutex);
731
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;
533} 739}
534 740
535DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 741bool AP_IsColorShuffle() {
742 std::lock_guard state_guard(GetState().state_mutex);
536 743
537bool AP_IsColorShuffle() { return GetState().color_shuffle; } 744 return GetState().color_shuffle;
745}
538 746
539bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 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);
540 755
541const std::map<std::string, std::string>& AP_GetPaintingMapping() {
542 return GetState().painting_mapping; 756 return GetState().painting_mapping;
543} 757}
544 758
545bool AP_IsPaintingMappedTo(const std::string& painting_id) { 759bool AP_IsPaintingMappedTo(const std::string& painting_id) {
760 std::lock_guard state_guard(GetState().state_mutex);
761
546 return GetState().painting_codomain.count(painting_id); 762 return GetState().painting_codomain.count(painting_id);
547} 763}
548 764
549const std::set<std::string>& AP_GetCheckedPaintings() { 765std::set<std::string> AP_GetCheckedPaintings() {
550 return GetState().GetCheckedPaintings(); 766 return GetState().GetCheckedPaintings();
551} 767}
552 768
@@ -554,11 +770,29 @@ bool AP_IsPaintingChecked(const std::string& painting_id) {
554 return GetState().IsPaintingChecked(painting_id); 770 return GetState().IsPaintingChecked(painting_id);
555} 771}
556 772
557int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 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}
558 780
559int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 781int AP_GetLevel2Requirement() {
782 std::lock_guard state_guard(GetState().state_mutex);
783
784 return GetState().level_2_requirement;
785}
786
787LocationChecks AP_GetLocationsChecks() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
790 return GetState().location_checks;
791}
560 792
561bool AP_IsLocationVisible(int classification) { 793bool AP_IsLocationVisible(int classification) {
794 std::lock_guard state_guard(GetState().state_mutex);
795
562 int world_state = 0; 796 int world_state = 0;
563 797
564 switch (GetState().location_checks) { 798 switch (GetState().location_checks) {
@@ -575,43 +809,76 @@ bool AP_IsLocationVisible(int classification) {
575 return false; 809 return false;
576 } 810 }
577 811
578 if (GetState().door_shuffle_mode && !GetState().early_color_hallways) { 812 if (GetState().door_shuffle_mode == kDOORS_MODE &&
813 !GetState().early_color_hallways) {
579 world_state |= kLOCATION_SMALL_SPHERE_ONE; 814 world_state |= kLOCATION_SMALL_SPHERE_ONE;
580 } 815 }
581 816
582 return (world_state & classification); 817 return (world_state & classification);
583} 818}
584 819
820PanelShuffleMode AP_GetPanelShuffleMode() {
821 std::lock_guard state_guard(GetState().state_mutex);
822
823 return GetState().panel_shuffle_mode;
824}
825
585VictoryCondition AP_GetVictoryCondition() { 826VictoryCondition AP_GetVictoryCondition() {
827 std::lock_guard state_guard(GetState().state_mutex);
828
586 return GetState().victory_condition; 829 return GetState().victory_condition;
587} 830}
588 831
589bool AP_HasAchievement(const std::string& achievement_name) { 832bool AP_HasEarlyColorHallways() {
590 return GetState().HasAchievement(achievement_name); 833 std::lock_guard state_guard(GetState().state_mutex);
834
835 return GetState().early_color_hallways;
591} 836}
592 837
593bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } 838bool AP_IsPilgrimageEnabled() {
839 std::lock_guard state_guard(GetState().state_mutex);
594 840
595bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } 841 return GetState().pilgrimage_enabled;
842}
596 843
597bool AP_DoesPilgrimageAllowRoofAccess() { 844bool AP_DoesPilgrimageAllowRoofAccess() {
845 std::lock_guard state_guard(GetState().state_mutex);
846
598 return GetState().pilgrimage_allows_roof_access; 847 return GetState().pilgrimage_allows_roof_access;
599} 848}
600 849
601bool AP_DoesPilgrimageAllowPaintings() { 850bool AP_DoesPilgrimageAllowPaintings() {
851 std::lock_guard state_guard(GetState().state_mutex);
852
602 return GetState().pilgrimage_allows_paintings; 853 return GetState().pilgrimage_allows_paintings;
603} 854}
604 855
605SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } 856SunwarpAccess AP_GetSunwarpAccess() {
857 std::lock_guard state_guard(GetState().state_mutex);
858
859 return GetState().sunwarp_access;
860}
606 861
607bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } 862bool AP_IsSunwarpShuffle() {
863 std::lock_guard state_guard(GetState().state_mutex);
864
865 return GetState().sunwarp_shuffle;
866}
608 867
609const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { 868std::map<int, SunwarpMapping> AP_GetSunwarpMapping() {
610 return GetState().sunwarp_mapping; 869 return GetState().sunwarp_mapping;
611} 870}
612 871
872bool AP_IsPostgameShuffle() { return GetState().postgame_shuffle; }
873
613bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 874bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
614 875
615std::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
616 return GetState().player_pos; 879 return GetState().player_pos;
617} 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 f8936e5..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,43 +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
54std::string AP_GetStatusMessage();
55
46std::string AP_GetSaveName(); 56std::string AP_GetSaveName();
47 57
48bool AP_HasCheckedGameLocation(int location_id); 58bool AP_HasCheckedGameLocation(int location_id);
49 59
50bool AP_HasCheckedHuntPanel(int location_id); 60// This doesn't lock the state mutex, for speed, so it must ONLY be called from
51 61// RecalculateReachability, which is only called from the APState thread anyway.
52bool AP_HasItem(int item_id, int quantity = 1); 62bool AP_HasItem(int item_id, int quantity = 1);
53 63
54std::string AP_GetItemName(int item_id); 64bool AP_HasItemSafe(int item_id, int quantity = 1);
55 65
56DoorShuffleMode AP_GetDoorShuffleMode(); 66DoorShuffleMode AP_GetDoorShuffleMode();
57 67
68bool AP_AreDoorsGrouped();
69
58bool AP_IsColorShuffle(); 70bool AP_IsColorShuffle();
59 71
60bool AP_IsPaintingShuffle(); 72bool AP_IsPaintingShuffle();
61 73
62const std::map<std::string, std::string>& AP_GetPaintingMapping(); 74std::map<std::string, std::string> AP_GetPaintingMapping();
63 75
64bool AP_IsPaintingMappedTo(const std::string& painting_id); 76bool AP_IsPaintingMappedTo(const std::string& painting_id);
65 77
66const std::set<std::string>& AP_GetCheckedPaintings(); 78std::set<std::string> AP_GetCheckedPaintings();
67 79
68bool AP_IsPaintingChecked(const std::string& painting_id); 80bool AP_IsPaintingChecked(const std::string& painting_id);
69 81
82void AP_RevealPaintings();
83
70int AP_GetMasteryRequirement(); 84int AP_GetMasteryRequirement();
71 85
72int AP_GetLevel2Requirement(); 86int AP_GetLevel2Requirement();
73 87
88LocationChecks AP_GetLocationsChecks();
89
74bool AP_IsLocationVisible(int classification); 90bool AP_IsLocationVisible(int classification);
75 91
76VictoryCondition AP_GetVictoryCondition(); 92PanelShuffleMode AP_GetPanelShuffleMode();
77 93
78bool AP_HasAchievement(const std::string& achievement_name); 94VictoryCondition AP_GetVictoryCondition();
79 95
80bool AP_HasEarlyColorHallways(); 96bool AP_HasEarlyColorHallways();
81 97
@@ -89,10 +105,14 @@ SunwarpAccess AP_GetSunwarpAccess();
89 105
90bool AP_IsSunwarpShuffle(); 106bool AP_IsSunwarpShuffle();
91 107
92const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); 108std::map<int, SunwarpMapping> AP_GetSunwarpMapping();
109
110bool AP_IsPostgameShuffle();
93 111
94bool AP_HasReachedGoal(); 112bool AP_HasReachedGoal();
95 113
96std::optional<std::tuple<int, int>> AP_GetPlayerPosition(); 114std::optional<std::tuple<int, int>> AP_GetPlayerPosition();
97 115
116bool AP_IsPanelSolved(int solve_index);
117
98#endif /* end of include guard: AP_STATE_H_664A4180 */ 118#endif /* end of include guard: AP_STATE_H_664A4180 */
diff --git a/src/area_popup.cpp b/src/area_popup.cpp index 8d6487e..c95e492 100644 --- a/src/area_popup.cpp +++ b/src/area_popup.cpp
@@ -7,6 +7,7 @@
7#include "ap_state.h" 7#include "ap_state.h"
8#include "game_data.h" 8#include "game_data.h"
9#include "global.h" 9#include "global.h"
10#include "icons.h"
10#include "tracker_config.h" 11#include "tracker_config.h"
11#include "tracker_panel.h" 12#include "tracker_panel.h"
12#include "tracker_state.h" 13#include "tracker_state.h"
@@ -15,60 +16,54 @@ AreaPopup::AreaPopup(wxWindow* parent, int area_id)
15 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) { 16 : wxScrolledCanvas(parent, wxID_ANY), area_id_(area_id) {
16 SetBackgroundStyle(wxBG_STYLE_PAINT); 17 SetBackgroundStyle(wxBG_STYLE_PAINT);
17 18
18 unchecked_eye_ = 19 LoadIcons();
19 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
20 wxBITMAP_TYPE_PNG)
21 .Scale(32, 32));
22 checked_eye_ = wxBitmap(
23 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
24 .Scale(32, 32));
25 20
21 // TODO: This is slow on high-DPI screens.
26 SetScrollRate(5, 5); 22 SetScrollRate(5, 5);
27 23
28 SetBackgroundColour(*wxBLACK); 24 SetBackgroundColour(*wxBLACK);
29 Hide(); 25 Hide();
30 26
31 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this); 27 Bind(wxEVT_PAINT, &AreaPopup::OnPaint, this);
28 Bind(wxEVT_DPI_CHANGED, &AreaPopup::OnDPIChanged, this);
32 29
33 UpdateIndicators(); 30 ResetIndicators();
34} 31}
35 32
36void AreaPopup::UpdateIndicators() { 33void AreaPopup::ResetIndicators() {
34 indicators_.clear();
35
37 const MapArea& map_area = GD_GetMapArea(area_id_); 36 const MapArea& map_area = GD_GetMapArea(area_id_);
37 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
38 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
38 39
39 // Start calculating extents. 40 // Start calculating extents.
40 wxMemoryDC mem_dc; 41 wxMemoryDC mem_dc;
41 mem_dc.SetFont(GetFont().Bold()); 42 mem_dc.SetFont(the_font.Bold());
42 wxSize header_extent = mem_dc.GetTextExtent(map_area.name); 43 header_extent_ = mem_dc.GetTextExtent(map_area.name);
43 44
44 int acc_height = header_extent.GetHeight() + 20; 45 int acc_height = header_extent_.GetHeight() + FromDIP(20);
45 int col_width = 0; 46 int col_width = 0;
46 47
47 mem_dc.SetFont(GetFont()); 48 mem_dc.SetFont(the_font);
48
49 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
50
51 std::vector<int> real_locations;
52 49
53 for (int section_id = 0; section_id < map_area.locations.size(); 50 for (int section_id = 0; section_id < map_area.locations.size();
54 section_id++) { 51 section_id++) {
55 const Location& location = map_area.locations.at(section_id); 52 const Location& location = map_area.locations.at(section_id);
56 53 if ((!AP_IsLocationVisible(location.classification) ||
57 if (tracker_panel->IsPanelsMode()) { 54 IsLocationPostgame(location.ap_location_id)) &&
58 if (!location.single_panel) { 55 !(location.hunt &&
59 continue; 56 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
60 } 57 !(location.single_panel &&
61 } else { 58 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS)) {
62 if (!AP_IsLocationVisible(location.classification) && 59 continue;
63 !(location.hunt && GetTrackerConfig().show_hunt_panels)) {
64 continue;
65 }
66 } 60 }
67 61
68 real_locations.push_back(section_id); 62 indicators_.emplace_back(section_id, kLOCATION, acc_height);
69 63
70 wxSize item_extent = mem_dc.GetTextExtent(location.name); 64 wxSize item_extent = mem_dc.GetTextExtent(location.name);
71 int item_height = std::max(32, item_extent.GetHeight()) + 10; 65 int item_height =
66 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
72 acc_height += item_height; 67 acc_height += item_height;
73 68
74 if (item_extent.GetWidth() > col_width) { 69 if (item_extent.GetWidth() > col_width) {
@@ -76,11 +71,18 @@ void AreaPopup::UpdateIndicators() {
76 } 71 }
77 } 72 }
78 73
79 if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { 74 if (AP_IsPaintingShuffle()) {
80 for (int painting_id : map_area.paintings) { 75 for (int painting_id : map_area.paintings) {
76 if (IsPaintingPostgame(painting_id)) {
77 continue;
78 }
79
80 indicators_.emplace_back(painting_id, kPAINTING, acc_height);
81
81 const PaintingExit& painting = GD_GetPaintingExit(painting_id); 82 const PaintingExit& painting = GD_GetPaintingExit(painting_id);
82 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with a friendly name. 83 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
83 int item_height = std::max(32, item_extent.GetHeight()) + 10; 84 int item_height =
85 std::max(FromDIP(32), item_extent.GetHeight()) + FromDIP(10);
84 acc_height += item_height; 86 acc_height += item_height;
85 87
86 if (item_extent.GetWidth() > col_width) { 88 if (item_extent.GetWidth() > col_width) {
@@ -89,80 +91,86 @@ void AreaPopup::UpdateIndicators() {
89 } 91 }
90 } 92 }
91 93
92 int item_width = col_width + 10 + 32; 94 int item_width = col_width + FromDIP(10 + 32);
93 int full_width = std::max(header_extent.GetWidth(), item_width) + 20; 95 full_width_ = std::max(header_extent_.GetWidth(), item_width) + FromDIP(20);
96 full_height_ = acc_height;
94 97
95 Fit(); 98 Fit();
96 SetVirtualSize(full_width, acc_height); 99 SetVirtualSize(full_width_, full_height_);
100
101 UpdateIndicators();
102}
103
104void AreaPopup::UpdateIndicators() {
105 const MapArea& map_area = GD_GetMapArea(area_id_);
106 wxFont the_font = GetFont().Scale(GetDPIScaleFactor());
107 TrackerPanel* tracker_panel = dynamic_cast<TrackerPanel*>(GetParent());
108
109 rendered_ = wxBitmap(full_width_, full_height_);
97 110
98 rendered_ = wxBitmap(full_width, acc_height); 111 wxMemoryDC mem_dc;
99 mem_dc.SelectObject(rendered_); 112 mem_dc.SelectObject(rendered_);
100 mem_dc.SetPen(*wxTRANSPARENT_PEN); 113 mem_dc.SetPen(*wxTRANSPARENT_PEN);
101 mem_dc.SetBrush(*wxBLACK_BRUSH); 114 mem_dc.SetBrush(*wxBLACK_BRUSH);
102 mem_dc.DrawRectangle({0, 0}, {full_width, acc_height}); 115 mem_dc.DrawRectangle({0, 0}, {full_width_, full_height_});
103 116
104 mem_dc.SetFont(GetFont().Bold()); 117 mem_dc.SetFont(the_font.Bold());
105 mem_dc.SetTextForeground(*wxWHITE); 118 mem_dc.SetTextForeground(*wxWHITE);
106 mem_dc.DrawText(map_area.name, 119 mem_dc.DrawText(map_area.name,
107 {(full_width - header_extent.GetWidth()) / 2, 10}); 120 {(full_width_ - header_extent_.GetWidth()) / 2, FromDIP(10)});
108 121
109 int cur_height = header_extent.GetHeight() + 20; 122 mem_dc.SetFont(the_font);
110 123
111 mem_dc.SetFont(GetFont()); 124 for (const IndicatorInfo& indicator : indicators_) {
125 switch (indicator.type) {
126 case kLOCATION: {
127 const Location& location = map_area.locations.at(indicator.id);
112 128
113 for (int section_id : real_locations) { 129 bool checked = false;
114 const Location& location = map_area.locations.at(section_id); 130 if (IsLocationWinCondition(location)) {
131 checked = AP_HasReachedGoal();
132 } else {
133 checked = AP_HasCheckedGameLocation(location.ap_location_id) ||
134 (location.single_panel &&
135 AP_IsPanelSolved(
136 GD_GetPanel(*location.single_panel).solve_index));
137 }
115 138
116 bool checked = false; 139 const wxBitmap* eye_ptr = checked ? checked_eye_ : unchecked_eye_;
117 if (IsLocationWinCondition(location)) {
118 checked = AP_HasReachedGoal();
119 } else if (tracker_panel->IsPanelsMode()) {
120 const Panel& panel = GD_GetPanel(*location.single_panel);
121 if (panel.non_counting) {
122 checked = AP_HasCheckedGameLocation(location.ap_location_id);
123 } else {
124 checked = tracker_panel->GetSolvedPanels().contains(panel.nodepath);
125 }
126 } else {
127 checked =
128 AP_HasCheckedGameLocation(location.ap_location_id) ||
129 (location.hunt && AP_HasCheckedHuntPanel(location.ap_location_id));
130 }
131
132 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_;
133 140
134 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 141 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
135 142
136 bool reachable = IsLocationReachable(location.ap_location_id); 143 bool reachable = IsLocationReachable(location.ap_location_id);
137 const wxColour* text_color = reachable ? wxWHITE : wxRED; 144 const wxColour* text_color = reachable ? wxWHITE : wxRED;
138 mem_dc.SetTextForeground(*text_color); 145 mem_dc.SetTextForeground(*text_color);
139 146
140 wxSize item_extent = mem_dc.GetTextExtent(location.name); 147 wxSize item_extent = mem_dc.GetTextExtent(location.name);
141 mem_dc.DrawText( 148 mem_dc.DrawText(
142 location.name, 149 location.name,
143 {10 + 32 + 10, cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 150 {FromDIP(10 + 32 + 10),
144 151 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
145 cur_height += 10 + 32;
146 }
147 152
148 if (AP_IsPaintingShuffle() && !tracker_panel->IsPanelsMode()) { 153 break;
149 for (int painting_id : map_area.paintings) { 154 }
150 const PaintingExit& painting = GD_GetPaintingExit(painting_id); 155 case kPAINTING: {
156 const PaintingExit& painting = GD_GetPaintingExit(indicator.id);
151 157
152 bool reachable = IsPaintingReachable(painting_id); 158 bool reachable = IsPaintingReachable(indicator.id);
153 const wxColour* text_color = reachable ? wxWHITE : wxRED; 159 const wxColour* text_color = reachable ? wxWHITE : wxRED;
154 mem_dc.SetTextForeground(*text_color); 160 mem_dc.SetTextForeground(*text_color);
155 161
156 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id); 162 bool checked = reachable && AP_IsPaintingChecked(painting.internal_id);
157 wxBitmap* eye_ptr = checked ? &checked_eye_ : &unchecked_eye_; 163 const wxBitmap* eye_ptr = checked ? checked_owl_ : unchecked_owl_;
158 mem_dc.DrawBitmap(*eye_ptr, {10, cur_height}); 164 mem_dc.DrawBitmap(*eye_ptr, {FromDIP(10), indicator.y});
159 165
160 wxSize item_extent = mem_dc.GetTextExtent(painting.internal_id); // TODO: Replace with friendly name. 166 wxSize item_extent = mem_dc.GetTextExtent(painting.display_name);
161 mem_dc.DrawText(painting.internal_id, 167 mem_dc.DrawText(
162 {10 + 32 + 10, 168 painting.display_name,
163 cur_height + (32 - mem_dc.GetFontMetrics().height) / 2}); 169 {FromDIP(10 + 32 + 10),
170 indicator.y + (FromDIP(32) - mem_dc.GetFontMetrics().height) / 2});
164 171
165 cur_height += 10 + 32; 172 break;
173 }
166 } 174 }
167 } 175 }
168} 176}
@@ -174,3 +182,21 @@ void AreaPopup::OnPaint(wxPaintEvent& event) {
174 182
175 event.Skip(); 183 event.Skip();
176} 184}
185
186void AreaPopup::OnDPIChanged(wxDPIChangedEvent& event) {
187 LoadIcons();
188 ResetIndicators();
189
190 event.Skip();
191}
192
193void AreaPopup::LoadIcons() {
194 unchecked_eye_ = GetTheIconCache().GetIcon("assets/unchecked.png",
195 FromDIP(wxSize{32, 32}));
196 checked_eye_ =
197 GetTheIconCache().GetIcon("assets/checked.png", FromDIP(wxSize{32, 32}));
198 unchecked_owl_ =
199 GetTheIconCache().GetIcon("assets/owl.png", FromDIP(wxSize{32, 32}));
200 checked_owl_ = GetTheIconCache().GetIcon("assets/checked_owl.png",
201 FromDIP(wxSize{32, 32}));
202}
diff --git a/src/area_popup.h b/src/area_popup.h index 00c644d..f8a2355 100644 --- a/src/area_popup.h +++ b/src/area_popup.h
@@ -7,19 +7,53 @@
7#include <wx/wx.h> 7#include <wx/wx.h>
8#endif 8#endif
9 9
10#include <vector>
11
10class AreaPopup : public wxScrolledCanvas { 12class AreaPopup : public wxScrolledCanvas {
11 public: 13 public:
12 AreaPopup(wxWindow* parent, int area_id); 14 AreaPopup(wxWindow* parent, int area_id);
13 15
16 void ResetIndicators();
14 void UpdateIndicators(); 17 void UpdateIndicators();
15 18
19 int GetFullWidth() const { return full_width_; }
20 int GetFullHeight() const { return full_height_; }
21
16 private: 22 private:
23 enum IndicatorType {
24 kLOCATION,
25 kPAINTING,
26 };
27
28 struct IndicatorInfo {
29 // For locations, the id is an index into the map area's locations list.
30 // For paintings, it is a real painting id.
31 int id;
32 IndicatorType type;
33 int y;
34
35 IndicatorInfo(int id, IndicatorType type, int y)
36 : id(id), type(type), y(y) {}
37 };
38
17 void OnPaint(wxPaintEvent& event); 39 void OnPaint(wxPaintEvent& event);
40 void OnDPIChanged(wxDPIChangedEvent& event);
41
42 void LoadIcons();
18 43
19 int area_id_; 44 int area_id_;
20 45
21 wxBitmap unchecked_eye_; 46 const wxBitmap* unchecked_eye_;
22 wxBitmap checked_eye_; 47 const wxBitmap* checked_eye_;
48 const wxBitmap* unchecked_owl_;
49 const wxBitmap* checked_owl_;
50
51 int full_width_ = 0;
52 int full_height_ = 0;
53 wxSize header_extent_;
54
55 std::vector<IndicatorInfo> indicators_;
56
23 wxBitmap rendered_; 57 wxBitmap rendered_;
24}; 58};
25 59
diff --git a/src/connection_dialog.cpp b/src/connection_dialog.cpp index 64fee98..b55a138 100644 --- a/src/connection_dialog.cpp +++ b/src/connection_dialog.cpp
@@ -4,17 +4,21 @@
4 4
5ConnectionDialog::ConnectionDialog() 5ConnectionDialog::ConnectionDialog()
6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") { 6 : wxDialog(nullptr, wxID_ANY, "Connect to Archipelago") {
7 server_box_ = 7 server_box_ = new wxTextCtrl(
8 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_server, 8 this, -1,
9 wxDefaultPosition, {300, -1}); 9 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_server),
10 player_box_ = 10 wxDefaultPosition, FromDIP(wxSize{300, -1}));
11 new wxTextCtrl(this, -1, GetTrackerConfig().connection_details.ap_player, 11 player_box_ = new wxTextCtrl(
12 wxDefaultPosition, {300, -1}); 12 this, -1,
13 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_player),
14 wxDefaultPosition, FromDIP(wxSize{300, -1}));
13 password_box_ = new wxTextCtrl( 15 password_box_ = new wxTextCtrl(
14 this, -1, GetTrackerConfig().connection_details.ap_password, 16 this, -1,
15 wxDefaultPosition, {300, -1}); 17 wxString::FromUTF8(GetTrackerConfig().connection_details.ap_password),
18 wxDefaultPosition, FromDIP(wxSize{300, -1}));
16 19
17 wxFlexGridSizer* form_sizer = new wxFlexGridSizer(2, 10, 10); 20 wxFlexGridSizer* form_sizer =
21 new wxFlexGridSizer(2, FromDIP(10), FromDIP(10));
18 22
19 form_sizer->Add( 23 form_sizer->Add(
20 new wxStaticText(this, -1, "Server:"), 24 new wxStaticText(this, -1, "Server:"),
@@ -30,17 +34,19 @@ ConnectionDialog::ConnectionDialog()
30 form_sizer->Add(password_box_, wxSizerFlags().Expand()); 34 form_sizer->Add(password_box_, wxSizerFlags().Expand());
31 35
32 history_list_ = new wxListBox(this, -1); 36 history_list_ = new wxListBox(this, -1);
33 for (const ConnectionDetails& details : GetTrackerConfig().connection_history) { 37 for (const ConnectionDetails& details :
38 GetTrackerConfig().connection_history) {
34 wxString display_text; 39 wxString display_text;
35 display_text << details.ap_player; 40 display_text << wxString::FromUTF8(details.ap_player);
36 display_text << " ("; 41 display_text << " (";
37 display_text << details.ap_server; 42 display_text << wxString::FromUTF8(details.ap_server);
38 display_text << ")"; 43 display_text << ")";
39 44
40 history_list_->Append(display_text); 45 history_list_->Append(display_text);
41 } 46 }
42 47
43 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen, this); 48 history_list_->Bind(wxEVT_LISTBOX, &ConnectionDialog::OnOldConnectionChosen,
49 this);
44 50
45 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL); 51 wxBoxSizer* mid_sizer = new wxBoxSizer(wxHORIZONTAL);
46 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand()); 52 mid_sizer->Add(form_sizer, wxSizerFlags().Proportion(3).Expand());
@@ -52,7 +58,8 @@ ConnectionDialog::ConnectionDialog()
52 this, -1, "Enter the details to connect to Archipelago."), 58 this, -1, "Enter the details to connect to Archipelago."),
53 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder()); 59 wxSizerFlags().Align(wxALIGN_LEFT).DoubleBorder());
54 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand()); 60 top_sizer->Add(mid_sizer, wxSizerFlags().DoubleBorder().Expand());
55 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL), wxSizerFlags().Border().Center()); 61 top_sizer->Add(CreateButtonSizer(wxOK | wxCANCEL),
62 wxSizerFlags().Border().Center());
56 63
57 SetSizerAndFit(top_sizer); 64 SetSizerAndFit(top_sizer);
58 65
@@ -62,9 +69,10 @@ ConnectionDialog::ConnectionDialog()
62 69
63void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) { 70void ConnectionDialog::OnOldConnectionChosen(wxCommandEvent& e) {
64 if (e.IsSelection()) { 71 if (e.IsSelection()) {
65 const ConnectionDetails& details = GetTrackerConfig().connection_history.at(e.GetSelection()); 72 const ConnectionDetails& details =
66 server_box_->SetValue(details.ap_server); 73 GetTrackerConfig().connection_history.at(e.GetSelection());
67 player_box_->SetValue(details.ap_player); 74 server_box_->SetValue(wxString::FromUTF8(details.ap_server));
68 password_box_->SetValue(details.ap_password); 75 player_box_->SetValue(wxString::FromUTF8(details.ap_player));
76 password_box_->SetValue(wxString::FromUTF8(details.ap_password));
69 } 77 }
70} 78}
diff --git a/src/connection_dialog.h b/src/connection_dialog.h index 9fe62fd..ec2ee72 100644 --- a/src/connection_dialog.h +++ b/src/connection_dialog.h
@@ -14,12 +14,12 @@ class ConnectionDialog : public wxDialog {
14 public: 14 public:
15 ConnectionDialog(); 15 ConnectionDialog();
16 16
17 std::string GetServerValue() { return server_box_->GetValue().ToStdString(); } 17 std::string GetServerValue() { return server_box_->GetValue().utf8_string(); }
18 18
19 std::string GetPlayerValue() { return player_box_->GetValue().ToStdString(); } 19 std::string GetPlayerValue() { return player_box_->GetValue().utf8_string(); }
20 20
21 std::string GetPasswordValue() { 21 std::string GetPasswordValue() {
22 return password_box_->GetValue().ToStdString(); 22 return password_box_->GetValue().utf8_string();
23 } 23 }
24 24
25 private: 25 private:
diff --git a/src/game_data.cpp b/src/game_data.cpp index c39e239..94b9888 100644 --- a/src/game_data.cpp +++ b/src/game_data.cpp
@@ -12,36 +12,11 @@
12 12
13namespace { 13namespace {
14 14
15LingoColor GetColorForString(const std::string &str) {
16 if (str == "black") {
17 return LingoColor::kBlack;
18 } else if (str == "red") {
19 return LingoColor::kRed;
20 } else if (str == "blue") {
21 return LingoColor::kBlue;
22 } else if (str == "yellow") {
23 return LingoColor::kYellow;
24 } else if (str == "orange") {
25 return LingoColor::kOrange;
26 } else if (str == "green") {
27 return LingoColor::kGreen;
28 } else if (str == "gray") {
29 return LingoColor::kGray;
30 } else if (str == "brown") {
31 return LingoColor::kBrown;
32 } else if (str == "purple") {
33 return LingoColor::kPurple;
34 } else {
35 TrackerLog(fmt::format("Invalid color: {}", str));
36
37 return LingoColor::kNone;
38 }
39}
40
41struct GameData { 15struct GameData {
42 std::vector<Room> rooms_; 16 std::vector<Room> rooms_;
43 std::vector<Door> doors_; 17 std::vector<Door> doors_;
44 std::vector<Panel> panels_; 18 std::vector<Panel> panels_;
19 std::vector<PanelDoor> panel_doors_;
45 std::vector<MapArea> map_areas_; 20 std::vector<MapArea> map_areas_;
46 std::vector<SubwayItem> subway_items_; 21 std::vector<SubwayItem> subway_items_;
47 std::vector<PaintingExit> paintings_; 22 std::vector<PaintingExit> paintings_;
@@ -49,6 +24,7 @@ struct GameData {
49 std::map<std::string, int> room_by_id_; 24 std::map<std::string, int> room_by_id_;
50 std::map<std::string, int> door_by_id_; 25 std::map<std::string, int> door_by_id_;
51 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_;
52 std::map<std::string, int> area_by_id_; 28 std::map<std::string, int> area_by_id_;
53 std::map<std::string, int> painting_by_id_; 29 std::map<std::string, int> painting_by_id_;
54 30
@@ -56,6 +32,7 @@ struct GameData {
56 32
57 std::map<std::string, int> room_by_painting_; 33 std::map<std::string, int> room_by_painting_;
58 std::map<int, int> room_by_sunwarp_; 34 std::map<int, int> room_by_sunwarp_;
35 std::map<int, int> panel_by_solve_index_;
59 36
60 std::vector<int> achievement_panels_; 37 std::vector<int> achievement_panels_;
61 38
@@ -66,6 +43,8 @@ struct GameData {
66 std::map<std::string, int> subway_item_by_painting_; 43 std::map<std::string, int> subway_item_by_painting_;
67 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_; 44 std::map<SubwaySunwarp, int> subway_item_by_sunwarp_;
68 45
46 std::map<int, std::string> item_by_ap_id_;
47
69 bool loaded_area_data_ = false; 48 bool loaded_area_data_ = false;
70 std::set<std::string> malconfigured_areas_; 49 std::set<std::string> malconfigured_areas_;
71 50
@@ -81,7 +60,7 @@ struct GameData {
81 ids_config["special_items"][color_name]) { 60 ids_config["special_items"][color_name]) {
82 std::string input_name = color_name; 61 std::string input_name = color_name;
83 input_name[0] = std::tolower(input_name[0]); 62 input_name[0] = std::tolower(input_name[0]);
84 ap_id_by_color_[GetColorForString(input_name)] = 63 ap_id_by_color_[GetLingoColorForString(input_name)] =
85 ids_config["special_items"][color_name].as<int>(); 64 ids_config["special_items"][color_name].as<int>();
86 } else { 65 } else {
87 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name)); 66 TrackerLog(fmt::format("Missing AP item ID for color {}", color_name));
@@ -98,8 +77,18 @@ struct GameData {
98 init_color_id("Brown"); 77 init_color_id("Brown");
99 init_color_id("Gray"); 78 init_color_id("Gray");
100 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
101 rooms_.reserve(lingo_config.size() * 2); 88 rooms_.reserve(lingo_config.size() * 2);
102 89
90 std::vector<int> panel_location_ids;
91
103 for (const auto &room_it : lingo_config) { 92 for (const auto &room_it : lingo_config) {
104 int room_id = AddOrGetRoom(room_it.first.as<std::string>()); 93 int room_id = AddOrGetRoom(room_it.first.as<std::string>());
105 94
@@ -177,12 +166,12 @@ struct GameData {
177 166
178 if (panel_it.second["colors"]) { 167 if (panel_it.second["colors"]) {
179 if (panel_it.second["colors"].IsScalar()) { 168 if (panel_it.second["colors"].IsScalar()) {
180 panels_[panel_id].colors.push_back(GetColorForString( 169 panels_[panel_id].colors.push_back(GetLingoColorForString(
181 panel_it.second["colors"].as<std::string>())); 170 panel_it.second["colors"].as<std::string>()));
182 } else { 171 } else {
183 for (const auto &color_node : panel_it.second["colors"]) { 172 for (const auto &color_node : panel_it.second["colors"]) {
184 panels_[panel_id].colors.push_back( 173 panels_[panel_id].colors.push_back(
185 GetColorForString(color_node.as<std::string>())); 174 GetLingoColorForString(color_node.as<std::string>()));
186 } 175 }
187 } 176 }
188 } 177 }
@@ -288,10 +277,11 @@ struct GameData {
288 ids_config["panels"][rooms_[room_id].name] && 277 ids_config["panels"][rooms_[room_id].name] &&
289 ids_config["panels"][rooms_[room_id].name] 278 ids_config["panels"][rooms_[room_id].name]
290 [panels_[panel_id].name]) { 279 [panels_[panel_id].name]) {
291 panels_[panel_id].ap_location_id = 280 int location_id = ids_config["panels"][rooms_[room_id].name]
292 ids_config["panels"][rooms_[room_id].name] 281 [panels_[panel_id].name]
293 [panels_[panel_id].name] 282 .as<int>();
294 .as<int>(); 283 panels_[panel_id].ap_location_id = location_id;
284 panel_location_ids.push_back(location_id);
295 } else { 285 } else {
296 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}", 286 TrackerLog(fmt::format("Missing AP location ID for panel {} - {}",
297 rooms_[room_id].name, 287 rooms_[room_id].name,
@@ -357,6 +347,9 @@ struct GameData {
357 ids_config["doors"][rooms_[room_id].name] 347 ids_config["doors"][rooms_[room_id].name]
358 [doors_[door_id].name]["item"] 348 [doors_[door_id].name]["item"]
359 .as<int>(); 349 .as<int>();
350
351 item_by_ap_id_[doors_[door_id].ap_item_id] =
352 doors_[door_id].item_name;
360 } else { 353 } else {
361 TrackerLog(fmt::format("Missing AP item ID for door {} - {}", 354 TrackerLog(fmt::format("Missing AP item ID for door {} - {}",
362 rooms_[room_id].name, 355 rooms_[room_id].name,
@@ -373,6 +366,9 @@ struct GameData {
373 doors_[door_id].group_ap_item_id = 366 doors_[door_id].group_ap_item_id =
374 ids_config["door_groups"][doors_[door_id].group_name] 367 ids_config["door_groups"][doors_[door_id].group_name]
375 .as<int>(); 368 .as<int>();
369
370 item_by_ap_id_[doors_[door_id].group_ap_item_id] =
371 doors_[door_id].group_name;
376 } else { 372 } else {
377 TrackerLog(fmt::format("Missing AP item ID for door group {}", 373 TrackerLog(fmt::format("Missing AP item ID for door group {}",
378 doors_[door_id].group_name)); 374 doors_[door_id].group_name));
@@ -430,6 +426,90 @@ struct GameData {
430 } 426 }
431 } 427 }
432 428
429 if (room_it.second["panel_doors"]) {
430 for (const auto &panel_door_it : room_it.second["panel_doors"]) {
431 std::string panel_door_name = panel_door_it.first.as<std::string>();
432 int panel_door_id =
433 AddOrGetPanelDoor(rooms_[room_id].name, panel_door_name);
434
435 std::map<std::string, std::vector<std::string>> panel_per_room;
436 int num_panels = 0;
437 for (const auto &panel_node : panel_door_it.second["panels"]) {
438 num_panels++;
439
440 int panel_id = -1;
441
442 if (panel_node.IsScalar()) {
443 panel_id = AddOrGetPanel(rooms_[room_id].name,
444 panel_node.as<std::string>());
445
446 panel_per_room[rooms_[room_id].name].push_back(
447 panel_node.as<std::string>());
448 } else {
449 panel_id = AddOrGetPanel(panel_node["room"].as<std::string>(),
450 panel_node["panel"].as<std::string>());
451
452 panel_per_room[panel_node["room"].as<std::string>()].push_back(
453 panel_node["panel"].as<std::string>());
454 }
455
456 Panel &panel = panels_[panel_id];
457 panel.panel_door = panel_door_id;
458 }
459
460 if (panel_door_it.second["item_name"]) {
461 panel_doors_[panel_door_id].item_name =
462 panel_door_it.second["item_name"].as<std::string>();
463 } else {
464 std::vector<std::string> room_strs;
465 for (const auto &[room_str, panels_str] : panel_per_room) {
466 room_strs.push_back(fmt::format(
467 "{} - {}", room_str, hatkirby::implode(panels_str, ", ")));
468 }
469
470 if (num_panels == 1) {
471 panel_doors_[panel_door_id].item_name =
472 fmt::format("{} (Panel)", room_strs[0]);
473 } else {
474 panel_doors_[panel_door_id].item_name = fmt::format(
475 "{} (Panels)", hatkirby::implode(room_strs, " and "));
476 }
477 }
478
479 if (ids_config["panel_doors"] &&
480 ids_config["panel_doors"][rooms_[room_id].name] &&
481 ids_config["panel_doors"][rooms_[room_id].name]
482 [panel_door_name]) {
483 panel_doors_[panel_door_id].ap_item_id =
484 ids_config["panel_doors"][rooms_[room_id].name][panel_door_name]
485 .as<int>();
486
487 item_by_ap_id_[panel_doors_[panel_door_id].ap_item_id] =
488 panel_doors_[panel_door_id].item_name;
489 } else {
490 TrackerLog(fmt::format("Missing AP item ID for panel door {} - {}",
491 rooms_[room_id].name, panel_door_name));
492 }
493
494 if (panel_door_it.second["panel_group"]) {
495 std::string panel_group =
496 panel_door_it.second["panel_group"].as<std::string>();
497
498 if (ids_config["panel_groups"] &&
499 ids_config["panel_groups"][panel_group]) {
500 panel_doors_[panel_door_id].group_ap_item_id =
501 ids_config["panel_groups"][panel_group].as<int>();
502
503 item_by_ap_id_[panel_doors_[panel_door_id].group_ap_item_id] =
504 panel_group;
505 } else {
506 TrackerLog(fmt::format(
507 "Missing AP item ID for panel door group {}", panel_group));
508 }
509 }
510 }
511 }
512
433 if (room_it.second["paintings"]) { 513 if (room_it.second["paintings"]) {
434 for (const auto &painting : room_it.second["paintings"]) { 514 for (const auto &painting : room_it.second["paintings"]) {
435 std::string internal_id = painting["id"].as<std::string>(); 515 std::string internal_id = painting["id"].as<std::string>();
@@ -437,6 +517,13 @@ struct GameData {
437 PaintingExit &painting_exit = paintings_[painting_id]; 517 PaintingExit &painting_exit = paintings_[painting_id];
438 painting_exit.room = room_id; 518 painting_exit.room = room_id;
439 519
520 if (painting["display_name"]) {
521 painting_exit.display_name =
522 painting["display_name"].as<std::string>();
523 } else {
524 painting_exit.display_name = painting_exit.internal_id;
525 }
526
440 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) && 527 if ((!painting["exit_only"] || !painting["exit_only"].as<bool>()) &&
441 (!painting["disable"] || !painting["disable"].as<bool>())) { 528 (!painting["disable"] || !painting["disable"].as<bool>())) {
442 painting_exit.entrance = true; 529 painting_exit.entrance = true;
@@ -478,33 +565,74 @@ struct GameData {
478 ids_config["progression"][progressive_item_name]) { 565 ids_config["progression"][progressive_item_name]) {
479 progressive_item_id = 566 progressive_item_id =
480 ids_config["progression"][progressive_item_name].as<int>(); 567 ids_config["progression"][progressive_item_name].as<int>();
568
569 item_by_ap_id_[progressive_item_id] = progressive_item_name;
481 } else { 570 } else {
482 TrackerLog(fmt::format("Missing AP item ID for progressive item {}", 571 TrackerLog(fmt::format("Missing AP item ID for progressive item {}",
483 progressive_item_name)); 572 progressive_item_name));
484 } 573 }
485 574
486 int index = 1; 575 if (progression_it.second["doors"]) {
487 for (const auto &stage : progression_it.second) { 576 int index = 1;
488 int door_id = -1; 577 for (const auto &stage : progression_it.second["doors"]) {
578 int door_id = -1;
579
580 if (stage.IsScalar()) {
581 door_id =
582 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>());
583 } else {
584 door_id = AddOrGetDoor(stage["room"].as<std::string>(),
585 stage["door"].as<std::string>());
586 }
489 587
490 if (stage.IsScalar()) { 588 doors_[door_id].progressives.push_back(
491 door_id = 589 {.item_name = progressive_item_name,
492 AddOrGetDoor(rooms_[room_id].name, stage.as<std::string>()); 590 .ap_item_id = progressive_item_id,
493 } else { 591 .quantity = index});
494 door_id = AddOrGetDoor(stage["room"].as<std::string>(), 592 index++;
495 stage["door"].as<std::string>());
496 } 593 }
594 }
595
596 if (progression_it.second["panel_doors"]) {
597 int index = 1;
598 for (const auto &stage : progression_it.second["panel_doors"]) {
599 int panel_door_id = -1;
600
601 if (stage.IsScalar()) {
602 panel_door_id = AddOrGetPanelDoor(rooms_[room_id].name,
603 stage.as<std::string>());
604 } else {
605 panel_door_id =
606 AddOrGetPanelDoor(stage["room"].as<std::string>(),
607 stage["panel_door"].as<std::string>());
608 }
497 609
498 doors_[door_id].progressives.push_back( 610 panel_doors_[panel_door_id].progressives.push_back(
499 {.item_name = progressive_item_name, 611 {.item_name = progressive_item_name,
500 .ap_item_id = progressive_item_id, 612 .ap_item_id = progressive_item_id,
501 .quantity = index}); 613 .quantity = index});
502 index++; 614 index++;
615 }
503 } 616 }
504 } 617 }
505 } 618 }
506 } 619 }
507 620
621 // Determine the panel solve indices from the sorted location IDs.
622 std::sort(panel_location_ids.begin(), panel_location_ids.end());
623
624 std::map<int, int> solve_index_by_location_id;
625 for (int i = 0; i < panel_location_ids.size(); i++) {
626 solve_index_by_location_id[panel_location_ids[i]] = i;
627 }
628
629 for (Panel &panel : panels_) {
630 if (panel.ap_location_id != -1) {
631 panel.solve_index = solve_index_by_location_id[panel.ap_location_id];
632 panel_by_solve_index_[panel.solve_index] = panel.id;
633 }
634 }
635
508 map_areas_.reserve(areas_config.size()); 636 map_areas_.reserve(areas_config.size());
509 637
510 std::map<std::string, int> fold_areas; 638 std::map<std::string, int> fold_areas;
@@ -525,7 +653,7 @@ struct GameData {
525 // Only locations for the panels are kept here. 653 // Only locations for the panels are kept here.
526 std::map<std::string, std::tuple<int, int>> locations_by_name; 654 std::map<std::string, std::tuple<int, int>> locations_by_name;
527 655
528 for (const Panel &panel : panels_) { 656 for (Panel &panel : panels_) {
529 int room_id = panel.room; 657 int room_id = panel.room;
530 std::string room_name = rooms_[room_id].name; 658 std::string room_name = rooms_[room_id].name;
531 659
@@ -541,6 +669,8 @@ struct GameData {
541 area_name = location_name.substr(0, divider_pos); 669 area_name = location_name.substr(0, divider_pos);
542 section_name = location_name.substr(divider_pos + 3); 670 section_name = location_name.substr(divider_pos + 3);
543 } 671 }
672 } else {
673 panel.location_name = location_name;
544 } 674 }
545 675
546 if (fold_areas.count(area_name)) { 676 if (fold_areas.count(area_name)) {
@@ -639,7 +769,8 @@ struct GameData {
639 MapArea &map_area = map_areas_[area_id]; 769 MapArea &map_area = map_areas_[area_id];
640 770
641 for (int painting_id : room.paintings) { 771 for (int painting_id : room.paintings) {
642 const PaintingExit &painting_obj = paintings_.at(painting_id); 772 PaintingExit &painting_obj = paintings_.at(painting_id);
773 painting_obj.map_area = area_id;
643 if (painting_obj.entrance) { 774 if (painting_obj.entrance) {
644 map_area.paintings.push_back(painting_id); 775 map_area.paintings.push_back(painting_id);
645 } 776 }
@@ -666,13 +797,10 @@ struct GameData {
666 subway_it["door"].as<std::string>()); 797 subway_it["door"].as<std::string>());
667 } 798 }
668 799
669 if (subway_it["paintings"]) { 800 if (subway_it["painting"]) {
670 for (const auto &painting_it : subway_it["paintings"]) { 801 std::string painting_id = subway_it["painting"].as<std::string>();
671 std::string painting_id = painting_it.as<std::string>(); 802 subway_item.painting = painting_id;
672 803 subway_item_by_painting_[painting_id] = subway_item.id;
673 subway_item.paintings.push_back(painting_id);
674 subway_item_by_painting_[painting_id] = subway_item.id;
675 }
676 } 804 }
677 805
678 if (subway_it["tags"]) { 806 if (subway_it["tags"]) {
@@ -681,6 +809,18 @@ struct GameData {
681 } 809 }
682 } 810 }
683 811
812 if (subway_it["entrances"]) {
813 for (const auto &entrance_it : subway_it["entrances"]) {
814 subway_item.entrances.push_back(entrance_it.as<std::string>());
815 }
816 }
817
818 if (subway_it["exits"]) {
819 for (const auto &exit_it : subway_it["exits"]) {
820 subway_item.exits.push_back(exit_it.as<std::string>());
821 }
822 }
823
684 if (subway_it["sunwarp"]) { 824 if (subway_it["sunwarp"]) {
685 SubwaySunwarp sunwarp; 825 SubwaySunwarp sunwarp;
686 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>(); 826 sunwarp.dots = subway_it["sunwarp"]["dots"].as<int>();
@@ -707,6 +847,10 @@ struct GameData {
707 subway_item.special = subway_it["special"].as<std::string>(); 847 subway_item.special = subway_it["special"].as<std::string>();
708 } 848 }
709 849
850 if (subway_it["tilted"]) {
851 subway_item.tilted = subway_it["tilted"].as<bool>();
852 }
853
710 subway_items_.push_back(subway_item); 854 subway_items_.push_back(subway_item);
711 } 855 }
712 856
@@ -760,6 +904,18 @@ struct GameData {
760 return panel_by_id_[full_name]; 904 return panel_by_id_[full_name];
761 } 905 }
762 906
907 int AddOrGetPanelDoor(std::string room, std::string panel) {
908 std::string full_name = room + " - " + panel;
909
910 if (!panel_doors_by_id_.count(full_name)) {
911 int panel_door_id = panel_doors_.size();
912 panel_doors_by_id_[full_name] = panel_door_id;
913 panel_doors_.push_back({});
914 }
915
916 return panel_doors_by_id_[full_name];
917 }
918
763 int AddOrGetArea(std::string area) { 919 int AddOrGetArea(std::string area) {
764 if (!area_by_id_.count(area)) { 920 if (!area_by_id_.count(area)) {
765 if (loaded_area_data_) { 921 if (loaded_area_data_) {
@@ -792,6 +948,11 @@ GameData &GetState() {
792 948
793} // namespace 949} // namespace
794 950
951bool SubwayItem::HasWarps() const {
952 return !(this->tags.empty() && this->entrances.empty() &&
953 this->exits.empty());
954}
955
795bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const { 956bool SubwaySunwarp::operator<(const SubwaySunwarp &rhs) const {
796 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type); 957 return std::tie(dots, type) < std::tie(rhs.dots, rhs.type);
797} 958}
@@ -810,6 +971,10 @@ const std::vector<Door> &GD_GetDoors() { return GetState().doors_; }
810 971
811const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); } 972const Door &GD_GetDoor(int door_id) { return GetState().doors_.at(door_id); }
812 973
974const PanelDoor &GD_GetPanelDoor(int panel_door_id) {
975 return GetState().panel_doors_.at(panel_door_id);
976}
977
813int GD_GetDoorByName(const std::string &name) { 978int GD_GetDoorByName(const std::string &name) {
814 return GetState().door_by_id_.at(name); 979 return GetState().door_by_id_.at(name);
815} 980}
@@ -818,6 +983,14 @@ const Panel &GD_GetPanel(int panel_id) {
818 return GetState().panels_.at(panel_id); 983 return GetState().panels_.at(panel_id);
819} 984}
820 985
986int GD_GetPanelBySolveIndex(int solve_index) {
987 return GetState().panel_by_solve_index_.at(solve_index);
988}
989
990const std::vector<PaintingExit> &GD_GetPaintings() {
991 return GetState().paintings_;
992}
993
821const PaintingExit &GD_GetPaintingExit(int painting_id) { 994const PaintingExit &GD_GetPaintingExit(int painting_id) {
822 return GetState().paintings_.at(painting_id); 995 return GetState().paintings_.at(painting_id);
823} 996}
@@ -860,3 +1033,38 @@ std::optional<int> GD_GetSubwayItemForPainting(const std::string &painting_id) {
860int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) { 1033int GD_GetSubwayItemForSunwarp(const SubwaySunwarp &sunwarp) {
861 return GetState().subway_item_by_sunwarp_.at(sunwarp); 1034 return GetState().subway_item_by_sunwarp_.at(sunwarp);
862} 1035}
1036
1037std::string GD_GetItemName(int id) {
1038 auto it = GetState().item_by_ap_id_.find(id);
1039 if (it != GetState().item_by_ap_id_.end()) {
1040 return it->second;
1041 } else {
1042 return "Unknown";
1043 }
1044}
1045
1046LingoColor GetLingoColorForString(const std::string &str) {
1047 if (str == "black") {
1048 return LingoColor::kBlack;
1049 } else if (str == "red") {
1050 return LingoColor::kRed;
1051 } else if (str == "blue") {
1052 return LingoColor::kBlue;
1053 } else if (str == "yellow") {
1054 return LingoColor::kYellow;
1055 } else if (str == "orange") {
1056 return LingoColor::kOrange;
1057 } else if (str == "green") {
1058 return LingoColor::kGreen;
1059 } else if (str == "gray") {
1060 return LingoColor::kGray;
1061 } else if (str == "brown") {
1062 return LingoColor::kBrown;
1063 } else if (str == "purple") {
1064 return LingoColor::kPurple;
1065 } else {
1066 TrackerLog(fmt::format("Invalid color: {}", str));
1067
1068 return LingoColor::kNone;
1069 }
1070}
diff --git a/src/game_data.h b/src/game_data.h index 3179365..ac911e5 100644 --- a/src/game_data.h +++ b/src/game_data.h
@@ -56,6 +56,8 @@ struct Panel {
56 bool non_counting = false; 56 bool non_counting = false;
57 int ap_location_id = -1; 57 int ap_location_id = -1;
58 bool hunt = false; 58 bool hunt = false;
59 int panel_door = -1;
60 int solve_index = -1;
59}; 61};
60 62
61struct ProgressiveRequirement { 63struct ProgressiveRequirement {
@@ -83,6 +85,13 @@ struct Door {
83 DoorType type = DoorType::kNormal; 85 DoorType type = DoorType::kNormal;
84}; 86};
85 87
88struct PanelDoor {
89 int ap_item_id = -1;
90 int group_ap_item_id = -1;
91 std::vector<ProgressiveRequirement> progressives;
92 std::string item_name;
93};
94
86struct Exit { 95struct Exit {
87 int source_room; 96 int source_room;
88 int destination_room; 97 int destination_room;
@@ -94,8 +103,10 @@ struct PaintingExit {
94 int id; 103 int id;
95 int room; 104 int room;
96 std::string internal_id; 105 std::string internal_id;
106 std::string display_name;
97 std::optional<int> door; 107 std::optional<int> door;
98 bool entrance = false; 108 bool entrance = false;
109 int map_area;
99}; 110};
100 111
101struct Room { 112struct Room {
@@ -146,11 +157,16 @@ struct SubwayItem {
146 int id; 157 int id;
147 int x; 158 int x;
148 int y; 159 int y;
160 bool tilted = false;
149 std::optional<int> door; 161 std::optional<int> door;
150 std::vector<std::string> paintings; 162 std::optional<std::string> painting;
151 std::vector<std::string> tags; 163 std::vector<std::string> tags; // 2-way teleports
164 std::vector<std::string> entrances; // teleport entrances
165 std::vector<std::string> exits; // teleport exits
152 std::optional<SubwaySunwarp> sunwarp; 166 std::optional<SubwaySunwarp> sunwarp;
153 std::optional<std::string> special; 167 std::optional<std::string> special;
168
169 bool HasWarps() const;
154}; 170};
155 171
156const std::vector<MapArea>& GD_GetMapAreas(); 172const std::vector<MapArea>& GD_GetMapAreas();
@@ -161,6 +177,9 @@ const std::vector<Door>& GD_GetDoors();
161const Door& GD_GetDoor(int door_id); 177const Door& GD_GetDoor(int door_id);
162int GD_GetDoorByName(const std::string& name); 178int GD_GetDoorByName(const std::string& name);
163const Panel& GD_GetPanel(int panel_id); 179const Panel& GD_GetPanel(int panel_id);
180int GD_GetPanelBySolveIndex(int solve_index);
181const PanelDoor& GD_GetPanelDoor(int panel_door_id);
182const std::vector<PaintingExit>& GD_GetPaintings();
164const PaintingExit& GD_GetPaintingExit(int painting_id); 183const PaintingExit& GD_GetPaintingExit(int painting_id);
165int GD_GetPaintingByName(const std::string& name); 184int GD_GetPaintingByName(const std::string& name);
166const std::vector<int>& GD_GetAchievementPanels(); 185const std::vector<int>& GD_GetAchievementPanels();
@@ -171,5 +190,8 @@ const std::vector<SubwayItem>& GD_GetSubwayItems();
171const SubwayItem& GD_GetSubwayItem(int id); 190const SubwayItem& GD_GetSubwayItem(int id);
172std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id); 191std::optional<int> GD_GetSubwayItemForPainting(const std::string& painting_id);
173int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp); 192int GD_GetSubwayItemForSunwarp(const SubwaySunwarp& sunwarp);
193std::string GD_GetItemName(int id);
194
195LingoColor GetLingoColorForString(const std::string& str);
174 196
175#endif /* end of include guard: GAME_DATA_H_9C42AC51 */ 197#endif /* end of include guard: GAME_DATA_H_9C42AC51 */
diff --git a/src/global.cpp b/src/global.cpp index 1eb3f8d..63f4a19 100644 --- a/src/global.cpp +++ b/src/global.cpp
@@ -26,17 +26,19 @@ std::string GetAbsolutePath(std::string_view path) {
26 return (GetExecutableDirectory() / path).string(); 26 return (GetExecutableDirectory() / path).string();
27} 27}
28 28
29bool IsLocationWinCondition(const Location& location) { 29std::string GetWinCondition() {
30 switch (AP_GetVictoryCondition()) { 30 switch (AP_GetVictoryCondition()) {
31 case kTHE_END: 31 case kTHE_END:
32 return location.ap_location_name == 32 return "Orange Tower Seventh Floor - THE END";
33 "Orange Tower Seventh Floor - THE END";
34 case kTHE_MASTER: 33 case kTHE_MASTER:
35 return location.ap_location_name == 34 return "Orange Tower Seventh Floor - THE MASTER";
36 "Orange Tower Seventh Floor - THE MASTER";
37 case kLEVEL_2: 35 case kLEVEL_2:
38 return location.ap_location_name == "Second Room - LEVEL 2"; 36 return "Second Room - LEVEL 2";
39 case kPILGRIMAGE: 37 case kPILGRIMAGE:
40 return location.ap_location_name == "Pilgrim Antechamber - PILGRIM"; 38 return "Pilgrim Antechamber - PILGRIM";
41 } 39 }
42} 40}
41
42bool IsLocationWinCondition(const Location& location) {
43 return location.ap_location_name == GetWinCondition();
44}
diff --git a/src/global.h b/src/global.h index 31ebde3..bdfa7ae 100644 --- a/src/global.h +++ b/src/global.h
@@ -10,6 +10,8 @@ const std::filesystem::path& GetExecutableDirectory();
10 10
11std::string GetAbsolutePath(std::string_view path); 11std::string GetAbsolutePath(std::string_view path);
12 12
13std::string GetWinCondition();
14
13bool IsLocationWinCondition(const Location& location); 15bool IsLocationWinCondition(const Location& location);
14 16
15#endif /* end of include guard: GLOBAL_H_44945DBA */ 17#endif /* end of include guard: GLOBAL_H_44945DBA */
diff --git a/src/godot_variant.cpp b/src/godot_variant.cpp deleted file mode 100644 index 1bc906f..0000000 --- a/src/godot_variant.cpp +++ /dev/null
@@ -1,83 +0,0 @@
1// Godot save decoder algorithm by Chris Souvey.
2
3#include "godot_variant.h"
4
5#include <algorithm>
6#include <charconv>
7#include <cstddef>
8#include <fstream>
9#include <string>
10#include <tuple>
11#include <variant>
12#include <vector>
13
14namespace {
15
16uint16_t ReadUint16(std::basic_istream<char>& stream) {
17 uint16_t result;
18 stream.read(reinterpret_cast<char*>(&result), 2);
19 return result;
20}
21
22uint32_t ReadUint32(std::basic_istream<char>& stream) {
23 uint32_t result;
24 stream.read(reinterpret_cast<char*>(&result), 4);
25 return result;
26}
27
28GodotVariant ParseVariant(std::basic_istream<char>& stream) {
29 uint16_t type = ReadUint16(stream);
30 stream.ignore(2);
31
32 switch (type) {
33 case 1: {
34 // bool
35 bool boolval = (ReadUint32(stream) == 1);
36 return {boolval};
37 }
38 case 15: {
39 // nodepath
40 uint32_t name_length = ReadUint32(stream) & 0x7fffffff;
41 uint32_t subname_length = ReadUint32(stream) & 0x7fffffff;
42 uint32_t flags = ReadUint32(stream);
43
44 std::vector<std::string> result;
45 for (size_t i = 0; i < name_length + subname_length; i++) {
46 uint32_t char_length = ReadUint32(stream);
47 uint32_t padded_length = (char_length % 4 == 0)
48 ? char_length
49 : (char_length + 4 - (char_length % 4));
50 std::vector<char> next_bytes(padded_length);
51 stream.read(next_bytes.data(), padded_length);
52 std::string next_piece;
53 std::copy(next_bytes.begin(),
54 std::next(next_bytes.begin(), char_length),
55 std::back_inserter(next_piece));
56 result.push_back(next_piece);
57 }
58
59 return {result};
60 }
61 case 19: {
62 // array
63 uint32_t length = ReadUint32(stream) & 0x7fffffff;
64 std::vector<GodotVariant> result;
65 for (size_t i = 0; i < length; i++) {
66 result.push_back(ParseVariant(stream));
67 }
68 return {result};
69 }
70 default: {
71 // eh
72 return {std::monostate{}};
73 }
74 }
75}
76
77} // namespace
78
79GodotVariant ParseGodotFile(std::string filename) {
80 std::ifstream file_stream(filename, std::ios_base::binary);
81 file_stream.ignore(4);
82 return ParseVariant(file_stream);
83}
diff --git a/src/godot_variant.h b/src/godot_variant.h deleted file mode 100644 index 620e569..0000000 --- a/src/godot_variant.h +++ /dev/null
@@ -1,28 +0,0 @@
1#ifndef GODOT_VARIANT_H_ED7F2EB6
2#define GODOT_VARIANT_H_ED7F2EB6
3
4#include <string>
5#include <variant>
6#include <vector>
7
8struct GodotVariant {
9 using value_type = std::variant<std::monostate, bool, std::vector<std::string>, std::vector<GodotVariant>>;
10
11 value_type value;
12
13 GodotVariant(value_type v) : value(v) {}
14
15 bool AsBool() const { return std::get<bool>(value); }
16
17 const std::vector<std::string>& AsNodePath() const {
18 return std::get<std::vector<std::string>>(value);
19 }
20
21 const std::vector<GodotVariant>& AsArray() const {
22 return std::get<std::vector<GodotVariant>>(value);
23 }
24};
25
26GodotVariant ParseGodotFile(std::string filename);
27
28#endif /* end of include guard: GODOT_VARIANT_H_ED7F2EB6 */
diff --git a/src/icons.cpp b/src/icons.cpp new file mode 100644 index 0000000..87ba037 --- /dev/null +++ b/src/icons.cpp
@@ -0,0 +1,22 @@
1#include "icons.h"
2
3#include "global.h"
4
5const wxBitmap* IconCache::GetIcon(const std::string& filename, wxSize size) {
6 std::tuple<std::string, int, int> key = {filename, size.x, size.y};
7
8 if (!icons_.count(key)) {
9 icons_[key] =
10 wxBitmap(wxImage(GetAbsolutePath(filename).c_str(),
11 wxBITMAP_TYPE_PNG)
12 .Scale(size.x, size.y));
13 }
14
15 return &icons_.at(key);
16}
17
18static IconCache* ICON_CACHE_INSTANCE = nullptr;
19
20void SetTheIconCache(IconCache* instance) { ICON_CACHE_INSTANCE = instance; }
21
22IconCache& GetTheIconCache() { return *ICON_CACHE_INSTANCE; }
diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 0000000..23dca2a --- /dev/null +++ b/src/icons.h
@@ -0,0 +1,25 @@
1#ifndef ICONS_H_B95159A6
2#define ICONS_H_B95159A6
3
4#include <wx/wxprec.h>
5
6#ifndef WX_PRECOMP
7#include <wx/wx.h>
8#endif
9
10#include <map>
11#include <string>
12#include <tuple>
13
14class IconCache {
15 public:
16 const wxBitmap* GetIcon(const std::string& filename, wxSize size);
17
18 private:
19 std::map<std::tuple<std::string, int, int>, wxBitmap> icons_;
20};
21
22void SetTheIconCache(IconCache* instance);
23IconCache& GetTheIconCache();
24
25#endif /* end of include guard: ICONS_H_B95159A6 */
diff --git a/src/ipc_dialog.cpp b/src/ipc_dialog.cpp 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 index 09fc331..8a08b58 100644 --- a/src/logger.cpp +++ b/src/logger.cpp
@@ -3,8 +3,10 @@
3#include <chrono> 3#include <chrono>
4#include <fstream> 4#include <fstream>
5#include <mutex> 5#include <mutex>
6#include <sstream>
6 7
7#include "global.h" 8#include "global.h"
9#include "log_dialog.h"
8 10
9namespace { 11namespace {
10 12
@@ -14,19 +16,49 @@ class Logger {
14 16
15 void LogLine(const std::string& text) { 17 void LogLine(const std::string& text) {
16 std::lock_guard guard(file_mutex_); 18 std::lock_guard guard(file_mutex_);
17 logfile_ << "[" << std::chrono::system_clock::now() << "] " << text 19 std::ostringstream line;
18 << std::endl; 20 line << "[" << std::chrono::system_clock::now() << "] " << text;
21
22 logfile_ << line.str() << std::endl;
19 logfile_.flush(); 23 logfile_.flush();
24
25 if (log_dialog_ != nullptr) {
26 log_dialog_->LogMessage(line.str());
27 }
28 }
29
30 std::string GetContents() {
31 std::lock_guard guard(file_mutex_);
32
33 std::ifstream file_in(GetAbsolutePath("debug.log"));
34 std::ostringstream buffer;
35 buffer << file_in.rdbuf();
36
37 return buffer.str();
38 }
39
40 void SetLogDialog(LogDialog* log_dialog) {
41 std::lock_guard guard(file_mutex_);
42 log_dialog_ = log_dialog;
20 } 43 }
21 44
22 private: 45 private:
23 std::ofstream logfile_; 46 std::ofstream logfile_;
24 std::mutex file_mutex_; 47 std::mutex file_mutex_;
48 LogDialog* log_dialog_ = nullptr;
25}; 49};
26 50
51Logger& GetLogger() {
52 static Logger* instance = new Logger();
53 return *instance;
54}
55
27} // namespace 56} // namespace
28 57
29void TrackerLog(std::string text) { 58void TrackerLog(std::string text) { GetLogger().LogLine(text); }
30 static Logger* instance = new Logger(); 59
31 instance->LogLine(text); 60std::string TrackerReadPastLog() { return GetLogger().GetContents(); }
61
62void TrackerSetLogDialog(LogDialog* log_dialog) {
63 GetLogger().SetLogDialog(log_dialog);
32} 64}
diff --git a/src/logger.h b/src/logger.h index a27839f..f669790 100644 --- a/src/logger.h +++ b/src/logger.h
@@ -3,6 +3,12 @@
3 3
4#include <string> 4#include <string>
5 5
6class LogDialog;
7
6void TrackerLog(std::string message); 8void TrackerLog(std::string message);
7 9
10std::string TrackerReadPastLog();
11
12void TrackerSetLogDialog(LogDialog* log_dialog);
13
8#endif /* end of include guard: LOGGER_H_9BDD07EA */ 14#endif /* end of include guard: LOGGER_H_9BDD07EA */
diff --git a/src/main.cpp b/src/main.cpp index 1d7cc9e..574b6df 100644 --- a/src/main.cpp +++ b/src/main.cpp
@@ -10,7 +10,7 @@
10 10
11class TrackerApp : public wxApp { 11class TrackerApp : public wxApp {
12 public: 12 public:
13 virtual bool OnInit() { 13 virtual bool OnInit() override {
14 GetTrackerConfig().Load(); 14 GetTrackerConfig().Load();
15 15
16 TrackerFrame *frame = new TrackerFrame(); 16 TrackerFrame *frame = new TrackerFrame();
diff --git a/src/network_set.cpp b/src/network_set.cpp index 2a9e12c..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,13 +16,14 @@ 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);
22} 23}
23 24
24void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2) { 25void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2, bool two_way) {
25 if (id2 > id1) { 26 if (two_way && id2 > id1) {
26 // Make sure id1 < id2
27 std::swap(id1, id2); 27 std::swap(id1, id2);
28 } 28 }
29 29
@@ -31,13 +31,22 @@ void NetworkSet::AddLinkToNetwork(int network_id, int id1, int id2) {
31 network_by_item_[network_id] = {}; 31 network_by_item_[network_id] = {};
32 } 32 }
33 33
34 network_by_item_[network_id].insert({id1, id2}); 34 NetworkNode node = {id1, id2, two_way};
35
36 network_by_item_[network_id].insert(node);
35} 37}
36 38
37bool NetworkSet::IsItemInNetwork(int id) const { 39bool NetworkSet::IsItemInNetwork(int id) const {
38 return network_by_item_.count(id); 40 return network_by_item_.count(id);
39} 41}
40 42
41const std::set<std::pair<int, int>>& NetworkSet::GetNetworkGraph(int id) const { 43const std::set<NetworkNode>& NetworkSet::GetNetworkGraph(int id) const {
42 return network_by_item_.at(id); 44 return network_by_item_.at(id);
43} 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 cec3f39..0f72052 100644 --- a/src/network_set.h +++ b/src/network_set.h
@@ -7,21 +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);
15 23
16 void AddLinkToNetwork(int network_id, int id1, int id2); 24 void AddLinkToNetwork(int network_id, int id1, int id2, bool two_way);
17 25
18 bool IsItemInNetwork(int id) const; 26 bool IsItemInNetwork(int id) const;
19 27
20 const std::set<std::pair<int, int>>& GetNetworkGraph(int id) const; 28 const std::set<NetworkNode>& GetNetworkGraph(int id) const;
21 29
22 private: 30 private:
23 31
24 std::map<int, std::set<std::pair<int, int>>> network_by_item_; 32 std::map<int, std::set<NetworkNode>> network_by_item_;
25}; 33};
26 34
27#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 5b3ff5f..55ac411 100644 --- a/src/subway_map.cpp +++ b/src/subway_map.cpp
@@ -9,31 +9,25 @@
9#include "ap_state.h" 9#include "ap_state.h"
10#include "game_data.h" 10#include "game_data.h"
11#include "global.h" 11#include "global.h"
12#include "report_popup.h"
12#include "tracker_state.h" 13#include "tracker_state.h"
13 14
14constexpr int AREA_ACTUAL_SIZE = 21; 15constexpr int AREA_ACTUAL_SIZE = 21;
15constexpr int OWL_ACTUAL_SIZE = 32; 16constexpr int OWL_ACTUAL_SIZE = 32;
17constexpr int PAINTING_RADIUS = 9; // the actual circles on the map are radius 11
18constexpr int PAINTING_EXIT_RADIUS = 6;
16 19
17enum class ItemDrawType { kNone, kBox, kOwl }; 20enum class ItemDrawType { kNone, kBox, kOwl, kOwlExit };
18 21
19namespace { 22namespace {
20 23
21std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item) { 24wxPoint GetSubwayItemMapCenter(const SubwayItem &subway_item) {
22 if (AP_IsSunwarpShuffle() && subway_item.sunwarp && 25 if (subway_item.painting) {
23 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) { 26 return {subway_item.x, subway_item.y};
24 int sunwarp_index = subway_item.sunwarp->dots - 1; 27 } else {
25 if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) { 28 return {subway_item.x + AREA_ACTUAL_SIZE / 2,
26 sunwarp_index += 6; 29 subway_item.y + AREA_ACTUAL_SIZE / 2};
27 }
28
29 for (const auto &[start_index, mapping] : AP_GetSunwarpMapping()) {
30 if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
31 return GD_GetSunwarpDoors().at(mapping.dots - 1);
32 }
33 }
34 } 30 }
35
36 return subway_item.door;
37} 31}
38 32
39} // namespace 33} // namespace
@@ -53,14 +47,6 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
53 return; 47 return;
54 } 48 }
55 49
56 unchecked_eye_ =
57 wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
58 wxBITMAP_TYPE_PNG)
59 .Scale(32, 32));
60 checked_eye_ = wxBitmap(
61 wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
62 .Scale(32, 32));
63
64 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>( 50 tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
65 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()), 51 quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
66 static_cast<float>(map_image_.GetHeight())}); 52 static_cast<float>(map_image_.GetHeight())});
@@ -79,50 +65,72 @@ SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
79 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this); 65 Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
80 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this); 66 Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);
81 67
82 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15}); 68 zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, FromDIP(wxPoint{15, 15}));
83 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this); 69 zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);
84 70
85 help_button_ = new wxButton(this, wxID_ANY, "Help"); 71 help_button_ = new wxButton(this, wxID_ANY, "Help");
86 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this); 72 help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
87 SetUpHelpButton(); 73 SetUpHelpButton();
74
75 report_popup_ = new ReportPopup(this);
88} 76}
89 77
90void SubwayMap::OnConnect() { 78void SubwayMap::OnConnect() {
91 networks_.Clear(); 79 networks_.Clear();
92 80
93 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;
94 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 84 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
95 if (AP_HasEarlyColorHallways() && 85 if (AP_HasEarlyColorHallways() &&
96 subway_item.special == "starting_room_paintings") { 86 subway_item.special == "early_color_hallways") {
97 tagged["early_ch"].push_back(subway_item.id); 87 entrances["early_ch"].push_back(subway_item.id);
98 } 88 }
99 89
100 if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) { 90 if (AP_IsPaintingShuffle() && subway_item.painting) {
101 continue; 91 continue;
102 } 92 }
103 93
104 for (const std::string &tag : subway_item.tags) { 94 for (const std::string &tag : subway_item.tags) {
105 tagged[tag].push_back(subway_item.id); 95 tagged[tag].push_back(subway_item.id);
106 } 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 }
107 103
108 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp && 104 if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) {
109 subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
110 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots); 105 std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots);
111 tagged[tag].push_back(subway_item.id); 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 }
112 } 116 }
113 117
114 if (!AP_IsPilgrimageEnabled() && 118 if (!AP_IsPilgrimageEnabled()) {
115 (subway_item.special == "sun_painting" || 119 if (subway_item.special == "sun_painting") {
116 subway_item.special == "sun_painting_exit")) { 120 entrances["sun_painting"].push_back(subway_item.id);
117 tagged["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 }
118 } 124 }
119 } 125 }
120 126
121 if (AP_IsSunwarpShuffle()) { 127 if (AP_IsSunwarpShuffle()) {
128 sunwarp_mapping_ = AP_GetSunwarpMapping();
129
122 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal}; 130 SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal};
123 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp); 131 int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp);
124 132
125 for (const auto &[index, mapping] : AP_GetSunwarpMapping()) { 133 for (const auto &[index, mapping] : sunwarp_mapping_) {
126 std::string tag = fmt::format("sunwarp{}", mapping.dots); 134 std::string tag = fmt::format("sunwarp{}", mapping.dots);
127 135
128 SubwaySunwarp fromWarp; 136 SubwaySunwarp fromWarp;
@@ -143,13 +151,14 @@ void SubwayMap::OnConnect() {
143 toWarp.type = SubwaySunwarpType::kExit; 151 toWarp.type = SubwaySunwarpType::kExit;
144 } 152 }
145 153
146 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp)); 154 entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
147 tagged[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp)); 155 exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));
148 156
149 networks_.AddLinkToNetwork( 157 networks_.AddLinkToNetwork(
150 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp), 158 final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp),
151 mapping.dots == 6 ? final_sunwarp_item 159 mapping.dots == 6 ? final_sunwarp_item
152 : GD_GetSubwayItemForSunwarp(toWarp)); 160 : GD_GetSubwayItemForSunwarp(toWarp),
161 false);
153 } 162 }
154 } 163 }
155 164
@@ -159,40 +168,61 @@ void SubwayMap::OnConnect() {
159 tag_it1++) { 168 tag_it1++) {
160 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();
161 tag_it2++) { 170 tag_it2++) {
162 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);
163 } 182 }
164 } 183 }
165 } 184 }
166 185
167 checked_paintings_.clear(); 186 checked_paintings_.clear();
187
188 UpdateIndicators();
168} 189}
169 190
170void SubwayMap::UpdateIndicators() { 191void SubwayMap::UpdateIndicators() {
192 if (AP_IsSunwarpShuffle()) {
193 sunwarp_mapping_ = AP_GetSunwarpMapping();
194 }
195
171 if (AP_IsPaintingShuffle()) { 196 if (AP_IsPaintingShuffle()) {
172 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) {
173 if (!checked_paintings_.count(painting_id)) { 202 if (!checked_paintings_.count(painting_id)) {
174 checked_paintings_.insert(painting_id); 203 checked_paintings_.insert(painting_id);
175 204
176 if (AP_GetPaintingMapping().count(painting_id)) { 205 if (painting_mapping.count(painting_id)) {
177 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id); 206 std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
178 std::optional<int> to_id = GD_GetSubwayItemForPainting( 207 std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id));
179 AP_GetPaintingMapping().at(painting_id));
180 208
181 if (from_id && to_id) { 209 if (from_id && to_id) {
182 networks_.AddLink(*from_id, *to_id); 210 networks_.AddLink(*from_id, *to_id, false);
183 } 211 }
184 } 212 }
185 } 213 }
186 } 214 }
187 } 215 }
188 216
217 report_popup_->UpdateIndicators();
218
189 Redraw(); 219 Redraw();
190} 220}
191 221
192void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp, 222void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
193 SubwaySunwarp to_sunwarp) { 223 SubwaySunwarp to_sunwarp) {
194 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp), 224 networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
195 GD_GetSubwayItemForSunwarp(to_sunwarp)); 225 GD_GetSubwayItemForSunwarp(to_sunwarp), false);
196} 226}
197 227
198void SubwayMap::Zoom(bool in) { 228void SubwayMap::Zoom(bool in) {
@@ -239,6 +269,9 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
239 SetZoomPos({zoom_x_, zoom_y_}); 269 SetZoomPos({zoom_x_, zoom_y_});
240 270
241 SetUpHelpButton(); 271 SetUpHelpButton();
272
273 zoom_slider_->SetSize(FromDIP(15), FromDIP(15), wxDefaultCoord,
274 wxDefaultCoord, wxSIZE_AUTO);
242 } 275 }
243 276
244 wxBufferedPaintDC dc(this); 277 wxBufferedPaintDC dc(this);
@@ -294,79 +327,15 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
294 } 327 }
295 328
296 if (hovered_item_) { 329 if (hovered_item_) {
297 // Note that these requirements are duplicated on OnMouseClick so that it
298 // knows when an item has a hover effect.
299 const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
300 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
301
302 if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
303 const std::map<std::string, bool> &report =
304 GetDoorRequirements(*subway_door);
305
306 int acc_height = 10;
307 int col_width = 0;
308
309 for (const auto &[text, obtained] : report) {
310 wxSize item_extent = dc.GetTextExtent(text);
311 int item_height = std::max(32, item_extent.GetHeight()) + 10;
312 acc_height += item_height;
313
314 if (item_extent.GetWidth() > col_width) {
315 col_width = item_extent.GetWidth();
316 }
317 }
318
319 int item_width = col_width + 10 + 32;
320 int full_width = item_width + 20;
321
322 wxPoint popup_pos =
323 MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
324 subway_item.y + AREA_ACTUAL_SIZE / 2});
325
326 if (popup_pos.x + full_width > GetSize().GetWidth()) {
327 popup_pos.x = GetSize().GetWidth() - full_width;
328 }
329 if (popup_pos.y + acc_height > GetSize().GetHeight()) {
330 popup_pos.y = GetSize().GetHeight() - acc_height;
331 }
332
333 dc.SetPen(*wxTRANSPARENT_PEN);
334 dc.SetBrush(*wxBLACK_BRUSH);
335 dc.DrawRectangle(popup_pos, {full_width, acc_height});
336
337 dc.SetFont(GetFont());
338
339 int cur_height = 10;
340
341 for (const auto &[text, obtained] : report) {
342 wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;
343
344 dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});
345
346 dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
347 wxSize item_extent = dc.GetTextExtent(text);
348 dc.DrawText(
349 text,
350 popup_pos +
351 wxPoint{10 + 32 + 10,
352 cur_height + (32 - dc.GetFontMetrics().height) / 2});
353
354 cur_height += 10 + 32;
355 }
356 }
357
358 if (networks_.IsItemInNetwork(*hovered_item_)) { 330 if (networks_.IsItemInNetwork(*hovered_item_)) {
359 dc.SetBrush(*wxTRANSPARENT_BRUSH); 331 dc.SetBrush(*wxTRANSPARENT_BRUSH);
360 332
361 for (const auto &[item_id1, item_id2] : 333 for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) {
362 networks_.GetNetworkGraph(*hovered_item_)) { 334 const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
363 const SubwayItem &item1 = GD_GetSubwayItem(item_id1); 335 const SubwayItem &item2 = GD_GetSubwayItem(node.exit);
364 const SubwayItem &item2 = GD_GetSubwayItem(item_id2);
365 336
366 wxPoint item1_pos = MapPosToRenderPos( 337 wxPoint item1_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item1));
367 {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2}); 338 wxPoint item2_pos = MapPosToRenderPos(GetSubwayItemMapCenter(item2));
368 wxPoint item2_pos = MapPosToRenderPos(
369 {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});
370 339
371 int left = std::min(item1_pos.x, item2_pos.x); 340 int left = std::min(item1_pos.x, item2_pos.x);
372 int top = std::min(item1_pos.y, item2_pos.y); 341 int top = std::min(item1_pos.y, item2_pos.y);
@@ -381,6 +350,12 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
381 dc.DrawLine(item1_pos, item2_pos); 350 dc.DrawLine(item1_pos, item2_pos);
382 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); 351 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
383 dc.DrawLine(item1_pos, item2_pos); 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 }
384 } else { 359 } else {
385 int ellipse_x; 360 int ellipse_x;
386 int ellipse_y; 361 int ellipse_y;
@@ -423,6 +398,12 @@ void SubwayMap::OnPaint(wxPaintEvent &event) {
423 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2)); 398 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
424 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2, 399 dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
425 halfheight * 2, start, end); 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);
406 }
426 } 407 }
427 } 408 }
428 } 409 }
@@ -443,9 +424,7 @@ void SubwayMap::OnMouseMove(wxMouseEvent &event) {
443 } 424 }
444 425
445 if (!sticky_hover_ && actual_hover_ != hovered_item_) { 426 if (!sticky_hover_ && actual_hover_ != hovered_item_) {
446 hovered_item_ = actual_hover_; 427 EvaluateHover();
447
448 Refresh();
449 } 428 }
450 429
451 if (scroll_mode_) { 430 if (scroll_mode_) {
@@ -487,13 +466,11 @@ void SubwayMap::OnMouseClick(wxMouseEvent &event) {
487 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) || 466 if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
488 networks_.IsItemInNetwork(*hovered_item_)) { 467 networks_.IsItemInNetwork(*hovered_item_)) {
489 if (actual_hover_ != hovered_item_) { 468 if (actual_hover_ != hovered_item_) {
490 hovered_item_ = actual_hover_; 469 EvaluateHover();
491 470
492 if (!hovered_item_) { 471 if (!hovered_item_) {
493 sticky_hover_ = false; 472 sticky_hover_ = false;
494 } 473 }
495
496 Refresh();
497 } else { 474 } else {
498 sticky_hover_ = !sticky_hover_; 475 sticky_hover_ = !sticky_hover_;
499 } 476 }
@@ -543,11 +520,13 @@ void SubwayMap::OnClickHelp(wxCommandEvent &event) {
543 "corner.\nClick on a side of the screen to start panning. It will follow " 520 "corner.\nClick on a side of the screen to start panning. It will follow "
544 "your mouse. Click again to stop.\nHover over a door to see the " 521 "your mouse. Click again to stop.\nHover over a door to see the "
545 "requirements to open it.\nHover over a warp or active painting to see " 522 "requirements to open it.\nHover over a warp or active painting to see "
546 "what it is connected to.\nIn painting shuffle, paintings that have not " 523 "what it is connected to.\nFor one-way connections, there will be a "
547 "yet been checked will not show their connections.\nA green shaded owl " 524 "circle at the exit.\nCircles represent paintings.\nA red circle means "
548 "means that there is a painting entrance there.\nA red shaded owl means " 525 "that the painting is locked by a door.\nA blue circle means painting "
549 "that there are only painting exits there.\nClick on a door or " 526 "shuffle is enabled and the painting has not been checked yet.\nA black "
550 "warp to make the popup stick until you click again.", 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.",
551 "Subway Map Help"); 530 "Subway Map Help");
552} 531}
553 532
@@ -559,23 +538,37 @@ void SubwayMap::Redraw() {
559 538
560 wxGCDC gcdc(dc); 539 wxGCDC gcdc(dc);
561 540
541 std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping();
542
562 for (const SubwayItem &subway_item : GD_GetSubwayItems()) { 543 for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
563 ItemDrawType draw_type = ItemDrawType::kNone; 544 ItemDrawType draw_type = ItemDrawType::kNone;
564 const wxBrush *brush_color = wxGREY_BRUSH; 545 const wxBrush *brush_color = wxGREY_BRUSH;
565 std::optional<wxColour> shade_color;
566 std::optional<int> subway_door = GetRealSubwayDoor(subway_item); 546 std::optional<int> subway_door = GetRealSubwayDoor(subway_item);
567 547
568 if (AP_HasEarlyColorHallways() && 548 if (AP_HasEarlyColorHallways() &&
569 subway_item.special == "starting_room_paintings") { 549 subway_item.special == "early_color_hallways") {
570 draw_type = ItemDrawType::kOwl; 550 draw_type = ItemDrawType::kOwl;
571 shade_color = wxColour(0, 255, 0, 128); 551 brush_color = wxGREEN_BRUSH;
552 } else if (subway_item.special == "starting_room_overhead") {
553 // Do not draw.
554 } else if (AP_IsColorShuffle() && subway_item.special &&
555 subway_item.special->starts_with("color_")) {
556 std::string color_name = subway_item.special->substr(6);
557 LingoColor lingo_color = GetLingoColorForString(color_name);
558 int color_item_id = GD_GetItemIdForColor(lingo_color);
559
560 draw_type = ItemDrawType::kBox;
561 if (AP_HasItemSafe(color_item_id)) {
562 brush_color = wxGREEN_BRUSH;
563 } else {
564 brush_color = wxRED_BRUSH;
565 }
572 } else if (subway_item.special == "sun_painting") { 566 } else if (subway_item.special == "sun_painting") {
573 if (!AP_IsPilgrimageEnabled()) { 567 if (!AP_IsPilgrimageEnabled()) {
568 draw_type = ItemDrawType::kOwl;
574 if (IsDoorOpen(*subway_item.door)) { 569 if (IsDoorOpen(*subway_item.door)) {
575 draw_type = ItemDrawType::kOwl; 570 brush_color = wxGREEN_BRUSH;
576 shade_color = wxColour(0, 255, 0, 128);
577 } else { 571 } else {
578 draw_type = ItemDrawType::kBox;
579 brush_color = wxRED_BRUSH; 572 brush_color = wxRED_BRUSH;
580 } 573 }
581 } 574 }
@@ -589,41 +582,28 @@ void SubwayMap::Redraw() {
589 } else { 582 } else {
590 brush_color = wxRED_BRUSH; 583 brush_color = wxRED_BRUSH;
591 } 584 }
592 } else if (!subway_item.paintings.empty()) { 585 } else if (subway_item.painting) {
593 if (AP_IsPaintingShuffle()) { 586 if (subway_door && !IsDoorOpen(*subway_door)) {
594 bool has_checked_painting = false; 587 draw_type = ItemDrawType::kOwl;
595 bool has_unchecked_painting = false; 588 brush_color = wxRED_BRUSH;
596 bool has_mapped_painting = false; 589 } else if (AP_IsPaintingShuffle()) {
597 bool has_codomain_painting = false; 590 if (!checked_paintings_.count(*subway_item.painting)) {
598 591 draw_type = ItemDrawType::kOwl;
599 for (const std::string &painting_id : subway_item.paintings) { 592 brush_color = wxBLUE_BRUSH;
600 if (checked_paintings_.count(painting_id)) { 593 } else if (painting_mapping.count(*subway_item.painting)) {
601 has_checked_painting = true; 594 draw_type = ItemDrawType::kOwl;
602 595 brush_color = wxGREEN_BRUSH;
603 if (AP_GetPaintingMapping().count(painting_id)) { 596 } else if (AP_IsPaintingMappedTo(*subway_item.painting)) {
604 has_mapped_painting = true; 597 draw_type = ItemDrawType::kOwlExit;
605 } else if (AP_IsPaintingMappedTo(painting_id)) { 598 brush_color = wxGREEN_BRUSH;
606 has_codomain_painting = true;
607 }
608 } else {
609 has_unchecked_painting = true;
610 }
611 } 599 }
612 600 } else if (subway_item.HasWarps()) {
613 if (has_unchecked_painting || has_mapped_painting || 601 brush_color = wxGREEN_BRUSH;
614 has_codomain_painting) { 602 if (!subway_item.exits.empty()) {
603 draw_type = ItemDrawType::kOwlExit;
604 } else {
615 draw_type = ItemDrawType::kOwl; 605 draw_type = ItemDrawType::kOwl;
616
617 if (has_checked_painting) {
618 if (has_mapped_painting) {
619 shade_color = wxColour(0, 255, 0, 128);
620 } else {
621 shade_color = wxColour(255, 0, 0, 128);
622 }
623 }
624 } 606 }
625 } else if (!subway_item.tags.empty()) {
626 draw_type = ItemDrawType::kOwl;
627 } 607 }
628 } else if (subway_door) { 608 } else if (subway_door) {
629 draw_type = ItemDrawType::kBox; 609 draw_type = ItemDrawType::kBox;
@@ -643,21 +623,40 @@ void SubwayMap::Redraw() {
643 if (draw_type == ItemDrawType::kBox) { 623 if (draw_type == ItemDrawType::kBox) {
644 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1)); 624 gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
645 gcdc.SetBrush(*brush_color); 625 gcdc.SetBrush(*brush_color);
646 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 626
647 } else if (draw_type == ItemDrawType::kOwl) { 627 if (subway_item.tilted) {
648 wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale( 628 constexpr int AREA_TILTED_SIDE =
649 real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR)); 629 static_cast<int>(AREA_ACTUAL_SIZE / 1.41421356237);
650 gcdc.DrawBitmap(owl_bitmap, real_area_pos); 630 const wxPoint poly_points[] = {{AREA_TILTED_SIDE, 0},
651 631 {2 * AREA_TILTED_SIDE, AREA_TILTED_SIDE},
652 if (shade_color) { 632 {AREA_TILTED_SIDE, 2 * AREA_TILTED_SIDE},
653 gcdc.SetBrush(wxBrush(*shade_color)); 633 {0, AREA_TILTED_SIDE}};
634 gcdc.DrawPolygon(4, poly_points, subway_item.x, subway_item.y);
635 } else {
654 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size}); 636 gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
655 } 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 }
656 } 653 }
657 } 654 }
658} 655}
659 656
660void SubwayMap::SetUpHelpButton() { 657void SubwayMap::SetUpHelpButton() {
658 help_button_->SetSize(wxDefaultCoord, wxDefaultCoord, wxDefaultCoord,
659 wxDefaultCoord, wxSIZE_AUTO);
661 help_button_->SetPosition({ 660 help_button_->SetPosition({
662 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15, 661 GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
663 15, 662 15,
@@ -693,6 +692,51 @@ void SubwayMap::EvaluateScroll(wxPoint pos) {
693 SetScrollSpeed(scroll_x, scroll_y); 692 SetScrollSpeed(scroll_x, scroll_y);
694} 693}
695 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
696wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const { 740wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
697 return {static_cast<int>(pos.x * render_width_ * zoom_ / 741 return {static_cast<int>(pos.x * render_width_ * zoom_ /
698 map_image_.GetSize().GetWidth() + 742 map_image_.GetSize().GetWidth() +
@@ -762,8 +806,33 @@ void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
762 zoom_slider_->SetValue((zoom - 1.0) / 0.25); 806 zoom_slider_->SetValue((zoom - 1.0) / 0.25);
763} 807}
764 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
765quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const { 827quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
766 const SubwayItem &subway_item = GD_GetSubwayItem(id); 828 const SubwayItem &subway_item = GD_GetSubwayItem(id);
767 return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y), 829 if (subway_item.painting) {
768 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 }
769} 838}
diff --git a/src/subway_map.h b/src/subway_map.h index feee8ff..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);
@@ -45,15 +48,16 @@ class SubwayMap : public wxPanel {
45 wxPoint RenderPosToMapPos(wxPoint pos) const; 48 wxPoint RenderPosToMapPos(wxPoint pos) const;
46 49
47 void EvaluateScroll(wxPoint pos); 50 void EvaluateScroll(wxPoint pos);
51 void EvaluateHover();
48 52
49 void SetZoomPos(wxPoint pos); 53 void SetZoomPos(wxPoint pos);
50 void SetScrollSpeed(int scroll_x, int scroll_y); 54 void SetScrollSpeed(int scroll_x, int scroll_y);
51 void SetZoom(double zoom, wxPoint static_point); 55 void SetZoom(double zoom, wxPoint static_point);
52 56
57 std::optional<int> GetRealSubwayDoor(const SubwayItem subway_item);
58
53 wxImage map_image_; 59 wxImage map_image_;
54 wxImage owl_image_; 60 wxImage owl_image_;
55 wxBitmap unchecked_eye_;
56 wxBitmap checked_eye_;
57 61
58 wxBitmap rendered_; 62 wxBitmap rendered_;
59 int render_x_ = 0; 63 int render_x_ = 0;
@@ -85,8 +89,13 @@ class SubwayMap : public wxPanel {
85 std::optional<int> actual_hover_; 89 std::optional<int> actual_hover_;
86 bool sticky_hover_ = false; 90 bool sticky_hover_ = false;
87 91
92 ReportPopup *report_popup_;
93
88 NetworkSet networks_; 94 NetworkSet networks_;
89 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_;
90}; 99};
91 100
92#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 3b6beda..e8d7ef6 100644 --- a/src/tracker_frame.cpp +++ b/src/tracker_frame.cpp
@@ -1,37 +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>
5#include <wx/filedlg.h> 6#include <wx/filedlg.h>
6#include <wx/notebook.h> 7#include <wx/notebook.h>
8#include <wx/splitter.h>
7#include <wx/stdpaths.h> 9#include <wx/stdpaths.h>
8#include <wx/webrequest.h> 10#include <wx/webrequest.h>
9 11
10#include <fmt/core.h> 12#include <algorithm>
11#include <nlohmann/json.hpp> 13#include <nlohmann/json.hpp>
12#include <sstream> 14#include <sstream>
13 15
14#include "achievements_pane.h" 16#include "achievements_pane.h"
15#include "ap_state.h" 17#include "ap_state.h"
16#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"
17#include "settings_dialog.h" 26#include "settings_dialog.h"
18#include "subway_map.h" 27#include "subway_map.h"
19#include "tracker_config.h" 28#include "tracker_config.h"
20#include "tracker_panel.h" 29#include "tracker_panel.h"
21#include "version.h" 30#include "version.h"
22 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
23enum TrackerFrameIds { 48enum TrackerFrameIds {
24 ID_CONNECT = 1, 49 ID_AP_CONNECT = 1,
25 ID_CHECK_FOR_UPDATES = 2, 50 ID_CHECK_FOR_UPDATES = 2,
26 ID_SETTINGS = 3, 51 ID_SETTINGS = 3,
27 ID_ZOOM_IN = 4, 52 ID_ZOOM_IN = 4,
28 ID_ZOOM_OUT = 5, 53 ID_ZOOM_OUT = 5,
29 ID_OPEN_SAVE_FILE = 6, 54 ID_IPC_CONNECT = 7,
55 ID_LOG_DIALOG = 8,
30}; 56};
31 57
32wxDEFINE_EVENT(STATE_RESET, wxCommandEvent); 58wxDEFINE_EVENT(STATE_RESET, wxCommandEvent);
33wxDEFINE_EVENT(STATE_CHANGED, wxCommandEvent); 59wxDEFINE_EVENT(STATE_CHANGED, StateChangedEvent);
34wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent); 60wxDEFINE_EVENT(STATUS_CHANGED, wxCommandEvent);
61wxDEFINE_EVENT(CONNECT_TO_AP, ApConnectEvent);
35 62
36TrackerFrame::TrackerFrame() 63TrackerFrame::TrackerFrame()
37 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition, 64 : wxFrame(nullptr, wxID_ANY, "Lingo Archipelago Tracker", wxDefaultPosition,
@@ -39,16 +66,24 @@ TrackerFrame::TrackerFrame()
39 ::wxInitAllImageHandlers(); 66 ::wxInitAllImageHandlers();
40 67
41 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();
42 75
43 wxMenu *menuFile = new wxMenu(); 76 wxMenu *menuFile = new wxMenu();
44 menuFile->Append(ID_CONNECT, "&Connect"); 77 menuFile->Append(ID_AP_CONNECT, "&Connect to Archipelago");
45 menuFile->Append(ID_OPEN_SAVE_FILE, "&Open Save Data\tCtrl-O"); 78 menuFile->Append(ID_IPC_CONNECT, "&Connect to Lingo");
46 menuFile->Append(ID_SETTINGS, "&Settings"); 79 menuFile->Append(ID_SETTINGS, "&Settings");
47 menuFile->Append(wxID_EXIT); 80 menuFile->Append(wxID_EXIT);
48 81
49 wxMenu *menuView = new wxMenu(); 82 wxMenu *menuView = new wxMenu();
50 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+"); 83 zoom_in_menu_item_ = menuView->Append(ID_ZOOM_IN, "Zoom In\tCtrl-+");
51 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--"); 84 zoom_out_menu_item_ = menuView->Append(ID_ZOOM_OUT, "Zoom Out\tCtrl--");
85 menuView->AppendSeparator();
86 menuView->Append(ID_LOG_DIALOG, "Show Log Window\tCtrl-L");
52 87
53 zoom_in_menu_item_->Enable(false); 88 zoom_in_menu_item_->Enable(false);
54 zoom_out_menu_item_->Enable(false); 89 zoom_out_menu_item_->Enable(false);
@@ -65,38 +100,53 @@ TrackerFrame::TrackerFrame()
65 SetMenuBar(menuBar); 100 SetMenuBar(menuBar);
66 101
67 CreateStatusBar(); 102 CreateStatusBar();
68 SetStatusText("Not connected to Archipelago.");
69 103
70 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT); 104 Bind(wxEVT_MENU, &TrackerFrame::OnAbout, this, wxID_ABOUT);
71 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT); 105 Bind(wxEVT_MENU, &TrackerFrame::OnExit, this, wxID_EXIT);
72 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);
73 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS); 108 Bind(wxEVT_MENU, &TrackerFrame::OnSettings, this, ID_SETTINGS);
74 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this, 109 Bind(wxEVT_MENU, &TrackerFrame::OnCheckForUpdates, this,
75 ID_CHECK_FOR_UPDATES); 110 ID_CHECK_FOR_UPDATES);
76 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN); 111 Bind(wxEVT_MENU, &TrackerFrame::OnZoomIn, this, ID_ZOOM_IN);
77 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT); 112 Bind(wxEVT_MENU, &TrackerFrame::OnZoomOut, this, ID_ZOOM_OUT);
113 Bind(wxEVT_MENU, &TrackerFrame::OnOpenLogWindow, this, ID_LOG_DIALOG);
78 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this); 114 Bind(wxEVT_NOTEBOOK_PAGE_CHANGED, &TrackerFrame::OnChangePage, this);
79 Bind(wxEVT_MENU, &TrackerFrame::OnOpenFile, this, ID_OPEN_SAVE_FILE); 115 Bind(wxEVT_SPLITTER_SASH_POS_CHANGED, &TrackerFrame::OnSashPositionChanged,
116 this);
80 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this); 117 Bind(STATE_RESET, &TrackerFrame::OnStateReset, this);
81 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this); 118 Bind(STATE_CHANGED, &TrackerFrame::OnStateChanged, this);
82 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));
123
124 splitter_window_ = new wxSplitterWindow(this, wxID_ANY);
125 splitter_window_->SetMinimumPaneSize(logicalSize.x / 5);
126
127 wxChoicebook *choicebook = new wxChoicebook(splitter_window_, wxID_ANY);
83 128
84 wxChoicebook *choicebook = new wxChoicebook(this, wxID_ANY);
85 achievements_pane_ = new AchievementsPane(choicebook); 129 achievements_pane_ = new AchievementsPane(choicebook);
86 choicebook->AddPage(achievements_pane_, "Achievements"); 130 choicebook->AddPage(achievements_pane_, "Achievements");
87 131
88 notebook_ = new wxNotebook(this, wxID_ANY); 132 items_pane_ = new ItemsPane(choicebook);
133 choicebook->AddPage(items_pane_, "Items");
134
135 options_pane_ = new OptionsPane(choicebook);
136 choicebook->AddPage(options_pane_, "Options");
137
138 paintings_pane_ = new PaintingsPane(choicebook);
139 choicebook->AddPage(paintings_pane_, "Paintings");
140
141 notebook_ = new wxNotebook(splitter_window_, wxID_ANY);
89 tracker_panel_ = new TrackerPanel(notebook_); 142 tracker_panel_ = new TrackerPanel(notebook_);
90 subway_map_ = new SubwayMap(notebook_); 143 subway_map_ = new SubwayMap(notebook_);
91 notebook_->AddPage(tracker_panel_, "Map"); 144 notebook_->AddPage(tracker_panel_, "Map");
92 notebook_->AddPage(subway_map_, "Subway"); 145 notebook_->AddPage(subway_map_, "Subway");
93 146
94 wxBoxSizer *top_sizer = new wxBoxSizer(wxHORIZONTAL); 147 splitter_window_->SplitVertically(choicebook, notebook_, logicalSize.x / 4);
95 top_sizer->Add(choicebook, wxSizerFlags().Expand().Proportion(1));
96 top_sizer->Add(notebook_, wxSizerFlags().Expand().Proportion(3));
97 148
98 SetSizerAndFit(top_sizer); 149 SetSize(logicalSize);
99 SetSize(1280, 728);
100 150
101 if (!GetTrackerConfig().asked_to_check_for_updates) { 151 if (!GetTrackerConfig().asked_to_check_for_updates) {
102 GetTrackerConfig().asked_to_check_for_updates = true; 152 GetTrackerConfig().asked_to_check_for_updates = true;
@@ -113,23 +163,28 @@ TrackerFrame::TrackerFrame()
113 } 163 }
114 164
115 if (GetTrackerConfig().should_check_for_updates) { 165 if (GetTrackerConfig().should_check_for_updates) {
116 CheckForUpdates(/*manual=*/false); 166 updater_->CheckForUpdates(/*invisible=*/true);
117 } 167 }
168
169 SetStatusText(GetStatusMessage());
118} 170}
119 171
120void TrackerFrame::SetStatusMessage(std::string message) { 172void TrackerFrame::ConnectToAp(std::string server, std::string user,
121 wxCommandEvent *event = new wxCommandEvent(STATUS_CHANGED); 173 std::string pass) {
122 event->SetString(message.c_str()); 174 QueueEvent(new ApConnectEvent(CONNECT_TO_AP, GetId(), std::move(server),
175 std::move(user), std::move(pass)));
176}
123 177
124 QueueEvent(event); 178void TrackerFrame::UpdateStatusMessage() {
179 QueueEvent(new wxCommandEvent(STATUS_CHANGED));
125} 180}
126 181
127void TrackerFrame::ResetIndicators() { 182void TrackerFrame::ResetIndicators() {
128 QueueEvent(new wxCommandEvent(STATE_RESET)); 183 QueueEvent(new wxCommandEvent(STATE_RESET));
129} 184}
130 185
131void TrackerFrame::UpdateIndicators() { 186void TrackerFrame::UpdateIndicators(StateUpdate state) {
132 QueueEvent(new wxCommandEvent(STATE_CHANGED)); 187 QueueEvent(new StateChangedEvent(STATE_CHANGED, GetId(), std::move(state)));
133} 188}
134 189
135void TrackerFrame::OnAbout(wxCommandEvent &event) { 190void TrackerFrame::OnAbout(wxCommandEvent &event) {
@@ -137,6 +192,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
137 about_info.SetName("Lingo Archipelago Tracker"); 192 about_info.SetName("Lingo Archipelago Tracker");
138 about_info.SetVersion(kTrackerVersion.ToString()); 193 about_info.SetVersion(kTrackerVersion.ToString());
139 about_info.AddDeveloper("hatkirby"); 194 about_info.AddDeveloper("hatkirby");
195 about_info.AddDeveloper("art0007i");
140 about_info.AddArtist("Brenton Wildes"); 196 about_info.AddArtist("Brenton Wildes");
141 about_info.AddArtist("kinrah"); 197 about_info.AddArtist("kinrah");
142 198
@@ -145,7 +201,7 @@ void TrackerFrame::OnAbout(wxCommandEvent &event) {
145 201
146void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); } 202void TrackerFrame::OnExit(wxCommandEvent &event) { Close(true); }
147 203
148void TrackerFrame::OnConnect(wxCommandEvent &event) { 204void TrackerFrame::OnApConnect(wxCommandEvent &event) {
149 ConnectionDialog dlg; 205 ConnectionDialog dlg;
150 206
151 if (dlg.ShowModal() == wxID_OK) { 207 if (dlg.ShowModal() == wxID_OK) {
@@ -175,6 +231,17 @@ void TrackerFrame::OnConnect(wxCommandEvent &event) {
175 } 231 }
176} 232}
177 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
178void TrackerFrame::OnSettings(wxCommandEvent &event) { 245void TrackerFrame::OnSettings(wxCommandEvent &event) {
179 SettingsDialog dlg; 246 SettingsDialog dlg;
180 247
@@ -182,15 +249,18 @@ void TrackerFrame::OnSettings(wxCommandEvent &event) {
182 GetTrackerConfig().should_check_for_updates = 249 GetTrackerConfig().should_check_for_updates =
183 dlg.GetShouldCheckForUpdates(); 250 dlg.GetShouldCheckForUpdates();
184 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas(); 251 GetTrackerConfig().hybrid_areas = dlg.GetHybridAreas();
185 GetTrackerConfig().show_hunt_panels = dlg.GetShowHuntPanels(); 252 GetTrackerConfig().visible_panels = dlg.GetVisiblePanels();
253 GetTrackerConfig().track_position = dlg.GetTrackPosition();
186 GetTrackerConfig().Save(); 254 GetTrackerConfig().Save();
187 255
188 UpdateIndicators(); 256 UpdateIndicators(StateUpdate{.cleared_locations = true,
257 .player_position = true,
258 .changed_settings = true});
189 } 259 }
190} 260}
191 261
192void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) { 262void TrackerFrame::OnCheckForUpdates(wxCommandEvent &event) {
193 CheckForUpdates(/*manual=*/true); 263 updater_->CheckForUpdates(/*invisible=*/false);
194} 264}
195 265
196void TrackerFrame::OnZoomIn(wxCommandEvent &event) { 266void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
@@ -199,112 +269,99 @@ void TrackerFrame::OnZoomIn(wxCommandEvent &event) {
199 } 269 }
200} 270}
201 271
202void TrackerFrame::OnZoomOut(wxCommandEvent& event) { 272void TrackerFrame::OnZoomOut(wxCommandEvent &event) {
203 if (notebook_->GetSelection() == 1) { 273 if (notebook_->GetSelection() == 1) {
204 subway_map_->Zoom(false); 274 subway_map_->Zoom(false);
205 } 275 }
206} 276}
207 277
208void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) { 278void TrackerFrame::OnOpenLogWindow(wxCommandEvent &event) {
209 zoom_in_menu_item_->Enable(event.GetSelection() == 1); 279 if (log_dialog_ == nullptr) {
210 zoom_out_menu_item_->Enable(event.GetSelection() == 1); 280 log_dialog_ = new LogDialog(this);
211} 281 log_dialog_->Show();
282 TrackerSetLogDialog(log_dialog_);
212 283
213void TrackerFrame::OnOpenFile(wxCommandEvent& event) { 284 log_dialog_->Bind(wxEVT_CLOSE_WINDOW, &TrackerFrame::OnCloseLogWindow,
214 wxFileDialog open_file_dialog( 285 this);
215 this, "Open Lingo Save File", 286 } else {
216 fmt::format("{}\\Godot\\app_userdata\\Lingo\\level1_stable", 287 log_dialog_->SetFocus();
217 wxStandardPaths::Get().GetUserConfigDir().ToStdString()),
218 AP_GetSaveName(), "Lingo save file (*.save)|*.save",
219 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
220 if (open_file_dialog.ShowModal() == wxID_CANCEL) {
221 return;
222 } 288 }
289}
223 290
224 std::string savedata_path = open_file_dialog.GetPath().ToStdString(); 291void TrackerFrame::OnCloseLogWindow(wxCloseEvent& event) {
292 TrackerSetLogDialog(nullptr);
293 log_dialog_ = nullptr;
225 294
226 if (panels_panel_ == nullptr) { 295 event.Skip();
227 panels_panel_ = new TrackerPanel(notebook_); 296}
228 notebook_->AddPage(panels_panel_, "Panels");
229 }
230 297
231 notebook_->SetSelection(notebook_->FindPage(panels_panel_)); 298void TrackerFrame::OnChangePage(wxBookCtrlEvent &event) {
232 panels_panel_->SetSavedataPath(savedata_path); 299 zoom_in_menu_item_->Enable(event.GetSelection() == 1);
300 zoom_out_menu_item_->Enable(event.GetSelection() == 1);
233} 301}
234 302
235void TrackerFrame::OnStateReset(wxCommandEvent& event) { 303void TrackerFrame::OnSashPositionChanged(wxSplitterEvent& event) {
236 tracker_panel_->UpdateIndicators(); 304 notebook_->Refresh();
237 achievements_pane_->UpdateIndicators();
238 subway_map_->OnConnect();
239 if (panels_panel_ != nullptr) {
240 notebook_->DeletePage(notebook_->FindPage(panels_panel_));
241 panels_panel_ = nullptr;
242 }
243 Refresh();
244} 305}
245 306
246void TrackerFrame::OnStateChanged(wxCommandEvent &event) { 307void TrackerFrame::OnStateReset(wxCommandEvent &event) {
247 tracker_panel_->UpdateIndicators(); 308 tracker_panel_->UpdateIndicators(/*reset=*/true);
248 achievements_pane_->UpdateIndicators(); 309 achievements_pane_->UpdateIndicators();
249 subway_map_->UpdateIndicators(); 310 items_pane_->ResetIndicators();
250 if (panels_panel_ != nullptr) { 311 options_pane_->OnConnect();
251 panels_panel_->UpdateIndicators(); 312 paintings_pane_->ResetIndicators();
252 } 313 subway_map_->OnConnect();
253 Refresh(); 314 Refresh();
254} 315}
255 316
256void TrackerFrame::OnStatusChanged(wxCommandEvent &event) { 317void TrackerFrame::OnStateChanged(StateChangedEvent &event) {
257 SetStatusText(event.GetString()); 318 const StateUpdate &state = event.GetState();
258} 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();
341 }
342 }
259 343
260void TrackerFrame::CheckForUpdates(bool manual) { 344 if (std::any_of(state.panels.begin(), state.panels.end(),
261 wxWebRequest request = wxWebSession::GetDefault().CreateRequest( 345 [](int solve_index) {
262 this, "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION"); 346 return GD_GetPanel(GD_GetPanelBySolveIndex(solve_index))
347 .achievement;
348 })) {
349 achievements_pane_->UpdateIndicators();
350 }
263 351
264 if (!request.IsOk()) { 352 if (!state.items.empty()) {
265 if (manual) { 353 items_pane_->UpdateIndicators(state.items);
266 wxMessageBox("Could not check for updates.", "Error", 354 }
267 wxOK | wxICON_ERROR);
268 } else {
269 SetStatusText("Could not check for updates.");
270 }
271 355
272 return; 356 if (!state.paintings.empty()) {
357 paintings_pane_->UpdateIndicators(state.paintings);
273 } 358 }
359}
274 360
275 Bind(wxEVT_WEBREQUEST_STATE, [this, manual](wxWebRequestEvent &evt) { 361void TrackerFrame::OnStatusChanged(wxCommandEvent &event) {
276 if (evt.GetState() == wxWebRequest::State_Completed) { 362 SetStatusText(wxString::FromUTF8(GetStatusMessage()));
277 std::string response = evt.GetResponse().AsString().ToStdString(); 363}
278
279 Version latest_version(response);
280 if (kTrackerVersion < latest_version) {
281 std::ostringstream message_text;
282 message_text << "There is a newer version of Lingo AP Tracker "
283 "available. You have "
284 << kTrackerVersion.ToString()
285 << ", and the latest version is "
286 << latest_version.ToString()
287 << ". Would you like to update?";
288
289 if (wxMessageBox(message_text.str(), "Update available", wxYES_NO) ==
290 wxYES) {
291 wxLaunchDefaultBrowser(
292 "https://code.fourisland.com/lingo-ap-tracker/about/"
293 "CHANGELOG.md");
294 }
295 } else if (manual) {
296 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker",
297 wxOK);
298 }
299 } else if (evt.GetState() == wxWebRequest::State_Failed) {
300 if (manual) {
301 wxMessageBox("Could not check for updates.", "Error",
302 wxOK | wxICON_ERROR);
303 } else {
304 SetStatusText("Could not check for updates.");
305 }
306 }
307 });
308 364
309 request.Start(); 365void TrackerFrame::OnConnectToAp(ApConnectEvent &event) {
366 AP_Connect(event.GetServer(), event.GetUser(), event.GetPass());
310} 367}
diff --git a/src/tracker_frame.h b/src/tracker_frame.h index 19bd0b3..00bbe70 100644 --- a/src/tracker_frame.h +++ b/src/tracker_frame.h
@@ -7,50 +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;
13class wxBookCtrlEvent; 24class wxBookCtrlEvent;
14class wxNotebook; 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};
15 74
16wxDECLARE_EVENT(STATE_RESET, wxCommandEvent); 75wxDECLARE_EVENT(STATE_RESET, wxCommandEvent);
17wxDECLARE_EVENT(STATE_CHANGED, wxCommandEvent); 76wxDECLARE_EVENT(STATE_CHANGED, StateChangedEvent);
18wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent); 77wxDECLARE_EVENT(STATUS_CHANGED, wxCommandEvent);
78wxDECLARE_EVENT(CONNECT_TO_AP, ApConnectEvent);
19 79
20class TrackerFrame : public wxFrame { 80class TrackerFrame : public wxFrame {
21 public: 81 public:
22 TrackerFrame(); 82 TrackerFrame();
23 83
24 void SetStatusMessage(std::string message); 84 void ConnectToAp(std::string server, std::string user, std::string pass);
85 void UpdateStatusMessage();
25 86
26 void ResetIndicators(); 87 void ResetIndicators();
27 void UpdateIndicators(); 88 void UpdateIndicators(StateUpdate state);
28 89
29 private: 90 private:
30 void OnExit(wxCommandEvent &event); 91 void OnExit(wxCommandEvent &event);
31 void OnAbout(wxCommandEvent &event); 92 void OnAbout(wxCommandEvent &event);
32 void OnConnect(wxCommandEvent &event); 93 void OnApConnect(wxCommandEvent &event);
94 void OnIpcConnect(wxCommandEvent &event);
33 void OnSettings(wxCommandEvent &event); 95 void OnSettings(wxCommandEvent &event);
34 void OnCheckForUpdates(wxCommandEvent &event); 96 void OnCheckForUpdates(wxCommandEvent &event);
35 void OnZoomIn(wxCommandEvent &event); 97 void OnZoomIn(wxCommandEvent &event);
36 void OnZoomOut(wxCommandEvent &event); 98 void OnZoomOut(wxCommandEvent &event);
99 void OnOpenLogWindow(wxCommandEvent &event);
100 void OnCloseLogWindow(wxCloseEvent &event);
37 void OnChangePage(wxBookCtrlEvent &event); 101 void OnChangePage(wxBookCtrlEvent &event);
38 void OnOpenFile(wxCommandEvent &event); 102 void OnSashPositionChanged(wxSplitterEvent &event);
39 103
40 void OnStateReset(wxCommandEvent &event); 104 void OnStateReset(wxCommandEvent &event);
41 void OnStateChanged(wxCommandEvent &event); 105 void OnStateChanged(StateChangedEvent &event);
42 void OnStatusChanged(wxCommandEvent &event); 106 void OnStatusChanged(wxCommandEvent &event);
107 void OnConnectToAp(ApConnectEvent &event);
108
109 std::unique_ptr<Updater> updater_;
43 110
44 void CheckForUpdates(bool manual); 111 wxSplitterWindow *splitter_window_;
45
46 wxNotebook *notebook_; 112 wxNotebook *notebook_;
47 TrackerPanel *tracker_panel_; 113 TrackerPanel *tracker_panel_;
48 AchievementsPane *achievements_pane_; 114 AchievementsPane *achievements_pane_;
115 ItemsPane *items_pane_;
116 OptionsPane *options_pane_;
117 PaintingsPane *paintings_pane_;
49 SubwayMap *subway_map_; 118 SubwayMap *subway_map_;
50 TrackerPanel *panels_panel_ = nullptr; 119 LogDialog *log_dialog_ = nullptr;
51 120
52 wxMenuItem *zoom_in_menu_item_; 121 wxMenuItem *zoom_in_menu_item_;
53 wxMenuItem *zoom_out_menu_item_; 122 wxMenuItem *zoom_out_menu_item_;
123
124 IconCache icons_;
54}; 125};
55 126
56#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 27e825a..ddb4df9 100644 --- a/src/tracker_panel.cpp +++ b/src/tracker_panel.cpp
@@ -9,7 +9,7 @@
9#include "area_popup.h" 9#include "area_popup.h"
10#include "game_data.h" 10#include "game_data.h"
11#include "global.h" 11#include "global.h"
12#include "godot_variant.h" 12#include "ipc_state.h"
13#include "tracker_config.h" 13#include "tracker_config.h"
14#include "tracker_state.h" 14#include "tracker_state.h"
15 15
@@ -43,67 +43,74 @@ TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
43 areas_.push_back(area); 43 areas_.push_back(area);
44 } 44 }
45 45
46 Resize();
46 Redraw(); 47 Redraw();
47 48
48 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this); 49 Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
49 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this); 50 Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
50} 51}
51 52
52void TrackerPanel::UpdateIndicators() { 53void TrackerPanel::UpdateIndicators(bool reset) {
53 for (AreaIndicator &area : areas_) { 54 if (reset) {
54 area.popup->UpdateIndicators(); 55 for (AreaIndicator &area : areas_) {
55 } 56 const MapArea &map_area = GD_GetMapArea(area.area_id);
56 57
57 Redraw(); 58 if ((!AP_IsLocationVisible(map_area.classification) ||
58} 59 IsAreaPostgame(area.area_id)) &&
59 60 !(map_area.hunt &&
60void TrackerPanel::SetSavedataPath(std::string savedata_path) { 61 GetTrackerConfig().visible_panels == TrackerConfig::kHUNT_PANELS) &&
61 if (!panels_mode_) { 62 !(map_area.has_single_panel &&
62 wxButton *refresh_button = new wxButton(this, wxID_ANY, "Refresh", {15, 15}); 63 GetTrackerConfig().visible_panels == TrackerConfig::kALL_PANELS) &&
63 refresh_button->Bind(wxEVT_BUTTON, &TrackerPanel::OnRefreshSavedata, this); 64 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
64 } 65 area.active = false;
65 66 } else {
66 savedata_path_ = savedata_path; 67 area.active = true;
67 panels_mode_ = true; 68 }
68
69 RefreshSavedata();
70}
71 69
72void TrackerPanel::RefreshSavedata() { 70 area.popup->ResetIndicators();
73 solved_panels_.clear(); 71 }
74 72
75 GodotVariant godot_variant = ParseGodotFile(*savedata_path_); 73 Resize();
76 for (const GodotVariant &panel_node : godot_variant.AsArray()) { 74 } else {
77 const std::vector<GodotVariant> &fields = panel_node.AsArray(); 75 for (AreaIndicator &area : areas_) {
78 if (fields[1].AsBool()) { 76 area.popup->UpdateIndicators();
79 const std::vector<std::string> &nodepath = fields[0].AsNodePath();
80 std::string key = fmt::format("{}/{}", nodepath[3], nodepath[4]);
81 solved_panels_.insert(key);
82 } 77 }
83 } 78 }
84 79
85 UpdateIndicators(); 80 Redraw();
86 Refresh();
87} 81}
88 82
89void TrackerPanel::OnPaint(wxPaintEvent &event) { 83void TrackerPanel::OnPaint(wxPaintEvent &event) {
90 if (GetSize() != rendered_.GetSize()) { 84 if (GetSize() != rendered_.GetSize()) {
85 Resize();
91 Redraw(); 86 Redraw();
92 } 87 }
93 88
94 wxBufferedPaintDC dc(this); 89 wxBufferedPaintDC dc(this);
95 dc.DrawBitmap(rendered_, 0, 0); 90 dc.DrawBitmap(rendered_, 0, 0);
96 91
97 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()) {
98 // 1588, 1194 103 // 1588, 1194
99 // 14x14 -> 154x154 104 // 14x14 -> 154x154
100 double intended_x = 105 double intended_x =
101 1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 106 1588.0 + (std::get<0>(*player_position) * (154.0 / 14.0));
102 double intended_y = 107 double intended_y =
103 1194.0 + (std::get<1>(*AP_GetPlayerPosition()) * (154.0 / 14.0)); 108 1194.0 + (std::get<1>(*player_position) * (154.0 / 14.0));
104 109
105 int real_x = offset_x_ + scale_x_ * intended_x - scaled_player_.GetWidth() / 2; 110 int real_x =
106 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;
107 114
108 dc.DrawBitmap(scaled_player_, real_x, real_y); 115 dc.DrawBitmap(scaled_player_, real_x, real_y);
109 } 116 }
@@ -125,12 +132,8 @@ void TrackerPanel::OnMouseMove(wxMouseEvent &event) {
125 event.Skip(); 132 event.Skip();
126} 133}
127 134
128void TrackerPanel::OnRefreshSavedata(wxCommandEvent &event) { 135void TrackerPanel::Resize() {
129 RefreshSavedata(); 136 wxSize panel_size = GetClientSize();
130}
131
132void TrackerPanel::Redraw() {
133 wxSize panel_size = GetSize();
134 wxSize image_size = map_image_.GetSize(); 137 wxSize image_size = map_image_.GetSize();
135 138
136 int final_x = 0; 139 int final_x = 0;
@@ -149,7 +152,7 @@ void TrackerPanel::Redraw() {
149 final_x = (panel_size.GetWidth() - final_width) / 2; 152 final_x = (panel_size.GetWidth() - final_width) / 2;
150 } 153 }
151 154
152 rendered_ = wxBitmap( 155 scaled_map_ = wxBitmap(
153 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL) 156 map_image_.Scale(final_width, final_height, wxIMAGE_QUALITY_NORMAL)
154 .Size(panel_size, {final_x, final_y}, 0, 0, 0)); 157 .Size(panel_size, {final_x, final_y}, 0, 0, 0));
155 158
@@ -164,30 +167,61 @@ void TrackerPanel::Redraw() {
164 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1, 167 wxBitmap(player_image_.Scale(player_width > 0 ? player_width : 1,
165 player_height > 0 ? player_height : 1)); 168 player_height > 0 ? player_height : 1));
166 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
167 wxMemoryDC dc; 212 wxMemoryDC dc;
168 dc.SelectObject(rendered_); 213 dc.SelectObject(rendered_);
169 214
170 int real_area_size =
171 final_width * AREA_EFFECTIVE_SIZE / image_size.GetWidth();
172 int actual_border_size = 215 int actual_border_size =
173 real_area_size * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE; 216 real_area_size_ * AREA_BORDER_SIZE / AREA_EFFECTIVE_SIZE;
174 const wxPoint upper_left_triangle[] = { 217 const wxPoint upper_left_triangle[] = {
175 {0, 0}, {0, real_area_size}, {real_area_size, 0}}; 218 {0, 0}, {0, real_area_size_}, {real_area_size_, 0}};
176 const wxPoint lower_right_triangle[] = {{0, real_area_size - 1}, 219 const wxPoint lower_right_triangle[] = {{0, real_area_size_ - 1},
177 {real_area_size - 1, 0}, 220 {real_area_size_ - 1, 0},
178 {real_area_size, real_area_size}}; 221 {real_area_size_, real_area_size_}};
179 222
180 for (AreaIndicator &area : areas_) { 223 for (AreaIndicator &area : areas_) {
181 const MapArea &map_area = GD_GetMapArea(area.area_id); 224 const MapArea &map_area = GD_GetMapArea(area.area_id);
182 if (panels_mode_) {
183 area.active = map_area.has_single_panel;
184 } else if (!AP_IsLocationVisible(map_area.classification) &&
185 !(map_area.hunt && GetTrackerConfig().show_hunt_panels) &&
186 !(AP_IsPaintingShuffle() && !map_area.paintings.empty())) {
187 area.active = false;
188 } else {
189 area.active = true;
190 }
191 225
192 if (!area.active) { 226 if (!area.active) {
193 continue; 227 continue;
@@ -199,19 +233,15 @@ void TrackerPanel::Redraw() {
199 bool has_unchecked = false; 233 bool has_unchecked = false;
200 if (IsLocationWinCondition(section)) { 234 if (IsLocationWinCondition(section)) {
201 has_unchecked = !AP_HasReachedGoal(); 235 has_unchecked = !AP_HasReachedGoal();
202 } else if (panels_mode_) { 236 } else if (AP_IsLocationVisible(section.classification) &&
203 if (section.single_panel) { 237 !IsLocationPostgame(section.ap_location_id)) {
204 const Panel &panel = GD_GetPanel(*section.single_panel);
205 if (panel.non_counting) {
206 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
207 } else {
208 has_unchecked = !solved_panels_.contains(panel.nodepath);
209 }
210 }
211 } else if (AP_IsLocationVisible(section.classification)) {
212 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id); 238 has_unchecked = !AP_HasCheckedGameLocation(section.ap_location_id);
213 } else if (section.hunt && GetTrackerConfig().show_hunt_panels) { 239 } else if ((section.hunt && GetTrackerConfig().visible_panels ==
214 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);
215 } 245 }
216 246
217 if (has_unchecked) { 247 if (has_unchecked) {
@@ -223,8 +253,12 @@ void TrackerPanel::Redraw() {
223 } 253 }
224 } 254 }
225 255
226 if (AP_IsPaintingShuffle() && !panels_mode_) { 256 if (AP_IsPaintingShuffle()) {
227 for (int painting_id : map_area.paintings) { 257 for (int painting_id : map_area.paintings) {
258 if (IsPaintingPostgame(painting_id)) {
259 continue;
260 }
261
228 const PaintingExit &painting = GD_GetPaintingExit(painting_id); 262 const PaintingExit &painting = GD_GetPaintingExit(painting_id);
229 bool reachable = IsPaintingReachable(painting_id); 263 bool reachable = IsPaintingReachable(painting_id);
230 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) { 264 if (!reachable || !AP_IsPaintingChecked(painting.internal_id)) {
@@ -237,10 +271,8 @@ void TrackerPanel::Redraw() {
237 } 271 }
238 } 272 }
239 273
240 int real_area_x = final_x + (map_area.map_x - (AREA_EFFECTIVE_SIZE / 2)) * 274 int real_area_x = area.real_x1;
241 final_width / image_size.GetWidth(); 275 int real_area_y = area.real_y1;
242 int real_area_y = final_y + (map_area.map_y - (AREA_EFFECTIVE_SIZE / 2)) *
243 final_width / image_size.GetWidth();
244 276
245 if (has_reachable_unchecked && has_unreachable_unchecked && 277 if (has_reachable_unchecked && has_unreachable_unchecked &&
246 GetTrackerConfig().hybrid_areas) { 278 GetTrackerConfig().hybrid_areas) {
@@ -254,7 +286,7 @@ void TrackerPanel::Redraw() {
254 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 286 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
255 dc.SetBrush(*wxTRANSPARENT_BRUSH); 287 dc.SetBrush(*wxTRANSPARENT_BRUSH);
256 dc.DrawRectangle({real_area_x, real_area_y}, 288 dc.DrawRectangle({real_area_x, real_area_y},
257 {real_area_size, real_area_size}); 289 {real_area_size_, real_area_size_});
258 290
259 } else { 291 } else {
260 const wxBrush *brush_color = wxGREY_BRUSH; 292 const wxBrush *brush_color = wxGREY_BRUSH;
@@ -269,30 +301,7 @@ void TrackerPanel::Redraw() {
269 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size)); 301 dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, actual_border_size));
270 dc.SetBrush(*brush_color); 302 dc.SetBrush(*brush_color);
271 dc.DrawRectangle({real_area_x, real_area_y}, 303 dc.DrawRectangle({real_area_x, real_area_y},
272 {real_area_size, real_area_size}); 304 {real_area_size_, real_area_size_});
273 }
274
275 area.real_x1 = real_area_x;
276 area.real_x2 = real_area_x + real_area_size;
277 area.real_y1 = real_area_y;
278 area.real_y2 = real_area_y + real_area_size;
279
280 int popup_x =
281 final_x + map_area.map_x * final_width / image_size.GetWidth();
282 int popup_y =
283 final_y + map_area.map_y * final_width / image_size.GetWidth();
284
285 area.popup->SetClientSize(
286 area.popup->GetVirtualSize().GetWidth(),
287 std::min(panel_size.GetHeight(),
288 area.popup->GetVirtualSize().GetHeight()));
289
290 if (popup_x + area.popup->GetSize().GetWidth() > panel_size.GetWidth()) {
291 popup_x = panel_size.GetWidth() - area.popup->GetSize().GetWidth();
292 } 305 }
293 if (popup_y + area.popup->GetSize().GetHeight() > panel_size.GetHeight()) {
294 popup_y = panel_size.GetHeight() - area.popup->GetSize().GetHeight();
295 }
296 area.popup->SetPosition({popup_x, popup_y});
297 } 306 }
298} 307}
diff --git a/src/tracker_panel.h b/src/tracker_panel.h index e1f515d..6825843 100644 --- a/src/tracker_panel.h +++ b/src/tracker_panel.h
@@ -17,15 +17,7 @@ class TrackerPanel : public wxPanel {
17 public: 17 public:
18 TrackerPanel(wxWindow *parent); 18 TrackerPanel(wxWindow *parent);
19 19
20 void UpdateIndicators(); 20 void UpdateIndicators(bool reset);
21
22 void SetSavedataPath(std::string savedata_path);
23
24 bool IsPanelsMode() const { return panels_mode_; }
25
26 const std::set<std::string> &GetSolvedPanels() const {
27 return solved_panels_;
28 }
29 21
30 private: 22 private:
31 struct AreaIndicator { 23 struct AreaIndicator {
@@ -40,14 +32,13 @@ class TrackerPanel : public wxPanel {
40 32
41 void OnPaint(wxPaintEvent &event); 33 void OnPaint(wxPaintEvent &event);
42 void OnMouseMove(wxMouseEvent &event); 34 void OnMouseMove(wxMouseEvent &event);
43 void OnRefreshSavedata(wxCommandEvent &event);
44 35
36 void Resize();
45 void Redraw(); 37 void Redraw();
46 38
47 void RefreshSavedata();
48
49 wxImage map_image_; 39 wxImage map_image_;
50 wxImage player_image_; 40 wxImage player_image_;
41 wxBitmap scaled_map_;
51 wxBitmap rendered_; 42 wxBitmap rendered_;
52 wxBitmap scaled_player_; 43 wxBitmap scaled_player_;
53 44
@@ -55,12 +46,9 @@ class TrackerPanel : public wxPanel {
55 int offset_y_ = 0; 46 int offset_y_ = 0;
56 double scale_x_ = 0; 47 double scale_x_ = 0;
57 double scale_y_ = 0; 48 double scale_y_ = 0;
49 int real_area_size_ = 0;
58 50
59 std::vector<AreaIndicator> areas_; 51 std::vector<AreaIndicator> areas_;
60
61 bool panels_mode_ = false;
62 std::optional<std::string> savedata_path_;
63 std::set<std::string> solved_panels_;
64}; 52};
65 53
66#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */ 54#endif /* end of include guard: TRACKER_PANEL_H_D675A54D */
diff --git a/src/tracker_state.cpp b/src/tracker_state.cpp index 2ee705c..bf2725a 100644 --- a/src/tracker_state.cpp +++ b/src/tracker_state.cpp
@@ -12,6 +12,7 @@
12 12
13#include "ap_state.h" 13#include "ap_state.h"
14#include "game_data.h" 14#include "game_data.h"
15#include "global.h"
15#include "logger.h" 16#include "logger.h"
16 17
17namespace { 18namespace {
@@ -19,11 +20,13 @@ namespace {
19struct Requirements { 20struct Requirements {
20 bool disabled = false; 21 bool disabled = false;
21 22
22 std::set<int> doors; // non-grouped, handles progressive 23 std::set<int> doors; // non-grouped, handles progressive
23 std::set<int> items; // all other items 24 std::set<int> panel_doors; // non-grouped, handles progressive
24 std::set<int> rooms; // maybe 25 std::set<int> items; // all other items
25 bool mastery = false; // maybe 26 std::set<int> rooms; // maybe
26 bool panel_hunt = false; // maybe 27 bool mastery = false; // maybe
28 bool panel_hunt = false; // maybe
29 bool postgame = false;
27 30
28 void Merge(const Requirements& rhs) { 31 void Merge(const Requirements& rhs) {
29 if (rhs.disabled) { 32 if (rhs.disabled) {
@@ -33,6 +36,9 @@ struct Requirements {
33 for (int id : rhs.doors) { 36 for (int id : rhs.doors) {
34 doors.insert(id); 37 doors.insert(id);
35 } 38 }
39 for (int id : rhs.panel_doors) {
40 panel_doors.insert(id);
41 }
36 for (int id : rhs.items) { 42 for (int id : rhs.items) {
37 items.insert(id); 43 items.insert(id);
38 } 44 }
@@ -41,6 +47,7 @@ struct Requirements {
41 } 47 }
42 mastery = mastery || rhs.mastery; 48 mastery = mastery || rhs.mastery;
43 panel_hunt = panel_hunt || rhs.panel_hunt; 49 panel_hunt = panel_hunt || rhs.panel_hunt;
50 postgame = postgame || rhs.postgame;
44 } 51 }
45}; 52};
46 53
@@ -78,15 +85,12 @@ class RequirementCalculator {
78 requirements.doors.insert(door_obj.id); 85 requirements.doors.insert(door_obj.id);
79 break; 86 break;
80 } 87 }
81 } else if (AP_GetDoorShuffleMode() == kNO_DOORS || door_obj.skip_item) { 88 } else if (AP_GetDoorShuffleMode() != kDOORS_MODE || door_obj.skip_item) {
82 requirements.rooms.insert(door_obj.room);
83
84 for (int panel_id : door_obj.panels) { 89 for (int panel_id : door_obj.panels) {
85 const Requirements& panel_reqs = GetPanel(panel_id); 90 const Requirements& panel_reqs = GetPanel(panel_id);
86 requirements.Merge(panel_reqs); 91 requirements.Merge(panel_reqs);
87 } 92 }
88 } else if (AP_GetDoorShuffleMode() == kSIMPLE_DOORS && 93 } else if (AP_AreDoorsGrouped() && !door_obj.group_name.empty()) {
89 !door_obj.group_name.empty()) {
90 requirements.items.insert(door_obj.group_ap_item_id); 94 requirements.items.insert(door_obj.group_ap_item_id);
91 } else { 95 } else {
92 requirements.doors.insert(door_obj.id); 96 requirements.doors.insert(door_obj.id);
@@ -133,6 +137,21 @@ class RequirementCalculator {
133 requirements.items.insert(GD_GetItemIdForColor(color)); 137 requirements.items.insert(GD_GetItemIdForColor(color));
134 } 138 }
135 } 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 }
136 155
137 panels_[panel_id] = requirements; 156 panels_[panel_id] = requirements;
138 } 157 }
@@ -148,11 +167,17 @@ class RequirementCalculator {
148struct TrackerState { 167struct TrackerState {
149 std::map<int, bool> reachability; 168 std::map<int, bool> reachability;
150 std::set<int> reachable_doors; 169 std::set<int> reachable_doors;
170 std::set<int> solveable_panels;
151 std::set<int> reachable_paintings; 171 std::set<int> reachable_paintings;
152 std::mutex reachability_mutex; 172 std::mutex reachability_mutex;
153 RequirementCalculator requirements; 173 RequirementCalculator requirements;
154 std::map<int, std::map<std::string, bool>> door_reports; 174 std::map<int, std::map<std::string, bool>> door_reports;
155 bool pilgrimage_doable = false; 175 bool pilgrimage_doable = false;
176
177 // If these are empty, it actually means everything is non-postgame.
178 std::set<int> non_postgame_areas;
179 std::set<int> non_postgame_locations;
180 std::set<int> non_postgame_paintings;
156}; 181};
157 182
158enum Decision { kYes, kNo, kMaybe }; 183enum Decision { kYes, kNo, kMaybe };
@@ -167,6 +192,11 @@ class StateCalculator;
167struct StateCalculatorOptions { 192struct StateCalculatorOptions {
168 int start; 193 int start;
169 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
170 StateCalculator* parent = nullptr; 200 StateCalculator* parent = nullptr;
171}; 201};
172 202
@@ -177,7 +207,21 @@ class StateCalculator {
177 explicit StateCalculator(StateCalculatorOptions options) 207 explicit StateCalculator(StateCalculatorOptions options)
178 : options_(options) {} 208 : options_(options) {}
179 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
180 void Calculate() { 220 void Calculate() {
221 painting_mapping_ = AP_GetPaintingMapping();
222 checked_paintings_ = AP_GetCheckedPaintings();
223 sunwarp_mapping_ = AP_GetSunwarpMapping();
224
181 std::list<int> panel_boundary; 225 std::list<int> panel_boundary;
182 std::list<int> painting_boundary; 226 std::list<int> painting_boundary;
183 std::list<Exit> flood_boundary; 227 std::list<Exit> flood_boundary;
@@ -217,12 +261,13 @@ class StateCalculator {
217 reachable_changed = true; 261 reachable_changed = true;
218 262
219 PaintingExit cur_painting = GD_GetPaintingExit(painting_id); 263 PaintingExit cur_painting = GD_GetPaintingExit(painting_id);
220 if (AP_GetPaintingMapping().count(cur_painting.internal_id) && 264 if (painting_mapping_.count(cur_painting.internal_id) &&
221 AP_GetCheckedPaintings().count(cur_painting.internal_id)) { 265 (checked_paintings_.count(cur_painting.internal_id) ||
266 options_.postgame_detection)) {
222 Exit painting_exit; 267 Exit painting_exit;
223 PaintingExit target_painting = 268 PaintingExit target_painting =
224 GD_GetPaintingExit(GD_GetPaintingByName( 269 GD_GetPaintingExit(GD_GetPaintingByName(
225 AP_GetPaintingMapping().at(cur_painting.internal_id))); 270 painting_mapping_.at(cur_painting.internal_id)));
226 painting_exit.source_room = cur_painting.room; 271 painting_exit.source_room = cur_painting.room;
227 painting_exit.destination_room = target_painting.room; 272 painting_exit.destination_room = target_painting.room;
228 painting_exit.type = EntranceType::kPainting; 273 painting_exit.type = EntranceType::kPainting;
@@ -281,8 +326,8 @@ class StateCalculator {
281 326
282 if (AP_IsSunwarpShuffle()) { 327 if (AP_IsSunwarpShuffle()) {
283 for (int index : room_obj.sunwarps) { 328 for (int index : room_obj.sunwarps) {
284 if (AP_GetSunwarpMapping().count(index)) { 329 if (sunwarp_mapping_.count(index)) {
285 const SunwarpMapping& sm = AP_GetSunwarpMapping().at(index); 330 const SunwarpMapping& sm = sunwarp_mapping_.at(index);
286 331
287 new_boundary.push_back( 332 new_boundary.push_back(
288 {.source_room = room_exit.destination_room, 333 {.source_room = room_exit.destination_room,
@@ -296,15 +341,14 @@ class StateCalculator {
296 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") { 341 if (AP_HasEarlyColorHallways() && room_obj.name == "Starting Room") {
297 new_boundary.push_back( 342 new_boundary.push_back(
298 {.source_room = room_exit.destination_room, 343 {.source_room = room_exit.destination_room,
299 .destination_room = GD_GetRoomByName("Outside The Undeterred"), 344 .destination_room = GD_GetRoomByName("Color Hallways"),
300 .type = EntranceType::kPainting}); 345 .type = EntranceType::kPainting});
301 } 346 }
302 347
303 if (AP_IsPilgrimageEnabled()) { 348 if (AP_IsPilgrimageEnabled()) {
304 int pilgrimage_start_id = GD_GetRoomByName("Hub Room"); 349 int pilgrimage_start_id = GD_GetRoomByName("Hub Room");
305 if (AP_IsSunwarpShuffle()) { 350 if (AP_IsSunwarpShuffle()) {
306 for (const auto& [start_index, mapping] : 351 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
307 AP_GetSunwarpMapping()) {
308 if (mapping.dots == 1) { 352 if (mapping.dots == 1) {
309 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index); 353 pilgrimage_start_id = GD_GetRoomForSunwarp(start_index);
310 } 354 }
@@ -343,6 +387,10 @@ class StateCalculator {
343 // evaluated. 387 // evaluated.
344 for (const Door& door : GD_GetDoors()) { 388 for (const Door& door : GD_GetDoors()) {
345 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]);
346 } 394 }
347 } 395 }
348 396
@@ -378,7 +426,8 @@ class StateCalculator {
378 } 426 }
379 427
380 private: 428 private:
381 Decision IsNonGroupedDoorReachable(const Door& door_obj) { 429 template <typename T>
430 Decision IsNonGroupedDoorReachable(const T& door_obj) {
382 bool has_item = AP_HasItem(door_obj.ap_item_id); 431 bool has_item = AP_HasItem(door_obj.ap_item_id);
383 432
384 if (!has_item) { 433 if (!has_item) {
@@ -399,29 +448,48 @@ class StateCalculator {
399 return kNo; 448 return kNo;
400 } 449 }
401 450
451 if (reqs.postgame && options_.postgame_detection) {
452 return kNo;
453 }
454
402 Decision final_decision = kYes; 455 Decision final_decision = kYes;
403 456
404 for (int door_id : reqs.doors) { 457 if (!options_.postgame_detection) {
405 const Door& door_obj = GD_GetDoor(door_id); 458 for (int door_id : reqs.doors) {
406 Decision decision = IsNonGroupedDoorReachable(door_obj); 459 const Door& door_obj = GD_GetDoor(door_id);
460 Decision decision = IsNonGroupedDoorReachable(door_obj);
407 461
408 if (report) { 462 if (report) {
409 (*report)[door_obj.item_name] = (decision == kYes); 463 (*report)[door_obj.item_name] = (decision == kYes);
410 } 464 }
411 465
412 if (decision != kYes) { 466 if (decision != kYes) {
413 final_decision = decision; 467 final_decision = decision;
468 }
414 } 469 }
415 }
416 470
417 for (int item_id : reqs.items) { 471 for (int panel_door_id : reqs.panel_doors) {
418 bool has_item = AP_HasItem(item_id); 472 const PanelDoor& panel_door_obj = GD_GetPanelDoor(panel_door_id);
419 if (report) { 473 Decision decision = IsNonGroupedDoorReachable(panel_door_obj);
420 (*report)[AP_GetItemName(item_id)] = has_item; 474
475 if (report) {
476 (*report)[panel_door_obj.item_name] = (decision == kYes);
477 }
478
479 if (decision != kYes) {
480 final_decision = decision;
481 }
421 } 482 }
422 483
423 if (!has_item) { 484 for (int item_id : reqs.items) {
424 final_decision = kNo; 485 bool has_item = AP_HasItem(item_id);
486 if (report) {
487 (*report)[GD_GetItemName(item_id)] = has_item;
488 }
489
490 if (!has_item) {
491 final_decision = kNo;
492 }
425 } 493 }
426 } 494 }
427 495
@@ -490,14 +558,7 @@ class StateCalculator {
490 } 558 }
491 559
492 Decision IsDoorReachable_Helper(int door_id) { 560 Decision IsDoorReachable_Helper(int door_id) {
493 if (door_report_.count(door_id)) { 561 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id));
494 door_report_[door_id].clear();
495 } else {
496 door_report_[door_id] = {};
497 }
498
499 return AreRequirementsSatisfied(GetState().requirements.GetDoor(door_id),
500 &door_report_[door_id]);
501 } 562 }
502 563
503 Decision IsDoorReachable(int door_id) { 564 Decision IsDoorReachable(int door_id) {
@@ -549,7 +610,7 @@ class StateCalculator {
549 if (AP_IsSunwarpShuffle()) { 610 if (AP_IsSunwarpShuffle()) {
550 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5); 611 pilgrimage_pairs = std::vector<std::tuple<int, int>>(5);
551 612
552 for (const auto& [start_index, mapping] : AP_GetSunwarpMapping()) { 613 for (const auto& [start_index, mapping] : sunwarp_mapping_) {
553 if (mapping.dots > 1) { 614 if (mapping.dots > 1) {
554 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index; 615 std::get<1>(pilgrimage_pairs[mapping.dots - 2]) = start_index;
555 } 616 }
@@ -620,23 +681,94 @@ class StateCalculator {
620 bool pilgrimage_doable_ = false; 681 bool pilgrimage_doable_ = false;
621 682
622 std::map<int, std::list<int>> paths_; 683 std::map<int, std::list<int>> paths_;
684
685 std::map<std::string, std::string> painting_mapping_;
686 std::set<std::string> checked_paintings_;
687 std::map<int, SunwarpMapping> sunwarp_mapping_;
623}; 688};
624 689
625} // namespace 690} // namespace
626 691
627void ResetReachabilityRequirements() { 692void ResetReachabilityRequirements() {
693 TrackerLog("Resetting tracker state...");
694
628 std::lock_guard reachability_guard(GetState().reachability_mutex); 695 std::lock_guard reachability_guard(GetState().reachability_mutex);
629 GetState().requirements.Reset(); 696 GetState().requirements.Reset();
697 GetState().reachable_doors.clear();
698 GetState().solveable_panels.clear();
699
700 if (AP_IsPostgameShuffle()) {
701 GetState().non_postgame_areas.clear();
702 GetState().non_postgame_locations.clear();
703 GetState().non_postgame_paintings.clear();
704 } else {
705 StateCalculator postgame_calculator(
706 {.start = GD_GetRoomByName("Menu"), .postgame_detection = true});
707 postgame_calculator.Calculate();
708
709 std::set<int>& non_postgame_areas = GetState().non_postgame_areas;
710 non_postgame_areas.clear();
711
712 std::set<int>& non_postgame_locations = GetState().non_postgame_locations;
713 non_postgame_locations.clear();
714
715 const std::set<int>& reachable_rooms =
716 postgame_calculator.GetReachableRooms();
717 const std::set<int>& solveable_panels =
718 postgame_calculator.GetSolveablePanels();
719
720 for (const MapArea& map_area : GD_GetMapAreas()) {
721 bool area_reachable = false;
722
723 for (const Location& location_section : map_area.locations) {
724 bool reachable = reachable_rooms.count(location_section.room);
725 if (reachable) {
726 for (int panel_id : location_section.panels) {
727 reachable &= (solveable_panels.count(panel_id) == 1);
728 }
729 }
730
731 if (!reachable && IsLocationWinCondition(location_section)) {
732 reachable = true;
733 }
734
735 if (reachable) {
736 non_postgame_locations.insert(location_section.ap_location_id);
737 area_reachable = true;
738 }
739 }
740
741 for (int painting_id : map_area.paintings) {
742 if (postgame_calculator.GetReachablePaintings().count(painting_id)) {
743 area_reachable = true;
744 }
745 }
746
747 if (area_reachable) {
748 non_postgame_areas.insert(map_area.id);
749 }
750 }
751
752 GetState().non_postgame_paintings =
753 postgame_calculator.GetReachablePaintings();
754 }
630} 755}
631 756
632void RecalculateReachability() { 757void RecalculateReachability() {
758 TrackerLog("Calculating reachability...");
759
633 std::lock_guard reachability_guard(GetState().reachability_mutex); 760 std::lock_guard reachability_guard(GetState().reachability_mutex);
634 761
762 // Receiving items and checking paintings should never remove access to doors
763 // or panels, so we can preload any doors and panels we already know are
764 // accessible from previous runs, in order to reduce the work.
635 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")}); 765 StateCalculator state_calculator({.start = GD_GetRoomByName("Menu")});
766 state_calculator.PreloadDoors(GetState().reachable_doors);
767 state_calculator.PreloadPanels(GetState().solveable_panels);
636 state_calculator.Calculate(); 768 state_calculator.Calculate();
637 769
638 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms(); 770 const std::set<int>& reachable_rooms = state_calculator.GetReachableRooms();
639 const std::set<int>& solveable_panels = state_calculator.GetSolveablePanels(); 771 std::set<int> solveable_panels = state_calculator.GetSolveablePanels();
640 772
641 std::map<int, bool> new_reachability; 773 std::map<int, bool> new_reachability;
642 for (const MapArea& map_area : GD_GetMapAreas()) { 774 for (const MapArea& map_area : GD_GetMapAreas()) {
@@ -667,6 +799,7 @@ void RecalculateReachability() {
667 799
668 std::swap(GetState().reachability, new_reachability); 800 std::swap(GetState().reachability, new_reachability);
669 std::swap(GetState().reachable_doors, new_reachable_doors); 801 std::swap(GetState().reachable_doors, new_reachable_doors);
802 std::swap(GetState().solveable_panels, solveable_panels);
670 std::swap(GetState().reachable_paintings, reachable_paintings); 803 std::swap(GetState().reachable_paintings, reachable_paintings);
671 std::swap(GetState().door_reports, door_reports); 804 std::swap(GetState().door_reports, door_reports);
672 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable(); 805 GetState().pilgrimage_doable = state_calculator.IsPilgrimageDoable();
@@ -705,3 +838,33 @@ bool IsPilgrimageDoable() {
705 838
706 return GetState().pilgrimage_doable; 839 return GetState().pilgrimage_doable;
707} 840}
841
842bool IsAreaPostgame(int area_id) {
843 std::lock_guard reachability_guard(GetState().reachability_mutex);
844
845 if (GetState().non_postgame_areas.empty()) {
846 return false;
847 } else {
848 return !GetState().non_postgame_areas.count(area_id);
849 }
850}
851
852bool IsLocationPostgame(int location_id) {
853 std::lock_guard reachability_guard(GetState().reachability_mutex);
854
855 if (GetState().non_postgame_locations.empty()) {
856 return false;
857 } else {
858 return !GetState().non_postgame_locations.count(location_id);
859 }
860}
861
862bool IsPaintingPostgame(int painting_id) {
863 std::lock_guard reachability_guard(GetState().reachability_mutex);
864
865 if (GetState().non_postgame_paintings.empty()) {
866 return false;
867 } else {
868 return !GetState().non_postgame_paintings.count(painting_id);
869 }
870}
diff --git a/src/tracker_state.h b/src/tracker_state.h index a8f155d..8f1002f 100644 --- a/src/tracker_state.h +++ b/src/tracker_state.h
@@ -18,4 +18,10 @@ const std::map<std::string, bool>& GetDoorRequirements(int door_id);
18 18
19bool IsPilgrimageDoable(); 19bool IsPilgrimageDoable();
20 20
21bool IsAreaPostgame(int area_id);
22
23bool IsLocationPostgame(int location_id);
24
25bool IsPaintingPostgame(int painting_id);
26
21#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */ 27#endif /* end of include guard: TRACKER_STATE_H_8639BC90 */
diff --git a/src/updater.cpp b/src/updater.cpp new file mode 100644 index 0000000..2b05daf --- /dev/null +++ b/src/updater.cpp
@@ -0,0 +1,309 @@
1#include "updater.h"
2
3#include <fmt/core.h>
4#include <openssl/evp.h>
5#include <openssl/sha.h>
6#include <wx/evtloop.h>
7#include <wx/progdlg.h>
8#include <wx/webrequest.h>
9#include <wx/wfstream.h>
10#include <wx/zipstrm.h>
11#include <yaml-cpp/yaml.h>
12
13#include <cstdio>
14#include <deque>
15#include <filesystem>
16#include <fstream>
17
18#include "global.h"
19#include "logger.h"
20#include "version.h"
21
22constexpr const char* kVersionFileUrl =
23 "https://code.fourisland.com/lingo-ap-tracker/plain/VERSION.yaml";
24constexpr const char* kChangelogUrl =
25 "https://code.fourisland.com/lingo-ap-tracker/about/CHANGELOG.md";
26
27namespace {
28
29std::string CalculateStringSha256(const wxString& data) {
30 unsigned char hash[SHA256_DIGEST_LENGTH];
31 EVP_MD_CTX* sha256 = EVP_MD_CTX_new();
32 EVP_DigestInit(sha256, EVP_sha256());
33 EVP_DigestUpdate(sha256, data.c_str(), data.length());
34 EVP_DigestFinal_ex(sha256, hash, nullptr);
35 EVP_MD_CTX_free(sha256);
36
37 char output[65] = {0};
38 for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
39 snprintf(output + (i * 2), 3, "%02x", hash[i]);
40 }
41
42 return std::string(output);
43}
44
45} // namespace
46
47Updater::Updater(wxFrame* parent) : parent_(parent) {
48 Bind(wxEVT_WEBREQUEST_STATE, &Updater::OnWebRequestState, this);
49}
50
51void Updater::Cleanup() {
52 std::filesystem::path oldDir = GetExecutableDirectory() / "old";
53 if (std::filesystem::is_directory(oldDir)) {
54 std::filesystem::remove_all(oldDir);
55 }
56}
57
58void Updater::CheckForUpdates(bool invisible) {
59 wxWebRequest versionRequest =
60 wxWebSession::GetDefault().CreateRequest(this, kVersionFileUrl);
61
62 if (invisible) {
63 update_state_ = UpdateState::GetVersionInvisible;
64
65 versionRequest.Start();
66 } else {
67 update_state_ = UpdateState::GetVersionManual;
68
69 if (DownloadWithProgress(versionRequest)) {
70 if (versionRequest.GetState() == wxWebRequest::State_Failed) {
71 wxMessageBox("Could not check for updates.", "Error",
72 wxOK | wxICON_ERROR);
73 } else if (versionRequest.GetState() == wxWebRequest::State_Completed) {
74 ProcessVersionFile(
75 versionRequest.GetResponse().AsString().utf8_string());
76 }
77 }
78 }
79}
80
81void Updater::OnWebRequestState(wxWebRequestEvent& evt) {
82 if (update_state_ == UpdateState::GetVersionInvisible) {
83 if (evt.GetState() == wxWebRequest::State_Completed) {
84 ProcessVersionFile(evt.GetResponse().AsString().utf8_string());
85 } else if (evt.GetState() == wxWebRequest::State_Failed) {
86 parent_->SetStatusText("Could not check for updates.");
87 }
88 }
89}
90
91void Updater::ProcessVersionFile(std::string data) {
92 try {
93 YAML::Node versionInfo = YAML::Load(data);
94 Version latestVersion(versionInfo["version"].as<std::string>());
95
96 if (kTrackerVersion < latestVersion) {
97 if (versionInfo["packages"]) {
98 std::string platformIdentifier;
99
100 if (wxPlatformInfo::Get().GetOperatingSystemId() == wxOS_WINDOWS_NT) {
101 platformIdentifier = "win64";
102 }
103
104 if (!platformIdentifier.empty() &&
105 versionInfo["packages"][platformIdentifier]) {
106 wxMessageDialog dialog(
107 nullptr,
108 fmt::format("There is a newer version of Lingo AP Tracker "
109 "available. You have {}, and the latest version is "
110 "{}. Would you like to update?",
111 kTrackerVersion.ToString(), latestVersion.ToString()),
112 "Update available", wxYES_NO | wxCANCEL);
113 dialog.SetYesNoLabels("Install update", "Open changelog");
114
115 int dlgResult = dialog.ShowModal();
116 if (dlgResult == wxID_YES) {
117 const YAML::Node& packageInfo =
118 versionInfo["packages"][platformIdentifier];
119 std::string packageUrl = packageInfo["url"].as<std::string>();
120 std::string packageChecksum =
121 packageInfo["checksum"].as<std::string>();
122
123 std::vector<std::filesystem::path> packageFiles;
124 if (packageInfo["files"]) {
125 for (const YAML::Node& filename : packageInfo["files"]) {
126 packageFiles.push_back(filename.as<std::string>());
127 }
128 }
129
130 std::vector<std::filesystem::path> deletedFiles;
131 if (packageInfo["deleted_files"]) {
132 for (const YAML::Node& filename : packageInfo["deleted_files"]) {
133 deletedFiles.push_back(filename.as<std::string>());
134 }
135 }
136
137 InstallUpdate(packageUrl, packageChecksum, packageFiles,
138 deletedFiles);
139 } else if (dlgResult == wxID_NO) {
140 wxLaunchDefaultBrowser(kChangelogUrl);
141 }
142
143 return;
144 }
145 }
146
147 if (wxMessageBox(
148 fmt::format("There is a newer version of Lingo AP Tracker "
149 "available. You have {}, and the latest version is "
150 "{}. Would you like to update?",
151 kTrackerVersion.ToString(), latestVersion.ToString()),
152 "Update available", wxYES_NO) == wxYES) {
153 wxLaunchDefaultBrowser(kChangelogUrl);
154 }
155 } else if (update_state_ == UpdateState::GetVersionManual) {
156 wxMessageBox("Lingo AP Tracker is up to date!", "Lingo AP Tracker", wxOK);
157 }
158 } catch (const std::exception& ex) {
159 wxMessageBox("Could not check for updates.", "Error", wxOK | wxICON_ERROR);
160 }
161}
162
163void Updater::InstallUpdate(std::string url, std::string checksum,
164 std::vector<std::filesystem::path> files,
165 std::vector<std::filesystem::path> deletedFiles) {
166 update_state_ = UpdateState::GetPackage;
167
168 wxWebRequest packageRequest =
169 wxWebSession::GetDefault().CreateRequest(this, url);
170
171 if (!DownloadWithProgress(packageRequest)) {
172 return;
173 }
174
175 bool download_issue = false;
176
177 wxFileName package_path;
178 package_path.AssignTempFileName("");
179
180 if (!package_path.IsOk()) {
181 download_issue = true;
182 } else {
183 wxFileOutputStream writeOut(package_path.GetFullPath());
184 wxString fileData = packageRequest.GetResponse().AsString();
185 writeOut.WriteAll(fileData.c_str(), fileData.length());
186
187 std::string downloadedChecksum = CalculateStringSha256(fileData);
188 if (downloadedChecksum != checksum) {
189 download_issue = true;
190 }
191 }
192
193 if (download_issue) {
194 if (wxMessageBox("There was an issue downloading the update. Would you "
195 "like to manually download it instead?",
196 "Error", wxYES_NO | wxICON_ERROR) == wxID_YES) {
197 wxLaunchDefaultBrowser(kChangelogUrl);
198 }
199 return;
200 }
201
202 std::filesystem::path newArea = GetExecutableDirectory();
203 std::filesystem::path oldArea = newArea / "old";
204 std::set<std::filesystem::path> folders;
205 std::set<std::filesystem::path> filesToMove;
206 for (const std::filesystem::path& existingFile : files) {
207 std::filesystem::path movedPath = oldArea / existingFile;
208 std::filesystem::path movedDir = movedPath;
209 movedDir.remove_filename();
210 folders.insert(movedDir);
211 filesToMove.insert(existingFile);
212 }
213 for (const std::filesystem::path& existingFile : deletedFiles) {
214 std::filesystem::path movedPath = oldArea / existingFile;
215 std::filesystem::path movedDir = movedPath;
216 movedDir.remove_filename();
217 folders.insert(movedDir);
218 }
219
220 for (const std::filesystem::path& newFolder : folders) {
221 TrackerLog(fmt::format("Creating directory {}", newFolder.string()));
222
223 std::filesystem::create_directories(newFolder);
224 }
225
226 for (const std::filesystem::path& existingFile : files) {
227 std::filesystem::path existingPath = newArea / existingFile;
228
229 if (std::filesystem::is_regular_file(existingPath)) {
230 std::filesystem::path movedPath = oldArea / existingFile;
231
232 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
233 movedPath.string()));
234
235 std::filesystem::rename(existingPath, movedPath);
236 }
237 }
238 for (const std::filesystem::path& existingFile : deletedFiles) {
239 std::filesystem::path existingPath = newArea / existingFile;
240
241 if (std::filesystem::is_regular_file(existingPath)) {
242 std::filesystem::path movedPath = oldArea / existingFile;
243
244 TrackerLog(fmt::format("Moving {} -> {}", existingPath.string(),
245 movedPath.string()));
246
247 std::filesystem::rename(existingPath, movedPath);
248 }
249 }
250
251 wxFileInputStream fileInputStream(package_path.GetFullPath());
252 wxZipInputStream zipStream(fileInputStream);
253 std::unique_ptr<wxZipEntry> zipEntry;
254 while ((zipEntry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) !=
255 nullptr) {
256 if (zipEntry->IsDir()) {
257 continue;
258 }
259
260 std::filesystem::path archivePath = zipEntry->GetName().utf8_string();
261
262 TrackerLog(fmt::format("Found {} in archive", archivePath.string()));
263
264 // Cut off the root folder name
265 std::filesystem::path subPath;
266 for (auto it = std::next(archivePath.begin()); it != archivePath.end();
267 it++) {
268 subPath /= *it;
269 }
270
271 std::filesystem::path pastePath = newArea / subPath;
272
273 wxFileOutputStream fileOutput(pastePath.string());
274 zipStream.Read(fileOutput);
275 }
276
277 if (wxMessageBox(
278 "Update installed! The tracker must be restarted for the changes to take "
279 "effect. Do you want to close the tracker?",
280 "Update installed", wxYES_NO) == wxYES) {
281 wxExit();
282 }
283}
284
285bool Updater::DownloadWithProgress(wxWebRequest& request) {
286 request.Start();
287
288 wxProgressDialog dialog("Checking for updates...", "Checking for updates...",
289 100, nullptr,
290 wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_CAN_ABORT |
291 wxPD_ELAPSED_TIME | wxPD_REMAINING_TIME);
292 while (request.GetState() != wxWebRequest::State_Completed &&
293 request.GetState() != wxWebRequest::State_Failed) {
294 if (request.GetBytesExpectedToReceive() == -1) {
295 if (!dialog.Pulse()) {
296 request.Cancel();
297 return false;
298 }
299 } else {
300 dialog.SetRange(request.GetBytesExpectedToReceive());
301 if (!dialog.Update(request.GetBytesReceived())) {
302 request.Cancel();
303 return false;
304 }
305 }
306 }
307
308 return true;
309}
diff --git a/src/updater.h b/src/updater.h new file mode 100644 index 0000000..c604a49 --- /dev/null +++ b/src/updater.h
@@ -0,0 +1,46 @@
1#ifndef UPDATER_H_809E7381
2#define UPDATER_H_809E7381
3
4#include <filesystem>
5#include <set>
6#include <string>
7
8#include <wx/wxprec.h>
9
10#ifndef WX_PRECOMP
11#include <wx/wx.h>
12#endif
13
14class wxWebRequest;
15class wxWebRequestEvent;
16
17class Updater : public wxEvtHandler {
18 public:
19 explicit Updater(wxFrame* parent);
20
21 void Cleanup();
22
23 void CheckForUpdates(bool invisible);
24
25 private:
26 enum class UpdateState {
27 GetVersionInvisible,
28 GetVersionManual,
29 GetPackage,
30 };
31
32 void OnWebRequestState(wxWebRequestEvent& event);
33
34 void ProcessVersionFile(std::string data);
35
36 void InstallUpdate(std::string url, std::string checksum,
37 std::vector<std::filesystem::path> files,
38 std::vector<std::filesystem::path> deletedFiles);
39
40 bool DownloadWithProgress(wxWebRequest& request);
41
42 wxFrame* parent_;
43 UpdateState update_state_ = UpdateState::GetVersionInvisible;
44};
45
46#endif /* end of include guard: UPDATER_H_809E7381 */
diff --git a/src/version.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 24c04b4..3439fda 100644 --- a/src/version.h +++ b/src/version.h
@@ -36,6 +36,6 @@ struct Version {
36 } 36 }
37}; 37};
38 38
39constexpr const Version kTrackerVersion = Version(0, 11, 1); 39constexpr const Version kTrackerVersion = Version(2, 0, 2);
40 40
41#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file 41#endif /* end of include guard: VERSION_H_C757E53C */ \ No newline at end of file
diff --git a/src/windows.rc b/src/windows.rc new file mode 100644 index 0000000..8ba30ed --- /dev/null +++ b/src/windows.rc
@@ -0,0 +1,3 @@
1#define wxUSE_RC_MANIFEST 1
2#define wxUSE_DPI_AWARE_MANIFEST 2
3#include "wx/msw/wx.rc"