#ifdef ENABLE_REMOTE_CONTROL #include "GameInteractor_Sail.h" #include #include #include template bool IsType(const SrcType* src) { return dynamic_cast(src) != nullptr; } void GameInteractorSail::Enable() { if (isEnabled) { return; } isEnabled = true; GameInteractor::Instance->EnableRemoteInteractor(); GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { HandleRemoteJson(payload); }); GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() { RegisterHooks(); }); } void GameInteractorSail::Disable() { if (!isEnabled) { return; } isEnabled = false; GameInteractor::Instance->DisableRemoteInteractor(); } void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { SPDLOG_INFO("[GameInteractorSail] Received payload: \n{}", payload.dump()); nlohmann::json responsePayload; responsePayload["type"] = "result"; responsePayload["status"] = "failure"; try { if (!payload.contains("id")) { SPDLOG_ERROR("[GameInteractorSail] Received payload without ID"); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } responsePayload["id"] = payload["id"]; if (!payload.contains("type")) { SPDLOG_ERROR("[GameInteractorSail] Received payload without type"); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } std::string payloadType = payload["type"].get(); if (payloadType == "command") { if (!payload.contains("command")) { SPDLOG_ERROR("[GameInteractorSail] Received command payload without command"); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } std::string command = payload["command"].get(); std::reinterpret_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command); responsePayload["status"] = "success"; GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } else if (payloadType == "effect") { if (!payload.contains("effect") || !payload["effect"].contains("type")) { SPDLOG_ERROR("[GameInteractorSail] Received effect payload without effect type"); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } std::string effectType = payload["effect"]["type"].get(); // Special case for "command" effect, so we can also run commands from the `simple_twitch_sail` script if (effectType == "command") { if (!payload["effect"].contains("command")) { SPDLOG_ERROR("[GameInteractorSail] Received command effect payload without command"); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } std::string command = payload["effect"]["command"].get(); std::reinterpret_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command); responsePayload["status"] = "success"; GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } if (effectType != "apply" && effectType != "remove") { SPDLOG_ERROR("[GameInteractorSail] Received effect payload with unknown effect type: {}", effectType); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } if (!GameInteractor::IsSaveLoaded()) { responsePayload["status"] = "try_again"; GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } GameInteractionEffectBase* giEffect = EffectFromJson(payload["effect"]); if (giEffect) { GameInteractionEffectQueryResult result; if (effectType == "remove") { if (IsType(giEffect)) { result = dynamic_cast(giEffect)->Remove(); } else { result = GameInteractionEffectQueryResult::NotPossible; } } else { result = giEffect->Apply(); } if (result == GameInteractionEffectQueryResult::Possible) { responsePayload["status"] = "success"; } else if (result == GameInteractionEffectQueryResult::TemporarilyNotPossible) { responsePayload["status"] = "try_again"; } GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } } else { SPDLOG_ERROR("[GameInteractorSail] Unknown payload type: {}", payloadType); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); return; } // If we get here, something went wrong, send the failure response SPDLOG_ERROR("[GameInteractorSail] Failed to handle remote JSON, sending failure response"); GameInteractor::Instance->TransmitJsonToRemote(responsePayload); } catch (const std::exception& e) { SPDLOG_ERROR("[GameInteractorSail] Exception handling remote JSON: {}", e.what()); } catch (...) { SPDLOG_ERROR("[GameInteractorSail] Unknown exception handling remote JSON"); } } GameInteractionEffectBase* GameInteractorSail::EffectFromJson(nlohmann::json payload) { if (!payload.contains("name")) { return nullptr; } std::string name = payload["name"].get(); if (name == "SetSceneFlag") { auto effect = new GameInteractionEffect::SetSceneFlag(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); effect->parameters[2] = payload["parameters"][2].get(); } return effect; } else if (name == "UnsetSceneFlag") { auto effect = new GameInteractionEffect::UnsetSceneFlag(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); effect->parameters[2] = payload["parameters"][2].get(); } return effect; } else if (name == "SetFlag") { auto effect = new GameInteractionEffect::SetFlag(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "UnsetFlag") { auto effect = new GameInteractionEffect::UnsetFlag(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "ModifyHeartContainers") { auto effect = new GameInteractionEffect::ModifyHeartContainers(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FillMagic") { return new GameInteractionEffect::FillMagic(); } else if (name == "EmptyMagic") { return new GameInteractionEffect::EmptyMagic(); } else if (name == "ModifyRupees") { auto effect = new GameInteractionEffect::ModifyRupees(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "NoUI") { return new GameInteractionEffect::NoUI(); } else if (name == "ModifyGravity") { auto effect = new GameInteractionEffect::ModifyGravity(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyHealth") { auto effect = new GameInteractionEffect::ModifyHealth(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetPlayerHealth") { auto effect = new GameInteractionEffect::SetPlayerHealth(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FreezePlayer") { return new GameInteractionEffect::FreezePlayer(); } else if (name == "BurnPlayer") { return new GameInteractionEffect::BurnPlayer(); } else if (name == "ElectrocutePlayer") { return new GameInteractionEffect::ElectrocutePlayer(); } else if (name == "KnockbackPlayer") { auto effect = new GameInteractionEffect::KnockbackPlayer(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyLinkSize") { auto effect = new GameInteractionEffect::ModifyLinkSize(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "InvisibleLink") { return new GameInteractionEffect::InvisibleLink(); } else if (name == "PacifistMode") { return new GameInteractionEffect::PacifistMode(); } else if (name == "DisableZTargeting") { return new GameInteractionEffect::DisableZTargeting(); } else if (name == "WeatherRainstorm") { return new GameInteractionEffect::WeatherRainstorm(); } else if (name == "ReverseControls") { return new GameInteractionEffect::ReverseControls(); } else if (name == "ForceEquipBoots") { auto effect = new GameInteractionEffect::ForceEquipBoots(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyRunSpeedModifier") { auto effect = new GameInteractionEffect::ModifyRunSpeedModifier(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "OneHitKO") { return new GameInteractionEffect::OneHitKO(); } else if (name == "ModifyDefenseModifier") { auto effect = new GameInteractionEffect::ModifyDefenseModifier(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "GiveOrTakeShield") { auto effect = new GameInteractionEffect::GiveOrTakeShield(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "TeleportPlayer") { auto effect = new GameInteractionEffect::TeleportPlayer(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ClearAssignedButtons") { auto effect = new GameInteractionEffect::ClearAssignedButtons(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetTimeOfDay") { auto effect = new GameInteractionEffect::SetTimeOfDay(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetCollisionViewer") { return new GameInteractionEffect::SetCollisionViewer(); } else if (name == "SetCosmeticsColor") { auto effect = new GameInteractionEffect::SetCosmeticsColor(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "RandomizeCosmetics") { return new GameInteractionEffect::RandomizeCosmetics(); } else if (name == "PressButton") { auto effect = new GameInteractionEffect::PressButton(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "PressRandomButton") { auto effect = new GameInteractionEffect::PressRandomButton(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "AddOrTakeAmmo") { auto effect = new GameInteractionEffect::AddOrTakeAmmo(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "RandomBombFuseTimer") { return new GameInteractionEffect::RandomBombFuseTimer(); } else if (name == "DisableLedgeGrabs") { return new GameInteractionEffect::DisableLedgeGrabs(); } else if (name == "RandomWind") { return new GameInteractionEffect::RandomWind(); } else if (name == "RandomBonks") { return new GameInteractionEffect::RandomBonks(); } else if (name == "PlayerInvincibility") { return new GameInteractionEffect::PlayerInvincibility(); } else if (name == "SlipperyFloor") { return new GameInteractionEffect::SlipperyFloor(); } else { SPDLOG_INFO("[GameInteractorSail] Unknown effect name: {}", name); return nullptr; } } // Workaround until we have a way to unregister hooks static bool hasRegisteredHooks = false; void GameInteractorSail::RegisterHooks() { if (hasRegisteredHooks) { return; } hasRegisteredHooks = true; GameInteractor::Instance->RegisterGameHook([](int32_t sceneNum) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnTransitionEnd"; payload["hook"]["sceneNum"] = sceneNum; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnLoadGame"; payload["hook"]["fileNum"] = fileNum; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnExitGame"; payload["hook"]["fileNum"] = fileNum; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnItemReceive"; payload["hook"]["tableId"] = itemEntry.tableId; payload["hook"]["getItemId"] = itemEntry.getItemId; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](void* refActor) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; Actor* actor = (Actor*)refActor; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnEnemyDefeat"; payload["hook"]["actorId"] = actor->id; payload["hook"]["params"] = actor->params; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](void* refActor) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; Actor* actor = (Actor*)refActor; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnActorInit"; payload["hook"]["actorId"] = actor->id; payload["hook"]["params"] = actor->params; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnFlagSet"; payload["hook"]["flagType"] = flagType; payload["hook"]["flag"] = flag; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnFlagUnset"; payload["hook"]["flagType"] = flagType; payload["hook"]["flag"] = flag; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnSceneFlagSet"; payload["hook"]["flagType"] = flagType; payload["hook"]["flag"] = flag; payload["hook"]["sceneNum"] = sceneNum; GameInteractor::Instance->TransmitJsonToRemote(payload); }); GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; nlohmann::json payload; payload["id"] = std::rand(); payload["type"] = "hook"; payload["hook"]["type"] = "OnSceneFlagUnset"; payload["hook"]["flagType"] = flagType; payload["hook"]["flag"] = flag; payload["hook"]["sceneNum"] = sceneNum; GameInteractor::Instance->TransmitJsonToRemote(payload); }); } #endif