From 458a97f7d1367de7d16668068d4ebc516708e104 Mon Sep 17 00:00:00 2001 From: Garrett Date: Sat, 15 Nov 2025 14:12:36 -0600 Subject: [PATCH 1/2] Add initial sail implementation --- .github/workflows/main.yml | 18 +- CMakeLists.txt | 6 - mm/2s2h/BenPort.cpp | 28 ++- mm/2s2h/Network/Network.cpp | 160 +++++++++++++++ mm/2s2h/Network/Network.h | 53 +++++ mm/2s2h/Network/NetworkMenu.cpp | 106 ++++++++++ mm/2s2h/Network/Sail/Sail.cpp | 339 ++++++++++++++++++++++++++++++++ mm/2s2h/Network/Sail/Sail.h | 21 ++ mm/2s2h/ShipUtils.cpp | 14 ++ mm/2s2h/ShipUtils.h | 1 + mm/CMakeLists.txt | 15 +- 11 files changed, 729 insertions(+), 32 deletions(-) create mode 100644 mm/2s2h/Network/Network.cpp create mode 100644 mm/2s2h/Network/Network.h create mode 100644 mm/2s2h/Network/NetworkMenu.cpp create mode 100644 mm/2s2h/Network/Sail/Sail.cpp create mode 100644 mm/2s2h/Network/Sail/Sail.h diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b67de5a2b..53d824c80e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -130,7 +130,7 @@ jobs: - name: Build 2Ship run: | export PATH="/usr/lib/ccache:/opt/homebrew/opt/ccache/libexec:/usr/local/opt/ccache/libexec:$PATH" - cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" + cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DBUILD_NETWORKING=1 cmake --build build-cmake --config Release --parallel 10 (cd build-cmake && cpack) @@ -207,6 +207,18 @@ jobs: cmake .. make sudo make install + - name: Install latest SDL_net + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + if [ ! -d "SDL2_net-2.2.0" ]; then + wget https://www.libsdl.org/projects/SDL_net/release/SDL2_net-2.2.0.tar.gz + tar -xzf SDL2_net-2.2.0.tar.gz + fi + cd SDL2_net-2.2.0 + ./configure + make -j 10 + sudo make install + sudo cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/ - name: Install libzip without crypto run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" @@ -229,7 +241,7 @@ jobs: - name: Build 2Ship run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DBUILD_REMOTE_CONTROL=1 + cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DBUILD_NETWORKING=1 cmake --build build-cmake --config Release -j3 (cd build-cmake && cpack -G External) @@ -296,7 +308,7 @@ jobs: VCPKG_ROOT: ${{github.workspace}}/vcpkg run: | set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH" - cmake -S . -B build-windows -G Ninja -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + cmake -S . -B build-windows -G Ninja -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DBUILD_NETWORKING=1 cmake --build build-windows --config Release --parallel 10 (cd build-windows && cpack) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a120c9489..9631746c49 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,12 +59,6 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") endif() endif() -if (CMAKE_SYSTEM_NAME MATCHES "Windows|Linux") - if(NOT DEFINED BUILD_CROWD_CONTROL) - set(BUILD_CROWD_CONTROL OFF) - endif() -endif() - # Enable the Gfx debugger in LUS to use libgfxd from ZAPDTR set(GFX_DEBUG_DISASSEMBLER ON) diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index d913c05920..f392bb4d2a 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -40,10 +40,7 @@ //#include #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" -#ifdef ENABLE_CROWD_CONTROL -#include "Enhancements/crowd-control/CrowdControl.h" -CrowdControl* CrowdControl::Instance; -#endif +#include "2s2h/Network/Sail/Sail.h" #include #include @@ -113,6 +110,7 @@ CrowdControl* CrowdControl::Instance; OTRGlobals* OTRGlobals::Instance; GameInteractor* GameInteractor::Instance; AudioCollection* AudioCollection::Instance; +Sail* Sail::Instance; extern "C" char** cameraStrings; bool prevAltAssets = false; @@ -704,6 +702,7 @@ extern "C" void InitOTR() { OTRGlobals::Instance = new OTRGlobals(); GameInteractor::Instance = new GameInteractor(); AudioCollection::Instance = new AudioCollection(); + Sail::Instance = new Sail(); LoadGuiTextures(); BenGui::SetupGuiElements(); ShipInit::InitAll(); @@ -733,15 +732,12 @@ extern "C" void InitOTR() { } srand(now); -#ifdef ENABLE_CROWD_CONTROL - CrowdControl::Instance = new CrowdControl(); - CrowdControl::Instance->Init(); - if (CVarGetInteger("gCrowdControl", 0)) { - CrowdControl::Instance->Enable(); - } else { - CrowdControl::Instance->Disable(); - } +#ifdef ENABLE_NETWORKING + SDLNet_Init(); #endif + if (CVarGetInteger("gNetwork.Sail.Enabled", 0)) { + Sail::Instance->Enable(); + } std::shared_ptr conf = OTRGlobals::Instance->context->GetConfig(); Ship::Context::GetInstance()->GetFileDropMgr()->RegisterDropHandler(BinarySaveConverter_HandleFileDropped); @@ -755,9 +751,11 @@ extern "C" void SaveManager_ThreadPoolWait() { extern "C" void DeinitOTR() { SaveManager_ThreadPoolWait(); OTRAudio_Exit(); -#ifdef ENABLE_CROWD_CONTROL - CrowdControl::Instance->Disable(); - CrowdControl::Instance->Shutdown(); + if (CVarGetInteger("gNetwork.Sail.Enabled", 0)) { + Sail::Instance->Disable(); + } +#ifdef ENABLE_NETWORKING + SDLNet_Quit(); #endif // Destroying gui here because we have shared ptrs to LUS objects which output to SPDLOG which is destroyed before diff --git a/mm/2s2h/Network/Network.cpp b/mm/2s2h/Network/Network.cpp new file mode 100644 index 0000000000..6845248ea6 --- /dev/null +++ b/mm/2s2h/Network/Network.cpp @@ -0,0 +1,160 @@ +#include "Network.h" +#include +#include + +// MARK: - Public + +void Network::Enable(const char* host, uint16_t port) { +#ifdef ENABLE_NETWORKING + if (isEnabled) { + return; + } + + if (SDLNet_ResolveHost(&networkAddress, host, port) == -1) { + SPDLOG_ERROR("[Network] SDLNet_ResolveHost: {}", SDLNet_GetError()); + } + + isEnabled = true; + + // First check if there is a thread running, if so, join it + if (receiveThread.joinable()) { + receiveThread.join(); + } + + receiveThread = std::thread(&Network::ReceiveFromServer, this); +#endif +} + +void Network::Disable() { + if (!isEnabled) { + return; + } + + isEnabled = false; + receiveThread.join(); +} + +void Network::OnIncomingData(char payload[512]) { +} + +void Network::OnIncomingJson(nlohmann::json payload) { +} + +void Network::OnConnected() { +} + +void Network::OnDisconnected() { +} + +void Network::ProcessOutgoingPackets() { +} + +void Network::SendDataToRemote(const char* payload) { +#ifdef ENABLE_NETWORKING + SPDLOG_DEBUG("[Network] Sending data: {}", payload); + SDLNet_TCP_Send(networkSocket, payload, strlen(payload) + 1); +#endif +} + +void Network::SendJsonToRemote(nlohmann::json payload) { + SendDataToRemote(payload.dump().c_str()); +} + +// MARK: - Private + +void Network::ReceiveFromServer() { +#ifdef ENABLE_NETWORKING + while (isEnabled) { + while (!isConnected && isEnabled) { + SPDLOG_TRACE("[Network] Attempting to make connection to server..."); + networkSocket = SDLNet_TCP_Open(&networkAddress); + + if (networkSocket) { + isConnected = true; + receivedData.clear(); + SPDLOG_INFO("[Network] Connection to server established!"); + + OnConnected(); + break; + } + } + + SDLNet_SocketSet socketSet = SDLNet_AllocSocketSet(1); + if (networkSocket) { + SDLNet_TCP_AddSocket(socketSet, networkSocket); + } + + // Listen to socket messages + while (isConnected && networkSocket && isEnabled) { + // we check first if socket has data, to not block in the TCP_Recv + int socketsReady = SDLNet_CheckSockets(socketSet, 0); + + if (socketsReady == -1) { + SPDLOG_ERROR("[Network] SDLNet_CheckSockets: {}", SDLNet_GetError()); + break; + } + + // Always process outgoing packets + ProcessOutgoingPackets(); + + if (socketsReady == 0) { + continue; + } + + char remoteDataReceived[512]; + memset(remoteDataReceived, 0, sizeof(remoteDataReceived)); + int len = SDLNet_TCP_Recv(networkSocket, &remoteDataReceived, sizeof(remoteDataReceived)); + if (!len || !networkSocket || len == -1) { + SPDLOG_ERROR("[Network] SDLNet_TCP_Recv: {}", SDLNet_GetError()); + break; + } + + HandleRemoteData(remoteDataReceived); + + receivedData.append(remoteDataReceived, len); + + // Proess all complete packets + size_t delimiterPos = receivedData.find('\0'); + while (delimiterPos != std::string::npos) { + // Extract the complete packet until the delimiter + std::string packet = receivedData.substr(0, delimiterPos); + // Remove the packet (including the delimiter) from the received data + receivedData.erase(0, delimiterPos + 1); + HandleRemoteJson(packet); + // Find the next delimiter + delimiterPos = receivedData.find('\0'); + } + } + + if (socketSet) { + SDLNet_FreeSocketSet(socketSet); + } + + if (isConnected) { + SDLNet_TCP_Close(networkSocket); + networkSocket = nullptr; + isConnected = false; + receivedData.clear(); + OnDisconnected(); + SPDLOG_INFO("[Network] Ending receiving thread..."); + } + } +#endif +} + +void Network::HandleRemoteData(char payload[512]) { + OnIncomingData(payload); +} + +void Network::HandleRemoteJson(std::string payload) { + SPDLOG_DEBUG("[Network] Received json: {}", payload); + nlohmann::json jsonPayload; + try { + jsonPayload = nlohmann::json::parse(payload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[Network] Failed to parse json: \n{}\n{}\n", payload, e.what()); + return; + } + + OnIncomingJson(jsonPayload); +} diff --git a/mm/2s2h/Network/Network.h b/mm/2s2h/Network/Network.h new file mode 100644 index 0000000000..ca42bc6c92 --- /dev/null +++ b/mm/2s2h/Network/Network.h @@ -0,0 +1,53 @@ +#ifndef NETWORK_H +#define NETWORK_H +#ifdef __cplusplus + +#include +#ifdef ENABLE_NETWORKING +#include +#endif +#include + +class Network { + private: +#ifdef ENABLE_NETWORKING + IPaddress networkAddress; + TCPsocket networkSocket; +#endif + std::thread receiveThread; + std::string receivedData; + + void ReceiveFromServer(); + void HandleRemoteData(char payload[512]); + void HandleRemoteJson(std::string payload); + + public: + bool isEnabled; + bool isConnected; + + void Enable(const char* host, uint16_t port); + void Disable(); + /** + * Raw data handler + * + * If you are developing a new remote, you should probably use the json methods instead. This + * method requires you to parse the data and ensure packets are complete manually, we cannot + * gaurentee that the data will be complete, or that it will only contain one packet with this + */ + virtual void OnIncomingData(char payload[512]); + /** + * Json handler + * + * This method will be called when a complete json packet is received. All json packets must + * be delimited by a null terminator (\0). + */ + virtual void OnIncomingJson(nlohmann::json payload); + virtual void OnConnected(); + virtual void OnDisconnected(); + virtual void ProcessOutgoingPackets(); + void SendDataToRemote(const char* payload); + virtual void SendJsonToRemote(nlohmann::json packet); +}; + +#endif // __cplusplus +#endif // NETWORK_H diff --git a/mm/2s2h/Network/NetworkMenu.cpp b/mm/2s2h/Network/NetworkMenu.cpp new file mode 100644 index 0000000000..763c152266 --- /dev/null +++ b/mm/2s2h/Network/NetworkMenu.cpp @@ -0,0 +1,106 @@ +#include "2s2h/BenGui/UIWidgets.hpp" +#include "ShipUtils.h" +#include "2s2h/BenGui/BenMenu.h" +#include "2s2h/BenGui/Notification.h" +#include "2s2h/Network/Sail/Sail.h" + +namespace BenGui { +extern std::shared_ptr mBenMenu; +} // namespace BenGui +using namespace UIWidgets; + +void RegisterNetworkMenu() { + // Add Network Menu + BenGui::mBenMenu->AddMenuEntry("Network", "gSettings.Menu.NetworkSidebarSection"); + WidgetPath path; + +#ifndef ENABLE_NETWORKING + path = { "Network", "Info", SECTION_COLUMN_1 }; + BenGui::mBenMenu->AddSidebarEntry("Network", path.sidebarName, 2); + + BenGui::mBenMenu + ->AddWidget(path, + ICON_FA_EXCLAMATION_TRIANGLE + " The Network features are unavailable because SoH was compiled without " + "network support (\"ENABLE_NETWORKING\" build flag).", + WIDGET_TEXT) + .Options(TextOptions().Color(Colors::Orange)); + return; +#endif + + // Sail + path = { "Network", "Sail", SECTION_COLUMN_1 }; + BenGui::mBenMenu->AddSidebarEntry("Network", path.sidebarName, 3); + BenGui::mBenMenu->AddWidget(path, "Host & Port", WIDGET_CUSTOM).CustomFunction([](WidgetInfo& info) { + ImGui::BeginDisabled(Sail::Instance->isEnabled); + ImGui::Text("%s", info.name.c_str()); + CVarInputString("##HostSail", "gNetwork.Sail.Host", + InputOptions() + .PlaceholderText("127.0.0.1") + .DefaultValue("127.0.0.1") + .Size(ImVec2(ImGui::GetContentRegionAvail().x - ImGui::GetFontSize() * 7, 0)) + .LabelPosition(LabelPosition::None)); + ImGui::SameLine(); + ImGui::Text(":"); + ImGui::SameLine(); + CVarInputInt("##PortSail", "gNetwork.Sail.Port", + InputOptions() + .PlaceholderText("43384") + .DefaultValue("43384") + .Size(ImVec2(ImGui::GetFontSize() * 5, 0)) + .LabelPosition(LabelPosition::None)); + ImGui::EndDisabled(); + }); + BenGui::mBenMenu->AddWidget(path, "Enable##Sail", WIDGET_BUTTON) + .PreFunc([](WidgetInfo& info) { + std::string host = CVarGetString("gNetwork.Sail.Host", "127.0.0.1"); + uint16_t port = CVarGetInteger("gNetwork.Sail.Port", 43384); + info.options->disabled = !(!isStringEmpty(host) && port > 1024 && port < 65535); + if (Sail::Instance->isEnabled) { + info.name = "Disable##Sail"; + } else { + info.name = "Enable##Sail"; + } + }) + .Callback([](WidgetInfo& info) { + if (Sail::Instance->isEnabled) { + CVarClear("gNetwork.Sail.Enabled"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + Sail::Instance->Disable(); + } else { + CVarSetInteger("gNetwork.Sail.Enabled", 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + Sail::Instance->Enable(); + } + }); + BenGui::mBenMenu->AddWidget(path, "Connecting...##Sail", WIDGET_TEXT).PreFunc([](WidgetInfo& info) { + info.isHidden = !Sail::Instance->isEnabled; + if (Sail::Instance->isConnected) { + info.name = "Connected##Sail"; + } else { + info.name = "Connecting...##Sail"; + } + }); + path.column = SECTION_COLUMN_2; + BenGui::mBenMenu->AddWidget(path, + "Sail is a networking protocol designed to facilitate remote " + "control of the 2Ship2Harkinian client. It is intended to " + "be utilized alongside a Sail server, for which we provide a " + "few straightforward implementations on our GitHub. The current " + "implementations available allow integration with Twitch chat " + "and SAMMI Bot, feel free to contribute your own!\n" + "\n" + "Click this button to copy the link to the Sail Github " + "page to your clipboard.", + WIDGET_TEXT); + BenGui::mBenMenu->AddWidget(path, ICON_FA_CLIPBOARD "##Sail", WIDGET_BUTTON) + .Callback([](WidgetInfo& info) { + ImGui::SetClipboardText("https://github.com/HarbourMasters/sail"); + Notification::Emit({ + .message = "Copied to clipboard", + }); + }) + .Options(ButtonOptions().Tooltip("https://github.com/HarbourMasters/sail")); +} + +static RegisterMenuInitFunc initFunc(RegisterNetworkMenu); \ No newline at end of file diff --git a/mm/2s2h/Network/Sail/Sail.cpp b/mm/2s2h/Network/Sail/Sail.cpp new file mode 100644 index 0000000000..acf1f0eada --- /dev/null +++ b/mm/2s2h/Network/Sail/Sail.cpp @@ -0,0 +1,339 @@ +#include "Sail.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" + +extern "C" { +#include "variables.h" +} + +std::unordered_map> filteredHookIds; +std::unordered_map hookIds; + +void OnSceneInitHandler(s8 sceneId, s8 spawnNum) { + if (!Sail::Instance->isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnSceneInit"; + payload["hook"]["sceneNum"] = sceneId; + + Sail::Instance->SendJsonToRemote(payload); +} + +void OnItemGiveHandler(u8 item) { + if (!Sail::Instance->isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnItemGive"; + payload["hook"]["itemId"] = item; + + Sail::Instance->SendJsonToRemote(payload); +} + +void OnActorInitHandler(void* refActor) { + if (!Sail::Instance->isConnected) + return; + + Actor* actor = (Actor*)refActor; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnActorInit"; + payload["hook"]["actorId"] = actor->id; + payload["hook"]["params"] = actor->params; + + Sail::Instance->SendJsonToRemote(payload); +} + +void OnFlagSetHandler(int16_t flagType, int16_t flag) { + if (!Sail::Instance->isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnFlagSet"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + + Sail::Instance->SendJsonToRemote(payload); +} + +void OnFlagUnsetHandler(int16_t flagType, int16_t flag) { + if (!Sail::Instance->isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnFlagUnset"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + + Sail::Instance->SendJsonToRemote(payload); +} + +void OnSceneFlagSetHandler(int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!Sail::Instance->isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnSceneFlagSet"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + payload["hook"]["sceneNum"] = sceneNum; + + Sail::Instance->SendJsonToRemote(payload); +} + +void OnSceneFlagUnsetHandler(int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!Sail::Instance->isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnSceneFlagUnset"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + payload["hook"]["sceneNum"] = sceneNum; + + Sail::Instance->SendJsonToRemote(payload); +} + +void Sail::Enable() { + Network::Enable(CVarGetString("gNetwork.Sail.Host", "127.0.0.1"), CVarGetInteger("gNetwork.Sail.Port", 43384)); +} + +#define HANDLE_ID_SUBSCRIBE(hookType) \ + { \ + if (eventName == #hookType) { \ + if (payload.contains("eventIdFilter")) { \ + int32_t eventIdFilter = payload["eventIdFilter"].get(); \ + if (!filteredHookIds[eventName].contains(eventIdFilter)) { \ + filteredHookIds[eventName][eventIdFilter] = \ + GameInteractor::Instance->RegisterGameHookForID(eventIdFilter, \ + hookType##Handler); \ + } \ + } else { \ + if (!hookIds.contains(eventName)) { \ + hookIds[eventName] = \ + GameInteractor::Instance->RegisterGameHook(hookType##Handler); \ + } \ + } \ + responsePayload["status"] = "success"; \ + } \ + } + +#define HANDLE_SUBSCRIBE(hookType) \ + { \ + if (eventName == #hookType) { \ + if (!hookIds.contains(eventName)) { \ + hookIds[eventName] = \ + GameInteractor::Instance->RegisterGameHook(hookType##Handler); \ + } \ + responsePayload["status"] = "success"; \ + } \ + } + +#define HANDLE_ID_UNSUBSCRIBE(hookType) \ + { \ + if (eventName == #hookType) { \ + if (payload.contains("eventIdFilter")) { \ + int32_t eventIdFilter = payload["eventIdFilter"].get(); \ + if (filteredHookIds[eventName].contains(eventIdFilter)) { \ + GameInteractor::Instance->UnregisterGameHookForID( \ + filteredHookIds[eventName][eventIdFilter]); \ + filteredHookIds[eventName].erase(eventIdFilter); \ + } \ + } else { \ + if (hookIds.contains(eventName)) { \ + GameInteractor::Instance->UnregisterGameHook(hookIds[eventName]); \ + hookIds.erase(eventName); \ + } \ + } \ + responsePayload["status"] = "success"; \ + } \ + } + +#define HANDLE_UNSUBSCRIBE(hookType) \ + { \ + if (eventName == #hookType) { \ + if (hookIds.contains(eventName)) { \ + GameInteractor::Instance->UnregisterGameHook(hookIds[eventName]); \ + hookIds.erase(eventName); \ + } \ + responsePayload["status"] = "success"; \ + } \ + } + +void Sail::OnIncomingJson(nlohmann::json payload) { + nlohmann::json responsePayload; + responsePayload["type"] = "result"; + responsePayload["status"] = "failure"; + + try { + if (!payload.contains("id")) { + SPDLOG_ERROR("[Sail] Received payload without ID"); + SendJsonToRemote(responsePayload); + return; + } + + responsePayload["id"] = payload["id"]; + + if (!payload.contains("type")) { + SPDLOG_ERROR("[Sail] Received payload without type"); + SendJsonToRemote(responsePayload); + return; + } + + std::string payloadType = payload["type"].get(); + + if (payloadType == "command") { + if (!payload.contains("command")) { + SPDLOG_ERROR("[Sail] Received command payload without command"); + SendJsonToRemote(responsePayload); + return; + } + + std::string command = payload["command"].get(); + std::reinterpret_pointer_cast( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) + ->Dispatch(command); + responsePayload["status"] = "success"; + SendJsonToRemote(responsePayload); + return; + } else if (payloadType == "effect") { + if (!payload.contains("effect") || !payload["effect"].contains("type")) { + SPDLOG_ERROR("[Sail] Received effect payload without effect type"); + SendJsonToRemote(responsePayload); + return; + } + + std::string effectType = payload["effect"]["type"].get(); + + // Special case for "command" effect, so we can also run commands from the `simple_twitch_sail` script + if (effectType == "command") { + if (!payload["effect"].contains("command")) { + SPDLOG_ERROR("[Sail] Received command effect payload without command"); + SendJsonToRemote(responsePayload); + return; + } + + std::string command = payload["effect"]["command"].get(); + std::reinterpret_pointer_cast( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) + ->Dispatch(command); + responsePayload["status"] = "success"; + SendJsonToRemote(responsePayload); + return; + } + + if (effectType == "teleport") { + if (gPlayState == NULL) { + SPDLOG_ERROR("[Sail] Teleport failed, no gPlayState"); + SendJsonToRemote(responsePayload); + return; + } + + if (!payload["effect"].contains("entranceId")) { + SPDLOG_ERROR("[Sail] Received teleport effect payload without entranceId"); + SendJsonToRemote(responsePayload); + return; + } + + gPlayState->nextEntrance = payload["effect"]["entranceId"].get(); + gSaveContext.nextCutsceneIndex = 0; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_INSTANT; + } + + if (effectType != "apply" && effectType != "remove") { + SPDLOG_ERROR("[Sail] Received effect payload with unknown effect type: {}", effectType); + SendJsonToRemote(responsePayload); + return; + } + + responsePayload["status"] = "success"; + SendJsonToRemote(responsePayload); + } else if (payloadType == "subscribe") { + if (!payload.contains("eventName")) { + SPDLOG_ERROR("[Sail] Received subscribe payload without eventName"); + SendJsonToRemote(responsePayload); + return; + } + + std::string eventName = payload["eventName"].get(); + + HANDLE_ID_SUBSCRIBE(OnSceneInit); + HANDLE_ID_SUBSCRIBE(OnItemGive); + HANDLE_ID_SUBSCRIBE(OnActorInit); + HANDLE_SUBSCRIBE(OnFlagSet); + HANDLE_SUBSCRIBE(OnFlagUnset); + HANDLE_SUBSCRIBE(OnSceneFlagSet); + HANDLE_SUBSCRIBE(OnSceneFlagUnset); + + responsePayload["status"] = "success"; + SendJsonToRemote(responsePayload); + return; + } else if (payloadType == "unsubscribe") { + if (!payload.contains("eventName")) { + SPDLOG_ERROR("[Sail] Received unsubscribe payload without eventName"); + SendJsonToRemote(responsePayload); + return; + } + + std::string eventName = payload["eventName"].get(); + + HANDLE_ID_UNSUBSCRIBE(OnSceneInit); + HANDLE_ID_UNSUBSCRIBE(OnItemGive); + HANDLE_ID_UNSUBSCRIBE(OnActorInit); + HANDLE_UNSUBSCRIBE(OnFlagSet); + HANDLE_UNSUBSCRIBE(OnFlagUnset); + HANDLE_UNSUBSCRIBE(OnSceneFlagSet); + HANDLE_UNSUBSCRIBE(OnSceneFlagUnset); + + SendJsonToRemote(responsePayload); + return; + } else { + SPDLOG_ERROR("[Sail] Unknown payload type: {}", payloadType); + SendJsonToRemote(responsePayload); + return; + } + + // If we get here, something went wrong, send the failure response + SPDLOG_ERROR("[Sail] Failed to handle remote JSON, sending failure response"); + SendJsonToRemote(responsePayload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[Sail] Exception handling remote JSON: {}", e.what()); + } catch (...) { SPDLOG_ERROR("[Sail] Unknown exception handling remote JSON"); } +} + +void Sail::OnConnected() { +} + +void Sail::OnDisconnected() { + UnregisterAllHooks(); +} + +void Sail::UnregisterAllHooks() { + for (auto& [eventName, hookId] : hookIds) { + GameInteractor::Instance->UnregisterGameHook(hookId); + } + hookIds.clear(); + + for (auto& [eventName, filteredHookId] : filteredHookIds) { + for (auto& [eventIdFilter, hookId] : filteredHookId) { + GameInteractor::Instance->UnregisterGameHookForID(hookId); + } + } + filteredHookIds.clear(); +} diff --git a/mm/2s2h/Network/Sail/Sail.h b/mm/2s2h/Network/Sail/Sail.h new file mode 100644 index 0000000000..b6d14da316 --- /dev/null +++ b/mm/2s2h/Network/Sail/Sail.h @@ -0,0 +1,21 @@ +#ifndef NETWORK_SAIL_H +#define NETWORK_SAIL_H +#ifdef __cplusplus + +#include "2s2h/Network/Network.h" + +class Sail : public Network { + private: + void UnregisterAllHooks(); + + public: + static Sail* Instance; + + void Enable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); +}; + +#endif // __cplusplus +#endif // NETWORK_SAIL_H diff --git a/mm/2s2h/ShipUtils.cpp b/mm/2s2h/ShipUtils.cpp index bb0b7a66dc..31717c7b11 100644 --- a/mm/2s2h/ShipUtils.cpp +++ b/mm/2s2h/ShipUtils.cpp @@ -497,3 +497,17 @@ std::string convertEnumToReadableName(const std::string& input) { return result; } + +bool isStringEmpty(std::string str) { + // Remove spaces at the beginning of the string + std::string::size_type start = str.find_first_not_of(' '); + // Remove spaces at the end of the string + std::string::size_type end = str.find_last_not_of(' '); + + // Check if the string is empty after stripping spaces + if (start == std::string::npos || end == std::string::npos) { + return true; // The string is empty + } else { + return false; // The string is not empty + } +} diff --git a/mm/2s2h/ShipUtils.h b/mm/2s2h/ShipUtils.h index 4845d15fc5..91a68d643f 100644 --- a/mm/2s2h/ShipUtils.h +++ b/mm/2s2h/ShipUtils.h @@ -28,6 +28,7 @@ extern ImVec4 Ship_GetItemColorTint(uint32_t itemId); uint32_t Ship_ConvertQuestIdToItem(uint32_t itemId); uint32_t Ship_ConvertItemIdToQuest(uint32_t itemId); extern uint32_t Ship_Hash(std::string str); +bool isStringEmpty(std::string str); extern "C" { #endif diff --git a/mm/CMakeLists.txt b/mm/CMakeLists.txt index 62a7bbfa5a..941ef5bb81 100644 --- a/mm/CMakeLists.txt +++ b/mm/CMakeLists.txt @@ -223,8 +223,8 @@ endif() find_package(SDL2) set(SDL2-INCLUDE ${SDL2_INCLUDE_DIRS}) -if (BUILD_CROWD_CONTROL) - find_package(SDL2_net) +if (BUILD_NETWORKING) + find_package(SDL2_net 2.2.0 REQUIRED) set(SDL2-NET-INCLUDE ${SDL_NET_INCLUDE_DIRS}) endif() @@ -253,9 +253,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "$<$:" "NDEBUG;" ">" - #"$<$:ENABLE_CROWD_CONTROL>" + "$<$:ENABLE_NETWORKING>" "INCLUDE_GAME_PRINTF;" - #"ENABLE_CROWD_CONTROL;" "F3DEX_GBI_2" "UNICODE;" "_UNICODE" @@ -288,8 +287,8 @@ elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") "$<$:" "NDEBUG;" ">" - "F3DEX_GBI_2;" - # "$<$:ENABLE_CROWD_CONTROL>" + "F3DEX_GBI_2" + "$<$:ENABLE_NETWORKING>" "_CONSOLE;" "_CRT_SECURE_NO_WARNINGS;" "ENABLE_OPENGL;" @@ -538,7 +537,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "glu32;" "SDL2::SDL2;" "SDL2::SDL2main;" - #"$<$:SDL2_net::SDL2_net-static>" + "$<$:SDL2_net::SDL2_net-static>" "glfw;" "winmm;" "imm32;" @@ -591,7 +590,7 @@ else() "Opus::opus" "Opusfile::Opusfile" SDL2::SDL2 - # "$<$:SDL2_net::SDL2_net>" + "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} Threads::Threads ) From 233e34f54b0b33a015a6e462a11497632f5d182e Mon Sep 17 00:00:00 2001 From: Garrett Date: Sun, 4 Jan 2026 22:36:52 -0600 Subject: [PATCH 2/2] Anchor Alpha --- mm/2s2h/BenGui/BenGui.cpp | 6 + mm/2s2h/BenGui/Menu.cpp | 2 +- mm/2s2h/BenGui/UIWidgets.cpp | 21 +- mm/2s2h/BenJsonConversions.hpp | 94 ++-- mm/2s2h/BenPort.cpp | 10 + mm/2s2h/DeveloperTools/SaveEditor.cpp | 14 +- .../Saving/SavingEnhancements.cpp | 3 + mm/2s2h/GameInteractor/GameInteractor.cpp | 12 + mm/2s2h/GameInteractor/GameInteractor.h | 3 + .../GameInteractor/GameInteractor_HookTable.h | 3 + mm/2s2h/Network/Anchor/Anchor.cpp | 420 ++++++++++++++++++ mm/2s2h/Network/Anchor/Anchor.h | 187 ++++++++ mm/2s2h/Network/Anchor/AnchorRoomWindow.cpp | 113 +++++ mm/2s2h/Network/Anchor/DummyPlayer.cpp | 216 +++++++++ mm/2s2h/Network/Anchor/JsonConversions.hpp | 31 ++ mm/2s2h/Network/Anchor/Menu.cpp | 336 ++++++++++++++ .../Network/Anchor/Packets/AllClientState.cpp | 71 +++ .../Network/Anchor/Packets/DamagePlayer.cpp | 70 +++ .../Network/Anchor/Packets/DisableAnchor.cpp | 14 + .../Network/Anchor/Packets/GameComplete.cpp | 42 ++ mm/2s2h/Network/Anchor/Packets/GiveItem.cpp | 59 +++ mm/2s2h/Network/Anchor/Packets/Handshake.cpp | 22 + mm/2s2h/Network/Anchor/Packets/PlayerSfx.cpp | 51 +++ .../Network/Anchor/Packets/PlayerUpdate.cpp | 108 +++++ .../Anchor/Packets/RequestTeamState.cpp | 37 ++ .../Anchor/Packets/RequestTeleport.cpp | 75 ++++ .../Network/Anchor/Packets/ServerMessage.cpp | 17 + .../Network/Anchor/Packets/SetCheckStatus.cpp | 42 ++ mm/2s2h/Network/Anchor/Packets/SetFlag.cpp | 50 +++ mm/2s2h/Network/Anchor/Packets/TeleportTo.cpp | 54 +++ mm/2s2h/Network/Anchor/Packets/UnsetFlag.cpp | 50 +++ .../Anchor/Packets/UpdateClientState.cpp | 73 +++ .../Anchor/Packets/UpdateDungeonItems.cpp | 40 ++ .../Anchor/Packets/UpdateRoomState.cpp | 57 +++ .../Anchor/Packets/UpdateTeamState.cpp | 151 +++++++ mm/2s2h/Network/NetworkMenu.cpp | 2 +- mm/2s2h/Rando/ActorBehavior/EnAkindonuts.cpp | 7 +- mm/2s2h/Rando/ActorBehavior/EnBal.cpp | 6 +- mm/2s2h/Rando/ActorBehavior/EnGirlA.cpp | 31 +- mm/2s2h/Rando/ActorBehavior/EnGs.cpp | 9 +- mm/2s2h/Rando/ActorBehavior/EnItem00.cpp | 2 +- mm/2s2h/Rando/CheckTracker/CheckTracker.cpp | 3 + mm/2s2h/Rando/ConvertItem.cpp | 19 + mm/2s2h/Rando/DrawItem.cpp | 12 +- mm/2s2h/Rando/GiveItem.cpp | 3 + mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp | 22 +- mm/2s2h/Rando/StaticData/Items.cpp | 13 +- mm/2s2h/Rando/StaticData/StaticData.h | 2 +- mm/include/z64save.h | 5 +- mm/src/code/z_actor.c | 5 + mm/src/code/z_sram_NES.c | 1 + 51 files changed, 2625 insertions(+), 71 deletions(-) create mode 100644 mm/2s2h/Network/Anchor/Anchor.cpp create mode 100644 mm/2s2h/Network/Anchor/Anchor.h create mode 100644 mm/2s2h/Network/Anchor/AnchorRoomWindow.cpp create mode 100644 mm/2s2h/Network/Anchor/DummyPlayer.cpp create mode 100644 mm/2s2h/Network/Anchor/JsonConversions.hpp create mode 100644 mm/2s2h/Network/Anchor/Menu.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/AllClientState.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/DamagePlayer.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/DisableAnchor.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/GameComplete.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/GiveItem.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/Handshake.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/PlayerSfx.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/PlayerUpdate.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/RequestTeamState.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/RequestTeleport.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/ServerMessage.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/SetCheckStatus.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/SetFlag.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/TeleportTo.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/UnsetFlag.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/UpdateClientState.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/UpdateDungeonItems.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/UpdateRoomState.cpp create mode 100644 mm/2s2h/Network/Anchor/Packets/UpdateTeamState.cpp diff --git a/mm/2s2h/BenGui/BenGui.cpp b/mm/2s2h/BenGui/BenGui.cpp index f7c9d8d077..4a056da338 100644 --- a/mm/2s2h/BenGui/BenGui.cpp +++ b/mm/2s2h/BenGui/BenGui.cpp @@ -34,6 +34,7 @@ #include "DeveloperTools/EventLog.h" #include "DeveloperTools/DLViewer.h" #include "DeveloperTools/MessageViewer.h" +#include "2s2h/Network/Anchor/Anchor.h" namespace BenGui { // MARK: - Delegates @@ -67,6 +68,7 @@ std::shared_ptr mTimesplitsSettingsWindow; std::shared_ptr mInputViewer; std::shared_ptr mInputViewerSettings; std::shared_ptr mModalWindow; +std::shared_ptr mAnchorRoomWindow; UIWidgets::Colors GetMenuThemeColor() { return mBenMenu->GetMenuThemeColor(); @@ -184,6 +186,9 @@ void SetupGuiElements() { mModalWindow = std::make_shared("gWindows.ModalWindow", "Modal Window"); gui->AddGuiWindow(mModalWindow); mModalWindow->Show(); + + mAnchorRoomWindow = std::make_shared("gWindows.AnchorRoom", "Anchor Room"); + gui->AddGuiWindow(mAnchorRoomWindow); } void Destroy() { @@ -215,6 +220,7 @@ void Destroy() { mItemTrackerSettingsWindow = nullptr; mInputViewer = nullptr; mInputViewerSettings = nullptr; + mAnchorRoomWindow = nullptr; } void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, diff --git a/mm/2s2h/BenGui/Menu.cpp b/mm/2s2h/BenGui/Menu.cpp index 510888b378..79dd4839a4 100644 --- a/mm/2s2h/BenGui/Menu.cpp +++ b/mm/2s2h/BenGui/Menu.cpp @@ -602,7 +602,7 @@ void Menu::DrawElement() { for (auto& label : menuOrder) { ImVec2 size = ImGui::CalcTextSize(label.c_str()); headerSizes.push_back(size); - headerWidth += size.x + style.FramePadding.x * 2; + headerWidth += size.x + style.FramePadding.x * 2 + style.ItemSpacing.x; if (label == headerIndex) { headerWidth += style.ItemSpacing.x; } diff --git a/mm/2s2h/BenGui/UIWidgets.cpp b/mm/2s2h/BenGui/UIWidgets.cpp index 80c8dbdbe3..af7df2745f 100644 --- a/mm/2s2h/BenGui/UIWidgets.cpp +++ b/mm/2s2h/BenGui/UIWidgets.cpp @@ -883,15 +883,18 @@ bool InputString(const char* label, std::string* value, const InputOptions& opti ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::Red)); } float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; - if (options.alignment == ComponentAlignment::Left) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::Text(label, *value->c_str()); - } - } else if (options.alignment == ComponentAlignment::Right) { - if (options.labelPosition == LabelPosition::Above) { - ImGui::NewLine(); - ImGui::SameLine(width - ImGui::CalcTextSize(label).x); - ImGui::Text(label, *value->c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label, NULL, true); + if (labelSize.x != 0) { + if (options.alignment == ComponentAlignment::Left) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::Text(label, *value->c_str()); + } + } else if (options.alignment == ComponentAlignment::Right) { + if (options.labelPosition == LabelPosition::Above) { + ImGui::NewLine(); + ImGui::SameLine(width - ImGui::CalcTextSize(label).x); + ImGui::Text(label, *value->c_str()); + } } } ImGui::SetNextItemWidth(width); diff --git a/mm/2s2h/BenJsonConversions.hpp b/mm/2s2h/BenJsonConversions.hpp index 1ba1f3beea..017ef48573 100644 --- a/mm/2s2h/BenJsonConversions.hpp +++ b/mm/2s2h/BenJsonConversions.hpp @@ -1,6 +1,7 @@ #ifndef BenJsonConversions_hpp #define BenJsonConversions_hpp +#include #include #include #include "build.h" @@ -8,25 +9,26 @@ extern "C" { #include "z64save.h" #include "macros.h" +#include "z64actor.h" } using json = nlohmann::json; -void to_json(json& j, const DpadSaveInfo& dpadEquips) { +inline void to_json(json& j, const DpadSaveInfo& dpadEquips) { j = json{ { "dpadItems", dpadEquips.dpadItems }, { "dpadSlots", dpadEquips.dpadSlots }, }; } -void from_json(const json& j, DpadSaveInfo& dpadEquips) { +inline void from_json(const json& j, DpadSaveInfo& dpadEquips) { for (int i = 0; i < ARRAY_COUNT(dpadEquips.dpadItems); i++) { j.at("dpadItems").at(i).get_to(dpadEquips.dpadItems[i]); j.at("dpadSlots").at(i).get_to(dpadEquips.dpadSlots[i]); } } -void to_json(json& j, const RandoSaveCheck& randoSaveCheck) { +inline void to_json(json& j, const RandoSaveCheck& randoSaveCheck) { j = json{ { "randoItemId", randoSaveCheck.randoItemId }, { "eligible", randoSaveCheck.eligible }, @@ -35,10 +37,11 @@ void to_json(json& j, const RandoSaveCheck& randoSaveCheck) { { "shuffled", randoSaveCheck.shuffled }, { "skipped", randoSaveCheck.skipped }, { "price", randoSaveCheck.price }, + { "multiWorldTeamIndex", randoSaveCheck.multiWorldTeamIndex }, }; } -void from_json(const json& j, RandoSaveCheck& randoSaveCheck) { +inline void from_json(const json& j, RandoSaveCheck& randoSaveCheck) { j.at("randoItemId").get_to(randoSaveCheck.randoItemId); j.at("eligible").get_to(randoSaveCheck.eligible); j.at("cycleObtained").get_to(randoSaveCheck.cycleObtained); @@ -46,9 +49,10 @@ void from_json(const json& j, RandoSaveCheck& randoSaveCheck) { j.at("shuffled").get_to(randoSaveCheck.shuffled); j.at("skipped").get_to(randoSaveCheck.skipped); j.at("price").get_to(randoSaveCheck.price); + j.at("multiWorldTeamIndex").get_to(randoSaveCheck.multiWorldTeamIndex); } -void to_json(json& j, const RandoSaveInfo& rando) { +inline void to_json(json& j, const RandoSaveInfo& rando) { j = json{ { "randoInf", rando.randoInf }, { "randoEvents", rando.randoEvents }, @@ -61,7 +65,7 @@ void to_json(json& j, const RandoSaveInfo& rando) { }; } -void from_json(const json& j, RandoSaveInfo& rando) { +inline void from_json(const json& j, RandoSaveInfo& rando) { j.at("randoInf").get_to(rando.randoInf); j.at("randoEvents").get_to(rando.randoEvents); j.at("randoSaveChecks").get_to(rando.randoSaveChecks); @@ -76,7 +80,7 @@ void from_json(const json& j, RandoSaveInfo& rando) { j.at("foundTriforcePieces").get_to(rando.foundTriforcePieces); } -void to_json(json& j, const Vec3f& vec) { +inline void to_json(json& j, const Vec3f& vec) { j = json{ { "x", vec.x }, { "y", vec.y }, @@ -84,13 +88,13 @@ void to_json(json& j, const Vec3f& vec) { }; } -void from_json(const json& j, Vec3f& vec) { +inline void from_json(const json& j, Vec3f& vec) { j.at("x").get_to(vec.x); j.at("y").get_to(vec.y); j.at("z").get_to(vec.z); } -void to_json(json& j, const RespawnData& respawnData) { +inline void to_json(json& j, const RespawnData& respawnData) { j = json{ { "pos", respawnData.pos }, { "yaw", respawnData.yaw }, @@ -104,7 +108,7 @@ void to_json(json& j, const RespawnData& respawnData) { }; } -void from_json(const json& j, RespawnData& respawnData) { +inline void from_json(const json& j, RespawnData& respawnData) { j.at("pos").get_to(respawnData.pos); j.at("yaw").get_to(respawnData.yaw); j.at("playerParams").get_to(respawnData.playerParams); @@ -116,7 +120,7 @@ void from_json(const json& j, RespawnData& respawnData) { j.at("tempCollectFlags").get_to(respawnData.tempCollectFlags); } -void to_json(json& j, const ShipSaveInfo& shipSaveInfo) { +inline void to_json(json& j, const ShipSaveInfo& shipSaveInfo) { uint8_t commitHash[8]; memcpy(commitHash, shipSaveInfo.commitHash, sizeof(commitHash)); @@ -136,7 +140,7 @@ void to_json(json& j, const ShipSaveInfo& shipSaveInfo) { } } -void from_json(const json& j, ShipSaveInfo& shipSaveInfo) { +inline void from_json(const json& j, ShipSaveInfo& shipSaveInfo) { j.at("dpadEquips").get_to(shipSaveInfo.dpadEquips); j.at("pauseSaveEntrance").get_to(shipSaveInfo.pauseSaveEntrance); j.at("saveType").get_to(shipSaveInfo.saveType); @@ -156,7 +160,7 @@ void from_json(const json& j, ShipSaveInfo& shipSaveInfo) { } } -void to_json(json& j, const ItemEquips& itemEquips) { +inline void to_json(json& j, const ItemEquips& itemEquips) { j = json{ { "buttonItems", itemEquips.buttonItems }, { "cButtonSlots", itemEquips.cButtonSlots }, @@ -164,7 +168,7 @@ void to_json(json& j, const ItemEquips& itemEquips) { }; } -void from_json(const json& j, ItemEquips& itemEquips) { +inline void from_json(const json& j, ItemEquips& itemEquips) { j.at("equipment").get_to(itemEquips.equipment); // buttonItems and cButtonSlots are arrays of arrays, so we need to manually parse them for (int i = 0; i < ARRAY_COUNT(itemEquips.buttonItems); i++) { @@ -173,7 +177,7 @@ void from_json(const json& j, ItemEquips& itemEquips) { } } -void to_json(json& j, const Inventory& inventory) { +inline void to_json(json& j, const Inventory& inventory) { // Setup and copy u8 arrays to avoid json treating char[] as strings // These char[] are not null-terminated, so saving as strings causes overflow/corruption uint8_t dekuPlaygroundPlayerName[3][8]; @@ -192,7 +196,7 @@ void to_json(json& j, const Inventory& inventory) { }; } -void from_json(const json& j, Inventory& inventory) { +inline void from_json(const json& j, Inventory& inventory) { j.at("items").get_to(inventory.items); j.at("ammo").get_to(inventory.ammo); j.at("upgrades").get_to(inventory.upgrades); @@ -207,7 +211,7 @@ void from_json(const json& j, Inventory& inventory) { } } -void to_json(json& j, const PermanentSceneFlags& permanentSceneFlags) { +inline void to_json(json& j, const PermanentSceneFlags& permanentSceneFlags) { j = json{ { "chest", permanentSceneFlags.chest }, { "switch0", permanentSceneFlags.switch0 }, @@ -219,7 +223,7 @@ void to_json(json& j, const PermanentSceneFlags& permanentSceneFlags) { }; } -void from_json(const json& j, PermanentSceneFlags& permanentSceneFlags) { +inline void from_json(const json& j, PermanentSceneFlags& permanentSceneFlags) { j.at("chest").get_to(permanentSceneFlags.chest); j.at("switch0").get_to(permanentSceneFlags.switch0); j.at("switch1").get_to(permanentSceneFlags.switch1); @@ -229,7 +233,7 @@ void from_json(const json& j, PermanentSceneFlags& permanentSceneFlags) { j.at("rooms").get_to(permanentSceneFlags.rooms); } -void to_json(json& j, const SavePlayerData& savePlayerData) { +inline void to_json(json& j, const SavePlayerData& savePlayerData) { // Setup and copy u8 arrays to avoid json treating char[] as strings // These char[] are not null-terminated, so saving as strings causes overflow/corruption u8 newf[6]; @@ -259,7 +263,7 @@ void to_json(json& j, const SavePlayerData& savePlayerData) { }; } -void from_json(const json& j, SavePlayerData& savePlayerData) { +inline void from_json(const json& j, SavePlayerData& savePlayerData) { j.at("newf").get_to(savePlayerData.newf); j.at("threeDayResetCount").get_to(savePlayerData.threeDayResetCount); j.at("playerName").get_to(savePlayerData.playerName); @@ -280,7 +284,21 @@ void from_json(const json& j, SavePlayerData& savePlayerData) { j.at("savedSceneId").get_to(savePlayerData.savedSceneId); } -void to_json(json& j, const Vec3s& vec) { +inline void from_json(const json& j, Color_RGB8& color) { + j.at("r").get_to(color.r); + j.at("g").get_to(color.g); + j.at("b").get_to(color.b); +} + +inline void to_json(json& j, const Color_RGB8& color) { + j = json{ + {"r", color.r}, + {"g", color.g}, + {"b", color.b} + }; +} + +inline void to_json(json& j, const Vec3s& vec) { j = json{ { "x", vec.x }, { "y", vec.y }, @@ -288,13 +306,25 @@ void to_json(json& j, const Vec3s& vec) { }; } -void from_json(const json& j, Vec3s& vec) { +inline void from_json(const json& j, Vec3s& vec) { j.at("x").get_to(vec.x); j.at("y").get_to(vec.y); j.at("z").get_to(vec.z); } -void to_json(json& j, const HorseData& horseData) { +inline void to_json(json& j, const PosRot& posRot) { + j = json{ + {"pos", posRot.pos}, + {"rot", posRot.rot} + }; +} + +inline void from_json(const json& j, PosRot& posRot) { + j.at("pos").get_to(posRot.pos); + j.at("rot").get_to(posRot.rot); +} + +inline void to_json(json& j, const HorseData& horseData) { j = json{ { "sceneId", horseData.sceneId }, { "pos", horseData.pos }, @@ -302,13 +332,13 @@ void to_json(json& j, const HorseData& horseData) { }; } -void from_json(const json& j, HorseData& horseData) { +inline void from_json(const json& j, HorseData& horseData) { j.at("sceneId").get_to(horseData.sceneId); j.at("pos").get_to(horseData.pos); j.at("yaw").get_to(horseData.yaw); } -void to_json(json& j, const SaveInfo& saveInfo) { +inline void to_json(json& j, const SaveInfo& saveInfo) { j = json{ { "playerData", saveInfo.playerData }, { "equips", saveInfo.equips }, @@ -345,7 +375,7 @@ void to_json(json& j, const SaveInfo& saveInfo) { }; } -void from_json(const json& j, SaveInfo& saveInfo) { +inline void from_json(const json& j, SaveInfo& saveInfo) { j.at("playerData").get_to(saveInfo.playerData); j.at("equips").get_to(saveInfo.equips); j.at("inventory").get_to(saveInfo.inventory); @@ -383,7 +413,7 @@ void from_json(const json& j, SaveInfo& saveInfo) { j.at("checksum").get_to(saveInfo.checksum); } -void to_json(json& j, const Save& save) { +inline void to_json(json& j, const Save& save) { j = json{ { "entrance", save.entrance }, { "equippedMask", save.equippedMask }, @@ -406,7 +436,7 @@ void to_json(json& j, const Save& save) { }; } -void from_json(const json& j, Save& save) { +inline void from_json(const json& j, Save& save) { j.at("entrance").get_to(save.entrance); j.at("equippedMask").get_to(save.equippedMask); j.at("isFirstCycle").get_to(save.isFirstCycle); @@ -427,7 +457,7 @@ void from_json(const json& j, Save& save) { j.at("shipSaveInfo").get_to(save.shipSaveInfo); } -void to_json(json& j, const SaveContext& saveContext) { +inline void to_json(json& j, const SaveContext& saveContext) { j = json{ { "save", saveContext.save }, { "eventInf", saveContext.eventInf }, @@ -444,7 +474,7 @@ void to_json(json& j, const SaveContext& saveContext) { }; } -void from_json(const json& j, SaveContext& saveContext) { +inline void from_json(const json& j, SaveContext& saveContext) { j.at("save").get_to(saveContext.save); j.at("eventInf").get_to(saveContext.eventInf); j.at("unk_1014").get_to(saveContext.unk_1014); @@ -459,7 +489,7 @@ void from_json(const json& j, SaveContext& saveContext) { j.at("pictoPhotoI5").get_to(saveContext.pictoPhotoI5); } -void to_json(json& j, const SaveOptions& saveOptions) { +inline void to_json(json& j, const SaveOptions& saveOptions) { j = json{ { "optionId", saveOptions.optionId }, { "language", saveOptions.language }, @@ -469,7 +499,7 @@ void to_json(json& j, const SaveOptions& saveOptions) { }; } -void from_json(const json& j, SaveOptions& saveOptions) { +inline void from_json(const json& j, SaveOptions& saveOptions) { j.at("optionId").get_to(saveOptions.optionId); j.at("language").get_to(saveOptions.language); j.at("audioSetting").get_to(saveOptions.audioSetting); diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index f392bb4d2a..a00ae125ac 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -41,6 +41,7 @@ #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" #include "2s2h/Network/Sail/Sail.h" +#include "2s2h/Network/Anchor/Anchor.h" #include #include @@ -111,6 +112,7 @@ OTRGlobals* OTRGlobals::Instance; GameInteractor* GameInteractor::Instance; AudioCollection* AudioCollection::Instance; Sail* Sail::Instance; +Anchor* Anchor::Instance; extern "C" char** cameraStrings; bool prevAltAssets = false; @@ -703,6 +705,8 @@ extern "C" void InitOTR() { GameInteractor::Instance = new GameInteractor(); AudioCollection::Instance = new AudioCollection(); Sail::Instance = new Sail(); + Anchor::Instance = new Anchor(); + LoadGuiTextures(); BenGui::SetupGuiElements(); ShipInit::InitAll(); @@ -738,6 +742,9 @@ extern "C" void InitOTR() { if (CVarGetInteger("gNetwork.Sail.Enabled", 0)) { Sail::Instance->Enable(); } + if (CVarGetInteger("gNetwork.Anchor.Enabled", 0)) { + Anchor::Instance->Enable(); + } std::shared_ptr conf = OTRGlobals::Instance->context->GetConfig(); Ship::Context::GetInstance()->GetFileDropMgr()->RegisterDropHandler(BinarySaveConverter_HandleFileDropped); @@ -754,6 +761,9 @@ extern "C" void DeinitOTR() { if (CVarGetInteger("gNetwork.Sail.Enabled", 0)) { Sail::Instance->Disable(); } + if (CVarGetInteger("gNetwork.Anchor.Enabled", 0)) { + Anchor::Instance->Disable(); + } #ifdef ENABLE_NETWORKING SDLNet_Quit(); #endif diff --git a/mm/2s2h/DeveloperTools/SaveEditor.cpp b/mm/2s2h/DeveloperTools/SaveEditor.cpp index 8fbdcddca0..c68a7c2a97 100644 --- a/mm/2s2h/DeveloperTools/SaveEditor.cpp +++ b/mm/2s2h/DeveloperTools/SaveEditor.cpp @@ -7,6 +7,7 @@ #include "2s2h/BenGui/Notification.h" #include "2s2h/Rando/Spoiler/Spoiler.h" #include "2s2h/ShipUtils.h" +#include "2s2h/Network/Anchor/Anchor.h" #include "interface/icon_item_dungeon_static/icon_item_dungeon_static.h" #include "archives/icon_item_24_static/icon_item_24_static_yar.h" @@ -2214,13 +2215,15 @@ void DrawRandoTab() { ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.2f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 0.1f)); - ImGui::BeginTable("Check List", 5); + ImGui::BeginTable("Check List", 6); ImGui::TableSetupColumn("Shuffled", ImGuiTableColumnFlags_NoHeaderLabel | ImGuiTableColumnFlags_WidthFixed, 30.0f); ImGui::TableSetupColumn("Eligible", ImGuiTableColumnFlags_NoHeaderLabel | ImGuiTableColumnFlags_WidthFixed, 30.0f); + ImGui::TableSetupColumn("CycleObtained", ImGuiTableColumnFlags_NoHeaderLabel | ImGuiTableColumnFlags_WidthFixed, + 30.0f); ImGui::TableSetupColumn("Obtained", ImGuiTableColumnFlags_NoHeaderLabel | ImGuiTableColumnFlags_WidthFixed, 30.0f); ImGui::TableSetupColumn("Check Name"); ImGui::TableSetupColumn("Reward"); - ImGui::TableSetupScrollFreeze(5, 1); + ImGui::TableSetupScrollFreeze(6, 1); ImGui::TableHeadersRow(); for (auto& [_, randoStaticCheck] : Rando::StaticData::Checks) { @@ -2243,6 +2246,9 @@ void DrawRandoTab() { UIWidgets::Checkbox((hiddenName + "eligible").c_str(), &randoSaveCheck.eligible); UIWidgets::Tooltip("Eligible"); ImGui::TableNextColumn(); + UIWidgets::Checkbox((hiddenName + "cycleObtained").c_str(), &randoSaveCheck.cycleObtained); + UIWidgets::Tooltip("CycleObtained"); + ImGui::TableNextColumn(); UIWidgets::Checkbox((hiddenName + "obtained").c_str(), &randoSaveCheck.obtained); UIWidgets::Tooltip("Obtained"); ImGui::TableNextColumn(); @@ -2252,6 +2258,10 @@ void DrawRandoTab() { ImGui::TableNextColumn(); UIWidgets::ComboboxWithSearch((hiddenName + "reward").c_str(), &randoSaveCheck.randoItemId, &randoItemIdComboboxMap, { .labelPosition = UIWidgets::LabelPosition::None }); + if (Anchor::Instance->roomState.teams.size() > 1) { + ImGui::SameLine(); + ImGui::Text("%s", Anchor::Instance->roomState.teams[randoSaveCheck.multiWorldTeamIndex].c_str()); + } } ImGui::EndTable(); diff --git a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp index 01d9e07da2..b68ac0314e 100644 --- a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp +++ b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp @@ -2,6 +2,7 @@ #include "BenPort.h" #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/ShipInit.hpp" +#include "2s2h/Network/Anchor/Anchor.h" extern "C" { #include @@ -252,6 +253,8 @@ void RegisterSavingEnhancements() { if (gSaveContext.save.shipSaveInfo.fileCompletedAt == 0) { SavingEnhancements_AdvancePlaytime(); gSaveContext.save.shipSaveInfo.fileCompletedAt = GetUnixTimestamp(); + Anchor::Instance->SendPacket_GameComplete(); + Anchor::Instance->ReleaseWorldForCurrentTeam(); } }); diff --git a/mm/2s2h/GameInteractor/GameInteractor.cpp b/mm/2s2h/GameInteractor/GameInteractor.cpp index 7acfa594be..79c9658fd5 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.cpp +++ b/mm/2s2h/GameInteractor/GameInteractor.cpp @@ -66,6 +66,10 @@ void GameInteractor_ExecuteAfterEndOfCycleSave() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteAfterOwlSave() { + GameInteractor::Instance->ExecuteHooks(); +} + void GameInteractor_ExecuteBeforeMoonCrashSaveReset() { GameInteractor::Instance->ExecuteHooks(); } @@ -93,6 +97,10 @@ void GameInteractor_ExecuteOnSceneInit(s16 sceneId, s8 spawnNum) { GameInteractor::Instance->ExecuteHooksForFilter(sceneId, spawnNum); } +void GameInteractor_ExecuteOnSceneSpawnActors() { + GameInteractor::Instance->ExecuteHooks(); +} + void GameInteractor_ExecuteOnRoomInit(s16 sceneId, s8 roomNum) { SPDLOG_DEBUG("OnRoomInit: sceneId: {}, roomNum: {}", sceneId, roomNum); GameInteractor::Instance->ExecuteHooks(sceneId, roomNum); @@ -191,6 +199,10 @@ void GameInteractor_ExecuteOnBossDefeated(s16 actorId) { GameInteractor::Instance->ExecuteHooksForFilter(actorId); } +void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId) { + GameInteractor::Instance->ExecuteHooks(sfxId); +} + void GameInteractor_ExecuteOnSceneFlagSet(s16 sceneId, FlagType flagType, u32 flag) { SPDLOG_DEBUG("OnSceneFlagSet: sceneId: {}, flagType: {}, flag: {}", sceneId, (u32)flagType, flag); GameInteractor::Instance->ExecuteHooks(sceneId, flagType, flag); diff --git a/mm/2s2h/GameInteractor/GameInteractor.h b/mm/2s2h/GameInteractor/GameInteractor.h index 8585c28545..0a752fc359 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.h +++ b/mm/2s2h/GameInteractor/GameInteractor.h @@ -496,6 +496,7 @@ void GameInteractor_ExecuteOnSaveLoad(s16 fileNum); void GameInteractor_ExecuteOnFileSelectSaveLoad(s16 fileNum, bool isOwlSave, SaveContext* saveContext); void GameInteractor_ExecuteBeforeEndOfCycleSave(); void GameInteractor_ExecuteAfterEndOfCycleSave(); +void GameInteractor_ExecuteAfterOwlSave(); void GameInteractor_ExecuteBeforeMoonCrashSaveReset(); void GameInteractor_ExecuteOnInterfaceDrawStart(); void GameInteractor_ExecuteAfterInterfaceClockDraw(); @@ -503,6 +504,7 @@ void GameInteractor_ExecuteBeforeInterfaceClockDraw(); void GameInteractor_ExecuteOnGameCompletion(); void GameInteractor_ExecuteOnSceneInit(s16 sceneId, s8 spawnNum); +void GameInteractor_ExecuteOnSceneSpawnActors(); void GameInteractor_ExecuteOnRoomInit(s16 sceneId, s8 roomNum); void GameInteractor_ExecuteAfterRoomSceneCommands(s16 sceneId, s8 roomNum); void GameInteractor_ExecuteOnPlayDrawWorldEnd(); @@ -518,6 +520,7 @@ void GameInteractor_ExecuteOnActorKill(Actor* actor); void GameInteractor_ExecuteOnActorDestroy(Actor* actor); void GameInteractor_ExecuteOnPlayerPostLimbDraw(Player* player, s32 limbIndex); void GameInteractor_ExecuteOnBossDefeated(s16 actorId); +void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId); void GameInteractor_ExecuteOnSceneFlagSet(s16 sceneId, FlagType flagType, u32 flag); void GameInteractor_ExecuteOnSceneFlagUnset(s16 sceneId, FlagType flagType, u32 flag); diff --git a/mm/2s2h/GameInteractor/GameInteractor_HookTable.h b/mm/2s2h/GameInteractor/GameInteractor_HookTable.h index 3034291c56..e881d11100 100644 --- a/mm/2s2h/GameInteractor/GameInteractor_HookTable.h +++ b/mm/2s2h/GameInteractor/GameInteractor_HookTable.h @@ -11,6 +11,7 @@ DEFINE_HOOK(OnSaveLoad, (s16 fileNum)) DEFINE_HOOK(OnFileSelectSaveLoad, (s16 fileNum, bool isOwlSave, SaveContext* saveContext)) DEFINE_HOOK(BeforeEndOfCycleSave, ()) DEFINE_HOOK(AfterEndOfCycleSave, ()) +DEFINE_HOOK(AfterOwlSave, ()) DEFINE_HOOK(BeforeMoonCrashSaveReset, ()) DEFINE_HOOK(OnInterfaceDrawStart, ()) DEFINE_HOOK(AfterInterfaceClockDraw, ()) @@ -18,6 +19,7 @@ DEFINE_HOOK(BeforeInterfaceClockDraw, ()) DEFINE_HOOK(OnGameCompletion, ()) DEFINE_HOOK(OnSceneInit, (s8 sceneId, s8 spawnNum)) +DEFINE_HOOK(OnSceneSpawnActors, ()) DEFINE_HOOK(OnRoomInit, (s8 sceneId, s8 roomNum)) DEFINE_HOOK(AfterRoomSceneCommands, (s8 sceneId, s8 roomNum)) DEFINE_HOOK(OnPlayDrawWorldEnd, ()) @@ -33,6 +35,7 @@ DEFINE_HOOK(OnActorKill, (Actor * actor)) DEFINE_HOOK(OnActorDestroy, (Actor * actor)) DEFINE_HOOK(OnPlayerPostLimbDraw, (Player * player, s32 limbIndex)) DEFINE_HOOK(OnBossDefeated, (s16 actorId)) +DEFINE_HOOK(OnPlayerSfx, (u16 sfxId)) DEFINE_HOOK(OnSceneFlagSet, (s16 sceneId, FlagType flagType, u32 flag)) DEFINE_HOOK(OnSceneFlagUnset, (s16 sceneId, FlagType flagType, u32 flag)) diff --git a/mm/2s2h/Network/Anchor/Anchor.cpp b/mm/2s2h/Network/Anchor/Anchor.cpp new file mode 100644 index 0000000000..b491646101 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Anchor.cpp @@ -0,0 +1,420 @@ +#include "Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenPort.h" +#include "2s2h/BenGui/UIWidgets.hpp" +#include "2s2h/NameTag/NameTag.h" +#include "2s2h/ShipUtils.h" +#include "2s2h/Rando/Spoiler/Spoiler.h" +#include "2s2h/Rando/MiscBehavior/MiscBehavior.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; +} + +// MARK: - Overrides + +void Anchor::Enable() { + Network::Enable(CVarGetString("gNetwork.Anchor.Host", "anchor.proxysaw.dev"), + CVarGetInteger("gNetwork.Anchor.Port", 43383)); + ownClientId = CVarGetInteger("gNetwork.Anchor.LastClientId", 0); + roomState.ownerClientId = 0; +} + +void Anchor::Disable() { + Network::Disable(); + + clients.clear(); + roomState.teams.clear(); + RefreshClientActors(); +} + +void Anchor::OnConnected() { + SendPacket_Handshake(); + RegisterHooks(); + + if (IsSaveLoaded()) { + SendPacket_RequestTeamState(); + } +} + +void Anchor::OnDisconnected() { + RegisterHooks(); +} + +void Anchor::ProcessOutgoingPackets() { + // Copy all queued packets while holding the lock, then send them after releasing + std::queue packetsToSend; + { + std::lock_guard lock(outgoingPacketQueueMutex); + packetsToSend.swap(outgoingPacketQueue); + } + + // Send packets without holding the lock + while (!packetsToSend.empty()) { + nlohmann::json payload = packetsToSend.front(); + packetsToSend.pop(); + + if (!payload.contains("quiet")) { + SPDLOG_DEBUG("[Anchor] Sending payload:\n{}", payload.dump()); + } + Network::SendJsonToRemote(payload); + } +} + +void Anchor::SendJsonToRemote(nlohmann::json payload) { + if (!isConnected) { + return; + } + + payload["clientId"] = ownClientId; + if (!payload.contains("quiet")) { + SPDLOG_DEBUG("[Anchor] Sending payload:\n{}", payload.dump()); + } + + if (payload["type"] == HANDSHAKE) { + Network::SendJsonToRemote(payload); + return; + } + + // Queue the packet to be sent on the network thread + std::lock_guard lock(outgoingPacketQueueMutex); + outgoingPacketQueue.push(payload); +} + +void Anchor::OnIncomingJson(nlohmann::json payload) { + // If it doesn't contain a type, it's not a valid payload + if (!payload.contains("type")) { + return; + } + + // If it's not a quiet payload, log it + if (!payload.contains("quiet")) { + SPDLOG_DEBUG("[Anchor] Received payload:\n{}", payload.dump()); + } + + std::string packetType = payload["type"].get(); + + // Ignore packets from mismatched clients, except for ALL_CLIENT_STATE or UPDATE_CLIENT_STATE + if (packetType != ALL_CLIENT_STATE && packetType != UPDATE_CLIENT_STATE) { + if (payload.contains("clientId")) { + uint32_t clientId = payload["clientId"].get(); + if (clients.contains(clientId) && clients[clientId].clientVersion != clientVersion) { + return; + } + } + } + + // Handle PLAYER_UPDATE packets immediately, no need to queue + if (packetType == PLAYER_UPDATE) { + HandlePacket_PlayerUpdate(payload); + return; + } + + // Queue all packets to be processed on the game thread + std::lock_guard lock(incomingPacketQueueMutex); + incomingPacketQueue.push(payload); +} + +void Anchor::ProcessIncomingPacketQueue() { + if (packetsQueuedFromTeamState) { + if (IsSaveLoaded()) { + packetsQueuedFromTeamState = false; + } else { + return; + } + } + + // Copy all queued packets while holding the lock, then process them after releasing + std::queue packetsToProcess; + { + std::lock_guard lock(incomingPacketQueueMutex); + packetsToProcess.swap(incomingPacketQueue); + } + + while (!packetsToProcess.empty()) { + if (packetsQueuedFromTeamState) + break; + + nlohmann::json payload = packetsToProcess.front(); + packetsToProcess.pop(); + + std::string packetType = payload["type"].get(); + + isProcessingIncomingPacket = true; + + // packetType here is a string so we can't use a switch statement + if (packetType == ALL_CLIENT_STATE) + HandlePacket_AllClientState(payload); + else if (packetType == DAMAGE_PLAYER) + HandlePacket_DamagePlayer(payload); + else if (packetType == DISABLE_ANCHOR) + HandlePacket_DisableAnchor(payload); + else if (packetType == GAME_COMPLETE) + HandlePacket_GameComplete(payload); + else if (packetType == GIVE_ITEM) + HandlePacket_GiveItem(payload); + else if (packetType == PLAYER_SFX) + HandlePacket_PlayerSfx(payload); + else if (packetType == PLAYER_UPDATE) + HandlePacket_PlayerUpdate(payload); + else if (packetType == UPDATE_TEAM_STATE) + HandlePacket_UpdateTeamState(payload); + else if (packetType == REQUEST_TEAM_STATE) + HandlePacket_RequestTeamState(payload); + else if (packetType == REQUEST_TELEPORT) + HandlePacket_RequestTeleport(payload); + else if (packetType == SERVER_MESSAGE) + HandlePacket_ServerMessage(payload); + else if (packetType == SET_CHECK_STATUS) + HandlePacket_SetCheckStatus(payload); + else if (packetType == SET_FLAG) + HandlePacket_SetFlag(payload); + else if (packetType == TELEPORT_TO) + HandlePacket_TeleportTo(payload); + else if (packetType == UNSET_FLAG) + HandlePacket_UnsetFlag(payload); + else if (packetType == UPDATE_CLIENT_STATE) + HandlePacket_UpdateClientState(payload); + else if (packetType == UPDATE_ROOM_STATE) + HandlePacket_UpdateRoomState(payload); + else if (packetType == UPDATE_DUNGEON_ITEMS) + HandlePacket_UpdateDungeonItems(payload); + + isProcessingIncomingPacket = false; + } +} + +static bool justReset = false; +static bool sGainedControl = false; + +void Anchor::RegisterHooks() { + COND_HOOK(OnSceneSpawnActors, isConnected, [&]() { + SendPacket_UpdateClientState(); + justReset = false; + + if (IsSaveLoaded()) { + RefreshClientActors(); + } + }); + + // HOOK(OnPresentFileSelect, isConnected, [&]() { + // SendPacket_UpdateClientState(); + // }); + + COND_ID_HOOK(ShouldActorInit, ACTOR_PLAYER, isConnected, [&](void* actorRef, bool* should) { + Actor* actor = (Actor*)actorRef; + + if (refreshingActors) { + // By the time we get here, the actor was already added to the ACTORCAT_PLAYER list, so we need to move it + Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, actor, ACTORCAT_NPC); + actor->id = ACTOR_ITEM_INBOX; + actor->category = ACTORCAT_NPC; + actor->init = DummyPlayer_Init; + actor->update = DummyPlayer_Update; + actor->draw = DummyPlayer_Draw; + actor->destroy = DummyPlayer_Destroy; + } + }); + + COND_ID_HOOK(OnActorUpdate, ACTOR_PLAYER, isConnected, [&](Actor* actor) { + sGainedControl = true; + SendPacket_PlayerUpdate(); + + if (shouldRefreshActors) { + shouldRefreshActors = false; + RefreshClientActors(); + } + }); + + COND_HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) { SendPacket_PlayerSfx(sfxId); }); + + COND_HOOK(OnSaveLoad, isConnected, [&](s16 fileNum) { + sGainedControl = false; + SendPacket_RequestTeamState(); + }); + + COND_HOOK(OnConsoleLogoUpdate, isConnected, [&]() { + if (!justReset) { + SendPacket_UpdateClientState(); + justReset = true; + } + }); + + COND_HOOK(AfterOwlSave, isConnected, [&]() { + if (gPlayState != NULL) { + SendPacket_UpdateTeamState(CVarGetString("gNetwork.Anchor.TeamId", "default")); + } + }); + + COND_HOOK(AfterEndOfCycleSave, isConnected, [&]() { + if (gPlayState != NULL) { + SendPacket_UpdateTeamState(CVarGetString("gNetwork.Anchor.TeamId", "default")); + } + }); + + COND_HOOK(OnGameStateUpdate, isConnected, [&]() { ProcessIncomingPacketQueue(); }); + + // COND_HOOK(OnFlagSet, isConnected, [&](s16 flagType, s16 flag) { SendPacket_SetFlag(SCENE_MAX, flagType, flag); + // }); + + // COND_HOOK(OnFlagUnset, isConnected, + // [&](s16 flagType, s16 flag) { SendPacket_UnsetFlag(SCENE_MAX, flagType, flag); }); + + // COND_HOOK(OnSceneFlagSet, isConnected, + // [&](s16 sceneId, s16 flagType, s16 flag) { SendPacket_SetFlag(sceneId, flagType, flag); }); + + // COND_HOOK(OnSceneFlagUnset, isConnected, + // [&](s16 sceneId, s16 flagType, s16 flag) { SendPacket_UnsetFlag(sceneId, flagType, flag); }); +} + +// MARK: - Misc/Helpers + +// Kills all existing anchor actors and respawns them with the new client data +void Anchor::RefreshClientActors() { + if (!IsSaveLoaded()) { + return; + } + + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_NPC].first; + + while (actor != NULL) { + if (actor->id == ACTOR_ITEM_INBOX && actor->update == DummyPlayer_Update) { + NameTag_RemoveAllForActor(actor); + Actor_Kill(actor); + } + actor = actor->next; + } + + actorIndexToClientId.clear(); + refreshingActors = true; + for (auto& [clientId, client] : clients) { + if (!client.online || client.self) { + continue; + } + + actorIndexToClientId.push_back(clientId); + // We are using a hook `ShouldActorInit` to override the init/update/draw/destroy functions of the Player we + // spawn We quickly store a mapping of "index" to clientId, then within the init function we use this to get the + // clientId and store it on player->zTargetActiveTimer (unused s32 for the dummy) for convenience + auto dummy = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_PLAYER, client.posRot.pos.x, + client.posRot.pos.y, client.posRot.pos.z, client.posRot.rot.x, client.posRot.rot.y, + client.posRot.rot.z, actorIndexToClientId.size() - 1); + client.player = (Player*)dummy; + } + refreshingActors = false; +} + +bool Anchor::IsSaveLoaded() { + if (gPlayState == nullptr) { + return false; + } + + if (GET_PLAYER(gPlayState) == nullptr) { + return false; + } + + if (gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) { + return false; + } + + if (gSaveContext.gameMode != GAMEMODE_NORMAL) { + return false; + } + + // Not in daytelop + if (!sGainedControl) { + return false; + } + + return true; +} + +void Anchor::InitializeMultiWorld(std::set& teams) { + roomState.teams = std::vector(teams.begin(), teams.end()); + teams.clear(); + SendPacket_UpdateRoomState(); + + std::string previousSpoiler = CVarGetString("gRando.SpoilerFile", ""); + int previousSpoilerFileIndex = CVarGetInteger("gRando.SpoilerFileIndex", 0); + CVarSetInteger("gRando.SpoilerFileIndex", -1); + + Ship_Random_Seed(Ship_Random(0, 1000000)); + + std::vector worlds; + std::vector> checkPool; + std::vector> itemPool; + + for (int i = 0; i < roomState.teams.size(); i++) { + CVarSetString("gRando.SpoilerFile", (roomState.teams[i] + ".json").c_str()); + Sram_InitNewSave(); + Rando::MiscBehavior::OnFileCreate(0); + + for (auto& [randoCheckId, randoStaticCheck] : Rando::StaticData::Checks) { + if (RANDO_SAVE_CHECKS[randoCheckId].shuffled && !RANDO_SAVE_CHECKS[randoCheckId].skipped) { + checkPool.push_back({ randoCheckId, i }); + itemPool.push_back({ RANDO_SAVE_CHECKS[randoCheckId].randoItemId, i }); + } + } + + SaveContext save = gSaveContext; + worlds.push_back(save); + } + + for (size_t i = 0; i < itemPool.size(); i++) { + std::swap(itemPool[i], itemPool[Ship_Random(0, itemPool.size() - 1)]); + } + + for (int i = 0; i < roomState.teams.size(); i++) { + gSaveContext = worlds[i]; + for (size_t j = 0; j < checkPool.size(); j++) { + if (checkPool[j].second != i) { + continue; + } + RANDO_SAVE_CHECKS[checkPool[j].first].randoItemId = itemPool[j].first; + RANDO_SAVE_CHECKS[checkPool[j].first].multiWorldTeamIndex = itemPool[j].second; + } + + SendPacket_UpdateTeamState(roomState.teams[i]); + } + + CVarSetString("gRando.SpoilerFile", previousSpoiler.c_str()); + CVarSetInteger("gRando.SpoilerFileIndex", previousSpoilerFileIndex); +} + +void Anchor::ReleaseWorldForCurrentTeam() { + if (!IsSaveLoaded() || roomState.teams.size() < 2) { + return; + } + + std::string currentTeamId = CVarGetString("gNetwork.Anchor.TeamId", "default"); + s16 currentTeamIndex = -1; + for (size_t i = 0; i < roomState.teams.size(); i++) { + if (roomState.teams[i] == currentTeamId) { + currentTeamIndex = i; + break; + ; + } + } + // Not on a valid team + if (currentTeamIndex == -1) { + return; + } + + for (int i = 0; i < RC_MAX; i++) { + auto& randoSaveCheck = RANDO_SAVE_CHECKS[i]; + + if (!randoSaveCheck.obtained && randoSaveCheck.multiWorldTeamIndex != currentTeamIndex) { + RandoItemId randoItemId = Rando::ConvertItem(RANDO_SAVE_CHECKS[i].randoItemId, (RandoCheckId)i); + randoSaveCheck.cycleObtained = true; + randoSaveCheck.obtained = true; + randoSaveCheck.eligible = false; + Anchor::Instance->SendPacket_SetCheckStatus((RandoCheckId)i); + Anchor::Instance->SendPacket_GiveItem( + 1, randoItemId, Anchor::Instance->roomState.teams[randoSaveCheck.multiWorldTeamIndex]); + } + } +} diff --git a/mm/2s2h/Network/Anchor/Anchor.h b/mm/2s2h/Network/Anchor/Anchor.h new file mode 100644 index 0000000000..8edd10353b --- /dev/null +++ b/mm/2s2h/Network/Anchor/Anchor.h @@ -0,0 +1,187 @@ +#ifndef NETWORK_ANCHOR_H +#define NETWORK_ANCHOR_H +#ifdef __cplusplus + +#include "2s2h/Network/Network.h" +#include +#include "build.h" +#include "2s2h/Rando/Rando.h" + +extern "C" { +#include "variables.h" +#include "z64.h" +} + +void DummyPlayer_Init(Actor* actor, PlayState* play); +void DummyPlayer_Update(Actor* actor, PlayState* play); +void DummyPlayer_Draw(Actor* actor, PlayState* play); +void DummyPlayer_Destroy(Actor* actor, PlayState* play); + +typedef struct { + uint32_t clientId; + std::string name; + Color_RGB8 color; + std::string clientVersion; + std::string teamId; + bool online; + bool self; + uint32_t seed; + bool isSaveLoaded; + bool isGameComplete; + s16 sceneId; + s32 entrance; + + // Only available in PLAYER_UPDATE packets + uint8_t transformation; + PosRot posRot; + u8 jointTable[159]; + u8 upperJointTable[159]; + uint8_t currentMask; + uint8_t rightHandType; + uint8_t leftHandType; + int8_t currentShield; + uint8_t sheathType; + int8_t heldItemAction; + uint8_t heldItemId; + int8_t itemAction; + uint32_t stateFlags1; + uint32_t stateFlags2; + uint32_t stateFlags3; + float unk_B0C; + int16_t unk_B28; + int16_t unk_ACC; + int8_t invincibilityTimer; + + // Ptr to the dummy player + Player* player; +} AnchorClient; + +typedef struct { + uint32_t ownerClientId; + u8 pvpMode; // 0 = off, 1 = on, 2 = on with friendly fire + u8 showLocationsMode; // 0 = none, 1 = team, 2 = all + u8 teleportMode; // 0 = off, 1 = team, 2 = all + u8 syncItemsAndFlags; // 0 = off, 1 = on + std::vector teams; // List of teams in multiworld, used for determining multiWorldTeamIndex per check +} RoomState; + +class Anchor : public Network { + private: + bool refreshingActors = false; + bool shouldRefreshActors = false; + bool justLoadedSave = false; + bool packetsQueuedFromTeamState = false; + bool isProcessingIncomingPacket = false; + std::queue incomingPacketQueue; + std::mutex incomingPacketQueueMutex; + std::queue outgoingPacketQueue; + std::mutex outgoingPacketQueueMutex; + + nlohmann::json PrepClientState(); + nlohmann::json PrepRoomState(); + void RegisterHooks(); + void RefreshClientActors(); + void HandlePacket_AllClientState(nlohmann::json payload); + void HandlePacket_DamagePlayer(nlohmann::json payload); + void HandlePacket_DisableAnchor(nlohmann::json payload); + void HandlePacket_GameComplete(nlohmann::json payload); + void HandlePacket_GiveItem(nlohmann::json payload); + void HandlePacket_PlayerSfx(nlohmann::json payload); + void HandlePacket_PlayerUpdate(nlohmann::json payload); + void HandlePacket_RequestTeamState(nlohmann::json payload); + void HandlePacket_RequestTeleport(nlohmann::json payload); + void HandlePacket_ServerMessage(nlohmann::json payload); + void HandlePacket_SetCheckStatus(nlohmann::json payload); + void HandlePacket_SetFlag(nlohmann::json payload); + void HandlePacket_TeleportTo(nlohmann::json payload); + void HandlePacket_UnsetFlag(nlohmann::json payload); + void HandlePacket_UpdateClientState(nlohmann::json payload); + void HandlePacket_UpdateDungeonItems(nlohmann::json payload); + void HandlePacket_UpdateRoomState(nlohmann::json payload); + void HandlePacket_UpdateTeamState(nlohmann::json payload); + + public: + uint32_t ownClientId; + inline static const std::string clientVersion = (char*)gBuildVersion; + + // Packet types // + inline static const std::string ALL_CLIENT_STATE = "ALL_CLIENT_STATE"; + inline static const std::string DAMAGE_PLAYER = "DAMAGE_PLAYER"; + inline static const std::string DISABLE_ANCHOR = "DISABLE_ANCHOR"; + inline static const std::string GAME_COMPLETE = "GAME_COMPLETE"; + inline static const std::string GIVE_ITEM = "GIVE_ITEM"; + inline static const std::string HANDSHAKE = "HANDSHAKE"; + inline static const std::string PLAYER_SFX = "PLAYER_SFX"; + inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE"; + inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE"; + inline static const std::string REQUEST_TELEPORT = "REQUEST_TELEPORT"; + inline static const std::string SERVER_MESSAGE = "SERVER_MESSAGE"; + inline static const std::string SET_CHECK_STATUS = "SET_CHECK_STATUS"; + inline static const std::string SET_FLAG = "SET_FLAG"; + inline static const std::string TELEPORT_TO = "TELEPORT_TO"; + inline static const std::string UNSET_FLAG = "UNSET_FLAG"; + inline static const std::string UPDATE_CLIENT_STATE = "UPDATE_CLIENT_STATE"; + inline static const std::string UPDATE_DUNGEON_ITEMS = "UPDATE_DUNGEON_ITEMS"; + inline static const std::string UPDATE_ROOM_STATE = "UPDATE_ROOM_STATE"; + inline static const std::string UPDATE_TEAM_STATE = "UPDATE_TEAM_STATE"; + + static Anchor* Instance; + std::map clients; + std::vector actorIndexToClientId; + RoomState roomState; + + void Enable(); + void Disable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void ProcessOutgoingPackets(); + void ProcessIncomingPacketQueue(); + void SendJsonToRemote(nlohmann::json packet); + bool IsSaveLoaded(); + bool CanTeleportTo(uint32_t clientId); + void InitializeMultiWorld(std::set& teams); + void ReleaseWorldForCurrentTeam(); + + void SendPacket_ClearTeamState(std::string teamId); + void SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage); + void SendPacket_GameComplete(); + void SendPacket_GiveItem(u16 modId, s16 getItemId, std::string targetTeamId = ""); + void SendPacket_Handshake(); + void SendPacket_PlayerSfx(u16 sfxId); + void SendPacket_PlayerUpdate(); + void SendPacket_RequestTeamState(); + void SendPacket_RequestTeleport(u32 clientId); + void SendPacket_SetCheckStatus(RandoCheckId randoCheckId); + void SendPacket_SetFlag(s16 sceneId, s16 flagType, s16 flag); + void SendPacket_TeleportTo(u32 clientId); + void SendPacket_UnsetFlag(s16 sceneId, s16 flagType, s16 flag); + void SendPacket_UpdateClientState(); + void SendPacket_UpdateDungeonItems(); + void SendPacket_UpdateRoomState(); + void SendPacket_UpdateTeamState(std::string targetTeamId); +}; + +typedef enum { + /* 0 */ DUMMY_PLAYER_HIT_RESPONSE_NONE, + /* 1 */ DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE, + /* 2 */ DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_SMALL, + /* 3 */ DUMMY_PLAYER_HIT_RESPONSE_ICE_TRAP, + /* 4 */ DUMMY_PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK, + /* 5 */ DUMMY_PLAYER_HIT_RESPONSE_STUN, + /* 6 */ DUMMY_PLAYER_HIT_RESPONSE_FIRE, + /* 7 */ DUMMY_PLAYER_HIT_RESPONSE_NORMAL, +} PlayerDamageResponseType; + +class AnchorRoomWindow : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override{}; + void DrawElement() override; + void Draw() override; + void UpdateElement() override{}; +}; + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_H diff --git a/mm/2s2h/Network/Anchor/AnchorRoomWindow.cpp b/mm/2s2h/Network/Anchor/AnchorRoomWindow.cpp new file mode 100644 index 0000000000..6b2d6153e8 --- /dev/null +++ b/mm/2s2h/Network/Anchor/AnchorRoomWindow.cpp @@ -0,0 +1,113 @@ +#include "Anchor.h" +#include "2s2h/ShipUtils.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +} + +void AnchorRoomWindow::Draw() { + if (!IsVisible() || !Anchor::Instance->isConnected) { + return; + } + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat("gNotifications.BgOpacity", 0.5f))); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + + auto vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowViewport(vp->ID); + + ImGui::Begin("Anchor Room", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar); + + DrawElement(); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +void AnchorRoomWindow::DrawElement() { + bool isGlobalRoom = (std::string("2ship-global") == CVarGetString("gNetwork.Anchor.RoomId", "")); + + if (isGlobalRoom) { + u32 activeClients = 0; + for (auto& [clientId, client] : Anchor::Instance->clients) { + if (client.online) { + activeClients++; + } + } + ImGui::Text("Players Online: %d", activeClients); + return; + } + + // First build a list of teams + std::set teams; + for (auto& [clientId, client] : Anchor::Instance->clients) { + teams.insert(client.teamId); + } + + for (auto& team : teams) { + if (teams.size() > 1) { + ImGui::SeparatorText(team.c_str()); + } + bool isOwnTeam = team == CVarGetString("gNetwork.Anchor.TeamId", "default"); + for (auto& [clientId, client] : Anchor::Instance->clients) { + if (client.teamId != team) { + continue; + } + + ImGui::PushID(clientId); + + if (client.clientId == Anchor::Instance->roomState.ownerClientId) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", ICON_FA_GAVEL); + ImGui::SameLine(); + } + + if (client.self) { + ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.8f, 1.0f), "%s", CVarGetString("gNetwork.Anchor.Name", "")); + } else if (!client.online) { + ImGui::TextColored(ImVec4(1, 1, 1, 0.3f), "%s - offline", client.name.c_str()); + ImGui::PopID(); + continue; + } else { + ImGui::Text("%s", client.name.c_str()); + } + + if (Anchor::Instance->roomState.showLocationsMode == 2 || + (Anchor::Instance->roomState.showLocationsMode == 1 && isOwnTeam)) { + if ((client.self ? Anchor::Instance->IsSaveLoaded() : client.isSaveLoaded)) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "- %s", + Ship_GetSceneName(client.self ? gPlayState->sceneId : client.sceneId)); + } + } + + if (Anchor::Instance->CanTeleportTo(client.clientId)) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(20.0f, 20.0f))) { + Anchor::Instance->SendPacket_RequestTeleport(client.clientId); + } + ImGui::PopStyleVar(); + } + + if (client.clientVersion != Anchor::clientVersion) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Incompatible version! Will not work together!"); + ImGui::Text("Yours: %s", Anchor::clientVersion.c_str()); + ImGui::Text("Theirs: %s", client.clientVersion.c_str()); + ImGui::EndTooltip(); + } + } + ImGui::PopID(); + } + } +} diff --git a/mm/2s2h/Network/Anchor/DummyPlayer.cpp b/mm/2s2h/Network/Anchor/DummyPlayer.cpp new file mode 100644 index 0000000000..c7b39f32d3 --- /dev/null +++ b/mm/2s2h/Network/Anchor/DummyPlayer.cpp @@ -0,0 +1,216 @@ +#include "Anchor.h" +#include "2s2h/NameTag/NameTag.h" +#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" + +extern "C" { +#include "macros.h" +#include "variables.h" +#include "functions.h" +#include "z64malloc.h" +extern PlayState* gPlayState; + +#include "objects/object_test3/object_test3.h" +#include "objects/gameplay_keep/gameplay_keep.h" +extern PlayerAgeProperties sPlayerAgeProperties[PLAYER_FORM_MAX]; +extern TexturePtr sPlayerEyesTextures[PLAYER_FORM_MAX][PLAYER_EYES_MAX]; +extern EffectBlureInit2 D_8085D30C; +extern EffectTireMarkInit D_8085D330; +void Player_DrawGameplay(PlayState* play, Player* player, s32 lod, Gfx* cullDList, + OverrideLimbDrawFlex overrideLimbDraw); +void Player_Anim_PlayOnceMorph(PlayState* play, Player* player, PlayerAnimationHeader* anim); +PlayerAnimationHeader* Player_GetIdleAnim(Player* player); +} + +// Hijacking player->zTargetActiveTimer (unused s32 for the dummy) to store the clientId for convenience +#define DUMMY_CLIENT_ID player->zTargetActiveTimer + +static DamageTable DummyPlayerDamageTable = { + /* Deku Nut */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Deku Stick */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Horse trample */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Explosives */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Zora boomerang */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Normal arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* UNK_DMG_0x06 */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Hookshot */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Goron punch */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_SMALL), + /* Sword */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Goron pound */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Fire arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_FIRE), + /* Ice arrow */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_ICE_TRAP), + /* Light arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Goron spikes */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Deku spin */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Deku bubble */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Deku launch */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_SMALL), + /* UNK_DMG_0x12 */ DMG_ENTRY(3, DUMMY_PLAYER_HIT_RESPONSE_ICE_TRAP), + /* Zora barrier */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Normal shield */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_NONE), + /* Light ray */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_NONE), + /* Thrown object */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Zora punch */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Spin attack */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Sword beam */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Normal Roll */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_NONE), + /* UNK_DMG_0x1B */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* UNK_DMG_0x1C */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_NONE), + /* Unblockable */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_NONE), + /* UNK_DMG_0x1E */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Powder Keg */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), +}; + +// Modeled after EnTest3_Init and Player_Init +void DummyPlayer_Init(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + uint32_t clientId = Anchor::Instance->actorIndexToClientId[actor->params]; + DUMMY_CLIENT_ID = clientId; + + if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID]; + + player->actor.room = -1; + player->csId = CS_ID_NONE; + player->transformation = client.transformation; + player->ageProperties = &sPlayerAgeProperties[player->transformation]; + player->heldItemAction = PLAYER_IA_NONE; + player->heldItemId = ITEM_OCARINA_OF_TIME; + + Player_SetModelGroup(player, PLAYER_MODELGROUP_DEFAULT); + play->playerInit(player, play, gPlayerSkeletons[player->transformation]); + + // Skipping Effect_Add(...), the dummy doesn't need weapon effects + + player->maskObjectSegment = ZeldaArena_Malloc(0x3800); + // Skipping part of play->func_18780, specifically Player_SetAction + Player_Anim_PlayOnceMorph(play, player, Player_GetIdleAnim(player)); + player->yaw = player->actor.shape.rot.y; + + // Ensures the actor is always updating/drawing even when culled/out of distance + actor->flags = + ACTOR_FLAG_UPDATE_CULLING_DISABLED | ACTOR_FLAG_DRAW_CULLING_DISABLED | ACTOR_FLAG_INSIDE_CULLING_VOLUME; + player->cylinder.base.acFlags = AC_ON | AC_TYPE_PLAYER; + player->cylinder.base.ocFlags2 = OC2_TYPE_1; + player->cylinder.elem.acElemFlags = ACELEM_ON | ACELEM_HOOKABLE | ACELEM_NO_HITMARK; + player->actor.flags |= ACTOR_FLAG_HOOKSHOT_PULLS_PLAYER; + player->cylinder.dim.radius = 30; + player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable; + + bool isGlobalRoom = (std::string("2ship-global") == CVarGetString("gNetwork.Anchor.RoomId", "")); + if (!isGlobalRoom) { + NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), { .yOffset = 30 }); + } +} + +// Update the actor with new data from the client +void DummyPlayer_Update(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID]; + + if (client.sceneId != gPlayState->sceneId || !client.online || !client.isSaveLoaded) { + actor->world.pos.x = -9999.0f; + actor->world.pos.y = -9999.0f; + actor->world.pos.z = -9999.0f; + actor->shape.shadowAlpha = 0; + return; + } + + actor->shape.shadowAlpha = 255; + Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot); + Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos); + memcpy(&player->jointTableBuffer, &client.jointTable, 159); + memcpy(&player->jointTableUpperBuffer, &client.upperJointTable, 159); + player->maskObjectLoadState = 0; + player->maskId = player->currentMask; + player->currentMask = client.currentMask; + player->rightHandType = client.rightHandType; + player->leftHandType = client.leftHandType; + player->currentShield = client.currentShield; + player->sheathType = client.sheathType; + player->heldItemAction = client.heldItemAction; + player->heldItemId = client.heldItemId; + player->itemAction = client.itemAction; + player->stateFlags1 = client.stateFlags1; + player->stateFlags2 = client.stateFlags2; + player->stateFlags3 = client.stateFlags3; + player->unk_B0C = client.unk_B0C; + player->unk_B28 = client.unk_B28; + player->unk_ACC = client.unk_ACC; + player->invincibilityTimer = client.invincibilityTimer; + Player_SetModels(player, Player_ActionToModelGroup(player, (PlayerItemAction)player->itemAction)); + + if (Anchor::Instance->roomState.pvpMode == 0 || + (Anchor::Instance->roomState.pvpMode == 1 && + client.teamId == CVarGetString("gNetwork.Anchor.TeamId", "default"))) { + actor->flags |= ACTOR_FLAG_LOCK_ON_DISABLED; + return; + } + + actor->flags &= ~ACTOR_FLAG_LOCK_ON_DISABLED; + + if (player->cylinder.base.acFlags & AC_HIT && player->invincibilityTimer == 0) { + Anchor::Instance->SendPacket_DamagePlayer(client.clientId, player->actor.colChkInfo.damageEffect, + player->actor.colChkInfo.damage); + if (player->actor.colChkInfo.damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) { + Actor_SetColorFilter(&player->actor, 0, 0xFF, 0, 24); + } else { + player->invincibilityTimer = 20; + } + } + + Collider_UpdateCylinder(&player->actor, &player->cylinder); + + if (!(player->stateFlags2 & PLAYER_STATE2_4000)) { + if (!(player->stateFlags1 & (PLAYER_STATE1_4 | PLAYER_STATE1_DEAD | PLAYER_STATE1_2000 | PLAYER_STATE1_4000 | + PLAYER_STATE1_800000))) { + CollisionCheck_SetOC(play, &play->colChkCtx, &player->cylinder.base); + } + + if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_4000000)) && + (player->invincibilityTimer <= 0)) { + CollisionCheck_SetAC(play, &play->colChkCtx, &player->cylinder.base); + + if (player->invincibilityTimer < 0) { + CollisionCheck_SetAT(play, &play->colChkCtx, &player->cylinder.base); + } + } + } + + if (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_10000000 | PLAYER_STATE1_20000000)) { + player->actor.colChkInfo.mass = MASS_IMMOVABLE; + } else { + player->actor.colChkInfo.mass = 50; + } + + Collider_ResetCylinderAC(play, &player->cylinder.base); +} + +void DummyPlayer_Draw(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID]; + + if (client.sceneId != gPlayState->sceneId || !client.online || !client.isSaveLoaded) { + return; + } + + Player_DrawGameplay(play, player, 1, gCullBackDList, Player_OverrideLimbDrawGameplayDefault); +} + +void DummyPlayer_Destroy(Actor* actor, PlayState* play) { +} diff --git a/mm/2s2h/Network/Anchor/JsonConversions.hpp b/mm/2s2h/Network/Anchor/JsonConversions.hpp new file mode 100644 index 0000000000..421b3d9364 --- /dev/null +++ b/mm/2s2h/Network/Anchor/JsonConversions.hpp @@ -0,0 +1,31 @@ +#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H +#define NETWORK_ANCHOR_JSON_CONVERSIONS_H +#ifdef __cplusplus + +#include +#include +#include "Anchor.h" +#include "BenJsonConversions.hpp" + +extern "C" { +} + +using json = nlohmann::json; + +inline void from_json(const json& j, AnchorClient& client) { + j.contains("clientId") ? j.at("clientId").get_to(client.clientId) : client.clientId = 0; + 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("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???"; + j.contains("teamId") ? j.at("teamId").get_to(client.teamId) : client.teamId = "default"; + j.contains("online") ? j.at("online").get_to(client.online) : client.online = false; + j.contains("self") ? j.at("self").get_to(client.self) : client.self = false; + j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0; + j.contains("isSaveLoaded") ? j.at("isSaveLoaded").get_to(client.isSaveLoaded) : client.isSaveLoaded = false; + j.contains("isGameComplete") ? j.at("isGameComplete").get_to(client.isGameComplete) : client.isGameComplete = false; + j.contains("sceneId") ? j.at("sceneId").get_to(client.sceneId) : client.sceneId = SCENE_MAX; + j.contains("entrance") ? j.at("entrance").get_to(client.entrance) : client.entrance = 0; +} + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_JSON_CONVERSIONS_H diff --git a/mm/2s2h/Network/Anchor/Menu.cpp b/mm/2s2h/Network/Anchor/Menu.cpp new file mode 100644 index 0000000000..a841fd251f --- /dev/null +++ b/mm/2s2h/Network/Anchor/Menu.cpp @@ -0,0 +1,336 @@ +#include "Anchor.h" +#include +#include "2s2h/BenGui/UIWidgets.hpp" +#include "ShipUtils.h" +#include "2s2h/BenGui/BenMenu.h" +#include "2s2h/BenGui/BenGui.hpp" +#include "2s2h/BenGui/Notification.h" +#include "2s2h/Network/Sail/Sail.h" +#include "2s2h/Rando/Spoiler/Spoiler.h" +#include "2s2h/Rando/MiscBehavior/MiscBehavior.h" + +namespace BenGui { +extern std::shared_ptr mBenMenu; +extern std::shared_ptr mAnchorRoomWindow; +} // namespace BenGui +using namespace UIWidgets; + +static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" }; +static std::vector teleportModes = { "None", "Team Only", "All" }; +static std::vector showLocationsModes = { "None", "Team Only", "All" }; + +void AnchorMainMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + + std::string host = CVarGetString("gNetwork.Anchor.Host", "anchor.hm64.org"); + uint16_t port = CVarGetInteger("gNetwork.Anchor.Port", 43383); + std::string anchorTeamId = CVarGetString("gNetwork.Anchor.TeamId", "default"); + std::string anchorRoomId = CVarGetString("gNetwork.Anchor.RoomId", ""); + std::string anchorName = CVarGetString("gNetwork.Anchor.Name", ""); + bool isFormValid = !isStringEmpty(host) && port > 1024 && port < 65535 && !isStringEmpty(anchorRoomId) && + !isStringEmpty(anchorName); + + ImGui::SeparatorText("Connection Settings"); + + ImGui::BeginDisabled(anchor->isEnabled); + ImGui::Text("Host & Port"); + if (UIWidgets::InputString("##Host", &host, + UIWidgets::InputOptions() + .Size(ImGui::GetContentRegionAvail() - + ImVec2((ImGui::GetFontSize() * 5 + ImGui::GetStyle().ItemSpacing.x), 0)) + .Color(THEME_COLOR))) { + CVarSetString("gNetwork.Anchor.Host", host.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + UIWidgets::PushStyleInput(THEME_COLOR); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) { + CVarSetInteger("gNetwork.Anchor.Port", port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + UIWidgets::PopStyleInput(); + + ImGui::Text("Name"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##Name", &anchorName, UIWidgets::InputOptions().Color(THEME_COLOR))) { + CVarSetString("gNetwork.Anchor.Name", anchorName.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Text("Room ID"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##RoomId", &anchorRoomId, + UIWidgets::InputOptions().IsSecret(anchor->isEnabled).Color(THEME_COLOR))) { + CVarSetString("gNetwork.Anchor.RoomId", anchorRoomId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Text("Team ID (Items & Flags Shared)"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##TeamId", &anchorTeamId, UIWidgets::InputOptions().Color(THEME_COLOR))) { + CVarSetString("gNetwork.Anchor.TeamId", anchorTeamId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Spacing(); + + if (UIWidgets::Button("Restore Defaults", UIWidgets::ButtonOptions() + .Size(ImVec2(ImGui::GetContentRegionAvail().x / 2, 0)) + .Color(UIWidgets::Colors::Red))) { + CVarSetString("gNetwork.Anchor.Host", "anchor.hm64.org"); + CVarSetInteger("gNetwork.Anchor.Port", 43383); + CVarSetString("gNetwork.Anchor.TeamId", "default"); + CVarSetString("gNetwork.Anchor.RoomId", ""); + CVarSetString("gNetwork.Anchor.Name", ""); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + + if (UIWidgets::Button("Global Room", UIWidgets::ButtonOptions() + .Color(UIWidgets::Colors::Blue) + .Tooltip("Always-online public room so you don't have to experience " + "Hyrule alone. PVP and syncing are disabled."))) { + CVarSetString("gNetwork.Anchor.Host", "anchor.hm64.org"); + CVarSetInteger("gNetwork.Anchor.Port", 43383); + CVarSetString("gNetwork.Anchor.TeamId", "default"); + CVarSetString("gNetwork.Anchor.RoomId", "2ship-global"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = anchor->isEnabled ? "Disable" : "Enable"; + UIWidgets::PushStyleButton(anchor->isEnabled ? UIWidgets::ColorValues.at(UIWidgets::Colors::Red) + : UIWidgets::ColorValues.at(UIWidgets::Colors::Green)); + if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { + if (anchor->isEnabled) { + CVarClear("gNetwork.Anchor.Enabled"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + anchor->Disable(); + } else { + CVarSetInteger("gNetwork.Anchor.Enabled", 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + anchor->Enable(); + } + } + UIWidgets::PopStyleButton(); + ImGui::EndDisabled(); + ImGui::Spacing(); + + if (!anchor->isEnabled) { + return; + } + + if (!anchor->isConnected) { + ImGui::Text("Connecting..."); + return; + } + + ImGui::SeparatorText("Current Room"); + ImGui::Text("%s Connected", ICON_FA_CHECK); + + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Request Team State")) { + anchor->SendPacket_RequestTeamState(); + } + UIWidgets::Tooltip("Try this if you are missing items or flags that your team members have collected"); + UIWidgets::PopStyleButton(); + + ImGui::SameLine(); + + UIWidgets::WindowButton("Toggle Anchor Room Window", "gWindows.AnchorRoom", BenGui::mAnchorRoomWindow); + if (!BenGui::mAnchorRoomWindow->IsVisible()) { + BenGui::mAnchorRoomWindow->DrawElement(); + } +} + +static std::set teamsForMwInit; +void AnchorAdminMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + bool isGlobalRoom = (std::string("2ship-global") == CVarGetString("gNetwork.Anchor.RoomId", "")); + + if (!anchor->isEnabled || !anchor->isConnected || anchor->roomState.ownerClientId != anchor->ownClientId || + isGlobalRoom) { + return; + } + + ImGui::SeparatorText("Room Settings (Admin Only)"); + + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Clear All Team State")) { + std::set teams; + for (auto& [clientId, client] : Anchor::Instance->clients) { + teams.insert(client.teamId); + } + for (auto& team : teams) { + anchor->SendPacket_ClearTeamState(team); + } + anchor->roomState.teams.clear(); + anchor->SendPacket_UpdateRoomState(); + } + UIWidgets::PopStyleButton(); + + if (UIWidgets::CVarCombobox("PvP Mode:", "gNetwork.Anchor.RoomSettings.PvpMode", pvpModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPosition::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCombobox("Show Locations For:", "gNetwork.Anchor.RoomSettings.ShowLocationsMode", + showLocationsModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPosition::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCombobox("Allow Teleporting To:", "gNetwork.Anchor.RoomSettings.TeleportMode", teleportModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPosition::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCheckbox("Sync Items & Flags", "gNetwork.Anchor.RoomSettings.SyncItemsAndFlags", + UIWidgets::CheckboxOptions().DefaultValue(true).Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + + ImGui::SeparatorText("Multi-World"); + + if (Anchor::Instance->roomState.teams.empty()) { + std::string teamToRemove; + ImGui::AlignTextToFramePadding(); + ImGui::Text("Teams:"); + ImGui::SameLine(); + if (UIWidgets::Button(ICON_FA_PLUS, { .size = UIWidgets::Sizes::Inline, .color = UIWidgets::Colors::Green })) { + ImGui::OpenPopup("TeamPopup"); + } + for (auto& team : teamsForMwInit) { + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", team.c_str()); + ImGui::SameLine(); + if (UIWidgets::Button(ICON_FA_TIMES, + { .size = UIWidgets::Sizes::Inline, .color = UIWidgets::Colors::Red })) { + teamToRemove = team; + } + } + if (!teamToRemove.empty()) { + teamsForMwInit.erase(teamToRemove); + teamToRemove.clear(); + } + if (ImGui::BeginPopup("TeamPopup")) { + ImGui::SeparatorText("Available Spoiler Logs:"); + for (auto& spoilerFile : Rando::Spoiler::spoilerOptions) { + std::string teamName = spoilerFile.substr(0, spoilerFile.size() - 5); + if (spoilerFile == "Generate New Seed" || teamsForMwInit.contains(teamName)) { + continue; + } + + if (ImGui::Selectable(teamName.c_str())) { + teamsForMwInit.insert(teamName); + } + } + ImGui::EndPopup(); + } + if (teamsForMwInit.size() < 2 || Anchor::Instance->IsSaveLoaded()) { + ImGui::BeginDisabled(); + } + if (UIWidgets::Button("Initialize Multi-World")) { + Anchor::Instance->InitializeMultiWorld(teamsForMwInit); + } + if (teamsForMwInit.size() < 2 || Anchor::Instance->IsSaveLoaded()) { + ImGui::EndDisabled(); + } + } else { + ImGui::Text("Teams:"); + for (auto& team : Anchor::Instance->roomState.teams) { + ImGui::Text("%s", team.c_str()); + } + + if (UIWidgets::Button("Release World For Current Team", + UIWidgets::ButtonOptions() + .Color(UIWidgets::Colors::Red) + .Tooltip("Releases the current multi-world team from the " + "room, allowing them to continue playing solo."))) { + Anchor::Instance->ReleaseWorldForCurrentTeam(); + } + } +} + +void AnchorInstructionsMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + + ImGui::TextWrapped( + "Important Notes:\n- All players involved should start at the file select screen with Anchor disabled.\n- " + "Multi-world and Co-op can be used together, but follow the multi-world instructions.\n- Multi-world only " + "supports no-logic, this will be the case for the forseeable future."); + + ImGui::Dummy(ImVec2(0.0f, 10.0f)); + + ImGui::SeparatorText("Co-op Usage Instructions"); + + ImGui::TextWrapped("1. The host player should set up a room by entering a Room ID and enable Anchor. (If just one " + "team, leave the team as 'default')"); + + ImGui::TextWrapped("2. Configure the room settings as desired (PvP, teleporting, etc)."); + + ImGui::TextWrapped("3. If multiple teams, other team leaders should enter the same Room ID and their associated " + "Team IDs and enable Anchor."); + + ImGui::TextWrapped("4. Host/Team leaders should then create a randomizer seed like normal, and load into the game. " + "This can match across teams but doesn't have to."); + + ImGui::TextWrapped("4. Once the Host/Team leaders are in-game, other players can enter the same Room ID and " + "associated Team IDs and enable Anchor."); + + ImGui::TextWrapped("5. Other players should then each create a new randomizer file (the configuration does not " + "matter, it will be overridden by the team leader's settings), and load into the game."); + + ImGui::TextWrapped("6. All players should now be connected and able to see each other in-game, occasionally you " + "may need to leave and re-enter South clock town if a player does not appear."); + + ImGui::Dummy(ImVec2(0.0f, 10.0f)); + + ImGui::SeparatorText("Multiworld Usage Instructions"); + + ImGui::TextWrapped("1. The host should prepare for the multiworld by creating spoiler files for each team " + "involved, ensuring that the name of the spoiler file matches the Team ID."); + + ImGui::TextWrapped("2. The host player should set up a room by entering a Room ID and the Team ID they belong to, " + "then enable Anchor."); + + ImGui::TextWrapped("3. Configure the room settings as desired (PvP, teleporting, etc)."); + + ImGui::TextWrapped("4. The host should then initialize the multiworld by selecting the Spoiler files for the " + "associated teams from the dropdown and clicking 'Initialize Multi-World'."); + + ImGui::TextWrapped( + "5. Other players should then enter the same Room ID and their associated Team IDs and enable Anchor."); + + ImGui::TextWrapped("6. All players should then each create a new randomizer file (the configuration does not " + "matter, it will be overridden by multi-world), and load into the game."); + + ImGui::TextWrapped("7. All players should now be connected and able to see each other in-game, occasionally you " + "may need to leave and re-enter South clock town if a player does not appear."); +} + +#ifdef ENABLE_NETWORKING +void RegisterAnchorMenu() { + // Add Network Menu + BenGui::mBenMenu->AddMenuEntry("Network", "gSettings.Menu.NetworkSidebarSection"); + + BenGui::mBenMenu->AddSidebarEntry("Network", "Anchor", 2); + WidgetPath path = { "Network", "Anchor", SECTION_COLUMN_1 }; + BenGui::mBenMenu->AddWidget(path, "AnchorMainMenu", WIDGET_CUSTOM).CustomFunction(AnchorMainMenu); + path.column = SECTION_COLUMN_2; + BenGui::mBenMenu->AddWidget(path, "AnchorAdminMenu", WIDGET_CUSTOM).CustomFunction(AnchorAdminMenu); + BenGui::mBenMenu->AddWidget(path, "AnchorInstructionsMenu", WIDGET_CUSTOM).CustomFunction(AnchorInstructionsMenu); +} + +static RegisterMenuInitFunc menuInitFunc(RegisterAnchorMenu); +#endif diff --git a/mm/2s2h/Network/Anchor/Packets/AllClientState.cpp b/mm/2s2h/Network/Anchor/Packets/AllClientState.cpp new file mode 100644 index 0000000000..07f67614c1 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/AllClientState.cpp @@ -0,0 +1,71 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "2s2h/BenPort.h" +#include "2s2h/BenGui/Notification.h" + +/** + * ALL_CLIENT_STATE + * + * Contains a list of all clients and their CLIENT_STATE currently connected to the server + * + * The server itself sends this packet to all clients when a client connects or disconnects + */ + +void Anchor::HandlePacket_AllClientState(nlohmann::json payload) { + std::vector newClients = payload["state"].get>(); + bool isGlobalRoom = (std::string("2ship-global") == CVarGetString("gNetwork.Anchor.RoomId", "")); + + // add new clients + for (auto& client : newClients) { + if (client.self) { + ownClientId = client.clientId; + CVarSetInteger("gNetwork.Anchor.LastClientId", ownClientId); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + clients[client.clientId].self = true; + } else { + clients[client.clientId].self = false; + if (clients.contains(client.clientId)) { + if (clients[client.clientId].online != client.online && !isGlobalRoom) { + Notification::Emit({ + .prefix = client.name, + .message = client.online ? "Connected" : "Disconnected", + }); + } + } else if (client.online && !isGlobalRoom) { + Notification::Emit({ + .prefix = client.name, + .message = "Connected", + }); + } + } + + clients[client.clientId].clientId = client.clientId; + clients[client.clientId].name = client.name; + clients[client.clientId].color = client.color; + clients[client.clientId].clientVersion = client.clientVersion; + clients[client.clientId].teamId = client.teamId; + clients[client.clientId].online = client.online; + clients[client.clientId].seed = client.seed; + clients[client.clientId].isSaveLoaded = client.isSaveLoaded; + clients[client.clientId].isGameComplete = client.isGameComplete; + clients[client.clientId].sceneId = client.sceneId; + clients[client.clientId].entrance = client.entrance; + } + + // remove clients that are no longer in the list + std::vector clientsToRemove; + for (auto& [clientId, client] : clients) { + if (std::find_if(newClients.begin(), newClients.end(), + [clientId](AnchorClient& c) { return c.clientId == clientId; }) == newClients.end()) { + clientsToRemove.push_back(clientId); + } + } + // (seperate loop to avoid iterator invalidation) + for (auto& clientId : clientsToRemove) { + clients.erase(clientId); + } + + RefreshClientActors(); +} diff --git a/mm/2s2h/Network/Anchor/Packets/DamagePlayer.cpp b/mm/2s2h/Network/Anchor/Packets/DamagePlayer.cpp new file mode 100644 index 0000000000..fcb50ca4b2 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/DamagePlayer.cpp @@ -0,0 +1,70 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" + +extern "C" { +#include "macros.h" +#include "functions.h" +extern PlayState* gPlayState; +void func_80833B18(PlayState* play, Player* player, s32 arg2, f32 speed, f32 velocityY, s16 arg5, + s32 invincibilityTimer); +} + +/** + * DAMAGE_PLAYER + */ + +void Anchor::SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = DAMAGE_PLAYER; + payload["targetClientId"] = clientId; + payload["damageEffect"] = damageEffect; + payload["damage"] = damage; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_DamagePlayer(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + if (!clients.contains(clientId) || clients[clientId].player == nullptr) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + Player* otherPlayer = anchorClient.player; + Player* self = GET_PLAYER(gPlayState); + + // Prevent incoming damage during cutscenes or item get sequences + if (Player_InBlockingCsMode(gPlayState, self) || self->stateFlags1 & PLAYER_STATE1_400 || + self->stateFlags1 & PLAYER_STATE1_10000000) { + return; + } + + u8 damageEffect = payload["damageEffect"].get(); + u8 damage = payload["damage"].get(); + + self->actor.colChkInfo.damage = damage * 8; // Arbitrary number currently, need to fine tune + + if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_FIRE) { + for (int i = 0; i < ARRAY_COUNT(self->bodyFlameTimers); i++) { + self->bodyFlameTimers[i] = Rand_S16Offset(0, 200); + } + self->bodyIsBurning = true; + } else if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) { + self->actor.freezeTimer = 20; + Actor_SetColorFilter(&self->actor, 0, 0xFF, 0, 24); + return; + } + + func_80833B18(gPlayState, self, damageEffect, 4.0f, 5.0f, + Actor_WorldYawTowardActor(&otherPlayer->actor, &self->actor), 20); +} diff --git a/mm/2s2h/Network/Anchor/Packets/DisableAnchor.cpp b/mm/2s2h/Network/Anchor/Packets/DisableAnchor.cpp new file mode 100644 index 0000000000..a7cd1f5053 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/DisableAnchor.cpp @@ -0,0 +1,14 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" + +/** + * DISABLE_ANCHOR + * + * No current use, potentially will be used for a future feature. + */ + +void Anchor::HandlePacket_DisableAnchor(nlohmann::json payload) { + Disable(); +} diff --git a/mm/2s2h/Network/Anchor/Packets/GameComplete.cpp b/mm/2s2h/Network/Anchor/Packets/GameComplete.cpp new file mode 100644 index 0000000000..b5a1c38c0c --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/GameComplete.cpp @@ -0,0 +1,42 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenGui/Notification.h" +#include "2s2h/ShipUtils.h" + +const std::string gameCompleteMessages[] = { + "killed Ganon", "saved Zelda", "proved their Courage", + "collected the Triforce", "is the Hero of Time", "proved Mido wrong", +}; + +/** + * GAME_COMPLETE + */ + +void Anchor::SendPacket_GameComplete() { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = GAME_COMPLETE; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_GameComplete(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + if (!clients.contains(clientId)) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + anchorClient.isGameComplete = true; + bool isGlobalRoom = (std::string("2ship-global") == CVarGetString("gNetwork.Anchor.RoomId", "")); + + Notification::Emit({ + .prefix = isGlobalRoom ? "Someone" : anchorClient.name, + .message = gameCompleteMessages[rand() % (sizeof(gameCompleteMessages) / sizeof(gameCompleteMessages[0]))], + }); +} diff --git a/mm/2s2h/Network/Anchor/Packets/GiveItem.cpp b/mm/2s2h/Network/Anchor/Packets/GiveItem.cpp new file mode 100644 index 0000000000..af2d9a1fc6 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/GiveItem.cpp @@ -0,0 +1,59 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenGui/Notification.h" +#include "2s2h/BenPort.h" +#include "2s2h/Rando/Rando.h" + +extern "C" { +#include "functions.h" +extern PlayState* gPlayState; +extern s16 D_801CFF94[250]; +} + +/** + * GIVE_ITEM + */ + +void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId, std::string targetTeamId) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = GIVE_ITEM; + payload["targetTeamId"] = targetTeamId == "" ? CVarGetString("gNetwork.Anchor.TeamId", "default") : targetTeamId; + payload["addToQueue"] = true; + payload["modId"] = modId; + payload["getItemId"] = getItemId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_GiveItem(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + AnchorClient& client = clients[clientId]; + + RandoItemId randoItemId = Rando::ConvertItem((RandoItemId)payload["getItemId"].get()); + std::string suffix = Rando::StaticData::GetItemName(randoItemId); + + if (randoItemId == RI_JUNK) { + randoItemId = Rando::CurrentJunkItem(); + } + + if (Rando::StaticData::Items[randoItemId].randoItemType != RITYPE_JUNK) { + Notification::Emit({ + .itemIcon = Rando::StaticData::GetIconTexturePath(randoItemId), + .prefix = client.name, + .message = "found your", + .suffix = suffix, + }); + } + + Rando::GiveItem(randoItemId); +} diff --git a/mm/2s2h/Network/Anchor/Packets/Handshake.cpp b/mm/2s2h/Network/Anchor/Packets/Handshake.cpp new file mode 100644 index 0000000000..1397141e02 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/Handshake.cpp @@ -0,0 +1,22 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenPort.h" + +/** + * HANDSHAKE + * + * Sent by the client to the server when it first connects to the server, sends over both the local room settings + * in case the room needs to be created, along with the current client state + */ + +void Anchor::SendPacket_Handshake() { + nlohmann::json payload; + payload["type"] = HANDSHAKE; + payload["roomId"] = CVarGetString("gNetwork.Anchor.RoomId", ""); + payload["roomState"] = PrepRoomState(); + payload["clientState"] = PrepClientState(); + + SendJsonToRemote(payload); +} diff --git a/mm/2s2h/Network/Anchor/Packets/PlayerSfx.cpp b/mm/2s2h/Network/Anchor/Packets/PlayerSfx.cpp new file mode 100644 index 0000000000..93cad0b634 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/PlayerSfx.cpp @@ -0,0 +1,51 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "functions.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * PLAYER_SFX + * + * Sound effects, only sent to other clients in the same scene as the player + */ + +void Anchor::SendPacket_PlayerSfx(u16 sfxId) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + + payload["type"] = PLAYER_SFX; + payload["sfxId"] = sfxId; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneId == gPlayState->sceneId && client.online && client.isSaveLoaded && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerSfx(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + u16 sfxId = payload["sfxId"].get(); + + if (!clients.contains(clientId) || !clients[clientId].player) { + return; + } + + Player_PlaySfx(clients[clientId].player, sfxId); +} diff --git a/mm/2s2h/Network/Anchor/Packets/PlayerUpdate.cpp b/mm/2s2h/Network/Anchor/Packets/PlayerUpdate.cpp new file mode 100644 index 0000000000..ee36c1b536 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/PlayerUpdate.cpp @@ -0,0 +1,108 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * PLAYER_UPDATE + * + * Contains real-time data necessary to update other clients in the same scene as the player + * + * Sent every frame to other clients within the same scene + * + * Note: This packet is sent _a lot_, so please do not include any unnecessary data in it + */ + +void Anchor::SendPacket_PlayerUpdate() { + if (!IsSaveLoaded()) { + return; + } + + uint32_t currentPlayerCount = 0; + for (auto& [clientId, client] : clients) { + if (client.sceneId == gPlayState->sceneId && client.online && client.isSaveLoaded && !client.self) { + currentPlayerCount++; + } + } + if (currentPlayerCount == 0) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + nlohmann::json payload; + + payload["type"] = PLAYER_UPDATE; + payload["sceneId"] = gPlayState->sceneId; + payload["entrance"] = gSaveContext.save.entrance; + payload["roomIndex"] = gPlayState->roomCtx.curRoom.num; + payload["transformation"] = player->transformation; + payload["posRot"]["pos"] = player->actor.world.pos; + payload["posRot"]["rot"] = player->actor.shape.rot; + payload["jointTable"] = player->jointTableBuffer; + payload["upperJointTable"] = player->jointTableUpperBuffer; + payload["currentMask"] = player->currentMask; + payload["rightHandType"] = player->rightHandType; + payload["leftHandType"] = player->leftHandType; + payload["currentShield"] = player->currentShield; + payload["sheathType"] = player->sheathType; + payload["heldItemAction"] = player->heldItemAction; + payload["heldItemId"] = player->heldItemId; + payload["itemAction"] = player->itemAction; + payload["stateFlags1"] = player->stateFlags1; + payload["stateFlags2"] = player->stateFlags2; + payload["stateFlags3"] = player->stateFlags3; + payload["unk_B0C"] = player->unk_B0C; + payload["unk_B28"] = player->unk_B28; + payload["unk_ACC"] = player->unk_ACC; + payload["invincibilityTimer"] = player->invincibilityTimer; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneId == gPlayState->sceneId && client.online && client.isSaveLoaded && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + if (clients.contains(clientId)) { + auto& client = clients[clientId]; + + if (client.transformation != payload["transformation"].get()) { + shouldRefreshActors = true; + } + + client.sceneId = payload["sceneId"].get(); + client.entrance = payload["entrance"].get(); + client.transformation = payload["transformation"].get(); + client.posRot = payload["posRot"].get(); + for (int i = 0; i < 159; i++) { + client.jointTable[i] = payload["jointTable"][i].get(); + client.upperJointTable[i] = payload["upperJointTable"][i].get(); + } + client.currentMask = payload["currentMask"].get(); + client.rightHandType = payload["rightHandType"].get(); + client.leftHandType = payload["leftHandType"].get(); + client.currentShield = payload["currentShield"].get(); + client.sheathType = payload["sheathType"].get(); + client.heldItemAction = payload["heldItemAction"].get(); + client.heldItemId = payload["heldItemId"].get(); + client.itemAction = payload["itemAction"].get(); + client.stateFlags1 = payload["stateFlags1"].get(); + client.stateFlags2 = payload["stateFlags2"].get(); + client.stateFlags3 = payload["stateFlags3"].get(); + client.unk_B0C = payload["unk_B0C"].get(); + client.unk_B28 = payload["unk_B28"].get(); + client.unk_ACC = payload["unk_ACC"].get(); + client.invincibilityTimer = payload["invincibilityTimer"].get(); + } +} diff --git a/mm/2s2h/Network/Anchor/Packets/RequestTeamState.cpp b/mm/2s2h/Network/Anchor/Packets/RequestTeamState.cpp new file mode 100644 index 0000000000..a27fe890ae --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/RequestTeamState.cpp @@ -0,0 +1,37 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/BenPort.h" + +/** + * REQUEST_TEAM_STATE + * + * Requests team state from the server, which will pass on the request to any connected teammates, or send the last + * known state if no teammates are connected. + * + * This fires when loading into a file while Anchor is connected, or when Anchor is connected while a file is already + * loaded + * + * Note: This can additionally be fired with a button in the menus to fix any desyncs that may have occurred in the save + * state + */ + +void Anchor::SendPacket_RequestTeamState() { + if (!roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = REQUEST_TEAM_STATE; + payload["targetTeamId"] = CVarGetString("gNetwork.Anchor.TeamId", "default"); + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_RequestTeamState(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + SendPacket_UpdateTeamState(CVarGetString("gNetwork.Anchor.TeamId", "default")); +} diff --git a/mm/2s2h/Network/Anchor/Packets/RequestTeleport.cpp b/mm/2s2h/Network/Anchor/Packets/RequestTeleport.cpp new file mode 100644 index 0000000000..378df23f81 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/RequestTeleport.cpp @@ -0,0 +1,75 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" + +/** + * REQUEST_TELEPORT + * + * Because we don't have all the necessary information to directly teleport to a player, we emit a request, + * in which they will respond with a TELEPORT_TO packet, with the necessary information. + */ + +void Anchor::SendPacket_RequestTeleport(uint32_t clientId) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = REQUEST_TELEPORT; + payload["targetClientId"] = clientId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_RequestTeleport(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + SendPacket_TeleportTo(clientId); +} + +// Reusable function to check if teleporting to a client is allowed +bool Anchor::CanTeleportTo(uint32_t clientId) { + // Teleporting is disabled + if (roomState.teleportMode == 0) { + return false; + } + + // You're not loaded into a save + if (!IsSaveLoaded()) { + return false; + } + + // The client doesn't exist + if (clients.find(clientId) == clients.end()) { + return false; + } + + AnchorClient& client = clients[clientId]; + + // The client is yourself + if (client.self) { + return false; + } + + // The client isn't online or loaded into a save + if (!client.online || !client.isSaveLoaded) { + return false; + } + + // Teleporting to team only, but the client is not on your team + std::string ownTeamId = CVarGetString("gNetwork.Anchor.TeamId", "default"); + if (roomState.teleportMode == 1 && client.teamId != ownTeamId) { + return false; + } + + // Problematic scenes for teleporting + if (client.sceneId == SCENE_MAX || client.sceneId == SCENE_KAKUSIANA) { + return false; + } + + return true; +} diff --git a/mm/2s2h/Network/Anchor/Packets/ServerMessage.cpp b/mm/2s2h/Network/Anchor/Packets/ServerMessage.cpp new file mode 100644 index 0000000000..22a65b5ddd --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/ServerMessage.cpp @@ -0,0 +1,17 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenGui/Notification.h" + +/** + * SERVER_MESSAGE + */ + +void Anchor::HandlePacket_ServerMessage(nlohmann::json payload) { + Notification::Emit({ + .prefix = "Server:", + .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f), + .message = payload["message"].get(), + }); +} diff --git a/mm/2s2h/Network/Anchor/Packets/SetCheckStatus.cpp b/mm/2s2h/Network/Anchor/Packets/SetCheckStatus.cpp new file mode 100644 index 0000000000..0bfab50b82 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/SetCheckStatus.cpp @@ -0,0 +1,42 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenJsonConversions.hpp" + +/** + * SET_CHECK_STATUS + * + * Fired when a check status is updated or skipped + */ + +void Anchor::SendPacket_SetCheckStatus(RandoCheckId randoCheckId) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = SET_CHECK_STATUS; + payload["targetTeamId"] = CVarGetString("gNetwork.Anchor.TeamId", "default"); + payload["addToQueue"] = true; + payload["randoCheckId"] = randoCheckId; + payload["data"] = RANDO_SAVE_CHECKS[randoCheckId]; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + RandoCheckId randoCheckId = payload["randoCheckId"].get(); + // RANDO_SAVE_CHECKS[randoCheckId].randoItemId = payload["data"]["randoItemId"].get(); + // RANDO_SAVE_CHECKS[randoCheckId].shuffled = payload["data"]["shuffled"].get(); + // RANDO_SAVE_CHECKS[randoCheckId].eligible = payload["data"]["eligible"].get(); + // RANDO_SAVE_CHECKS[randoCheckId].cycleObtained = payload["data"]["cycleObtained"].get(); + RANDO_SAVE_CHECKS[randoCheckId].obtained = payload["data"]["obtained"].get(); + RANDO_SAVE_CHECKS[randoCheckId].skipped = payload["data"]["skipped"].get(); + // RANDO_SAVE_CHECKS[randoCheckId].price = payload["data"]["price"].get(); + // RANDO_SAVE_CHECKS[randoCheckId].multiWorldTeamIndex = payload["data"]["multiWorldTeamIndex"].get(); +} diff --git a/mm/2s2h/Network/Anchor/Packets/SetFlag.cpp b/mm/2s2h/Network/Anchor/Packets/SetFlag.cpp new file mode 100644 index 0000000000..05df1594de --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/SetFlag.cpp @@ -0,0 +1,50 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenPort.h" + +/** + * SET_FLAG + * + * Fired when a flag is set in the save context + */ + +void Anchor::SendPacket_SetFlag(s16 sceneId, s16 flagType, s16 flag) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = SET_FLAG; + payload["targetTeamId"] = CVarGetString("gNetwork.Anchor.TeamId", "default"); + payload["addToQueue"] = true; + payload["sceneId"] = sceneId; + payload["flagType"] = flagType; + payload["flag"] = flag; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_SetFlag(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + s16 sceneId = payload["sceneId"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + // if (sceneId == SCENE_MAX) { + // auto effect = new GameInteractionEffect::SetFlag(); + // effect->parameters[0] = payload["flagType"].get(); + // effect->parameters[1] = payload["flag"].get(); + // effect->Apply(); + // } else { + // auto effect = new GameInteractionEffect::SetSceneFlag(); + // effect->parameters[0] = payload["sceneId"].get(); + // effect->parameters[1] = payload["flagType"].get(); + // effect->parameters[2] = payload["flag"].get(); + // effect->Apply(); + // } +} diff --git a/mm/2s2h/Network/Anchor/Packets/TeleportTo.cpp b/mm/2s2h/Network/Anchor/Packets/TeleportTo.cpp new file mode 100644 index 0000000000..007ab48dd3 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/TeleportTo.cpp @@ -0,0 +1,54 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" + +extern "C" { +#include "macros.h" +extern PlayState* gPlayState; +} + +/** + * TELEPORT_TO + * + * See REQUEST_TELEPORT for more information, this is the second part of the process. + */ + +void Anchor::SendPacket_TeleportTo(uint32_t clientId) { + if (!IsSaveLoaded()) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + + nlohmann::json payload; + payload["type"] = TELEPORT_TO; + payload["targetClientId"] = clientId; + payload["entrance"] = gSaveContext.save.entrance; + payload["roomIndex"] = gPlayState->roomCtx.curRoom.num; + payload["posRot"] = player->actor.world; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + s32 entrance = payload["entrance"].get(); + s8 roomIndex = payload["roomIndex"].get(); + PosRot posRot = payload["posRot"].get(); + + gPlayState->nextEntrance = entrance; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_INSTANT; + gSaveContext.respawn[RESPAWN_MODE_DOWN].entrance = entrance; + gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = roomIndex; + gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = posRot.pos; + gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = posRot.rot.y; + gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = 0xDFF; + gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK_FAST; + gSaveContext.respawnFlag = -8; +} diff --git a/mm/2s2h/Network/Anchor/Packets/UnsetFlag.cpp b/mm/2s2h/Network/Anchor/Packets/UnsetFlag.cpp new file mode 100644 index 0000000000..1ef59fe2a8 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/UnsetFlag.cpp @@ -0,0 +1,50 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenPort.h" + +/** + * UNSET_FLAG + * + * Fired when a flag is unset in the save context + */ + +void Anchor::SendPacket_UnsetFlag(s16 sceneId, s16 flagType, s16 flag) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = UNSET_FLAG; + payload["targetTeamId"] = CVarGetString("gNetwork.Anchor.TeamId", "default"); + payload["addToQueue"] = true; + payload["sceneId"] = sceneId; + payload["flagType"] = flagType; + payload["flag"] = flag; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + s16 sceneId = payload["sceneId"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + // if (sceneId == SCENE_MAX) { + // auto effect = new GameInteractionEffect::UnsetFlag(); + // effect->parameters[0] = payload["flagType"].get(); + // effect->parameters[1] = payload["flag"].get(); + // effect->Apply(); + // } else { + // auto effect = new GameInteractionEffect::UnsetSceneFlag(); + // effect->parameters[0] = payload["sceneId"].get(); + // effect->parameters[1] = payload["flagType"].get(); + // effect->parameters[2] = payload["flag"].get(); + // effect->Apply(); + // } +} diff --git a/mm/2s2h/Network/Anchor/Packets/UpdateClientState.cpp b/mm/2s2h/Network/Anchor/Packets/UpdateClientState.cpp new file mode 100644 index 0000000000..8206b6d498 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/UpdateClientState.cpp @@ -0,0 +1,73 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "2s2h/BenPort.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_CLIENT_STATE + * + * Contains a small subset of data that is cached on the server and important for the client to know for various reasons + * + * Sent on various events, such as changing scenes, soft resetting, finishing the game, opening file select, etc. + * + * Note: This packet should be cross version compatible, so if you add anything here don't assume all clients will be + * providing it, consider doing a `contains` check before accessing any version specific data + */ + +nlohmann::json Anchor::PrepClientState() { + nlohmann::json payload; + payload["name"] = CVarGetString("gNetwork.Anchor.Name", ""); + payload["color"] = CVarGetColor24("gNetwork.Anchor.Color", { 100, 255, 100 }); + payload["clientVersion"] = clientVersion; + payload["teamId"] = CVarGetString("gNetwork.Anchor.TeamId", "default"); + payload["online"] = true; + + if (IsSaveLoaded()) { + payload["seed"] = 0; // TODO: Implement seed + payload["isSaveLoaded"] = true; + payload["isGameComplete"] = false; // TODO: Implement game completion check + payload["sceneId"] = gPlayState->sceneId; + payload["entrance"] = gSaveContext.save.entrance; + } else { + payload["seed"] = 0; + payload["isSaveLoaded"] = false; + payload["isGameComplete"] = false; + payload["sceneId"] = SCENE_MAX; + payload["entrance"] = 0x00; + } + + return payload; +} + +void Anchor::SendPacket_UpdateClientState() { + nlohmann::json payload; + payload["type"] = UPDATE_CLIENT_STATE; + payload["state"] = PrepClientState(); + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateClientState(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + if (clients.contains(clientId)) { + AnchorClient client = payload["state"].get(); + clients[clientId].clientId = clientId; + clients[clientId].name = client.name; + clients[clientId].color = client.color; + clients[clientId].clientVersion = client.clientVersion; + clients[clientId].teamId = client.teamId; + clients[clientId].online = client.online; + clients[clientId].seed = client.seed; + clients[clientId].isSaveLoaded = client.isSaveLoaded; + clients[clientId].isGameComplete = client.isGameComplete; + clients[clientId].sceneId = client.sceneId; + clients[clientId].entrance = client.entrance; + } +} diff --git a/mm/2s2h/Network/Anchor/Packets/UpdateDungeonItems.cpp b/mm/2s2h/Network/Anchor/Packets/UpdateDungeonItems.cpp new file mode 100644 index 0000000000..be88c32c4a --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -0,0 +1,40 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/BenPort.h" + +/** + * UPDATE_DUNGEON_ITEMS + * + * This is for 2 things, first is updating the dungeon items in vanilla saves, and second is + * for ensuring the amount of keys used is synced as players are using them. + */ + +void Anchor::SendPacket_UpdateDungeonItems() { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = UPDATE_DUNGEON_ITEMS; + payload["targetTeamId"] = CVarGetString("gNetwork.Anchor.TeamId", "default"); + payload["addToQueue"] = true; + payload["mapIndex"] = gSaveContext.mapIndex; + payload["dungeonItems"] = gSaveContext.save.saveInfo.inventory.dungeonItems[gSaveContext.mapIndex]; + payload["dungeonKeys"] = gSaveContext.save.saveInfo.inventory.dungeonKeys[gSaveContext.mapIndex]; + payload["strayFairies"] = gSaveContext.save.saveInfo.inventory.strayFairies[gSaveContext.mapIndex]; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + u16 mapIndex = payload["mapIndex"].get(); + gSaveContext.save.saveInfo.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get(); + gSaveContext.save.saveInfo.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get(); + gSaveContext.save.saveInfo.inventory.strayFairies[mapIndex] = payload["strayFairies"].get(); +} diff --git a/mm/2s2h/Network/Anchor/Packets/UpdateRoomState.cpp b/mm/2s2h/Network/Anchor/Packets/UpdateRoomState.cpp new file mode 100644 index 0000000000..8f7bdb9232 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/UpdateRoomState.cpp @@ -0,0 +1,57 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "2s2h/BenPort.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_ROOM_STATE + */ + +nlohmann::json Anchor::PrepRoomState() { + nlohmann::json payload; + payload["ownerClientId"] = ownClientId; + bool isGlobalRoom = (std::string("2ship-global") == CVarGetString("gNetwork.Anchor.RoomId", "")); + + if (isGlobalRoom) { + payload["pvpMode"] = 0; + payload["teleportMode"] = 0; + payload["showLocationsMode"] = 0; + payload["syncItemsAndFlags"] = 0; + payload["teams"] = std::vector(); + } else { + payload["pvpMode"] = CVarGetInteger("gNetwork.Anchor.RoomSettings.PvpMode", 1); + payload["teleportMode"] = CVarGetInteger("gNetwork.Anchor.RoomSettings.TeleportMode", 1); + payload["showLocationsMode"] = CVarGetInteger("gNetwork.Anchor.RoomSettings.ShowLocationsMode", 1); + payload["syncItemsAndFlags"] = CVarGetInteger("gNetwork.Anchor.RoomSettings.SyncItemsAndFlags", 1); + payload["teams"] = roomState.teams; + } + + return payload; +} + +void Anchor::SendPacket_UpdateRoomState() { + nlohmann::json payload; + payload["type"] = UPDATE_ROOM_STATE; + payload["state"] = PrepRoomState(); + + Network::SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateRoomState(nlohmann::json payload) { + if (!payload.contains("state")) { + return; + } + + roomState.ownerClientId = payload["state"]["ownerClientId"].get(); + roomState.pvpMode = payload["state"]["pvpMode"].get(); + roomState.teleportMode = payload["state"]["teleportMode"].get(); + roomState.showLocationsMode = payload["state"]["showLocationsMode"].get(); + roomState.syncItemsAndFlags = payload["state"]["syncItemsAndFlags"].get(); + roomState.teams = payload["state"]["teams"].get>(); +} diff --git a/mm/2s2h/Network/Anchor/Packets/UpdateTeamState.cpp b/mm/2s2h/Network/Anchor/Packets/UpdateTeamState.cpp new file mode 100644 index 0000000000..801891b2a3 --- /dev/null +++ b/mm/2s2h/Network/Anchor/Packets/UpdateTeamState.cpp @@ -0,0 +1,151 @@ +#include "2s2h/Network/Anchor/Anchor.h" +#include "2s2h/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "2s2h/BenPort.h" +#include "2s2h/BenGui/Notification.h" +#include "2s2h/Rando/Rando.h" +#include "2s2h/Rando/CheckTracker/CheckTracker.h" +#include "2s2h/Rando/ActorBehavior/ActorBehavior.h" +#include "2s2h/ShipInit.hpp" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_TEAM_STATE + * + * Pushes the current save state to the server for other teammates to use. + * + * Fires when the server passes on a REQUEST_TEAM_STATE packet, or when this client saves the game + * + * When sending this packet we will assume that the team queue has been emptied for this client, so the queue + * stored in the server will be cleared. + * + * When receiving this packet, if there is items in the team queue, we will play them back in order. + */ + +void Anchor::SendPacket_UpdateTeamState(std::string targetTeamId) { + if (!roomState.syncItemsAndFlags) { + return; + } + + json payload; + payload["type"] = UPDATE_TEAM_STATE; + payload["targetTeamId"] = targetTeamId; + + // Assume the team queue has been emptied, so clear it + payload["queue"] = json::array(); + + payload["state"] = gSaveContext.save; + // TODO: Manually update scene flags from actorCtx + + // Hack to reduce the amount of bytes for this data + if (IS_RANDO) { + payload["state"]["shipSaveInfo"]["rando"].erase("randoSaveChecks"); + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"] = json::array(); + for (int i = 0; i < RC_MAX; i++) { + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i] = json::array(); + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][0] = RANDO_SAVE_CHECKS[i].randoItemId; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][1] = (u8)RANDO_SAVE_CHECKS[i].shuffled; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][2] = (u8)RANDO_SAVE_CHECKS[i].eligible; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][3] = + (u8)RANDO_SAVE_CHECKS[i].cycleObtained; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][4] = (u8)RANDO_SAVE_CHECKS[i].obtained; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][5] = (u8)RANDO_SAVE_CHECKS[i].skipped; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][6] = RANDO_SAVE_CHECKS[i].price; + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"][i][7] = + RANDO_SAVE_CHECKS[i].multiWorldTeamIndex; + } + } + + SendJsonToRemote(payload); +} + +void Anchor::SendPacket_ClearTeamState(std::string teamId) { + json payload; + payload["type"] = UPDATE_TEAM_STATE; + payload["targetTeamId"] = teamId; + payload["queue"] = json::array(); + payload["state"] = json::object(); + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) { + if (!roomState.syncItemsAndFlags) { + return; + } + + // This can happen in between file select and the game starting, so we cant use this check, but we need to ensure we + // be careful to wrap PlayState usage in this check + // if (!IsSaveLoaded()) { + // return; + // } + + if (payload.contains("state")) { + // Hack to reduce the amount of bytes for this data + if (IS_RANDO && payload["state"]["shipSaveInfo"].contains("rando")) { + auto stuff = + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecksCopy"].get>>(); + for (int i = 0; i < RC_MAX; i++) { + payload["state"]["shipSaveInfo"]["rando"]["randoSaveChecks"][i] = RandoSaveCheck{ + (RandoItemId)stuff[i][0], (bool)stuff[i][1], (bool)stuff[i][2], (bool)stuff[i][3], + (bool)stuff[i][4], (bool)stuff[i][5], (u16)stuff[i][6], (s16)stuff[i][7], + }; + } + } + + Save loadedData = payload["state"].get(); + + // Restore bottle contents (unless it's the Deku Princess) + for (int i = 0; i < 6; i++) { + if (gSaveContext.save.saveInfo.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_NONE && + gSaveContext.save.saveInfo.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_DEKU_PRINCESS) { + loadedData.saveInfo.inventory.items[SLOT_BOTTLE_1 + i] = + gSaveContext.save.saveInfo.inventory.items[SLOT_BOTTLE_1 + i]; + } + } + + // Restore ammo if it's non-zero, unless it's beans + for (int i = 0; i < ARRAY_COUNT(gSaveContext.save.saveInfo.inventory.ammo); i++) { + if (gSaveContext.save.saveInfo.inventory.ammo[i] != 0 && i != SLOT(ITEM_MAGIC_BEANS)) { + loadedData.saveInfo.inventory.ammo[i] = gSaveContext.save.saveInfo.inventory.ammo[i]; + } + } + + // Restore stuff that shouldn't be synced + loadedData.saveInfo.checksum = gSaveContext.save.saveInfo.checksum; + loadedData.shipSaveInfo.fileCreatedAt = gSaveContext.save.shipSaveInfo.fileCreatedAt; + memcpy(loadedData.saveInfo.playerData.newf, gSaveContext.save.saveInfo.playerData.newf, + sizeof(loadedData.saveInfo.playerData.newf)); + memcpy(&loadedData.shipSaveInfo.dpadEquips, &gSaveContext.save.shipSaveInfo.dpadEquips, + sizeof(loadedData.shipSaveInfo.dpadEquips)); + memcpy(loadedData.saveInfo.equips.cButtonSlots, gSaveContext.save.saveInfo.equips.cButtonSlots, + sizeof(loadedData.saveInfo.equips.cButtonSlots)); + memcpy(loadedData.saveInfo.equips.buttonItems, gSaveContext.save.saveInfo.equips.buttonItems, + sizeof(loadedData.saveInfo.equips.buttonItems)); + memcpy(loadedData.saveInfo.playerData.playerName, gSaveContext.save.saveInfo.playerData.playerName, + sizeof(loadedData.saveInfo.playerData.playerName)); + + gSaveContext.save.saveInfo = loadedData.saveInfo; + gSaveContext.save.shipSaveInfo = loadedData.shipSaveInfo; + + Notification::Emit({ + .message = "Save updated from team", + }); + Rando::CheckTracker::OnFileLoad(); + Rando::ActorBehavior::OnFileLoad(); + ShipInit::Init("IS_RANDO"); + } + + if (payload.contains("queue")) { + std::lock_guard lock(incomingPacketQueueMutex); + for (auto& item : payload["queue"]) { + nlohmann::json itemPayload = nlohmann::json::parse(item.get()); + incomingPacketQueue.push(itemPayload); + } + packetsQueuedFromTeamState = true; + } +} diff --git a/mm/2s2h/Network/NetworkMenu.cpp b/mm/2s2h/Network/NetworkMenu.cpp index 763c152266..298369a663 100644 --- a/mm/2s2h/Network/NetworkMenu.cpp +++ b/mm/2s2h/Network/NetworkMenu.cpp @@ -11,7 +11,7 @@ using namespace UIWidgets; void RegisterNetworkMenu() { // Add Network Menu - BenGui::mBenMenu->AddMenuEntry("Network", "gSettings.Menu.NetworkSidebarSection"); + // BenGui::mBenMenu->AddMenuEntry("Network", "gSettings.Menu.NetworkSidebarSection"); WidgetPath path; #ifndef ENABLE_NETWORKING diff --git a/mm/2s2h/Rando/ActorBehavior/EnAkindonuts.cpp b/mm/2s2h/Rando/ActorBehavior/EnAkindonuts.cpp index 95f4b6b2b7..c3b38c7d4b 100644 --- a/mm/2s2h/Rando/ActorBehavior/EnAkindonuts.cpp +++ b/mm/2s2h/Rando/ActorBehavior/EnAkindonuts.cpp @@ -25,7 +25,9 @@ void EnAkindonuts_ReplacePurchaseMessage(RandoCheckId randoCheckId, RandoInf ran auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); entry.msg = "I'll sell you %g{{item}}%w for %r{{rupees}} Rupees%w!\xE0"; - CustomMessage::Replace(&entry.msg, "{{item}}", Rando::StaticData::GetItemName(randoSaveCheck.randoItemId)); + CustomMessage::Replace( + &entry.msg, "{{item}}", + Rando::StaticData::GetItemName(randoSaveCheck.randoItemId, true, randoSaveCheck.multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{{rupees}}", std::to_string(cost)); CustomMessage::LoadCustomMessageIntoFont(entry); @@ -112,7 +114,8 @@ void Rando::ActorBehavior::InitEnAkindonutsBehavior() { CustomMessage::Replace( &entry.msg, "{{item}}", - Rando::StaticData::GetItemName(RANDO_SAVE_CHECKS[RC_SOUTHERN_SWAMP_SCRUB_BEANS].randoItemId)); + Rando::StaticData::GetItemName(RANDO_SAVE_CHECKS[RC_SOUTHERN_SWAMP_SCRUB_BEANS].randoItemId, true, + RANDO_SAVE_CHECKS[RC_SOUTHERN_SWAMP_SCRUB_BEANS].multiWorldTeamIndex)); CustomMessage::LoadCustomMessageIntoFont(entry); *loadFromMessageTable = false; }); diff --git a/mm/2s2h/Rando/ActorBehavior/EnBal.cpp b/mm/2s2h/Rando/ActorBehavior/EnBal.cpp index 277e71e159..cd7cf74b0d 100644 --- a/mm/2s2h/Rando/ActorBehavior/EnBal.cpp +++ b/mm/2s2h/Rando/ActorBehavior/EnBal.cpp @@ -30,9 +30,11 @@ void OnOpenShopText(u16* textId, bool* loadFromMessageTable) { "\x02No thanks"; CustomMessage::Replace(&entry.msg, "{item1}", - Rando::StaticData::GetItemName(RANDO_SAVE_CHECKS[randoCheckId1].randoItemId, false)); + Rando::StaticData::GetItemName(RANDO_SAVE_CHECKS[randoCheckId1].randoItemId, false, + RANDO_SAVE_CHECKS[randoCheckId1].multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{item2}", - Rando::StaticData::GetItemName(RANDO_SAVE_CHECKS[randoCheckId2].randoItemId, false)); + Rando::StaticData::GetItemName(RANDO_SAVE_CHECKS[randoCheckId2].randoItemId, false, + RANDO_SAVE_CHECKS[randoCheckId2].multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{price1}", std::to_string(RANDO_SAVE_CHECKS[randoCheckId1].price)); CustomMessage::Replace(&entry.msg, "{price2}", std::to_string(RANDO_SAVE_CHECKS[randoCheckId2].price)); CustomMessage::EnsureMessageEnd(&entry.msg); diff --git a/mm/2s2h/Rando/ActorBehavior/EnGirlA.cpp b/mm/2s2h/Rando/ActorBehavior/EnGirlA.cpp index 6ee339972f..8e20b6cb4b 100644 --- a/mm/2s2h/Rando/ActorBehavior/EnGirlA.cpp +++ b/mm/2s2h/Rando/ActorBehavior/EnGirlA.cpp @@ -2,6 +2,7 @@ #include #include "2s2h/CustomMessage/CustomMessage.h" #include "2s2h/Rando/MiscBehavior/Traps.h" +#include "2s2h/Network/Anchor/Anchor.h" extern "C" { #include "variables.h" @@ -67,11 +68,23 @@ void EnGirlA_RandoBuyFunc(PlayState* play, EnGirlA* enGirlA) { auto& randoSaveCheck = RANDO_SAVE_CHECKS[enGirlA->actor.world.rot.z]; RandoItemId randoItemId = Rando::ConvertItem(randoSaveCheck.randoItemId, (RandoCheckId)enGirlA->actor.world.rot.z); randoSaveCheck.obtained = true; + randoSaveCheck.cycleObtained = true; Rupees_ChangeBy(-play->msgCtx.unk1206C); - if (randoItemId == RI_TRAP) { - RollTrapType(); + + bool isForYou = Anchor::Instance->roomState.teams.size() < 2 || + Anchor::Instance->roomState.teams[randoSaveCheck.multiWorldTeamIndex] == + std::string(CVarGetString("gNetwork.Anchor.TeamId", "default")); + Anchor::Instance->SendPacket_SetCheckStatus((RandoCheckId)enGirlA->actor.world.rot.z); + if (isForYou) { + if (randoItemId == RI_TRAP) { + RollTrapType(); + } + Rando::GiveItem(randoItemId); + Anchor::Instance->SendPacket_GiveItem(1, randoItemId); + } else { + Anchor::Instance->SendPacket_GiveItem(1, randoItemId, + Anchor::Instance->roomState.teams[randoSaveCheck.multiWorldTeamIndex]); } - Rando::GiveItem(randoItemId); } void EnGirlA_RandoBuyFanfareFunc(PlayState* play, EnGirlA* enGirlA) { @@ -284,7 +297,7 @@ void Rando::ActorBehavior::InitEnGirlABehavior() { return; } - auto randoSaveCheck = RANDO_SAVE_CHECKS[randoCheckId]; + auto& randoSaveCheck = RANDO_SAVE_CHECKS[randoCheckId]; auto randoStaticItem = Rando::StaticData::Items[randoSaveCheck.randoItemId]; auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); @@ -292,7 +305,9 @@ void Rando::ActorBehavior::InitEnGirlABehavior() { entry.autoFormat = false; entry.msg = "\x01{{itemName}}: {{rupees}} Rupees\x11\x00"; entry.msg += '\x00'; - CustomMessage::Replace(&entry.msg, "{{itemName}}", randoStaticItem.name); + CustomMessage::Replace( + &entry.msg, "{{itemName}}", + Rando::StaticData::GetItemName(randoSaveCheck.randoItemId, false, randoSaveCheck.multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{{rupees}}", std::to_string(randoSaveCheck.price)); if (!Rando::IsItemObtainable(randoSaveCheck.randoItemId, randoCheckId) && randoSaveCheck.obtained) { @@ -314,7 +329,7 @@ void Rando::ActorBehavior::InitEnGirlABehavior() { return; } - auto randoSaveCheck = RANDO_SAVE_CHECKS[randoCheckId]; + auto& randoSaveCheck = RANDO_SAVE_CHECKS[randoCheckId]; auto randoStaticItem = Rando::StaticData::Items[randoSaveCheck.randoItemId]; auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); @@ -322,7 +337,9 @@ void Rando::ActorBehavior::InitEnGirlABehavior() { entry.autoFormat = false; entry.firstItemCost = randoSaveCheck.price; entry.msg = "\x01{{itemName}}: {{rupees}} Rupees\x02\x11\xC2I'll buy it\x11No thanks\xBF"; - CustomMessage::Replace(&entry.msg, "{{itemName}}", randoStaticItem.name); + CustomMessage::Replace( + &entry.msg, "{{itemName}}", + Rando::StaticData::GetItemName(randoSaveCheck.randoItemId, false, randoSaveCheck.multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{{rupees}}", std::to_string(randoSaveCheck.price)); CustomMessage::LoadCustomMessageIntoFont(entry); diff --git a/mm/2s2h/Rando/ActorBehavior/EnGs.cpp b/mm/2s2h/Rando/ActorBehavior/EnGs.cpp index 07f75b1a9f..e8e2bad83f 100644 --- a/mm/2s2h/Rando/ActorBehavior/EnGs.cpp +++ b/mm/2s2h/Rando/ActorBehavior/EnGs.cpp @@ -94,7 +94,9 @@ void Rando::ActorBehavior::InitEnGsBehavior() { entry.msg = "They say %g{{item}}%w is hidden at %y{{location}}%w."; - CustomMessage::Replace(&entry.msg, "{{item}}", Rando::StaticData::GetItemName(saveCheck.randoItemId)); + CustomMessage::Replace( + &entry.msg, "{{item}}", + Rando::StaticData::GetItemName(saveCheck.randoItemId, true, saveCheck.multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{{location}}", Ship_GetSceneName(Rando::StaticData::Checks[randoCheckId].sceneId)); @@ -142,8 +144,9 @@ void Rando::ActorBehavior::InitEnGsBehavior() { entry.msg = "Wise choice... They say %g{{item}}%w is hidden at %y{{location}}%w."; - CustomMessage::Replace(&entry.msg, "{{item}}", - Rando::StaticData::GetItemName(saveCheck.randoItemId)); + CustomMessage::Replace( + &entry.msg, "{{item}}", + Rando::StaticData::GetItemName(saveCheck.randoItemId, true, saveCheck.multiWorldTeamIndex)); CustomMessage::Replace(&entry.msg, "{{location}}", Ship_GetSceneName(Rando::StaticData::Checks[randoCheckId].sceneId)); diff --git a/mm/2s2h/Rando/ActorBehavior/EnItem00.cpp b/mm/2s2h/Rando/ActorBehavior/EnItem00.cpp index 4daa74bb1a..6580f65348 100644 --- a/mm/2s2h/Rando/ActorBehavior/EnItem00.cpp +++ b/mm/2s2h/Rando/ActorBehavior/EnItem00.cpp @@ -105,7 +105,7 @@ void Rando::ActorBehavior::InitEnItem00Behavior() { auto randoSaveCheck = RANDO_SAVE_CHECKS[randoStaticCheck.randoCheckId]; - if (!randoSaveCheck.shuffled || randoSaveCheck.cycleObtained) { + if (!randoSaveCheck.shuffled) { return; } diff --git a/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp b/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp index 2411b43d4e..b5be0cbff6 100644 --- a/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp +++ b/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp @@ -5,6 +5,7 @@ #include "2s2h/BenGui/UIWidgets.hpp" #include "2s2h/Rando/StaticData/StaticData.h" #include +#include "2s2h/Network/Anchor/Anchor.h" // Image Icons #include "assets/2s2h_assets.h" @@ -267,6 +268,7 @@ void CheckTrackerDrawLogicalList() { : IM_COL32(255, 255, 255, 0)); if (ImGui::IsItemClicked()) { randoSaveCheck.skipped = !randoSaveCheck.skipped; + Anchor::Instance->SendPacket_SetCheckStatus(checkId); } ImGui::TableNextColumn(); } @@ -447,6 +449,7 @@ void CheckTrackerDrawNonLogicalList() { : IM_COL32(255, 255, 255, 0)); if (ImGui::IsItemClicked()) { randoSaveCheck.skipped = !randoSaveCheck.skipped; + Anchor::Instance->SendPacket_SetCheckStatus(randoCheckId); } ImGui::TableNextColumn(); } diff --git a/mm/2s2h/Rando/ConvertItem.cpp b/mm/2s2h/Rando/ConvertItem.cpp index 26ba83a947..cd077fe962 100644 --- a/mm/2s2h/Rando/ConvertItem.cpp +++ b/mm/2s2h/Rando/ConvertItem.cpp @@ -1,6 +1,7 @@ #include "Rando/Rando.h" #include "2s2h/ShipUtils.h" #include +#include "2s2h/Network/Anchor/Anchor.h" // Copied from z_player.c, we could instead move this to a header file, idk typedef struct GetItemEntry { @@ -101,6 +102,13 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId) hasObtainedCheck = RANDO_SAVE_CHECKS[randoCheckId].obtained; } + bool isForYou = randoCheckId == RC_UNKNOWN || Anchor::Instance->roomState.teams.size() < 2 || + Anchor::Instance->roomState.teams[RANDO_SAVE_CHECKS[randoCheckId].multiWorldTeamIndex] == + std::string(CVarGetString("gNetwork.Anchor.TeamId", "default")); + if (!isForYou) { + return !hasObtainedCheck; + } + u8 vanillaCantObtain = false; if (Rando::StaticData::Items[randoItemId].itemId != ITEM_NONE && Rando::StaticData::Items[randoItemId].getItemId != GI_NONE) { @@ -469,6 +477,17 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId) } RandoItemId Rando::ConvertItem(RandoItemId randoItemId, RandoCheckId randoCheckId) { + bool isForYou = randoCheckId == RC_UNKNOWN || Anchor::Instance->roomState.teams.size() < 2 || + Anchor::Instance->roomState.teams[RANDO_SAVE_CHECKS[randoCheckId].multiWorldTeamIndex] == + std::string(CVarGetString("gNetwork.Anchor.TeamId", "default")); + if (!isForYou) { + if (IsItemObtainable(randoItemId, randoCheckId)) { + return randoItemId; + } else { + return RI_JUNK; + } + } + if (IsItemObtainable(randoItemId, randoCheckId)) { switch (randoItemId) { case RI_PROGRESSIVE_BOMB_BAG: diff --git a/mm/2s2h/Rando/DrawItem.cpp b/mm/2s2h/Rando/DrawItem.cpp index fa96c72dbd..3c5289827b 100644 --- a/mm/2s2h/Rando/DrawItem.cpp +++ b/mm/2s2h/Rando/DrawItem.cpp @@ -498,12 +498,22 @@ void Rando::DrawItem(RandoItemId randoItemId, Actor* actor) { DrawOwlStatue(); break; case RI_PROGRESSIVE_LULLABY: + Rando::DrawItem(RI_SONG_LULLABY, actor); + break; case RI_PROGRESSIVE_MAGIC: + Rando::DrawItem(RI_SINGLE_MAGIC, actor); + break; case RI_PROGRESSIVE_BOW: + Rando::DrawItem(RI_BOW, actor); + break; case RI_PROGRESSIVE_BOMB_BAG: + Rando::DrawItem(RI_BOMB_BAG_20, actor); + break; case RI_PROGRESSIVE_SWORD: + Rando::DrawItem(RI_SWORD_KOKIRI, actor); + break; case RI_PROGRESSIVE_WALLET: - Rando::DrawItem(Rando::ConvertItem(randoItemId), actor); + Rando::DrawItem(RI_WALLET_ADULT, actor); break; case RI_SOUL_GOHT: DrawGoht(); diff --git a/mm/2s2h/Rando/GiveItem.cpp b/mm/2s2h/Rando/GiveItem.cpp index 5d404db23d..3c4c66cf0b 100644 --- a/mm/2s2h/Rando/GiveItem.cpp +++ b/mm/2s2h/Rando/GiveItem.cpp @@ -1,5 +1,6 @@ #include "Rando/Rando.h" #include "Rando/MiscBehavior/MiscBehavior.h" +#include "2s2h/Network/Anchor/Anchor.h" extern "C" { #include "variables.h" @@ -119,6 +120,8 @@ void Rando::GiveItem(RandoItemId randoItemId) { .cutsceneIndex = 0xFFF7, .transitionTrigger = TRANS_TRIGGER_START, .transitionType = TRANS_TYPE_FADE_BLACK }); + Anchor::Instance->SendPacket_GameComplete(); + Anchor::Instance->ReleaseWorldForCurrentTeam(); } break; // Technically these should never be used, but leaving them here just in case diff --git a/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp b/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp index de35190761..1bf02f9893 100644 --- a/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp +++ b/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp @@ -7,6 +7,7 @@ #include "2s2h/Rando/StaticData/StaticData.h" #include "2s2h/ShipUtils.h" #include "Traps.h" +#include "2s2h/Network/Anchor/Anchor.h" extern "C" { #include "variables.h" @@ -47,10 +48,16 @@ void Rando::MiscBehavior::CheckQueue() { RandoItemId randoItemId = Rando::ConvertItem(randoSaveCheck.randoItemId, (RandoCheckId)CUSTOM_ITEM_PARAM); std::string prefix = "You found"; - std::string message = Rando::StaticData::GetItemName(randoItemId); + std::string message = + Rando::StaticData::GetItemName(randoItemId, true, randoSaveCheck.multiWorldTeamIndex); - if (randoItemId == RI_JUNK) { - randoItemId = Rando::CurrentJunkItem(); + bool isForYou = Anchor::Instance->roomState.teams.size() < 2 || + Anchor::Instance->roomState.teams[randoSaveCheck.multiWorldTeamIndex] == + std::string(CVarGetString("gNetwork.Anchor.TeamId", "default")); + if (isForYou) { + if (randoItemId == RI_JUNK) { + randoItemId = Rando::CurrentJunkItem(); + } } if (randoItemId == RI_TRIFORCE_PIECE) { if (gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces + 1 >= @@ -90,10 +97,17 @@ void Rando::MiscBehavior::CheckQueue() { }); } } - Rando::GiveItem(randoItemId); randoSaveCheck.cycleObtained = true; randoSaveCheck.obtained = true; randoSaveCheck.eligible = false; + Anchor::Instance->SendPacket_SetCheckStatus((RandoCheckId)CUSTOM_ITEM_PARAM); + if (isForYou) { + Rando::GiveItem(randoItemId); + Anchor::Instance->SendPacket_GiveItem(1, randoItemId); + } else { + Anchor::Instance->SendPacket_GiveItem( + 1, randoItemId, Anchor::Instance->roomState.teams[randoSaveCheck.multiWorldTeamIndex]); + } queued = false; CUSTOM_ITEM_PARAM = randoItemId; }, diff --git a/mm/2s2h/Rando/StaticData/Items.cpp b/mm/2s2h/Rando/StaticData/Items.cpp index 8afe1c06d1..da3a576f3d 100644 --- a/mm/2s2h/Rando/StaticData/Items.cpp +++ b/mm/2s2h/Rando/StaticData/Items.cpp @@ -3,6 +3,7 @@ #include "2s2h/ShipUtils.h" #include "2s2h/Rando/Rando.h" #include "2s2h_assets.h" +#include "2s2h/Network/Anchor/Anchor.h" extern "C" { extern s16 D_801CFF94[250]; @@ -387,17 +388,23 @@ bool ShouldShowGetItemCutscene(RandoItemId itemId) { } } -std::string GetItemName(RandoItemId randoItemId, bool includeArticle) { +std::string GetItemName(RandoItemId randoItemId, bool includeArticle, s16 multiWorldTeamIndex) { std::string result; - if (includeArticle && !Ship_IsCStringEmpty(Rando::StaticData::Items[randoItemId].article)) { + bool isForYou = multiWorldTeamIndex == -1 || Anchor::Instance->roomState.teams.size() < 2 || + Anchor::Instance->roomState.teams[multiWorldTeamIndex] == + std::string(CVarGetString("gNetwork.Anchor.TeamId", "default")); + + if (!isForYou) { + result += Anchor::Instance->roomState.teams[multiWorldTeamIndex] + "'s "; + } else if (includeArticle && !Ship_IsCStringEmpty(Rando::StaticData::Items[randoItemId].article)) { result += Rando::StaticData::Items[randoItemId].article; result += " "; } result += Rando::StaticData::Items[randoItemId].name; - if (randoItemId == RI_JUNK) { + if (randoItemId == RI_JUNK && isForYou) { result += std::string(" (") + Rando::StaticData::Items[Rando::CurrentJunkItem()].name + ")"; } diff --git a/mm/2s2h/Rando/StaticData/StaticData.h b/mm/2s2h/Rando/StaticData/StaticData.h index 858c437164..c6012aed74 100644 --- a/mm/2s2h/Rando/StaticData/StaticData.h +++ b/mm/2s2h/Rando/StaticData/StaticData.h @@ -52,7 +52,7 @@ RandoItemId GetItemIdFromName(const char* name); u8 GetIconForZMessage(RandoItemId itemId); const char* GetIconTexturePath(RandoItemId itemId); bool ShouldShowGetItemCutscene(RandoItemId itemId); -std::string GetItemName(RandoItemId randoItemId, bool includeArticle = true); +std::string GetItemName(RandoItemId randoItemId, bool includeArticle = true, s16 multiWorldTeamIndex = -1); std::string GetTrapMessage(); struct RandoStaticOption { diff --git a/mm/include/z64save.h b/mm/include/z64save.h index 29a2c899a3..acc4f47896 100644 --- a/mm/include/z64save.h +++ b/mm/include/z64save.h @@ -33,8 +33,8 @@ typedef enum RespawnMode { /* 8 */ RESPAWN_MODE_MAX } RespawnMode; -// 2S2H [Port] Quadruple the size of the Save Buffer to support more data, eg rando -#define SAVE_BUFFER_SIZE 0x4000 * 4 +// 2S2H [Port] Increase the size of the Save Buffer to support more data, eg rando +#define SAVE_BUFFER_SIZE 0x4000 * 8 #define SAVE_BUFFER_SIZE_HALF (SAVE_BUFFER_SIZE / 2) // 2S2H [Enhancement] Extended for file 3 support @@ -377,6 +377,7 @@ typedef struct RandoSaveCheck { bool obtained; bool skipped; u16 price; // Only applicable for shops/merchants + s16 multiWorldTeamIndex; } RandoSaveCheck; typedef struct RandoSaveInfo { diff --git a/mm/src/code/z_actor.c b/mm/src/code/z_actor.c index 6fed8a8753..5ad480655e 100644 --- a/mm/src/code/z_actor.c +++ b/mm/src/code/z_actor.c @@ -2491,6 +2491,10 @@ void Player_PlaySfx(Player* player, u16 sfxId) { &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); } } + + if (player->actor.id == ACTOR_PLAYER) { + GameInteractor_ExecuteOnPlayerSfx(sfxId); + } } /** @@ -2683,6 +2687,7 @@ void Actor_SpawnSetupActors(PlayState* play, ActorContext* actorCtx) { // Prevents re-spawning the setup actors play->numSetupActors = -play->numSetupActors; + GameInteractor_ExecuteOnSceneSpawnActors(); } } diff --git a/mm/src/code/z_sram_NES.c b/mm/src/code/z_sram_NES.c index 03c3c301ee..0b81536bd2 100644 --- a/mm/src/code/z_sram_NES.c +++ b/mm/src/code/z_sram_NES.c @@ -2098,6 +2098,7 @@ void Sram_StartWriteToFlashOwlSave(SramContext* sramCtx) { sramCtx->startWriteOsTime = osGetTime(); sramCtx->status = 7; + GameInteractor_ExecuteAfterOwlSave(); } void Sram_UpdateWriteToFlashOwlSave(SramContext* sramCtx) {