From 59735b44a411f1f72c2e0e6e54e84d3090f69798 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Wed, 2 Oct 2024 18:26:57 -0500 Subject: [PATCH] Add initial sail implementation --- .github/workflows/main.yml | 19 ++- CMakeLists.txt | 6 - mm/2s2h/BenGui/SearchableMenuItems.h | 120 +++++++++++++- mm/2s2h/BenGui/UIWidgets.cpp | 105 +++++++++++++ mm/2s2h/BenGui/UIWidgets.hpp | 27 ++++ mm/2s2h/BenPort.cpp | 29 ++-- mm/2s2h/Network/Network.cpp | 145 +++++++++++++++++ mm/2s2h/Network/Network.h | 50 ++++++ mm/2s2h/Network/Sail.cpp | 226 +++++++++++++++++++++++++++ mm/2s2h/Network/Sail.h | 24 +++ mm/2s2h/ShipUtils.cpp | 14 ++ mm/2s2h/ShipUtils.h | 2 + mm/CMakeLists.txt | 13 +- 13 files changed, 749 insertions(+), 31 deletions(-) create mode 100644 mm/2s2h/Network/Network.cpp create mode 100644 mm/2s2h/Network/Network.h create mode 100644 mm/2s2h/Network/Sail.cpp create mode 100644 mm/2s2h/Network/Sail.h 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/SearchableMenuItems.h b/mm/2s2h/BenGui/SearchableMenuItems.h index 682b1fd1e6..22075321ea 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_STRING, + 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_STRING, + { .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,35 @@ void SearchMenuGetItem(widgetInfo& widget) { } } break; + case WIDGET_CVAR_INPUT_STRING: { + if (UIWidgets::CVarInputString( + widget.widgetName.c_str(), widget.widgetCVar, + { + .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .defaultValue = std::get(widget.widgetOptions.defaultVariant), + })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + } break; + case WIDGET_CVAR_INPUT_INT: { + if (UIWidgets::CVarInputInt(widget.widgetName.c_str(), widget.widgetCVar, + { + .color = menuTheme[menuThemeIndex], + .tooltip = widget.widgetTooltip, + .disabled = disabledValue, + .disabledTooltip = disabledTooltip, + .defaultValue = std::get(widget.widgetOptions.defaultVariant), + })) { + if (widget.widgetCallback != nullptr) { + widget.widgetCallback(widget); + } + } + } break; case WIDGET_SLIDER_INT: { int32_t* pointer = std::get(widget.widgetOptions.valuePointer); if (pointer == nullptr) { diff --git a/mm/2s2h/BenGui/UIWidgets.cpp b/mm/2s2h/BenGui/UIWidgets.cpp index 632aac6b01..eba54a6326 100644 --- a/mm/2s2h/BenGui/UIWidgets.cpp +++ b/mm/2s2h/BenGui/UIWidgets.cpp @@ -524,6 +524,111 @@ bool CVarColorPicker(const char* label, const char* cvarName, Color_RGBA8 defaul return changed; } +void PushStyleInput(const ImVec4& color) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(color.x, color.y, color.z, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, ImVec4(1.0, 1.0, 1.0, 0.4f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrabActive, ImVec4(1.0, 1.0, 1.0, 0.5f)); + ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 8.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); +} + +void PopStyleInput() { + ImGui::PopStyleVar(4); + ImGui::PopStyleColor(6); +} + +// Reference: imgui-src/misc/cpp/imgui_stdlib.cpp +int InputTextResizeCallback(ImGuiInputTextCallbackData* data) { + std::string* value = (std::string*)data->UserData; + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { + value->resize(data->BufTextLen); + data->Buf = (char*)value->c_str(); + } + return 0; +} + +bool InputString(const char* label, std::string* value, const InputStringOptions& options) { + bool dirty = false; + ImGui::PushID(label); + ImGui::BeginGroup(); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + PushStyleInput(options.color); + ImGui::BeginDisabled(options.disabled); + if (options.labelPosition != LabelPosition::None) { + ImGui::Text(label); + } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputText(invisibleLabel, (char*)value->c_str(), value->capacity() + 1, + ImGuiInputTextFlags_CallbackResize, InputTextResizeCallback, value)) { + dirty = true; + } + ImGui::EndDisabled(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && + strcmp(options.disabledTooltip, "") != 0) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + PopStyleInput(); + ImGui::EndGroup(); + ImGui::PopID(); + return dirty; +} + +bool CVarInputString(const char* label, const char* cvarName, const InputStringOptions& options) { + std::string value = CVarGetString(cvarName, options.defaultValue); + bool dirty = InputString(label, &value, options); + if (dirty) { + CVarSetString(cvarName, value.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + return dirty; +} + +bool InputInt(const char* label, int32_t* value, const InputIntOptions& options) { + bool dirty = false; + ImGui::PushID(label); + ImGui::BeginGroup(); + std::string invisibleLabelStr = "##" + std::string(label); + const char* invisibleLabel = invisibleLabelStr.c_str(); + PushStyleInput(options.color); + ImGui::BeginDisabled(options.disabled); + if (options.labelPosition != LabelPosition::None) { + ImGui::Text(label); + } + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputInt(invisibleLabel, value, NULL, NULL)) { + dirty = true; + } + ImGui::EndDisabled(); + if (options.disabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && + strcmp(options.disabledTooltip, "") != 0) { + ImGui::SetTooltip("%s", WrappedText(options.disabledTooltip).c_str()); + } else if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && strcmp(options.tooltip, "") != 0) { + ImGui::SetTooltip("%s", WrappedText(options.tooltip).c_str()); + } + PopStyleInput(); + ImGui::EndGroup(); + ImGui::PopID(); + return dirty; +} + +bool CVarInputInt(const char* label, const char* cvarName, const InputIntOptions& options) { + int32_t value = CVarGetInteger(cvarName, options.defaultValue); + bool dirty = InputInt(label, &value, options); + if (dirty) { + CVarSetInteger(cvarName, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + return dirty; +} + void DrawFlagArray32(const std::string& name, uint32_t& flags) { ImGui::PushID(name.c_str()); for (int32_t flagIndex = 0; flagIndex < 32; flagIndex++) { diff --git a/mm/2s2h/BenGui/UIWidgets.hpp b/mm/2s2h/BenGui/UIWidgets.hpp index 90357360d8..68f6969dd0 100644 --- a/mm/2s2h/BenGui/UIWidgets.hpp +++ b/mm/2s2h/BenGui/UIWidgets.hpp @@ -413,6 +413,33 @@ namespace UIWidgets { bool SliderFloat(const char* label, float* value, float min, float max, const FloatSliderOptions& options = {}); bool CVarSliderFloat(const char* label, const char* cvarName, float min, float max, const float defaultValue, const FloatSliderOptions& options = {}); bool CVarColorPicker(const char* label, const char* cvarName, Color_RGBA8 defaultColor); + + struct InputStringOptions { + const ImVec4 color = Colors::Gray; + const char* tooltip = ""; + bool disabled = false; + const char* disabledTooltip = ""; + const char* placeholder = ""; + const char* defaultValue = ""; // Only applicable to CVarInputString + LabelPosition labelPosition = LabelPosition::Above; + }; + + struct InputIntOptions { + const ImVec4 color = Colors::Gray; + const char* tooltip = ""; + bool disabled = false; + const char* disabledTooltip = ""; + const char* placeholder = ""; + int32_t defaultValue = 0; // Only applicable to CVarInputInt + LabelPosition labelPosition = LabelPosition::Above; + }; + + void PushStyleInput(const ImVec4& color = Colors::Indigo); + void PopStyleInput(); + bool InputString(const char* label, std::string* value, const InputStringOptions& options = {}); + bool CVarInputString(const char* label, const char* cvarName, const InputStringOptions& options = {}); + bool InputInt(const char* label, int32_t* value, const InputIntOptions& options = {}); + bool CVarInputInt(const char* label, const char* cvarName, const InputIntOptions& options = {}); void DrawFlagArray32(const std::string& name, uint32_t& flags); void DrawFlagArray16(const std::string& name, uint16_t& flags); void DrawFlagArray8(const std::string& name, uint8_t& flags); 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..1255cb10de --- /dev/null +++ b/mm/2s2h/Network/Sail.cpp @@ -0,0 +1,226 @@ +#ifdef ENABLE_NETWORKING + +#include "Sail.h" +#include +#include +#include "2s2h/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); + }); +} + +#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..c085877cb0 100644 --- a/mm/CMakeLists.txt +++ b/mm/CMakeLists.txt @@ -273,8 +273,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() @@ -328,9 +328,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 +360,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 +557,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 +593,7 @@ else() "libultraship;" "ZAPDLib;" SDL2::SDL2 - # "$<$:SDL2_net::SDL2_net>" + "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} Threads::Threads )