about summary refs log tree commit diff stats
Commit message (Expand)AuthorAgeFilesLines
...
* Converted puzzle symbols to an enumStar Rauchenberger2025-08-20208-973/+997
* Committed map dump scriptStar Rauchenberger2025-08-201-0/+171
* Added the_sweetStar Rauchenberger2025-08-207-0/+289
* Added the_sun_templeStar Rauchenberger2025-08-2010-2/+167
* Implemented complete_at=1 in the apworldStar Rauchenberger2025-08-201-3/+11
* Maps have display names nowStar Rauchenberger2025-08-2048-6/+81
* Added "endings" object typeStar Rauchenberger2025-08-2018-8/+160
* Added the_sturdyStar Rauchenberger2025-08-207-2/+206
* Added the_stormyStar Rauchenberger2025-08-1912-0/+238
* Added the_sirenicStar Rauchenberger2025-08-197-1/+231
* Store IDs in a yaml fileStar Rauchenberger2025-08-198-5918/+1862
* Added the_shopStar Rauchenberger2025-08-184-1/+324
* Added the_revitalizedStar Rauchenberger2025-08-1812-2/+257
* Added the_repetitiveStar Rauchenberger2025-08-1825-144/+1553
* Added the_relentlessStar Rauchenberger2025-08-1812-0/+672
* Added the_quietStar Rauchenberger2025-08-186-0/+159
* Validate that nodes in game files are usedStar Rauchenberger2025-08-1833-30/+627
* Validate that node paths aren't used multiple timesStar Rauchenberger2025-08-174-8/+40
* Fill the item pool with "Nothing"sStar Rauchenberger2025-08-173-1/+14
* Added the_plazaStar Rauchenberger2025-08-1714-2/+1216
* Added the_perceptiveStar Rauchenberger2025-08-172-0/+8
* Added the_partialStar Rauchenberger2025-08-179-2/+435
* Added the_parthenonStar Rauchenberger2025-08-179-2/+254
* Added the_owlStar Rauchenberger2025-08-1713-1/+935
* Added the_orbStar Rauchenberger2025-08-178-1/+296
* Added the_nuancedStar Rauchenberger2025-08-167-0/+358
* Added the_livelyStar Rauchenberger2025-08-166-1/+148
* Added the_literateStar Rauchenberger2025-08-166-1/+152
* Added the_lionizedStar Rauchenberger2025-08-167-2/+155
* Minor changes for compiling on WindowsStar Rauchenberger2025-08-164-3/+5
* Started writing a data validatorStar Rauchenberger2025-08-1620-28/+1080
* Assigned IDs for the_jubilant, the_keen, and the_linearStar Rauchenberger2025-08-162-2/+243
* Added the_linearStar Rauchenberger2025-08-154-2/+93
* Added the_liberatedStar Rauchenberger2025-08-155-0/+107
* Added the_keenStar Rauchenberger2025-08-155-1/+147
* Added the_jubilantStar Rauchenberger2025-08-146-1/+210
* Assigned IDs for the_hive, the_impressive, and the_invisibleStar Rauchenberger2025-08-145-6/+311
* Added the_invisibleStar Rauchenberger2025-08-145-1/+87
* Added the_impressiveStar Rauchenberger2025-08-1412-6/+194
* Added the_hiveStar Rauchenberger2025-08-146-1/+412
* Added the_hinterlandsStar Rauchenberger2025-08-142-0/+32
* Assigned IDs for the_gold, the_graveyard, and the_greatStar Rauchenberger2025-08-144-5/+974
* Added the_greatStar Rauchenberger2025-08-1447-105/+2201
* Fixed some issues with door logicStar Rauchenberger2025-08-142-8/+28
* Added the_graveyardStar Rauchenberger2025-08-146-2/+91
* Added the_goldStar Rauchenberger2025-08-141-0/+10
* Assigned IDs for the_galleryStar Rauchenberger2025-08-135-2/+117
* Added the_galleryStar Rauchenberger2025-08-1310-41/+565
* Assigned IDs for the_extravagantStar Rauchenberger2025-08-121-0/+133
* Added the_extravagantStar Rauchenberger2025-08-1217-0/+435
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
#include "ap_state.h"

#define HAS_STD_FILESYSTEM
#define _WEBSOCKETPP_CPP11_STRICT_
#pragma comment(lib, "crypt32")

#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 "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::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::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 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;

  std::map<std::string, std::string> painting_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(
            "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(
              "Hunt|" + std::to_string(location.ap_location_id));
        }
      }

      tracked_data_storage_keys.push_back("PlayerPos");

      initialized = true;
    }

    tracker_frame->SetStatusMessage("Connecting to Archipelago server....");
    TrackerLog("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);
    }

    inventory.clear();
    checked_locations.clear();
    data_storage.clear();
    player_pos = std::nullopt;
    victory_data_storage_key.clear();
    door_shuffle_mode = kNO_DOORS;
    color_shuffle = false;
    painting_shuffle = false;
    painting_mapping.clear();
    mastery_requirement = 21;
    level_2_requirement = 223;
    location_checks = kNORMAL_LOCATIONS;
    victory_condition = kTHE_END;
    early_color_hallways = false;

    connected = false;
    has_connection_result = false;

    apclient->set_room_info_handler([this, player, password]() {
      inventory.clear();

      TrackerLog("Connected to Archipelago server. Authenticating as " +
                 player +
                 (password.empty() ? " without password"
                                   : " with password " + password));
      tracker_frame->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("Location: " + std::to_string(location_id));
          }

          RefreshTracker();
        });

    apclient->set_slot_disconnected_handler([this]() {
      tracker_frame->SetStatusMessage(
          "Disconnected from Archipelago. Attempting to reconnect...");
      TrackerLog(
          "Slot disconnected from Archipelago. Attempting to reconnect...");
    });

    apclient->set_socket_disconnected_handler([this]() {
      tracker_frame->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("Item: " + std::to_string(item.item));
          }

          RefreshTracker();
        });

    apclient->set_retrieved_handler(
        [this](const std::map<std::string, nlohmann::json>& data) {
          for (const auto& [key, value] : data) {
            HandleDataStorage(key, value);
          }

          RefreshTracker();
        });

    apclient->set_set_reply_handler([this](const std::string& key,
                                           const nlohmann::json& value,
                                           const nlohmann::json&) {
      HandleDataStorage(key, value);
      RefreshTracker();
    });

    apclient->set_slot_connected_handler([this](
                                             const nlohmann::json& slot_data) {
      tracker_frame->SetStatusMessage("Connected to Archipelago!");
      TrackerLog("Connected to Archipelago!");

      data_storage_prefix =
          "Lingo_" + std::to_string(apclient->get_player_number()) + "_";
      door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
      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;

      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();
        }
      }

      connected = true;
      has_connection_result = true;

      RefreshTracker();

      std::list<std::string> corrected_keys;
      for (const std::string& key : tracked_data_storage_keys) {
        corrected_keys.push_back(data_storage_prefix + key);
      }

      {
        std::ostringstream vdsks;
        vdsks << "_read_client_status_" << apclient->get_team_number() << "_"
              << apclient->get_player_number();
        victory_data_storage_key = vdsks.str();
      }

      corrected_keys.push_back(victory_data_storage_key);

      apclient->Get(corrected_keys);
      apclient->SetNotify(corrected_keys);
    });

    apclient->set_slot_refused_handler(
        [this](const std::list<std::string>& errors) {
          connected = false;
          has_connection_result = true;

          tracker_frame->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 (!has_connection_result) {
      if (interval == 0) {
        connected = false;
        has_connection_result = true;

        DestroyClient();

        tracker_frame->SetStatusMessage("Disconnected from Archipelago.");

        TrackerLog("Timeout while connecting to Archipelago server.");
        wxMessageBox("Timeout while connecting to Archipelago server.",
                     "Connection failed", wxOK | wxICON_ERROR);
      }

      std::this_thread::sleep_for(std::chrono::milliseconds(100));

      interval--;
    }

    if (connected) {
      RefreshTracker();
    } else {
      client_active = false;
    }
  }

  void HandleDataStorage(const std::string& key, const nlohmann::json& value) {
    if (value.is_boolean()) {
      data_storage[key] = value.get<bool>();
      TrackerLog("Data storage " + key + " retrieved as " +
                 (value.get<bool>() ? "true" : "false"));
    } else if (value.is_number()) {
      data_storage[key] = value.get<int>();
      TrackerLog("Data storage " + key + " retrieved as " +
                 std::to_string(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("Data storage " + key + " retrieved as dictionary");
    } else if (value.is_null()) {
      if (key.ends_with("PlayerPos")) {
        player_pos = std::nullopt;
      } else {
        data_storage.erase(key);
      }
      
      TrackerLog("Data storage " + key + " retrieved as null");
    }
  }

  bool HasCheckedGameLocation(int location_id) {
    return checked_locations.count(location_id);
  }

  bool HasCheckedHuntPanel(int location_id) {
    std::string key =
        data_storage_prefix + "Hunt|" + std::to_string(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 = data_storage_prefix + "Achievement|" + name;
    return data_storage.count(key) && std::any_cast<bool>(data_storage.at(key));
  }

  void RefreshTracker() {
    TrackerLog("Refreshing display...");

    RecalculateReachability();
    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("Could not find AP item ID for " + item_name);
    }

    return ap_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);
}

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);
}

DoorShuffleMode AP_GetDoorShuffleMode() { return GetState().door_shuffle_mode; }

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;
}

int AP_GetMasteryRequirement() { return GetState().mastery_requirement; }

int AP_GetLevel2Requirement() { return GetState().level_2_requirement; }

bool AP_IsLocationVisible(int classification) {
  switch (GetState().location_checks) {
    case kNORMAL_LOCATIONS:
      return classification & kLOCATION_NORMAL;
    case kREDUCED_LOCATIONS:
      return classification & kLOCATION_REDUCED;
    case kPANELSANITY:
      return classification & kLOCATION_INSANITY;
    default:
      return false;
  }
}

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_HasReachedGoal() { return GetState().HasReachedGoal(); }

std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
  return GetState().player_pos;
}