about summary refs log blame commit diff stats
path: root/src/ap_state.cpp
blob: a0d0f66147cd3b5e21c57eb1ae797670ffe81e95 (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 <hkutil/string.h>

#include <apclient.hpp>
#include <apuuid.hpp>
#include <chrono>
#include <exception>
#include <filesystem>
#include <list>
#include <memory>
#include <mutex>
#include <set>
#include <thread>
#include <tuple>

#include "game_data.h"
#include "tracker_frame.h"
#include "tracker_state.h"

constexpr int AP_MAJOR = 0;
constexpr int AP_MINOR = 4;
constexpr int AP_REVISION = 0;

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::map<int64_t, int> inventory;
  std::set<int64_t> checked_locations;

  std::map<std::tuple<int, int>, int64_t> ap_id_by_location_id;
  std::map<std::string, int64_t> ap_id_by_item_name;
  std::map<LingoColor, int64_t> ap_id_by_color;
  std::map<int64_t, std::string> progressive_item_by_ap_id;

  DoorShuffleMode door_shuffle_mode = kNO_DOORS;
  bool color_shuffle = false;
  bool painting_shuffle = false;
  int mastery_requirement = 21;
  bool reduce_checks = false;

  std::map<std::string, std::string> painting_mapping;

  void Connect(std::string server, std::string player, std::string password) {
    if (!initialized) {
      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();

      initialized = true;
    }

    tracker_frame->SetStatusMessage("Connecting to Archipelago server....");

    {
      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();
    door_shuffle_mode = kNO_DOORS;
    color_shuffle = false;
    painting_shuffle = false;
    painting_mapping.clear();
    mastery_requirement = 21;
    reduce_checks = false;

    connected = false;
    has_connection_result = false;

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

      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);
            std::cout << "Location: " << location_id << std::endl;
          }

          RefreshTracker();
        });

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

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

          RefreshTracker();
        });

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

      door_shuffle_mode = slot_data["shuffle_doors"].get<DoorShuffleMode>();
      color_shuffle = slot_data["shuffle_colors"].get<bool>();
      painting_shuffle = slot_data["shuffle_paintings"].get<bool>();
      mastery_requirement = slot_data["mastery_achievements"].get<int>();
      reduce_checks = (door_shuffle_mode == kNO_DOORS) &&
                      slot_data["reduce_checks"].get<bool>();

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

    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, " ");

          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.");

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

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

      interval--;
    }

    if (connected) {
      for (const MapArea& map_area : GD_GetMapAreas()) {
        for (int section_id = 0; section_id < map_area.locations.size();
             section_id++) {
          const Location& location = map_area.locations.at(section_id);

          int64_t ap_id = apclient->get_location_id(location.ap_location_name);
          if (ap_id == APClient::INVALID_NAME_ID) {
            std::cout << "Could not find AP location ID for "
                      << location.ap_location_name << std::endl;
          } else {
            ap_id_by_location_id[{map_area.id, section_id}] = ap_id;
          }
        }
      }

      for (const Door& door : GD_GetDoors()) {
        if (!door.skip_item) {
          ap_id_by_item_name[door.item_name] = GetItemId(door.item_name);

          if (!door.group_name.empty() &&
              !ap_id_by_item_name.count(door.group_name)) {
            ap_id_by_item_name[door.group_name] = GetItemId(door.group_name);
          }

          for (const ProgressiveRequirement& prog_req : door.progressives) {
            ap_id_by_item_name[prog_req.item_name] =
                GetItemId(prog_req.item_name);
          }
        }
      }

      ap_id_by_color[LingoColor::kBlack] = GetItemId("Black");
      ap_id_by_color[LingoColor::kRed] = GetItemId("Red");
      ap_id_by_color[LingoColor::kBlue] = GetItemId("Blue");
      ap_id_by_color[LingoColor::kYellow] = GetItemId("Yellow");
      ap_id_by_color[LingoColor::kPurple] = GetItemId("Purple");
      ap_id_by_color[LingoColor::kOrange] = GetItemId("Orange");
      ap_id_by_color[LingoColor::kGreen] = GetItemId("Green");
      ap_id_by_color[LingoColor::kBrown] = GetItemId("Brown");
      ap_id_by_color[LingoColor::kGray] = GetItemId("Gray");

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

  bool HasCheckedGameLocation(int area_id, int section_id) {
    std::tuple<int, int> location_key = {area_id, section_id};

    if (ap_id_by_location_id.count(location_key)) {
      return checked_locations.count(ap_id_by_location_id.at(location_key));
    } else {
      return false;
    }
  }

  bool HasColorItem(LingoColor color) {
    if (ap_id_by_color.count(color)) {
      return inventory.count(ap_id_by_color.at(color));
    } else {
      return false;
    }
  }

  bool HasItem(const std::string& item, int quantity) {
    if (ap_id_by_item_name.count(item)) {
      int64_t ap_id = ap_id_by_item_name.at(item);
      return inventory.count(ap_id) && inventory.at(ap_id) >= quantity;
    } else {
      return false;
    }
  }

  void RefreshTracker() {
    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) {
      std::cout << "Could not find AP item ID for " << item_name << std::endl;
    }

    return ap_id;
  }

  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 area_id, int section_id) {
  return GetState().HasCheckedGameLocation(area_id, section_id);
}

bool AP_HasColorItem(LingoColor color) {
  return GetState().HasColorItem(color);
}

bool AP_HasItem(const std::string& item, int quantity) {
  return GetState().HasItem(item, 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; }

bool AP_IsReduceChecks() { return GetState().reduce_checks; }