Refactor network usage and adapt Sail/CC to changes

This commit is contained in:
Garrett Cox 2024-10-18 11:07:22 -05:00
parent 29d7c54250
commit df6763257b
24 changed files with 539 additions and 531 deletions

View File

@ -8,11 +8,6 @@
#include "soh/Enhancements/item-tables/ItemTableTypes.h"
#include <z64.h>
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 <source_location> support, the Hook Debugger will not be avaliable")
#endif
#ifdef ENABLE_REMOTE_CONTROL
#include <SDL2/SDL_net.h>
#include <nlohmann/json.hpp>
#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<void(char payload[512])> method);
void RegisterRemoteJsonHandler(std::function<void(nlohmann::json)> method);
void RegisterRemoteConnectedHandler(std::function<void()> method);
void RegisterRemoteDisconnectedHandler(std::function<void()> 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<void(char payload[512])> remoteDataHandler;
std::function<void(nlohmann::json)> remoteJsonHandler;
std::function<void()> remoteConnectedHandler;
std::function<void()> remoteDisconnectedHandler;
void ReceiveFromServer();
void HandleRemoteData(char payload[512]);
void HandleRemoteJson(std::string payload);
#endif
};
#undef GET_CURRENT_REGISTERING_INFO

View File

@ -1,183 +0,0 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "GameInteractor.h"
#include <spdlog/spdlog.h>
#include <imgui.h>
#include <imgui_internal.h>
#include <unordered_map>
#include <tuple>
#include <type_traits>
#include <libultraship/libultraship.h>
#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<void(char payload[512])> 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<void(nlohmann::json)> method) {
remoteJsonHandler = method;
}
void GameInteractor::RegisterRemoteConnectedHandler(std::function<void()> method) {
remoteConnectedHandler = method;
}
void GameInteractor::RegisterRemoteDisconnectedHandler(std::function<void()> 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

View File

@ -8,6 +8,7 @@
#include <spdlog/spdlog.h>
#include <spdlog/fmt/fmt.h>
#include <regex>
#include "soh/OTRGlobals.h"
extern "C" {
#include <z64.h>
@ -18,30 +19,18 @@ extern PlayState* gPlayState;
}
void CrowdControl::Enable() {
if (isEnabled) {
return;
Network::Enable(CVarGetString(CVAR_REMOTE_CROWD_CONTROL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Port"), 43384));
}
isEnabled = true;
GameInteractor::Instance->EnableRemoteInteractor();
GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) {
HandleRemoteData(payload);
});
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

View File

@ -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 <SDL2/SDL_net.h>
#include <cstdint>
#include <thread>
#include <memory>
#include <map>
#include <vector>
#include <iostream>
#include <chrono>
#include <future>
#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 {
/// <summary>The effect executed successfully.</summary>
@ -75,8 +66,6 @@ class CrowdControl {
std::thread ccThreadProcess;
bool isEnabled;
std::vector<Effect*> 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

145
soh/soh/Network/Network.cpp Normal file
View File

@ -0,0 +1,145 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "Network.h"
#include <spdlog/spdlog.h>
#include <libultraship/libultraship.h>
// 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

50
soh/soh/Network/Network.h Normal file
View File

@ -0,0 +1,50 @@
#ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_H
#define NETWORK_H
#ifdef __cplusplus
#include <thread>
#include <SDL2/SDL_net.h>
#include <nlohmann/json.hpp>
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

View File

@ -1,41 +1,31 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "GameInteractor_Sail.h"
#include "Sail.h"
#include <libultraship/bridge.h>
#include <libultraship/libultraship.h>
#include <nlohmann/json.hpp>
#include "soh/OTRGlobals.h"
#include "soh/util.h"
template <class DstType, class SrcType>
bool IsType(const SrcType* src) {
return dynamic_cast<const DstType*>(src) != nullptr;
}
void GameInteractorSail::Enable() {
if (isEnabled) {
return;
void Sail::Enable() {
Network::Enable(CVarGetString(CVAR_REMOTE_SAIL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_SAIL("Port"), 43384));
}
isEnabled = true;
GameInteractor::Instance->EnableRemoteInteractor();
GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) {
HandleRemoteJson(payload);
});
GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() {
void Sail::OnConnected() {
RegisterHooks();
});
}
void GameInteractorSail::Disable() {
if (!isEnabled) {
return;
void Sail::OnDisconnected() {
RegisterHooks();
}
isEnabled = false;
GameInteractor::Instance->DisableRemoteInteractor();
}
void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) {
SPDLOG_INFO("[GameInteractorSail] Received payload: \n{}", payload.dump());
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::string>();
std::reinterpret_pointer_cast<Ship::ConsoleWindow>(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::string>();
std::reinterpret_pointer_cast<Ship::ConsoleWindow>(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<GameInteractor::OnTransitionEnd>(onTransitionEndHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnLoadGame>(onLoadGameHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnExitGame>(onExitGameHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnItemReceive>(onItemReceiveHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnEnemyDefeat>(onEnemyDefeatHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorInit>(onActorInitHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnFlagSet>(onFlagSetHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnFlagUnset>(onFlagUnsetHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneFlagSet>(onSceneFlagSetHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneFlagUnset>(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<GameInteractor::OnTransitionEnd>([](int32_t sceneNum) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onTransitionEndHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnTransitionEnd>([&](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<GameInteractor::OnLoadGame>([](int32_t fileNum) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onLoadGameHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnLoadGame>([&](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<GameInteractor::OnExitGame>([](int32_t fileNum) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onExitGameHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnExitGame>([&](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<GameInteractor::OnItemReceive>([](GetItemEntry itemEntry) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onItemReceiveHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnItemReceive>([&](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<GameInteractor::OnEnemyDefeat>([](void* refActor) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onEnemyDefeatHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnEnemyDefeat>([&](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<GameInteractor::OnActorInit>([](void* refActor) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onActorInitHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorInit>([&](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<GameInteractor::OnFlagSet>([](int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onFlagSetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagSet>([&](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<GameInteractor::OnFlagUnset>([](int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onFlagUnsetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagUnset>([&](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<GameInteractor::OnSceneFlagSet>([](int16_t sceneNum, int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onSceneFlagSetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagSet>([&](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<GameInteractor::OnSceneFlagUnset>([](int16_t sceneNum, int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return;
onSceneFlagUnsetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagUnset>([&](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

View File

@ -1,29 +1,26 @@
#ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_SAIL_H
#define NETWORK_SAIL_H
#ifdef __cplusplus
#include <SDL2/SDL_net.h>
#include <cstdint>
#include <thread>
#include <memory>
#include <map>
#include <vector>
#include <iostream>
#include <chrono>
#include <future>
#include "./GameInteractor.h"
#include "soh/Network/Network.h"
#include "soh/Enhancements/game-interactor/GameInteractor.h"
class GameInteractorSail {
class Sail : public Network {
private:
bool isEnabled;
void HandleRemoteJson(nlohmann::json payload);
GameInteractionEffectBase* EffectFromJson(nlohmann::json payload);
void RegisterHooks();
public:
static GameInteractorSail* Instance;
static Sail* Instance;
void Enable();
void Disable();
void OnIncomingJson(nlohmann::json payload);
void OnConnected();
void OnDisconnected();
void DrawMenu();
};
#endif
#endif
#endif // __cplusplus
#endif // NETWORK_SAIL_H
#endif // ENABLE_REMOTE_CONTROL

View File

@ -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:
if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) {
CrowdControl::Instance->Enable();
break;
}
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:
if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) {
CrowdControl::Instance->Disable();
break;
}
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Disable();
}
SDLNet_Quit();
#endif

View File

@ -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);

View File

@ -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"

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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 */

View File

@ -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
}

View File

@ -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

View File

@ -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);

View File

@ -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.

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}