Files
Shiip-of-Hakinian-Espanol/soh/soh/Network/Archipelago/Archipelago.cpp

571 lines
22 KiB
C++

#include "Archipelago.h"
#include "soh/util.h"
#include <apuuid.hpp>
#include <apclient.hpp>
#include <fstream>
#include <filesystem>
#include <iostream>
#include <string>
#include "soh/Network/Archipelago/ArchipelagoConsoleWindow.h"
#include "soh/Enhancements/randomizer/randomizerTypes.h"
#include "soh/Enhancements/randomizer/static_data.h"
#include "soh/Enhancements/randomizer/context.h"
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
#include "soh/Notification/Notification.h"
#include "soh/ShipInit.hpp"
#include "soh/SaveManager.h"
extern "C" {
#include "variables.h"
#include "macros.h"
extern PlayState* gPlayState;
}
ArchipelagoClient::ArchipelagoClient() {
uuid = ap_get_uuid("uuid");
gameWon = false;
itemQueued = false;
disconnecting = false;
}
ArchipelagoClient& ArchipelagoClient::GetInstance() {
static ArchipelagoClient Client;
return Client;
}
bool ArchipelagoClient::StartClient() {
if(apClient != NULL) {
apClient.reset();
}
disconnecting = false;
apClient = std::unique_ptr<APClient>(
new APClient(uuid, AP_Client_consts::AP_GAME_NAME,
CVarGetString(CVAR_REMOTE_ARCHIPELAGO("ServerAddress"), "localhost:38281"), "cacert.pem"));
apClient->set_room_info_handler([&]() {
std::list<std::string> tags;
// tags.push_back("DeathLink"); // todo, implement deathlink
apClient->ConnectSlot(CVarGetString(CVAR_REMOTE_ARCHIPELAGO("SlotName"), ""),
CVarGetString(CVAR_REMOTE_ARCHIPELAGO("Password"), ""),
0b001, tags);
});
apClient->set_slot_connected_handler([&](const nlohmann::json data) {
ArchipelagoConsole_SendMessage("[LOG] Connected.", true);
ArchipelagoClient::StartLocationScouts();
slotData = data;
// if we are already in game when we connect
// we won't have to request an itemSynch
if(GameInteractor::IsSaveLoaded(true)) {
if(!isRightSaveLoaded()) {
disconnecting = true;
ArchipelagoConsole_SendMessage("[ERROR] Connected to incorrect slot, disconnecting...");
return;
}
SynchSentLocations();
SynchReceivedLocations();
}
});
apClient->set_items_received_handler([&](const std::list<APClient::NetworkItem>& items) {
if(disconnecting) {
return;
}
for(const APClient::NetworkItem& item : items) {
ApItem apItem;
const std::string game = apClient->get_player_game(item.player);
apItem.itemName = apClient->get_item_name(item.item, AP_Client_consts::AP_GAME_NAME);
apItem.locationName = apClient->get_location_name(item.location, game);
apItem.playerName = apClient->get_player_alias(item.player);
apItem.flags = item.flags;
apItem.index = item.index;
OnItemReceived(apItem);
}
});
apClient->set_location_info_handler([&](const std::list<APClient::NetworkItem>& items) {
if(disconnecting) {
return;
}
scoutedItems.clear();
for(const APClient::NetworkItem& item: items) {
ApItem apItem;
const std::string game = apClient->get_player_game(item.player);
apItem.itemName = apClient->get_item_name(item.item, game);
apItem.locationName = apClient->get_location_name(item.location, AP_Client_consts::AP_GAME_NAME);
apItem.playerName = apClient->get_player_alias(item.player);
apItem.flags = item.flags;
apItem.index = item.index;
scoutedItems.push_back(apItem);
const std::string itemName = apItem.itemName;
const std::string playerName = apItem.playerName;
const std::string locationName = apItem.locationName;
std::string logMessage = "[LOG] Location scouted: " + itemName + " for " + playerName + " in location " + locationName;
ArchipelagoConsole_SendMessage(logMessage.c_str(), true);
}
ArchipelagoConsole_SendMessage("[LOG] Scouting finished.", true);
}); // todo maybe move these functions to a lambda, since they don't have to be static anymore
apClient->set_location_checked_handler([&](const std::list<int64_t> locations) {
if(disconnecting) {
return;
}
for(const int64_t apLoc : locations) {
QueueExternalCheck(apLoc);
}
});
apClient->set_print_json_handler([&](const APClient::PrintJSONArgs& arg) {
if(disconnecting) {
return;
}
std::vector<ColoredTextNode> coloredNodes;
for(const APClient::TextNode& node : arg.data) {
APClient* client = apClient.get();
std::string color;
std::string text;
if(node.type == "player_id") {
int id = std::stoi(node.text);
if (color.empty() && id == client->get_player_number()) color = "magenta";
else if(color.empty()) color = "yellow";
text = client->get_player_alias(id);
} else if (node.type == "item_id") {
int64_t id = std::stoll(node.text);
if(color.empty()) {
if (node.flags & APClient::ItemFlags::FLAG_ADVANCEMENT) color = "plum";
else if (node.flags & APClient::ItemFlags::FLAG_NEVER_EXCLUDE) color = "slateblue";
else if (node.flags & APClient::ItemFlags::FLAG_TRAP) color = "salmon";
else color = "cyan";
}
text = client->get_item_name(id, client->get_player_game(node.player));
} else if (node.type == "location_id") {
int64_t id = std::stoll(node.text);
if (color.empty()) color = "blue";
text = client->get_location_name(id, client->get_player_game(node.player));
} else if (node.type == "hint_status") {
text = node.text;
if (node.hintStatus == APClient::HINT_FOUND) color = "green";
else if (node.hintStatus == APClient::HINT_UNSPECIFIED) color = "grey";
else if (node.hintStatus == APClient::HINT_NO_PRIORITY) color = "slateblue";
else if (node.hintStatus == APClient::HINT_AVOID) color = "salmon";
else if (node.hintStatus == APClient::HINT_PRIORITY) color = "plum";
else color = "red"; // unknown status -> red
} else if (node.type == "ERROR") {
color = "ERROR";
text = node.text;
} else if (node.type == "LOG") {
color = "LOG";
text = node.text;
} else {
color = "white";
text = node.text;
}
ColoredTextNode Colornode;
Colornode.color = color;
Colornode.text = text;
coloredNodes.push_back(Colornode);
}
ArchipelagoConsole_PrintJson(coloredNodes);
});
return true;
}
void ArchipelagoClient::GameLoaded() {
if(apClient == nullptr) {
return;
}
// if its not an AP save, disconnect
if(!IS_ARCHIPELAGO) {
ArchipelagoConsole_SendMessage("[ERROR] Loaded save is not not an archipelago save, disconnecting...");
disconnecting = true;
return;
}
if(!isRightSaveLoaded()) {
ArchipelagoConsole_SendMessage("[ERROR] Loaded save is not associated with connected slot, disconnecting...");
disconnecting = true;
return;
}
ArchipelagoConsole_SendMessage("[LOG] Synching Items and Locations.", true);
SynchItems();
SynchSentLocations();
SynchReceivedLocations();
}
void ArchipelagoClient::StartLocationScouts() {
std::set<int64_t> missing_loc_set = apClient->get_missing_locations();
std::set<int64_t> found_loc_set = apClient->get_checked_locations();
std::list<int64_t> location_list;
for(const int64_t loc_id : missing_loc_set) {
location_list.emplace_back(loc_id);
}
for(const int64_t loc_id : found_loc_set) {
location_list.emplace_back(loc_id);
}
apClient->LocationScouts(location_list);
}
void ArchipelagoClient::SynchItems() {
// Send a Synch request to get any items we may have missed
ArchipelagoConsole_SendMessage("[LOG] Sending synch request", true);
apClient->Sync();
}
void ArchipelagoClient::SynchSentLocations() {
// send already checked locations
std::list<int64_t> checkedLocations;
for(const auto& loc : Rando::StaticData::GetLocationTable()) {
const RandomizerCheck rc = loc.GetRandomizerCheck();
if(Rando::Context::GetInstance()->GetItemLocation(rc)->HasObtained()) {
const int64_t apLocation = apClient->get_location_id(loc.GetName());
checkedLocations.emplace_back(apLocation);
}
}
std::string locationLog = "[LOG] Synching " + std::to_string(checkedLocations.size())+ " checks already found in game";
ArchipelagoConsole_SendMessage(locationLog.c_str(), true);
apClient->LocationChecks(checkedLocations);
}
void ArchipelagoClient::SynchReceivedLocations() {
// Open checks that have been found previously but went unsaved
for(const int64_t apLoc : apClient->get_checked_locations()) {
QueueExternalCheck(apLoc);
}
}
void ArchipelagoClient::QueueExternalCheck(const int64_t apLocation) {
const std::string checkName = apClient->get_location_name(apLocation, AP_Client_consts::AP_GAME_NAME);
const uint32_t RC = static_cast<uint32_t>(Rando::StaticData::locationNameToEnum[checkName]);
if(RC == RC_UNKNOWN_CHECK) {
ArchipelagoConsole_SendMessage("[ERROR] Attempting to queue RC_UNKOWN_CHECK, skipping", false);
return;
}
// Don't queue checks we already have
if(Rando::Context::GetInstance()->GetItemLocation(RC)->HasObtained()) {
return;
}
std::string locationLog = "[LOG] Externaly checking: " + checkName;
ArchipelagoConsole_SendMessage(locationLog.c_str(), true);
GameInteractor_ExecuteOnRandomizerExternalCheck(RC);
}
bool ArchipelagoClient::IsConnected() {
if(apClient == nullptr) {
return false;
}
return apClient->get_state() == APClient::State::SLOT_CONNECTED;
}
void ArchipelagoClient::CheckLocation(RandomizerCheck sohCheckId) {
if(sohCheckId == RC_UNKNOWN_CHECK) {
ArchipelagoConsole_SendMessage("[ERROR] trying to send RC_UNKNOWN_CHECK, skipping", false);
return;
}
if(!IsConnected()) {
return;
}
std::string apName = Rando::StaticData::GetLocation(sohCheckId)->GetName();
if (apName.empty()) {
return;
}
int64_t apItemId = apClient->get_location_id(std::string(apName));
std::string logMessage = "[LOG] Checked: " + apName + "(" + std::to_string(apItemId) + "), sending to AP server";
ArchipelagoConsole_SendMessage(logMessage.c_str(), true);
apClient->LocationChecks({ apItemId });
}
void ArchipelagoClient::OnItemReceived(const ApItem apItem) {
if(!GameInteractor::IsSaveLoaded(true)) {
// Don't queue up any items when we aren't in game
// Any Items missed this way will get synched when we load the save
return;
}
std::string logMessage = "[LOG] Received " + apItem.itemName;
ArchipelagoConsole_SendMessage(logMessage.c_str(), true);
if(apItem.index < gSaveContext.ship.quest.data.archipelago.lastReceivedItemIndex) {
// Skip queueing any items we already have
std::string logMessage = "[LOG] Skipping giving " + apItem.itemName + ". We received this previously.";
ArchipelagoConsole_SendMessage(logMessage.c_str(), true);
return;
}
// add item to the queue
receiveQueue.push(apItem);
}
void ArchipelagoClient::QueueItem(const ApItem item) {
std::string logMessage = "[LOG] Giving " + item.itemName;
ArchipelagoConsole_SendMessage(logMessage.c_str(), true);
const RandomizerGet RG = Rando::StaticData::itemNameToEnum[item.itemName];
if(RG == RG_NONE) {
return;
}
itemQueued = true;
GameInteractor_ExecuteOnArchipelagoItemReceived(static_cast<int32_t>(RG));
}
void ArchipelagoClient::SendGameWon() {
if(!gameWon) {
apClient->StatusUpdate(APClient::ClientStatus::GOAL);
gameWon = true;
}
}
void ArchipelagoClient::SendMessageToConsole(const std::string message) {
// local commands not implemented yet
if(message.starts_with("/")) {
ArchipelagoConsole_SendMessage("Ship of Harkinian does not have any local commands yet.\nUse \"!help\" to see server commands instead", false);
return;
}
if(apClient == nullptr) {
ArchipelagoConsole_SendMessage("[ERROR] Could not send message. Please Connect to your slot.", false);
return;
}
apClient->Say(message);
}
void ArchipelagoClient::Poll() {
if(apClient == nullptr) {
return;
}
if(disconnecting) {
apClient->reset();
apClient = nullptr;
return;
}
// queue another item to be received
if(!itemQueued && receiveQueue.size() > 0) {
const ApItem item = receiveQueue.front();
receiveQueue.pop();
QueueItem(item);
}
apClient->poll();
}
bool ArchipelagoClient::isRightSaveLoaded() const {
const bool seedMatch = apClient->get_seed().compare(gSaveContext.ship.quest.data.archipelago.roomHash) == 0;
const bool slotMatch = GetSlotName().compare(gSaveContext.ship.quest.data.archipelago.slotName) == 0;
return seedMatch && slotMatch;
}
const std::string ArchipelagoClient::GetSlotName() const {
if(apClient == NULL) {
return "";
}
return apClient->get_slot();
}
const nlohmann::json ArchipelagoClient::GetSlotData() {
return slotData;
}
const std::vector<ArchipelagoClient::ApItem>& ArchipelagoClient::GetScoutedItems() {
return scoutedItems;
}
const char* ArchipelagoClient::GetConnectionStatus() {
if (!apClient) {
return "Disconnected!";
}
APClient::State clientStatus = apClient->get_state();
switch (clientStatus) {
case APClient::State::DISCONNECTED: {
return "Disconnected!";
}
case APClient::State::SOCKET_CONNECTING: {
return "Socket Connecting!";
}
case APClient::State::SOCKET_CONNECTED: {
return "Socket Connected!";
}
case APClient::State::ROOM_INFO: {
return "Room info Received!";
}
case APClient::State::SLOT_CONNECTED: {
return "Slot Connected!";
}
default:
return "";
}
}
extern "C" void Archipelago_InitSaveFile() {
gSaveContext.ship.quest.data.archipelago.isArchipelago = 1;
nlohmann::json slotData = ArchipelagoClient::GetInstance().GetSlotData();
gSaveContext.ship.quest.data.archipelago.deathLink = slotData["death_link"];
std::vector<ArchipelagoClient::ApItem> scoutedItems = ArchipelagoClient::GetInstance().GetScoutedItems();
ArchipelagoClient& client = ArchipelagoClient::GetInstance();
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.roomHash, client.apClient->get_seed(),
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.roomHash));
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.slotName, client.apClient->get_slot(),
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.slotName));
for (uint32_t i = 0; i < scoutedItems.size(); i++) {
RandomizerCheck rc = Rando::StaticData::locationNameToEnum[scoutedItems[i].locationName];
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.locations[rc].itemName,
scoutedItems[i].itemName,
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations[rc].itemName));
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.locations[rc].playerName,
scoutedItems[i].playerName,
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations[rc].playerName));
}
}
void LoadArchipelagoData() {
SaveManager::Instance->LoadData("isArchipelago", gSaveContext.ship.quest.data.archipelago.isArchipelago);
SaveManager::Instance->LoadData("lastReceivedItemIndex",
gSaveContext.ship.quest.data.archipelago.lastReceivedItemIndex);
SaveManager::Instance->LoadData("deathLink", gSaveContext.ship.quest.data.archipelago.deathLink);
SaveManager::Instance->LoadCharArray("roomHash", gSaveContext.ship.quest.data.archipelago.roomHash,
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.roomHash));
SaveManager::Instance->LoadCharArray("slotName", gSaveContext.ship.quest.data.archipelago.slotName,
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.slotName));
SaveManager::Instance->LoadArray(
"locations", ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations),
[](size_t i) {
SaveManager::Instance->LoadStruct("", [&i]() {
SaveManager::Instance->LoadCharArray(
"itemName", gSaveContext.ship.quest.data.archipelago.locations[i].itemName,
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations[i].itemName));
SaveManager::Instance->LoadCharArray(
"playerName", gSaveContext.ship.quest.data.archipelago.locations[i].playerName,
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations[i].playerName));
});
});
}
void SaveArchipelagoData(SaveContext* saveContext, int sectionID, bool fullSave) {
SaveManager::Instance->SaveData("isArchipelago", saveContext->ship.quest.data.archipelago.isArchipelago);
SaveManager::Instance->SaveData("lastReceivedItemIndex",
saveContext->ship.quest.data.archipelago.lastReceivedItemIndex);
SaveManager::Instance->SaveData("deathLink", saveContext->ship.quest.data.archipelago.deathLink);
SaveManager::Instance->SaveData("roomHash", saveContext->ship.quest.data.archipelago.roomHash);
SaveManager::Instance->SaveData("slotName", saveContext->ship.quest.data.archipelago.slotName);
SaveManager::Instance->SaveArray(
"locations", ARRAY_COUNT(saveContext->ship.quest.data.archipelago.locations), [&](size_t i) {
SaveManager::Instance->SaveStruct("", [&]() {
SaveManager::Instance->SaveData("itemName",
saveContext->ship.quest.data.archipelago.locations[i].itemName);
SaveManager::Instance->SaveData("playerName",
saveContext->ship.quest.data.archipelago.locations[i].playerName);
});
});
}
void InitArchipelagoData(bool isDebug) {
gSaveContext.ship.quest.data.archipelago.isArchipelago = 0;
gSaveContext.ship.quest.data.archipelago.lastReceivedItemIndex = 0;
gSaveContext.ship.quest.data.archipelago.deathLink = 0;
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.roomHash, "",
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.roomHash));
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.slotName, "",
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.slotName));
for (uint32_t i = 0; i < ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations); i++) {
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.locations[i].itemName, "",
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations[i].itemName));
SohUtils::CopyStringToCharArray(gSaveContext.ship.quest.data.archipelago.locations[i].playerName, "",
ARRAY_COUNT(gSaveContext.ship.quest.data.archipelago.locations[i].playerName));
}
}
void RegisterArchipelago() {
// make sure the client is constructed
ArchipelagoClient::GetInstance();
CVarSetInteger(CVAR_REMOTE_ARCHIPELAGO("Connected"), 0);
COND_HOOK(GameInteractor::OnGameFrameUpdate, true, [](){ArchipelagoClient::GetInstance().Poll();});
COND_HOOK(GameInteractor::OnLoadGame, true, [](int32_t file_id){ArchipelagoClient::GetInstance().GameLoaded();});
COND_HOOK(GameInteractor::OnRandomizerItemGivenHooks, IS_ARCHIPELAGO,
[](uint32_t rc, GetItemEntry gi, uint8_t isGiSkipped) {
if (rc == RC_ARCHIPELAGO_RECEIVED_ITEM) {
gSaveContext.ship.quest.data.archipelago.lastReceivedItemIndex++;
ArchipelagoClient::GetInstance().itemQueued = false;
} else {
ArchipelagoClient::GetInstance().CheckLocation((RandomizerCheck)rc);
if (isGiSkipped && gi.modIndex == MOD_RANDOMIZER &&
(gi.getItemId == RG_ARCHIPELAGO_ITEM_PROGRESSIVE || gi.getItemId == RG_ARCHIPELAGO_ITEM_USEFUL ||
gi.getItemId == RG_ARCHIPELAGO_ITEM_JUNK)) {
const char* itemIcon = "";
switch (gi.getItemId) {
case RG_ARCHIPELAGO_ITEM_PROGRESSIVE:
itemIcon = "Archipelago Progressive Icon";
break;
case RG_ARCHIPELAGO_ITEM_USEFUL:
itemIcon = "Archipelago Useful Icon";
break;
case RG_ARCHIPELAGO_ITEM_JUNK:
itemIcon = "Archipelago Junk Icon";
break;
}
Notification::Emit({
.itemIcon = itemIcon,
.prefix = std::string(gSaveContext.ship.quest.data.archipelago.locations[rc].itemName),
.message = " for ",
.suffix = std::string(gSaveContext.ship.quest.data.archipelago.locations[rc].playerName)
});
}
}
});
}
static RegisterShipInitFunc initFunc(RegisterArchipelago, { "IS_ARCHIPELAGO" });