diff --git a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp index c88c9a65f3f..6218ecbdf7b 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp @@ -639,4 +639,17 @@ namespace GameInteractionEffect { void SlipperyFloor::_Remove() { GameInteractor::State::SlipperyFloorActive = 0; } + + // MARK: - GiveItem + GameInteractionEffectQueryResult GiveItem::CanBeApplied() { + if (!GameInteractor::IsSaveLoaded()) { + return GameInteractionEffectQueryResult::NotPossible; + } + + return GameInteractionEffectQueryResult::Possible; + } + + void GiveItem::_Apply() { + GameInteractor::RawAction::GiveItem(parameters[0], parameters[1]); + } } diff --git a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h index ebc1b6fac22..b370a6754f2 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h @@ -262,6 +262,11 @@ namespace GameInteractionEffect { void _Apply() override; void _Remove() override; }; + + class GiveItem: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { + GameInteractionEffectQueryResult CanBeApplied() override; + void _Apply() override; + }; } #endif /* __cplusplus */ diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 37a0e1725d9..a9bca839dca 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -9,6 +9,7 @@ typedef enum { GI_SCHEME_BUILT_IN, GI_SCHEME_CROWD_CONTROL, + GI_SCHEME_ANCHOR, } GIScheme; typedef enum { @@ -220,6 +221,7 @@ class GameInteractor { static void SetFlag(int16_t flagType, int16_t chestNum); static void UnsetFlag(int16_t flagType, int16_t chestNum); static void AddOrRemoveHealthContainers(int16_t amount); + static void GiveItem(uint16_t modId, uint16_t itemId); static void AddOrRemoveMagic(int8_t amount); static void HealOrDamagePlayer(int16_t hearts); static void SetPlayerHealth(int16_t hearts); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp new file mode 100644 index 00000000000..3f9a2aa89fd --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp @@ -0,0 +1,661 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "GameInteractor_Anchor.h" +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include "macros.h" +#include "z64scene.h" +#include "z64actor.h" +#include "functions.h" +extern "C" void Overlay_DisplayText(float duration, const char* text); +extern "C" s16 gEnPartnerId; +extern PlayState* gPlayState; +extern SaveContext gSaveContext; +} + +using json = nlohmann::json; + +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); +} + +void to_json(json& j, const Color_RGB8& color) { + j = json{ + {"r", color.r}, + {"g", color.g}, + {"b", color.b} + }; +} + +void from_json(const json& j, AnchorClientWithoutPos& client) { + j.at("clientId").get_to(client.clientId); + j.at("clientVersion").get_to(client.clientVersion); + j.at("name").get_to(client.name); + j.at("color").get_to(client.color); +} + +void to_json(json& j, const SavedSceneFlags& flags) { + j = json{ + {"chest", flags.chest}, + {"swch", flags.swch}, + {"clear", flags.clear}, + {"collect", flags.collect}, + }; +} + +void from_json(const json& j, SavedSceneFlags& flags) { + j.at("chest").get_to(flags.chest); + j.at("swch").get_to(flags.swch); + j.at("clear").get_to(flags.clear); + j.at("collect").get_to(flags.collect); +} + +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} + }; +} + +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); +} + +void to_json(json& j, const SohStats& sohStats) { + j = json{ + {"locationsSkipped", sohStats.locationsSkipped}, + }; +} + +void from_json(const json& j, SohStats& sohStats) { + j.at("locationsSkipped").get_to(sohStats.locationsSkipped); +} + +void to_json(json& j, const SaveContext& saveContext) { + 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", saveContext.sceneFlags}, + {"eventChkInf", saveContext.eventChkInf}, + {"itemGetInf", saveContext.itemGetInf}, + {"infTable", saveContext.infTable}, + {"randomizerInf", saveContext.randomizerInf}, + {"gsFlags", saveContext.gsFlags}, + {"inventory", saveContext.inventory}, + {"sohStats", saveContext.sohStats}, + {"adultTradeItems", saveContext.adultTradeItems}, + }; +} + +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); + j.at("sceneFlags").get_to(saveContext.sceneFlags); + 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); +} + +void to_json(json& j, const Vec3f& vec) { + j = json{ + {"x", vec.x}, + {"y", vec.y}, + {"z", vec.z} + }; +} + +void to_json(json& j, const Vec3s& vec) { + j = json{ + {"x", vec.x}, + {"y", vec.y}, + {"z", vec.z} + }; +} + +void to_json(json& j, const PosRot& posRot) { + j = json{ + {"pos", posRot.pos}, + {"rot", posRot.rot} + }; +} + +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); +} + +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); +} + +void from_json(const json& j, PosRot& posRot) { + j.at("pos").get_to(posRot.pos); + j.at("rot").get_to(posRot.rot); +} + +std::map GameInteractorAnchor::AnchorClients = {}; +std::vector GameInteractorAnchor::FairyIndexToClientId = {}; +std::string GameInteractorAnchor::clientVersion = "Anchor Build 3"; + +void Anchor_DisplayMessage(std::string message) { + Overlay_DisplayText(CVarGetInteger("gInterpolationFPS", 20) * 0.5f, message.c_str()); +} + +void GameInteractorAnchor::Enable() { + if (isEnabled) { + return; + } + + isEnabled = true; + GameInteractor::Instance->EnableRemoteInteractor(); + GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { + HandleRemoteJson(payload); + }); + GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() { + Anchor_DisplayMessage("Connected to Anchor"); + nlohmann::json payload; + payload["data"]["name"] = CVarGetString("gRemote.AnchorName", ""); + payload["data"]["color"] = CVarGetColor24("gRemote.AnchorColor", { 100, 255, 100 }); + payload["data"]["clientVersion"] = GameInteractorAnchor::clientVersion; + payload["type"] = "UPDATE_CLIENT_DATA"; + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterRemoteDisconnectedHandler([&]() { + Anchor_DisplayMessage("Disconnected from Anchor"); + }); +} + +void GameInteractorAnchor::Disable() { + if (!isEnabled) { + return; + } + + isEnabled = false; + GameInteractor::Instance->DisableRemoteInteractor(); + + GameInteractorAnchor::AnchorClients.clear(); + if (GameInteractor::IsSaveLoaded()) { + Anchor_SpawnClientFairies(); + } +} + +void GameInteractorAnchor::TransmitJsonToRemote(nlohmann::json payload) { + payload["roomId"] = CVarGetString("gRemote.AnchorRoomId", ""); + if (!payload.contains("quiet")) { + SPDLOG_INFO("Sending payload:\n{}", payload.dump()); + } + GameInteractor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_ParseSaveStateFromRemote(nlohmann::json payload); +void Anchor_PushSaveStateToRemote(); + +void GameInteractorAnchor::HandleRemoteJson(nlohmann::json payload) { + if (!payload.contains("type")) { + return; + } + + if (!payload.contains("quiet")) { + SPDLOG_INFO("Received payload:\n{}", payload.dump()); + } + + if (payload["type"] == "GIVE_ITEM") { + auto effect = new GameInteractionEffect::GiveItem(); + effect->parameters[0] = payload["modId"].get(); + effect->parameters[1] = payload["getItemId"].get(); + if (effect->Apply() == Possible) { + GetItemEntry getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(effect->parameters[0], effect->parameters[1]); + SPDLOG_INFO("Received from {}", payload["clientId"].get()); + AnchorClient anchorClient = GameInteractorAnchor::AnchorClients[payload["clientId"].get()]; + if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) { + if (getItemEntry.modIndex == MOD_NONE) { + Anchor_DisplayMessage("Received " + SohUtils::GetItemName(getItemEntry.itemId) + " from " + anchorClient.name); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + Anchor_DisplayMessage("Received " + SohUtils::GetRandomizerItemName(getItemEntry.getItemId) + " from " + anchorClient.name); + } + } + } + } + if (payload["type"] == "SET_SCENE_FLAG") { + 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(); + } + if (payload["type"] == "SET_FLAG") { + auto effect = new GameInteractionEffect::SetFlag(); + effect->parameters[0] = payload["flagType"].get(); + effect->parameters[1] = payload["flag"].get(); + effect->Apply(); + + // If mweep flag replace ruto's letter + if ( + payload["flagType"].get() == FLAG_EVENT_CHECK_INF && + payload["flag"].get() == EVENTCHKINF_KING_ZORA_MOVED && + Inventory_HasSpecificBottle(ITEM_LETTER_RUTO) + ) { + Inventory_ReplaceItem(gPlayState, ITEM_LETTER_RUTO, ITEM_BOTTLE); + } + } + if (payload["type"] == "UNSET_SCENE_FLAG") { + 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(); + } + if (payload["type"] == "UNSET_FLAG") { + auto effect = new GameInteractionEffect::UnsetFlag(); + effect->parameters[0] = payload["flagType"].get(); + effect->parameters[1] = payload["flag"].get(); + effect->Apply(); + } + if (payload["type"] == "CLIENT_UPDATE") { + uint32_t clientId = payload["clientId"].get(); + + if (GameInteractorAnchor::AnchorClients.contains(clientId)) { + GameInteractorAnchor::AnchorClients[clientId].scene = payload["sceneNum"].get(); + GameInteractorAnchor::AnchorClients[clientId].posRot = payload["posRot"].get(); + } + } + if (payload["type"] == "PUSH_SAVE_STATE" && GameInteractor::IsSaveLoaded()) { + Anchor_ParseSaveStateFromRemote(payload); + } + if (payload["type"] == "REQUEST_SAVE_STATE" && GameInteractor::IsSaveLoaded()) { + Anchor_PushSaveStateToRemote(); + } + if (payload["type"] == "ALL_CLIENT_DATA") { + std::vector newClients = payload["clients"].get>(); + + // add new clients + for (auto& client : newClients) { + if (!GameInteractorAnchor::AnchorClients.contains(client.clientId)) { + GameInteractorAnchor::AnchorClients[client.clientId] = { + client.clientId, + client.clientVersion, + client.name, + client.color, + SCENE_ID_MAX, + { -9999, -9999, -9999, 0, 0, 0 } + }; + Anchor_DisplayMessage(client.name + " connected"); + } + } + + // remove clients that are no longer in the list + std::vector clientsToRemove; + for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { + if (std::find_if(newClients.begin(), newClients.end(), [clientId = clientId](AnchorClientWithoutPos& c) { return c.clientId == clientId; }) == newClients.end()) { + clientsToRemove.push_back(clientId); + } + } + for (auto& clientId : clientsToRemove) { + Anchor_DisplayMessage(GameInteractorAnchor::AnchorClients[clientId].name + " disconnected"); + GameInteractorAnchor::AnchorClients.erase(clientId); + } + + if (GameInteractor::IsSaveLoaded()) { + Anchor_SpawnClientFairies(); + } + } + if (payload["type"] == "SKIP_LOCATION" && GameInteractor::IsSaveLoaded()) { + gSaveContext.sohStats.locationsSkipped[payload["locationIndex"].get()] = payload["skipped"].get(); + } + if (payload["type"] == "UPDATE_BEANS_BOUGHT" && GameInteractor::IsSaveLoaded()) { + BEANS_BOUGHT = payload["amount"].get(); + } + if (payload["type"] == "CONSUME_ADULT_TRADE_ITEM" && GameInteractor::IsSaveLoaded()) { + uint8_t itemId = payload["itemId"].get(); + gSaveContext.adultTradeItems &= ~ADULT_TRADE_FLAG(itemId); + Inventory_ReplaceItem(gPlayState, itemId, Randomizer_GetNextAdultTradeItem()); + } + if (payload["type"] == "UPDATE_KEY_COUNT" && GameInteractor::IsSaveLoaded()) { + gSaveContext.inventory.dungeonKeys[payload["sceneNum"].get()] = payload["amount"].get(); + } + if (payload["type"] == "SERVER_MESSAGE") { + Anchor_DisplayMessage("Server: " + payload["message"].get()); + } + if (payload["type"] == "DISABLE_ANCHOR") { + GameInteractor::Instance->isRemoteInteractorEnabled = false; + GameInteractorAnchor::Instance->isEnabled = false; + } +} + +void Anchor_PushSaveStateToRemote() { + json payload = gSaveContext; + payload["type"] = "PUSH_SAVE_STATE"; + // manually update current scene flags + payload["sceneFlags"][gPlayState->sceneNum]["chest"] = gPlayState->actorCtx.flags.chest; + payload["sceneFlags"][gPlayState->sceneNum]["swch"] = gPlayState->actorCtx.flags.swch; + payload["sceneFlags"][gPlayState->sceneNum]["clear"] = gPlayState->actorCtx.flags.clear; + payload["sceneFlags"][gPlayState->sceneNum]["collect"] = gPlayState->actorCtx.flags.collect; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_RequestSaveStateFromRemote() { + nlohmann::json payload; + payload["type"] = "REQUEST_SAVE_STATE"; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_ParseSaveStateFromRemote(nlohmann::json payload) { + SaveContext loadedData = payload.get(); + + 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; + // TODO: Packet to live update this + gSaveContext.adultTradeItems = loadedData.adultTradeItems; + + for (int i = 0; i < 124; i++) { + gSaveContext.sceneFlags[i] = loadedData.sceneFlags[i]; + if (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 < 9; i++) { + gSaveContext.randomizerInf[i] = loadedData.randomizerInf[i]; + } + + for (int i = 0; i < 6; i++) { + gSaveContext.gsFlags[i] = loadedData.gsFlags[i]; + } + + for (int i = 0; i < 746; i++) { + if (!gSaveContext.sohStats.locationsSkipped[i]) { + gSaveContext.sohStats.locationsSkipped[i] = loadedData.sohStats.locationsSkipped[i]; + } + } + + // Restore master sword state + u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_SWORD, 1); + if (hasMasterSword) { + loadedData.inventory.equipment |= 0x2; + } else { + loadedData.inventory.equipment &= ~0x2; + } + + // TODO: Packet to live update ruto's letter replacement + // 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 + for (int i = 0; i < ARRAY_COUNT(gSaveContext.inventory.ammo); i++) { + if (gSaveContext.inventory.ammo[i] != 0) { + loadedData.inventory.ammo[i] = gSaveContext.inventory.ammo[i]; + } + } + + gSaveContext.inventory = loadedData.inventory; + Anchor_DisplayMessage("State loaded from remote!"); +}; + +uint8_t Anchor_GetClientScene(uint32_t fairyIndex) { + uint32_t clientId = GameInteractorAnchor::FairyIndexToClientId[fairyIndex]; + if (GameInteractorAnchor::AnchorClients.find(clientId) == GameInteractorAnchor::AnchorClients.end()) { + return SCENE_ID_MAX; + } + + return GameInteractorAnchor::AnchorClients[clientId].scene; +} + +PosRot Anchor_GetClientPosition(uint32_t fairyIndex) { + uint32_t clientId = GameInteractorAnchor::FairyIndexToClientId[fairyIndex]; + if (GameInteractorAnchor::AnchorClients.find(clientId) == GameInteractorAnchor::AnchorClients.end()) { + return {-9999.0, -9999.0, -9999.0, 0, 0, 0}; + } + + return GameInteractorAnchor::AnchorClients[clientId].posRot; +} + +Color_RGB8 Anchor_GetClientColor(uint32_t fairyIndex) { + uint32_t clientId = GameInteractorAnchor::FairyIndexToClientId[fairyIndex]; + if (GameInteractorAnchor::AnchorClients.find(clientId) == GameInteractorAnchor::AnchorClients.end()) { + return {100, 255, 100}; + } + + return GameInteractorAnchor::AnchorClients[clientId].color; +} + +void Anchor_SpawnClientFairies() { + if (gPlayState == NULL) return; + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_ITEMACTION].head; + while (actor != NULL) { + if (gEnPartnerId == actor->id) { + Actor_Kill(actor); + } + actor = actor->next; + } + + GameInteractorAnchor::FairyIndexToClientId.clear(); + + uint32_t i = 0; + for (auto [clientId, client] : GameInteractorAnchor::AnchorClients) { + GameInteractorAnchor::FairyIndexToClientId.push_back(clientId); + Actor_Spawn(&gPlayState->actorCtx, gPlayState, gEnPartnerId, -9999.0, -9999.0, -9999.0, 0, 0, 0, 3 + i, false); + i++; + } +} + +void Anchor_RegisterHooks() { + GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || gSaveContext.fileNum > 2) return; + + Anchor_RequestSaveStateFromRemote(); + }); + GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { + if (itemEntry.modIndex == MOD_NONE && (itemEntry.itemId == ITEM_KEY_SMALL || itemEntry.itemId == ITEM_KEY_BOSS || itemEntry.itemId == ITEM_SWORD_MASTER)) { + return; + } + + if (CVarGetInteger("gFromGI", 0)) { + CVarClear("gFromGI"); + return; + } + + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "GIVE_ITEM"; + payload["modId"] = itemEntry.tableId; + payload["getItemId"] = itemEntry.getItemId; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + nlohmann::json payload; + + payload["type"] = "SET_SCENE_FLAG"; + payload["sceneNum"] = sceneNum; + payload["flagType"] = flagType; + payload["flag"] = flag; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { + if (flagType == FLAG_INF_TABLE && flag == INFTABLE_SWORDLESS) { + return; + } + + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + nlohmann::json payload; + + payload["type"] = "SET_FLAG"; + payload["flagType"] = flagType; + payload["flag"] = flag; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + nlohmann::json payload; + + payload["type"] = "UNSET_SCENE_FLAG"; + payload["sceneNum"] = sceneNum; + payload["flagType"] = flagType; + payload["flag"] = flag; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { + if (flagType == FLAG_INF_TABLE && flag == INFTABLE_SWORDLESS) { + return; + } + + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + nlohmann::json payload; + + payload["type"] = "UNSET_FLAG"; + payload["flagType"] = flagType; + payload["flag"] = flag; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([]() { + static uint32_t lastPlayerCount = 0; + uint32_t currentPlayerCount = GameInteractorAnchor::AnchorClients.size(); + if (!GameInteractor::Instance->isRemoteInteractorConnected || gPlayState == NULL || !GameInteractor::Instance->IsSaveLoaded()) { + lastPlayerCount = currentPlayerCount; + return; + } + Player* player = GET_PLAYER(gPlayState); + nlohmann::json payload; + float currentPosition = player->actor.world.pos.x + player->actor.world.pos.y + player->actor.world.pos.z + player->actor.world.rot.y; + static float lastPosition = 0.0f; + + if (currentPosition == lastPosition && currentPlayerCount == lastPlayerCount) return; + + payload["type"] = "CLIENT_UPDATE"; + payload["sceneNum"] = gPlayState->sceneNum; + payload["posRot"] = player->actor.world; + payload["quiet"] = true; + + lastPosition = currentPosition; + lastPlayerCount = currentPlayerCount; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); +} + +void Anchor_SkipLocation(uint32_t locationIndex, bool skipped) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "SKIP_LOCATION"; + payload["locationIndex"] = locationIndex; + payload["skipped"] = skipped; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_UpdateBeansBought(uint8_t amount) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "UPDATE_BEANS_BOUGHT"; + payload["amount"] = amount; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_ConsumeAdultTradeItem(uint8_t itemId) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "CONSUME_ADULT_TRADE_ITEM"; + payload["itemId"] = itemId; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_UpdateKeyCount(int16_t sceneNum, int8_t amount) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "UPDATE_KEY_COUNT"; + payload["sceneNum"] = sceneNum; + payload["amount"] = amount; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +#endif diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h new file mode 100644 index 00000000000..89a65ee0200 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h @@ -0,0 +1,62 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#ifdef __cplusplus +#include + +#include "z64actor.h" +#include "./GameInteractor.h" + +typedef struct { + uint32_t clientId; + std::string clientVersion; + std::string name; + Color_RGB8 color; + uint8_t scene; + PosRot posRot; +} AnchorClient; + +typedef struct { + uint32_t clientId; + std::string clientVersion; + std::string name; + Color_RGB8 color; +} AnchorClientWithoutPos; + +class GameInteractorAnchor { + private: + bool isEnabled; + + void HandleRemoteJson(nlohmann::json payload); + public: + static GameInteractorAnchor* Instance; + static std::map AnchorClients; + static std::vector FairyIndexToClientId; + static std::string clientVersion; + + void Enable(); + void Disable(); + + void TransmitJsonToRemote(nlohmann::json payload); +}; +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +void Anchor_RegisterHooks(); +void Anchor_PushSaveStateToRemote(); +void Anchor_RequestSaveStateFromRemote(); +uint8_t Anchor_GetClientScene(uint32_t fairyIndex); +PosRot Anchor_GetClientPosition(uint32_t fairyIndex); +Color_RGB8 Anchor_GetClientColor(uint32_t fairyIndex); +void Anchor_SpawnClientFairies(); +void Anchor_SkipLocation(uint32_t locationIndex, bool skipped); +void Anchor_UpdateBeansBought(uint8_t amount); +void Anchor_ConsumeAdultTradeItem(uint8_t itemId); +void Anchor_UpdateKeyCount(int16_t sceneNum, int8_t amount); + +#ifdef __cplusplus +} +#endif diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_RawAction.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_RawAction.cpp index 91940c13aa8..998d752c0f0 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_RawAction.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_RawAction.cpp @@ -2,6 +2,7 @@ #include #include "soh/Enhancements/cosmetics/CosmeticsEditor.h" #include "soh/Enhancements/randomizer/3drando/random.hpp" +#include #include #include "soh/Enhancements/debugger/colViewer.h" @@ -120,6 +121,24 @@ void GameInteractor::RawAction::ElectrocutePlayer() { func_80837C0C(gPlayState, player, 4, 0, 0, 0, 0); } +void GameInteractor::RawAction::GiveItem(uint16_t modId, uint16_t itemId) { + CVarSetInteger("gFromGI", 1); + GetItemEntry getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(modId, itemId); + Player* player = GET_PLAYER(gPlayState); + 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++; + } else { + Randomizer_Item_Give(gPlayState, getItemEntry); + } + } +}; + void GameInteractor::RawAction::KnockbackPlayer(float strength) { Player* player = GET_PLAYER(gPlayState); func_8002F71C(gPlayState, &player->actor, strength * 5, player->actor.world.rot.y + 0x8000, strength * 5); diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index 93482ffa5d5..4fa272319a3 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -13,6 +13,9 @@ extern "C" { #include #include #include "soh/Enhancements/enhancementTypes.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif extern "C" { #include @@ -443,7 +446,11 @@ void DrawGameplayStatsHeader() { ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, { 4.0f, 4.0f }); ImGui::BeginTable("gameplayStatsHeader", 1, ImGuiTableFlags_BordersOuter); ImGui::TableSetupColumn("stat", ImGuiTableColumnFlags_WidthStretch); +#ifdef ENABLE_REMOTE_CONTROL + GameplayStatsRow("Build Version:", GameInteractorAnchor::clientVersion); +#else GameplayStatsRow("Build Version:", (char*) gBuildVersion); +#endif if (gSaveContext.sohStats.rtaTiming) { GameplayStatsRow("Total Time (RTA):", formatTimestampGameplayStat(GAMEPLAYSTAT_TOTAL_TIME), gSaveContext.sohStats.gameComplete ? COLOR_GREEN : COLOR_WHITE); } else { diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index 2048ead3168..3e01d7d522f 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -6,6 +6,9 @@ #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/randomizer/3drando/random.hpp" #include "soh/Enhancements/cosmetics/authenticGfxPatches.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif extern "C" { #include @@ -621,4 +624,7 @@ void InitMods() { RegisterBonkDamage(); RegisterMenuPathFix(); RegisterMirrorModeHandler(); + #ifdef ENABLE_REMOTE_CONTROL + Anchor_RegisterHooks(); + #endif } diff --git a/soh/soh/Enhancements/randomizer/adult_trade_shuffle.c b/soh/soh/Enhancements/randomizer/adult_trade_shuffle.c index c1acc100b82..f5eb621ca9a 100644 --- a/soh/soh/Enhancements/randomizer/adult_trade_shuffle.c +++ b/soh/soh/Enhancements/randomizer/adult_trade_shuffle.c @@ -2,10 +2,16 @@ #include "functions.h" #include "variables.h" #include "macros.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif void Randomizer_ConsumeAdultTradeItem(PlayState* play, u8 itemId) { gSaveContext.adultTradeItems &= ~ADULT_TRADE_FLAG(itemId); Inventory_ReplaceItem(play, itemId, Randomizer_GetNextAdultTradeItem()); +#ifdef ENABLE_REMOTE_CONTROL + Anchor_ConsumeAdultTradeItem(itemId); +#endif } u8 Randomizer_GetNextAdultTradeItem() { diff --git a/soh/soh/Enhancements/randomizer/adult_trade_shuffle.h b/soh/soh/Enhancements/randomizer/adult_trade_shuffle.h index 13d905026c9..ec21b61c112 100644 --- a/soh/soh/Enhancements/randomizer/adult_trade_shuffle.h +++ b/soh/soh/Enhancements/randomizer/adult_trade_shuffle.h @@ -6,8 +6,16 @@ #define ADULT_TRADE_FLAG(itemId) (1 << (itemId - ITEM_POCKET_EGG)) #define PLAYER_HAS_SHUFFLED_ADULT_TRADE_ITEM(itemID) (gSaveContext.adultTradeItems & ADULT_TRADE_FLAG(itemID)) +#ifdef __cplusplus +extern "C" { +#endif + void Randomizer_ConsumeAdultTradeItem(PlayState* play, u8 itemId); u8 Randomizer_GetNextAdultTradeItem(); u8 Randomizer_GetPrevAdultTradeItem(); +#ifdef __cplusplus +} +#endif + #endif diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index 6998c6bb5c5..13daae99bc3 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -9,6 +9,9 @@ #include "3drando/item_location.hpp" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "randomizerTypes.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif extern "C" { @@ -819,9 +822,15 @@ void DrawLocation(RandomizerCheckObject rcObj, RandomizerCheckShow* thisCheckSta if (skipped) { gSaveContext.sohStats.locationsSkipped[rcObj.rc] = 0; *thisCheckStatus = RCSHOW_UNCHECKED; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_SkipLocation(rcObj.rc, false); +#endif } else { gSaveContext.sohStats.locationsSkipped[rcObj.rc] = 1; *thisCheckStatus = RCSHOW_SKIPPED; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_SkipLocation(rcObj.rc, true); +#endif } } } else { diff --git a/soh/soh/Enhancements/randomizer/savefile.cpp b/soh/soh/Enhancements/randomizer/savefile.cpp index 4b546e1f013..dfa8632f663 100644 --- a/soh/soh/Enhancements/randomizer/savefile.cpp +++ b/soh/soh/Enhancements/randomizer/savefile.cpp @@ -22,7 +22,6 @@ void StartingItemGive(GetItemEntry getItemEntry) { } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { if (getItemEntry.getItemId == RG_ICE_TRAP) { gSaveContext.pendingIceTrapCount++; - GameInteractor::Instance->ExecuteHooks(getItemEntry); } else { Randomizer_Item_Give(NULL, getItemEntry); } diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 94fb11da71a..e9b9296109c 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -78,8 +78,10 @@ #ifdef ENABLE_REMOTE_CONTROL #include "Enhancements/crowd-control/CrowdControl.h" #include "Enhancements/game-interactor/GameInteractor_BuiltIn.h" +#include "Enhancements/game-interactor/GameInteractor_Anchor.h" CrowdControl* CrowdControl::Instance; GameInteractorBuiltIn* GameInteractorBuiltIn::Instance; +GameInteractorAnchor* GameInteractorAnchor::Instance; #endif #include "Enhancements/mods.h" @@ -801,6 +803,7 @@ extern "C" void InitOTR() { #ifdef ENABLE_REMOTE_CONTROL CrowdControl::Instance = new CrowdControl(); GameInteractorBuiltIn::Instance = new GameInteractorBuiltIn(); + GameInteractorAnchor::Instance = new GameInteractorAnchor(); #endif clearMtx = (uintptr_t)&gMtxClear; @@ -832,6 +835,9 @@ extern "C" void InitOTR() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Enable(); break; + case GI_SCHEME_ANCHOR: + GameInteractorAnchor::Instance->Enable(); + break; } } #endif @@ -857,6 +863,9 @@ extern "C" void DeinitOTR() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Disable(); break; + case GI_SCHEME_ANCHOR: + GameInteractorAnchor::Instance->Disable(); + break; } } SDLNet_Quit(); diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index d91ad60956a..a8d6871fb54 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -14,6 +14,7 @@ #ifdef ENABLE_REMOTE_CONTROL #include "Enhancements/crowd-control/CrowdControl.h" #include "Enhancements/game-interactor/GameInteractor_BuiltIn.h" +#include "Enhancements/game-interactor/GameInteractor_Anchor.h" #endif @@ -1346,9 +1347,17 @@ void DrawRemoteControlMenu() { if (ImGui::BeginMenu("Network")) { static std::string ip = CVarGetString("gRemote.IP", "127.0.0.1"); static uint16_t port = CVarGetInteger("gRemote.Port", 43384); - bool isFormValid = !isStringEmpty(CVarGetString("gRemote.IP", "127.0.0.1")) && port > 1024 && port < 65535; - - const char* remoteOptions[2] = { "Built-in", "Crowd Control"}; + static std::string AnchorName = CVarGetString("gRemote.AnchorName", ""); + static std::string anchorRoomId = CVarGetString("gRemote.AnchorRoomId", ""); + bool isFormValid = !isStringEmpty(CVarGetString("gRemote.IP", "127.0.0.1")) && port > 1024 && port < 65535 && ( + CVarGetInteger("gRemote.Scheme", GI_SCHEME_BUILT_IN) != GI_SCHEME_ANCHOR || + ( + !isStringEmpty(CVarGetString("gRemote.AnchorName", "")) && + !isStringEmpty(CVarGetString("gRemote.AnchorRoomId", "")) + ) + ); + + const char* remoteOptions[3] = { "Built-in", "Crowd Control", "Anchor" }; ImGui::BeginDisabled(GameInteractor::Instance->isRemoteInteractorEnabled); ImGui::Text("Remote Interaction Scheme"); @@ -1361,10 +1370,16 @@ void DrawRemoteControlMenu() { ip = "127.0.0.1"; port = 43384; break; + case GI_SCHEME_ANCHOR: + CVarSetString("gRemote.IP", "anchor.proxysaw.dev"); + CVarSetInteger("gRemote.Port", 43384); + ip = "anchor.proxysaw.dev"; + port = 43384; + break; } LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); } - + if (CVarGetInteger("gRemote.Scheme", GI_SCHEME_BUILT_IN) != GI_SCHEME_ANCHOR) { ImGui::Text("Remote IP & Port"); if (ImGui::InputText("##gRemote.IP", (char*)ip.c_str(), ip.capacity() + 1)) { CVarSetString("gRemote.IP", ip.c_str()); @@ -1377,8 +1392,30 @@ void DrawRemoteControlMenu() { CVarSetInteger("gRemote.Port", port); LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); } - ImGui::PopItemWidth(); + } else { + ImGui::Text("Fairy Color & Name"); + static Color_RGB8 color = CVarGetColor24("gRemote.AnchorColor", { 100, 255, 100 }); + static ImVec4 colorVec = ImVec4(color.r / 255.0, color.g / 255.0, color.b / 255.0, 1); + if (ImGui::ColorEdit3("##gRemote.AnchorColor", (float*)&colorVec, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + color.r = colorVec.x * 255.0; + color.g = colorVec.y * 255.0; + color.b = colorVec.z * 255.0; + + CVarSetColor24("gRemote.AnchorColor", color); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (ImGui::InputText("##gRemote.AnchorName", (char*)AnchorName.c_str(), AnchorName.capacity() + 1)) { + CVarSetString("gRemote.AnchorName", AnchorName.c_str()); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + ImGui::Text("Room ID"); + if (ImGui::InputText("##gRemote.AnchorRoomId", (char*)anchorRoomId.c_str(), anchorRoomId.capacity() + 1)) { + CVarSetString("gRemote.AnchorRoomId", anchorRoomId.c_str()); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + } ImGui::EndDisabled(); ImGui::Spacing(); @@ -1396,6 +1433,9 @@ void DrawRemoteControlMenu() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Disable(); break; + case GI_SCHEME_ANCHOR: + GameInteractorAnchor::Instance->Disable(); + break; } } else { CVarSetInteger("gRemote.Enabled", 1); @@ -1407,6 +1447,9 @@ void DrawRemoteControlMenu() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Enable(); break; + case GI_SCHEME_ANCHOR: + GameInteractorAnchor::Instance->Enable(); + break; } } } @@ -1421,6 +1464,28 @@ void DrawRemoteControlMenu() { } } + if (GameInteractor::Instance->isRemoteInteractorConnected && CVarGetInteger("gRemote.Scheme", GI_SCHEME_BUILT_IN) == GI_SCHEME_ANCHOR) { + ImGui::Text("Players in Room:"); + ImGui::Text("%s", CVarGetString("gRemote.AnchorName", "")); + for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { + ImGui::Text("%s", client.name.c_str()); + if (client.clientVersion != GameInteractorAnchor::clientVersion) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 1, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Client version mismatch (%s)", client.clientVersion.c_str()); + ImGui::EndTooltip(); + } + } + } + + ImGui::Spacing(); + if (ImGui::Button("Request State", ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) { + Anchor_RequestSaveStateFromRemote(); + } + } + ImGui::Dummy(ImVec2(0.0f, 0.0f)); ImGui::EndMenu(); } diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c index cfe5a96d3aa..6d3782dd8f6 100644 --- a/soh/src/code/z_en_item00.c +++ b/soh/src/code/z_en_item00.c @@ -741,6 +741,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 + // OTRTODO: remove special case for bombchu when its 2D drop is implemented if (CVarGetInteger("gNewDrops", 0) || this->actor.params == ITEM00_BOMBCHU) { //set the rotation system on selected model only :) if ((this->actor.params == ITEM00_RUPEE_GREEN) || (this->actor.params == ITEM00_RUPEE_BLUE) || diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 632056246b0..4f54da630e3 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -1741,6 +1741,11 @@ u8 Return_Item(u8 itemID, ModIndex modId, ItemID returnItem) { GetItemEntry gie = { ITEM_SOLD_OUT, 0, 0, 0, 0, 0, 0, 0, 0, false, ITEM_FROM_NPC, ITEM_CATEGORY_LESSER, NULL }; return Return_Item_Entry(gie, returnItem); } + // Master sword doesn't have an ItemTable entry, so pass custom entry instead (This will go away with master sword shuffle) + if (itemID == ITEM_SWORD_MASTER) { + GetItemEntry gie = { ITEM_SWORD_MASTER, 0, 0, 0, 0, 0, 0, 0, 0, false, ITEM_FROM_NPC, ITEM_CATEGORY_MAJOR, NULL }; + return Return_Item_Entry(gie, returnItem); + } GetItemID getItemID = GetGetItemIDFromItemID(itemID); if (getItemID != GI_MAX) { @@ -2194,6 +2199,14 @@ u8 Item_Give(PlayState* play, u8 item) { } else if ((item == ITEM_HEART_PIECE_2) || (item == ITEM_HEART_PIECE)) { gSaveContext.inventory.questItems += 1 << (QUEST_HEART_PIECE + 4); gSaveContext.sohStats.heartPieces++; + if (CVarGetInteger("gFromGI", 0)) { + gSaveContext.healthAccumulator = 0x140; + if ((s32)(gSaveContext.inventory.questItems & 0xF0000000) == 0x40000000) { + gSaveContext.inventory.questItems ^= 0x40000000; + gSaveContext.healthCapacity += 0x10; + gSaveContext.health += 0x10; + } + } return Return_Item(item, MOD_NONE, ITEM_NONE); } else if (item == ITEM_HEART_CONTAINER) { gSaveContext.healthCapacity += 0x10; diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 352ae03cd5c..2885b51edcf 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -11,6 +11,9 @@ #include #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #include @@ -744,6 +747,13 @@ void Play_Init(GameState* thisx) { GET_PLAYER(play)->actor.world.pos.y + Player_GetHeight(GET_PLAYER(play)) + 5.0f, GET_PLAYER(play)->actor.world.pos.z, 0, 0, 0, 1, true); } +#ifdef ENABLE_REMOTE_CONTROL + // #region SOH [Co-op] + if (CVarGetInteger("gRemote.Scheme", 0) == GI_SCHEME_ANCHOR) { + Anchor_SpawnClientFairies(); + } + // #endregion +#endif } void Play_Update(PlayState* play) { 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 925a66ba253..b243a748e2f 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 @@ -275,7 +275,7 @@ void BgBreakwall_Wait(BgBreakwall* this, PlayState* play) { } // Break the floor immediately in Boss Rush so the player can jump in the hole immediately. - if (this->collider.base.acFlags & 2 || blueFireArrowHit || gSaveContext.isBossRush) { + if (this->collider.base.acFlags & 2 || blueFireArrowHit || gSaveContext.isBossRush || Flags_GetSwitch(play, this->dyna.actor.params & 0x3F)) { Vec3f effectPos; s32 wallType = ((this->dyna.actor.params >> 13) & 3) & 0xFF; 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 ad11f19fd97..de07d7a52b2 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 @@ -281,6 +281,13 @@ 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; @@ -410,6 +417,13 @@ 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 c5f05341aaf..b7ef2219c91 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,9 @@ #include "z_door_gerudo.h" #include "objects/object_door_gerudo/object_door_gerudo.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define FLAGS 0 @@ -101,6 +104,9 @@ void func_8099485C(DoorGerudo* this, PlayState* play) { if (this->unk_164 != 0) { this->actionFunc = func_8099496C; gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] -= 1; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateKeyCount(gSaveContext.mapIndex, gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]); +#endif Flags_SetSwitch(play, this->dyna.actor.params & 0x3F); Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK); } else { 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 f7556386f98..198b69dff10 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 @@ -23,6 +23,9 @@ #include "objects/object_menkuri_objects/object_menkuri_objects.h" #include "objects/object_demo_kekkai/object_demo_kekkai.h" #include "objects/object_ouke_haka/object_ouke_haka.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define FLAGS ACTOR_FLAG_UPDATE_WHILE_CULLED @@ -375,6 +378,9 @@ void func_80996B0C(DoorShutter* this, PlayState* play) { Flags_SetSwitch(play, this->dyna.actor.params & 0x3F); if (this->doorType != SHUTTER_BOSS) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateKeyCount(gSaveContext.mapIndex, gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]); +#endif Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK); } else { Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK_B); 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 685949b8189..f3183d6fa86 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 @@ -10,6 +10,9 @@ #include "objects/object_hidan_objects/object_hidan_objects.h" #include "objects/object_mizu_objects/object_mizu_objects.h" #include "objects/object_haka_door/object_haka_door.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define FLAGS ACTOR_FLAG_UPDATE_WHILE_CULLED @@ -201,6 +204,9 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) { (player->stateFlags1 & 0x8000000) ? 0.75f : 1.5f); if (this->lockTimer != 0) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateKeyCount(gSaveContext.mapIndex, gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]); +#endif Flags_SetSwitch(play, this->actor.params & 0x3F); Audio_PlayActorSound2(&this->actor, NA_SE_EV_CHAIN_KEY_UNLOCK); } diff --git a/soh/src/overlays/actors/ovl_En_Fr/z_en_fr.c b/soh/src/overlays/actors/ovl_En_Fr/z_en_fr.c index 38c54471f75..3885195b9d8 100644 --- a/soh/src/overlays/actors/ovl_En_Fr/z_en_fr.c +++ b/soh/src/overlays/actors/ovl_En_Fr/z_en_fr.c @@ -3,6 +3,8 @@ #include "vt.h" #include "objects/object_fr/object_fr.h" #include +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_FRIENDLY | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_NO_FREEZE_OCARINA) @@ -955,6 +957,7 @@ void EnFr_SetReward(EnFr* this, PlayState* play) { if ((songIndex >= FROG_ZL) && (songIndex <= FROG_SOT)) { if (!(gSaveContext.eventChkInf[13] & sSongIndex[songIndex])) { gSaveContext.eventChkInf[13] |= sSongIndex[songIndex]; + GameInteractor_ExecuteOnFlagSet(FLAG_EVENT_CHECK_INF, (EVENTCHKINF_SONGS_FOR_FROGS_INDEX << 4) + (int)log2(sSongIndex[songIndex])); if (!gSaveContext.n64ddFlag) { this->reward = GI_RUPEE_PURPLE; } else { @@ -967,6 +970,7 @@ void EnFr_SetReward(EnFr* this, PlayState* play) { } else if (songIndex == FROG_STORMS) { if (!(gSaveContext.eventChkInf[13] & sSongIndex[songIndex])) { gSaveContext.eventChkInf[13] |= sSongIndex[songIndex]; + GameInteractor_ExecuteOnFlagSet(FLAG_EVENT_CHECK_INF, (EVENTCHKINF_SONGS_FOR_FROGS_INDEX << 4) + (int)log2(sSongIndex[songIndex])); if (!gSaveContext.n64ddFlag) { this->reward = GI_HEART_PIECE; } else { @@ -979,6 +983,7 @@ void EnFr_SetReward(EnFr* this, PlayState* play) { } else if (songIndex == FROG_CHOIR_SONG) { if (!(gSaveContext.eventChkInf[13] & sSongIndex[songIndex])) { gSaveContext.eventChkInf[13] |= sSongIndex[songIndex]; + GameInteractor_ExecuteOnFlagSet(FLAG_EVENT_CHECK_INF, (EVENTCHKINF_SONGS_FOR_FROGS_INDEX << 4) + (int)log2(sSongIndex[songIndex])); if (!gSaveContext.n64ddFlag) { this->reward = GI_HEART_PIECE; } else { diff --git a/soh/src/overlays/actors/ovl_En_GirlA/z_en_girla.c b/soh/src/overlays/actors/ovl_En_GirlA/z_en_girla.c index 96d0c5ec3c2..f575f60af43 100644 --- a/soh/src/overlays/actors/ovl_En_GirlA/z_en_girla.c +++ b/soh/src/overlays/actors/ovl_En_GirlA/z_en_girla.c @@ -988,10 +988,6 @@ void EnGirlA_ItemGive_Randomizer(PlayState* play, EnGirlA* this) { Randomizer_Item_Give(play, getItemEntry); } - if (getItemEntry.itemId == GI_ICE_TRAP || getItemEntry.itemId == RG_ICE_TRAP) { - GameInteractor_ExecuteOnItemReceiveHooks(getItemEntry); - } - Flags_SetRandomizerInf(shopItemIdentity.randomizerInf); Rupees_ChangeBy(-this->basePrice); } diff --git a/soh/src/overlays/actors/ovl_En_Md/z_en_md.c b/soh/src/overlays/actors/ovl_En_Md/z_en_md.c index 90da4c01b4f..5d1fb9bcd74 100644 --- a/soh/src/overlays/actors/ovl_En_Md/z_en_md.c +++ b/soh/src/overlays/actors/ovl_En_Md/z_en_md.c @@ -785,6 +785,17 @@ void func_80AAB948(EnMd* this, PlayState* play) { player->stateFlags2 |= 0x800000; } } + + // #region SOH [Co-op] + if (Flags_GetEventChkInf(EVENTCHKINF_SHOWED_MIDO_SWORD_SHIELD) && this->interactInfo.talkState == NPC_TALK_STATE_IDLE && (play->sceneNum == SCENE_SPOT04)) { + func_80AAA92C(this, 3); + func_80AAA93C(this); + this->waypoint = 1; + this->interactInfo.talkState = NPC_TALK_STATE_IDLE; + this->actionFunc = func_80AABD0C; + this->actor.speedXZ = 1.5f; + } + // #endregion } void func_80AABC10(EnMd* this, PlayState* play) { diff --git a/soh/src/overlays/actors/ovl_En_Ms/z_en_ms.c b/soh/src/overlays/actors/ovl_En_Ms/z_en_ms.c index 039e7297f01..b098d289149 100644 --- a/soh/src/overlays/actors/ovl_En_Ms/z_en_ms.c +++ b/soh/src/overlays/actors/ovl_En_Ms/z_en_ms.c @@ -6,6 +6,9 @@ #include "z_en_ms.h" #include "objects/object_ms/object_ms.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_FRIENDLY) @@ -163,11 +166,17 @@ void EnMs_Sell(EnMs* this, PlayState* play) { gSaveContext.pendingSaleMod = itemEntry.modIndex; GiveItemEntryFromActor(&this->actor, play, itemEntry, 90.0f, 10.0f); BEANS_BOUGHT = 10; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateBeansBought(BEANS_BOUGHT); +#endif } else { GetItemEntry entry = ItemTable_Retrieve(GI_BEAN); gSaveContext.pendingSaleMod = entry.modIndex; gSaveContext.pendingSale = entry.itemId; func_8002F434(&this->actor, play, GI_BEAN, 90.0f, 10.0f); +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateBeansBought(BEANS_BOUGHT); +#endif } } } diff --git a/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c b/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c index 60e0e8b48fd..55ffd768c5a 100644 --- a/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c +++ b/soh/src/overlays/actors/ovl_En_Partner/z_en_partner.c @@ -11,6 +11,9 @@ #include #include #include +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define FLAGS (ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED | ACTOR_FLAG_DRAGGED_BY_HOOKSHOT | ACTOR_FLAG_CAN_PRESS_SWITCH) @@ -75,9 +78,19 @@ void EnPartner_Init(Actor* thisx, PlayState* play) { this->innerColor.b = 255.0f; this->innerColor.a = 255.0f; - this->outerColor.r = 0.0f; - this->outerColor.g = 255.0f; - this->outerColor.b = 0.0f; +#ifdef ENABLE_REMOTE_CONTROL + if (this->actor.params >= 3) { + Color_RGB8 color = Anchor_GetClientColor(this->actor.params - 3); + this->outerColor.r = color.r; + this->outerColor.g = color.g; + this->outerColor.b = color.b; + } else { + this->outerColor.r = 0.0f; + this->outerColor.g = 255.0f; + this->outerColor.b = 0.0f; + } +#endif + this->outerColor.a = 255.0f; this->usedItemButton = 0xFF; @@ -131,11 +144,19 @@ void EnPartner_UpdateLights(EnPartner* this, PlayState* play) { Player* player; player = GET_PLAYER(play); - Lights_PointNoGlowSetInfo(&this->lightInfoNoGlow, player->actor.world.pos.x, (s16)(player->actor.world.pos.y) + 69, - player->actor.world.pos.z, 200, 255, 200, lightRadius); + if (this->actor.params >= 3) { + Lights_PointNoGlowSetInfo(&this->lightInfoNoGlow, player->actor.world.pos.x, (s16)(player->actor.world.pos.y) + 69, + player->actor.world.pos.z, 200, 200, 200, lightRadius); + + Lights_PointGlowSetInfo(&this->lightInfoGlow, this->actor.world.pos.x, this->actor.world.pos.y + 9, + this->actor.world.pos.z, 200, 200, 200, glowLightRadius); + } else { + Lights_PointNoGlowSetInfo(&this->lightInfoNoGlow, player->actor.world.pos.x, (s16)(player->actor.world.pos.y) + 69, + player->actor.world.pos.z, 200, 255, 200, lightRadius); - Lights_PointGlowSetInfo(&this->lightInfoGlow, this->actor.world.pos.x, this->actor.world.pos.y + 9, + Lights_PointGlowSetInfo(&this->lightInfoGlow, this->actor.world.pos.x, this->actor.world.pos.y + 9, this->actor.world.pos.z, 200, 255, 200, glowLightRadius); + } Actor_SetScale(&this->actor, this->actor.scale.x); } @@ -576,6 +597,40 @@ void EnPartner_Update(Actor* thisx, PlayState* play) { s32 pad; EnPartner* this = (EnPartner*)thisx; +#ifdef ENABLE_REMOTE_CONTROL + if (this->actor.params >= 3) { + if (Anchor_GetClientScene(this->actor.params - 3) == play->sceneNum) { + PosRot coopPlayerPos = Anchor_GetClientPosition(this->actor.params - 3); + // if hidden, immediately update position + if (this->actor.world.pos.y == -9999.0f) { + this->actor.world = coopPlayerPos; + this->actor.world.pos.y += Player_GetHeight(GET_PLAYER(play)); + this->actor.shape.rot = coopPlayerPos.rot; + // Otherwise smoothly update position + } else { + float dist = 0.0f; + dist += Math_SmoothStepToF(&this->actor.world.pos.x, coopPlayerPos.pos.x, 0.5f, 1000.0f, 0.0f); + dist += Math_SmoothStepToF(&this->actor.world.pos.y, coopPlayerPos.pos.y + Player_GetHeight(GET_PLAYER(play)), 0.5f, 1000.0f, 0.0f); + dist += Math_SmoothStepToF(&this->actor.world.pos.z, coopPlayerPos.pos.z, 0.5f, 1000.0f, 0.0f); + if (dist > 1.0f) { + EnPartner_SpawnSparkles(this, play, 12); + } + this->actor.world.rot = coopPlayerPos.rot; + this->actor.shape.rot = coopPlayerPos.rot; + } + } else { + this->actor.world.pos.x = -9999.0f; + this->actor.world.pos.y = -9999.0f; + this->actor.world.pos.z = -9999.0f; + } + + thisx->shape.shadowAlpha = 0xFF; + SkelAnime_Update(&this->skelAnime); + EnPartner_UpdateLights(this, play); + return; + } +#endif + Input sControlInput = play->state.input[this->actor.params]; f32 relX = sControlInput.cur.stick_x / 10.0f * (CVarGetInteger("gMirroredWorld", 0) ? -1 : 1); 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 cb64be23e46..1bd98656f79 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 @@ -189,6 +189,14 @@ void func_80AFB950(EnSi* this, PlayState* play) { void EnSi_Update(Actor* thisx, PlayState* play) { EnSi* this = (EnSi*)thisx; + // #region SOH [Co-op] + // Check to see if this gold skull token has already been retrieved. + if (GET_GS_FLAGS((thisx->params & 0x1F00) >> 8) & (thisx->params & 0xFF)) { + Actor_Kill(&this->actor); + return; + } + // #endregion + Actor_MoveForward(&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 79a8d8bfbc3..ffb8946edb7 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 @@ -914,6 +914,14 @@ void func_80B0E9BC(EnSw* this, PlayState* play) { void EnSw_Update(Actor* thisx, PlayState* play) { EnSw* this = (EnSw*)thisx; + // #region SOH [Co-op] + // Check to see if this gold skull token has already been retrieved. + 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 35f6d5457b7..3093f65b5fb 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 @@ -53,6 +53,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 b300b68534b..558b52bbd91 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 @@ -126,7 +126,7 @@ void ObjBombiwa_Update(Actor* thisx, PlayState* play) { s32 pad; 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)) { 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..40bfc569103 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,10 @@ void ObjHamishi_Update(Actor* thisx, PlayState* play) { ObjHamishi_Shake(this); - if ((this->collider.base.acFlags & AC_HIT) && (this->collider.info.acHitInfo->toucher.dmgFlags & 0x40000040)) { + 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)) { this->shakeFrames = 15; this->shakePosSize = 2.0f; this->shakeRotSize = 400.0f; diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 86a6854cf65..b10c67eaf7a 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -6329,6 +6329,7 @@ s32 func_8083E5A8(Player* this, PlayState* play) { this->stateFlags1 &= ~(PLAYER_STATE1_GETTING_ITEM | PLAYER_STATE1_ITEM_OVER_HEAD); this->actor.colChkInfo.damage = 0; func_80837C0C(play, this, 3, 0.0f, 0.0f, 0, 20); + GameInteractor_ExecuteOnItemReceiveHooks(ItemTable_RetrieveEntry(MOD_RANDOMIZER, RG_ICE_TRAP)); this->getItemId = GI_NONE; this->getItemEntry = (GetItemEntry) GET_ITEM_NONE; // Gameplay stats: Increment Ice Trap count @@ -6361,7 +6362,6 @@ s32 func_8083E5A8(Player* this, PlayState* play) { Player_SetPendingFlag(this, play); Message_StartTextbox(play, 0xF8, NULL); Audio_PlayFanfare(NA_BGM_SMALL_ITEM_GET); - GameInteractor_ExecuteOnItemReceiveHooks(this->getItemEntry); gSaveContext.pendingIceTrapCount++; return 1; } @@ -12790,7 +12790,6 @@ s32 func_8084DFF4(PlayState* play, Player* this) { this->unk_862 = 0; gSaveContext.pendingIceTrapCount++; Player_SetPendingFlag(this, play); - GameInteractor_ExecuteOnItemReceiveHooks(giEntry); } this->getItemId = GI_NONE;