diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp index 085b35c2aca..2b6004c4fe7 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -82,9 +83,10 @@ void from_json(const json& j, AnchorClient& client) { j.contains("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???"; j.contains("name") ? j.at("name").get_to(client.name) : client.name = "???"; j.contains("color") ? j.at("color").get_to(client.color) : client.color = {255, 255, 255}; - j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = "???"; + j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0; + j.contains("fileNum") ? j.at("fileNum").get_to(client.fileNum) : client.fileNum = 0xFF; j.contains("gameComplete") ? j.at("gameComplete").get_to(client.gameComplete) : client.gameComplete = false; - j.contains("scene") ? j.at("scene").get_to(client.scene) : client.scene = SCENE_ID_MAX; + j.contains("sceneNum") ? j.at("sceneNum").get_to(client.sceneNum) : client.sceneNum = SCENE_ID_MAX; j.contains("roomIndex") ? j.at("roomIndex").get_to(client.roomIndex) : client.roomIndex = 0; j.contains("entranceIndex") ? j.at("entranceIndex").get_to(client.entranceIndex) : client.entranceIndex = 0; j.contains("posRot") ? j.at("posRot").get_to(client.posRot) : client.posRot = { -9999, -9999, -9999, 0, 0, 0 }; @@ -141,7 +143,7 @@ void to_json(json& j, const SohStats& sohStats) { void from_json(const json& j, SohStats& sohStats) { j.at("locationsSkipped").get_to(sohStats.locationsSkipped); - j.contains("fileCreatedAt") ? j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt) : gSaveContext.sohStats.fileCreatedAt; + j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt); } void to_json(json& j, const SaveContext& saveContext) { @@ -188,8 +190,7 @@ void from_json(const json& j, SaveContext& saveContext) { std::map GameInteractorAnchor::AnchorClients = {}; std::vector GameInteractorAnchor::FairyIndexToClientId = {}; -std::string GameInteractorAnchor::clientVersion = "Anchor Build 11"; -std::string GameInteractorAnchor::seed = "00000"; +std::string GameInteractorAnchor::clientVersion = "Anchor Build 12 (alpha)"; std::vector> receivedItems = {}; std::vector anchorMessages = {}; uint32_t notificationId = 0; @@ -197,20 +198,25 @@ uint32_t notificationId = 0; void Anchor_DisplayMessage(AnchorMessage message = {}) { message.id = notificationId++; anchorMessages.push_back(message); + Audio_PlaySoundGeneral(NA_SE_SY_METRONOME, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); } void Anchor_SendClientData() { - GameInteractorAnchor::seed = std::accumulate(std::begin(gSaveContext.seedIcons), std::end(gSaveContext.seedIcons), std::string(), [](std::string a, int b) { - return a + std::to_string(b); - }); - nlohmann::json payload; payload["data"]["name"] = CVarGetString("gRemote.AnchorName", ""); payload["data"]["color"] = CVarGetColor24("gRemote.AnchorColor", { 100, 255, 100 }); payload["data"]["clientVersion"] = GameInteractorAnchor::clientVersion; - payload["data"]["seed"] = GameInteractorAnchor::seed; + payload["data"]["seed"] = gSaveContext.finalSeed; + payload["data"]["fileNum"] = gSaveContext.fileNum; payload["data"]["gameComplete"] = gSaveContext.sohStats.gameComplete; payload["type"] = "UPDATE_CLIENT_DATA"; + + if (gPlayState != NULL) { + payload["data"]["sceneNum"] = gPlayState->sceneNum; + } else { + payload["data"]["sceneNum"] = SCENE_ID_MAX; + } + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); } @@ -226,7 +232,12 @@ void GameInteractorAnchor::Enable() { }); GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() { Anchor_DisplayMessage({ .message = "Connected to Anchor" }); - Anchor_SendClientData(); + if (GameInteractor::IsSaveLoaded() || gSaveContext.fileNum == 0xFF) { + Anchor_SendClientData(); + } + if (GameInteractor::IsSaveLoaded()) { + Anchor_RequestSaveStateFromRemote(); + } }); GameInteractor::Instance->RegisterRemoteDisconnectedHandler([&]() { Anchor_DisplayMessage({ .message = "Disconnected from Anchor" }); @@ -336,7 +347,7 @@ void GameInteractorAnchor::HandleRemoteJson(nlohmann::json payload) { uint32_t clientId = payload["clientId"].get(); if (GameInteractorAnchor::AnchorClients.contains(clientId)) { - GameInteractorAnchor::AnchorClients[clientId].scene = payload["sceneNum"].get(); + GameInteractorAnchor::AnchorClients[clientId].sceneNum = payload["sceneNum"].get(); GameInteractorAnchor::AnchorClients[clientId].roomIndex = payload.contains("roomIndex") ? payload.at("roomIndex").get() : 0; GameInteractorAnchor::AnchorClients[clientId].entranceIndex = payload.contains("entranceIndex") ? payload.at("entranceIndex").get() : 0; GameInteractorAnchor::AnchorClients[clientId].posRot = payload["posRot"].get(); @@ -360,6 +371,7 @@ void GameInteractorAnchor::HandleRemoteJson(nlohmann::json payload) { client.name, client.color, client.seed, + client.fileNum, client.gameComplete, SCENE_ID_MAX, 0, @@ -402,7 +414,9 @@ void GameInteractorAnchor::HandleRemoteJson(nlohmann::json payload) { GameInteractorAnchor::AnchorClients[clientId].name = client.name; GameInteractorAnchor::AnchorClients[clientId].color = client.color; GameInteractorAnchor::AnchorClients[clientId].seed = client.seed; + GameInteractorAnchor::AnchorClients[clientId].fileNum = client.fileNum; GameInteractorAnchor::AnchorClients[clientId].gameComplete = client.gameComplete; + GameInteractorAnchor::AnchorClients[clientId].sceneNum = client.sceneNum; } } if (payload["type"] == "SKIP_LOCATION" && GameInteractor::IsSaveLoaded()) { @@ -440,6 +454,9 @@ void GameInteractorAnchor::HandleRemoteJson(nlohmann::json payload) { GameInteractor::Instance->isRemoteInteractorEnabled = false; GameInteractorAnchor::Instance->isEnabled = false; } + if (payload["type"] == "RESET") { + std::reinterpret_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch("reset"); + } } void Anchor_PushSaveStateToRemote() { @@ -511,6 +528,7 @@ void Anchor_ParseSaveStateFromRemote(nlohmann::json payload) { gSaveContext.sohStats.locationsSkipped[i] = loadedData.sohStats.locationsSkipped[i]; } } + gSaveContext.sohStats.fileCreatedAt = loadedData.sohStats.fileCreatedAt; // Restore master sword state u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_SWORD, 1); @@ -544,7 +562,7 @@ uint8_t Anchor_GetClientScene(uint32_t fairyIndex) { return SCENE_ID_MAX; } - return GameInteractorAnchor::AnchorClients[clientId].scene; + return GameInteractorAnchor::AnchorClients[clientId].sceneNum; } PosRot Anchor_GetClientPosition(uint32_t fairyIndex) { @@ -595,12 +613,29 @@ void Anchor_SpawnClientFairies() { } } +static uint32_t lastSceneNum = SCENE_ID_MAX; + void Anchor_RegisterHooks() { - GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || gSaveContext.fileNum > 2) return; + GameInteractor::Instance->RegisterGameHook([]() { + if (gPlayState == NULL || !GameInteractor::Instance->isRemoteInteractorConnected) return; + + // Moved to a new scene + if (lastSceneNum != gPlayState->sceneNum) { + Anchor_SendClientData(); + } + + // Player loaded into file + if (lastSceneNum == SCENE_ID_MAX && GameInteractor::Instance->IsSaveLoaded()) { + Anchor_RequestSaveStateFromRemote(); + } + + lastSceneNum = gPlayState->sceneNum; + }); + GameInteractor::Instance->RegisterGameHook([]() { + lastSceneNum = SCENE_ID_MAX; + if (!GameInteractor::Instance->isRemoteInteractorConnected) return; Anchor_SendClientData(); - Anchor_RequestSaveStateFromRemote(); }); GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { if (itemEntry.modIndex == MOD_NONE && (itemEntry.itemId == ITEM_KEY_SMALL || itemEntry.itemId == ITEM_KEY_BOSS || itemEntry.itemId == ITEM_SWORD_MASTER)) { @@ -700,7 +735,12 @@ void Anchor_RegisterHooks() { lastPosition = currentPosition; lastPlayerCount = currentPlayerCount; - GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { + if (client.sceneNum == gPlayState->sceneNum) { + payload["targetClientId"] = clientId; + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + } + } }); } @@ -793,17 +833,26 @@ void AnchorPlayerLocationWindow::DrawElement() { ); ImGui::TextColored(gSaveContext.sohStats.gameComplete ? GREEN : WHITE, "%s", CVarGetString("gRemote.AnchorName", "")); - if (gPlayState != NULL) { + if (gPlayState != NULL && gSaveContext.fileNum != 255) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.5, 0.5, 0.5, 1), "%s", SohUtils::GetSceneName(gPlayState->sceneNum).c_str()); } for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { ImGui::PushID(clientId); ImGui::TextColored(client.gameComplete ? GREEN : WHITE, "%s", client.name.c_str()); - if (client.scene < SCENE_ID_MAX) { + if (client.seed != gSaveContext.finalSeed && client.fileNum != 0xFF && gSaveContext.fileNum != 0xFF) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Seed mismatch (%u != %u)", client.seed, gSaveContext.finalSeed); + ImGui::EndTooltip(); + } + } + if (client.sceneNum < SCENE_ID_MAX && client.fileNum != 0xFF) { ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.5, 0.5, 0.5, 1), "%s", SohUtils::GetSceneName(client.scene).c_str()); - if (gPlayState != NULL && client.scene != SCENE_GROTTOS && client.scene != SCENE_ID_MAX) { + ImGui::TextColored(ImVec4(0.5, 0.5, 0.5, 1), "%s", SohUtils::GetSceneName(client.sceneNum).c_str()); + if (gPlayState != NULL && client.sceneNum != SCENE_GROTTOS && client.sceneNum != SCENE_ID_MAX) { ImGui::SameLine(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); if (ImGui::Button(ICON_FA_CHEVRON_RIGHT, ImVec2(ImGui::GetFontSize() * 1.0f, ImGui::GetFontSize() * 1.0f))) { diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h index 31b83b0614c..41c8841b84e 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h @@ -12,9 +12,10 @@ typedef struct { std::string clientVersion; std::string name; Color_RGB8 color; - std::string seed; + uint32_t seed; + uint8_t fileNum; bool gameComplete; - uint8_t scene; + uint8_t sceneNum; uint8_t roomIndex; uint32_t entranceIndex; PosRot posRot; @@ -30,7 +31,6 @@ class GameInteractorAnchor { static std::map AnchorClients; static std::vector FairyIndexToClientId; static std::string clientVersion; - static std::string seed; void Enable(); void Disable(); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp index 2cbd6b379f0..eea9cd457ff 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp @@ -73,12 +73,14 @@ void GameInteractor::DisableRemoteInteractor() { } void GameInteractor::TransmitDataToRemote(const char* payload) { - SDLNet_TCP_Send(remoteSocket, payload, strlen(payload) + 1); + SDLNet_TCP_Send(remoteSocket, payload, strlen(payload)); } // Appends a newline character to the end of the json payload and sends it to the remote void GameInteractor::TransmitJsonToRemote(nlohmann::json payload) { - TransmitDataToRemote(payload.dump().c_str()); + // TODO: Migrate anchor server to use null terminators instead of newlines + std::string payloadString = payload.dump() + '\n'; + TransmitDataToRemote(payloadString.c_str()); } // MARK: - Private @@ -134,7 +136,8 @@ void GameInteractor::ReceiveFromServer() { receivedData.append(remoteDataReceived, len); // Proess all complete packets - size_t delimiterPos = receivedData.find('\0'); + // TODO: Migrate anchor server to use null terminators instead of newlines + size_t delimiterPos = receivedData.find('\n'); while (delimiterPos != std::string::npos) { // Extract the complete packet until the delimiter std::string packet = receivedData.substr(0, delimiterPos); @@ -142,7 +145,7 @@ void GameInteractor::ReceiveFromServer() { receivedData.erase(0, delimiterPos + 1); HandleRemoteJson(packet); // Find the next delimiter - delimiterPos = receivedData.find('\0'); + delimiterPos = receivedData.find('\n'); } } diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index c56969b82af..21243ea0257 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -1590,12 +1590,12 @@ void DrawRemoteControlMenu() { ImGui::EndTooltip(); } } - if (client.seed != GameInteractorAnchor::seed) { + if (client.seed != gSaveContext.finalSeed && client.fileNum != 0xFF && gSaveContext.fileNum != 0xFF) { ImGui::SameLine(); ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); - ImGui::Text("Seed mismatch (%s)", client.seed.c_str()); + ImGui::Text("Seed mismatch (%u != %u)", client.seed, gSaveContext.finalSeed); ImGui::EndTooltip(); } }