diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 02931a1491..fbcc413f6d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -214,7 +214,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 -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) @@ -257,6 +257,7 @@ jobs: SDL2-2.30.3 tinyxml2-10.0.0 libzip-1.10.1 + SDL2_net-2.2.0 - name: Install latest SDL run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" @@ -283,6 +284,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" @@ -305,7 +318,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) @@ -365,7 +378,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 921ac70b6e..cb85cd5547 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,12 +22,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/BenGui/BenMenuBar.cpp b/mm/2s2h/BenGui/BenMenuBar.cpp index 081d139558..f08ec2f864 100644 --- a/mm/2s2h/BenGui/BenMenuBar.cpp +++ b/mm/2s2h/BenGui/BenMenuBar.cpp @@ -10,6 +10,9 @@ #include "2s2h/Enhancements/Enhancements.h" #include "2s2h/DeveloperTools/DeveloperTools.h" #include "HudEditor.h" +#ifdef ENABLE_NETWORKING +#include "2s2h/Network/Sail.h" +#endif extern "C" { #include "z64.h" @@ -876,6 +879,15 @@ void DrawDeveloperToolsMenu() { } } +#ifdef ENABLE_NETWORKING +void DrawRemoteControlMenu() { + if (UIWidgets::BeginMenu("Network")) { + Sail::Instance->DrawMenu(); + ImGui::EndMenu(); + } +} +#endif + void BenMenuBar::InitElement() { UpdateWindowBackendObjects(); } @@ -909,6 +921,12 @@ void BenMenuBar::DrawElement() { ImGui::SetCursorPosY(0.0f); +#ifdef ENABLE_NETWORKING + DrawRemoteControlMenu(); + + ImGui::SetCursorPosY(0.0f); +#endif + ImGui::PopStyleVar(1); ImGui::EndMenuBar(); } diff --git a/mm/2s2h/BenGui/SearchableMenuItems.h b/mm/2s2h/BenGui/SearchableMenuItems.h index 682b1fd1e6..982e2355b7 100644 --- a/mm/2s2h/BenGui/SearchableMenuItems.h +++ b/mm/2s2h/BenGui/SearchableMenuItems.h @@ -6,6 +6,8 @@ #include "variables.h" #include #include +#include "ShipUtils.h" +#include "2s2h/Network/Sail.h" extern "C" { #include "functions.h" @@ -43,7 +45,9 @@ typedef enum { DISABLE_FOR_MOTION_BLUR_MODE, DISABLE_FOR_MOTION_BLUR_OFF, DISABLE_FOR_FRAME_ADVANCE_OFF, - DISABLE_FOR_WARP_POINT_NOT_SET + DISABLE_FOR_WARP_POINT_NOT_SET, + DISABLE_FOR_SAIL_FORM_INVALID, + DISABLE_FOR_SAIL_ENABLED, } DisableOption; struct widgetInfo; @@ -67,6 +71,8 @@ typedef enum { WIDGET_CVAR_COMBOBOX, WIDGET_CVAR_SLIDER_INT, WIDGET_CVAR_SLIDER_FLOAT, + WIDGET_CVAR_INPUT_TEXT, + WIDGET_CVAR_INPUT_INT, WIDGET_BUTTON, WIDGET_COLOR_24, // color picker without alpha WIDGET_COLOR_32, // color picker with alpha @@ -327,7 +333,16 @@ static std::map disabledMap = { "Frame Advance is Disabled" } }, { DISABLE_FOR_WARP_POINT_NOT_SET, { [](disabledInfo& info) -> bool { return !CVarGetInteger(WARP_POINT_CVAR "Saved", 0); }, - "Warp Point Not Saved" } } + "Warp Point Not Saved" } }, + { DISABLE_FOR_SAIL_FORM_INVALID, + { [](disabledInfo& info) -> bool { + return !(!isStringEmpty(CVarGetString("gNetwork.Sail.Host", "127.0.0.1")) && + CVarGetInteger("gNetwork.Sail.Port", 43384) > 1024 && + CVarGetInteger("gNetwork.Sail.Port", 43384) < 65535); + }, + "Invalid Host/Port" } }, + { DISABLE_FOR_SAIL_ENABLED, + { [](disabledInfo& info) -> bool { return Sail::Instance->isEnabled; }, "Sail is Enabled" } }, }; std::unordered_map menuThemeOptions = { @@ -692,6 +707,78 @@ void AddSettings() { WIDGET_WINDOW_BUTTON, { .size = UIWidgets::Sizes::Inline, .windowName = "2S2H Input Editor" } } } } }); +#ifdef ENABLE_NETWORKING + // Network + settingsSidebar.push_back( + { "Network", + 3, + { { + { .widgetName = "Sail", .widgetType = WIDGET_SEPARATOR_TEXT }, + { "Host", + "gNetwork.Sail.Host", + "", + WIDGET_CVAR_INPUT_TEXT, + { .defaultVariant = "127.0.0.1" }, + {}, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_SAIL_ENABLED).active) { + info.activeDisables.push_back(DISABLE_FOR_SAIL_ENABLED); + } + } }, + { "Port", + "gNetwork.Sail.Port", + "", + WIDGET_CVAR_INPUT_INT, + { .defaultVariant = 43384 }, + {}, + [](widgetInfo& info) { + if (disabledMap.at(DISABLE_FOR_SAIL_ENABLED).active) { + info.activeDisables.push_back(DISABLE_FOR_SAIL_ENABLED); + } + } }, + { "Connect", + "", + "Connect/Disconnect to the Sail server.", + WIDGET_BUTTON, + {}, + [](widgetInfo& info) { + if (Sail::Instance->isEnabled) { + CVarClear("gNetwork.Sail.Enabled"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Sail::Instance->Disable(); + } else { + CVarSetInteger("gNetwork.Sail.Enabled", 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Sail::Instance->Enable(); + } + }, + [](widgetInfo& info) { + if (Sail::Instance->isEnabled) { + info.widgetName = "Disconnect"; + } else { + info.widgetName = "Connect"; + } + if (disabledMap.at(DISABLE_FOR_SAIL_FORM_INVALID).active) + info.activeDisables.push_back(DISABLE_FOR_SAIL_FORM_INVALID); + } }, + { "Connected", + "", + "Displays the current connection status.", + WIDGET_TEXT, + {}, + {}, + [](widgetInfo& info) { + if (Sail::Instance->isEnabled && Sail::Instance->isConnected) { + info.widgetName = "Connected"; + } else if (Sail::Instance->isEnabled) { + info.widgetName = "Connecting..."; + } else { + info.isHidden = true; + } + } }, + } } }); +#endif + if (CVarGetInteger("gSettings.SidebarSearch", 0)) { settingsSidebar.insert(settingsSidebar.begin() + searchSidebarIndex, searchSidebarEntry); } @@ -1575,6 +1662,43 @@ void SearchMenuGetItem(widgetInfo& widget) { } } break; + case WIDGET_CVAR_INPUT_TEXT: { + ImGui::BeginDisabled(disabledValue); + UIWidgets::PushStyleSlider(menuTheme[menuThemeIndex]); + ImGui::PushItemWidth(ImGui::GetWindowWidth()); + ImGui::Text(widget.widgetName.c_str()); + std::string cvarValue = + CVarGetString(widget.widgetCVar, std::get(widget.widgetOptions.defaultVariant)); + if (ImGui::InputText(("##" + widget.widgetName).c_str(), (char*)cvarValue.c_str(), + cvarValue.capacity() + 1, 0)) { + CVarSetString(widget.widgetCVar, cvarValue.c_str()); + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + ImGui::PopItemWidth(); + UIWidgets::PopStyleSlider(); + ImGui::EndDisabled(); + } break; + case WIDGET_CVAR_INPUT_INT: { + ImGui::BeginDisabled(disabledValue); + UIWidgets::PushStyleSlider(menuTheme[menuThemeIndex]); + ImGui::PushItemWidth(ImGui::GetWindowWidth()); + ImGui::Text(widget.widgetName.c_str()); + int32_t cvarValue = + CVarGetInteger(widget.widgetCVar, std::get(widget.widgetOptions.defaultVariant)); + if (ImGui::InputInt(("##" + widget.widgetName).c_str(), &cvarValue, NULL, NULL)) { + CVarSetInteger(widget.widgetCVar, cvarValue); + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + ImGui::PopItemWidth(); + UIWidgets::PopStyleSlider(); + ImGui::EndDisabled(); + } break; case WIDGET_SLIDER_INT: { int32_t* pointer = std::get(widget.widgetOptions.valuePointer); if (pointer == nullptr) { diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index 0c3c9dda86..f7c8f9d447 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -41,9 +41,9 @@ //#include #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" -#ifdef ENABLE_CROWD_CONTROL -#include "Enhancements/crowd-control/CrowdControl.h" -CrowdControl* CrowdControl::Instance; +#ifdef ENABLE_NETWORKING +#include "2s2h/Network/Sail.h" +Sail* Sail::Instance; #endif #include @@ -512,6 +512,10 @@ extern "C" void InitOTR() { OTRGlobals::Instance = new OTRGlobals(); GameInteractor::Instance = new GameInteractor(); +#ifdef ENABLE_NETWORKING + Sail::Instance = new Sail(); +#endif + BenGui::SetupGuiElements(); InitEnhancements(); InitDeveloperTools(); @@ -533,13 +537,10 @@ 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(); + if (CVarGetInteger("gNetwork.Sail.Enabled", 0)) { + Sail::Instance->Enable(); } #endif @@ -553,9 +554,11 @@ extern "C" void SaveManager_ThreadPoolWait() { extern "C" void DeinitOTR() { SaveManager_ThreadPoolWait(); OTRAudio_Exit(); -#ifdef ENABLE_CROWD_CONTROL - CrowdControl::Instance->Disable(); - CrowdControl::Instance->Shutdown(); +#ifdef ENABLE_NETWORKING + if (CVarGetInteger("gNetwork.Sail.Enabled", 0)) { + Sail::Instance->Disable(); + } + 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..cd6cc42481 --- /dev/null +++ b/mm/2s2h/Network/Network.cpp @@ -0,0 +1,145 @@ +#ifdef ENABLE_NETWORKING + +#include "Network.h" +#include +#include + +// MARK: - Public + +void Network::Enable(const char* host, uint16_t port) { + 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); +} + +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::SendDataToRemote(const char* payload) { + SPDLOG_DEBUG("[Network] Sending data: {}", payload); + SDLNet_TCP_Send(networkSocket, payload, strlen(payload) + 1); +} + +void Network::SendJsonToRemote(nlohmann::json payload) { + SendDataToRemote(payload.dump().c_str()); +} + +// MARK: - Private + +void Network::ReceiveFromServer() { + while (isEnabled) { + while (!isConnected && isEnabled) { + SPDLOG_TRACE("[Network] Attempting to make connection to server..."); + networkSocket = SDLNet_TCP_Open(&networkAddress); + + if (networkSocket) { + isConnected = true; + 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; + } + + 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 (isConnected) { + SDLNet_TCP_Close(networkSocket); + isConnected = false; + OnDisconnected(); + SPDLOG_INFO("[Network] Ending receiving thread..."); + } + } +} + +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); +} + +#endif diff --git a/mm/2s2h/Network/Network.h b/mm/2s2h/Network/Network.h new file mode 100644 index 0000000000..06010fe361 --- /dev/null +++ b/mm/2s2h/Network/Network.h @@ -0,0 +1,50 @@ +#ifdef ENABLE_NETWORKING +#ifndef NETWORK_H +#define NETWORK_H +#ifdef __cplusplus + +#include +#include +#include + +class Network { + private: + IPaddress networkAddress; + TCPsocket networkSocket; + 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(); + void SendDataToRemote(const char* payload); + void SendJsonToRemote(nlohmann::json packet); +}; + +#endif // __cplusplus +#endif // NETWORK_H +#endif // ENABLE_NETWORKING diff --git a/mm/2s2h/Network/Sail.cpp b/mm/2s2h/Network/Sail.cpp new file mode 100644 index 0000000000..ef99d6d9a6 --- /dev/null +++ b/mm/2s2h/Network/Sail.cpp @@ -0,0 +1,292 @@ +#ifdef ENABLE_NETWORKING + +#include "Sail.h" +#include +#include +#include "2s2h/Enhancements/GameInteractor/GameInteractor.h" +#include "2s2h/BenGui/UIWidgets.hpp" +#include "ShipUtils.h" + +void Sail::Enable() { + Network::Enable(CVarGetString("gNetwork.Sail.Host", "127.0.0.1"), CVarGetInteger("gNetwork.Sail.Port", 43384)); +} + +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 != "apply" && effectType != "remove") { + SPDLOG_ERROR("[Sail] Received effect payload with unknown effect type: {}", effectType); + SendJsonToRemote(responsePayload); + return; + } + + responsePayload["status"] = "success"; + SendJsonToRemote(responsePayload); + } 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() { + RegisterHooks(); +} + +void Sail::OnDisconnected() { + RegisterHooks(); +} + +void Sail::RegisterHooks() { + static HOOK_ID onSceneInitHook = 0; + static HOOK_ID onItemGiveHook = 0; + static HOOK_ID onActorInitHook = 0; + static HOOK_ID onFlagSetHook = 0; + static HOOK_ID onFlagUnsetHook = 0; + static HOOK_ID onSceneFlagSetHook = 0; + static HOOK_ID onSceneFlagUnsetHook = 0; + + GameInteractor::Instance->UnregisterGameHook(onSceneInitHook); + GameInteractor::Instance->UnregisterGameHook(onItemGiveHook); + GameInteractor::Instance->UnregisterGameHook(onActorInitHook); + GameInteractor::Instance->UnregisterGameHook(onFlagSetHook); + GameInteractor::Instance->UnregisterGameHook(onFlagUnsetHook); + GameInteractor::Instance->UnregisterGameHook(onSceneFlagSetHook); + GameInteractor::Instance->UnregisterGameHook(onSceneFlagUnsetHook); + + if (!isConnected) { + return; + } + + onSceneInitHook = + GameInteractor::Instance->RegisterGameHook([&](s8 sceneId, s8 spawnNum) { + if (!isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnSceneInit"; + payload["hook"]["sceneNum"] = sceneId; + + SendJsonToRemote(payload); + }); + onItemGiveHook = GameInteractor::Instance->RegisterGameHook([&](u8 item) { + if (!isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnItemGive"; + payload["hook"]["itemId"] = item; + + SendJsonToRemote(payload); + }); + onActorInitHook = GameInteractor::Instance->RegisterGameHook([&](void* refActor) { + if (!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; + + SendJsonToRemote(payload); + }); + onFlagSetHook = + GameInteractor::Instance->RegisterGameHook([&](int16_t flagType, int16_t flag) { + if (!isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnFlagSet"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + + SendJsonToRemote(payload); + }); + onFlagUnsetHook = + GameInteractor::Instance->RegisterGameHook([&](int16_t flagType, int16_t flag) { + if (!isConnected) + return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnFlagUnset"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + + SendJsonToRemote(payload); + }); + onSceneFlagSetHook = GameInteractor::Instance->RegisterGameHook( + [&](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!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; + + SendJsonToRemote(payload); + }); + onSceneFlagUnsetHook = GameInteractor::Instance->RegisterGameHook( + [&](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!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; + + SendJsonToRemote(payload); + }); +} + +void Sail::DrawMenu() { + ImGui::SeparatorText("Sail"); + UIWidgets::Tooltip("Sail is a networking protocol designed to facilitate remote " + "control of the Ship of Harkinian 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 the question mark to copy the link to the Sail Github " + "page to your clipboard."); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText("https://github.com/HarbourMasters/sail"); + } + + static std::string ip = CVarGetString("gNetwork.Sail.Host", "127.0.0.1"); + static uint16_t port = CVarGetInteger("gNetwork.Sail.Port", 43384); + bool isFormValid = !isStringEmpty(CVarGetString("gNetwork.Sail.Host", "127.0.0.1")) && port > 1024 && port < 65535; + + ImGui::BeginDisabled(isEnabled); + + ImGui::Text("Remote IP & Port"); + UIWidgets::PushStyleSlider(); + if (ImGui::InputText("##gNetwork.Sail.Host", (char*)ip.c_str(), ip.capacity() + 1)) { + CVarSetString("gNetwork.Sail.Host", ip.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + + ImGui::SameLine(); + ImGui::PushItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##gNetwork.Sail.Port", ImGuiDataType_U16, &port)) { + CVarSetInteger("gNetwork.Sail.Port", port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + UIWidgets::PopStyleSlider(); + + ImGui::PopItemWidth(); + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = isEnabled ? "Disable" : "Enable"; + if (UIWidgets::Button(buttonLabel)) { + if (isEnabled) { + CVarClear("gNetwork.Sail.Enabled"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Sail::Instance->Disable(); + } else { + CVarSetInteger("gNetwork.Sail.Enabled", 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Sail::Instance->Enable(); + } + } + ImGui::EndDisabled(); + + if (isEnabled) { + ImGui::Spacing(); + if (isConnected) { + ImGui::Text("Connected"); + } else { + ImGui::Text("Connecting..."); + } + } +} + +#endif diff --git a/mm/2s2h/Network/Sail.h b/mm/2s2h/Network/Sail.h new file mode 100644 index 0000000000..3dd46875d9 --- /dev/null +++ b/mm/2s2h/Network/Sail.h @@ -0,0 +1,24 @@ +#ifdef ENABLE_NETWORKING +#ifndef NETWORK_SAIL_H +#define NETWORK_SAIL_H +#ifdef __cplusplus + +#include "Network.h" + +class Sail : public Network { + private: + void RegisterHooks(); + + public: + static Sail* Instance; + + void Enable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void DrawMenu(); +}; + +#endif // __cplusplus +#endif // NETWORK_SAIL_H +#endif // ENABLE_NETWORKING diff --git a/mm/2s2h/ShipUtils.cpp b/mm/2s2h/ShipUtils.cpp index 5d50eb62e2..9d62dfd898 100644 --- a/mm/2s2h/ShipUtils.cpp +++ b/mm/2s2h/ShipUtils.cpp @@ -54,3 +54,17 @@ extern "C" TexturePtr Ship_GetCharFontTextureNES(u8 character) { return (TexturePtr)fontTbl[adjustedChar]; } + +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 f30c39aac6..c4229a8807 100644 --- a/mm/2s2h/ShipUtils.h +++ b/mm/2s2h/ShipUtils.h @@ -5,6 +5,7 @@ #include "PR/ultratypes.h" #ifdef __cplusplus +#include extern "C" { #endif @@ -14,6 +15,7 @@ TexturePtr Ship_GetCharFontTextureNES(u8 character); #ifdef __cplusplus } +bool isStringEmpty(std::string str); #endif #endif // SHIP_UTILS_H diff --git a/mm/CMakeLists.txt b/mm/CMakeLists.txt index 4640ff349c..2140b06ad5 100644 --- a/mm/CMakeLists.txt +++ b/mm/CMakeLists.txt @@ -273,9 +273,15 @@ endif() find_package(SDL2) set(SDL2-INCLUDE ${SDL2_INCLUDE_DIRS}) -if (BUILD_CROWD_CONTROL) +if (BUILD_NETWORKING) find_package(SDL2_net) - set(SDL2-NET-INCLUDE ${SDL_NET_INCLUDE_DIRS}) + + if(NOT SDL2_net_FOUND) + message(STATUS "SDL2_net not found (it's possible the version installed is too old). Disabling BUILD_NETWORKING.") + set(BUILD_NETWORKING 0) + else() + set(SDL2-NET-INCLUDE ${SDL_NET_INCLUDE_DIRS}) + endif() endif() target_include_directories(${PROJECT_NAME} PRIVATE assets @@ -328,9 +334,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "$<$:" "NDEBUG" ">" - #"$<$:ENABLE_CROWD_CONTROL>" + "$<$:ENABLE_NETWORKING>" "INCLUDE_GAME_PRINTF;" - #"ENABLE_CROWD_CONTROL;" "F3DEX_GBI_2" "UNICODE;" "_UNICODE" @@ -361,7 +366,7 @@ elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") "NDEBUG" ">" "F3DEX_GBI_2" - # "$<$:ENABLE_CROWD_CONTROL>" + "$<$:ENABLE_NETWORKING>" "SPDLOG_ACTIVE_LEVEL=0;" "_CONSOLE;" "_CRT_SECURE_NO_WARNINGS;" @@ -558,7 +563,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;" @@ -594,7 +599,7 @@ else() "libultraship;" "ZAPDLib;" SDL2::SDL2 - # "$<$:SDL2_net::SDL2_net>" + "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} Threads::Threads )