#include "ap_state.h" #define HAS_STD_FILESYSTEM #define _WEBSOCKETPP_CPP11_STRICT_ #pragma comment(lib, "crypt32") #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "game_data.h" #include "ipc_state.h" #include "logger.h" #include "tracker_frame.h" #include "tracker_state.h" constexpr int AP_MAJOR = 0; constexpr int AP_MINOR = 4; constexpr int AP_REVISION = 5; constexpr const char* CERT_STORE_PATH = "cacert.pem"; constexpr int ITEM_HANDLING = 7; // <- all constexpr int CONNECTION_TIMEOUT = 50000; // 50 seconds constexpr int CONNECTION_BACKOFF_INTERVAL = 100; namespace { struct APState { // Initialized on main thread bool initialized = false; TrackerFrame* tracker_frame = nullptr; std::list tracked_data_storage_keys; // Client std::mutex client_mutex; std::unique_ptr apclient; // Protected state std::mutex state_mutex; std::string status_message = "Not connected to Archipelago."; bool connected = false; std::string connection_failure; int remaining_loops = 0; std::string data_storage_prefix; std::string victory_data_storage_key; std::string save_name; std::map inventory; std::set checked_locations; std::map data_storage; std::optional> player_pos; DoorShuffleMode door_shuffle_mode = kNO_DOORS; bool group_doors = false; bool color_shuffle = false; bool painting_shuffle = false; int mastery_requirement = 21; int level_2_requirement = 223; LocationChecks location_checks = kNORMAL_LOCATIONS; VictoryCondition victory_condition = kTHE_END; bool early_color_hallways = false; bool pilgrimage_enabled = false; bool pilgrimage_allows_roof_access = false; bool pilgrimage_allows_paintings = false; SunwarpAccess sunwarp_access = kSUNWARP_ACCESS_NORMAL; bool sunwarp_shuffle = false; std::map painting_mapping; std::set painting_codomain; std::map sunwarp_mapping; void Connect(std::string server, std::string player, std::string password) { Initialize(); { std::lock_guard state_guard(state_mutex); SetStatusMessage("Connecting to Archipelago server...."); } TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server)); // Creating and setting up the client has to all be done while holding the // client mutex, so that the other thread doesn't try to poll before we add // handlers, etc. { TrackerLog("Destroying old AP client..."); std::lock_guard client_guard(client_mutex); if (apclient) { DestroyClient(); } std::string cert_store = ""; if (std::filesystem::exists(CERT_STORE_PATH)) { cert_store = CERT_STORE_PATH; } apclient = std::make_unique(ap_get_uuid(""), "Lingo", server, cert_store); { std::lock_guard state_guard(state_mutex); connected = false; connection_failure.clear(); remaining_loops = CONNECTION_TIMEOUT / CONNECTION_BACKOFF_INTERVAL; save_name.clear(); inventory.clear(); checked_locations.clear(); data_storage.clear(); player_pos = std::nullopt; victory_data_storage_key.clear(); door_shuffle_mode = kNO_DOORS; group_doors = false; color_shuffle = false; painting_shuffle = false; painting_mapping.clear(); painting_codomain.clear(); mastery_requirement = 21; level_2_requirement = 223; location_checks = kNORMAL_LOCATIONS; victory_condition = kTHE_END; early_color_hallways = false; pilgrimage_enabled = false; pilgrimage_allows_roof_access = false; pilgrimage_allows_paintings = false; sunwarp_access = kSUNWARP_ACCESS_NORMAL; sunwarp_shuffle = false; sunwarp_mapping.clear(); } apclient->set_room_info_handler( [this, player, password]() { OnRoomInfo(player, password); }); apclient->set_location_checked_handler( [this](const std::list& locations) { OnLocationChecked(locations); }); apclient->set_slot_disconnected_handler( [this]() { OnSlotDisconnected(); }); apclient->set_socket_disconnected_handler( [this]() { OnSocketDisconnected(); }); apclient->set_items_received_handler( [this](const std::list& items) { OnItemsReceived(items); }); apclient->set_retrieved_handler( [this](const std::map& data) { OnRetrieved(data); }); apclient->set_set_reply_handler( [this](const std::string& key, const nlohmann::json& value, const nlohmann::json&) { OnSetReply(key, value); }); apclient->set_slot_connected_handler( [this, player, server](const nlohmann::json& slot_data) { OnSlotConnected(player, server, slot_data); }); apclient->set_slot_refused_handler( [this](const std::list& errors) { OnSlotRefused(errors); }); } } std::string GetStatusMessage() { std::lock_guard state_guard(state_mutex); return status_message; } bool HasCheckedGameLocation(int location_id) { std::lock_guard state_guard(state_mutex); return checked_locations.count(location_id); } bool HasCheckedHuntPanel(int location_id) { std::lock_guard state_guard(state_mutex); std::string key = fmt::format("{}Hunt|{}", data_storage_prefix, location_id); return data_storage.count(key) && std::any_cast(data_storage.at(key)); } bool HasItem(int item_id, int quantity) { return inventory.count(item_id) && inventory.at(item_id) >= quantity; } bool HasAchievement(const std::string& name) { std::lock_guard state_guard(state_mutex); std::string key = fmt::format("{}Achievement|{}", data_storage_prefix, name); return data_storage.count(key) && std::any_cast(data_storage.at(key)); } const std::set& GetCheckedPaintings() { std::lock_guard state_guard(state_mutex); std::string key = fmt::format("{}Paintings", data_storage_prefix); if (!data_storage.count(key)) { data_storage[key] = std::set(); } return std::any_cast&>(data_storage.at(key)); } bool IsPaintingChecked(const std::string& painting_id) { const auto& checked_paintings = GetCheckedPaintings(); std::lock_guard state_guard(state_mutex); return checked_paintings.count(painting_id) || (painting_mapping.count(painting_id) && checked_paintings.count(painting_mapping.at(painting_id))); } std::string GetItemName(int id) { return apclient->get_item_name(id); } bool HasReachedGoal() { std::lock_guard state_guard(state_mutex); return data_storage.count(victory_data_storage_key) && std::any_cast(data_storage.at(victory_data_storage_key)) == 30; // CLIENT_GOAL } private: void Initialize() { if (!initialized) { TrackerLog("Initializing APState..."); std::thread([this]() { Thread(); }).detach(); for (int panel_id : GD_GetAchievementPanels()) { tracked_data_storage_keys.push_back(fmt::format( "Achievement|{}", GD_GetPanel(panel_id).achievement_name)); } for (const MapArea& map_area : GD_GetMapAreas()) { for (const Location& location : map_area.locations) { tracked_data_storage_keys.push_back( fmt::format("Hunt|{}", location.ap_location_id)); } } tracked_data_storage_keys.push_back("PlayerPos"); tracked_data_storage_keys.push_back("Paintings"); initialized = true; } } void Thread() { std::string display_error; for (;;) { { std::lock_guard client_guard(client_mutex); if (apclient) { apclient->poll(); { std::lock_guard state_guard(state_mutex); if (!connected) { if (!connection_failure.empty()) { TrackerLog(connection_failure); display_error = connection_failure; connection_failure.clear(); DestroyClient(); } else { remaining_loops--; if (remaining_loops <= 0) { DestroyClient(); SetStatusMessage("Disconnected from Archipelago."); TrackerLog("Timeout while connecting to Archipelago server."); display_error = "Timeout while connecting to Archipelago server."; } } } } } } if (!display_error.empty()) { wxMessageBox(display_error, "Connection failed", wxOK | wxICON_ERROR); display_error.clear(); } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } void OnRoomInfo(std::string player, std::string password) { { std::lock_guard state_guard(state_mutex); inventory.clear(); SetStatusMessage("Connected to Archipelago server. Authenticating..."); } TrackerLog(fmt::format( "Connected to Archipelago server. Authenticating as {} {}", player, (password.empty() ? "without password" : "with password " + password))); apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"}, {AP_MAJOR, AP_MINOR, AP_REVISION}); } void OnLocationChecked(const std::list& locations) { { std::lock_guard state_guard(state_mutex); for (const int64_t location_id : locations) { checked_locations.insert(location_id); TrackerLog(fmt::format("Location: {}", location_id)); } } RefreshTracker(false); } void OnSlotDisconnected() { std::lock_guard state_guard(state_mutex); SetStatusMessage( "Disconnected from Archipelago. Attempting to reconnect..."); TrackerLog( "Slot disconnected from Archipelago. Attempting to reconnect..."); } void OnSocketDisconnected() { std::lock_guard state_guard(state_mutex); SetStatusMessage( "Disconnected from Archipelago. Attempting to reconnect..."); TrackerLog( "Socket disconnected from Archipelago. Attempting to reconnect..."); } void OnItemsReceived(const std::list& items) { { std::lock_guard state_guard(state_mutex); for (const APClient::NetworkItem& item : items) { inventory[item.item]++; TrackerLog(fmt::format("Item: {}", item.item)); } } RefreshTracker(false); } void OnRetrieved(const std::map& data) { { std::lock_guard state_guard(state_mutex); for (const auto& [key, value] : data) { HandleDataStorage(key, value); } } RefreshTracker(false); } void OnSetReply(const std::string& key, const nlohmann::json& value) { { std::lock_guard state_guard(state_mutex); HandleDataStorage(key, value); } RefreshTracker(false); } void OnSlotConnected(std::string player, std::string server, const nlohmann::json& slot_data) { IPC_SetTrackerSlot(server, player); TrackerLog("Connected to Archipelago!"); { std::lock_guard state_guard(state_mutex); SetStatusMessage( fmt::format("Connected to Archipelago! ({}@{}).", player, server)); save_name = fmt::format("zzAP_{}_{}.save", apclient->get_seed(), apclient->get_player_number()); data_storage_prefix = fmt::format("Lingo_{}_", apclient->get_player_number()); door_shuffle_mode = slot_data["shuffle_doors"].get(); if (slot_data.contains("group_doors")) { group_doors = slot_data.contains("group_doors") && slot_data["group_doors"].get() == 1; } else { // If group_doors doesn't exist yet, that means kPANELS_MODE is // actually kSIMPLE_DOORS. if (door_shuffle_mode == kPANELS_MODE) { door_shuffle_mode = kDOORS_MODE; group_doors = true; } } color_shuffle = slot_data["shuffle_colors"].get() == 1; painting_shuffle = slot_data["shuffle_paintings"].get() == 1; mastery_requirement = slot_data["mastery_achievements"].get(); level_2_requirement = slot_data["level_2_requirement"].get(); location_checks = slot_data["location_checks"].get(); victory_condition = slot_data["victory_condition"].get(); early_color_hallways = slot_data.contains("early_color_hallways") && slot_data["early_color_hallways"].get() == 1; pilgrimage_enabled = slot_data.contains("enable_pilgrimage") && slot_data["enable_pilgrimage"].get() == 1; pilgrimage_allows_roof_access = slot_data.contains("pilgrimage_allows_roof_access") && slot_data["pilgrimage_allows_roof_access"].get() == 1; pilgrimage_allows_paintings = slot_data.contains("pilgrimage_allows_paintings") && slot_data["pilgrimage_allows_paintings"].get() == 1; sunwarp_access = slot_data.contains("sunwarp_access") ? slot_data["sunwarp_access"].get() : kSUNWARP_ACCESS_NORMAL; sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") && slot_data["shuffle_sunwarps"].get() == 1; if (painting_shuffle && slot_data.contains("painting_entrance_to_exit")) { painting_mapping.clear(); for (const auto& mapping_it : slot_data["painting_entrance_to_exit"].items()) { painting_mapping[mapping_it.key()] = mapping_it.value(); painting_codomain.insert(mapping_it.value()); } } if (sunwarp_shuffle && slot_data.contains("sunwarp_permutation")) { std::vector inverted_sunwarps; for (const auto& item : slot_data["sunwarp_permutation"]) { inverted_sunwarps.push_back(item); } for (int i = 0; i < 6; i++) { sunwarp_mapping[inverted_sunwarps[i]] = SunwarpMapping{ .dots = i + 1, .exit_index = inverted_sunwarps[i + 6]}; } } std::list corrected_keys; for (const std::string& key : tracked_data_storage_keys) { corrected_keys.push_back(data_storage_prefix + key); } victory_data_storage_key = fmt::format("_read_client_status_{}_{}", apclient->get_team_number(), apclient->get_player_number()); corrected_keys.push_back(victory_data_storage_key); apclient->Get(corrected_keys); apclient->SetNotify(corrected_keys); connected = true; } ResetReachabilityRequirements(); RefreshTracker(true); } void OnSlotRefused(const std::list& errors) { std::vector error_messages; error_messages.push_back("Could not connect to Archipelago."); for (const std::string& error : errors) { if (error == "InvalidSlot") { error_messages.push_back("Invalid player name."); } else if (error == "InvalidGame") { error_messages.push_back("The specified player is not playing Lingo."); } else if (error == "IncompatibleVersion") { error_messages.push_back( "The Archipelago server is not the correct version for this " "client."); } else if (error == "InvalidPassword") { error_messages.push_back("Incorrect password."); } else if (error == "InvalidItemsHandling") { error_messages.push_back( "Invalid item handling flag. This is a bug with the tracker. " "Please report it to the lingo-ap-tracker GitHub."); } else { error_messages.push_back("Unknown error."); } } { std::lock_guard state_guard(state_mutex); connection_failure = hatkirby::implode(error_messages, " "); SetStatusMessage("Disconnected from Archipelago."); } } // Assumes state mutex is locked. void SetStatusMessage(std::string msg) { status_message = std::move(msg); tracker_frame->UpdateStatusMessage(); } // Assumes state mutex is locked. void HandleDataStorage(const std::string& key, const nlohmann::json& value) { if (value.is_boolean()) { data_storage[key] = value.get(); TrackerLog(fmt::format("Data storage {} retrieved as {}", key, (value.get() ? "true" : "false"))); } else if (value.is_number()) { data_storage[key] = value.get(); TrackerLog(fmt::format("Data storage {} retrieved as {}", key, value.get())); } else if (value.is_object()) { if (key.ends_with("PlayerPos")) { auto map_value = value.get>(); player_pos = std::tuple(map_value["x"], map_value["z"]); } else { data_storage[key] = value.get>(); } TrackerLog(fmt::format("Data storage {} retrieved as dictionary", key)); } else if (value.is_null()) { if (key.ends_with("PlayerPos")) { player_pos = std::nullopt; } else { data_storage.erase(key); } TrackerLog(fmt::format("Data storage {} retrieved as null", key)); } else if (value.is_array()) { auto list_value = value.get>(); if (key.ends_with("Paintings")) { data_storage[key] = std::set(list_value.begin(), list_value.end()); } else { data_storage[key] = list_value; } TrackerLog(fmt::format("Data storage {} retrieved as list: [{}]", key, hatkirby::implode(list_value, ", "))); } } // State mutex should NOT be locked. void RefreshTracker(bool reset) { TrackerLog("Refreshing display..."); std::string prev_msg; { std::lock_guard state_guard(state_mutex); prev_msg = status_message; SetStatusMessage(fmt::format("{} Recalculating...", status_message)); } RecalculateReachability(); if (reset) { tracker_frame->ResetIndicators(); } else { tracker_frame->UpdateIndicators(); } { std::lock_guard state_guard(state_mutex); SetStatusMessage(prev_msg); } } void DestroyClient() { apclient->reset(); apclient.reset(); } }; APState& GetState() { static APState* instance = new APState(); return *instance; } } // namespace void AP_SetTrackerFrame(TrackerFrame* arg) { GetState().tracker_frame = arg; } void AP_Connect(std::string server, std::string player, std::string password) { GetState().Connect(server, player, password); } std::string AP_GetStatusMessage() { return GetState().GetStatusMessage(); } std::string AP_GetSaveName() { std::lock_guard state_guard(GetState().state_mutex); return GetState().save_name; } bool AP_HasCheckedGameLocation(int location_id) { return GetState().HasCheckedGameLocation(location_id); } bool AP_HasCheckedHuntPanel(int location_id) { return GetState().HasCheckedHuntPanel(location_id); } bool AP_HasItem(int item_id, int quantity) { return GetState().HasItem(item_id, quantity); } std::string AP_GetItemName(int item_id) { return GetState().GetItemName(item_id); } DoorShuffleMode AP_GetDoorShuffleMode() { std::lock_guard state_guard(GetState().state_mutex); return GetState().door_shuffle_mode; } bool AP_AreDoorsGrouped() { std::lock_guard state_guard(GetState().state_mutex); return GetState().group_doors; } bool AP_IsColorShuffle() { std::lock_guard state_guard(GetState().state_mutex); return GetState().color_shuffle; } bool AP_IsPaintingShuffle() { std::lock_guard state_guard(GetState().state_mutex); return GetState().painting_shuffle; } std::map AP_GetPaintingMapping() { std::lock_guard state_guard(GetState().state_mutex); return GetState().painting_mapping; } bool AP_IsPaintingMappedTo(const std::string& painting_id) { std::lock_guard state_guard(GetState().state_mutex); return GetState().painting_codomain.count(painting_id); } std::set AP_GetCheckedPaintings() { return GetState().GetCheckedPaintings(); } bool AP_IsPaintingChecked(const std::string& painting_id) { return GetState().IsPaintingChecked(painting_id); } int AP_GetMasteryRequirement() { std::lock_guard state_guard(GetState().state_mutex); return GetState().mastery_requirement; } int AP_GetLevel2Requirement() { std::lock_guard state_guard(GetState().state_mutex); return GetState().level_2_requirement; } bool AP_IsLocationVisible(int classification) { std::lock_guard state_guard(GetState().state_mutex); int world_state = 0; switch (GetState().location_checks) { case kNORMAL_LOCATIONS: world_state = kLOCATION_NORMAL; break; case kREDUCED_LOCATIONS: world_state = kLOCATION_REDUCED; break; case kPANELSANITY: world_state = kLOCATION_INSANITY; break; default: return false; } if (GetState().door_shuffle_mode == kDOORS_MODE && !GetState().early_color_hallways) { world_state |= kLOCATION_SMALL_SPHERE_ONE; } return (world_state & classification); } VictoryCondition AP_GetVictoryCondition() { std::lock_guard state_guard(GetState().state_mutex); return GetState().victory_condition; } bool AP_HasAchievement(const std::string& achievement_name) { return GetState().HasAchievement(achievement_name); } bool AP_HasEarlyColorHallways() { std::lock_guard state_guard(GetState().state_mutex); return GetState().early_color_hallways; } bool AP_IsPilgrimageEnabled() { std::lock_guard state_guard(GetState().state_mutex); return GetState().pilgrimage_enabled; } bool AP_DoesPilgrimageAllowRoofAccess() { std::lock_guard state_guard(GetState().state_mutex); return GetState().pilgrimage_allows_roof_access; } bool AP_DoesPilgrimageAllowPaintings() { std::lock_guard state_guard(GetState().state_mutex); return GetState().pilgrimage_allows_paintings; } SunwarpAccess AP_GetSunwarpAccess() { std::lock_guard state_guard(GetState().state_mutex); return GetState().sunwarp_access; } bool AP_IsSunwarpShuffle() { std::lock_guard state_guard(GetState().state_mutex); return GetState().sunwarp_shuffle; } std::map AP_GetSunwarpMapping() { return GetState().sunwarp_mapping; } bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); } std::optional> AP_GetPlayerPosition() { std::lock_guard state_guard(GetState().state_mutex); return GetState().player_pos; }