#include "tracker_panel.h"
#include <wx/dcbuffer.h>
#include "ap_state.h"
#include "area_popup.h"
#include "game_data.h"
#include "global.h"
#include "tracker_config.h"
#include "tracker_state.h"
constexpr int AREA_ACTUAL_SIZE = 64;
constexpr int AREA_BORDER_SIZE = 5;
constexpr int AREA_EFFECTIVE_SIZE = AREA_ACTUAL_SIZE + AREA_BORDER_SIZE * 2;
constexpr int PLAYER_SIZE = 96;
TrackerPanel::TrackerPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
SetBackgroundStyle(wxBG_STYLE_PAINT);
map_image_ = wxImage(GetAbsolutePath("assets/lingo_map.png").c_str(),
wxBITMAP_TYPE_PNG);
if (!map_image_.IsOk()) {
return;
}
player_image_ =
wxImage(GetAbsolutePath("assets/player.png").c_str(), wxBITMAP_TYPE_PNG);
if (!player_image_.IsOk()) {
return;
}
for (const MapArea &map_area : GD_GetMapAreas()) {
AreaIndicator area;
area.area_id = map_area.id;
area.popup = new AreaPopup(this, map_area.id);
area.popup->SetPosition({0, 0});
areas_.push_back(area);
}
Redraw();
Bind(wxEVT_PAINT, &TrackerPanel::OnPaint, this);
Bind(wxEVT_MOTION, &TrackerPanel::OnMouseMove, this);
}
void TrackerPanel::UpdateIndicators() {
for (AreaIndicator &area : areas_) {
area.popup->UpdateIndicators();
}
Redraw();
}
void TrackerPanel::OnPaint(wxPaintEvent &event) {
if (GetSize() != rendered_.GetSize()) {
Redraw();
}
wxBufferedPaintDC dc(this);
dc.DrawBitmap(rendered_, 0, 0);
if (AP_GetPlayerPosition().has_value()) {
// 1588, 1194
// 14x14 -> 154x154
double intended_x =
1588.0 + (std::get<0>(*AP_GetPlayerPosition()) * (154.0 / 14.0));
double intended_y =
1194.0 + (std::get<1>(*AP_GetPlayerPosition()) *#include "ap_state.h"
#define HAS_STD_FILESYSTEM
#define _WEBSOCKETPP_CPP11_STRICT_
#pragma comment(lib, "crypt32")
#include <fmt/core.h>
#include <hkutil/string.h>
#include <any>
#include <apclient.hpp>
#include <apuuid.hpp>
#include <chrono>
#include <exception>
#include <filesystem>
#include <list>
#include <memory>
#include <mutex>
#include <set>
#include <sstream>
#include <thread>
#include <tuple>
#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
namespace {
struct APState {
std::unique_ptr<APClient> apclient;
bool initialized = false;
TrackerFrame* tracker_frame = nullptr;
bool client_active = false;
std::string status_message = "Not connected to Archipelago.";
std::mutex client_mutex;
bool connected = false;
bool has_connection_result = false;
std::string data_storage_prefix;
std::list<std::string> tracked_data_storage_keys;
std::string victory_data_storage_key;
std::string save_name;
std::map<int64_t, int> inventory;
std::set<int64_t> checked_locations;
std::map<std::string, std::any> data_storage;
std::optional<std::tuple<int, int>> 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<std::string, std::string> painting_mapping;
std::set<std::string> painting_codomain;
std::map<int, SunwarpMapping> sunwarp_mapping;
void Connect(std::string server, std::string player, std::string password) {
if (!initialized) {
TrackerLog("Initializing APState...");
std::thread([this]() {
for (;;) {
{
std::lock_guard client_guard(client_mutex);
if (apclient) {
apclient->poll();
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}).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;
}
{
std::lock_guard client_guard(client_mutex);
SetStatusMessage("Connecting to Archipelago server....");
}
TrackerLog(fmt::format("Connecting to Archipelago server ({})...", server));
{
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<APClient>(ap_get_uuid(""), "Lingo", server,
cert_store);
}
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();
std::mutex connection_mutex;
connected = false;
has_connection_result = false;
apclient->set_room_info_handler([this, player, password]() {
inventory.clear();
TrackerLog(fmt::format(
"Connected to Archipelago server. Authenticating as {} {}", player,
(password.empty() ? "without password"
: "with password " + password)));
SetStatusMessage("Connected to Archipelago server. Authenticating...");
apclient->ConnectSlot(player, password, ITEM_HANDLING, {"Tracker"},
{AP_MAJOR, AP_MINOR, AP_REVISION});
});
apclient->set_location_checked_handler(
[this](const std::list<int64_t>& locations) {
for (const int64_t location_id : locations) {
checked_locations.insert(location_id);
TrackerLog(fmt::format("Location: {}", location_id));
}
RefreshTracker(false);
});
apclient->set_slot_disconnected_handler([this]() {
SetStatusMessage(
"Disconnected from Archipelago. Attempting to reconnect...");
TrackerLog(
"Slot disconnected from Archipelago. Attempting to reconnect...");
});
apclient->set_socket_disconnected_handler([this]() {
SetStatusMessage(
"Disconnected from Archipelago. Attempting to reconnect...");
TrackerLog(
"Socket disconnected from Archipelago. Attempting to reconnect...");
});
apclient->set_items_received_handler(
[this](const std::list<APClient::NetworkItem>& items) {
for (const APClient::NetworkItem& item : items) {
inventory[item.item]++;
TrackerLog(fmt::format("Item: {}", item.item));
}
RefreshTracker(false);
});
apclient->set_retrieved_handler(
[this](const std::map<std::string, nlohmann::json>& data) {
for (const auto& [key, value] : data) {
HandleDataStorage(key, value);
}
RefreshTracker(false);
});
apclient->set_set_reply_handler([this](const std::string& key,
const nlohmann::json& value,
const nlohmann::json&) {
HandleDataStorage(key, value);
RefreshTracker(false);
});
apclient->set_slot_connected_handler([this, player, server,
&connection_mutex](
const nlohmann::json& slot_data) {
SetStatusMessage(
fmt::format("Connected to Archipelago! ({}@{}).", player, server));
TrackerLog("Connected to Archipelago!");
IPC_SetTrackerSlot(server, player);
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<DoorShuffleMode>();
if (slot_data.contains("group_doors")) {
group_doors = slot_data.contains("group_doors") &&
slot_data["group_doors"].get<int>() == 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<int>() == 1;
painting_shuffle = slot_data["shuffle_paintings"].get<int>() == 1;
mastery_requirement = slot_data["mastery_achievements"].get<int>();
level_2_requirement = slot_data["level_2_requirement"].get<int>();
location_checks = slot_data["location_checks"].get<LocationChecks>();
victory_condition =
slot_data["victory_condition"].get<VictoryCondition>();
early_color_hallways = slot_data.contains("early_color_hallways") &&
slot_data["early_color_hallways"].get<int>() == 1;
pilgrimage_enabled = slot_data.contains("enable_pilgrimage") &&
slot_data["enable_pilgrimage"].get<int>() == 1;
pilgrimage_allows_roof_access =
slot_data.contains("pilgrimage_allows_roof_access") &&
slot_data["pilgrimage_allows_roof_access"].get<int>() == 1;
pilgrimage_allows_paintings =
slot_data.contains("pilgrimage_allows_paintings") &&
slot_data["pilgrimage_allows_paintings"].get<int>() == 1;
sunwarp_access = slot_data.contains("sunwarp_access")
? slot_data["sunwarp_access"].get<SunwarpAccess>()
: kSUNWARP_ACCESS_NORMAL;
sunwarp_shuffle = slot_data.contains("shuffle_sunwarps") &&
slot_data["shuffle_sunwarps"].get<int>() == 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<int> 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<std::string> 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);
ResetReachabilityRequirements();
RefreshTracker(true);
{
std::lock_guard connection_lock(connection_mutex);
if (!has_connection_result) {
connected = true;
has_connection_result = true;
}
}
});
apclient->set_slot_refused_handler(
[this, &connection_mutex](const std::list<std::string>& errors) {
{
std::lock_guard connection_lock(connection_mutex);
connected = false;
has_connection_result = true;
}
SetStatusMessage("Disconnected from Archipelago.");
std::vector<std::string> 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::string full_message = hatkirby::implode(error_messages, " ");
TrackerLog(full_message);
wxMessageBox(full_message, "Connection failed", wxOK | wxICON_ERROR);
});
client_active = true;
int timeout = 5000; // 5 seconds
int interval = 100;
int remaining_loops = timeout / interval;
while (true) {
{
std::lock_guard connection_lock(connection_mutex);
if (has_connection_result) {
break;
}
}
if (interval == 0) {
DestroyClient();
{
std::lock_guard client_guard(client_mutex);
SetStatusMessage("Disconnected from Archipelago.");
}
TrackerLog("Timeout while connecting to Archipelago server.");
wxMessageBox("Timeout while connecting to Archipelago server.",
"Connection failed", wxOK | wxICON_ERROR);
{
std::lock_guard connection_lock(connection_mutex);
connected = false;
has_connection_result = true;
}
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
interval--;
}
if (connected) {
client_active = false;
}
}
std::string GetStatusMessage() {
std::lock_guard client_guard(client_mutex);
return status_message;
}
void SetStatusMessage(std::string msg) {
status_message = std::move(msg);
tracker_frame->UpdateStatusMessage();
}
void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
if (value.is_boolean()) {
data_storage[key] = value.get<bool>();
TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
(value.get<bool>() ? "true" : "false")));
} else if (value.is_number()) {
data_storage[key] = value.get<int>();
TrackerLog(fmt::format("Data storage {} retrieved as {}", key,
value.get<int>()));
} else if (value.is_object()) {
if (key.ends_with("PlayerPos")) {
auto map_value = value.get<std::map<std::string, int>>();
player_pos = std::tuple<int, int>(map_value["x"], map_value["z"]);
} else {
data_storage[key] = value.get<std::map<std::string, int>>();
}
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<std::vector<std::string>>();
if (key.ends_with("Paintings")) {
data_storage[key] =
std::set<std::string>(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, ", ")));
}
}
bool HasCheckedGameLocation(int location_id) {
return checked_locations.count(location_id);
}
bool HasCheckedHuntPanel(int location_id) {
std::string key =
fmt::format("{}Hunt|{}", data_storage_prefix, location_id);
return data_storage.count(key) && std::any_cast<bool>(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::string key =
fmt::format("{}Achievement|{}", data_storage_prefix, name);
return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
}
const std::set<std::string>& GetCheckedPaintings() {
std::string key = fmt::format("{}Paintings", data_storage_prefix);
if (!data_storage.count(key)) {
data_storage[key] = std::set<std::string>();
}
return std::any_cast<const std::set<std::string>&>(data_storage.at(key));
}
bool IsPaintingChecked(const std::string& painting_id) {
const auto& checked_paintings = GetCheckedPaintings();
return checked_paintings.count(painting_id) ||
(painting_mapping.count(painting_id) &&
checked_paintings.count(painting_mapping.at(painting_id)));
}
void RefreshTracker(bool reset) {
TrackerLog("Refreshing display...");
RecalculateReachability();
if (reset) {
tracker_frame->ResetIndicators();
} else {
tracker_frame->UpdateIndicators();
}
}
int64_t GetItemId(const std::string& item_name) {
int64_t ap_id = apclient->get_item_id(item_name);
if (ap_id == APClient::INVALID_NAME_ID) {
TrackerLog(fmt::format("Could not find AP item ID for {}", item_name));
}
return ap_id;
}
std::string GetItemName(int id) { return apclient->get_item_name(id); }
bool HasReachedGoal() {
return data_storage.count(victory_data_storage_key) &&
std::any_cast<int>(data_storage.at(victory_data_storage_key)) ==
30; // CLIENT_GOAL
}
void DestroyClient() {
client_active = false;
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() { 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() { return GetState().door_shuffle_mode; }
bool AP_AreDoorsGrouped() { return GetState().group_doors; }
bool AP_IsColorShuffle() { return GetState().color_shuffle; }
bool AP_IsPaintingShuffle() { return GetState().painting_shuffle; }
const std::map<std::string, std::string>& AP_GetPaintingMapping() {
return GetState().painting_mapping;
}
bool AP_IsPaintingMappedTo(const std::string& painting_id) {
return GetState().painting_codomain.count(painting_id);
}
const std::set<std::string>& AP_GetCheckedPaintings() {
return GetState().GetCheckedPaintings();
}
bool AP_IsPaintingChecked(const std::string& painting_id) {
return GetState().IsPaintingChecked(painting_id);
}
int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }
int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }
bool AP_IsLocationVisible(int classification) {
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() {
return GetState().victory_condition;
}
bool AP_HasAchievement(const std::string& achievement_name) {
return GetState().HasAchievement(achievement_name);
}
bool AP_HasEarlyColorHallways() { return GetState().early_color_hallways; }
bool AP_IsPilgrimageEnabled() { return GetState().pilgrimage_enabled; }
bool AP_DoesPilgrimageAllowRoofAccess() {
return GetState().pilgrimage_allows_roof_access;
}
bool AP_DoesPilgrimageAllowPaintings() {
return GetState().pilgrimage_allows_paintings;
}
SunwarpAccess AP_GetSunwarpAccess() { return GetState().sunwarp_access; }
bool AP_IsSunwarpShuffle() { return GetState().sunwarp_shuffle; }
const std::map<int, SunwarpMapping>& AP_GetSunwarpMapping() {
return GetState().sunwarp_mapping;
}
bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }
std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
return GetState().player_pos;
}