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 "soh/Enhancements/item-tables/ItemTableTypes.h"
#include <z64.h> #include <z64.h>
typedef enum {
GI_SCHEME_SAIL,
GI_SCHEME_CROWD_CONTROL,
} GIScheme;
typedef enum { typedef enum {
/* 0x00 */ GI_LINK_SIZE_NORMAL, /* 0x00 */ GI_LINK_SIZE_NORMAL,
/* 0x01 */ GI_LINK_SIZE_GIANT, /* 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") #pragma message("Compiling without <source_location> support, the Hook Debugger will not be avaliable")
#endif #endif
#ifdef ENABLE_REMOTE_CONTROL
#include <SDL2/SDL_net.h>
#include <nlohmann/json.hpp>
#endif
typedef uint32_t HOOK_ID; typedef uint32_t HOOK_ID;
enum HookType { enum HookType {
@ -606,20 +596,6 @@ public:
static void SetPacifistMode(bool active); 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 // Effects
static GameInteractionEffectQueryResult CanApplyEffect(GameInteractionEffectBase* effect); static GameInteractionEffectQueryResult CanApplyEffect(GameInteractionEffectBase* effect);
static GameInteractionEffectQueryResult ApplyEffect(GameInteractionEffectBase* effect); static GameInteractionEffectQueryResult ApplyEffect(GameInteractionEffectBase* effect);
@ -874,21 +850,6 @@ public:
static GameInteractionEffectQueryResult SpawnEnemyWithOffset(uint32_t enemyId, int32_t enemyParams); static GameInteractionEffectQueryResult SpawnEnemyWithOffset(uint32_t enemyId, int32_t enemyParams);
static GameInteractionEffectQueryResult SpawnActor(uint32_t actorId, int32_t actorParams); 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 #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/spdlog.h>
#include <spdlog/fmt/fmt.h> #include <spdlog/fmt/fmt.h>
#include <regex> #include <regex>
#include "soh/OTRGlobals.h"
extern "C" { extern "C" {
#include <z64.h> #include <z64.h>
@ -18,30 +19,18 @@ extern PlayState* gPlayState;
} }
void CrowdControl::Enable() { void CrowdControl::Enable() {
if (isEnabled) { Network::Enable(CVarGetString(CVAR_REMOTE_CROWD_CONTROL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Port"), 43384));
return;
} }
isEnabled = true; void CrowdControl::OnConnected() {
GameInteractor::Instance->EnableRemoteInteractor();
GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) {
HandleRemoteData(payload);
});
ccThreadProcess = std::thread(&CrowdControl::ProcessActiveEffects, this); ccThreadProcess = std::thread(&CrowdControl::ProcessActiveEffects, this);
} }
void CrowdControl::Disable() { void CrowdControl::OnDisconnected() {
if (!isEnabled) {
return;
}
isEnabled = false;
ccThreadProcess.join(); ccThreadProcess.join();
GameInteractor::Instance->DisableRemoteInteractor();
} }
void CrowdControl::HandleRemoteData(nlohmann::json payload) { void CrowdControl::OnIncomingJson(nlohmann::json payload) {
Effect* incomingEffect = ParseMessage(payload); Effect* incomingEffect = ParseMessage(payload);
if (!incomingEffect) { if (!incomingEffect) {
return; return;
@ -139,7 +128,7 @@ void CrowdControl::EmitMessage(uint32_t eventId, long timeRemaining, EffectResul
SPDLOG_INFO("[CrowdControl] Sending payload:\n{}", payload.dump()); SPDLOG_INFO("[CrowdControl] Sending payload:\n{}", payload.dump());
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
} }
CrowdControl::EffectResult CrowdControl::ExecuteEffect(Effect* effect) { 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()); 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* effect = new Effect();
effect->lastExecutionResult = EffectResult::Initiate; effect->lastExecutionResult = EffectResult::Initiate;
effect->id = dataReceived["id"]; effect->id = dataReceived["id"];
@ -770,4 +765,68 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) {
return effect; 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 #endif

View File

@ -1,25 +1,16 @@
#ifdef ENABLE_REMOTE_CONTROL #ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_CROWD_CONTROL_H
#ifndef _CROWDCONTROL_C #define NETWORK_CROWD_CONTROL_H
#define _CROWDCONTROL_C
#endif
#include "stdint.h"
#ifdef __cplusplus #ifdef __cplusplus
#include <SDL2/SDL_net.h>
#include <cstdint>
#include <thread> #include <thread>
#include <memory> #include <memory>
#include <map>
#include <vector> #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: private:
enum EffectResult { enum EffectResult {
/// <summary>The effect executed successfully.</summary> /// <summary>The effect executed successfully.</summary>
@ -75,8 +66,6 @@ class CrowdControl {
std::thread ccThreadProcess; std::thread ccThreadProcess;
bool isEnabled;
std::vector<Effect*> activeEffects; std::vector<Effect*> activeEffects;
std::mutex activeEffectsMutex; std::mutex activeEffectsMutex;
@ -92,7 +81,12 @@ class CrowdControl {
public: public:
static CrowdControl* Instance; static CrowdControl* Instance;
void Enable(); 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 #ifdef ENABLE_REMOTE_CONTROL
#include "GameInteractor_Sail.h" #include "Sail.h"
#include <libultraship/bridge.h> #include <libultraship/bridge.h>
#include <libultraship/libultraship.h> #include <libultraship/libultraship.h>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "soh/OTRGlobals.h"
#include "soh/util.h"
template <class DstType, class SrcType> template <class DstType, class SrcType>
bool IsType(const SrcType* src) { bool IsType(const SrcType* src) {
return dynamic_cast<const DstType*>(src) != nullptr; return dynamic_cast<const DstType*>(src) != nullptr;
} }
void GameInteractorSail::Enable() { void Sail::Enable() {
if (isEnabled) { Network::Enable(CVarGetString(CVAR_REMOTE_SAIL("Host"), "127.0.0.1"), CVarGetInteger(CVAR_REMOTE_SAIL("Port"), 43384));
return;
} }
isEnabled = true; void Sail::OnConnected() {
GameInteractor::Instance->EnableRemoteInteractor();
GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) {
HandleRemoteJson(payload);
});
GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() {
RegisterHooks(); RegisterHooks();
});
} }
void GameInteractorSail::Disable() { void Sail::OnDisconnected() {
if (!isEnabled) { RegisterHooks();
return;
} }
isEnabled = false; void Sail::OnIncomingJson(nlohmann::json payload) {
GameInteractor::Instance->DisableRemoteInteractor(); SPDLOG_INFO("[Sail] Received payload: \n{}", payload.dump());
}
void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) {
SPDLOG_INFO("[GameInteractorSail] Received payload: \n{}", payload.dump());
nlohmann::json responsePayload; nlohmann::json responsePayload;
responsePayload["type"] = "result"; responsePayload["type"] = "result";
@ -43,16 +33,16 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) {
try { try {
if (!payload.contains("id")) { if (!payload.contains("id")) {
SPDLOG_ERROR("[GameInteractorSail] Received payload without ID"); SPDLOG_ERROR("[Sail] Received payload without ID");
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
responsePayload["id"] = payload["id"]; responsePayload["id"] = payload["id"];
if (!payload.contains("type")) { if (!payload.contains("type")) {
SPDLOG_ERROR("[GameInteractorSail] Received payload without type"); SPDLOG_ERROR("[Sail] Received payload without type");
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
@ -60,20 +50,20 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) {
if (payloadType == "command") { if (payloadType == "command") {
if (!payload.contains("command")) { if (!payload.contains("command")) {
SPDLOG_ERROR("[GameInteractorSail] Received command payload without command"); SPDLOG_ERROR("[Sail] Received command payload without command");
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
std::string command = payload["command"].get<std::string>(); std::string command = payload["command"].get<std::string>();
std::reinterpret_pointer_cast<Ship::ConsoleWindow>(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command); std::reinterpret_pointer_cast<Ship::ConsoleWindow>(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command);
responsePayload["status"] = "success"; responsePayload["status"] = "success";
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} else if (payloadType == "effect") { } else if (payloadType == "effect") {
if (!payload.contains("effect") || !payload["effect"].contains("type")) { if (!payload.contains("effect") || !payload["effect"].contains("type")) {
SPDLOG_ERROR("[GameInteractorSail] Received effect payload without effect type"); SPDLOG_ERROR("[Sail] Received effect payload without effect type");
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; 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 // Special case for "command" effect, so we can also run commands from the `simple_twitch_sail` script
if (effectType == "command") { if (effectType == "command") {
if (!payload["effect"].contains("command")) { if (!payload["effect"].contains("command")) {
SPDLOG_ERROR("[GameInteractorSail] Received command effect payload without command"); SPDLOG_ERROR("[Sail] Received command effect payload without command");
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
std::string command = payload["effect"]["command"].get<std::string>(); std::string command = payload["effect"]["command"].get<std::string>();
std::reinterpret_pointer_cast<Ship::ConsoleWindow>(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command); std::reinterpret_pointer_cast<Ship::ConsoleWindow>(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command);
responsePayload["status"] = "success"; responsePayload["status"] = "success";
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
if (effectType != "apply" && effectType != "remove") { if (effectType != "apply" && effectType != "remove") {
SPDLOG_ERROR("[GameInteractorSail] Received effect payload with unknown effect type: {}", effectType); SPDLOG_ERROR("[Sail] Received effect payload with unknown effect type: {}", effectType);
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
if (!GameInteractor::IsSaveLoaded()) { if (!GameInteractor::IsSaveLoaded()) {
responsePayload["status"] = "try_again"; responsePayload["status"] = "try_again";
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
@ -124,26 +114,26 @@ void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) {
} else if (result == GameInteractionEffectQueryResult::TemporarilyNotPossible) { } else if (result == GameInteractionEffectQueryResult::TemporarilyNotPossible) {
responsePayload["status"] = "try_again"; responsePayload["status"] = "try_again";
} }
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
} else { } else {
SPDLOG_ERROR("[GameInteractorSail] Unknown payload type: {}", payloadType); SPDLOG_ERROR("[Sail] Unknown payload type: {}", payloadType);
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
return; return;
} }
// If we get here, something went wrong, send the failure response // If we get here, something went wrong, send the failure response
SPDLOG_ERROR("[GameInteractorSail] Failed to handle remote JSON, sending failure response"); SPDLOG_ERROR("[Sail] Failed to handle remote JSON, sending failure response");
GameInteractor::Instance->TransmitJsonToRemote(responsePayload); SendJsonToRemote(responsePayload);
} catch (const std::exception& e) { } catch (const std::exception& e) {
SPDLOG_ERROR("[GameInteractorSail] Exception handling remote JSON: {}", e.what()); SPDLOG_ERROR("[Sail] Exception handling remote JSON: {}", e.what());
} catch (...) { } 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")) { if (!payload.contains("name")) {
return nullptr; return nullptr;
} }
@ -331,22 +321,51 @@ GameInteractionEffectBase* GameInteractorSail::EffectFromJson(nlohmann::json pay
} else if (name == "SlipperyFloor") { } else if (name == "SlipperyFloor") {
return new GameInteractionEffect::SlipperyFloor(); return new GameInteractionEffect::SlipperyFloor();
} else { } else {
SPDLOG_INFO("[GameInteractorSail] Unknown effect name: {}", name); SPDLOG_INFO("[Sail] Unknown effect name: {}", name);
return nullptr; return nullptr;
} }
} }
// Workaround until we have a way to unregister hooks void Sail::RegisterHooks() {
static bool hasRegisteredHooks = false; 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() { GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnTransitionEnd>(onTransitionEndHook);
if (hasRegisteredHooks) { 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; return;
} }
hasRegisteredHooks = true;
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnTransitionEnd>([](int32_t sceneNum) { onTransitionEndHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnTransitionEnd>([&](int32_t sceneNum) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -354,10 +373,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["type"] = "OnTransitionEnd"; payload["hook"]["type"] = "OnTransitionEnd";
payload["hook"]["sceneNum"] = sceneNum; payload["hook"]["sceneNum"] = sceneNum;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnLoadGame>([](int32_t fileNum) { onLoadGameHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnLoadGame>([&](int32_t fileNum) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -365,10 +384,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["type"] = "OnLoadGame"; payload["hook"]["type"] = "OnLoadGame";
payload["hook"]["fileNum"] = fileNum; payload["hook"]["fileNum"] = fileNum;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnExitGame>([](int32_t fileNum) { onExitGameHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnExitGame>([&](int32_t fileNum) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -376,10 +395,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["type"] = "OnExitGame"; payload["hook"]["type"] = "OnExitGame";
payload["hook"]["fileNum"] = fileNum; payload["hook"]["fileNum"] = fileNum;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnItemReceive>([](GetItemEntry itemEntry) { onItemReceiveHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnItemReceive>([&](GetItemEntry itemEntry) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -388,10 +407,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["tableId"] = itemEntry.tableId; payload["hook"]["tableId"] = itemEntry.tableId;
payload["hook"]["getItemId"] = itemEntry.getItemId; payload["hook"]["getItemId"] = itemEntry.getItemId;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnEnemyDefeat>([](void* refActor) { onEnemyDefeatHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnEnemyDefeat>([&](void* refActor) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
Actor* actor = (Actor*)refActor; Actor* actor = (Actor*)refActor;
nlohmann::json payload; nlohmann::json payload;
@ -401,10 +420,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["actorId"] = actor->id; payload["hook"]["actorId"] = actor->id;
payload["hook"]["params"] = actor->params; payload["hook"]["params"] = actor->params;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorInit>([](void* refActor) { onActorInitHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorInit>([&](void* refActor) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
Actor* actor = (Actor*)refActor; Actor* actor = (Actor*)refActor;
nlohmann::json payload; nlohmann::json payload;
@ -414,10 +433,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["actorId"] = actor->id; payload["hook"]["actorId"] = actor->id;
payload["hook"]["params"] = actor->params; payload["hook"]["params"] = actor->params;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagSet>([](int16_t flagType, int16_t flag) { onFlagSetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagSet>([&](int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -426,10 +445,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["flagType"] = flagType; payload["hook"]["flagType"] = flagType;
payload["hook"]["flag"] = flag; payload["hook"]["flag"] = flag;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagUnset>([](int16_t flagType, int16_t flag) { onFlagUnsetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagUnset>([&](int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -438,10 +457,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["flagType"] = flagType; payload["hook"]["flagType"] = flagType;
payload["hook"]["flag"] = flag; payload["hook"]["flag"] = flag;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagSet>([](int16_t sceneNum, int16_t flagType, int16_t flag) { onSceneFlagSetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagSet>([&](int16_t sceneNum, int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -451,10 +470,10 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["flag"] = flag; payload["hook"]["flag"] = flag;
payload["hook"]["sceneNum"] = sceneNum; payload["hook"]["sceneNum"] = sceneNum;
GameInteractor::Instance->TransmitJsonToRemote(payload); SendJsonToRemote(payload);
}); });
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagUnset>([](int16_t sceneNum, int16_t flagType, int16_t flag) { onSceneFlagUnsetHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagUnset>([&](int16_t sceneNum, int16_t flagType, int16_t flag) {
if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; if (!isConnected || !GameInteractor::IsSaveLoaded()) return;
nlohmann::json payload; nlohmann::json payload;
payload["id"] = std::rand(); payload["id"] = std::rand();
@ -464,8 +483,76 @@ void GameInteractorSail::RegisterHooks() {
payload["hook"]["flag"] = flag; payload["hook"]["flag"] = flag;
payload["hook"]["sceneNum"] = sceneNum; 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 #ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_SAIL_H
#define NETWORK_SAIL_H
#ifdef __cplusplus #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: private:
bool isEnabled;
void HandleRemoteJson(nlohmann::json payload);
GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); GameInteractionEffectBase* EffectFromJson(nlohmann::json payload);
void RegisterHooks(); void RegisterHooks();
public: public:
static GameInteractorSail* Instance; static Sail* Instance;
void Enable(); 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" #include "ActorDB.h"
#ifdef ENABLE_REMOTE_CONTROL #ifdef ENABLE_REMOTE_CONTROL
#include "Enhancements/crowd-control/CrowdControl.h" #include "soh/Network/CrowdControl/CrowdControl.h"
#include "Enhancements/game-interactor/GameInteractor_Sail.h" #include "soh/Network/Sail/Sail.h"
CrowdControl* CrowdControl::Instance; CrowdControl* CrowdControl::Instance;
GameInteractorSail* GameInteractorSail::Instance; Sail* Sail::Instance;
#endif #endif
#include "Enhancements/mods.h" #include "Enhancements/mods.h"
@ -1173,7 +1173,7 @@ extern "C" void InitOTR() {
#ifdef ENABLE_REMOTE_CONTROL #ifdef ENABLE_REMOTE_CONTROL
CrowdControl::Instance = new CrowdControl(); CrowdControl::Instance = new CrowdControl();
GameInteractorSail::Instance = new GameInteractorSail(); Sail::Instance = new Sail();
#endif #endif
OTRMessage_Init(); OTRMessage_Init();
@ -1203,15 +1203,11 @@ extern "C" void InitOTR() {
srand(now); srand(now);
#ifdef ENABLE_REMOTE_CONTROL #ifdef ENABLE_REMOTE_CONTROL
SDLNet_Init(); SDLNet_Init();
if (CVarGetInteger(CVAR_REMOTE("Enabled"), 0)) { if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("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(); CrowdControl::Instance->Enable();
break;
} }
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Enable();
} }
#endif #endif
} }
@ -1224,15 +1220,11 @@ extern "C" void DeinitOTR() {
SaveManager_ThreadPoolWait(); SaveManager_ThreadPoolWait();
OTRAudio_Exit(); OTRAudio_Exit();
#ifdef ENABLE_REMOTE_CONTROL #ifdef ENABLE_REMOTE_CONTROL
if (CVarGetInteger(CVAR_REMOTE("Enabled"), 0)) { if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("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(); CrowdControl::Instance->Disable();
break;
} }
if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) {
Sail::Instance->Disable();
} }
SDLNet_Quit(); SDLNet_Quit();
#endif #endif

View File

@ -91,6 +91,8 @@ uint32_t IsGameMasterQuest();
#define CVAR_DEVELOPER_TOOLS(var) CVAR_PREFIX_DEVELOPER_TOOLS "." var #define CVAR_DEVELOPER_TOOLS(var) CVAR_PREFIX_DEVELOPER_TOOLS "." var
#define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var #define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var
#define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." 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 #ifndef __cplusplus
void InitOTR(void); void InitOTR(void);

View File

@ -33,11 +33,6 @@
#include "soh/resource/type/Skeleton.h" #include "soh/resource/type/Skeleton.h"
#include "libultraship/libultraship.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/game-interactor/GameInteractor.h"
#include "Enhancements/cosmetics/authenticGfxPatches.h" #include "Enhancements/cosmetics/authenticGfxPatches.h"
#include "Enhancements/resolution-editor/ResolutionEditor.h" #include "Enhancements/resolution-editor/ResolutionEditor.h"

View File

@ -17,8 +17,8 @@
#include "soh/Enhancements/mods.h" #include "soh/Enhancements/mods.h"
#include "Enhancements/cosmetics/authenticGfxPatches.h" #include "Enhancements/cosmetics/authenticGfxPatches.h"
#ifdef ENABLE_REMOTE_CONTROL #ifdef ENABLE_REMOTE_CONTROL
#include "Enhancements/crowd-control/CrowdControl.h" #include "soh/Network/CrowdControl/CrowdControl.h"
#include "Enhancements/game-interactor/GameInteractor_Sail.h" #include "soh/Network/Sail/Sail.h"
#endif #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 #ifdef ENABLE_REMOTE_CONTROL
void DrawRemoteControlMenu() { void DrawRemoteControlMenu() {
if (ImGui::BeginMenu("Network")) { if (ImGui::BeginMenu("Network")) {
static std::string ip = CVarGetString(CVAR_REMOTE("IP"), "127.0.0.1"); Sail::Instance->DrawMenu();
static uint16_t port = CVarGetInteger(CVAR_REMOTE("Port"), 43384); CrowdControl::Instance->DrawMenu();
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));
ImGui::EndMenu(); ImGui::EndMenu();
} }
} }

View File

@ -814,4 +814,18 @@ namespace UIWidgets {
float sz = ImGui::GetFrameHeight(); float sz = ImGui::GetFrameHeight();
return StateButtonEx(str_id, label, ImVec2(sz, sz), ImGuiButtonFlags_None); 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 DrawFlagArray16(const std::string& name, uint16_t& flags);
void DrawFlagArray8(const std::string& name, uint8_t& flags); void DrawFlagArray8(const std::string& name, uint8_t& flags);
bool StateButton(const char* str_id, const char* label); bool StateButton(const char* str_id, const char* label);
bool InputString(const char* label, std::string* value);
} }
#endif /* UIWidgets_hpp */ #endif /* UIWidgets_hpp */

View File

@ -390,3 +390,16 @@ size_t SohUtils::CopyStringToCharBuffer(char* buffer, const std::string& source,
return 0; 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 // 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. // 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); size_t CopyStringToCharBuffer(char* buffer, const std::string& source, size_t maxBufferSize);
bool IsStringEmpty(std::string str);
} // namespace SohUtils } // namespace SohUtils

View File

@ -335,7 +335,7 @@ void EnItem00_SetupAction(EnItem00* this, EnItem00ActionFunc actionFunc) {
void EnItem00_SetObjectDependency(EnItem00* this, PlayState* play, s16 objectIndex) { void EnItem00_SetObjectDependency(EnItem00* this, PlayState* play, s16 objectIndex) {
// Remove object dependency for Enemy Randomizer and Crowd Control to allow Like-likes to // 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. // 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; this->actor.objBankIndex = 0;
} else { } else {
this->actor.objBankIndex = Object_GetIndex(&play->objectCtx, objectIndex); 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); camera = GET_ACTIVE_CAM(play);
camId = camera->camDataIdx; 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 // This prevents a crash when using items that change the
// camera (such as din's fire), voiding out or dying on // camera (such as din's fire), voiding out or dying on
// scenes with prerendered backgrounds. // 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 || 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. // 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) || (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); Flags_SetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F);
BgMoriBigst_SetupDone(this, play); 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. // Check for if all enemies are defeated with enemy randomizer or crowd control on.
uint8_t roomCleared = uint8_t roomCleared =
(!CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) && (!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) || 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)); Flags_GetTempClear(play, this->dyna.actor.room));
if (roomCleared) { if (roomCleared) {
Flags_SetClear(play, this->dyna.actor.room); 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. // 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. // 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); Actor_ChangeCategory(play, &play->actorCtx, thisx, ACTORCAT_ENEMY);
} }

View File

@ -234,7 +234,7 @@ void func_80A74398(Actor* thisx, PlayState* play) {
func_80A74714(this); func_80A74714(this);
uint8_t enemyRandoCCActive = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || 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) { if (this->switchFlags != 0xFF) {
// In vanilla gameplay, Iron Knuckles are despawned based on specific flags in specific scenarios. // 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. // Don't set flag when Enemy Rando or CrowdControl are on.
// Instead Iron Knuckles rely on the "clear room" flag. // Instead Iron Knuckles rely on the "clear room" flag.
if (this->switchFlags != 0xFF && !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) && 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); Flags_SetSwitch(play, this->switchFlags);
} }
Actor_Kill(&this->actor); 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 // 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)) { && (thisx->params == 2 || thisx->params == 3)) {
this->skelAnime.playSpeed = 1.0f; 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. // 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 // 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. // 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 (!enemyRandoCCActive || (enemyRandoCCActive && this->actor.yDistToPlayer <= 100.0f && this->actor.yDistToPlayer >= -100.0f)) {
if ((this->actor.params != 2) && (this->unk_305 == 0)) { if ((this->actor.params != 2) && (this->unk_305 == 0)) {
func_80AE37BC(this); func_80AE37BC(this);
@ -668,7 +668,7 @@ void func_80AE3C98(EnRd* this, PlayState* play) {
if (SkelAnime_Update(&this->skelAnime)) { if (SkelAnime_Update(&this->skelAnime)) {
if (this->unk_30C == 0) { 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. // 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) { if (!Flags_GetSwitch(play, this->unk_312 & 0x7F) && !enemyRandoCCActive) {
Flags_SetSwitch(play, this->unk_312 & 0x7F); 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. // 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. // 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); Actor_ChangeCategory(play, &play->actorCtx, thisx, ACTORCAT_ENEMY);
} }