about summary refs log blame commit diff stats
path: root/src/ap_state.cpp
blob: 4ac0cce6413e24ba9468d7b2d165b1a64ae81145 (plain) (tree)
1
2
3
4
5
6
7
8
9
                     

                                  
 
                     
                          
              
                       

                     
                     
               

                 
                  
                 
                
 
                      
                      
                   
                          
                          
 
                           
                              
 
                                                     
                                           

                                                        
           
 
                
                               
                           
                                        
                                                   
 
           
                          




                                                               
 
                         
                                 
 
                                  
                                       
 
                        
                                      
                                               
                                                 
 
                                                
                           

                                
                                
                                                     
                                                
                                    
                                  
                                             
                                                        
                               
 
                                                      
                                          
                                                
 
                                                                              



































































































































































                                                                                
                       
                                            
 
                                                   
 
                                                      
                                                                       
       

                                                             
                                                               

         
                                                       
                                                       
 
                         
   
 
                              
 



                                                   
 
                                                     
 






















                                                                                
       
 

                                                                              
       
                                                                  
     



                                                             
 
                        
 
                                                                             
     
 

                                                                                
 

                                                                       
 

                                                               
 



                                                             
 
                          
 
                                             
 



                                                                          
 
                                             
 
































                                                                            
                                    











                                                              
 
                                                                             
 
                                                                      
                           
                                                                  
                                                                            


                                                               
                                                                       



                                                
                                                                        
                                                                         
                                                                        
                                                                           
                                                                 
                                                                               
                                                                          




                                                                     



                                                                             
 
                                                                                
 

                                                                  
                                                       
         
       
 










                                                                         



                                                                

                                                                               

                                                         
                                          
 
                       
 

                                    
 



















                                                                               
       
     
 

                                                                  
 
                                                         
   
                                   




                                          
                                   

                                                                               
                                                                      
                                           
                                                                    






                                                                          
                                                                              




                                       
 
                                                                        
                                  
                                                              
                                       
                                                                        
              
                                       
       
                                                                            

     

                                        
 

                                               
 
                                                                           
     
                              




                                        
 
                                               
 
                                 
   
                        
                      
   



                                           
 
               
 




                                                                               
                                                                           



                                                      
 
                                                        
 


                                                     
                                               
 


                                         



                                                      
 



                                                      
 
                                                      
 






                                                      
 

                                                            
                                     
 
 
                                                            
                                                      

                                                         
                                                

                                          


                                                           



                                                      
 



                                                      
 
                                               
                                                      
                      
                                       
                                     
                            
                                      
                      
                                       
                   
   
 
                                                    


                                              
 
 
                                           
                                                      

                                      

                                                             
 






                                                      
 
                                       
 
                                         
                                                      


                                                  
                                                      

                                                
                                                      
 






                                                      
 
                                                      

                                    
                                                                
                                                            
                                                      
                               
#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

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<std::string> tracked_data_storage_keys;

  // Client
  std::mutex client_mutex;
  std::unique_ptr<APClient> 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<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) {
    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<APClient>(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<int64_t>& 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<APClient::NetworkItem>& items) {
            OnItemsReceived(items);
          });

      apclient->set_retrieved_handler(
          [this](const std::map<std::string, nlohmann::json>& 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<std::string>& 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<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::lock_guard state_guard(state_mutex);

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

    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<int>(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<int64_t>& 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<APClient::NetworkItem>& 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<std::string, nlohmann::json>& 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<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);

      connected = true;
    }

    ResetReachabilityRequirements();
    RefreshTracker(true);
  }

  void OnSlotRefused(const std::list<std::string>& errors) {
    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::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<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, ", ")));
    }
  }

  // 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<std::string, std::string> 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<std::string> 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<int, SunwarpMapping> AP_GetSunwarpMapping() {
  return GetState().sunwarp_mapping;
}

bool AP_HasReachedGoal() { return GetState().HasReachedGoal(); }

std::optional<std::tuple<int, int>> AP_GetPlayerPosition() {
  std::lock_guard state_guard(GetState().state_mutex);

  return GetState().player_pos;
}