From df6763257b39cdd2225f2035a0e909024db62e0b Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Fri, 18 Oct 2024 11:07:22 -0500 Subject: [PATCH] Refactor network usage and adapt Sail/CC to changes --- .../game-interactor/GameInteractor.h | 39 --- .../game-interactor/GameInteractor_Remote.cpp | 183 ------------- soh/soh/Network/CrowdControl/CrowdControl.cpp | 95 +++++-- soh/soh/Network/CrowdControl/CrowdControl.h | 34 +-- soh/soh/Network/Network.cpp | 145 ++++++++++ soh/soh/Network/Network.h | 50 ++++ soh/soh/Network/Sail/Sail.cpp | 255 ++++++++++++------ soh/soh/Network/Sail/Sail.h | 43 ++- soh/soh/OTRGlobals.cpp | 36 +-- soh/soh/OTRGlobals.h | 2 + soh/soh/SohGui.cpp | 5 - soh/soh/SohMenuBar.cpp | 129 +-------- soh/soh/UIWidgets.cpp | 14 + soh/soh/UIWidgets.hpp | 1 + soh/soh/util.cpp | 13 + soh/soh/util.h | 2 + soh/src/code/z_en_item00.c | 2 +- soh/src/code/z_room.c | 2 +- .../ovl_Bg_Mori_Bigst/z_bg_mori_bigst.c | 2 +- .../actors/ovl_En_Blkobj/z_en_blkobj.c | 4 +- .../actors/ovl_En_Clear_Tag/z_en_clear_tag.c | 2 +- soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c | 6 +- soh/src/overlays/actors/ovl_En_Rd/z_en_rd.c | 4 +- .../actors/ovl_En_Torch2/z_en_torch2.c | 2 +- 24 files changed, 539 insertions(+), 531 deletions(-) delete mode 100644 soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp create mode 100644 soh/soh/Network/Network.cpp create mode 100644 soh/soh/Network/Network.h diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 5e30441db..ca44cf59c 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -8,11 +8,6 @@ #include "soh/Enhancements/item-tables/ItemTableTypes.h" #include -typedef enum { - GI_SCHEME_SAIL, - GI_SCHEME_CROWD_CONTROL, -} GIScheme; - typedef enum { /* 0x00 */ GI_LINK_SIZE_NORMAL, /* 0x01 */ GI_LINK_SIZE_GIANT, @@ -524,11 +519,6 @@ void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state); #pragma message("Compiling without support, the Hook Debugger will not be avaliable") #endif -#ifdef ENABLE_REMOTE_CONTROL -#include -#include -#endif - typedef uint32_t HOOK_ID; enum HookType { @@ -606,20 +596,6 @@ public: static void SetPacifistMode(bool active); }; - #ifdef ENABLE_REMOTE_CONTROL - bool isRemoteInteractorEnabled; - bool isRemoteInteractorConnected; - - void EnableRemoteInteractor(); - void DisableRemoteInteractor(); - void RegisterRemoteDataHandler(std::function method); - void RegisterRemoteJsonHandler(std::function method); - void RegisterRemoteConnectedHandler(std::function method); - void RegisterRemoteDisconnectedHandler(std::function method); - void TransmitDataToRemote(const char* payload); - void TransmitJsonToRemote(nlohmann::json packet); - #endif - // Effects static GameInteractionEffectQueryResult CanApplyEffect(GameInteractionEffectBase* effect); static GameInteractionEffectQueryResult ApplyEffect(GameInteractionEffectBase* effect); @@ -874,21 +850,6 @@ public: static GameInteractionEffectQueryResult SpawnEnemyWithOffset(uint32_t enemyId, int32_t enemyParams); static GameInteractionEffectQueryResult SpawnActor(uint32_t actorId, int32_t actorParams); }; - - private: - #ifdef ENABLE_REMOTE_CONTROL - IPaddress remoteIP; - TCPsocket remoteSocket; - std::thread remoteThreadReceive; - std::function remoteDataHandler; - std::function remoteJsonHandler; - std::function remoteConnectedHandler; - std::function remoteDisconnectedHandler; - - void ReceiveFromServer(); - void HandleRemoteData(char payload[512]); - void HandleRemoteJson(std::string payload); - #endif }; #undef GET_CURRENT_REGISTERING_INFO diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp deleted file mode 100644 index 662d2ea78..000000000 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp +++ /dev/null @@ -1,183 +0,0 @@ -#ifdef ENABLE_REMOTE_CONTROL - -#include "GameInteractor.h" -#include -#include -#include -#include -#include -#include -#include -#include "soh/OTRGlobals.h" - -// MARK: - Remote - -void GameInteractor::EnableRemoteInteractor() { - if (isRemoteInteractorEnabled) { - return; - } - - if (SDLNet_ResolveHost(&remoteIP, CVarGetString(CVAR_REMOTE("IP"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE("Port"), 43384)) == -1) { - SPDLOG_ERROR("[GameInteractor] SDLNet_ResolveHost: {}", SDLNet_GetError()); - } - - isRemoteInteractorEnabled = true; - - // First check if there is a thread running, if so, join it - if (remoteThreadReceive.joinable()) { - remoteThreadReceive.join(); - } - - remoteThreadReceive = std::thread(&GameInteractor::ReceiveFromServer, this); -} - -/** - * 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 - */ -void GameInteractor::RegisterRemoteDataHandler(std::function method) { - remoteDataHandler = method; -} - -/** - * 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). - */ -void GameInteractor::RegisterRemoteJsonHandler(std::function method) { - remoteJsonHandler = method; -} - -void GameInteractor::RegisterRemoteConnectedHandler(std::function method) { - remoteConnectedHandler = method; -} - -void GameInteractor::RegisterRemoteDisconnectedHandler(std::function method) { - remoteDisconnectedHandler = method; -} - -void GameInteractor::DisableRemoteInteractor() { - if (!isRemoteInteractorEnabled) { - return; - } - - isRemoteInteractorEnabled = false; - remoteThreadReceive.join(); - remoteDataHandler = nullptr; - remoteJsonHandler = nullptr; - remoteConnectedHandler = nullptr; - remoteDisconnectedHandler = nullptr; -} - -void GameInteractor::TransmitDataToRemote(const char* payload) { - SDLNet_TCP_Send(remoteSocket, payload, strlen(payload) + 1); -} - -// Appends a newline character to the end of the json payload and sends it to the remote -void GameInteractor::TransmitJsonToRemote(nlohmann::json payload) { - TransmitDataToRemote(payload.dump().c_str()); -} - -// MARK: - Private - -std::string receivedData; - -void GameInteractor::ReceiveFromServer() { - while (isRemoteInteractorEnabled) { - while (!isRemoteInteractorConnected && isRemoteInteractorEnabled) { - SPDLOG_TRACE("[GameInteractor] Attempting to make connection to server..."); - remoteSocket = SDLNet_TCP_Open(&remoteIP); - - if (remoteSocket) { - isRemoteInteractorConnected = true; - SPDLOG_INFO("[GameInteractor] Connection to server established!"); - - if (remoteConnectedHandler) { - remoteConnectedHandler(); - } - break; - } - } - - SDLNet_SocketSet socketSet = SDLNet_AllocSocketSet(1); - if (remoteSocket) { - SDLNet_TCP_AddSocket(socketSet, remoteSocket); - } - - // Listen to socket messages - while (isRemoteInteractorConnected && remoteSocket && isRemoteInteractorEnabled) { - // 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("[GameInteractor] SDLNet_CheckSockets: {}", SDLNet_GetError()); - break; - } - - if (socketsReady == 0) { - continue; - } - - char remoteDataReceived[512]; - memset(remoteDataReceived, 0, sizeof(remoteDataReceived)); - int len = SDLNet_TCP_Recv(remoteSocket, &remoteDataReceived, sizeof(remoteDataReceived)); - if (!len || !remoteSocket || len == -1) { - SPDLOG_ERROR("[GameInteractor] 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 (isRemoteInteractorConnected) { - SDLNet_TCP_Close(remoteSocket); - isRemoteInteractorConnected = false; - if (remoteDisconnectedHandler) { - remoteDisconnectedHandler(); - } - SPDLOG_INFO("[GameInteractor] Ending receiving thread..."); - } - } -} - -void GameInteractor::HandleRemoteData(char payload[512]) { - if (remoteDataHandler) { - remoteDataHandler(payload); - return; - } -} - -void GameInteractor::HandleRemoteJson(std::string payload) { - nlohmann::json jsonPayload; - try { - jsonPayload = nlohmann::json::parse(payload); - } catch (const std::exception& e) { - SPDLOG_ERROR("[GameInteractor] Failed to parse json: \n{}\n{}\n", payload, e.what()); - return; - } - - if (remoteJsonHandler) { - remoteJsonHandler(jsonPayload); - return; - } -} - -#endif diff --git a/soh/soh/Network/CrowdControl/CrowdControl.cpp b/soh/soh/Network/CrowdControl/CrowdControl.cpp index 6443a7ff3..bc8055140 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.cpp +++ b/soh/soh/Network/CrowdControl/CrowdControl.cpp @@ -8,6 +8,7 @@ #include #include #include +#include "soh/OTRGlobals.h" extern "C" { #include @@ -18,30 +19,18 @@ extern PlayState* gPlayState; } void CrowdControl::Enable() { - if (isEnabled) { - return; - } - - isEnabled = true; - GameInteractor::Instance->EnableRemoteInteractor(); - GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { - HandleRemoteData(payload); - }); + Network::Enable(CVarGetString(CVAR_REMOTE_CROWD_CONTROL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Port"), 43384)); +} +void CrowdControl::OnConnected() { ccThreadProcess = std::thread(&CrowdControl::ProcessActiveEffects, this); } -void CrowdControl::Disable() { - if (!isEnabled) { - return; - } - - isEnabled = false; +void CrowdControl::OnDisconnected() { ccThreadProcess.join(); - GameInteractor::Instance->DisableRemoteInteractor(); } -void CrowdControl::HandleRemoteData(nlohmann::json payload) { +void CrowdControl::OnIncomingJson(nlohmann::json payload) { Effect* incomingEffect = ParseMessage(payload); if (!incomingEffect) { return; @@ -139,7 +128,7 @@ void CrowdControl::EmitMessage(uint32_t eventId, long timeRemaining, EffectResul SPDLOG_INFO("[CrowdControl] Sending payload:\n{}", payload.dump()); - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); } CrowdControl::EffectResult CrowdControl::ExecuteEffect(Effect* effect) { @@ -185,6 +174,12 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { SPDLOG_INFO("[CrowdControl] Received payload:\n{}", dataReceived.dump()); + if (!dataReceived.contains("code")) { + // This seems to happen when the CC session ends + SPDLOG_ERROR("[CrowdControl] Payload does not contain code, ignoring."); + return nullptr; + } + Effect* effect = new Effect(); effect->lastExecutionResult = EffectResult::Initiate; effect->id = dataReceived["id"]; @@ -770,4 +765,68 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { return effect; } +void CrowdControl::DrawMenu() { + ImGui::PushID("CrowdControl"); + + static std::string host = CVarGetString(CVAR_REMOTE_CROWD_CONTROL("Host"), "127.0.0.1"); + static uint16_t port = CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Port"), 43384); + bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535; + + ImGui::SeparatorText("Crowd Control"); + UIWidgets::Tooltip( + "Crowd Control is a platform that allows viewers to interact " + "with a streamer's game in real time.\n" + "\n" + "Click the question mark to copy the link to the Crowd Control " + "website to your clipboard." + ); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText("https://crowdcontrol.live"); + } + + ImGui::BeginDisabled(isEnabled); + ImGui::Text("Host & Port"); + if (UIWidgets::InputString("##Host", &host)) { + CVarSetString(CVAR_REMOTE_CROWD_CONTROL("Host"), host.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + + ImGui::SameLine(); + ImGui::PushItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) { + CVarSetInteger(CVAR_REMOTE_CROWD_CONTROL("Port"), port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + ImGui::PopItemWidth(); + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = isEnabled ? "Disable" : "Enable"; + if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { + if (isEnabled) { + CVarClear(CVAR_REMOTE_CROWD_CONTROL("Enabled")); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Disable(); + } else { + CVarSetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Enable(); + } + } + ImGui::EndDisabled(); + + if (isEnabled) { + ImGui::Spacing(); + if (isConnected) { + ImGui::Text("Connected"); + } else { + ImGui::Text("Connecting..."); + } + } + + ImGui::PopID(); +} + #endif diff --git a/soh/soh/Network/CrowdControl/CrowdControl.h b/soh/soh/Network/CrowdControl/CrowdControl.h index bb06cc5b1..534197807 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.h +++ b/soh/soh/Network/CrowdControl/CrowdControl.h @@ -1,25 +1,16 @@ #ifdef ENABLE_REMOTE_CONTROL - -#ifndef _CROWDCONTROL_C -#define _CROWDCONTROL_C -#endif - -#include "stdint.h" - +#ifndef NETWORK_CROWD_CONTROL_H +#define NETWORK_CROWD_CONTROL_H #ifdef __cplusplus -#include -#include + #include #include -#include #include -#include -#include -#include -#include "../game-interactor/GameInteractor.h" +#include "soh/Network/Network.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" -class CrowdControl { +class CrowdControl : public Network { private: enum EffectResult { /// The effect executed successfully. @@ -75,8 +66,6 @@ class CrowdControl { std::thread ccThreadProcess; - bool isEnabled; - std::vector activeEffects; std::mutex activeEffectsMutex; @@ -92,7 +81,12 @@ class CrowdControl { public: static CrowdControl* Instance; void Enable(); - void Disable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void DrawMenu(); }; -#endif -#endif + +#endif // __cplusplus +#endif // NETWORK_CROWD_CONTROL_H +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Network.cpp b/soh/soh/Network/Network.cpp new file mode 100644 index 000000000..c0bd87e6e --- /dev/null +++ b/soh/soh/Network/Network.cpp @@ -0,0 +1,145 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#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 // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Network.h b/soh/soh/Network/Network.h new file mode 100644 index 000000000..6bd4f8164 --- /dev/null +++ b/soh/soh/Network/Network.h @@ -0,0 +1,50 @@ +#ifdef ENABLE_REMOTE_CONTROL +#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); + virtual void SendJsonToRemote(nlohmann::json packet); +}; + +#endif // __cplusplus +#endif // NETWORK_H +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Sail/Sail.cpp b/soh/soh/Network/Sail/Sail.cpp index 77d48dc80..bf856c8f0 100644 --- a/soh/soh/Network/Sail/Sail.cpp +++ b/soh/soh/Network/Sail/Sail.cpp @@ -1,41 +1,31 @@ #ifdef ENABLE_REMOTE_CONTROL -#include "GameInteractor_Sail.h" +#include "Sail.h" #include #include #include +#include "soh/OTRGlobals.h" +#include "soh/util.h" template bool IsType(const SrcType* src) { - return dynamic_cast(src) != nullptr; + return dynamic_cast(src) != nullptr; } -void GameInteractorSail::Enable() { - if (isEnabled) { - return; - } - - isEnabled = true; - GameInteractor::Instance->EnableRemoteInteractor(); - GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { - HandleRemoteJson(payload); - }); - GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() { - RegisterHooks(); - }); +void Sail::Enable() { + Network::Enable(CVarGetString(CVAR_REMOTE_SAIL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_SAIL("Port"), 43384)); } -void GameInteractorSail::Disable() { - if (!isEnabled) { - return; - } - - isEnabled = false; - GameInteractor::Instance->DisableRemoteInteractor(); +void Sail::OnConnected() { + RegisterHooks(); } -void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { - SPDLOG_INFO("[GameInteractorSail] Received payload: \n{}", payload.dump()); +void Sail::OnDisconnected() { + RegisterHooks(); +} + +void Sail::OnIncomingJson(nlohmann::json payload) { + SPDLOG_INFO("[Sail] Received payload: \n{}", payload.dump()); nlohmann::json responsePayload; responsePayload["type"] = "result"; @@ -43,16 +33,16 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { try { if (!payload.contains("id")) { - SPDLOG_ERROR("[GameInteractorSail] Received payload without ID"); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SPDLOG_ERROR("[Sail] Received payload without ID"); + SendJsonToRemote(responsePayload); return; } responsePayload["id"] = payload["id"]; if (!payload.contains("type")) { - SPDLOG_ERROR("[GameInteractorSail] Received payload without type"); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SPDLOG_ERROR("[Sail] Received payload without type"); + SendJsonToRemote(responsePayload); return; } @@ -60,20 +50,20 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { if (payloadType == "command") { if (!payload.contains("command")) { - SPDLOG_ERROR("[GameInteractorSail] Received command payload without command"); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + 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"; - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SendJsonToRemote(responsePayload); return; } else if (payloadType == "effect") { if (!payload.contains("effect") || !payload["effect"].contains("type")) { - SPDLOG_ERROR("[GameInteractorSail] Received effect payload without effect type"); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SPDLOG_ERROR("[Sail] Received effect payload without effect type"); + SendJsonToRemote(responsePayload); return; } @@ -82,27 +72,27 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { // 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("[GameInteractorSail] Received command effect payload without command"); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + 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"; - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SendJsonToRemote(responsePayload); return; } if (effectType != "apply" && effectType != "remove") { - SPDLOG_ERROR("[GameInteractorSail] Received effect payload with unknown effect type: {}", effectType); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SPDLOG_ERROR("[Sail] Received effect payload with unknown effect type: {}", effectType); + SendJsonToRemote(responsePayload); return; } if (!GameInteractor::IsSaveLoaded()) { responsePayload["status"] = "try_again"; - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SendJsonToRemote(responsePayload); return; } @@ -124,26 +114,26 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { } else if (result == GameInteractionEffectQueryResult::TemporarilyNotPossible) { responsePayload["status"] = "try_again"; } - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SendJsonToRemote(responsePayload); return; } } else { - SPDLOG_ERROR("[GameInteractorSail] Unknown payload type: {}", payloadType); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SPDLOG_ERROR("[Sail] Unknown payload type: {}", payloadType); + SendJsonToRemote(responsePayload); return; } // If we get here, something went wrong, send the failure response - SPDLOG_ERROR("[GameInteractorSail] Failed to handle remote JSON, sending failure response"); - GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + SPDLOG_ERROR("[Sail] Failed to handle remote JSON, sending failure response"); + SendJsonToRemote(responsePayload); } catch (const std::exception& e) { - SPDLOG_ERROR("[GameInteractorSail] Exception handling remote JSON: {}", e.what()); + SPDLOG_ERROR("[Sail] Exception handling remote JSON: {}", e.what()); } catch (...) { - SPDLOG_ERROR("[GameInteractorSail] Unknown exception handling remote JSON"); + SPDLOG_ERROR("[Sail] Unknown exception handling remote JSON"); } } -GameInteractionEffectBase* GameInteractorSail::EffectFromJson(nlohmann::json payload) { +GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { if (!payload.contains("name")) { return nullptr; } @@ -331,22 +321,51 @@ GameInteractionEffectBase* GameInteractorSail::EffectFromJson(nlohmann::json pay } else if (name == "SlipperyFloor") { return new GameInteractionEffect::SlipperyFloor(); } else { - SPDLOG_INFO("[GameInteractorSail] Unknown effect name: {}", name); + SPDLOG_INFO("[Sail] Unknown effect name: {}", name); return nullptr; } } -// Workaround until we have a way to unregister hooks -static bool hasRegisteredHooks = false; +void Sail::RegisterHooks() { + static HOOK_ID onTransitionEndHook = 0; + static HOOK_ID onLoadGameHook = 0; + static HOOK_ID onExitGameHook = 0; + static HOOK_ID onItemReceiveHook = 0; + static HOOK_ID onEnemyDefeatHook = 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; -void GameInteractorSail::RegisterHooks() { - if (hasRegisteredHooks) { + GameInteractor::Instance->UnregisterGameHook(onTransitionEndHook); + GameInteractor::Instance->UnregisterGameHook(onLoadGameHook); + GameInteractor::Instance->UnregisterGameHook(onExitGameHook); + GameInteractor::Instance->UnregisterGameHook(onItemReceiveHook); + GameInteractor::Instance->UnregisterGameHook(onEnemyDefeatHook); + GameInteractor::Instance->UnregisterGameHook(onActorInitHook); + GameInteractor::Instance->UnregisterGameHook(onFlagSetHook); + GameInteractor::Instance->UnregisterGameHook(onFlagUnsetHook); + GameInteractor::Instance->UnregisterGameHook(onSceneFlagSetHook); + GameInteractor::Instance->UnregisterGameHook(onSceneFlagUnsetHook); + + onTransitionEndHook = 0; + onLoadGameHook = 0; + onExitGameHook = 0; + onItemReceiveHook = 0; + onEnemyDefeatHook = 0; + onActorInitHook = 0; + onFlagSetHook = 0; + onFlagUnsetHook = 0; + onSceneFlagSetHook = 0; + onSceneFlagUnsetHook = 0; + + if (!isConnected) { return; } - hasRegisteredHooks = true; - GameInteractor::Instance->RegisterGameHook([](int32_t sceneNum) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onTransitionEndHook = GameInteractor::Instance->RegisterGameHook([&](int32_t sceneNum) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -354,10 +373,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["type"] = "OnTransitionEnd"; payload["hook"]["sceneNum"] = sceneNum; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onLoadGameHook = GameInteractor::Instance->RegisterGameHook([&](int32_t fileNum) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -365,10 +384,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["type"] = "OnLoadGame"; payload["hook"]["fileNum"] = fileNum; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onExitGameHook = GameInteractor::Instance->RegisterGameHook([&](int32_t fileNum) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -376,10 +395,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["type"] = "OnExitGame"; payload["hook"]["fileNum"] = fileNum; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onItemReceiveHook = GameInteractor::Instance->RegisterGameHook([&](GetItemEntry itemEntry) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -388,10 +407,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["tableId"] = itemEntry.tableId; payload["hook"]["getItemId"] = itemEntry.getItemId; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](void* refActor) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onEnemyDefeatHook = GameInteractor::Instance->RegisterGameHook([&](void* refActor) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; Actor* actor = (Actor*)refActor; nlohmann::json payload; @@ -401,10 +420,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["actorId"] = actor->id; payload["hook"]["params"] = actor->params; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](void* refActor) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onActorInitHook = GameInteractor::Instance->RegisterGameHook([&](void* refActor) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; Actor* actor = (Actor*)refActor; nlohmann::json payload; @@ -414,10 +433,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["actorId"] = actor->id; payload["hook"]["params"] = actor->params; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onFlagSetHook = GameInteractor::Instance->RegisterGameHook([&](int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -426,10 +445,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["flagType"] = flagType; payload["hook"]["flag"] = flag; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onFlagUnsetHook = GameInteractor::Instance->RegisterGameHook([&](int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -438,10 +457,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["flagType"] = flagType; payload["hook"]["flag"] = flag; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onSceneFlagSetHook = GameInteractor::Instance->RegisterGameHook([&](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -451,10 +470,10 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["flag"] = flag; payload["hook"]["sceneNum"] = sceneNum; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); - GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { - if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + onSceneFlagUnsetHook = GameInteractor::Instance->RegisterGameHook([&](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); @@ -464,8 +483,76 @@ void GameInteractorSail::RegisterHooks() { payload["hook"]["flag"] = flag; payload["hook"]["sceneNum"] = sceneNum; - GameInteractor::Instance->TransmitJsonToRemote(payload); + SendJsonToRemote(payload); }); } -#endif +void Sail::DrawMenu() { + ImGui::PushID("Sail"); + + static std::string host = CVarGetString(CVAR_REMOTE_SAIL("Host"), "127.0.0.1"); + static uint16_t port = CVarGetInteger(CVAR_REMOTE_SAIL("Port"), 43384); + bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535; + + 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"); + } + + ImGui::BeginDisabled(isEnabled); + ImGui::Text("Host & Port"); + if (UIWidgets::InputString("##Host", &host)) { + CVarSetString(CVAR_REMOTE_SAIL("Host"), host.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + + ImGui::SameLine(); + ImGui::PushItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) { + CVarSetInteger(CVAR_REMOTE_SAIL("Port"), port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + ImGui::PopItemWidth(); + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = isEnabled ? "Disable" : "Enable"; + if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { + if (isEnabled) { + CVarClear(CVAR_REMOTE_SAIL("Enabled")); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Disable(); + } else { + CVarSetInteger(CVAR_REMOTE_SAIL("Enabled"), 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + Enable(); + } + } + ImGui::EndDisabled(); + + if (isEnabled) { + ImGui::Spacing(); + if (isConnected) { + ImGui::Text("Connected"); + } else { + ImGui::Text("Connecting..."); + } + } + + ImGui::PopID(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Sail/Sail.h b/soh/soh/Network/Sail/Sail.h index cb90c65c7..8c7c9d55e 100644 --- a/soh/soh/Network/Sail/Sail.h +++ b/soh/soh/Network/Sail/Sail.h @@ -1,29 +1,26 @@ #ifdef ENABLE_REMOTE_CONTROL - +#ifndef NETWORK_SAIL_H +#define NETWORK_SAIL_H #ifdef __cplusplus -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "./GameInteractor.h" +#include "soh/Network/Network.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" -class GameInteractorSail { - private: - bool isEnabled; +class Sail : public Network { + private: + GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); + void RegisterHooks(); - void HandleRemoteJson(nlohmann::json payload); - GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); - void RegisterHooks(); - public: - static GameInteractorSail* Instance; - void Enable(); - void Disable(); + public: + static Sail* Instance; + + void Enable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void DrawMenu(); }; -#endif -#endif + +#endif // __cplusplus +#endif // NETWORK_SAIL_H +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 907ae3cef..7edfd17b5 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -79,10 +79,10 @@ #include "ActorDB.h" #ifdef ENABLE_REMOTE_CONTROL -#include "Enhancements/crowd-control/CrowdControl.h" -#include "Enhancements/game-interactor/GameInteractor_Sail.h" +#include "soh/Network/CrowdControl/CrowdControl.h" +#include "soh/Network/Sail/Sail.h" CrowdControl* CrowdControl::Instance; -GameInteractorSail* GameInteractorSail::Instance; +Sail* Sail::Instance; #endif #include "Enhancements/mods.h" @@ -1173,7 +1173,7 @@ extern "C" void InitOTR() { #ifdef ENABLE_REMOTE_CONTROL CrowdControl::Instance = new CrowdControl(); - GameInteractorSail::Instance = new GameInteractorSail(); + Sail::Instance = new Sail(); #endif OTRMessage_Init(); @@ -1203,15 +1203,11 @@ extern "C" void InitOTR() { srand(now); #ifdef ENABLE_REMOTE_CONTROL SDLNet_Init(); - if (CVarGetInteger(CVAR_REMOTE("Enabled"), 0)) { - switch (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL)) { - case GI_SCHEME_SAIL: - GameInteractorSail::Instance->Enable(); - break; - case GI_SCHEME_CROWD_CONTROL: - CrowdControl::Instance->Enable(); - break; - } + if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) { + CrowdControl::Instance->Enable(); + } + if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { + Sail::Instance->Enable(); } #endif } @@ -1224,15 +1220,11 @@ extern "C" void DeinitOTR() { SaveManager_ThreadPoolWait(); OTRAudio_Exit(); #ifdef ENABLE_REMOTE_CONTROL - if (CVarGetInteger(CVAR_REMOTE("Enabled"), 0)) { - switch (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL)) { - case GI_SCHEME_SAIL: - GameInteractorSail::Instance->Disable(); - break; - case GI_SCHEME_CROWD_CONTROL: - CrowdControl::Instance->Disable(); - break; - } + if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) { + CrowdControl::Instance->Disable(); + } + if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { + Sail::Instance->Disable(); } SDLNet_Quit(); #endif diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index b1607c6c3..1ed0ab808 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -91,6 +91,8 @@ uint32_t IsGameMasterQuest(); #define CVAR_DEVELOPER_TOOLS(var) CVAR_PREFIX_DEVELOPER_TOOLS "." var #define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var #define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." var +#define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE(".CrowdControl." var) +#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var) #ifndef __cplusplus void InitOTR(void); diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp index 76e91acb5..90db44501 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -33,11 +33,6 @@ #include "soh/resource/type/Skeleton.h" #include "libultraship/libultraship.h" -#ifdef ENABLE_REMOTE_CONTROL -#include "Enhancements/crowd-control/CrowdControl.h" -#include "Enhancements/game-interactor/GameInteractor_Sail.h" -#endif - #include "Enhancements/game-interactor/GameInteractor.h" #include "Enhancements/cosmetics/authenticGfxPatches.h" #include "Enhancements/resolution-editor/ResolutionEditor.h" diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index 050cbf463..7f7c949bf 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -17,8 +17,8 @@ #include "soh/Enhancements/mods.h" #include "Enhancements/cosmetics/authenticGfxPatches.h" #ifdef ENABLE_REMOTE_CONTROL -#include "Enhancements/crowd-control/CrowdControl.h" -#include "Enhancements/game-interactor/GameInteractor_Sail.h" +#include "soh/Network/CrowdControl/CrowdControl.h" +#include "soh/Network/Sail/Sail.h" #endif @@ -1990,132 +1990,11 @@ void DrawDeveloperToolsMenu() { } } -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 -} - #ifdef ENABLE_REMOTE_CONTROL void DrawRemoteControlMenu() { if (ImGui::BeginMenu("Network")) { - static std::string ip = CVarGetString(CVAR_REMOTE("IP"), "127.0.0.1"); - static uint16_t port = CVarGetInteger(CVAR_REMOTE("Port"), 43384); - bool isFormValid = !isStringEmpty(CVarGetString(CVAR_REMOTE("IP"), "127.0.0.1")) && port > 1024 && port < 65535; - - const char* remoteOptions[2] = { "Sail", "Crowd Control"}; - - ImGui::BeginDisabled(GameInteractor::Instance->isRemoteInteractorEnabled); - ImGui::Text("Remote Interaction Scheme"); - if (UIWidgets::EnhancementCombobox(CVAR_REMOTE("Scheme"), remoteOptions, GI_SCHEME_SAIL)) { - auto scheme = CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL); - switch (scheme) { - case GI_SCHEME_SAIL: - case GI_SCHEME_CROWD_CONTROL: - CVarSetString(CVAR_REMOTE("IP"), "127.0.0.1"); - CVarSetInteger(CVAR_REMOTE("Port"), 43384); - ip = "127.0.0.1"; - port = 43384; - break; - } - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); - } - switch (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL)) { - case GI_SCHEME_SAIL: - UIWidgets::InsertHelpHoverText( - "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"); - } - break; - case GI_SCHEME_CROWD_CONTROL: - UIWidgets::InsertHelpHoverText( - "Crowd Control is a platform that allows viewers to interact " - "with a streamer's game in real time.\n" - "\n" - "Click the question mark to copy the link to the Crowd Control " - "website to your clipboard." - ); - if (ImGui::IsItemClicked()) { - ImGui::SetClipboardText("https://crowdcontrol.live"); - } - break; - } - - ImGui::Text("Remote IP & Port"); - if (ImGui::InputText("##gRemote.IP", (char*)ip.c_str(), ip.capacity() + 1)) { - CVarSetString(CVAR_REMOTE("IP"), ip.c_str()); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); - } - - ImGui::SameLine(); - ImGui::PushItemWidth(ImGui::GetFontSize() * 5); - if (ImGui::InputScalar("##gRemote.Port", ImGuiDataType_U16, &port)) { - CVarSetInteger(CVAR_REMOTE("Port"), port); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); - } - - ImGui::PopItemWidth(); - ImGui::EndDisabled(); - - ImGui::Spacing(); - - ImGui::BeginDisabled(!isFormValid); - const char* buttonLabel = GameInteractor::Instance->isRemoteInteractorEnabled ? "Disable" : "Enable"; - if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { - if (GameInteractor::Instance->isRemoteInteractorEnabled) { - CVarClear(CVAR_REMOTE("Enabled")); - CVarClear(CVAR_REMOTE("CrowdControl")); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); - switch (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL)) { - case GI_SCHEME_SAIL: - GameInteractorSail::Instance->Disable(); - break; - case GI_SCHEME_CROWD_CONTROL: - CrowdControl::Instance->Disable(); - break; - } - } else { - CVarSetInteger(CVAR_REMOTE("Enabled"), 1); - Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); - switch (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL)) { - case GI_SCHEME_SAIL: - GameInteractorSail::Instance->Enable(); - break; - case GI_SCHEME_CROWD_CONTROL: - CrowdControl::Instance->Enable(); - break; - } - } - } - ImGui::EndDisabled(); - - if (GameInteractor::Instance->isRemoteInteractorEnabled) { - ImGui::Spacing(); - if (GameInteractor::Instance->isRemoteInteractorConnected) { - ImGui::Text("Connected"); - } else { - ImGui::Text("Connecting..."); - } - } - - ImGui::Dummy(ImVec2(0.0f, 0.0f)); + Sail::Instance->DrawMenu(); + CrowdControl::Instance->DrawMenu(); ImGui::EndMenu(); } } diff --git a/soh/soh/UIWidgets.cpp b/soh/soh/UIWidgets.cpp index 5a41fe7cd..1d7a56c00 100644 --- a/soh/soh/UIWidgets.cpp +++ b/soh/soh/UIWidgets.cpp @@ -814,4 +814,18 @@ namespace UIWidgets { float sz = ImGui::GetFrameHeight(); return StateButtonEx(str_id, label, ImVec2(sz, sz), ImGuiButtonFlags_None); } + + // 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) { + return ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, ImGuiInputTextFlags_CallbackResize, InputTextResizeCallback, value); + } } diff --git a/soh/soh/UIWidgets.hpp b/soh/soh/UIWidgets.hpp index c52802d52..5903220d2 100644 --- a/soh/soh/UIWidgets.hpp +++ b/soh/soh/UIWidgets.hpp @@ -102,6 +102,7 @@ namespace UIWidgets { void DrawFlagArray16(const std::string& name, uint16_t& flags); void DrawFlagArray8(const std::string& name, uint8_t& flags); bool StateButton(const char* str_id, const char* label); + bool InputString(const char* label, std::string* value); } #endif /* UIWidgets_hpp */ diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index 2e84d2d24..d5f1e8418 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -390,3 +390,16 @@ size_t SohUtils::CopyStringToCharBuffer(char* buffer, const std::string& source, return 0; } + +bool SohUtils::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/soh/soh/util.h b/soh/soh/util.h index 10619b89a..a37279a18 100644 --- a/soh/soh/util.h +++ b/soh/soh/util.h @@ -20,4 +20,6 @@ namespace SohUtils { // Copies a string into a char buffer up to maxBufferSize characters. This does NOT insert a null terminator // on the end, as this is used for in-game messages which are not null-terminated. size_t CopyStringToCharBuffer(char* buffer, const std::string& source, size_t maxBufferSize); + + bool IsStringEmpty(std::string str); } // namespace SohUtils diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c index bca44cf26..90b535966 100644 --- a/soh/src/code/z_en_item00.c +++ b/soh/src/code/z_en_item00.c @@ -335,7 +335,7 @@ void EnItem00_SetupAction(EnItem00* this, EnItem00ActionFunc actionFunc) { void EnItem00_SetObjectDependency(EnItem00* this, PlayState* play, s16 objectIndex) { // Remove object dependency for Enemy Randomizer and Crowd Control to allow Like-likes to // drop equipment correctly in rooms where Like-likes normally don't spawn. - if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0))) { + if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) { this->actor.objBankIndex = 0; } else { this->actor.objBankIndex = Object_GetIndex(&play->objectCtx, objectIndex); diff --git a/soh/src/code/z_room.c b/soh/src/code/z_room.c index 8970dac1b..2db187375 100644 --- a/soh/src/code/z_room.c +++ b/soh/src/code/z_room.c @@ -413,7 +413,7 @@ BgImage* func_80096A74(PolygonType1* polygon1, PlayState* play) { camera = GET_ACTIVE_CAM(play); camId = camera->camDataIdx; - if (camId == -1 && (CVarGetInteger(CVAR_CHEAT("NoRestrictItems"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0)))) { + if (camId == -1 && (CVarGetInteger(CVAR_CHEAT("NoRestrictItems"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)))) { // This prevents a crash when using items that change the // camera (such as din's fire), voiding out or dying on // scenes with prerendered backgrounds. diff --git a/soh/src/overlays/actors/ovl_Bg_Mori_Bigst/z_bg_mori_bigst.c b/soh/src/overlays/actors/ovl_Bg_Mori_Bigst/z_bg_mori_bigst.c index 8576934de..e4531cb4a 100644 --- a/soh/src/overlays/actors/ovl_Bg_Mori_Bigst/z_bg_mori_bigst.c +++ b/soh/src/overlays/actors/ovl_Bg_Mori_Bigst/z_bg_mori_bigst.c @@ -223,7 +223,7 @@ void BgMoriBigst_StalfosPairFight(BgMoriBigst* this, PlayState* play) { if ((this->dyna.actor.home.rot.z == 0 || // Check if all enemies are defeated instead of the regular stalfos when enemy randomizer or crowd control is on. (Flags_GetTempClear(play, this->dyna.actor.room) && (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || - ((CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0)))))) && !Player_InCsMode(play)) { + ((CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)))))) && !Player_InCsMode(play)) { Flags_SetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F); BgMoriBigst_SetupDone(this, play); } diff --git a/soh/src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.c b/soh/src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.c index 0d367a411..43d142fab 100644 --- a/soh/src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.c +++ b/soh/src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.c @@ -106,9 +106,9 @@ void EnBlkobj_DarkLinkFight(EnBlkobj* this, PlayState* play) { // Check for if all enemies are defeated with enemy randomizer or crowd control on. uint8_t roomCleared = (!CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) && - !(CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0)) && + !(CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) && Actor_Find(&play->actorCtx, ACTOR_EN_TORCH2, ACTORCAT_BOSS) == NULL) || - ((CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0))) && + ((CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) && Flags_GetTempClear(play, this->dyna.actor.room)); if (roomCleared) { Flags_SetClear(play, this->dyna.actor.room); diff --git a/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c b/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c index f0f6b5bb5..c19f43dc3 100644 --- a/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c +++ b/soh/src/overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.c @@ -262,7 +262,7 @@ void EnClearTag_Init(Actor* thisx, PlayState* play) { // Change Arwing to regular enemy instead of boss with enemy randomizer and crowd control. // This way Arwings will be considered for "clear enemy" rooms properly. - if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0))) { + if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) { Actor_ChangeCategory(play, &play->actorCtx, thisx, ACTORCAT_ENEMY); } diff --git a/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c b/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c index 1fad5680b..9ab8735dd 100644 --- a/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c +++ b/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c @@ -234,7 +234,7 @@ void func_80A74398(Actor* thisx, PlayState* play) { func_80A74714(this); uint8_t enemyRandoCCActive = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || - (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0)); + (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)); if (this->switchFlags != 0xFF) { // In vanilla gameplay, Iron Knuckles are despawned based on specific flags in specific scenarios. @@ -665,7 +665,7 @@ void func_80A75A38(EnIk* this, PlayState* play) { // Don't set flag when Enemy Rando or CrowdControl are on. // Instead Iron Knuckles rely on the "clear room" flag. if (this->switchFlags != 0xFF && !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) && - !(CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0))) { + !(CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) { Flags_SetSwitch(play, this->switchFlags); } Actor_Kill(&this->actor); @@ -1468,7 +1468,7 @@ void EnIk_Init(Actor* thisx, PlayState* play) { } // Immediately trigger Iron Knuckle for Enemy Rando and Crowd Control - if ((CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0))) + if ((CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) && (thisx->params == 2 || thisx->params == 3)) { this->skelAnime.playSpeed = 1.0f; } diff --git a/soh/src/overlays/actors/ovl_En_Rd/z_en_rd.c b/soh/src/overlays/actors/ovl_En_Rd/z_en_rd.c index e90360bfa..8990f67ea 100644 --- a/soh/src/overlays/actors/ovl_En_Rd/z_en_rd.c +++ b/soh/src/overlays/actors/ovl_En_Rd/z_en_rd.c @@ -254,7 +254,7 @@ void func_80AE2744(EnRd* this, PlayState* play) { // Add a height check to redeads/gibdos freeze when Enemy Randomizer is on. // Without the height check, redeads/gibdos can freeze the player from insane distances in // vertical rooms (like the first room in Deku Tree), making these rooms nearly unplayable. - s8 enemyRandoCCActive = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0)); + s8 enemyRandoCCActive = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)); if (!enemyRandoCCActive || (enemyRandoCCActive && this->actor.yDistToPlayer <= 100.0f && this->actor.yDistToPlayer >= -100.0f)) { if ((this->actor.params != 2) && (this->unk_305 == 0)) { func_80AE37BC(this); @@ -668,7 +668,7 @@ void func_80AE3C98(EnRd* this, PlayState* play) { if (SkelAnime_Update(&this->skelAnime)) { if (this->unk_30C == 0) { - s8 enemyRandoCCActive = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0)); + s8 enemyRandoCCActive = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)); // Don't set this flag in Enemy Rando as it can overlap with other objects using the same flag. if (!Flags_GetSwitch(play, this->unk_312 & 0x7F) && !enemyRandoCCActive) { Flags_SetSwitch(play, this->unk_312 & 0x7F); diff --git a/soh/src/overlays/actors/ovl_En_Torch2/z_en_torch2.c b/soh/src/overlays/actors/ovl_En_Torch2/z_en_torch2.c index 51b1d9e53..28bc936c5 100644 --- a/soh/src/overlays/actors/ovl_En_Torch2/z_en_torch2.c +++ b/soh/src/overlays/actors/ovl_En_Torch2/z_en_torch2.c @@ -128,7 +128,7 @@ void EnTorch2_Init(Actor* thisx, PlayState* play2) { // Change Dark Link to regular enemy instead of boss with enemy randomizer and crowd control. // This way Dark Link will be considered for "clear enemy" rooms properly. - if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE("Scheme"), GI_SCHEME_SAIL) == GI_SCHEME_CROWD_CONTROL && CVarGetInteger(CVAR_REMOTE("Enabled"), 0))) { + if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0))) { Actor_ChangeCategory(play, &play->actorCtx, thisx, ACTORCAT_ENEMY); }