From e6e7a7b5492066558ea79587f8b0d70c4564ce6b Mon Sep 17 00:00:00 2001 From: David Chavez Date: Sun, 6 Nov 2022 11:00:34 +0100 Subject: [PATCH] Refactor CrowdControl Setup (#1890) --- CMakeLists.txt | 4 + Dockerfile | 9 + Jenkinsfile | 2 +- soh/CMakeLists.txt | 11 +- .../crowd-control/CrowdControl.cpp | 669 +++++++++--------- .../Enhancements/crowd-control/CrowdControl.h | 43 +- soh/soh/GameMenuBar.cpp | 12 + soh/soh/OTRGlobals.cpp | 5 +- 8 files changed, 420 insertions(+), 335 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d7875b23a..76eefe045 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,10 @@ set(PROJECT_TEAM "github.com/harbourmasters" CACHE STRING "") set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT soh) add_compile_options($<$:/MP>) +if (CMAKE_SYSTEM_NAME MATCHES "Windows|Linux") + set(BUILD_CROWD_CONTROL ON) +endif() + if (CMAKE_SYSTEM_NAME STREQUAL "Windows") include(CMake/automate-vcpkg.cmake) diff --git a/Dockerfile b/Dockerfile index 3115970bb..9020516a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,15 @@ RUN curl -sLO https://libsdl.org/release/SDL2-${SDL2VER}.tar.gz && \ rm ../SDL2-${SDL2VER}.tar.gz && \ cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/ +ENV SDL2NETVER=2.2.0 +RUN curl -sLO https://www.libsdl.org/projects/SDL_net/release/SDL2_net-${SDL2NETVER}.tar.gz && \ + tar -xzf SDL2_net-${SDL2NETVER}.tar.gz && \ + cd SDL2_net-${SDL2NETVER} && \ + ./configure --build=x86_64-linux-gnu && \ + make -j$(nproc) && make install && \ + rm ../SDL2_net-${SDL2NETVER}.tar.gz && \ + cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/ + RUN \ ln -sf /proc/self/mounts /etc/mtab && \ mkdir -p /usr/local/share/keyring/ && \ diff --git a/Jenkinsfile b/Jenkinsfile index 4ca4820f7..7446f3088 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -60,7 +60,7 @@ pipeline { catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { unstash 'assets' bat """ - "${env.CMAKE}" -S . -B "build\\${env.PLATFORM}" -G "Visual Studio 17 2022" -T ${env.TOOLSET} -A ${env.PLATFORM} -D Python_EXECUTABLE=${env.PYTHON} -D CMAKE_BUILD_TYPE:STRING=Release + "${env.CMAKE}" -S . -B "build\\${env.PLATFORM}" -G "Visual Studio 17 2022" -T ${env.TOOLSET} -A ${env.PLATFORM} -D Python_EXECUTABLE=${env.PYTHON} -DCMAKE_BUILD_TYPE:STRING=Release "${env.CMAKE}" --build ".\\build\\${env.PLATFORM}" --target OTRGui --config Release "${env.CMAKE}" --build ".\\build\\${env.PLATFORM}" --config Release cd ".\\build\\${env.PLATFORM}" diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index faf6012d1..e188698c0 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -250,7 +250,7 @@ set(Header_Files__soh__Enhancements__item_tables source_group("Header Files\\soh\\Enhancements\\item-tables" FILES ${Header_Files__soh__Enhancements__item_tables}) -if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +if (BUILD_CROWD_CONTROL) set(Header_Files__soh__Enhancements__crowd_control "soh/Enhancements/crowd-control/CrowdControl.h" ) @@ -397,7 +397,7 @@ set(Source_Files__soh__Enhancements__item_tables source_group("Source Files\\soh\\Enhancements\\item-tables" FILES ${Source_Files__soh__Enhancements__item_tables}) -if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +if (BUILD_CROWD_CONTROL) set(Source_Files__soh__Enhancements__crowd_control "soh/Enhancements/crowd-control/CrowdControl.cpp" ) @@ -1710,7 +1710,7 @@ endif() find_package(SDL2) set(SDL2-INCLUDE ${SDL2_INCLUDE_DIRS}) -if (CMAKE_SYSTEM_NAME STREQUAL "Windows") +if (BUILD_CROWD_CONTROL) find_package(SDL2_net) set(SDL2-NET-INCLUDE ${SDL_NET_INCLUDE_DIRS}) endif() @@ -1741,6 +1741,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "$<$:" "NDEBUG" ">" + "$<$:ENABLE_CROWD_CONTROL>" "INCLUDE_GAME_PRINTF;" "ENABLE_CROWD_CONTROL;" "UNICODE;" @@ -1787,6 +1788,7 @@ elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") "$<$:" "NDEBUG" ">" + "$<$:ENABLE_CROWD_CONTROL>" "SPDLOG_ACTIVE_LEVEL=0;" "_CONSOLE;" "_CRT_SECURE_NO_WARNINGS;" @@ -1984,7 +1986,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "glu32;" "SDL2::SDL2;" "SDL2::SDL2main;" - "SDL2_net::SDL2_net-static;" + "$<$:SDL2_net::SDL2_net-static>" "glfw;" "winmm;" "imm32;" @@ -2036,6 +2038,7 @@ else() "libultraship;" "ZAPDUtils;" SDL2::SDL2 + "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} Threads::Threads ) diff --git a/soh/soh/Enhancements/crowd-control/CrowdControl.cpp b/soh/soh/Enhancements/crowd-control/CrowdControl.cpp index b61e5972f..d87fd8d04 100644 --- a/soh/soh/Enhancements/crowd-control/CrowdControl.cpp +++ b/soh/soh/Enhancements/crowd-control/CrowdControl.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include extern "C" { @@ -19,268 +21,305 @@ extern PlayState* gPlayState; #define CMD_EXECUTE SohImGui::GetConsole()->Dispatch -void CrowdControl::InitCrowdControl() { +#define EFFECT_HIGH_GRAVITY "high_gravity" +#define EFFECT_LOW_GRAVITY "low_gravity" +#define EFFECT_DAMAGE_MULTIPLIER "damage_multiplier" +#define EFFECT_DEFENSE_MULTIPLIER "defense_multiplier" +#define EFFECT_GIANT_LINK "giant_link" +#define EFFECT_MINISH_LINK "minish_link" +#define EFFECT_INVISIBLE_LINK "invisible" +#define EFFECT_PAPER_LINK "paper_link" +#define EFFECT_FREEZE "freeze" +#define EFFECT_DAMAGE "damage" +#define EFFECT_HEAL "heal" +#define EFFECT_KNOCKBACK "knockback" +#define EFFECT_ELECTROCUTE "electrocute" +#define EFFECT_BURN "burn" +#define EFFECT_KILL "kill" +#define EFFECT_HOVER_BOOTS "hover_boots" +#define EFFECT_IRON_BOOTS "iron_boots" +#define EFFECT_ADD_HEART_CONTAINER "add_heart_container" +#define EFFECT_REMOVE_HEART_CONTAINER "remove_heart_container" +#define EFFECT_NO_UI "no_ui" +#define EFFECT_FILL_MAGIC "fill_magic" +#define EFFECT_EMPTY_MAGIC "empty_magic" +#define EFFECT_OHKO "ohko" +#define EFFECT_PACIFIST "pacifist" +#define EFFECT_RAINSTORM "rainstorm" +#define EFFECT_REVERSE_CONTROLS "reverse" +#define EFFECT_ADD_RUPEES "add_rupees" +#define EFFECT_REMOVE_RUPEES "remove_rupees" +#define EFFECT_INCREASE_SPEED "increase_speed" +#define EFFECT_DECREASE_SPEED "decrease_speed" +#define EFFECT_NO_Z_TARGETING "no_z" + +#define EFFECT_SPAWN_WALLMASTER "spawn_wallmaster" +#define EFFECT_SPAWN_ARWING "spawn_arwing" +#define EFFECT_SPAWN_DARK_LINK "spawn_darklink" +#define EFFECT_SPAWN_STALFOS "spawn_stalfos" +#define EFFECT_SPAWN_WOLFOS "spawn_wolfos" +#define EFFECT_SPAWN_FREEZARD "spawn_freezard" +#define EFFECT_SPAWN_KEESE "spawn_keese" +#define EFFECT_SPAWN_ICE_KEESE "spawn_icekeese" +#define EFFECT_SPAWN_FIRE_KEESE "spawn_firekeese" +#define EFFECT_SPAWN_TEKTITE "spawn_tektite" +#define EFFECT_SPAWN_LIKE_LIKE "spawn_likelike" +#define EFFECT_SPAWN_CUCCO_STORM "cucco_storm" + + +void CrowdControl::Init() { SDLNet_Init(); +} + +void CrowdControl::Shutdown() { + SDLNet_Quit(); +} + +void CrowdControl::Enable() { + if (isEnabled) { + return; + } if (SDLNet_ResolveHost(&ip, "127.0.0.1", 43384) == -1) { - printf("SDLNet_ResolveHost: %s\n", SDLNet_GetError()); + SPDLOG_ERROR("[CrowdControl] SDLNet_ResolveHost: {}", SDLNet_GetError()); } - ccThreadReceive = std::thread(&CrowdControl::ReceiveFromCrowdControl, this); + isEnabled = true; + ccThreadReceive = std::thread(&CrowdControl::ListenToServer, this); + ccThreadProcess = std::thread(&CrowdControl::ProcessActiveEffects, this); } -void CrowdControl::RunCrowdControl(CCPacket* packet) { - bool paused = false; - EffectResult lastResult = EffectResult::NotReady; - bool isTimed = packet->timeRemaining > 0; - - while (connected) { - EffectResult effectResult = ExecuteEffect(packet->effectType.c_str(), packet->effectValue, 0); - if (effectResult == EffectResult::Success) { - // If we have a success after being previously paused, we fire the Resumed event - if (paused && packet->timeRemaining > 0) { - paused = false; - nlohmann::json dataSend; - dataSend["id"] = packet->packetId; - dataSend["type"] = 0; - dataSend["timeRemaining"] = packet->timeRemaining; - dataSend["status"] = EffectResult::Resumed; - - std::string jsonResponse = dataSend.dump(); - SDLNet_TCP_Send(tcpsock, jsonResponse.c_str(), jsonResponse.size() + 1); - } - - // If time remaining has reached 0 or was 0, we have finished let's remove the command and end the thread - if (packet->timeRemaining <= 0) { - receivedCommandsMutex.lock(); - receivedCommands.erase(std::remove(receivedCommands.begin(), receivedCommands.end(), packet), receivedCommands.end()); - receivedCommandsMutex.unlock(); - RemoveEffect(packet->effectType.c_str()); - - // If not timed, let's fire the one and only success - if (!isTimed) { - nlohmann::json dataSend; - dataSend["id"] = packet->packetId; - dataSend["type"] = 0; - dataSend["timeRemaining"] = packet->timeRemaining; - dataSend["status"] = EffectResult::Success; - - std::string jsonResponse = dataSend.dump(); - SDLNet_TCP_Send(tcpsock, jsonResponse.c_str(), jsonResponse.size() + 1); - - delete packet; - } - - return; - } - - // Decrement remaining time by the second that elapsed between thread runs - packet->timeRemaining -= 1000; - // We don't want to emit repeated events, so ensure we only emit changes in events - if (effectResult != lastResult) { - lastResult = effectResult; - - nlohmann::json dataSend; - dataSend["id"] = packet->packetId; - dataSend["type"] = 0; - dataSend["timeRemaining"] = packet->timeRemaining; - dataSend["status"] = effectResult; - - std::string jsonResponse = dataSend.dump(); - SDLNet_TCP_Send(tcpsock, jsonResponse.c_str(), jsonResponse.size() + 1); - } - } else if (effectResult == EffectResult::Retry && paused == false && packet->timeRemaining > 0) { - paused = true; - nlohmann::json dataSend; - dataSend["id"] = packet->packetId; - dataSend["type"] = 0; - dataSend["timeRemaining"] = packet->timeRemaining; - dataSend["status"] = EffectResult::Paused; - - std::string jsonResponse = dataSend.dump(); - SDLNet_TCP_Send(tcpsock, jsonResponse.c_str(), jsonResponse.size() + 1); - } - - std::this_thread::sleep_for(std::chrono::seconds(1)); +void CrowdControl::Disable() { + if (!isEnabled) { + return; } + + isEnabled = false; + ccThreadReceive.join(); + ccThreadProcess.join(); } -void CrowdControl::ReceiveFromCrowdControl() -{ - printf("Waiting for a connection from Crowd Control..."); +void CrowdControl::ListenToServer() { + while (isEnabled) { + while (!connected) { + SPDLOG_TRACE("[CrowdControl] Attempting to make connection to server..."); + tcpsock = SDLNet_TCP_Open(&ip); - while (!connected && CVar_GetS32("gCrowdControl", 0)) { - tcpsock = SDLNet_TCP_Open(&ip); - - if (tcpsock) { - connected = true; - printf("Connected to Crowd Control!"); - break; - } - } - - while (connected && CVar_GetS32("gCrowdControl", 0) && tcpsock) { - int len = SDLNet_TCP_Recv(tcpsock, &received, sizeof(received)); - - if (!len || !tcpsock || len == -1) { - printf("SDLNet_TCP_Recv: %s\n", SDLNet_GetError()); - break; + if (tcpsock) { + connected = true; + SPDLOG_TRACE("[CrowdControl] Connection to server established!"); + break; + } } - try { - nlohmann::json dataReceived = nlohmann::json::parse(received); + auto socketSet = SDLNet_AllocSocketSet(1); + SDLNet_TCP_AddSocket(socketSet, tcpsock); - CCPacket* packet = new CCPacket(); - packet->packetId = dataReceived["id"]; - auto parameters = dataReceived["parameters"]; - if (parameters.size() > 0) { - packet->effectValue = dataReceived["parameters"][0]; - } - packet->effectType = dataReceived["code"].get(); + // Listen to socket messages + while (connected && tcpsock && isEnabled) { + // we check first if socket has data, to not block in the TCP_Recv + int socketsReady = SDLNet_CheckSockets(socketSet, 0); - if (packet->effectType == "high_gravity" || - packet->effectType == "low_gravity") { - packet->effectCategory = "gravity"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "damage_multiplier" - || packet->effectType == "defense_multiplier" - ) { - packet->effectCategory = "defense"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "giant_link" || - packet->effectType == "minish_link" || - packet->effectType == "invisible" || - packet->effectType == "paper_link") { - packet->effectCategory = "link_size"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "freeze" || - packet->effectType == "damage" || - packet->effectType == "heal" || - packet->effectType == "knockback" || - packet->effectType == "electrocute" || - packet->effectType == "burn" || - packet->effectType == "kill") { - packet->effectCategory = "link_damage"; - } - else if (packet->effectType == "hover_boots" || - packet->effectType == "iron_boots") { - packet->effectCategory = "boots"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "add_heart_container" || - packet->effectType == "remove_heart_container") { - packet->effectCategory = "heart_container"; - } - else if (packet->effectType == "no_ui") { - packet->effectCategory = "ui"; - packet->timeRemaining = 60000; - } - else if (packet->effectType == "fill_magic" || - packet->effectType == "empty_magic") { - packet->effectCategory = "magic"; - } - else if (packet->effectType == "ohko") { - packet->effectCategory = "ohko"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "pacifist") { - packet->effectCategory = "pacifist"; - packet->timeRemaining = 15000; - } - else if (packet->effectType == "rainstorm") { - packet->effectCategory = "weather"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "reverse") { - packet->effectCategory = "controls"; - packet->timeRemaining = 60000; - } - else if (packet->effectType == "add_rupees" - || packet->effectType == "remove_rupees" - ) { - packet->effectCategory = "rupees"; - } - else if (packet->effectType == "increase_speed" - || packet->effectType == "decrease_speed" - ) { - packet->effectCategory = "speed"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "no_z") { - packet->effectCategory = "no_z"; - packet->timeRemaining = 30000; - } - else if (packet->effectType == "spawn_wallmaster" || - packet->effectType == "spawn_arwing" || - packet->effectType == "spawn_darklink" || - packet->effectType == "spawn_stalfos" || - packet->effectType == "spawn_wolfos" || - packet->effectType == "spawn_freezard" || - packet->effectType == "spawn_keese" || - packet->effectType == "spawn_icekeese" || - packet->effectType == "spawn_firekeese" || - packet->effectType == "spawn_tektite" || - packet->effectType == "spawn_likelike" || - packet->effectType == "cucco_storm") { - packet->effectCategory = "spawn"; - } - else { - packet->effectCategory = "none"; - packet->timeRemaining = 0; + if (socketsReady == -1) { + SPDLOG_ERROR("[CrowdControl] SDLNet_CheckSockets: {}", SDLNet_GetError()); + break; } - // Check if running effect is possible - EffectResult effectResult = ExecuteEffect(packet->effectType.c_str(), packet->effectValue, 1); - if (effectResult == EffectResult::Retry || effectResult == EffectResult::Failure) { - nlohmann::json dataSend; - dataSend["id"] = packet->packetId; - dataSend["type"] = 0; - dataSend["timeRemaining"] = packet->timeRemaining; - dataSend["status"] = effectResult; + if (socketsReady == 0) { + continue; + } - std::string jsonResponse = dataSend.dump(); - SDLNet_TCP_Send(tcpsock, jsonResponse.c_str(), jsonResponse.size() + 1); + int len = SDLNet_TCP_Recv(tcpsock, &received, sizeof(received)); + if (!len || !tcpsock || len == -1) { + SPDLOG_ERROR("[CrowdControl] SDLNet_TCP_Recv: {}", SDLNet_GetError()); + break; + } + + Effect* incomingEffect = ParseMessage(received); + if (!incomingEffect) { + continue; + } + + // If effect is a one off run, let's execute + if (!incomingEffect->timeRemaining) { + EffectResult result = + ExecuteEffect(incomingEffect->type.c_str(), incomingEffect->value, false); + EmitMessage(tcpsock, incomingEffect->id, incomingEffect->timeRemaining, result); } else { - bool anotherEffectOfCategoryActive = false; - for (CCPacket* pack : receivedCommands) { - if (pack != packet && pack->effectCategory == packet->effectCategory && pack->packetId < packet->packetId) { - anotherEffectOfCategoryActive = true; - - nlohmann::json dataSend; - dataSend["id"] = packet->packetId; - dataSend["type"] = 0; - dataSend["timeRemaining"] = packet->timeRemaining; - dataSend["status"] = EffectResult::Retry; - - std::string jsonResponse = dataSend.dump(); - SDLNet_TCP_Send(tcpsock, jsonResponse.c_str(), jsonResponse.size() + 1); + // check if a conflicting event is already active + bool isConflictingEffectActive = false; + for (Effect* pack : activeEffects) { + if (pack != incomingEffect && pack->category == incomingEffect->category && + pack->id < incomingEffect->id) { + isConflictingEffectActive = true; + EmitMessage(tcpsock, incomingEffect->id, incomingEffect->timeRemaining, + EffectResult::Retry); break; } } - if (anotherEffectOfCategoryActive != true) { - receivedCommandsMutex.lock(); - receivedCommands.push_back(packet); - receivedCommandsMutex.unlock(); - std::thread t = std::thread(&CrowdControl::RunCrowdControl, this, packet); - t.detach(); + // check if effect can be executed + EffectResult result = + ExecuteEffect(incomingEffect->type.c_str(), incomingEffect->value, true); + if (result == EffectResult::Retry || result == EffectResult::Failure) { + EmitMessage(tcpsock, incomingEffect->id, incomingEffect->timeRemaining, result); + continue; + } + + if (!isConflictingEffectActive) { + activeEffectsMutex.lock(); + activeEffects.push_back(incomingEffect); + activeEffectsMutex.unlock(); } } - } catch (nlohmann::json::parse_error& e) { - printf("Error parsing JSON: %s\n", e.what()); - continue; + } + + if (connected) { + SDLNet_TCP_Close(tcpsock); + connected = false; + SPDLOG_TRACE("[CrowdControl] Ending Listen thread..."); } } +} - if (connected) { - SDLNet_TCP_Close(tcpsock); - SDLNet_Quit(); - connected = false; +void CrowdControl::ProcessActiveEffects() { + while (isEnabled) { + // we only want to send events when status changes, on start we send Success, + // if it fails at some point, we send Pause, and when it starts to succeed again we send Success. + activeEffectsMutex.lock(); + auto it = activeEffects.begin(); + + while (it != activeEffects.end()) { + Effect *effect = *it; + EffectResult result = ExecuteEffect(effect->type.c_str(), effect->value, false); + if (result == EffectResult::Success) { + // If time remaining has reached 0, we have finished the effect + if (effect->timeRemaining <= 0) { + it = activeEffects.erase(std::remove(activeEffects.begin(), activeEffects.end(), effect), + activeEffects.end()); + RemoveEffect(effect->type.c_str()); + + delete effect; + } else { + // If we have a success after previously being paused, fire Resume event + if (effect->isPaused) { + effect->isPaused = false; + EmitMessage(tcpsock, effect->id, effect->timeRemaining, EffectResult::Resumed); + } else { + effect->timeRemaining -= 1000; + if (result != effect->lastExecutionResult) { + effect->lastExecutionResult = result; + EmitMessage(tcpsock, effect->id, effect->timeRemaining, EffectResult::Success); + } + } + + it++; + } + } else { // Timed effects only do Success or Retry + if (!effect->isPaused && effect->timeRemaining > 0) { + effect->isPaused = true; + EmitMessage(tcpsock, effect->id, effect->timeRemaining, EffectResult::Paused); + } + + it++; + } + } + + activeEffectsMutex.unlock(); + std::this_thread::sleep_for(std::chrono::seconds(1)); } + + SPDLOG_TRACE("[CrowdControl] Ending Process thread..."); +} + +// MARK: - Helpers + +void CrowdControl::EmitMessage(TCPsocket socket, uint32_t eventId, long timeRemaining, EffectResult status) { + nlohmann::json payload; + + payload["id"] = eventId; + payload["type"] = 0; + payload["timeRemaining"] = timeRemaining; + payload["status"] = status; + + std::string jsonPayload = payload.dump(); + SDLNet_TCP_Send(socket, jsonPayload.c_str(), jsonPayload.size() + 1); +} + +CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { + nlohmann::json dataReceived = nlohmann::json::parse(payload, nullptr, false); + if (dataReceived.is_discarded()) { + SPDLOG_ERROR("Error parsing JSON"); + return nullptr; + } + + Effect* effect = new Effect(); + effect->lastExecutionResult = EffectResult::Initiate; + effect->id = dataReceived["id"]; + auto parameters = dataReceived["parameters"]; + if (parameters.size() > 0) { + effect->value = dataReceived["parameters"][0]; + } + effect->type = dataReceived["code"].get(); + + if (effect->type == EFFECT_HIGH_GRAVITY || effect->type == EFFECT_LOW_GRAVITY) { + effect->category = "gravity"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_DAMAGE_MULTIPLIER || effect->type == EFFECT_DEFENSE_MULTIPLIER) { + effect->category = "defense"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_GIANT_LINK || effect->type == EFFECT_MINISH_LINK || + effect->type == EFFECT_INVISIBLE_LINK || effect->type == EFFECT_PAPER_LINK) { + effect->category = "link_size"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_FREEZE || effect->type == EFFECT_DAMAGE || effect->type == EFFECT_HEAL || + effect->type == EFFECT_KNOCKBACK || effect->type == EFFECT_ELECTROCUTE || + effect->type == EFFECT_BURN || effect->type == EFFECT_KILL) { + effect->category = "link_damage"; + } else if (effect->type == EFFECT_HOVER_BOOTS || effect->type == EFFECT_IRON_BOOTS) { + effect->category = "boots"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_ADD_HEART_CONTAINER || effect->type == EFFECT_REMOVE_HEART_CONTAINER) { + effect->category = "heart_container"; + } else if (effect->type == EFFECT_NO_UI) { + effect->category = "ui"; + effect->timeRemaining = 60000; + } else if (effect->type == EFFECT_FILL_MAGIC || effect->type == EFFECT_EMPTY_MAGIC) { + effect->category = "magic"; + } else if (effect->type == EFFECT_OHKO) { + effect->category = "ohko"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_PACIFIST) { + effect->category = "pacifist"; + effect->timeRemaining = 15000; + } else if (effect->type == EFFECT_RAINSTORM) { + effect->category = "weather"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_REVERSE_CONTROLS) { + effect->category = "controls"; + effect->timeRemaining = 60000; + } else if (effect->type == EFFECT_ADD_RUPEES || effect->type == EFFECT_REMOVE_RUPEES) { + effect->category = "rupees"; + } else if (effect->type == EFFECT_INCREASE_SPEED || effect->type == EFFECT_DECREASE_SPEED) { + effect->category = "speed"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_NO_Z_TARGETING) { + effect->category = "no_z"; + effect->timeRemaining = 30000; + } else if (effect->type == EFFECT_SPAWN_WALLMASTER || effect->type == EFFECT_SPAWN_ARWING || + effect->type == EFFECT_SPAWN_DARK_LINK || effect->type == EFFECT_SPAWN_STALFOS || + effect->type == EFFECT_SPAWN_WOLFOS || effect->type == EFFECT_SPAWN_FREEZARD || + effect->type == EFFECT_SPAWN_KEESE || effect->type == EFFECT_SPAWN_ICE_KEESE || + effect->type == EFFECT_SPAWN_FIRE_KEESE || effect->type == EFFECT_SPAWN_TEKTITE || + effect->type == EFFECT_SPAWN_LIKE_LIKE || effect->type == EFFECT_SPAWN_CUCCO_STORM) { + effect->category = "spawn"; + } else { + effect->category = "none"; + effect->timeRemaining = 0; + } + + return effect; } CrowdControl::EffectResult CrowdControl::ExecuteEffect(std::string effectId, uint32_t value, bool dryRun) { @@ -293,21 +332,21 @@ CrowdControl::EffectResult CrowdControl::ExecuteEffect(std::string effectId, uin Player* player = GET_PLAYER(gPlayState); if (player != NULL) { - if (effectId == "add_heart_container") { + if (effectId == EFFECT_ADD_HEART_CONTAINER) { if (gSaveContext.healthCapacity >= 0x140) { return EffectResult::Failure; } - if (dryRun == 0) CMD_EXECUTE("add_heart_container"); + if (dryRun == 0) CMD_EXECUTE(EFFECT_ADD_HEART_CONTAINER); return EffectResult::Success; - } else if (effectId == "remove_heart_container") { + } else if (effectId == EFFECT_REMOVE_HEART_CONTAINER) { if ((gSaveContext.healthCapacity - 0x10) <= 0) { return EffectResult::Failure; } - if (dryRun == 0) CMD_EXECUTE("remove_heart_container"); + if (dryRun == 0) CMD_EXECUTE(EFFECT_REMOVE_HEART_CONTAINER); return EffectResult::Success; - } else if (effectId == "fill_magic") { + } else if (effectId == EFFECT_FILL_MAGIC) { if (!gSaveContext.magicAcquired) { return EffectResult::Failure; } @@ -316,87 +355,87 @@ CrowdControl::EffectResult CrowdControl::ExecuteEffect(std::string effectId, uin return EffectResult::Failure; } - if (dryRun == 0) CMD_EXECUTE("fill_magic"); + if (dryRun == 0) CMD_EXECUTE(EFFECT_FILL_MAGIC); return EffectResult::Success; - } else if (effectId == "empty_magic") { + } else if (effectId == EFFECT_EMPTY_MAGIC) { if (!gSaveContext.magicAcquired || gSaveContext.magic <= 0) { return EffectResult::Failure; } - if (dryRun == 0) CMD_EXECUTE("empty_magic"); + if (dryRun == 0) CMD_EXECUTE(EFFECT_EMPTY_MAGIC); return EffectResult::Success; - } else if (effectId == "add_rupees") { - if (dryRun == 0) CMD_EXECUTE(std::format("update_rupees {}", value)); + } else if (effectId == EFFECT_ADD_RUPEES) { + if (dryRun == 0) CMD_EXECUTE(fmt::format("update_rupees {}", value)); return EffectResult::Success; - } else if (effectId == "remove_rupees") { + } else if (effectId == EFFECT_REMOVE_RUPEES) { if (gSaveContext.rupees - value < 0) { return EffectResult::Failure; } - if (dryRun == 0) CMD_EXECUTE(std::format("update_rupees -{}", value)); + if (dryRun == 0) CMD_EXECUTE(fmt::format("update_rupees -{}", value)); return EffectResult::Success; } } if (player != NULL && !Player_InBlockingCsMode(gPlayState, player) && gPlayState->pauseCtx.state == 0 && gPlayState->msgCtx.msgMode == 0) { - if (effectId == "high_gravity") { + if (effectId == EFFECT_HIGH_GRAVITY) { if (dryRun == 0) CMD_EXECUTE("gravity 2"); return EffectResult::Success; - } else if (effectId == "low_gravity") { + } else if (effectId == EFFECT_LOW_GRAVITY) { if (dryRun == 0) CMD_EXECUTE("gravity 0"); return EffectResult::Success; - } else if (effectId == "kill" - || effectId == "freeze" - || effectId == "burn" - || effectId == "electrocute" - || effectId == "cucco_storm" + } else if (effectId == EFFECT_KILL + || effectId == EFFECT_FREEZE + || effectId == EFFECT_BURN + || effectId == EFFECT_ELECTROCUTE + || effectId == EFFECT_SPAWN_CUCCO_STORM ) { if (PlayerGrounded(player)) { - if (dryRun == 0) CMD_EXECUTE(std::format("{}", effectId)); + if (dryRun == 0) CMD_EXECUTE(fmt::format("{}", effectId)); return EffectResult::Success; } return EffectResult::Failure; - } else if (effectId == "heal" - || effectId == "knockback" + } else if (effectId == EFFECT_HEAL + || effectId == EFFECT_KNOCKBACK ) { - if (dryRun == 0) CMD_EXECUTE(std::format("{} {}", effectId, value)); + if (dryRun == 0) CMD_EXECUTE(fmt::format("{} {}", effectId, value)); return EffectResult::Success; - } else if (effectId == "giant_link" - || effectId == "minish_link" - || effectId == "no_ui" - || effectId == "invisible" - || effectId == "paper_link" - || effectId == "no_z" - || effectId == "ohko" - || effectId == "pacifist" - || effectId == "rainstorm" + } else if (effectId == EFFECT_GIANT_LINK + || effectId == EFFECT_MINISH_LINK + || effectId == EFFECT_NO_UI + || effectId == EFFECT_INVISIBLE_LINK + || effectId == EFFECT_PAPER_LINK + || effectId == EFFECT_NO_Z_TARGETING + || effectId == EFFECT_OHKO + || effectId == EFFECT_PACIFIST + || effectId == EFFECT_RAINSTORM ) { - if (dryRun == 0) CMD_EXECUTE(std::format("{} 1", effectId)); + if (dryRun == 0) CMD_EXECUTE(fmt::format("{} 1", effectId)); return EffectResult::Success; - } else if (effectId == "reverse") { + } else if (effectId == EFFECT_REVERSE_CONTROLS) { if (dryRun == 0) CMD_EXECUTE("reverse_controls 1"); return EffectResult::Success; - } else if (effectId == "iron_boots") { + } else if (effectId == EFFECT_IRON_BOOTS) { if (dryRun == 0) CMD_EXECUTE("boots iron"); return EffectResult::Success; - } else if (effectId == "hover_boots") { + } else if (effectId == EFFECT_HOVER_BOOTS) { if (dryRun == 0) CMD_EXECUTE("boots hover"); return EffectResult::Success; } else if (effectId == "give_dekushield") { if (dryRun == 0) CMD_EXECUTE("givedekushield"); return EffectResult::Success; - } else if (effectId == "spawn_wallmaster" - || effectId == "spawn_arwing" - || effectId == "spawn_darklink" - || effectId == "spawn_stalfos" - || effectId == "spawn_wolfos" - || effectId == "spawn_freezard" - || effectId == "spawn_keese" - || effectId == "spawn_icekeese" - || effectId == "spawn_firekeese" - || effectId == "spawn_tektite" - || effectId == "spawn_likelike" + } else if (effectId == EFFECT_SPAWN_WALLMASTER + || effectId == EFFECT_SPAWN_ARWING + || effectId == EFFECT_SPAWN_DARK_LINK + || effectId == EFFECT_SPAWN_STALFOS + || effectId == EFFECT_SPAWN_WOLFOS + || effectId == EFFECT_SPAWN_FREEZARD + || effectId == EFFECT_SPAWN_KEESE + || effectId == EFFECT_SPAWN_ICE_KEESE + || effectId == EFFECT_SPAWN_FIRE_KEESE + || effectId == EFFECT_SPAWN_TEKTITE + || effectId == EFFECT_SPAWN_LIKE_LIKE ) { if (dryRun == 0) { if (CrowdControl::SpawnEnemy(effectId)) { @@ -406,24 +445,24 @@ CrowdControl::EffectResult CrowdControl::ExecuteEffect(std::string effectId, uin } } return EffectResult::Success; - } else if (effectId == "increase_speed") { + } else if (effectId == EFFECT_INCREASE_SPEED) { if (dryRun == 0) CMD_EXECUTE("speed_modifier 2"); return EffectResult::Success; - } else if (effectId == "decrease_speed") { + } else if (effectId == EFFECT_DECREASE_SPEED) { if (dryRun == 0) CMD_EXECUTE("speed_modifier -2"); return EffectResult::Success; - } else if (effectId == "damage_multiplier") { - if (dryRun == 0) CMD_EXECUTE(std::format("defense_modifier -{}", value)); + } else if (effectId == EFFECT_DAMAGE_MULTIPLIER) { + if (dryRun == 0) CMD_EXECUTE(fmt::format("defense_modifier -{}", value)); return EffectResult::Success; - } else if (effectId == "defense_multiplier") { - if (dryRun == 0) CMD_EXECUTE(std::format("defense_modifier {}", value)); + } else if (effectId == EFFECT_DEFENSE_MULTIPLIER) { + if (dryRun == 0) CMD_EXECUTE(fmt::format("defense_modifier {}", value)); return EffectResult::Success; - } else if (effectId == "damage") { + } else if (effectId == EFFECT_DAMAGE) { if ((gSaveContext.healthCapacity - 0x10) <= 0) { return EffectResult::Failure; } - if (dryRun == 0) CMD_EXECUTE(std::format("{} {}", effectId, value)); + if (dryRun == 0) CMD_EXECUTE(fmt::format("{} {}", effectId, value)); return EffectResult::Success; } } @@ -440,49 +479,49 @@ bool CrowdControl::SpawnEnemy(std::string effectId) { float posYOffset = 0; float posZOffset = 0; - if (effectId == "spawn_wallmaster") { + if (effectId == EFFECT_SPAWN_WALLMASTER) { enemyId = 17; - } else if (effectId == "spawn_arwing") { + } else if (effectId == EFFECT_SPAWN_ARWING) { enemyId = 315; enemyParams = 1; posYOffset = 100; - } else if (effectId == "spawn_darklink") { + } else if (effectId == EFFECT_SPAWN_DARK_LINK) { enemyId = 51; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_stalfos") { + } else if (effectId == EFFECT_SPAWN_STALFOS) { enemyId = 2; enemyParams = 2; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_wolfos") { + } else if (effectId == EFFECT_SPAWN_WOLFOS) { enemyId = 431; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_freezard") { + } else if (effectId == EFFECT_SPAWN_FREEZARD) { enemyId = 289; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_keese") { + } else if (effectId == EFFECT_SPAWN_KEESE) { enemyId = 19; enemyParams = 2; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_icekeese") { + } else if (effectId == EFFECT_SPAWN_ICE_KEESE) { enemyId = 19; enemyParams = 4; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_firekeese") { + } else if (effectId == EFFECT_SPAWN_FIRE_KEESE) { enemyId = 19; enemyParams = 1; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_tektite") { + } else if (effectId == EFFECT_SPAWN_TEKTITE) { enemyId = 27; posXOffset = 75; posYOffset = 50; - } else if (effectId == "spawn_likelike") { + } else if (effectId == EFFECT_SPAWN_LIKE_LIKE) { enemyId = 221; posXOffset = 75; posYOffset = 50; @@ -501,34 +540,34 @@ void CrowdControl::RemoveEffect(std::string effectId) { Player* player = GET_PLAYER(gPlayState); if (player != NULL) { - if (effectId == "giant_link" - || effectId == "minish_link" - || effectId == "no_ui" - || effectId == "invisible" - || effectId == "paper_link" - || effectId == "no_z" - || effectId == "ohko" - || effectId == "pacifist" - || effectId == "rainstorm" + if (effectId == EFFECT_GIANT_LINK + || effectId == EFFECT_MINISH_LINK + || effectId == EFFECT_NO_UI + || effectId == EFFECT_INVISIBLE_LINK + || effectId == EFFECT_PAPER_LINK + || effectId == EFFECT_NO_Z_TARGETING + || effectId == EFFECT_OHKO + || effectId == EFFECT_PACIFIST + || effectId == EFFECT_RAINSTORM ) { - CMD_EXECUTE(std::format("{} 0", effectId)); + CMD_EXECUTE(fmt::format("{} 0", effectId)); return; - } else if (effectId == "iron_boots" || effectId == "hover_boots") { + } else if (effectId == EFFECT_IRON_BOOTS || effectId == EFFECT_HOVER_BOOTS) { CMD_EXECUTE("boots kokiri"); return; - } else if (effectId == "high_gravity" || effectId == "low_gravity") { + } else if (effectId == EFFECT_HIGH_GRAVITY || effectId == EFFECT_LOW_GRAVITY) { CMD_EXECUTE("gravity 1"); return; - } else if (effectId == "reverse") { + } else if (effectId == EFFECT_REVERSE_CONTROLS) { CMD_EXECUTE("reverse_controls 0"); return; - } else if (effectId == "increase_speed" - || effectId == "decrease_speed" + } else if (effectId == EFFECT_INCREASE_SPEED + || effectId == EFFECT_DECREASE_SPEED ) { CMD_EXECUTE("speed_modifier 0"); return; - } else if (effectId == "damage_multiplier" - || effectId == "defense_multiplier" + } else if (effectId == EFFECT_DAMAGE_MULTIPLIER + || effectId == EFFECT_DEFENSE_MULTIPLIER ) { CMD_EXECUTE("defense_modifier 0"); return; diff --git a/soh/soh/Enhancements/crowd-control/CrowdControl.h b/soh/soh/Enhancements/crowd-control/CrowdControl.h index d35e19ef8..6b1dfd4ee 100644 --- a/soh/soh/Enhancements/crowd-control/CrowdControl.h +++ b/soh/soh/Enhancements/crowd-control/CrowdControl.h @@ -19,14 +19,6 @@ class CrowdControl { private: - typedef struct CCPacket { - uint32_t packetId; - std::string effectType; - uint32_t effectValue; - std::string effectCategory; - long timeRemaining; - } CCPacket; - enum EffectResult { /// The effect executed successfully. Success = 0x00, @@ -46,6 +38,8 @@ class CrowdControl { Resumed = 0x07, /// The timed effect has finished. Finished = 0x08, + /// Effect is being initiated. SoH exclusive to check against if an effect state has changed or not. + Initiate = 0xFE, /// The processor isn't ready to start or has shut down. NotReady = 0xFF }; @@ -64,28 +58,49 @@ class CrowdControl { long timeRemaining; ResponseType type = ResponseType::EffectRequest; }; + + typedef struct Effect { + uint32_t id; + std::string type; + uint32_t value; + std::string category; + long timeRemaining; + + // Metadata used while executing (only for timed effects) + bool isPaused; + EffectResult lastExecutionResult; + } Effect; std::thread ccThreadReceive; + std::thread ccThreadProcess; TCPsocket tcpsock; IPaddress ip; + bool isEnabled; bool connected; char received[512]; - std::vector receivedCommands; - std::mutex receivedCommandsMutex; + std::vector activeEffects; + std::mutex activeEffectsMutex; - void RunCrowdControl(CCPacket* packet); - void ReceiveFromCrowdControl(); + void ListenToServer(); + void ProcessActiveEffects(); + + void EmitMessage(TCPsocket socket, uint32_t eventId, long timeRemaining, + CrowdControl::EffectResult status); + Effect* ParseMessage(char payload[512]); EffectResult ExecuteEffect(std::string effectId, uint32_t value, bool dryRun); - bool SpawnEnemy(std::string effectId); void RemoveEffect(std::string effectId); + bool SpawnEnemy(std::string effectId); public: static CrowdControl* Instance; - void InitCrowdControl(); + void Init(); + void Shutdown(); + void Enable(); + void Disable(); }; #endif #endif diff --git a/soh/soh/GameMenuBar.cpp b/soh/soh/GameMenuBar.cpp index 918efa2a6..0a9e4d1ce 100644 --- a/soh/soh/GameMenuBar.cpp +++ b/soh/soh/GameMenuBar.cpp @@ -28,6 +28,10 @@ #include "soh/SaveManager.h" #include "OTRGlobals.h" +#ifdef ENABLE_CROWD_CONTROL +#include "Enhancements/crowd-control/CrowdControl.h" +#endif + #define EXPERIMENTAL() \ ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 50, 50, 255)); \ UIWidgets::Spacer(3.0f); \ @@ -1545,9 +1549,17 @@ namespace GameMenuBar { } ImGui::PopStyleVar(3); ImGui::PopStyleColor(1); +#ifdef ENABLE_CROWD_CONTROL UIWidgets::PaddedEnhancementCheckbox("Crowd Control", "gCrowdControl", true, false); UIWidgets::Tooltip("Requires a full SoH restart to take effect!\n\nEnables CrowdControl. Will attempt to connect to the local Crowd Control server."); + if (CVar_GetS32("gCrowdControl", 0)) { + CrowdControl::Instance->Enable(); + } else { + CrowdControl::Instance->Disable(); + } +#endif + UIWidgets::PaddedSeparator(); if (ImGui::BeginMenu("Rando Enhancements")) diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 1e2487348..25ebf9847 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -451,12 +451,15 @@ extern "C" void InitOTR() { #ifdef ENABLE_CROWD_CONTROL CrowdControl::Instance = new CrowdControl(); - CrowdControl::Instance->InitCrowdControl(); + CrowdControl::Instance->Init(); #endif } extern "C" void DeinitOTR() { OTRAudio_Exit(); +#ifdef ENABLE_CROWD_CONTROL + CrowdControl::Instance->Shutdown(); +#endif } #ifdef _WIN32