diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index a2d5c56ec52..b2789b14d65 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -18,8 +18,11 @@ DEFINE_HOOK(OnFlagSet, (int16_t flagType, int16_t flag)); DEFINE_HOOK(OnFlagUnset, (int16_t flagType, int16_t flag)); DEFINE_HOOK(OnSceneSpawnActors, ()); DEFINE_HOOK(OnPlayerUpdate, ()); +DEFINE_HOOK(OnPlayerSfx, (u16 sfxId)); DEFINE_HOOK(OnOcarinaSongAction, ()); DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price)); +DEFINE_HOOK(OnDungeonKeyUsed, (uint16_t mapIndex)); +DEFINE_HOOK(ShouldActorInit, (void* actor, bool* result)); DEFINE_HOOK(OnActorInit, (void* actor)); DEFINE_HOOK(OnActorUpdate, (void* actor)); DEFINE_HOOK(OnActorKill, (void* actor)); @@ -32,7 +35,7 @@ DEFINE_HOOK(OnPlayerBottleUpdate, (int16_t contents)); DEFINE_HOOK(OnPlayDestroy, ()); DEFINE_HOOK(OnPlayDrawEnd, ()); DEFINE_HOOK(OnVanillaBehavior, (GIVanillaBehavior flag, bool* result, va_list originalArgs)); -DEFINE_HOOK(OnSaveFile, (int32_t fileNum)); +DEFINE_HOOK(OnSaveFile, (int32_t fileNum, int32_t sectionID)); DEFINE_HOOK(OnLoadFile, (int32_t fileNum)); DEFINE_HOOK(OnDeleteFile, (int32_t fileNum)); @@ -59,3 +62,7 @@ DEFINE_HOOK(OnSetGameLanguage, ()); DEFINE_HOOK(OnFileDropped, (std::string filePath)); DEFINE_HOOK(OnAssetAltChange, ()); DEFINE_HOOK(OnKaleidoUpdate, ()); + +DEFINE_HOOK(OnRandoSetCheckStatus, (RandomizerCheck rc, RandomizerCheckStatus status)); +DEFINE_HOOK(OnRandoSetIsSkipped, (RandomizerCheck rc, bool isSkipped)); +DEFINE_HOOK(OnRandoEntranceDiscovered, (u16 entranceIndex, u8 isReversedEntrance)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 39fc298a889..ad9ad90a420 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -64,6 +64,10 @@ void GameInteractor_ExecuteOnPlayerUpdate() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId) { + GameInteractor::Instance->ExecuteHooks(sfxId); +} + void GameInteractor_ExecuteOnOcarinaSongAction() { GameInteractor::Instance->ExecuteHooks(); } @@ -72,6 +76,19 @@ void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t pr GameInteractor::Instance->ExecuteHooks(cursorIndex, price); } +void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex) { + GameInteractor::Instance->ExecuteHooks(mapIndex); +} + +bool GameInteractor_ShouldActorInit(void* actor) { + bool result = true; + GameInteractor::Instance->ExecuteHooks(actor, &result); + GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor, &result); + GameInteractor::Instance->ExecuteHooksForPtr((uintptr_t)actor, actor, &result); + GameInteractor::Instance->ExecuteHooksForFilter(actor, &result); + return result; +} + void GameInteractor_ExecuteOnActorInit(void* actor) { GameInteractor::Instance->ExecuteHooks(actor); GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor); @@ -154,8 +171,8 @@ bool GameInteractor_Should(GIVanillaBehavior flag, u32 result, ...) { // MARK: - Save Files -void GameInteractor_ExecuteOnSaveFile(int32_t fileNum) { - GameInteractor::Instance->ExecuteHooks(fileNum); +void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID) { + GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); } void GameInteractor_ExecuteOnLoadFile(int32_t fileNum) { @@ -255,3 +272,8 @@ void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void)) { void GameInteractor_ExecuteOnKaleidoUpdate() { GameInteractor::Instance->ExecuteHooks(); } + +// MARK: - Rando +void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) { + GameInteractor::Instance->ExecuteHooks(entranceIndex, isReversedEntrance); +} diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 3438d269d84..f484bc95ded 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -18,7 +18,9 @@ void GameInteractor_ExecuteOnFlagSet(int16_t flagType, int16_t flag); void GameInteractor_ExecuteOnFlagUnset(int16_t flagType, int16_t flag); void GameInteractor_ExecuteOnSceneSpawnActors(); void GameInteractor_ExecuteOnPlayerUpdate(); +void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId); void GameInteractor_ExecuteOnOcarinaSongAction(); +bool GameInteractor_ShouldActorInit(void* actor); void GameInteractor_ExecuteOnActorInit(void* actor); void GameInteractor_ExecuteOnActorUpdate(void* actor); void GameInteractor_ExecuteOnActorKill(void* actor); @@ -30,12 +32,13 @@ void GameInteractor_ExecuteOnPlayerHealthChange(int16_t amount); void GameInteractor_ExecuteOnPlayerBottleUpdate(int16_t contents); void GameInteractor_ExecuteOnOcarinaSongAction(); void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t price); +void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex); void GameInteractor_ExecuteOnPlayDestroy(); void GameInteractor_ExecuteOnPlayDrawEnd(); bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...); // MARK: - Save Files -void GameInteractor_ExecuteOnSaveFile(int32_t fileNum); +void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID); void GameInteractor_ExecuteOnLoadFile(int32_t fileNum); void GameInteractor_ExecuteOnDeleteFile(int32_t fileNum); @@ -69,6 +72,9 @@ void GameInteractor_RegisterOnAssetAltChange(void (*fn)(void)); //Mark: - Pause Menu void GameInteractor_ExecuteOnKaleidoUpdate(); +// MARK: - Rando +void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance); + #ifdef __cplusplus } #endif diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 0bdd2b0cc6c..6a6ac103dd5 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -995,12 +995,12 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l if (item00->itemEntry.modIndex == MOD_NONE) { Notification::Emit({ .itemIcon = GetTextureForItemId(item00->itemEntry.itemId), - .message = "You found ", + .message = "You found", .suffix = SohUtils::GetItemName(item00->itemEntry.itemId), }); } else if (item00->itemEntry.modIndex == MOD_RANDOMIZER) { Notification::Emit({ - .message = "You found ", + .message = "You found", .suffix = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId).GetName().english, }); } diff --git a/soh/soh/Enhancements/randomizer/item_location.cpp b/soh/soh/Enhancements/randomizer/item_location.cpp index 9f0f84509ee..1a11c087c18 100644 --- a/soh/soh/Enhancements/randomizer/item_location.cpp +++ b/soh/soh/Enhancements/randomizer/item_location.cpp @@ -130,6 +130,7 @@ bool ItemLocation::HasObtained() const { void ItemLocation::SetCheckStatus(RandomizerCheckStatus status_) { status = status_; + GameInteractor::Instance->ExecuteHooks(rc, status); } RandomizerCheckStatus ItemLocation::GetCheckStatus() { @@ -138,6 +139,7 @@ RandomizerCheckStatus ItemLocation::GetCheckStatus() { void ItemLocation::SetIsSkipped(bool isSkipped_) { isSkipped = isSkipped_; + GameInteractor::Instance->ExecuteHooks(rc, isSkipped); } bool ItemLocation::GetIsSkipped() { diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index 7a492fe9c57..cb770c54f78 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -15,6 +15,7 @@ #include "global.h" #include "entrance.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" extern PlayState* gPlayState; @@ -778,6 +779,8 @@ void Entrance_SetEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) { return; } + GameInteractor_ExecuteOnRandoEntranceDiscovered(entranceIndex, isReversedEntrance); + u16 bitsPerIndex = sizeof(u32) * 8; u32 idx = entranceIndex / bitsPerIndex; if (idx < SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT) { diff --git a/soh/soh/Network/Anchor/Anchor.cpp b/soh/soh/Network/Anchor/Anchor.cpp new file mode 100644 index 00000000000..7d1b701ab2b --- /dev/null +++ b/soh/soh/Network/Anchor/Anchor.cpp @@ -0,0 +1,394 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" +#include "soh/Enhancements/nametag.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; +} + +// MARK: - Overrides + +void Anchor::Enable() { + Network::Enable(CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.proxysaw.dev"), CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383)); + ownClientId = CVarGetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), 0); + roomState.ownerClientId = 0; +} + +void Anchor::Disable() { + Network::Disable(); + + clients.clear(); + RefreshClientActors(); +} + +void Anchor::OnConnected() { + SendPacket_Handshake(); + RegisterHooks(); + + if (IsSaveLoaded()) { + SendPacket_RequestTeamState(); + } +} + +void Anchor::OnDisconnected() { + RegisterHooks(); +} + +void Anchor::SendJsonToRemote(nlohmann::json payload) { + if (!isConnected) { + return; + } + + payload["clientId"] = ownClientId; + if (!payload.contains("quiet")) { + SPDLOG_INFO("[Anchor] Sending payload:\n{}", payload.dump()); + } + Network::SendJsonToRemote(payload); +} + +void Anchor::OnIncomingJson(nlohmann::json payload) { + // If it doesn't contain a type, it's not a valid payload + if (!payload.contains("type")) { + return; + } + + // If it's not a quiet payload, log it + if (!payload.contains("quiet")) { + SPDLOG_INFO("[Anchor] Received payload:\n{}", payload.dump()); + } + + std::string packetType = payload["type"].get(); + + // Ignore packets from mismatched clients, except for ALL_CLIENT_STATE or UPDATE_CLIENT_STATE + if (packetType != ALL_CLIENT_STATE && packetType != UPDATE_CLIENT_STATE) { + if (payload.contains("clientId")) { + uint32_t clientId = payload["clientId"].get(); + if (clients.contains(clientId) && clients[clientId].clientVersion != clientVersion) { + return; + } + } + } + + // packetType here is a string so we can't use a switch statement + if (packetType == ALL_CLIENT_STATE) HandlePacket_AllClientState(payload); + else if (packetType == CONSUME_ADULT_TRADE_ITEM) HandlePacket_ConsumeAdultTradeItem(payload); + else if (packetType == DAMAGE_PLAYER) HandlePacket_DamagePlayer(payload); + else if (packetType == DISABLE_ANCHOR) HandlePacket_DisableAnchor(payload); + else if (packetType == ENTRANCE_DISCOVERED) HandlePacket_EntranceDiscovered(payload); + else if (packetType == GAME_COMPLETE) HandlePacket_GameComplete(payload); + else if (packetType == GIVE_ITEM) HandlePacket_GiveItem(payload); + else if (packetType == PLAYER_SFX) HandlePacket_PlayerSfx(payload); + else if (packetType == PLAYER_UPDATE) HandlePacket_PlayerUpdate(payload); + else if (packetType == UPDATE_TEAM_STATE) HandlePacket_UpdateTeamState(payload); + else if (packetType == REQUEST_TEAM_STATE) HandlePacket_RequestTeamState(payload); + else if (packetType == REQUEST_TELEPORT) HandlePacket_RequestTeleport(payload); + else if (packetType == SERVER_MESSAGE) HandlePacket_ServerMessage(payload); + else if (packetType == SET_CHECK_STATUS) HandlePacket_SetCheckStatus(payload); + else if (packetType == SET_FLAG) HandlePacket_SetFlag(payload); + else if (packetType == TELEPORT_TO) HandlePacket_TeleportTo(payload); + else if (packetType == UNSET_FLAG) HandlePacket_UnsetFlag(payload); + else if (packetType == UPDATE_BEANS_COUNT) HandlePacket_UpdateBeansCount(payload); + else if (packetType == UPDATE_CLIENT_STATE) HandlePacket_UpdateClientState(payload); + else if (packetType == UPDATE_ROOM_STATE) HandlePacket_UpdateRoomState(payload); + else if (packetType == UPDATE_DUNGEON_ITEMS) HandlePacket_UpdateDungeonItems(payload); +} + +// Macros to let us easily register and unregister functions when the anchor is enabled/disabled +#define HOOK(hook, condition, body) \ + static HOOK_ID hook = 0; \ + GameInteractor::Instance->UnregisterGameHook(hook); \ + hook = 0; \ + if (condition) { \ + hook = GameInteractor::Instance->RegisterGameHook(body); \ + } + +#define HOOK_FOR_ID(hook, condition, id, body) \ + static HOOK_ID hook = 0; \ + GameInteractor::Instance->UnregisterGameHookForID(hook); \ + hook = 0; \ + if (condition) { \ + hook = GameInteractor::Instance->RegisterGameHookForID(id, body); \ + } + +void Anchor::RegisterHooks() { + HOOK(OnSceneSpawnActors, isConnected, [&]() { + SendPacket_UpdateClientState(); + + if (IsSaveLoaded()) { + RefreshClientActors(); + } + }); + + HOOK(OnPresentFileSelect, isConnected, [&]() { + SendPacket_UpdateClientState(); + }); + + HOOK_FOR_ID(ShouldActorInit, isConnected, ACTOR_PLAYER, [&](void* actorRef, bool* should) { + Actor* actor = (Actor*)actorRef; + + if (refreshingActors) { + // By the time we get here, the actor was already added to the ACTORCAT_PLAYER list, so we need to move it + Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, actor, ACTORCAT_NPC); + actor->id = ACTOR_EN_OE2; + actor->category = ACTORCAT_NPC; + actor->init = DummyPlayer_Init; + actor->update = DummyPlayer_Update; + actor->draw = DummyPlayer_Draw; + actor->destroy = DummyPlayer_Destroy; + } + }); + + HOOK(OnPlayerUpdate, isConnected, [&]() { + if (justLoadedSave) { + justLoadedSave = false; + SendPacket_RequestTeamState(); + } + SendPacket_PlayerUpdate(); + }); + + HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) { + SendPacket_PlayerSfx(sfxId); + }); + + HOOK(OnLoadGame, isConnected, [&](s16 fileNum) { + justLoadedSave = true; + }); + + HOOK(OnSaveFile, isConnected, [&](s16 fileNum, int sectionID) { + if (sectionID == 0) { + SendPacket_UpdateTeamState(); + } + }); + + HOOK(OnFlagSet, isConnected, [&](s16 flagType, s16 flag) { + SendPacket_SetFlag(SCENE_ID_MAX, flagType, flag); + }); + + HOOK(OnFlagUnset, isConnected, [&](s16 flagType, s16 flag) { + SendPacket_UnsetFlag(SCENE_ID_MAX, flagType, flag); + }); + + HOOK(OnSceneFlagSet, isConnected, [&](s16 sceneNum, s16 flagType, s16 flag) { + SendPacket_SetFlag(sceneNum, flagType, flag); + }); + + HOOK(OnSceneFlagUnset, isConnected, [&](s16 sceneNum, s16 flagType, s16 flag) { + SendPacket_UnsetFlag(sceneNum, flagType, flag); + }); + + HOOK(OnRandoSetCheckStatus, isConnected, [&](RandomizerCheck rc, RandomizerCheckStatus status) { + if (!isHandlingUpdateTeamState) { + SendPacket_SetCheckStatus(rc); + } + }); + + HOOK(OnRandoSetIsSkipped, isConnected, [&](RandomizerCheck rc, bool isSkipped) { + if (!isHandlingUpdateTeamState) { + SendPacket_SetCheckStatus(rc); + } + }); + + HOOK(OnRandoEntranceDiscovered, isConnected, [&](u16 entranceIndex, u8 isReversedEntrance) { + SendPacket_EntranceDiscovered(entranceIndex); + }); + + HOOK_FOR_ID(OnBossDefeat, isConnected, ACTOR_BOSS_GANON2, [&](void* refActor) { + SendPacket_GameComplete(); + }); + + HOOK(OnItemReceive, isConnected, [&](GetItemEntry itemEntry) { + // Handle vanilla dungeon items a bit differently + if (itemEntry.modIndex == MOD_NONE && (itemEntry.itemId >= ITEM_KEY_BOSS && itemEntry.itemId <= ITEM_KEY_SMALL)) { + SendPacket_UpdateDungeonItems(); + return; + } + + SendPacket_GiveItem(itemEntry.tableId, itemEntry.getItemId); + }); + + HOOK(OnDungeonKeyUsed, isConnected, [&](uint16_t mapIndex) { + // Handle vanilla dungeon items a bit differently + SendPacket_UpdateDungeonItems(); + }); +} + +// MARK: - Misc/Helpers + +// Kills all existing anchor actors and respawns them with the new client data +void Anchor::RefreshClientActors() { + if (!IsSaveLoaded()) { + return; + } + + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_NPC].head; + + while (actor != NULL) { + if (actor->id == ACTOR_EN_OE2 && actor->update == DummyPlayer_Update) { + NameTag_RemoveAllForActor(actor); + Actor_Kill(actor); + } + actor = actor->next; + } + + actorIndexToClientId.clear(); + refreshingActors = true; + for (auto& [clientId, client] : clients) { + if (!client.online || client.self) { + continue; + } + + actorIndexToClientId.push_back(clientId); + // We are using a hook `ShouldActorInit` to override the init/update/draw/destroy functions of the Player we spawn + // We quickly store a mapping of "index" to clientId, then within the init function we use this to get the clientId + // and store it on player->zTargetActiveTimer (unused s32 for the dummy) for convenience + auto dummy = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_PLAYER, client.posRot.pos.x, + client.posRot.pos.y, client.posRot.pos.z, client.posRot.rot.x, client.posRot.rot.y, + client.posRot.rot.z, actorIndexToClientId.size() - 1, false); + client.player = (Player*)dummy; + } + refreshingActors = false; +} + +bool Anchor::IsSaveLoaded() { + if (gPlayState == nullptr) { + return false; + } + + if (GET_PLAYER(gPlayState) == nullptr) { + return false; + } + + if (gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) { + return false; + } + + if (gSaveContext.gameMode != GAMEMODE_NORMAL) { + return false; + } + + return true; +} + +// MARK: - UI + +void Anchor::DrawMenu() { + ImGui::PushID("Anchor"); + + std::string host = CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.proxysaw.dev"); + uint16_t port = CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383); + std::string anchorTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + std::string anchorRoomId = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""); + std::string anchorName = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), ""); + bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535 && + !SohUtils::IsStringEmpty(anchorRoomId) && !SohUtils::IsStringEmpty(anchorName); + + ImGui::SeparatorText("Anchor"); + // UIWidgets::Tooltip("Anchor Stuff"); + if (ImGui::IsItemClicked()) { + // ImGui::SetClipboardText("https://github.com/garrettjoecox/anchor"); + } + + ImGui::BeginDisabled(isEnabled); + ImGui::Text("Host & Port"); + if (UIWidgets::InputString("##Host", &host)) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), host.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) { + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::Text("Tunic Color & Name"); + static Color_RGBA8 color = CVarGetColor(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100, 255 }); + static ImVec4 colorVec = ImVec4(color.r / 255.0, color.g / 255.0, color.b / 255.0, 1); + if (ImGui::ColorEdit3("##Color", (float*)&colorVec, + ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + color.r = colorVec.x * 255.0; + color.g = colorVec.y * 255.0; + color.b = colorVec.z * 255.0; + + CVarSetColor(CVAR_REMOTE_ANCHOR("Color"), color); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##Name", &anchorName)) { + CVarSetString(CVAR_REMOTE_ANCHOR("Name"), anchorName.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Text("Room ID"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##RoomId", &anchorRoomId, isEnabled ? ImGuiInputTextFlags_Password : 0)) { + CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), anchorRoomId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Text("Team ID (Items & Flags Shared)"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##TeamId", &anchorTeamId)) { + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), anchorTeamId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + 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_ANCHOR("Enabled")); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + Disable(); + } else { + CVarSetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + Enable(); + } + } + ImGui::EndDisabled(); + + if (isEnabled) { + ImGui::Spacing(); + if (isConnected) { + ImGui::Text("Connected"); + + if (roomState.ownerClientId == ownClientId) { + if (ImGui::BeginMenu("Room Settings")) { + ImGui::Text("PvP Mode:"); + static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" }; + if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), pvpModes, 1)) { + SendPacket_UpdateRoomState(); + } + ImGui::Text("Show Locations For:"); + static const char* showLocationsModes[3] = { "None", "Team Only", "All" }; + if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), showLocationsModes, 1)) { + SendPacket_UpdateRoomState(); + } + ImGui::Text("Allow Teleporting To:"); + static const char* teleportModes[3] = { "None", "Team Only", "All" }; + if (UIWidgets::EnhancementCombobox(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), teleportModes, 1)) { + SendPacket_UpdateRoomState(); + } + ImGui::EndMenu(); + } + } + } else { + ImGui::Text("Connecting..."); + } + } + + ImGui::PopID(); +} + +#endif diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h new file mode 100644 index 00000000000..eceb02c1df1 --- /dev/null +++ b/soh/soh/Network/Anchor/Anchor.h @@ -0,0 +1,176 @@ +#ifdef ENABLE_REMOTE_CONTROL +#ifndef NETWORK_ANCHOR_H +#define NETWORK_ANCHOR_H +#ifdef __cplusplus + +#include "soh/Network/Network.h" +#include + +extern "C" { +#include "variables.h" +#include "z64.h" +} + +void DummyPlayer_Init(Actor* actor, PlayState* play); +void DummyPlayer_Update(Actor* actor, PlayState* play); +void DummyPlayer_Draw(Actor* actor, PlayState* play); +void DummyPlayer_Destroy(Actor* actor, PlayState* play); + +typedef struct { + uint32_t clientId; + std::string name; + Color_RGB8 color; + std::string clientVersion; + std::string teamId; + bool online; + bool self; + uint32_t seed; + bool isSaveLoaded; + bool isGameComplete; + s16 sceneNum; + s32 entranceIndex; + + // Only available in PLAYER_UPDATE packets + s32 linkAge; + PosRot posRot; + Vec3s jointTable[24]; + Vec3s upperLimbRot; + s8 currentBoots; + s8 currentShield; + s8 currentTunic; + u32 stateFlags1; + u32 stateFlags2; + u8 buttonItem0; + s8 itemAction; + s8 heldItemAction; + u8 modelGroup; + s8 invincibilityTimer; + s16 unk_862; + s8 actionVar1; + + // Ptr to the dummy player + Player* player; +} AnchorClient; + +typedef struct { + uint32_t ownerClientId; + u8 pvpMode; // 0 = off, 1 = on, 2 = on with friendly fire + u8 showLocationsMode; // 0 = none, 1 = team, 2 = all + u8 teleportMode; // 0 = off, 1 = team, 2 = all +} RoomState; + +class Anchor : public Network { + private: + bool refreshingActors = false; + bool justLoadedSave = false; + bool isHandlingUpdateTeamState = false; + uint32_t ownClientId; + + nlohmann::json PrepClientState(); + nlohmann::json PrepRoomState(); + void RegisterHooks(); + void RefreshClientActors(); + void HandlePacket_AllClientState(nlohmann::json payload); + void HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload); + void HandlePacket_DamagePlayer(nlohmann::json payload); + void HandlePacket_DisableAnchor(nlohmann::json payload); + void HandlePacket_EntranceDiscovered(nlohmann::json payload); + void HandlePacket_GameComplete(nlohmann::json payload); + void HandlePacket_GiveItem(nlohmann::json payload); + void HandlePacket_PlayerSfx(nlohmann::json payload); + void HandlePacket_PlayerUpdate(nlohmann::json payload); + void HandlePacket_RequestTeamState(nlohmann::json payload); + void HandlePacket_RequestTeleport(nlohmann::json payload); + void HandlePacket_ServerMessage(nlohmann::json payload); + void HandlePacket_SetCheckStatus(nlohmann::json payload); + void HandlePacket_SetFlag(nlohmann::json payload); + void HandlePacket_TeleportTo(nlohmann::json payload); + void HandlePacket_UnsetFlag(nlohmann::json payload); + void HandlePacket_UpdateBeansCount(nlohmann::json payload); + void HandlePacket_UpdateClientState(nlohmann::json payload); + void HandlePacket_UpdateDungeonItems(nlohmann::json payload); + void HandlePacket_UpdateRoomState(nlohmann::json payload); + void HandlePacket_UpdateTeamState(nlohmann::json payload); + + public: + inline static const std::string clientVersion = (char*)gBuildVersion; + + // Packet types // + inline static const std::string ALL_CLIENT_STATE = "ALL_CLIENT_STATE"; + inline static const std::string CONSUME_ADULT_TRADE_ITEM = "CONSUME_ADULT_TRADE_ITEM"; + inline static const std::string DAMAGE_PLAYER = "DAMAGE_PLAYER"; + inline static const std::string DISABLE_ANCHOR = "DISABLE_ANCHOR"; + inline static const std::string ENTRANCE_DISCOVERED = "ENTRANCE_DISCOVERED"; + inline static const std::string GAME_COMPLETE = "GAME_COMPLETE"; + inline static const std::string GIVE_ITEM = "GIVE_ITEM"; + inline static const std::string HANDSHAKE = "HANDSHAKE"; + inline static const std::string PLAYER_SFX = "PLAYER_SFX"; + inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE"; + inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE"; + inline static const std::string REQUEST_TELEPORT = "REQUEST_TELEPORT"; + inline static const std::string SERVER_MESSAGE = "SERVER_MESSAGE"; + inline static const std::string SET_CHECK_STATUS = "SET_CHECK_STATUS"; + inline static const std::string SET_FLAG = "SET_FLAG"; + inline static const std::string TELEPORT_TO = "TELEPORT_TO"; + inline static const std::string UNSET_FLAG = "UNSET_FLAG"; + inline static const std::string UPDATE_BEANS_COUNT = "UPDATE_BEANS_COUNT"; + inline static const std::string UPDATE_CLIENT_STATE = "UPDATE_CLIENT_STATE"; + inline static const std::string UPDATE_DUNGEON_ITEMS = "UPDATE_DUNGEON_ITEMS"; + inline static const std::string UPDATE_ROOM_STATE = "UPDATE_ROOM_STATE"; + inline static const std::string UPDATE_TEAM_STATE = "UPDATE_TEAM_STATE"; + + static Anchor* Instance; + std::map clients; + std::vector actorIndexToClientId; + RoomState roomState; + + void Enable(); + void Disable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void DrawMenu(); + void SendJsonToRemote(nlohmann::json packet); + bool IsSaveLoaded(); + + void SendPacket_ConsumeAdultTradeItem(u8 itemId); + void SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage); + void SendPacket_EntranceDiscovered(u16 entranceIndex); + void SendPacket_GameComplete(); + void SendPacket_GiveItem(u16 modId, s16 getItemId); + void SendPacket_Handshake(); + void SendPacket_PlayerSfx(u16 sfxId); + void SendPacket_PlayerUpdate(); + void SendPacket_RequestTeamState(); + void SendPacket_RequestTeleport(u32 clientId); + void SendPacket_SetCheckStatus(RandomizerCheck rc); + void SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag); + void SendPacket_TeleportTo(u32 clientId); + void SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag); + void SendPacket_UpdateBeansCount(); + void SendPacket_UpdateClientState(); + void SendPacket_UpdateDungeonItems(); + void SendPacket_UpdateRoomState(); + void SendPacket_UpdateTeamState(); +}; + +typedef enum { + // Starting at 5 to continue from the last value in the PlayerDamageResponseType enum + DUMMY_PLAYER_HIT_RESPONSE_STUN = 5, + DUMMY_PLAYER_HIT_RESPONSE_FIRE, + DUMMY_PLAYER_HIT_RESPONSE_NORMAL, +} DummyPlayerDamageResponseType; + +class AnchorRoomWindow : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override {}; + void DrawElement() override {}; + void Draw() override; + void UpdateElement() override {}; +}; + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_H +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/AnchorRoomWindow.cpp b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp new file mode 100644 index 00000000000..7a683eca0c2 --- /dev/null +++ b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp @@ -0,0 +1,118 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "Anchor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; +} + +void AnchorRoomWindow::Draw() { + if (!Anchor::Instance->isConnected) { + return; + } + + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Notifications.BgOpacity"), 0.5f))); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + + auto vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowViewport(vp->ID); + + ImGui::Begin("Anchor Room", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoScrollbar + ); + + // First build a list of teams + std::set teams; + for (auto& [clientId, client] : Anchor::Instance->clients) { + teams.insert(client.teamId); + } + + for (auto& team : teams) { + if (teams.size() > 1) { + ImGui::SeparatorText(team.c_str()); + } + bool isOwnTeam = team == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + for (auto& [clientId, client] : Anchor::Instance->clients) { + if (client.teamId != team) { + continue; + } + + if (client.clientId == Anchor::Instance->roomState.ownerClientId) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", ICON_FA_GAVEL); + ImGui::SameLine(); + } + + if (client.self) { + ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.8f, 1.0f), "%s", CVarGetString(CVAR_REMOTE_ANCHOR("Name"), "")); + } else if (!client.online) { + ImGui::TextColored(ImVec4(1, 1, 1, 0.3f), "%s - offline", client.name.c_str()); + continue; + } else { + ImGui::Text("%s", client.name.c_str()); + } + + if (Anchor::Instance->roomState.showLocationsMode == 2 || (Anchor::Instance->roomState.showLocationsMode == 1 && isOwnTeam)) { + if ((client.self ? Anchor::Instance->IsSaveLoaded() : client.isSaveLoaded)) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "- %s", SohUtils::GetSceneName(client.self ? gPlayState->sceneNum : client.sceneNum).c_str()); + } + } + + if ( + Anchor::Instance->IsSaveLoaded() && !client.self && client.isSaveLoaded && + (Anchor::Instance->roomState.teleportMode == 2 || (Anchor::Instance->roomState.teleportMode == 1 && isOwnTeam)) + ) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(15.0f, 15.0f))) { + Anchor::Instance->SendPacket_RequestTeleport(client.clientId); + } + ImGui::PopStyleVar(); + } + + if (client.clientVersion != Anchor::clientVersion) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Incompatible version! Will not work together!"); + ImGui::Text("Yours: %s", Anchor::clientVersion.c_str()); + ImGui::Text("Theirs: %s", client.clientVersion.c_str()); + ImGui::EndTooltip(); + } + } + uint32_t seed = IS_RANDO ? Rando::Context::GetInstance()->GetSettings()->GetSeed() : 0; + if (client.isSaveLoaded && Anchor::Instance->IsSaveLoaded() && client.seed != seed && client.online) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Seed mismatch! Continuing will break things!"); + ImGui::Text("Yours: %u", seed); + ImGui::Text("Theirs: %u", client.seed); + ImGui::EndTooltip(); + } + } + } + } + + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/DummyPlayer.cpp b/soh/soh/Network/Anchor/DummyPlayer.cpp new file mode 100644 index 00000000000..63c6daf03e5 --- /dev/null +++ b/soh/soh/Network/Anchor/DummyPlayer.cpp @@ -0,0 +1,216 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "Anchor.h" +#include "soh/Enhancements/nametag.h" +#include "soh/frame_interpolation.h" + +extern "C" { +#include "macros.h" +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; + +void Player_UseItem(PlayState* play, Player* player, s32 item); +void Player_Draw(Actor* actor, PlayState* play); +} + +// Hijacking player->zTargetActiveTimer (unused s32 for the dummy) to store the clientId for convenience +#define DUMMY_CLIENT_ID player->zTargetActiveTimer + +static DamageTable DummyPlayerDamageTable = { + /* Deku nut */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Deku stick */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Slingshot */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Explosive */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Boomerang */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Normal arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Hammer swing */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Hookshot */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Kokiri sword */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Master sword */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Giant's Knife */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Fire arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_FIRE), + /* Ice arrow */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_ICE_TRAP), + /* Light arrow */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Unk arrow 1 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE), + /* Unk arrow 2 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE), + /* Unk arrow 3 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE), + /* Fire magic */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_FIRE), + /* Ice magic */ DMG_ENTRY(3, PLAYER_HIT_RESPONSE_ICE_TRAP), + /* Light magic */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Shield */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Mirror Ray */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Kokiri spin */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Giant spin */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Master spin */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Kokiri jump */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Giant jump */ DMG_ENTRY(8, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Master jump */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Unknown 1 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Unblockable */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Hammer jump */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Unknown 2 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), +}; + +void DummyPlayer_Init(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + uint32_t clientId = Anchor::Instance->actorIndexToClientId[actor->params]; + DUMMY_CLIENT_ID = clientId; + + if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID]; + + // Hack to account for usage of gSaveContext in Player_Init + s32 originalAge = gSaveContext.linkAge; + gSaveContext.linkAge = client.linkAge; + + // #region modeled after EnTorch2_Init and Player_Init + actor->room = -1; + player->itemAction = player->heldItemAction = -1; + player->heldItemId = ITEM_NONE; + Player_UseItem(play, player, ITEM_NONE); + Player_SetModelGroup(player, Player_ActionToModelGroup(player, player->heldItemAction)); + play->playerInit(player, play, gPlayerSkelHeaders[client.linkAge]); + + play->func_11D54(player, play); + // #endregion + + player->cylinder.base.acFlags = AC_ON | AC_TYPE_PLAYER; + player->cylinder.base.ocFlags2 = OC2_TYPE_1; + player->cylinder.info.bumperFlags = BUMP_ON | BUMP_HOOKABLE | BUMP_NO_HITMARK; + player->actor.flags |= ACTOR_FLAG_DRAGGED_BY_HOOKSHOT; + player->cylinder.dim.radius = 30; + player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable; + + gSaveContext.linkAge = originalAge; + + NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), { .yOffset = 30 }); +} + +void Math_Vec3s_Copy(Vec3s* dest, Vec3s* src) { + dest->x = src->x; + dest->y = src->y; + dest->z = src->z; +} + +// Update the actor with new data from the client +void DummyPlayer_Update(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID]; + + if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) { + actor->world.pos.x = -9999.0f; + actor->world.pos.y = -9999.0f; + actor->world.pos.z = -9999.0f; + actor->shape.shadowAlpha = 0; + return; + } + + actor->shape.shadowAlpha = 255; + Math_Vec3s_Copy(&player->upperLimbRot, &client.upperLimbRot); + Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot); + Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos); + player->skelAnime.jointTable = client.jointTable; + player->currentBoots = client.currentBoots; + player->currentShield = client.currentShield; + player->currentTunic = client.currentTunic; + player->stateFlags1 = client.stateFlags1; + player->stateFlags2 = client.stateFlags2; + player->itemAction = client.itemAction; + player->heldItemAction = client.heldItemAction; + player->invincibilityTimer = client.invincibilityTimer; + player->unk_862 = client.unk_862; + player->av1.actionVar1 = client.actionVar1; + + if (player->modelGroup != client.modelGroup) { + // Hack to account for usage of gSaveContext + s32 originalAge = gSaveContext.linkAge; + gSaveContext.linkAge = client.linkAge; + u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0]; + gSaveContext.equips.buttonItems[0] = client.buttonItem0; + Player_SetModelGroup(player, client.modelGroup); + gSaveContext.linkAge = originalAge; + gSaveContext.equips.buttonItems[0] = originalButtonItem0; + } + + if ( + Anchor::Instance->roomState.pvpMode == 0 || + (Anchor::Instance->roomState.pvpMode == 1 && client.teamId == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default")) + ) { + return; + } + + if (player->cylinder.base.acFlags & AC_HIT && player->invincibilityTimer == 0) { + Anchor::Instance->SendPacket_DamagePlayer(client.clientId, player->actor.colChkInfo.damageEffect, player->actor.colChkInfo.damage); + if (player->actor.colChkInfo.damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) { + Actor_SetColorFilter(&player->actor, 0, 0xFF, 0, 24); + } else { + player->invincibilityTimer = 20; + } + } + + Collider_UpdateCylinder(&player->actor, &player->cylinder); + + if (!(player->stateFlags2 & PLAYER_STATE2_FROZEN)) { + if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_HANGING_OFF_LEDGE | PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_ON_HORSE))) { + CollisionCheck_SetOC(play, &play->colChkCtx, &player->cylinder.base); + } + + if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_DAMAGED)) && (player->invincibilityTimer <= 0)) { + CollisionCheck_SetAC(play, &play->colChkCtx, &player->cylinder.base); + + if (player->invincibilityTimer < 0) { + CollisionCheck_SetAT(play, &play->colChkCtx, &player->cylinder.base); + } + } + } + + if (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_IN_ITEM_CS | PLAYER_STATE1_IN_CUTSCENE)) { + player->actor.colChkInfo.mass = MASS_IMMOVABLE; + } else { + player->actor.colChkInfo.mass = 50; + } + + Collider_ResetCylinderAC(play, &player->cylinder.base); +} + +void DummyPlayer_Draw(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + if (!Anchor::Instance->clients.contains(DUMMY_CLIENT_ID)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[DUMMY_CLIENT_ID]; + + if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) { + return; + } + + // Hack to account for usage of gSaveContext in Player_Draw + s32 originalAge = gSaveContext.linkAge; + gSaveContext.linkAge = client.linkAge; + u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0]; + gSaveContext.equips.buttonItems[0] = client.buttonItem0; + + Player_Draw((Actor*)player, play); + gSaveContext.linkAge = originalAge; + gSaveContext.equips.buttonItems[0] = originalButtonItem0; +} + +void DummyPlayer_Destroy(Actor* actor, PlayState* play) { +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp new file mode 100644 index 00000000000..3ec04f0437f --- /dev/null +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -0,0 +1,186 @@ +#ifdef ENABLE_REMOTE_CONTROL +#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H +#define NETWORK_ANCHOR_JSON_CONVERSIONS_H +#ifdef __cplusplus + +#include +#include +#include "Anchor.h" + +extern "C" { +#include "z64.h" +} + +using json = nlohmann::json; + +inline void from_json(const json& j, Color_RGB8& color) { + j.at("r").get_to(color.r); + j.at("g").get_to(color.g); + j.at("b").get_to(color.b); +} + +inline void to_json(json& j, const Color_RGB8& color) { + j = json{ + {"r", color.r}, + {"g", color.g}, + {"b", color.b} + }; +} + +inline void to_json(json& j, const Vec3f& vec) { + j = json{ + {"x", vec.x}, + {"y", vec.y}, + {"z", vec.z} + }; +} + +inline void to_json(json& j, const Vec3s& vec) { + j = json{ + {"x", vec.x}, + {"y", vec.y}, + {"z", vec.z} + }; +} + +inline void from_json(const json& j, Vec3f& vec) { + j.at("x").get_to(vec.x); + j.at("y").get_to(vec.y); + j.at("z").get_to(vec.z); +} + +inline void from_json(const json& j, Vec3s& vec) { + j.at("x").get_to(vec.x); + j.at("y").get_to(vec.y); + j.at("z").get_to(vec.z); +} + +inline void to_json(json& j, const PosRot& posRot) { + j = json{ + {"pos", posRot.pos}, + {"rot", posRot.rot} + }; +} + +inline void from_json(const json& j, PosRot& posRot) { + j.at("pos").get_to(posRot.pos); + j.at("rot").get_to(posRot.rot); +} + +inline void from_json(const json& j, AnchorClient& client) { + j.contains("clientId") ? j.at("clientId").get_to(client.clientId) : client.clientId = 0; + j.contains("name") ? j.at("name").get_to(client.name) : client.name = "???"; + j.contains("color") ? j.at("color").get_to(client.color) : client.color = { 255, 255, 255 }; + j.contains("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???"; + j.contains("teamId") ? j.at("teamId").get_to(client.teamId) : client.teamId = "default"; + j.contains("online") ? j.at("online").get_to(client.online) : client.online = false; + j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0; + j.contains("isSaveLoaded") ? j.at("isSaveLoaded").get_to(client.isSaveLoaded) : client.isSaveLoaded = false; + j.contains("isGameComplete") ? j.at("isGameComplete").get_to(client.isGameComplete) : client.isGameComplete = false; + j.contains("sceneNum") ? j.at("sceneNum").get_to(client.sceneNum) : client.sceneNum = SCENE_ID_MAX; + j.contains("entranceIndex") ? j.at("entranceIndex").get_to(client.entranceIndex) : client.entranceIndex = 0; + j.contains("self") ? j.at("self").get_to(client.self) : client.self = false; +} + +inline void to_json(json& j, const Inventory& inventory) { + j = json{ + {"items", inventory.items}, + {"ammo", inventory.ammo}, + {"equipment", inventory.equipment}, + {"upgrades", inventory.upgrades}, + {"questItems", inventory.questItems}, + {"dungeonItems", inventory.dungeonItems}, + {"dungeonKeys", inventory.dungeonKeys}, + {"defenseHearts", inventory.defenseHearts}, + {"gsTokens", inventory.gsTokens} + }; +} + +inline void from_json(const json& j, Inventory& inventory) { + j.at("items").get_to(inventory.items); + j.at("ammo").get_to(inventory.ammo); + j.at("equipment").get_to(inventory.equipment); + j.at("upgrades").get_to(inventory.upgrades); + j.at("questItems").get_to(inventory.questItems); + j.at("dungeonItems").get_to(inventory.dungeonItems); + j.at("dungeonKeys").get_to(inventory.dungeonKeys); + j.at("defenseHearts").get_to(inventory.defenseHearts); + j.at("gsTokens").get_to(inventory.gsTokens); +} + +inline void to_json(json& j, const SohStats& sohStats) { + j = json{ + {"entrancesDiscovered", sohStats.entrancesDiscovered}, + {"fileCreatedAt", sohStats.fileCreatedAt}, + }; +} + +inline void from_json(const json& j, SohStats& sohStats) { + j.at("entrancesDiscovered").get_to(sohStats.entrancesDiscovered); + j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt); +} + +inline void to_json(json& j, const SaveContext& saveContext) { + std::vector sceneFlagsArray; + for (const auto& sceneFlags : saveContext.sceneFlags) { + sceneFlagsArray.push_back(sceneFlags.chest); + sceneFlagsArray.push_back(sceneFlags.swch); + sceneFlagsArray.push_back(sceneFlags.clear); + sceneFlagsArray.push_back(sceneFlags.collect); + } + + j = json{ + {"healthCapacity", saveContext.healthCapacity}, + {"magicLevel", saveContext.magicLevel}, + {"magicCapacity", saveContext.magicCapacity}, + {"isMagicAcquired", saveContext.isMagicAcquired}, + {"isDoubleMagicAcquired", saveContext.isDoubleMagicAcquired}, + {"isDoubleDefenseAcquired", saveContext.isDoubleDefenseAcquired}, + {"bgsFlag", saveContext.bgsFlag}, + {"swordHealth", saveContext.swordHealth}, + {"sceneFlags", sceneFlagsArray}, + {"eventChkInf", saveContext.eventChkInf}, + {"itemGetInf", saveContext.itemGetInf}, + {"infTable", saveContext.infTable}, + {"randomizerInf", saveContext.randomizerInf}, + {"gsFlags", saveContext.gsFlags}, + {"inventory", saveContext.inventory}, + {"sohStats", saveContext.sohStats}, + {"adultTradeItems", saveContext.adultTradeItems}, + {"triforcePiecesCollected", saveContext.triforcePiecesCollected}, + {"questId", saveContext.questId}, + }; +} + +inline void from_json(const json& j, SaveContext& saveContext) { + j.at("healthCapacity").get_to(saveContext.healthCapacity); + j.at("magicLevel").get_to(saveContext.magicLevel); + j.at("magicCapacity").get_to(saveContext.magicCapacity); + j.at("isMagicAcquired").get_to(saveContext.isMagicAcquired); + j.at("isDoubleMagicAcquired").get_to(saveContext.isDoubleMagicAcquired); + j.at("isDoubleDefenseAcquired").get_to(saveContext.isDoubleDefenseAcquired); + j.at("bgsFlag").get_to(saveContext.bgsFlag); + j.at("swordHealth").get_to(saveContext.swordHealth); + std::vector sceneFlagsArray; + j.at("sceneFlags").get_to(sceneFlagsArray); + for (int i = 0; i < 124; i++) { + saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4]; + saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1]; + saveContext.sceneFlags[i].clear = sceneFlagsArray[i * 4 + 2]; + saveContext.sceneFlags[i].collect = sceneFlagsArray[i * 4 + 3]; + } + j.at("eventChkInf").get_to(saveContext.eventChkInf); + j.at("itemGetInf").get_to(saveContext.itemGetInf); + j.at("infTable").get_to(saveContext.infTable); + j.at("randomizerInf").get_to(saveContext.randomizerInf); + j.at("gsFlags").get_to(saveContext.gsFlags); + j.at("inventory").get_to(saveContext.inventory); + j.at("sohStats").get_to(saveContext.sohStats); + j.at("adultTradeItems").get_to(saveContext.adultTradeItems); + j.at("triforcePiecesCollected").get_to(saveContext.triforcePiecesCollected); + j.at("questId").get_to(saveContext.questId); +} + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_JSON_CONVERSIONS_H +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/AllClientState.cpp b/soh/soh/Network/Anchor/Packets/AllClientState.cpp new file mode 100644 index 00000000000..d9d455535b8 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/AllClientState.cpp @@ -0,0 +1,74 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/OTRGlobals.h" +#include "soh/Notification/Notification.h" + +/** + * ALL_CLIENT_STATE + * + * Contains a list of all clients and their CLIENT_STATE currently connected to the server + * + * The server itself sends this packet to all clients when a client connects or disconnects + */ + +void Anchor::HandlePacket_AllClientState(nlohmann::json payload) { + std::vector newClients = payload["state"].get>(); + + // add new clients + for (auto& client : newClients) { + if (client.self) { + ownClientId = client.clientId; + CVarSetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), ownClientId); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + clients[client.clientId].self = true; + } else { + clients[client.clientId].self = false; + if (clients.contains(client.clientId)) { + if (clients[client.clientId].online != client.online) { + Notification::Emit({ + .prefix = client.name, + .message = client.online ? "Connected" : "Disconnected", + }); + } + } else if (client.online) { + Notification::Emit({ + .prefix = client.name, + .message = "Connected", + }); + } + } + + clients[client.clientId].clientId = client.clientId; + clients[client.clientId].name = client.name; + clients[client.clientId].color = client.color; + clients[client.clientId].clientVersion = client.clientVersion; + clients[client.clientId].teamId = client.teamId; + clients[client.clientId].online = client.online; + clients[client.clientId].seed = client.seed; + clients[client.clientId].isSaveLoaded = client.isSaveLoaded; + clients[client.clientId].isGameComplete = client.isGameComplete; + clients[client.clientId].sceneNum = client.sceneNum; + clients[client.clientId].entranceIndex = client.entranceIndex; + } + + // remove clients that are no longer in the list + std::vector clientsToRemove; + for (auto& [clientId, client] : clients) { + if (std::find_if(newClients.begin(), newClients.end(), + [clientId](AnchorClient& c) { return c.clientId == clientId; }) == newClients.end()) { + clientsToRemove.push_back(clientId); + } + } + // (seperate loop to avoid iterator invalidation) + for (auto& clientId : clientsToRemove) { + clients.erase(clientId); + } + + RefreshClientActors(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp b/soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp new file mode 100644 index 00000000000..645916f6dac --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/ConsumeAdultTradeItem.cpp @@ -0,0 +1,46 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +#include "soh/Enhancements/randomizer/adult_trade_shuffle.h" +extern PlayState* gPlayState; +} + +/** + * CONSUME_ADULT_TRADE_ITEM + * + * This is primarily to just get rid of used adult trade items to prevent confusion for other players. + * Whatever flags/items are given from adult trade checks are synced by other packets. + */ + +void Anchor::SendPacket_ConsumeAdultTradeItem(u8 itemId) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = CONSUME_ADULT_TRADE_ITEM; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["itemId"] = itemId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint8_t itemId = payload["itemId"].get(); + gSaveContext.adultTradeItems &= ~ADULT_TRADE_FLAG(itemId); + Inventory_ReplaceItem(gPlayState, itemId, Randomizer_GetNextAdultTradeItem()); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp new file mode 100644 index 00000000000..e2da27eef43 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp @@ -0,0 +1,62 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +#include "macros.h" +#include "functions.h" +extern PlayState* gPlayState; +void func_80838280(Player* player); +} + +/** + * DAMAGE_PLAYER + */ + +void Anchor::SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = DAMAGE_PLAYER; + payload["targetClientId"] = clientId; + payload["damageEffect"] = damageEffect; + payload["damage"] = damage; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_DamagePlayer(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + if (!clients.contains(clientId) || clients[clientId].player == nullptr) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + Player* otherPlayer = anchorClient.player; + Player* self = GET_PLAYER(gPlayState); + + u8 damageEffect = payload["damageEffect"].get(); + u8 damage = payload["damage"].get(); + + self->actor.colChkInfo.damage = damage * 8; // Arbitrary number currently, need to fine tune + + if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_FIRE) { + for (int i = 0; i < ARRAY_COUNT(self->bodyFlameTimers); i++) { + self->bodyFlameTimers[i] = Rand_S16Offset(0, 200); + } + self->bodyIsBurning = true; + } else if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) { + self->actor.freezeTimer = 20; + Actor_SetColorFilter(&self->actor, 0, 0xFF, 0, 24); + return; + } + + func_80837C0C(gPlayState, self, damageEffect, 4.0f, 5.0f, Actor_WorldYawTowardActor(&otherPlayer->actor, &self->actor), 20); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp new file mode 100644 index 00000000000..6f01ddef62a --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp @@ -0,0 +1,18 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +/** + * DISABLE_ANCHOR + * + * No current use, potentially will be used for a future feature. + */ + +void Anchor::HandlePacket_DisableAnchor(nlohmann::json payload) { + Disable(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp new file mode 100644 index 00000000000..f5c27a82f13 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp @@ -0,0 +1,37 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" +#include "soh/OTRGlobals.h" + +/** + * ENTRANCE_DISCOVERED + */ + +void Anchor::SendPacket_EntranceDiscovered(u16 entranceIndex) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = ENTRANCE_DISCOVERED; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["entranceIndex"] = entranceIndex; + payload["quiet"] = true; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_EntranceDiscovered(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + u16 entranceIndex = payload["entranceIndex"].get(); + Entrance_SetEntranceDiscovered(entranceIndex, 1); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/GameComplete.cpp b/soh/soh/Network/Anchor/Packets/GameComplete.cpp new file mode 100644 index 00000000000..cd7d72d5bd4 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/GameComplete.cpp @@ -0,0 +1,48 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" +#include "soh/Enhancements/randomizer/3drando/random.hpp" + +const std::string gameCompleteMessages[] = { + "killed Ganon", + "saved Zelda", + "proved their Courage", + "collected the Triforce", + "is the Hero of Time", + "proved Mido wrong", +}; + +/** + * GAME_COMPLETE + */ + +void Anchor::SendPacket_GameComplete() { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = GAME_COMPLETE; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_GameComplete(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + if (!clients.contains(clientId)) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + anchorClient.isGameComplete = true; + Notification::Emit({ + .prefix = anchorClient.name, + .message = RandomElement(gameCompleteMessages), + }); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/GiveItem.cpp b/soh/soh/Network/Anchor/Packets/GiveItem.cpp new file mode 100644 index 00000000000..f6a6e05d2a8 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/GiveItem.cpp @@ -0,0 +1,104 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" +#include "soh/Enhancements/randomizer/randomizer.h" +#include "soh/ImGuiUtils.h" +#include "soh/Enhancements/item-tables/ItemTableManager.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +extern PlayState* gPlayState; +} + +/** + * GIVE_ITEM + */ + +static bool gettingItem; +static uint8_t incomingIceTraps; + +void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId) { + if (!IsSaveLoaded() || gettingItem) { + return; + } + + if (modId == MOD_RANDOMIZER && getItemId == RG_ICE_TRAP && incomingIceTraps > 0) { + incomingIceTraps = MAX(incomingIceTraps - 1, 0); + return; + } + + nlohmann::json payload; + payload["type"] = GIVE_ITEM; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["modId"] = modId; + payload["getItemId"] = getItemId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_GiveItem(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + AnchorClient& client = clients[clientId]; + + GetItemEntry getItemEntry; + if (payload["modId"].get() == MOD_NONE) { + getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, payload["getItemId"].get()); + } else { + getItemEntry = Rando::StaticData::RetrieveItem(payload["getItemId"].get()).GetGIEntry_Copy(); + } + + gettingItem = true; + if (getItemEntry.modIndex == MOD_NONE) { + if (getItemEntry.getItemId == GI_SWORD_BGS) { + gSaveContext.bgsFlag = true; + } + Item_Give(gPlayState, getItemEntry.itemId); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + if (getItemEntry.getItemId == RG_ICE_TRAP) { + gSaveContext.pendingIceTrapCount++; + incomingIceTraps++; + } else { + Randomizer_Item_Give(gPlayState, getItemEntry); + } + } + + // Handle if the player gets a 4th heart piece (usually handled in z_message) + s32 heartPieces = (s32)(gSaveContext.inventory.questItems & 0xF0000000) >> (QUEST_HEART_PIECE + 4); + if (heartPieces >= 4) { + gSaveContext.inventory.questItems &= ~0xF0000000; + gSaveContext.inventory.questItems += (heartPieces % 4) << (QUEST_HEART_PIECE + 4); + gSaveContext.healthCapacity += 0x10 * (heartPieces / 4); + gSaveContext.health += 0x10 * (heartPieces / 4); + gSaveContext.healthAccumulator = 0x140; + } + gettingItem = false; + + if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) { + if (getItemEntry.modIndex == MOD_NONE) { + Notification::Emit({ + .itemIcon = GetTextureForItemId(getItemEntry.itemId), + .prefix = client.name, + .message = "found", + .suffix = SohUtils::GetItemName(getItemEntry.itemId), + }); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + Notification::Emit({ + .prefix = client.name, + .message = "found", + .suffix = Rando::StaticData::RetrieveItem((RandomizerGet)getItemEntry.getItemId).GetName().english, + }); + } + } +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/Handshake.cpp b/soh/soh/Network/Anchor/Packets/Handshake.cpp new file mode 100644 index 00000000000..592da493216 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/Handshake.cpp @@ -0,0 +1,26 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * HANDSHAKE + * + * Sent by the client to the server when it first connects to the server, sends over both the local room settings + * in case the room needs to be created, along with the current client state + */ + +void Anchor::SendPacket_Handshake() { + nlohmann::json payload; + payload["type"] = HANDSHAKE; + payload["roomId"] = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""); + payload["roomState"] = PrepRoomState(); + payload["clientState"] = PrepClientState(); + + SendJsonToRemote(payload); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp new file mode 100644 index 00000000000..dc2741d9a38 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp @@ -0,0 +1,51 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "functions.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * PLAYER_SFX + * + * Sound effects, only sent to other clients in the same scene as the player + */ + +void Anchor::SendPacket_PlayerSfx(u16 sfxId) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + + payload["type"] = PLAYER_SFX; + payload["sfxId"] = sfxId; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerSfx(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + u16 sfxId = payload["sfxId"].get(); + + if (!clients.contains(clientId) || !clients[clientId].player) { + return; + } + + Player_PlaySfx((Actor*)clients[clientId].player, sfxId); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp new file mode 100644 index 00000000000..7a604d80205 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp @@ -0,0 +1,120 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * PLAYER_UPDATE + * + * Contains real-time data necessary to update other clients in the same scene as the player + * + * Sent every frame to other clients within the same scene + * + * Note: This packet is sent _a lot_, so please do not include any unnecessary data in it + */ + +void Anchor::SendPacket_PlayerUpdate() { + if (!IsSaveLoaded()) { + return; + } + + uint32_t currentPlayerCount = 0; + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded) { + currentPlayerCount++; + } + } + if (currentPlayerCount == 0) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + nlohmann::json payload; + + payload["type"] = PLAYER_UPDATE; + payload["sceneNum"] = gPlayState->sceneNum; + payload["entranceIndex"] = gSaveContext.entranceIndex; + payload["linkAge"] = gSaveContext.linkAge; + payload["posRot"]["pos"] = player->actor.world.pos; + payload["posRot"]["rot"] = player->actor.shape.rot; + std::vector jointArray; + for (const auto& joint : player->jointTable) { + jointArray.push_back(joint.x); + jointArray.push_back(joint.y); + jointArray.push_back(joint.z); + } + payload["jointTable"] = jointArray; + payload["upperLimbRot"] = player->upperLimbRot; + payload["currentBoots"] = player->currentBoots; + payload["currentShield"] = player->currentShield; + payload["currentTunic"] = player->currentTunic; + payload["stateFlags1"] = player->stateFlags1; + payload["stateFlags2"] = player->stateFlags2; + payload["buttonItem0"] = gSaveContext.equips.buttonItems[0]; + payload["itemAction"] = player->itemAction; + payload["heldItemAction"] = player->heldItemAction; + payload["modelGroup"] = player->modelGroup; + payload["invincibilityTimer"] = player->invincibilityTimer; + payload["unk_862"] = player->unk_862; + payload["actionVar1"] = player->av1.actionVar1; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + bool shouldRefreshActors = false; + + if (clients.contains(clientId)) { + auto& client = clients[clientId]; + + if (client.linkAge != payload["linkAge"].get()) { + shouldRefreshActors = true; + } + + client.sceneNum = payload["sceneNum"].get(); + client.entranceIndex = payload["entranceIndex"].get(); + client.linkAge = payload["linkAge"].get(); + client.posRot = payload["posRot"].get(); + std::vector jointArray = payload["jointTable"]; + for (int i = 0; i < 24; i++) { + client.jointTable[i].x = jointArray[i * 3]; + client.jointTable[i].y = jointArray[i * 3 + 1]; + client.jointTable[i].z = jointArray[i * 3 + 2]; + } + client.upperLimbRot = payload["upperLimbRot"].get(); + client.currentBoots = payload["currentBoots"].get(); + client.currentShield = payload["currentShield"].get(); + client.currentTunic = payload["currentTunic"].get(); + client.stateFlags1 = payload["stateFlags1"].get(); + client.stateFlags2 = payload["stateFlags2"].get(); + client.buttonItem0 = payload["buttonItem0"].get(); + client.itemAction = payload["itemAction"].get(); + client.heldItemAction = payload["heldItemAction"].get(); + client.modelGroup = payload["modelGroup"].get(); + client.invincibilityTimer = payload["invincibilityTimer"].get(); + client.unk_862 = payload["unk_862"].get(); + client.actionVar1 = payload["actionVar1"].get(); + } + + if (shouldRefreshActors) { + RefreshClientActors(); + } +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp new file mode 100644 index 00000000000..f2a556dd1f1 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp @@ -0,0 +1,37 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/OTRGlobals.h" + +/** + * REQUEST_TEAM_STATE + * + * Requests team state from the server, which will pass on the request to any connected teammates, or send the last known + * state if no teammates are connected. + * + * This fires when loading into a file while Anchor is connected, or when Anchor is connected while a file is already + * loaded + * + * Note: This can additionally be fired with a button in the menus to fix any desyncs that may have occurred in the save + * state + */ + +void Anchor::SendPacket_RequestTeamState() { + nlohmann::json payload; + payload["type"] = REQUEST_TEAM_STATE; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_RequestTeamState(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + SendPacket_UpdateTeamState(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp new file mode 100644 index 00000000000..fdd79e536f6 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp @@ -0,0 +1,36 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +/** + * REQUEST_TELEPORT + * + * Because we don't have all the necessary information to directly teleport to a player, we emit a request, + * in which they will respond with a TELEPORT_TO packet, with the necessary information. + */ + +void Anchor::SendPacket_RequestTeleport(uint32_t clientId) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = REQUEST_TELEPORT; + payload["targetClientId"] = clientId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_RequestTeleport(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + SendPacket_TeleportTo(clientId); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/ServerMessage.cpp b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp new file mode 100644 index 00000000000..3af07c8b839 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp @@ -0,0 +1,21 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" + +/** + * SERVER_MESSAGE + */ + +void Anchor::HandlePacket_ServerMessage(nlohmann::json payload) { + Notification::Emit({ + .prefix = "Server:", + .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f), + .message = payload["message"].get(), + }); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp new file mode 100644 index 00000000000..276e5258662 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp @@ -0,0 +1,53 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * SET_CHECK_STATUS + * + * Fired when a check status is updated or skipped + */ + +void Anchor::SendPacket_SetCheckStatus(RandomizerCheck rc) { + if (!IsSaveLoaded()) { + return; + } + + auto randoContext = Rando::Context::GetInstance(); + + nlohmann::json payload; + payload["type"] = SET_CHECK_STATUS; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["rc"] = rc; + payload["status"] = randoContext->GetItemLocation(rc)->GetCheckStatus(); + payload["skipped"] = randoContext->GetItemLocation(rc)->GetIsSkipped(); + payload["quiet"] = true; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + auto randoContext = Rando::Context::GetInstance(); + + RandomizerCheck rc = payload["rc"].get(); + RandomizerCheckStatus status = payload["status"].get(); + bool skipped = payload["skipped"].get(); + + if (randoContext->GetItemLocation(rc)->GetCheckStatus() != status) { + randoContext->GetItemLocation(rc)->SetCheckStatus(status); + } + if (randoContext->GetItemLocation(rc)->GetIsSkipped() != skipped) { + randoContext->GetItemLocation(rc)->SetIsSkipped(skipped); + } +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp new file mode 100644 index 00000000000..b7438ab95c5 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp @@ -0,0 +1,50 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * SET_FLAG + * + * Fired when a flag is set in the save context + */ + +void Anchor::SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag) { + nlohmann::json payload; + payload["type"] = SET_FLAG; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["sceneNum"] = sceneNum; + payload["flagType"] = flagType; + payload["flag"] = flag; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_SetFlag(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + s16 sceneNum = payload["sceneNum"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + if (sceneNum == SCENE_ID_MAX) { + auto effect = new GameInteractionEffect::SetFlag(); + effect->parameters[0] = payload["flagType"].get(); + effect->parameters[1] = payload["flag"].get(); + effect->Apply(); + } else { + auto effect = new GameInteractionEffect::SetSceneFlag(); + effect->parameters[0] = payload["sceneNum"].get(); + effect->parameters[1] = payload["flagType"].get(); + effect->parameters[2] = payload["flag"].get(); + effect->Apply(); + } +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp new file mode 100644 index 00000000000..05e3cbb8362 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp @@ -0,0 +1,63 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" + +extern "C" { +#include "macros.h" +extern PlayState* gPlayState; +} + +/** + * TELEPORT_TO + * + * See REQUEST_TELEPORT for more information, this is the second part of the process. + */ + +void Anchor::SendPacket_TeleportTo(uint32_t clientId) { + if (!IsSaveLoaded()) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + + nlohmann::json payload; + payload["type"] = TELEPORT_TO; + payload["targetClientId"] = clientId; + payload["entranceIndex"] = gSaveContext.entranceIndex; + payload["roomIndex"] = gPlayState->roomCtx.curRoom.num; + payload["posRot"] = player->actor.world; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + s32 entranceIndex = payload["entranceIndex"].get(); + s8 roomIndex = payload["roomIndex"].get(); + PosRot posRot = payload["posRot"].get(); + + gPlayState->nextEntranceIndex = entranceIndex; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_INSTANT; + gSaveContext.respawn[RESPAWN_MODE_DOWN].entranceIndex = entranceIndex; + gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = roomIndex; + gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = posRot.pos; + gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = posRot.rot.y; + gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = 0xDFF; + gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK_FAST; + gSaveContext.respawnFlag = 1; + static HOOK_ID hookId = 0; + hookId = REGISTER_VB_SHOULD(VB_INFLICT_VOID_DAMAGE, { + *should = false; + GameInteractor::Instance->UnregisterGameHookForID(hookId); + }); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp new file mode 100644 index 00000000000..91da3f9b1da --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp @@ -0,0 +1,51 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * UNSET_FLAG + * + * Fired when a flag is unset in the save context + */ + +void Anchor::SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag) { + nlohmann::json payload; + payload["type"] = UNSET_FLAG; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["sceneNum"] = sceneNum; + payload["flagType"] = flagType; + payload["flag"] = flag; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + s16 sceneNum = payload["sceneNum"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + if (sceneNum == SCENE_ID_MAX) { + auto effect = new GameInteractionEffect::UnsetFlag(); + effect->parameters[0] = payload["flagType"].get(); + effect->parameters[1] = payload["flag"].get(); + effect->Apply(); + } else { + auto effect = new GameInteractionEffect::UnsetSceneFlag(); + effect->parameters[0] = payload["sceneNum"].get(); + effect->parameters[1] = payload["flagType"].get(); + effect->parameters[2] = payload["flag"].get(); + effect->Apply(); + } + +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp new file mode 100644 index 00000000000..89e1d644367 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp @@ -0,0 +1,43 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "macros.h" +} + +/** + * UPDATE_BEANS_COUNT + * + * Keeps the client's bean count in sync as they buy/use them + */ + +void Anchor::SendPacket_UpdateBeansCount() { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = UPDATE_BEANS_COUNT; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["amount"] = AMMO(ITEM_BEAN); + payload["amountBought"] = BEANS_BOUGHT; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateBeansCount(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + AMMO(ITEM_BEAN) = payload["amount"].get(); + BEANS_BOUGHT = payload["amountBought"].get(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp new file mode 100644 index 00000000000..237f1baa01a --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp @@ -0,0 +1,77 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_CLIENT_STATE + * + * Contains a small subset of data that is cached on the server and important for the client to know for various reasons + * + * Sent on various events, such as changing scenes, soft resetting, finishing the game, opening file select, etc. + * + * Note: This packet should be cross version compatible, so if you add anything here don't assume all clients will be + * providing it, consider doing a `contains` check before accessing any version specific data + */ + +nlohmann::json Anchor::PrepClientState() { + nlohmann::json payload; + payload["name"] = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), ""); + payload["color"] = CVarGetColor24(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100 }); + payload["clientVersion"] = clientVersion; + payload["teamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["online"] = true; + + if (IsSaveLoaded()) { + payload["seed"] = IS_RANDO ? Rando::Context::GetInstance()->GetSettings()->GetSeed() : 0; + payload["isSaveLoaded"] = true; + payload["isGameComplete"] = gSaveContext.sohStats.gameComplete; + payload["sceneNum"] = gPlayState->sceneNum; + payload["entranceIndex"] = gSaveContext.entranceIndex; + } else { + payload["seed"] = 0; + payload["isSaveLoaded"] = false; + payload["isGameComplete"] = false; + payload["sceneNum"] = SCENE_ID_MAX; + payload["entranceIndex"] = 0x00; + } + + return payload; +} + +void Anchor::SendPacket_UpdateClientState() { + nlohmann::json payload; + payload["type"] = UPDATE_CLIENT_STATE; + payload["state"] = PrepClientState(); + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateClientState(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + if (clients.contains(clientId)) { + AnchorClient client = payload["state"].get(); + clients[clientId].clientId = clientId; + clients[clientId].name = client.name; + clients[clientId].color = client.color; + clients[clientId].clientVersion = client.clientVersion; + clients[clientId].teamId = client.teamId; + clients[clientId].online = client.online; + clients[clientId].seed = client.seed; + clients[clientId].isSaveLoaded = client.isSaveLoaded; + clients[clientId].isGameComplete = client.isGameComplete; + clients[clientId].sceneNum = client.sceneNum; + clients[clientId].entranceIndex = client.entranceIndex; + } +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp new file mode 100644 index 00000000000..c7beaa6b816 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -0,0 +1,42 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * UPDATE_DUNGEON_ITEMS + * + * This is for 2 things, first is updating the dungeon items in vanilla saves, and second is + * for ensuring the amount of keys used is synced as players are using them. + */ + +void Anchor::SendPacket_UpdateDungeonItems() { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = UPDATE_DUNGEON_ITEMS; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["mapIndex"] = gSaveContext.mapIndex; + payload["dungeonItems"] = gSaveContext.inventory.dungeonItems[gSaveContext.mapIndex]; + payload["dungeonKeys"] = gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + u16 mapIndex = payload["mapIndex"].get(); + gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get(); + gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp new file mode 100644 index 00000000000..ce08f3640fc --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp @@ -0,0 +1,47 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_ROOM_STATE + */ + +nlohmann::json Anchor::PrepRoomState() { + nlohmann::json payload; + payload["ownerClientId"] = ownClientId; + payload["pvpMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), 1); + payload["showLocationsMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), 1); + payload["teleportMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), 1); + + return payload; +} + +void Anchor::SendPacket_UpdateRoomState() { + nlohmann::json payload; + payload["type"] = UPDATE_ROOM_STATE; + payload["state"] = PrepRoomState(); + + Network::SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateRoomState(nlohmann::json payload) { + if (!payload.contains("state")) { + return; + } + + roomState.ownerClientId = payload["state"]["ownerClientId"].get(); + roomState.pvpMode = payload["state"]["pvpMode"].get(); + roomState.showLocationsMode = payload["state"]["showLocationsMode"].get(); + roomState.teleportMode = payload["state"]["teleportMode"].get(); +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp new file mode 100644 index 00000000000..cbc9088eb53 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp @@ -0,0 +1,259 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/Enhancements/randomizer/entrance.h" +#include "soh/Enhancements/randomizer/dungeon.h" +#include "soh/OTRGlobals.h" +#include "soh/Notification/Notification.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_TEAM_STATE + * + * Pushes the current save state to the server for other teammates to use. + * + * Fires when the server passes on a REQUEST_TEAM_STATE packet, or when this client saves the game + * + * When sending this packet we will assume that the team queue has been emptied for this client, so the queue + * stored in the server will be cleared. + * + * When receiving this packet, if there is items in the team queue, we will play them back in order. + */ + +void Anchor::SendPacket_UpdateTeamState() { + if (!IsSaveLoaded()) { + return; + } + + json payload; + payload["type"] = UPDATE_TEAM_STATE; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + + // Assume the team queue has been emptied, so clear it + payload["queue"] = json::array(); + + payload["state"] = gSaveContext; + // manually update current scene flags + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4] = gPlayState->actorCtx.flags.chest; + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 1] = gPlayState->actorCtx.flags.swch; + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 2] = gPlayState->actorCtx.flags.clear; + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 3] = gPlayState->actorCtx.flags.collect; + + // The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player doesn't have to generate the seed themselves + // Currently it doesn't work :) + if (IS_RANDO) { + auto randoContext = Rando::Context::GetInstance(); + + payload["state"]["rando"] = json::object(); + payload["state"]["rando"]["itemLocations"] = json::array(); + for (int i = 0; i < RC_MAX; i++) { + payload["state"]["rando"]["itemLocations"][i] = json::array(); + // payload["state"]["rando"]["itemLocations"][i]["rgID"] = randoContext->GetItemLocation(i)->GetPlacedRandomizerGet(); + payload["state"]["rando"]["itemLocations"][i][0] = randoContext->GetItemLocation(i)->GetCheckStatus(); + payload["state"]["rando"]["itemLocations"][i][1] = (u8)randoContext->GetItemLocation(i)->GetIsSkipped(); + + // if (randoContext->GetItemLocation(i)->GetPlacedRandomizerGet() == RG_ICE_TRAP) { + // payload["state"]["rando"]["itemLocations"][i]["fakeRgID"] = randoContext->GetItemOverride(i).LooksLike(); + // payload["state"]["rando"]["itemLocations"][i]["trickName"] = json::object(); + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"] = randoContext->GetItemOverride(i).GetTrickName().GetEnglish(); + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"] = randoContext->GetItemOverride(i).GetTrickName().GetFrench(); + // } + // if (randoContext->GetItemLocation(i)->HasCustomPrice()) { + // payload["state"]["rando"]["itemLocations"][i]["price"] = randoContext->GetItemLocation(i)->GetPrice(); + // } + } + + // auto entranceCtx = randoContext->GetEntranceShuffler(); + // for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) { + // payload["state"]["rando"]["entrances"][i] = json::object(); + // payload["state"]["rando"]["entrances"][i]["type"] = entranceCtx->entranceOverrides[i].type; + // payload["state"]["rando"]["entrances"][i]["index"] = entranceCtx->entranceOverrides[i].index; + // payload["state"]["rando"]["entrances"][i]["destination"] = entranceCtx->entranceOverrides[i].destination; + // payload["state"]["rando"]["entrances"][i]["override"] = entranceCtx->entranceOverrides[i].override; + // payload["state"]["rando"]["entrances"][i]["overrideDestination"] = entranceCtx->entranceOverrides[i].overrideDestination; + // } + + // payload["state"]["rando"]["seed"] = json::array(); + // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) { + // payload["state"]["rando"]["seed"][i] = randoContext->hashIconIndexes[i]; + // } + // payload["state"]["rando"]["inputSeed"] = randoContext->GetSettings()->GetSeedString(); + // payload["state"]["rando"]["finalSeed"] = randoContext->GetSettings()->GetSeed(); + + // payload["state"]["rando"]["randoSettings"] = json::array(); + // for (int i = 0; i < RSK_MAX; i++) { + // payload["state"]["rando"]["randoSettings"][i] = randoContext->GetOption((RandomizerSettingKey(i))).GetSelectedOptionIndex(); + // } + + // payload["state"]["rando"]["masterQuestDungeonCount"] = randoContext->GetDungeons()->CountMQ(); + // payload["state"]["rando"]["masterQuestDungeons"] = json::array(); + // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) { + // payload["state"]["rando"]["masterQuestDungeons"][i] = randoContext->GetDungeon(i)->IsMQ(); + // } + // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) { + // payload["state"]["rando"]["requiredTrials"][i] = randoContext->GetTrial(i)->IsRequired(); + // } + } + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) { + isHandlingUpdateTeamState = true; + // This can happen in between file select and the game starting, so we cant use this check, but we need to ensure we + // be careful to wrap PlayState usage in this check + // if (!IsSaveLoaded()) { + // return; + // } + + if (payload.contains("state")) { + SaveContext loadedData = payload["state"].get(); + + gSaveContext.questId = loadedData.questId; + gSaveContext.healthCapacity = loadedData.healthCapacity; + gSaveContext.magicLevel = loadedData.magicLevel; + gSaveContext.magicCapacity = gSaveContext.magic = loadedData.magicCapacity; + gSaveContext.isMagicAcquired = loadedData.isMagicAcquired; + gSaveContext.isDoubleMagicAcquired = loadedData.isDoubleMagicAcquired; + gSaveContext.isDoubleDefenseAcquired = loadedData.isDoubleDefenseAcquired; + gSaveContext.bgsFlag = loadedData.bgsFlag; + gSaveContext.swordHealth = loadedData.swordHealth; + gSaveContext.adultTradeItems = loadedData.adultTradeItems; + gSaveContext.triforcePiecesCollected = loadedData.triforcePiecesCollected; + + for (int i = 0; i < 124; i++) { + gSaveContext.sceneFlags[i] = loadedData.sceneFlags[i]; + if (IsSaveLoaded() && gPlayState->sceneNum == i) { + gPlayState->actorCtx.flags.chest = loadedData.sceneFlags[i].chest; + gPlayState->actorCtx.flags.swch = loadedData.sceneFlags[i].swch; + gPlayState->actorCtx.flags.clear = loadedData.sceneFlags[i].clear; + gPlayState->actorCtx.flags.collect = loadedData.sceneFlags[i].collect; + } + } + + for (int i = 0; i < 14; i++) { + gSaveContext.eventChkInf[i] = loadedData.eventChkInf[i]; + } + + for (int i = 0; i < 4; i++) { + gSaveContext.itemGetInf[i] = loadedData.itemGetInf[i]; + } + + // Skip last row of infTable, don't want to sync swordless flag + for (int i = 0; i < 29; i++) { + gSaveContext.infTable[i] = loadedData.infTable[i]; + } + + for (int i = 0; i < 17; i++) { + gSaveContext.randomizerInf[i] = loadedData.randomizerInf[i]; + } + + for (int i = 0; i < 6; i++) { + gSaveContext.gsFlags[i] = loadedData.gsFlags[i]; + } + + gSaveContext.sohStats.fileCreatedAt = loadedData.sohStats.fileCreatedAt; + + // Restore master sword state + u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_TYPE_SWORD, 1); + if (hasMasterSword) { + loadedData.inventory.equipment |= 0x2; + } else { + loadedData.inventory.equipment &= ~0x2; + } + + // Restore bottle contents (unless it's ruto's letter) + for (int i = 0; i < 4; i++) { + if (gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_NONE && gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_LETTER_RUTO) { + loadedData.inventory.items[SLOT_BOTTLE_1 + i] = gSaveContext.inventory.items[SLOT_BOTTLE_1 + i]; + } + } + + // Restore ammo if it's non-zero, unless it's beans + for (int i = 0; i < ARRAY_COUNT(gSaveContext.inventory.ammo); i++) { + if (gSaveContext.inventory.ammo[i] != 0 && i != SLOT(ITEM_BEAN) && i != SLOT(ITEM_BEAN + 1)) { + loadedData.inventory.ammo[i] = gSaveContext.inventory.ammo[i]; + } + } + + gSaveContext.inventory = loadedData.inventory; + + // The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player doesn't have to generate the seed themselves + // Currently it doesn't work :) + if (IS_RANDO && payload["state"].contains("rando")) { + auto randoContext = Rando::Context::GetInstance(); + + for (int i = 0; i < RC_MAX; i++) { + // randoContext->GetItemLocation(i)->RefPlacedItem() = payload["state"]["rando"]["itemLocations"][i]["rgID"].get(); + OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetCheckStatus(payload["state"]["rando"]["itemLocations"][i][0].get()); + OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetIsSkipped(payload["state"]["rando"]["itemLocations"][i][0].get()); + + // if (payload["state"]["rando"]["itemLocations"][i].contains("fakeRgID")) { + // randoContext->overrides.emplace(static_cast(i), Rando::ItemOverride(static_cast(i), payload["state"]["rando"]["itemLocations"][i]["fakeRgID"].get())); + // randoContext->GetItemOverride(i).GetTrickName().english = payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"].get(); + // randoContext->GetItemOverride(i).GetTrickName().french = payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"].get(); + // } + // if (payload["state"]["rando"]["itemLocations"][i].contains("price")) { + // u16 price = payload["state"]["rando"]["itemLocations"][i]["price"].get(); + // if (price > 0) { + // randoContext->GetItemLocation(i)->SetCustomPrice(price); + // } + // } + } + + // auto entranceCtx = randoContext->GetEntranceShuffler(); + // for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) { + // entranceCtx->entranceOverrides[i].type = payload["state"]["rando"]["entrances"][i]["type"].get(); + // entranceCtx->entranceOverrides[i].index = payload["state"]["rando"]["entrances"][i]["index"].get(); + // entranceCtx->entranceOverrides[i].destination = payload["state"]["rando"]["entrances"][i]["destination"].get(); + // entranceCtx->entranceOverrides[i].override = payload["state"]["rando"]["entrances"][i]["override"].get(); + // entranceCtx->entranceOverrides[i].overrideDestination = payload["state"]["rando"]["entrances"][i]["overrideDestination"].get(); + // } + + // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) { + // randoContext->hashIconIndexes[i] = payload["state"]["rando"]["seed"][i].get(); + // } + // randoContext->GetSettings()->SetSeedString(payload["state"]["rando"]["inputSeed"].get()); + // randoContext->GetSettings()->SetSeed(payload["state"]["rando"]["finalSeed"].get()); + + // for (int i = 0; i < RSK_MAX; i++) { + // randoContext->GetOption(RandomizerSettingKey(i)).SetSelectedIndex(payload["state"]["rando"]["randoSettings"][i].get()); + // } + + // randoContext->GetDungeons()->ClearAllMQ(); + // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) { + // if (payload["state"]["rando"]["masterQuestDungeons"][i].get()) { + // randoContext->GetDungeon(i)->SetMQ(); + // } + // } + + // randoContext->GetTrials()->SkipAll(); + // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) { + // if (payload["state"]["rando"]["requiredTrials"][i].get()) { + // randoContext->GetTrial(i)->SetAsRequired(); + // } + // } + } + + Notification::Emit({ + .message = "Save updated from team", + }); + } + + if (payload.contains("queue")) { + for (auto& item : payload["queue"]) { + nlohmann::json itemPayload = nlohmann::json::parse(item.get()); + Anchor::Instance->OnIncomingJson(itemPayload); + } + } + isHandlingUpdateTeamState = false; +} + +#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index b5627db3658..362aab44792 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -77,8 +77,10 @@ #ifdef ENABLE_REMOTE_CONTROL #include "soh/Network/CrowdControl/CrowdControl.h" #include "soh/Network/Sail/Sail.h" +#include "soh/Network/Anchor/Anchor.h" CrowdControl* CrowdControl::Instance; Sail* Sail::Instance; +Anchor* Anchor::Instance; #endif #include "Enhancements/mods.h" @@ -1164,6 +1166,7 @@ extern "C" void InitOTR() { #ifdef ENABLE_REMOTE_CONTROL CrowdControl::Instance = new CrowdControl(); Sail::Instance = new Sail(); + Anchor::Instance = new Anchor(); #endif OTRMessage_Init(); @@ -1199,6 +1202,9 @@ extern "C" void InitOTR() { if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { Sail::Instance->Enable(); } + if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) { + Anchor::Instance->Enable(); + } #endif } @@ -1216,6 +1222,9 @@ extern "C" void DeinitOTR() { if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { Sail::Instance->Disable(); } + if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) { + Anchor::Instance->Disable(); + } SDLNet_Quit(); #endif diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 1edec30258c..4f12f00c7d3 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -1223,7 +1223,7 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se delete saveContext; InitMeta(fileNum); - GameInteractor::Instance->ExecuteHooks(fileNum); + GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); saveMtx.unlock(); } diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp index 2dbf62a99a6..fdfe76d3f92 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -39,6 +39,9 @@ #include "Enhancements/debugger/MessageViewer.h" #include "soh/Notification/Notification.h" #include "soh/Enhancements/TimeDisplay/TimeDisplay.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Network/Anchor/Anchor.h" +#endif bool isBetaQuestEnabled = false; @@ -138,6 +141,9 @@ namespace SohGui { std::shared_ptr mModalWindow; std::shared_ptr mNotificationWindow; std::shared_ptr mTimeDisplayWindow; +#ifdef ENABLE_REMOTE_CONTROL + std::shared_ptr mAnchorRoomWindow; +#endif void SetupGuiElements() { auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); @@ -225,6 +231,10 @@ namespace SohGui { mNotificationWindow->Show(); mTimeDisplayWindow = std::make_shared(CVAR_WINDOW("TimeDisplayEnabled"), "Additional Timers"); gui->AddGuiWindow(mTimeDisplayWindow); +#ifdef ENABLE_REMOTE_CONTROL + mAnchorRoomWindow = std::make_shared(CVAR_WINDOW("AnchorRoom"), "Anchor Room"); + gui->AddGuiWindow(mAnchorRoomWindow); +#endif } void Destroy() { @@ -261,6 +271,9 @@ namespace SohGui { mTimeSplitWindow = nullptr; mPlandomizerWindow = nullptr; mTimeDisplayWindow = nullptr; +#ifdef ENABLE_REMOTE_CONTROL + mAnchorRoomWindow = nullptr; +#endif } void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, std::function button1callback, std::function button2callback) { diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index ba9e683bf25..a76bf509e13 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -22,6 +22,7 @@ #ifdef ENABLE_REMOTE_CONTROL #include "soh/Network/CrowdControl/CrowdControl.h" #include "soh/Network/Sail/Sail.h" +#include "soh/Network/Anchor/Anchor.h" #endif @@ -2102,6 +2103,7 @@ void DrawRemoteControlMenu() { if (ImGui::BeginMenu("Network")) { Sail::Instance->DrawMenu(); CrowdControl::Instance->DrawMenu(); + Anchor::Instance->DrawMenu(); ImGui::EndMenu(); } } diff --git a/soh/soh/UIWidgets.cpp b/soh/soh/UIWidgets.cpp index 23af9f613e0..958685eac4a 100644 --- a/soh/soh/UIWidgets.cpp +++ b/soh/soh/UIWidgets.cpp @@ -825,7 +825,7 @@ namespace UIWidgets { 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); + bool InputString(const char* label, std::string* value, ImGuiInputTextFlags flags) { + return ImGui::InputText(label, (char*)value->c_str(), value->capacity() + 1, ImGuiInputTextFlags_CallbackResize | flags, InputTextResizeCallback, value); } } diff --git a/soh/soh/UIWidgets.hpp b/soh/soh/UIWidgets.hpp index 5903220d2fe..0ad2a470378 100644 --- a/soh/soh/UIWidgets.hpp +++ b/soh/soh/UIWidgets.hpp @@ -102,7 +102,7 @@ namespace UIWidgets { void DrawFlagArray16(const std::string& name, uint16_t& flags); void DrawFlagArray8(const std::string& name, uint8_t& flags); bool StateButton(const char* str_id, const char* label); - bool InputString(const char* label, std::string* value); + bool InputString(const char* label, std::string* value, ImGuiInputTextFlags flags = 0); } #endif /* UIWidgets_hpp */ diff --git a/soh/soh/cvar_prefixes.h b/soh/soh/cvar_prefixes.h index 6ddde97bdcc..cee81167e65 100644 --- a/soh/soh/cvar_prefixes.h +++ b/soh/soh/cvar_prefixes.h @@ -14,4 +14,5 @@ #define CVAR_GENERAL(var) CVAR_PREFIX_GENERAL "." var #define CVAR_REMOTE(var) CVAR_PREFIX_REMOTE "." var #define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE(".CrowdControl." var) -#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var) \ No newline at end of file +#define CVAR_REMOTE_SAIL(var) CVAR_REMOTE(".Sail." var) +#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE(".Anchor." var) diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index faa17123129..4b43e2ba9a6 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -117,6 +117,7 @@ std::vector sceneNames = { "Castle Hedge Maze (Early)", "Sasa Test", "Treasure Chest Room", + "Unknown", }; std::vector itemNames = { diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 70c7a3233b4..9c241898f45 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -1231,14 +1231,20 @@ void Actor_Init(Actor* actor, PlayState* play) { ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f); if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) { Actor_SetObjectDependency(play, actor); - actor->init(actor, play); - actor->init = NULL; - GameInteractor_ExecuteOnActorInit(actor); + if (GameInteractor_ShouldActorInit(actor)) { + actor->init(actor, play); + actor->init = NULL; - // For enemy health bar we need to know the max health during init - if (actor->category == ACTORCAT_ENEMY) { - actor->maximumHealth = actor->colChkInfo.health; + GameInteractor_ExecuteOnActorInit(actor); + + // For enemy health bar we need to know the max health during init + if (actor->category == ACTORCAT_ENEMY) { + actor->maximumHealth = actor->colChkInfo.health; + } + } else { + actor->init = NULL; + Actor_Kill(actor); } } } @@ -2210,6 +2216,10 @@ void Player_PlaySfx(Actor* actor, u16 sfxId) { // Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &D_801333E0 , &D_801333E0, &D_801333E8); Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &freqMultiplier, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); } + + if (actor->id == ACTOR_PLAYER) { + GameInteractor_ExecuteOnPlayerSfx(sfxId); + } } void Audio_PlayActorSound2(Actor* actor, u16 sfxId) { @@ -2589,14 +2599,20 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) { Actor_SetObjectDependency(play, actor); - actor->init(actor, play); - actor->init = NULL; - GameInteractor_ExecuteOnActorInit(actor); + if (GameInteractor_ShouldActorInit(actor)) { + actor->init(actor, play); + actor->init = NULL; + + GameInteractor_ExecuteOnActorInit(actor); - // For enemy health bar we need to know the max health during init - if (actor->category == ACTORCAT_ENEMY) { - actor->maximumHealth = actor->colChkInfo.health; + // For enemy health bar we need to know the max health during init + if (actor->category == ACTORCAT_ENEMY) { + actor->maximumHealth = actor->colChkInfo.health; + } + } else { + actor->init = NULL; + Actor_Kill(actor); } } actor = actor->next; diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c index 1bd85971811..b71a19febd0 100644 --- a/soh/src/code/z_en_item00.c +++ b/soh/src/code/z_en_item00.c @@ -775,6 +775,13 @@ void EnItem00_Update(Actor* thisx, PlayState* play) { EnItem00* this = (EnItem00*)thisx; s32 pad; + // #region SOH [Co-op] + if (Flags_GetCollectible(play, this->collectibleFlag)) { + Actor_Kill(&this->actor); + return; + } + // #endregion + // Rotate some drops when 3D drops are on, otherwise reset rotation back to 0 for billboard effect if ( (this->actor.params == ITEM00_HEART && this->unk_15A >= 0) || diff --git a/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c b/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c index 160dc15f90c..cf57bdc55c0 100644 --- a/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c +++ b/soh/src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.c @@ -211,7 +211,9 @@ void func_8086ED50(BgBombwall* this, PlayState* play) { } void func_8086ED70(BgBombwall* this, PlayState* play) { - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { + // #endregion this->collider.base.acFlags &= ~AC_HIT; func_8086EDFC(this, play); Flags_SetSwitch(play, this->dyna.actor.params & 0x3F); diff --git a/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c b/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c index 9f89e7980bf..bee6b115543 100644 --- a/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c +++ b/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c @@ -277,7 +277,9 @@ void BgBreakwall_Wait(BgBreakwall* this, PlayState* play) { } } - if (GameInteractor_Should(VB_BG_BREAKWALL_BREAK, this->collider.base.acFlags & 2 || blueFireArrowHit)) { + // #region SOH [Co-op] + if (GameInteractor_Should(VB_BG_BREAKWALL_BREAK, this->collider.base.acFlags & 2 || blueFireArrowHit) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { + // #endregion Vec3f effectPos; s32 wallType = ((this->dyna.actor.params >> 13) & 3) & 0xFF; diff --git a/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c b/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c index f419646038f..4d3ed967ffa 100644 --- a/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c +++ b/soh/src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.c @@ -268,7 +268,9 @@ void func_80882E54(BgHakaZou* this, PlayState* play) { } void func_80883000(BgHakaZou* this, PlayState* play) { - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->switchFlag)) { + // #endregion Flags_SetSwitch(play, this->switchFlag); if (this->dyna.actor.params == STA_GIANT_BIRD_STATUE) { diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c index c1d40218839..6ce78615c02 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c @@ -126,8 +126,10 @@ void BgHidanDalm_Destroy(Actor* thisx, PlayState* play) { void BgHidanDalm_Wait(BgHidanDalm* this, PlayState* play) { Player* player = GET_PLAYER(play); - if ((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) && - (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) { + // #region SOH [Co-op] + if (((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) && + (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) || Flags_GetSwitch(play, this->switchFlag)) { + // #endregion this->collider.base.acFlags &= ~AC_HIT; if ((this->collider.elements[0].info.bumperFlags & BUMP_HIT) || (this->collider.elements[1].info.bumperFlags & BUMP_HIT)) { diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c index 8185335716f..e49f33a9a90 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.c @@ -278,7 +278,9 @@ void func_80888734(BgHidanHamstep* this) { } void func_808887C4(BgHidanHamstep* this, PlayState* play) { - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params >> 8) & 0xFF)) { + // #endregion OnePointCutscene_Init(play, 3310, 100, &this->dyna.actor, MAIN_CAM); Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_HAMMER_SWITCH); this->collider.base.acFlags = AC_NONE; diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c index ea49fbd4f71..5079c200ed9 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.c @@ -201,7 +201,9 @@ void func_8088960C(BgHidanHrock* this, PlayState* play) { } void func_808896B8(BgHidanHrock* this, PlayState* play) { - if (this->collider.base.acFlags & 2) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & 2) || Flags_GetSwitch(play, this->unk_16A)) { + // #endregion this->collider.base.acFlags &= ~2; this->actionFunc = func_808894B0; this->dyna.actor.flags |= ACTOR_FLAG_UPDATE_WHILE_CULLED; diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c index c9cbb039f08..71f586f453b 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c @@ -303,7 +303,9 @@ void BgHidanKowarerukabe_Update(Actor* thisx, PlayState* play) { BgHidanKowarerukabe* this = (BgHidanKowarerukabe*)thisx; s32 pad; - if (Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) { + // #region SOH [Co-op] + if ((Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) || Flags_GetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F)) { + // #endregion BgHidanKowarerukabe_Break(this, play); Flags_SetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F); diff --git a/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c b/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c index 9810f27495a..d0998097892 100644 --- a/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c +++ b/soh/src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.c @@ -333,10 +333,12 @@ void func_8089107C(BgIceShelter* this, PlayState* play) { MeltOnIceArrowHit(this, this->cylinder2, type, play); } // Default blue fire check - if (this->cylinder1.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->cylinder1.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { this->cylinder1.base.acFlags &= ~AC_HIT; - if ((this->cylinder1.base.ac != NULL) && (this->cylinder1.base.ac->id == ACTOR_EN_ICE_HONO)) { + if (((this->cylinder1.base.ac != NULL) && (this->cylinder1.base.ac->id == ACTOR_EN_ICE_HONO)) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { + // #endregion if (type == 4) { if (this->dyna.actor.parent != NULL) { this->dyna.actor.parent->freezeTimer = 50; diff --git a/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c b/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c index cb36b743dbf..bb7333c520c 100644 --- a/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c +++ b/soh/src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.c @@ -142,7 +142,9 @@ void BgJyaBombchuiwa_SetupWaitForExplosion(BgJyaBombchuiwa* this, PlayState* pla } void BgJyaBombchuiwa_WaitForExplosion(BgJyaBombchuiwa* this, PlayState* play) { - if ((this->collider.base.acFlags & AC_HIT) || (this->timer > 0)) { + // #region SOH [Co-op] + if (((this->collider.base.acFlags & AC_HIT) || (this->timer > 0)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) { + // #endregion if (this->timer == 0) { OnePointCutscene_Init(play, 3410, -99, &this->actor, MAIN_CAM); } diff --git a/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c b/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c index d0e4b947178..9edb9edc65d 100644 --- a/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c +++ b/soh/src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.c @@ -163,7 +163,9 @@ void BgJyaBombiwa_Break(BgJyaBombiwa* this, PlayState* play) { void BgJyaBombiwa_Update(Actor* thisx, PlayState* play) { BgJyaBombiwa* this = (BgJyaBombiwa*)thisx; - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { + // #endregion BgJyaBombiwa_Break(this, play); Flags_SetSwitch(play, this->dyna.actor.params & 0x3F); SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); diff --git a/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c b/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c index 89db58c0e3e..8eaa8ff1067 100644 --- a/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c +++ b/soh/src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.c @@ -467,7 +467,9 @@ void BgMizuBwall_SpawnDebris(BgMizuBwall* this, PlayState* play) { void BgMizuBwall_Idle(BgMizuBwall* this, PlayState* play) { BgMizuBwall_SetAlpha(this, play); - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, ((u16)this->dyna.actor.params >> 8) & 0x3F)) { + // #endregion this->collider.base.acFlags &= ~AC_HIT; Flags_SetSwitch(play, ((u16)this->dyna.actor.params >> 8) & 0x3F); this->breakTimer = 1; diff --git a/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c b/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c index e40302b6623..5269510fe6a 100644 --- a/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c +++ b/soh/src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.c @@ -183,7 +183,9 @@ void BgSpot08Bakudankabe_Destroy(Actor* thisx, PlayState* play) { void BgSpot08Bakudankabe_Update(Actor* thisx, PlayState* play) { BgSpot08Bakudankabe* this = (BgSpot08Bakudankabe*)thisx; - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) { + // #endregion func_808B0324(this, play); Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F)); SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); diff --git a/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c b/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c index 3b7f7c7e4f3..81fe0a06b80 100644 --- a/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c +++ b/soh/src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.c @@ -135,7 +135,9 @@ void BgSpot11Bakudankabe_Destroy(Actor* thisx, PlayState* play) { void BgSpot11Bakudankabe_Update(Actor* thisx, PlayState* play) { BgSpot11Bakudankabe* this = (BgSpot11Bakudankabe*)thisx; - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) { + // #endregion func_808B2218(this, play); Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F)); SoundSource_PlaySfxAtFixedWorldPos(play, &D_808B2738, 40, NA_SE_EV_WALL_BROKEN); diff --git a/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c b/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c index 6537a169059..fe934bd2df3 100644 --- a/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c +++ b/soh/src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.c @@ -114,7 +114,9 @@ void BgSpot17Bakudankabe_Destroy(Actor* thisx, PlayState* play) { void BgSpot17Bakudankabe_Update(Actor* thisx, PlayState* play) { BgSpot17Bakudankabe* this = (BgSpot17Bakudankabe*)thisx; - if (this->dyna.actor.xzDistToPlayer < 650.0f && func_80033684(play, &this->dyna.actor) != NULL) { + // #region SOH [Co-op] + if ((this->dyna.actor.xzDistToPlayer < 650.0f && func_80033684(play, &this->dyna.actor) != NULL) || Flags_GetSwitch(play, (this->dyna.actor.params & 0x3F))) { + // #endregion func_808B6BC0(this, play); Flags_SetSwitch(play, (this->dyna.actor.params & 0x3F)); SoundSource_PlaySfxAtFixedWorldPos(play, &this->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); diff --git a/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c b/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c index 23d7fac96c3..f2556541bb2 100644 --- a/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c +++ b/soh/src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.c @@ -146,7 +146,9 @@ void func_808BEFF4(BgYdanMaruta* this, PlayState* play) { } void func_808BF078(BgYdanMaruta* this, PlayState* play) { - if (this->collider.base.acFlags & AC_HIT) { + // #region SOH [Co-op] + if ((this->collider.base.acFlags & AC_HIT) || Flags_GetSwitch(play, this->switchFlag)) { + // #endregion this->unk_16A = 20; Flags_SetSwitch(play, this->switchFlag); Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME); diff --git a/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c b/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c index 59402d658f2..f448d55a015 100644 --- a/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c +++ b/soh/src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.c @@ -282,6 +282,12 @@ void BgYdanSp_FloorWebIdle(BgYdanSp* this, PlayState* play) { webPos.x = this->dyna.actor.world.pos.x; webPos.y = this->dyna.actor.world.pos.y - 50.0f; webPos.z = this->dyna.actor.world.pos.z; + // #region SOH [Co-op] + if (Flags_GetSwitch(play, this->isDestroyedSwitchFlag)) { + BgYdanSp_BurnWeb(this, play); + return; + } + // #endregion if (Player_IsBurningStickInRange(play, &webPos, 70.0f, 50.0f) != 0) { this->dyna.actor.home.pos.x = player->meleeWeaponInfo[0].tip.x; this->dyna.actor.home.pos.z = player->meleeWeaponInfo[0].tip.z; @@ -411,6 +417,11 @@ void BgYdanSp_WallWebIdle(BgYdanSp* this, PlayState* play) { BgYdanSp_BurnWeb(this, play); } } + // #region SOH [Co-op] + if (Flags_GetSwitch(play, this->isDestroyedSwitchFlag)) { + BgYdanSp_BurnWeb(this, play); + } + // #endregion CollisionCheck_SetAC(play, &play->colChkCtx, &this->trisCollider.base); } diff --git a/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c b/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c index 99dd5e43f1f..3db8803a652 100644 --- a/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c +++ b/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c @@ -6,6 +6,7 @@ #include "z_door_gerudo.h" #include "objects/object_door_gerudo/object_door_gerudo.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #define FLAGS 0 @@ -103,6 +104,7 @@ void func_8099485C(DoorGerudo* this, PlayState* play) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] -= 1; Flags_SetSwitch(play, this->dyna.actor.params & 0x3F); Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK); + GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex); } else { s32 direction = func_80994750(this, play); diff --git a/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c b/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c index c842571e73f..14cff8380f7 100644 --- a/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c +++ b/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c @@ -379,6 +379,7 @@ void func_80996B0C(DoorShutter* this, PlayState* play) { if (this->doorType != SHUTTER_BOSS) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--; Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK); + GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex); } else { Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK_B); } @@ -645,6 +646,11 @@ void DoorShutter_Update(Actor* thisx, PlayState* play) { if (!(player->stateFlags1 & (PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_GETTING_ITEM | PLAYER_STATE1_IN_ITEM_CS)) || (this->actionFunc == DoorShutter_SetupType)) { this->actionFunc(this, play); } + // #region SOH [Co-op] + if (Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { + DECR(this->unk_16E); + } + // #endregion } Gfx* func_80997838(PlayState* play, DoorShutter* this, Gfx* p) { diff --git a/soh/src/overlays/actors/ovl_En_Door/z_en_door.c b/soh/src/overlays/actors/ovl_En_Door/z_en_door.c index 92670921de6..c8edec4b2f8 100644 --- a/soh/src/overlays/actors/ovl_En_Door/z_en_door.c +++ b/soh/src/overlays/actors/ovl_En_Door/z_en_door.c @@ -11,6 +11,7 @@ #include "objects/object_mizu_objects/object_mizu_objects.h" #include "objects/object_haka_door/object_haka_door.h" #include "soh/ResourceManagerHelpers.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #define FLAGS ACTOR_FLAG_UPDATE_WHILE_CULLED @@ -204,6 +205,7 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--; Flags_SetSwitch(play, this->actor.params & 0x3F); Audio_PlayActorSound2(&this->actor, NA_SE_EV_CHAIN_KEY_UNLOCK); + GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex); } } else if (!Player_InCsMode(play)) { if (fabsf(playerPosRelToDoor.y) < 20.0f && fabsf(playerPosRelToDoor.x) < 20.0f && @@ -231,6 +233,11 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) { this->actionFunc = EnDoor_AjarOpen; } } + // #region SOH [Co-op] + if (Flags_GetSwitch(play, this->actor.params & 0x3F)) { + DECR(this->lockTimer); + } + // #endregion } void EnDoor_WaitForCheck(EnDoor* this, PlayState* play) { diff --git a/soh/src/overlays/actors/ovl_En_Si/z_en_si.c b/soh/src/overlays/actors/ovl_En_Si/z_en_si.c index 338dfaf12cc..8f64213cb36 100644 --- a/soh/src/overlays/actors/ovl_En_Si/z_en_si.c +++ b/soh/src/overlays/actors/ovl_En_Si/z_en_si.c @@ -149,6 +149,13 @@ void func_80AFB950(EnSi* this, PlayState* play) { void EnSi_Update(Actor* thisx, PlayState* play) { EnSi* this = (EnSi*)thisx; + // #region SOH [Co-op] + if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) { + Actor_Kill(&this->actor); + return; + } + // #endregion + Actor_MoveXZGravity(&this->actor); Actor_UpdateBgCheckInfo(play, &this->actor, 0.0f, 0.0f, 0.0f, 4); this->actionFunc(this, play); diff --git a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c index 93dd9c9d375..98da453f349 100644 --- a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c +++ b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.c @@ -916,6 +916,13 @@ void func_80B0E9BC(EnSw* this, PlayState* play) { void EnSw_Update(Actor* thisx, PlayState* play) { EnSw* this = (EnSw*)thisx; + // #region SOH [Co-op] + if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) { + Actor_Kill(&this->actor); + return; + } + // #endregion + SkelAnime_Update(&this->skelAnime); func_80B0C9F0(this, play); this->actionFunc(this, play); diff --git a/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c b/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c index 9d3c9c240b9..a47b63cf367 100644 --- a/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c +++ b/soh/src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.c @@ -55,6 +55,13 @@ void ItemBHeart_Destroy(Actor* thisx, PlayState* play) { void ItemBHeart_Update(Actor* thisx, PlayState* play) { ItemBHeart* this = (ItemBHeart*)thisx; + // #region SOH [Co-op] + if (Flags_GetCollectible(play, 0x1F)) { + Actor_Kill(&this->actor); + return; + } + // #endregion + func_80B85264(this, play); Actor_UpdateBgCheckInfo(play, &this->actor, 0.0f, 0.0f, 0.0f, 4); if (Actor_HasParent(&this->actor, play)) { diff --git a/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c b/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c index 602236816e0..9ec4f10bf06 100644 --- a/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c +++ b/soh/src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.c @@ -125,8 +125,10 @@ void ObjBombiwa_Update(Actor* thisx, PlayState* play) { ObjBombiwa* this = (ObjBombiwa*)thisx; s32 pad; + // #region SOH [Co-op] if ((func_80033684(play, &this->actor) != NULL) || - ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040))) { + ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) { + // #endregion ObjBombiwa_Break(this, play); Flags_SetSwitch(play, this->actor.params & 0x3F); SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 80, NA_SE_EV_WALL_BROKEN); diff --git a/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c b/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c index e17cf0d8019..2611f25ce2d 100644 --- a/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c +++ b/soh/src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.c @@ -171,10 +171,12 @@ void ObjHamishi_Update(Actor* thisx, PlayState* play) { ObjHamishi_Shake(this); - if ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) { + // #region SOH [Co-op] + if (((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) || Flags_GetSwitch(play, this->actor.params & 0x3F)) { this->collider.base.acFlags &= ~AC_HIT; this->hitCount++; - if (this->hitCount < 2) { + if (this->hitCount < 2 && !Flags_GetSwitch(play, this->actor.params & 0x3F)) { + // #endregion this->shakeFrames = 15; this->shakePosSize = 2.0f; this->shakeRotSize = 400.0f;