about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorStar Rauchenberger <fefferburbia@gmail.com>2024-12-19 22:48:41 -0500
committerStar Rauchenberger <fefferburbia@gmail.com>2024-12-19 22:48:41 -0500
commit5a7559e39d2cd8306a99adbc6d39e90716b14687 (patch)
treeab8e99c780b6a1dd6d27f0521f052e20bd7e3798
parent4f2ac8d575549501e29a7c0088293db2e5e236dd (diff)
downloadlingo-ap-tracker-5a7559e39d2cd8306a99adbc6d39e90716b14687.tar.gz
lingo-ap-tracker-5a7559e39d2cd8306a99adbc6d39e90716b14687.tar.bz2
lingo-ap-tracker-5a7559e39d2cd8306a99adbc6d39e90716b14687.zip
Rewrote APState so connection happens on thread
The whole file is more thread-safe than before, with a few notable exceptions. This fixes a read-after-free issue where, when reconnecting after a disconnection, the client thread would attempt to lock a mutex owned and already destroyed by the main thread.
-rw-r--r--src/ap_state.cpp684
-rw-r--r--src/ap_state.h8
2 files changed, 418 insertions, 274 deletions
diff --git a/src/ap_state.cpp b/src/ap_state.cpp index 80d96a1..908c3a8 100644 --- a/src/ap_state.cpp +++ b/src/ap_state.cpp
@@ -34,24 +34,31 @@ constexpr int AP_REVISION = 5;
34constexpr const char* CERT_STORE_PATH = "cacert.pem"; 34constexpr const char* CERT_STORE_PATH = "cacert.pem";
35constexpr int ITEM_HANDLING = 7; // <- all 35constexpr int ITEM_HANDLING = 7; // <- all
36 36
37constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds
38constexpr int CONNECTION_BACKOFF_INTERVAL = 100;
39
37namespace { 40namespace {
38 41
39struct APState { 42struct APState {
40 std::unique_ptr<APClient> apclient; 43 // Initialized on main thread
41
42 bool initialized = false; 44 bool initialized = false;
43
44 TrackerFrame* tracker_frame = nullptr; 45 TrackerFrame* tracker_frame = nullptr;
46 std::list<std::string> tracked_data_storage_keys;
45 47
46 bool client_active = false; 48 // Client
47 std::string status_message = "Not connected to Archipelago.";
48 std::mutex client_mutex; 49 std::mutex client_mutex;
50 std::unique_ptr<APClient> apclient;
51
52 // Protected state
53 std::mutex state_mutex;
54
55 std::string status_message = "Not connected to Archipelago.";
49 56
50 bool connected = false; 57 bool connected = false;
51 bool has_connection_result = false; 58 std::string connection_failure;
59 int remaining_loops = 0;
52 60
53 std::string data_storage_prefix; 61 std::string data_storage_prefix;
54 std::list<std::string> tracked_data_storage_keys;
55 std::string victory_data_storage_key; 62 std::string victory_data_storage_key;
56 63
57 std::string save_name; 64 std::string save_name;
@@ -81,21 +88,175 @@ struct APState {
81 std::map<int, SunwarpMapping> sunwarp_mapping; 88 std::map<int, SunwarpMapping> sunwarp_mapping;
82 89
83 void Connect(std::string server, std::string player, std::string password) { 90 void Connect(std::string server, std::string player, std::string password) {
91 Initialize();
92
93 {
94 std::lock_guard state_guard(state_mutex);
95 SetStatusMessage("Connecting to Archipelago server....");
96 }
97 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
98
99 // Creating and setting up the client has to all be done while holding the
100 // client mutex, so that the other thread doesn't try to poll before we add
101 // handlers, etc.
102 {
103 TrackerLog("Destroying old AP client...");
104
105 std::lock_guard client_guard(client_mutex);
106
107 if (apclient) {
108 DestroyClient();
109 }
110
111 std::string cert_store = "";
112 if (std::filesystem::exists(CERT_STORE_PATH)) {
113 cert_store = CERT_STORE_PATH;
114 }
115
116 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server,
117 cert_store);
118
119 {
120 std::lock_guard state_guard(state_mutex);
121
122 connected = false;
123 connection_failure.clear();
124 remaining_loops = CONNECTION_TIMEOUT / CONNECTION_BACKOFF_INTERVAL;
125
126 save_name.clear();
127 inventory.clear();
128 checked_locations.clear();
129 data_storage.clear();
130 player_pos = std::nullopt;
131 victory_data_storage_key.clear();
132 door_shuffle_mode = kNO_DOORS;
133 group_doors = false;
134 color_shuffle = false;
135 painting_shuffle = false;
136 painting_mapping.clear();
137 painting_codomain.clear();
138 mastery_requirement = 21;
139 level_2_requirement = 223;
140 location_checks = kNORMAL_LOCATIONS;
141 victory_condition = kTHE_END;
142 early_color_hallways = false;
143 pilgrimage_enabled = false;
144 pilgrimage_allows_roof_access = false;
145 pilgrimage_allows_paintings = false;
146 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
147 sunwarp_shuffle = false;
148 sunwarp_mapping.clear();
149 }
150
151 apclient->set_room_info_handler(
152 [this, player, password]() { OnRoomInfo(player, password); });
153
154 apclient->set_location_checked_handler(
155 [this](const std::list<int64_t>& locations) {
156 OnLocationChecked(locations);
157 });
158
159 apclient->set_slot_disconnected_handler(
160 [this]() { OnSlotDisconnected(); });
161
162 apclient->set_socket_disconnected_handler(
163 [this]() { OnSocketDisconnected(); });
164
165 apclient->set_items_received_handler(
166 [this](const std::list<APClient::NetworkItem>& items) {
167 OnItemsReceived(items);
168 });
169
170 apclient->set_retrieved_handler(
171 [this](const std::map<std::string, nlohmann::json>& data) {
172 OnRetrieved(data);
173 });
174
175 apclient->set_set_reply_handler(
176 [this](const std::string& key, const nlohmann::json& value,
177 const nlohmann::json&) { OnSetReply(key, value); });
178
179 apclient->set_slot_connected_handler(
180 [this, player, server](const nlohmann::json& slot_data) {
181 OnSlotConnected(player, server, slot_data);
182 });
183
184 apclient->set_slot_refused_handler(
185 [this](const std::list<std::string>& errors) {
186 OnSlotRefused(errors);
187 });
188 }
189 }
190
191 std::string GetStatusMessage() {
192 std::lock_guard state_guard(state_mutex);
193
194 return status_message;
195 }
196
197 bool HasCheckedGameLocation(int location_id) {
198 std::lock_guard state_guard(state_mutex);
199
200 return checked_locations.count(location_id);
201 }
202
203 bool HasCheckedHuntPanel(int location_id) {
204 std::lock_guard state_guard(state_mutex);
205
206 std::string key =
207 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
208 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
209 }
210
211 bool HasItem(int item_id, int quantity) {
212 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
213 }
214
215 bool HasAchievement(const std::string& name) {
216 std::lock_guard state_guard(state_mutex);
217
218 std::string key =
219 fmt::format("{}Achievement|{}", data_storage_prefix, name);
220 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
221 }
222
223 const std::set<std::string>& GetCheckedPaintings() {
224 std::lock_guard state_guard(state_mutex);
225
226 std::string key = fmt::format("{}Paintings", data_storage_prefix);
227 if (!data_storage.count(key)) {
228 data_storage[key] = std::set<std::string>();
229 }
230
231 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
232 }
233
234 bool IsPaintingChecked(const std::string& painting_id) {
235 const auto& checked_paintings = GetCheckedPaintings();
236
237 std::lock_guard state_guard(state_mutex);
238
239 return checked_paintings.count(painting_id) ||
240 (painting_mapping.count(painting_id) &&
241 checked_paintings.count(painting_mapping.at(painting_id)));
242 }
243
244 std::string GetItemName(int id) { return apclient->get_item_name(id); }
245
246 bool HasReachedGoal() {
247 std::lock_guard state_guard(state_mutex);
248
249 return data_storage.count(victory_data_storage_key) &&
250 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
251 30; // CLIENT_GOAL
252 }
253
254 private:
255 void Initialize() {
84 if (!initialized) { 256 if (!initialized) {
85 TrackerLog("Initializing APState..."); 257 TrackerLog("Initializing APState...");
86 258
87 std::thread([this]() { 259 std::thread([this]() { Thread(); }).detach();
88 for (;;) {
89 {
90 std::lock_guard client_guard(client_mutex);
91 if (apclient) {
92 apclient->poll();
93 }
94 }
95
96 std::this_thread::sleep_for(std::chrono::milliseconds(100));
97 }
98 }).detach();
99 260
100 for (int panel_id : GD_GetAchievementPanels()) { 261 for (int panel_id : GD_GetAchievementPanels()) {
101 tracked_data_storage_keys.push_back(fmt::format( 262 tracked_data_storage_keys.push_back(fmt::format(
@@ -114,130 +275,148 @@ struct APState {
114 275
115 initialized = true; 276 initialized = true;
116 } 277 }
278 }
117 279
118 { 280 void Thread() {
119 std::lock_guard client_guard(client_mutex); 281 std::string display_error;
120 SetStatusMessage("Connecting to Archipelago server....");
121 }
122 TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
123 282
124 { 283 for (;;) {
125 TrackerLog("Destroying old AP client..."); 284 {
285 std::lock_guard client_guard(client_mutex);
286 if (apclient) {
287 apclient->poll();
126 288
127 std::lock_guard client_guard(client_mutex); 289 {
290 std::lock_guard state_guard(state_mutex);
128 291
129 if (apclient) { 292 if (!connected) {
130 DestroyClient(); 293 if (!connection_failure.empty()) {
294 TrackerLog(connection_failure);
295
296 display_error = connection_failure;
297 connection_failure.clear();
298
299 DestroyClient();
300 } else {
301 remaining_loops--;
302
303 if (remaining_loops <= 0) {
304 DestroyClient();
305
306 SetStatusMessage("Disconnected from Archipelago.");
307 TrackerLog("Timeout while connecting to Archipelago server.");
308
309 display_error =
310 "Timeout while connecting to Archipelago server.";
311 }
312 }
313 }
314 }
315 }
131 } 316 }
132 317
133 std::string cert_store = ""; 318 if (!display_error.empty()) {
134 if (std::filesystem::exists(CERT_STORE_PATH)) { 319 wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR);
135 cert_store = CERT_STORE_PATH; 320 display_error.clear();
136 } 321 }
137 322
138 apclient = std::make_unique<APClient>(ap_get_uuid(""), "Lingo", server, 323 std::this_thread::sleep_for(std::chrono::milliseconds(100));
139 cert_store);
140 } 324 }
325 }
326
327 void OnRoomInfo(std::string player, std::string password) {
328 {
329 std::lock_guard state_guard(state_mutex);
141 330
142 save_name.clear();
143 inventory.clear();
144 checked_locations.clear();
145 data_storage.clear();
146 player_pos = std::nullopt;
147 victory_data_storage_key.clear();
148 door_shuffle_mode = kNO_DOORS;
149 group_doors = false;
150 color_shuffle = false;
151 painting_shuffle = false;
152 painting_mapping.clear();
153 painting_codomain.clear();
154 mastery_requirement = 21;
155 level_2_requirement = 223;
156 location_checks = kNORMAL_LOCATIONS;
157 victory_condition = kTHE_END;
158 early_color_hallways = false;
159 pilgrimage_enabled = false;
160 pilgrimage_allows_roof_access = false;
161 pilgrimage_allows_paintings = false;
162 sunwarp_access = kSUNWARP_ACCESS_NORMAL;
163 sunwarp_shuffle = false;
164 sunwarp_mapping.clear();
165
166 std::mutex connection_mutex;
167 connected = false;
168 has_connection_result = false;
169
170 apclient->set_room_info_handler([this, player, password]() {
171 inventory.clear(); 331 inventory.clear();
172 332
173 TrackerLog(fmt::format(
174 "Connected to Archipelago server. Authenticating as {} {}", player,
175 (password.empty() ? "without password"
176 : "with password " + password)));
177 SetStatusMessage("Connected to Archipelago server. Authenticating..."); 333 SetStatusMessage("Connected to Archipelago server. Authenticating...");
334 }
178 335
179 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"}, 336 TrackerLog(fmt::format(
180 {AP_MAJOR, AP_MINOR, AP_REVISION}); 337 "Connected to Archipelago server. Authenticating as {} {}", player,
181 }); 338 (password.empty() ? "without password" : "with password " + password)));
182 339
183 apclient->set_location_checked_handler( 340 apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
184 [this](const std::list<int64_t>& locations) { 341 {AP_MAJOR, AP_MINOR, AP_REVISION});
185 for (const int64_t location_id : locations) { 342 }
186 checked_locations.insert(location_id);
187 TrackerLog(fmt::format("Location: {}", location_id));
188 }
189 343
190 RefreshTracker(false); 344 void OnLocationChecked(const std::list<int64_t>& locations) {
191 }); 345 {
346 std::lock_guard state_guard(state_mutex);
192 347
193 apclient->set_slot_disconnected_handler([this]() { 348 for (const int64_t location_id : locations) {
194 SetStatusMessage( 349 checked_locations.insert(location_id);
195 "Disconnected from Archipelago. Attempting to reconnect..."); 350 TrackerLog(fmt::format("Location: {}", location_id));
196 TrackerLog( 351 }
197 "Slot disconnected from Archipelago. Attempting to reconnect..."); 352 }
198 });
199 353
200 apclient->set_socket_disconnected_handler([this]() { 354 RefreshTracker(false);
201 SetStatusMessage( 355 }
202 "Disconnected from Archipelago. Attempting to reconnect...");
203 TrackerLog(
204 "Socket disconnected from Archipelago. Attempting to reconnect...");
205 });
206
207 apclient->set_items_received_handler(
208 [this](const std::list<APClient::NetworkItem>& items) {
209 for (const APClient::NetworkItem& item : items) {
210 inventory[item.item]++;
211 TrackerLog(fmt::format("Item: {}", item.item));
212 }
213 356
214 RefreshTracker(false); 357 void OnSlotDisconnected() {
215 }); 358 std::lock_guard state_guard(state_mutex);
216 359
217 apclient->set_retrieved_handler( 360 SetStatusMessage(
218 [this](const std::map<std::string, nlohmann::json>& data) { 361 "Disconnected from Archipelago. Attempting to reconnect...");
219 for (const auto& [key, value] : data) { 362 TrackerLog(
220 HandleDataStorage(key, value); 363 "Slot disconnected from Archipelago. Attempting to reconnect...");
221 } 364 }
222 365
223 RefreshTracker(false); 366 void OnSocketDisconnected() {
224 }); 367 std::lock_guard state_guard(state_mutex);
225 368
226 apclient->set_set_reply_handler([this](const std::string& key, 369 SetStatusMessage(
227 const nlohmann::json& value, 370 "Disconnected from Archipelago. Attempting to reconnect...");
228 const nlohmann::json&) { 371 TrackerLog(
372 "Socket disconnected from Archipelago. Attempting to reconnect...");
373 }
374
375 void OnItemsReceived(const std::list<APClient::NetworkItem>& items) {
376 {
377 std::lock_guard state_guard(state_mutex);
378
379 for (const APClient::NetworkItem& item : items) {
380 inventory[item.item]++;
381 TrackerLog(fmt::format("Item: {}", item.item));
382 }
383 }
384
385 RefreshTracker(false);
386 }
387
388 void OnRetrieved(const std::map<std::string, nlohmann::json>& data) {
389 {
390 std::lock_guard state_guard(state_mutex);
391
392 for (const auto& [key, value] : data) {
393 HandleDataStorage(key, value);
394 }
395 }
396
397 RefreshTracker(false);
398 }
399
400 void OnSetReply(const std::string& key, const nlohmann::json& value) {
401 {
402 std::lock_guard state_guard(state_mutex);
229 HandleDataStorage(key, value); 403 HandleDataStorage(key, value);
230 RefreshTracker(false); 404 }
231 }); 405
406 RefreshTracker(false);
407 }
408
409 void OnSlotConnected(std::string player, std::string server,
410 const nlohmann::json& slot_data) {
411 IPC_SetTrackerSlot(server, player);
412
413 TrackerLog("Connected to Archipelago!");
414
415 {
416 std::lock_guard state_guard(state_mutex);
232 417
233 apclient->set_slot_connected_handler([this, player, server,
234 &connection_mutex](
235 const nlohmann::json& slot_data) {
236 SetStatusMessage( 418 SetStatusMessage(
237 fmt::format("Connected to Archipelago! ({}@{}).", player, server)); 419 fmt::format("Connected to Archipelago! ({}@{}).", player, server));
238 TrackerLog("Connected to Archipelago!");
239
240 IPC_SetTrackerSlot(server, player);
241 420
242 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), 421 save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(),
243 apclient->get_player_number()); 422 apclient->get_player_number());
@@ -248,8 +427,8 @@ struct APState {
248 group_doors = slot_data.contains("group_doors") && 427 group_doors = slot_data.contains("group_doors") &&
249 slot_data["group_doors"].get<int>() == 1; 428 slot_data["group_doors"].get<int>() == 1;
250 } else { 429 } else {
251 // If group_doors doesn't exist yet, that means kPANELS_MODE is actually 430 // If group_doors doesn't exist yet, that means kPANELS_MODE is
252 // kSIMPLE_DOORS. 431 // actually kSIMPLE_DOORS.
253 if (door_shuffle_mode == kPANELS_MODE) { 432 if (door_shuffle_mode == kPANELS_MODE) {
254 door_shuffle_mode = kDOORS_MODE; 433 door_shuffle_mode = kDOORS_MODE;
255 group_doors = true; 434 group_doors = true;
@@ -314,112 +493,53 @@ struct APState {
314 apclient->Get(corrected_keys); 493 apclient->Get(corrected_keys);
315 apclient->SetNotify(corrected_keys); 494 apclient->SetNotify(corrected_keys);
316 495
317 ResetReachabilityRequirements(); 496 connected = true;
318 RefreshTracker(true); 497 }
319
320 {
321 std::lock_guard connection_lock(connection_mutex);
322 if (!has_connection_result) {
323 connected = true;
324 has_connection_result = true;
325 }
326 }
327 });
328
329 apclient->set_slot_refused_handler(
330 [this, &connection_mutex](const std::list<std::string>& errors) {
331 {
332 std::lock_guard connection_lock(connection_mutex);
333 connected = false;
334 has_connection_result = true;
335 }
336
337 SetStatusMessage("Disconnected from Archipelago.");
338
339 std::vector<std::string> error_messages;
340 error_messages.push_back("Could not connect to Archipelago.");
341
342 for (const std::string& error : errors) {
343 if (error == "InvalidSlot") {
344 error_messages.push_back("Invalid player name.");
345 } else if (error == "InvalidGame") {
346 error_messages.push_back(
347 "The specified player is not playing Lingo.");
348 } else if (error == "IncompatibleVersion") {
349 error_messages.push_back(
350 "The Archipelago server is not the correct version for this "
351 "client.");
352 } else if (error == "InvalidPassword") {
353 error_messages.push_back("Incorrect password.");
354 } else if (error == "InvalidItemsHandling") {
355 error_messages.push_back(
356 "Invalid item handling flag. This is a bug with the tracker. "
357 "Please report it to the lingo-ap-tracker GitHub.");
358 } else {
359 error_messages.push_back("Unknown error.");
360 }
361 }
362
363 std::string full_message = hatkirby::implode(error_messages, " ");
364 TrackerLog(full_message);
365
366 wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
367 });
368
369 client_active = true;
370
371 int timeout = 5000; // 5 seconds
372 int interval = 100;
373 int remaining_loops = timeout / interval;
374 while (true) {
375 {
376 std::lock_guard connection_lock(connection_mutex);
377 if (has_connection_result) {
378 break;
379 }
380 }
381
382 if (interval == 0) {
383 DestroyClient();
384 498
385 { 499 ResetReachabilityRequirements();
386 std::lock_guard client_guard(client_mutex); 500 RefreshTracker(true);
387 SetStatusMessage("Disconnected from Archipelago."); 501 }
388 }
389 TrackerLog("Timeout while connecting to Archipelago server.");
390 wxMessageBox("Timeout while connecting to Archipelago server.",
391 "Connection failed", wxOK | wxICON_ERROR);
392
393 {
394 std::lock_guard connection_lock(connection_mutex);
395 connected = false;
396 has_connection_result = true;
397 }
398 502
399 break; 503 void OnSlotRefused(const std::list<std::string>& errors) {
504 std::vector<std::string> error_messages;
505 error_messages.push_back("Could not connect to Archipelago.");
506
507 for (const std::string& error : errors) {
508 if (error == "InvalidSlot") {
509 error_messages.push_back("Invalid player name.");
510 } else if (error == "InvalidGame") {
511 error_messages.push_back("The specified player is not playing Lingo.");
512 } else if (error == "IncompatibleVersion") {
513 error_messages.push_back(
514 "The Archipelago server is not the correct version for this "
515 "client.");
516 } else if (error == "InvalidPassword") {
517 error_messages.push_back("Incorrect password.");
518 } else if (error == "InvalidItemsHandling") {
519 error_messages.push_back(
520 "Invalid item handling flag. This is a bug with the tracker. "
521 "Please report it to the lingo-ap-tracker GitHub.");
522 } else {
523 error_messages.push_back("Unknown error.");
400 } 524 }
401
402 std::this_thread::sleep_for(std::chrono::milliseconds(100));
403
404 interval--;
405 } 525 }
406 526
407 if (connected) { 527 {
408 client_active = false; 528 std::lock_guard state_guard(state_mutex);
409 } 529 connection_failure = hatkirby::implode(error_messages, " ");
410 }
411 530
412 std::string GetStatusMessage() { 531 SetStatusMessage("Disconnected from Archipelago.");
413 std::lock_guard client_guard(client_mutex); 532 }
414 return status_message;
415 } 533 }
416 534
535 // Assumes state mutex is locked.
417 void SetStatusMessage(std::string msg) { 536 void SetStatusMessage(std::string msg) {
418 status_message = std::move(msg); 537 status_message = std::move(msg);
419 538
420 tracker_frame->UpdateStatusMessage(); 539 tracker_frame->UpdateStatusMessage();
421 } 540 }
422 541
542 // Assumes state mutex is locked.
423 void HandleDataStorage(const std::string& key, const nlohmann::json& value) { 543 void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
424 if (value.is_boolean()) { 544 if (value.is_boolean()) {
425 data_storage[key] = value.get<bool>(); 545 data_storage[key] = value.get<bool>();
@@ -461,46 +581,18 @@ struct APState {
461 } 581 }
462 } 582 }
463 583
464 bool HasCheckedGameLocation(int location_id) { 584 // State mutex should NOT be locked.
465 return checked_locations.count(location_id); 585 void RefreshTracker(bool reset) {
466 } 586 TrackerLog("Refreshing display...");
467
468 bool HasCheckedHuntPanel(int location_id) {
469 std::string key =
470 fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
471 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
472 }
473
474 bool HasItem(int item_id, int quantity) {
475 return inventory.count(item_id) && inventory.at(item_id) >= quantity;
476 }
477 587
478 bool HasAchievement(const std::string& name) { 588 std::string prev_msg;
479 std::string key = 589 {
480 fmt::format("{}Achievement|{}", data_storage_prefix, name); 590 std::lock_guard state_guard(state_mutex);
481 return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
482 }
483 591
484 const std::set<std::string>& GetCheckedPaintings() { 592 prev_msg = status_message;
485 std::string key = fmt::format("{}Paintings", data_storage_prefix); 593 SetStatusMessage(fmt::format("{} Recalculating...", status_message));
486 if (!data_storage.count(key)) {
487 data_storage[key] = std::set<std::string>();
488 } 594 }
489 595
490 return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
491 }
492
493 bool IsPaintingChecked(const std::string& painting_id) {
494 const auto& checked_paintings = GetCheckedPaintings();
495
496 return checked_paintings.count(painting_id) ||
497 (painting_mapping.count(painting_id) &&
498 checked_paintings.count(painting_mapping.at(painting_id)));
499 }
500
501 void RefreshTracker(bool reset) {
502 TrackerLog("Refreshing display...");
503
504 RecalculateReachability(); 596 RecalculateReachability();
505 597
506 if (reset) { 598 if (reset) {
@@ -508,27 +600,15 @@ struct APState {
508 } else { 600 } else {
509 tracker_frame->UpdateIndicators(); 601 tracker_frame->UpdateIndicators();
510 } 602 }
511 }
512
513 int64_t GetItemId(const std::string& item_name) {
514 int64_t ap_id = apclient->get_item_id(item_name);
515 if (ap_id == APClient::INVALID_NAME_ID) {
516 TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
517 }
518 603
519 return ap_id; 604 {
520 } 605 std::lock_guard state_guard(state_mutex);
521
522 std::string GetItemName(int id) { return apclient->get_item_name(id); }
523 606
524 bool HasReachedGoal() { 607 SetStatusMessage(prev_msg);
525 return data_storage.count(victory_data_storage_key) && 608 }
526 std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
527 30; // CLIENT_GOAL
528 } 609 }
529 610
530 void DestroyClient() { 611 void DestroyClient() {
531 client_active = false;
532 apclient->reset(); 612 apclient->reset();
533 apclient.reset(); 613 apclient.reset();
534 } 614 }
@@ -549,7 +629,11 @@ void AP_Connect(std::string server, std::string player, std::string password) {
549 629
550std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); } 630std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); }
551 631
552std::string AP_GetSaveName() { return GetState().save_name; } 632std::string AP_GetSaveName() {
633 std::lock_guard state_guard(GetState().state_mutex);
634
635 return GetState().save_name;
636}
553 637
554bool AP_HasCheckedGameLocation(int location_id) { 638bool AP_HasCheckedGameLocation(int location_id) {
555 return GetState().HasCheckedGameLocation(location_id); 639 return GetState().HasCheckedGameLocation(location_id);
@@ -567,19 +651,37 @@ std::string AP_GetItemName(int item_id) {
567 return GetState().GetItemName(item_id); 651 return GetState().GetItemName(item_id);
568} 652}
569 653
570DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; } 654DoorShuffleMode AP_GetDoorShuffleMode() {
655 std::lock_guard state_guard(GetState().state_mutex);
656
657 return GetState().door_shuffle_mode;
658}
571 659
572bool AP_AreDoorsGrouped() { return GetState().group_doors; } 660bool AP_AreDoorsGrouped() {
661 std::lock_guard state_guard(GetState().state_mutex);
662
663 return GetState().group_doors;
664}
573 665
574bool AP_IsColorShuffle() { return GetState().color_shuffle; } 666bool AP_IsColorShuffle() {
667 std::lock_guard state_guard(GetState().state_mutex);
575 668
576bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; } 669 return GetState().color_shuffle;
670}
671
672bool AP_IsPaintingShuffle() {
673 std::lock_guard state_guard(GetState().state_mutex);
674
675 return GetState().painting_shuffle;
676}
577 677
578const std::map<std::string, std::string>& AP_GetPaintingMapping() { 678const std::map<std::string, std::string>& AP_GetPaintingMapping() {
579 return GetState().painting_mapping; 679 return GetState().painting_mapping;
580} 680}
581 681
582bool AP_IsPaintingMappedTo(const std::string& painting_id) { 682bool AP_IsPaintingMappedTo(const std::string& painting_id) {
683 std::lock_guard state_guard(GetState().state_mutex);
684
583 return GetState().painting_codomain.count(painting_id); 685 return GetState().painting_codomain.count(painting_id);
584} 686}
585 687
@@ -591,11 +693,21 @@ bool AP_IsPaintingChecked(const std::string& painting_id) {
591 return GetState().IsPaintingChecked(painting_id); 693 return GetState().IsPaintingChecked(painting_id);
592} 694}
593 695
594int AP_GetMasteryRequirement() { return GetState().mastery_requirement; } 696int AP_GetMasteryRequirement() {
697 std::lock_guard state_guard(GetState().state_mutex);
698
699 return GetState().mastery_requirement;
700}
595 701
596int AP_GetLevel2Requirement() { return GetState().level_2_requirement; } 702int AP_GetLevel2Requirement() {
703 std::lock_guard state_guard(GetState().state_mutex);
704
705 return GetState().level_2_requirement;
706}
597 707
598bool AP_IsLocationVisible(int classification) { 708bool AP_IsLocationVisible(int classification) {
709 std::lock_guard state_guard(GetState().state_mutex);
710
599 int world_state = 0; 711 int world_state = 0;
600 712
601 switch (GetState().location_checks) { 713 switch (GetState().location_checks) {
@@ -621,6 +733,8 @@ bool AP_IsLocationVisible(int classification) {
621} 733}
622 734
623VictoryCondition AP_GetVictoryCondition() { 735VictoryCondition AP_GetVictoryCondition() {
736 std::lock_guard state_guard(GetState().state_mutex);
737
624 return GetState().victory_condition; 738 return GetState().victory_condition;
625} 739}
626 740
@@ -628,21 +742,41 @@ bool AP_HasAchievement(const std::string& achievement_name) {
628 return GetState().HasAchievement(achievement_name); 742 return GetState().HasAchievement(achievement_name);
629} 743}
630 744
631bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; } 745bool AP_HasEarlyColorHallways() {
746 std::lock_guard state_guard(GetState().state_mutex);
747
748 return GetState().early_color_hallways;
749}
750
751bool AP_IsPilgrimageEnabled() {
752 std::lock_guard state_guard(GetState().state_mutex);
632 753
633bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; } 754 return GetState().pilgrimage_enabled;
755}
634 756
635bool AP_DoesPilgrimageAllowRoofAccess() { 757bool AP_DoesPilgrimageAllowRoofAccess() {
758 std::lock_guard state_guard(GetState().state_mutex);
759
636 return GetState().pilgrimage_allows_roof_access; 760 return GetState().pilgrimage_allows_roof_access;
637} 761}
638 762
639bool AP_DoesPilgrimageAllowPaintings() { 763bool AP_DoesPilgrimageAllowPaintings() {
764 std::lock_guard state_guard(GetState().state_mutex);
765
640 return GetState().pilgrimage_allows_paintings; 766 return GetState().pilgrimage_allows_paintings;
641} 767}
642 768
643SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; } 769SunwarpAccess AP_GetSunwarpAccess() {
770 std::lock_guard state_guard(GetState().state_mutex);
644 771
645bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; } 772 return GetState().sunwarp_access;
773}
774
775bool AP_IsSunwarpShuffle() {
776 std::lock_guard state_guard(GetState().state_mutex);
777
778 return GetState().sunwarp_shuffle;
779}
646 780
647const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() { 781const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() {
648 return GetState().sunwarp_mapping; 782 return GetState().sunwarp_mapping;
@@ -651,5 +785,7 @@ const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() {
651bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } 785bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
652 786
653std::optional<std::tuple<int, int>> AP_GetPlayerPosition() { 787std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
788 std::lock_guard state_guard(GetState().state_mutex);
789
654 return GetState().player_pos; 790 return GetState().player_pos;
655} 791}
diff --git a/src/ap_state.h b/src/ap_state.h index f310ee8..a23b13d 100644 --- a/src/ap_state.h +++ b/src/ap_state.h
@@ -51,8 +51,13 @@ bool AP_HasCheckedGameLocation(int location_id);
51 51
52bool AP_HasCheckedHuntPanel(int location_id); 52bool AP_HasCheckedHuntPanel(int location_id);
53 53
54// This doesn't lock the state mutex, for speed, so it must ONLY be called from
55// RecalculateReachability, which is only called from the APState thread anyway.
54bool AP_HasItem(int item_id, int quantity = 1); 56bool AP_HasItem(int item_id, int quantity = 1);
55 57
58// This doesn't lock the client mutex because it is ONLY to be called from
59// RecalculateReachability, which is only called from within a client callback
60// anyway.
56std::string AP_GetItemName(int item_id); 61std::string AP_GetItemName(int item_id);
57 62
58DoorShuffleMode AP_GetDoorShuffleMode(); 63DoorShuffleMode AP_GetDoorShuffleMode();
@@ -63,10 +68,12 @@ bool AP_IsColorShuffle();
63 68
64bool AP_IsPaintingShuffle(); 69bool AP_IsPaintingShuffle();
65 70
71// WARNING: Not thread-safe!
66const std::map<std::string, std::string>& AP_GetPaintingMapping(); 72const std::map<std::string, std::string>& AP_GetPaintingMapping();
67 73
68bool AP_IsPaintingMappedTo(const std::string& painting_id); 74bool AP_IsPaintingMappedTo(const std::string& painting_id);
69 75
76// WARNING: Not thread-safe!
70const std::set<std::string>& AP_GetCheckedPaintings(); 77const std::set<std::string>& AP_GetCheckedPaintings();
71 78
72bool AP_IsPaintingChecked(const std::string& painting_id); 79bool AP_IsPaintingChecked(const std::string& painting_id);
@@ -93,6 +100,7 @@ SunwarpAccess AP_GetSunwarpAccess();
93 100
94bool AP_IsSunwarpShuffle(); 101bool AP_IsSunwarpShuffle();
95 102
103// WARNING: Not thread-safe!
96const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping(); 104const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping();
97 105
98bool AP_HasReachedGoal(); 106bool AP_HasReachedGoal();