#ifdef ENABLE_CROWD_CONTROL #include "CrowdControl.h" #include #include #include #include #include #include #include extern "C" { #include #include "variables.h" #include "functions.h" #include "macros.h" extern PlayState* gPlayState; } #include "../debugconsole.h" #define CMD_EXECUTE SohImGui::GetConsole()->Dispatch #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) { SPDLOG_ERROR("[CrowdControl] SDLNet_ResolveHost: {}", SDLNet_GetError()); } isEnabled = true; ccThreadReceive = std::thread(&CrowdControl::ListenToServer, this); ccThreadProcess = std::thread(&CrowdControl::ProcessActiveEffects, this); } void CrowdControl::Disable() { if (!isEnabled) { return; } isEnabled = false; ccThreadReceive.join(); ccThreadProcess.join(); } void CrowdControl::ListenToServer() { while (isEnabled) { while (!connected && isEnabled) { SPDLOG_TRACE("[CrowdControl] Attempting to make connection to server..."); tcpsock = SDLNet_TCP_Open(&ip); if (tcpsock) { connected = true; SPDLOG_TRACE("[CrowdControl] Connection to server established!"); break; } } SDLNet_SocketSet socketSet = SDLNet_AllocSocketSet(1); if (tcpsock) { SDLNet_TCP_AddSocket(socketSet, tcpsock); } // 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 (socketsReady == -1) { SPDLOG_ERROR("[CrowdControl] SDLNet_CheckSockets: {}", SDLNet_GetError()); break; } if (socketsReady == 0) { continue; } 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 { // 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; } } // 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(); } } } if (connected) { SDLNet_TCP_Close(tcpsock); connected = false; SPDLOG_TRACE("[CrowdControl] Ending Listen thread..."); } } } 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) { // Don't execute effect and don't advance timer when the player is not in a proper loaded savefile // and when they're busy dying. if (gPlayState == NULL || gPlayState->gameOverCtx.state > 0 || gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) { return EffectResult::Retry; } Player* player = GET_PLAYER(gPlayState); if (player != NULL) { if (effectId == EFFECT_ADD_HEART_CONTAINER) { if (gSaveContext.healthCapacity >= 0x140) { return EffectResult::Failure; } if (dryRun == 0) CMD_EXECUTE(EFFECT_ADD_HEART_CONTAINER); return EffectResult::Success; } else if (effectId == EFFECT_REMOVE_HEART_CONTAINER) { if ((gSaveContext.healthCapacity - 0x10) <= 0) { return EffectResult::Failure; } if (dryRun == 0) CMD_EXECUTE(EFFECT_REMOVE_HEART_CONTAINER); return EffectResult::Success; } else if (effectId == EFFECT_FILL_MAGIC) { if (!gSaveContext.isMagicAcquired) { return EffectResult::Failure; } if (gSaveContext.magic >= (gSaveContext.isDoubleMagicAcquired + 1) + 0x30) { return EffectResult::Failure; } if (dryRun == 0) CMD_EXECUTE(EFFECT_FILL_MAGIC); return EffectResult::Success; } else if (effectId == EFFECT_EMPTY_MAGIC) { if (!gSaveContext.isMagicAcquired || gSaveContext.magic <= 0) { return EffectResult::Failure; } if (dryRun == 0) CMD_EXECUTE(EFFECT_EMPTY_MAGIC); return EffectResult::Success; } else if (effectId == EFFECT_ADD_RUPEES) { if (dryRun == 0) CMD_EXECUTE(fmt::format("update_rupees {}", value)); return EffectResult::Success; } else if (effectId == EFFECT_REMOVE_RUPEES) { if (gSaveContext.rupees - value < 0) { return EffectResult::Failure; } 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 == EFFECT_HIGH_GRAVITY) { if (dryRun == 0) CMD_EXECUTE("gravity 2"); return EffectResult::Success; } else if (effectId == EFFECT_LOW_GRAVITY) { if (dryRun == 0) CMD_EXECUTE("gravity 0"); return EffectResult::Success; } 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(fmt::format("{}", effectId)); return EffectResult::Success; } return EffectResult::Failure; } else if (effectId == EFFECT_HEAL || effectId == EFFECT_KNOCKBACK ) { if (dryRun == 0) CMD_EXECUTE(fmt::format("{} {}", effectId, value)); return EffectResult::Success; } 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(fmt::format("{} 1", effectId)); return EffectResult::Success; } else if (effectId == EFFECT_REVERSE_CONTROLS) { if (dryRun == 0) CMD_EXECUTE("reverse_controls 1"); return EffectResult::Success; } else if (effectId == EFFECT_IRON_BOOTS) { if (dryRun == 0) CMD_EXECUTE("boots iron"); return EffectResult::Success; } 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 == 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)) { return EffectResult::Success; } else { return EffectResult::Failure; } } return EffectResult::Success; } else if (effectId == EFFECT_INCREASE_SPEED) { if (dryRun == 0) CMD_EXECUTE("speed_modifier 2"); return EffectResult::Success; } else if (effectId == EFFECT_DECREASE_SPEED) { if (dryRun == 0) CMD_EXECUTE("speed_modifier -2"); return EffectResult::Success; } else if (effectId == EFFECT_DAMAGE_MULTIPLIER) { if (dryRun == 0) CMD_EXECUTE(fmt::format("defense_modifier -{}", value)); return EffectResult::Success; } else if (effectId == EFFECT_DEFENSE_MULTIPLIER) { if (dryRun == 0) CMD_EXECUTE(fmt::format("defense_modifier {}", value)); return EffectResult::Success; } else if (effectId == EFFECT_DAMAGE) { if ((gSaveContext.health - (16 * value)) <= 0) { return EffectResult::Failure; } if (dryRun == 0) CMD_EXECUTE(fmt::format("{} {}", effectId, value)); return EffectResult::Success; } } return EffectResult::Retry; } bool CrowdControl::SpawnEnemy(std::string effectId) { Player* player = GET_PLAYER(gPlayState); int enemyId = 0; int enemyParams = 0; float posXOffset = 0; float posYOffset = 0; float posZOffset = 0; if (effectId == EFFECT_SPAWN_WALLMASTER) { enemyId = 17; } else if (effectId == EFFECT_SPAWN_ARWING) { // Don't allow Arwings in certain areas because they cause issues. // Locations: King dodongo room, Morpha room, Twinrova room, Ganondorf room, Fishing pond, Ganon's room // TODO: Swap this to disabling the option in CC options menu instead. if (gPlayState->sceneNum == SCENE_DDAN_BOSS || gPlayState->sceneNum == SCENE_MIZUSIN_BS || gPlayState->sceneNum == SCENE_JYASINBOSS || gPlayState->sceneNum == SCENE_GANON_BOSS || gPlayState->sceneNum == SCENE_TURIBORI || gPlayState->sceneNum == SCENE_GANON_DEMO) { return 0; } enemyId = 315; enemyParams = 1; posYOffset = 100; } else if (effectId == EFFECT_SPAWN_DARK_LINK) { enemyId = 51; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_STALFOS) { enemyId = 2; enemyParams = 2; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_WOLFOS) { enemyId = 431; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_FREEZARD) { enemyId = 289; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_KEESE) { enemyId = 19; enemyParams = 2; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_ICE_KEESE) { enemyId = 19; enemyParams = 4; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_FIRE_KEESE) { enemyId = 19; enemyParams = 1; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_TEKTITE) { enemyId = 27; posXOffset = 75; posYOffset = 50; } else if (effectId == EFFECT_SPAWN_LIKE_LIKE) { enemyId = 221; posXOffset = 75; posYOffset = 50; } return Actor_Spawn(&gPlayState->actorCtx, gPlayState, enemyId, player->actor.world.pos.x + posXOffset, player->actor.world.pos.y + posYOffset, player->actor.world.pos.z + posZOffset, 0, 0, 0, enemyParams, 0); } void CrowdControl::RemoveEffect(std::string effectId) { if (gPlayState == NULL) { return; } Player* player = GET_PLAYER(gPlayState); if (player != NULL) { 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(fmt::format("{} 0", effectId)); return; } else if (effectId == EFFECT_IRON_BOOTS || effectId == EFFECT_HOVER_BOOTS) { CMD_EXECUTE("boots kokiri"); return; } else if (effectId == EFFECT_HIGH_GRAVITY || effectId == EFFECT_LOW_GRAVITY) { CMD_EXECUTE("gravity 1"); return; } else if (effectId == EFFECT_REVERSE_CONTROLS) { CMD_EXECUTE("reverse_controls 0"); return; } else if (effectId == EFFECT_INCREASE_SPEED || effectId == EFFECT_DECREASE_SPEED ) { CMD_EXECUTE("speed_modifier 0"); return; } else if (effectId == EFFECT_DAMAGE_MULTIPLIER || effectId == EFFECT_DEFENSE_MULTIPLIER ) { CMD_EXECUTE("defense_modifier 0"); return; } } } #endif