diff --git a/soh/include/functions.h b/soh/include/functions.h index 3a1aa8ae24a..fb9346adccd 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -1567,6 +1567,7 @@ s32 func_800C0808(PlayState* play, s16 camId, Player* player, s16 arg3); s32 Play_CameraChangeSetting(PlayState* play, s16 camId, s16 arg2); void func_800C08AC(PlayState* play, s16 camId, s16 arg2); void Play_SaveSceneFlags(PlayState* play); +void Play_SetRespawnData(PlayState* play, s32 respawnMode, s16 entranceIndex, s32 roomIndex, s32 playerParams, Vec3f* pos, s16 yaw); void Play_SetupRespawnPoint(PlayState* play, s32 respawnMode, s32 playerParams); void Play_TriggerVoidOut(PlayState* play); void Play_TriggerRespawn(PlayState* play); diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp index 9af4eadb7e9..9a517e3e5d0 100644 --- a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp @@ -131,12 +131,36 @@ std::map itemMapping = { ITEM_MAP_ENTRY(ITEM_WALLET_GIANT), ITEM_MAP_ENTRY(ITEM_SEEDS), ITEM_MAP_ENTRY(ITEM_FISHING_POLE), + ITEM_MAP_ENTRY(ITEM_SONG_MINUET), + ITEM_MAP_ENTRY(ITEM_SONG_BOLERO), + ITEM_MAP_ENTRY(ITEM_SONG_SERENADE), + ITEM_MAP_ENTRY(ITEM_SONG_REQUIEM), + ITEM_MAP_ENTRY(ITEM_SONG_NOCTURNE), + ITEM_MAP_ENTRY(ITEM_SONG_PRELUDE), + ITEM_MAP_ENTRY(ITEM_SONG_LULLABY), + ITEM_MAP_ENTRY(ITEM_SONG_EPONA), + ITEM_MAP_ENTRY(ITEM_SONG_SARIA), + ITEM_MAP_ENTRY(ITEM_SONG_SUN), + ITEM_MAP_ENTRY(ITEM_SONG_TIME), + ITEM_MAP_ENTRY(ITEM_SONG_STORMS), + ITEM_MAP_ENTRY(ITEM_MEDALLION_FOREST), + ITEM_MAP_ENTRY(ITEM_MEDALLION_FIRE), + ITEM_MAP_ENTRY(ITEM_MEDALLION_WATER), + ITEM_MAP_ENTRY(ITEM_MEDALLION_SPIRIT), + ITEM_MAP_ENTRY(ITEM_MEDALLION_SHADOW), + ITEM_MAP_ENTRY(ITEM_MEDALLION_LIGHT), + ITEM_MAP_ENTRY(ITEM_KOKIRI_EMERALD), + ITEM_MAP_ENTRY(ITEM_GORON_RUBY), + ITEM_MAP_ENTRY(ITEM_ZORA_SAPPHIRE), + ITEM_MAP_ENTRY(ITEM_STONE_OF_AGONY), + ITEM_MAP_ENTRY(ITEM_GERUDO_CARD), + ITEM_MAP_ENTRY(ITEM_SKULL_TOKEN), + ITEM_MAP_ENTRY(ITEM_HEART_CONTAINER), + ITEM_MAP_ENTRY(ITEM_HEART_PIECE), ITEM_MAP_ENTRY(ITEM_KEY_BOSS), ITEM_MAP_ENTRY(ITEM_COMPASS), ITEM_MAP_ENTRY(ITEM_DUNGEON_MAP), ITEM_MAP_ENTRY(ITEM_KEY_SMALL), - ITEM_MAP_ENTRY(ITEM_HEART_CONTAINER), - ITEM_MAP_ENTRY(ITEM_HEART_PIECE), ITEM_MAP_ENTRY(ITEM_MAGIC_SMALL), ITEM_MAP_ENTRY(ITEM_MAGIC_LARGE) }; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp index a0469d0c398..4ebd2f195c3 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp @@ -640,4 +640,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 aa7605a34ea..0b79f532a28 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 { @@ -230,6 +231,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..256de9b8d96 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.cpp @@ -0,0 +1,1105 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "GameInteractor_Anchor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include "macros.h" +#include "z64scene.h" +#include "z64actor.h" +#include "functions.h" +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 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 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 to_json(json& j, const PosRot& posRot) { + j = json{ + {"pos", posRot.pos}, + {"rot", posRot.rot} + }; +} + +void from_json(const json& j, PosRot& posRot) { + j.at("pos").get_to(posRot.pos); + j.at("rot").get_to(posRot.rot); +} + +void from_json(const json& j, AnchorClient& client) { + j.contains("clientId") ? j.at("clientId").get_to(client.clientId) : client.clientId = 0; + j.contains("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???"; + 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("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0; + j.contains("fileNum") ? j.at("fileNum").get_to(client.fileNum) : client.fileNum = 0xFF; + j.contains("gameComplete") ? j.at("gameComplete").get_to(client.gameComplete) : client.gameComplete = false; + j.contains("sceneNum") ? j.at("sceneNum").get_to(client.sceneNum) : client.sceneNum = SCENE_ID_MAX; + j.contains("roomIndex") ? j.at("roomIndex").get_to(client.roomIndex) : client.roomIndex = 0; + j.contains("entranceIndex") ? j.at("entranceIndex").get_to(client.entranceIndex) : client.entranceIndex = 0; + j.contains("posRot") ? j.at("posRot").get_to(client.posRot) : client.posRot = { -9999, -9999, -9999, 0, 0, 0 }; +} + +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{ + {"entrancesDiscovered", sohStats.entrancesDiscovered}, + {"fileCreatedAt", sohStats.fileCreatedAt}, + }; +} + +void from_json(const json& j, SohStats& sohStats) { + j.at("entrancesDiscovered").get_to(sohStats.entrancesDiscovered); + j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt); +} + +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}, + {"checkTrackerData", saveContext.checkTrackerData}, + {"triforcePiecesCollected", saveContext.triforcePiecesCollected}, + }; +} + +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); + j.at("checkTrackerData").get_to(saveContext.checkTrackerData); + j.at("triforcePiecesCollected").get_to(saveContext.triforcePiecesCollected); +} + +std::map GameInteractorAnchor::AnchorClients = {}; +std::vector GameInteractorAnchor::ActorIndexToClientId = {}; +std::string GameInteractorAnchor::clientVersion = "Anchor Build 12 (alpha 5)"; +std::vector> receivedItems = {}; +std::vector discoveredEntrances = {}; +std::vector anchorMessages = {}; +uint32_t notificationId = 0; + +void Anchor_DisplayMessage(AnchorMessage message = {}) { + message.id = notificationId++; + anchorMessages.push_back(message); + Audio_PlaySoundGeneral(NA_SE_SY_METRONOME, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); +} + +void Anchor_SendClientData() { + nlohmann::json payload; + payload["data"]["name"] = CVarGetString("gRemote.AnchorName", ""); + payload["data"]["color"] = CVarGetColor24("gRemote.AnchorColor", { 100, 255, 100 }); + payload["data"]["clientVersion"] = GameInteractorAnchor::clientVersion; + payload["data"]["gameComplete"] = gSaveContext.sohStats.gameComplete; + payload["type"] = "UPDATE_CLIENT_DATA"; + + if (GameInteractor::Instance->IsSaveLoaded()) { + payload["data"]["seed"] = gSaveContext.finalSeed; + payload["data"]["fileNum"] = gSaveContext.fileNum; + payload["data"]["sceneNum"] = gPlayState->sceneNum; + payload["data"]["entranceIndex"] = gSaveContext.entranceIndex; + } else { + payload["data"]["seed"] = 0; + payload["data"]["fileNum"] = 0xFF; + payload["data"]["sceneNum"] = SCENE_ID_MAX; + payload["data"]["entranceIndex"] = 0x00; + } + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void GameInteractorAnchor::Enable() { + if (isEnabled) { + return; + } + + isEnabled = true; + GameInteractor::Instance->EnableRemoteInteractor(); + GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { + HandleRemoteJson(payload); + }); + GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() { + Anchor_DisplayMessage({ .message = "Connected to Anchor" }); + if (GameInteractor::IsSaveLoaded() || gSaveContext.fileNum == 0xFF) { + Anchor_SendClientData(); + } + if (GameInteractor::IsSaveLoaded()) { + Anchor_RequestSaveStateFromRemote(); + } + }); + GameInteractor::Instance->RegisterRemoteDisconnectedHandler([&]() { + Anchor_DisplayMessage({ .message = "Disconnected from Anchor" }); + }); +} + +void GameInteractorAnchor::Disable() { + if (!isEnabled) { + return; + } + + isEnabled = false; + GameInteractor::Instance->DisableRemoteInteractor(); + + GameInteractorAnchor::AnchorClients.clear(); + Anchor_RefreshClientActors(); +} + +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"] != "ALL_CLIENT_DATA" && payload["type"] != "UPDATE_CLIENT_DATA")) { + if (payload.contains("clientId")) { + uint32_t clientId = payload["clientId"].get(); + if (GameInteractorAnchor::AnchorClients.contains(clientId) && GameInteractorAnchor::AnchorClients[clientId].clientVersion != GameInteractorAnchor::clientVersion) { + return; + } + } + } + + if (payload["type"] == "GIVE_ITEM") { + auto effect = new GameInteractionEffect::GiveItem(); + effect->parameters[0] = payload["modId"].get(); + effect->parameters[1] = payload["getItemId"].get(); + CVarSetInteger("gFromGI", 1); + CheckTracker::AddItemReceived(ItemTableManager::Instance->RetrieveItemEntry(effect->parameters[0], effect->parameters[1])); + receivedItems.push_back({ payload["modId"].get(), payload["getItemId"].get() }); + if (effect->Apply() == Possible) { + GetItemEntry getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(effect->parameters[0], effect->parameters[1]); + + AnchorClient anchorClient = GameInteractorAnchor::AnchorClients[payload["clientId"].get()]; + if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) { + if (getItemEntry.modIndex == MOD_NONE) { + Anchor_DisplayMessage({ + .itemIcon = SohUtils::GetIconNameFromItemID(getItemEntry.itemId), + .prefix = SohUtils::GetItemName(getItemEntry.itemId), + .message = "from", + .suffix = anchorClient.name + }); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + Anchor_DisplayMessage({ + .itemIcon = SohUtils::GetIconNameFromItemID(SohUtils::GetItemIdIconFromRandomizerGet(getItemEntry.getItemId)), + .prefix = OTRGlobals::Instance->gRandomizer->EnumToSpoilerfileGetName[(RandomizerGet)getItemEntry.getItemId][gSaveContext.language], + .message = "from", + .suffix = anchorClient.name + }); + } + } + } + CVarClear("gFromGI"); + } + 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].sceneNum = payload["sceneNum"].get(); + GameInteractorAnchor::AnchorClients[clientId].roomIndex = payload.contains("roomIndex") ? payload.at("roomIndex").get() : 0; + GameInteractorAnchor::AnchorClients[clientId].entranceIndex = payload.contains("entranceIndex") ? payload.at("entranceIndex").get() : 0; + 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, + client.seed, + client.fileNum, + client.gameComplete, + client.sceneNum, + 0, + client.entranceIndex, + { -9999, -9999, -9999, 0, 0, 0 } + }; + Anchor_DisplayMessage({ + .prefix = client.name, + .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f), + .message = "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](AnchorClient& c) { return c.clientId == clientId; }) == newClients.end()) { + clientsToRemove.push_back(clientId); + } + } + for (auto& clientId : clientsToRemove) { + Anchor_DisplayMessage({ + .prefix = GameInteractorAnchor::AnchorClients[clientId].name, + .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f), + .message = "disconnected" + }); + GameInteractorAnchor::AnchorClients.erase(clientId); + } + + Anchor_RefreshClientActors(); + } + if (payload["type"] == "UPDATE_CLIENT_DATA") { + uint32_t clientId = payload["clientId"].get(); + if (GameInteractorAnchor::AnchorClients.contains(clientId)) { + AnchorClient client = payload["data"].get(); + GameInteractorAnchor::AnchorClients[clientId].clientVersion = client.clientVersion; + GameInteractorAnchor::AnchorClients[clientId].name = client.name; + GameInteractorAnchor::AnchorClients[clientId].color = client.color; + GameInteractorAnchor::AnchorClients[clientId].seed = client.seed; + GameInteractorAnchor::AnchorClients[clientId].fileNum = client.fileNum; + GameInteractorAnchor::AnchorClients[clientId].gameComplete = client.gameComplete; + GameInteractorAnchor::AnchorClients[clientId].sceneNum = client.sceneNum; + GameInteractorAnchor::AnchorClients[clientId].entranceIndex = client.entranceIndex; + } + } + if (payload["type"] == "UPDATE_CHECK_DATA" && GameInteractor::IsSaveLoaded()) { + auto check = payload["locationIndex"].get(); + auto data = payload["checkData"].get(); + CheckTracker::UpdateCheck(check, data); + } + if (payload["type"] == "ENTRANCE_DISCOVERED") { + auto entranceIndex = payload["entranceIndex"].get(); + discoveredEntrances.push_back(entranceIndex); + Entrance_SetEntranceDiscovered(entranceIndex); + } + if (payload["type"] == "UPDATE_BEANS_BOUGHT" && GameInteractor::IsSaveLoaded()) { + BEANS_BOUGHT = payload["amount"].get(); + } + if (payload["type"] == "UPDATE_BEANS_COUNT" && GameInteractor::IsSaveLoaded()) { + AMMO(ITEM_BEAN) = 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"] == "GIVE_DUNGEON_ITEM" && GameInteractor::IsSaveLoaded()) { + gSaveContext.inventory.dungeonItems[payload["sceneNum"].get()] |= gBitFlags[payload["itemId"].get() - ITEM_KEY_BOSS]; + } + if (payload["type"] == "GAME_COMPLETE") { + AnchorClient anchorClient = GameInteractorAnchor::AnchorClients[payload["clientId"].get()]; + Anchor_DisplayMessage({ + .prefix = anchorClient.name, + .message = "has killed Ganon.", + }); + } + if (payload["type"] == "REQUEST_TELEPORT") { + Anchor_TeleportToPlayer(payload["clientId"].get()); + } + if (payload["type"] == "TELEPORT_TO") { + uint32_t entranceIndex = payload["entranceIndex"].get(); + uint32_t roomIndex = payload["roomIndex"].get(); + PosRot posRot = payload["posRot"].get(); + + Play_SetRespawnData(gPlayState, RESPAWN_MODE_DOWN, entranceIndex, roomIndex, 0xDFF, &posRot.pos, posRot.rot.y); + Play_TriggerVoidOut(gPlayState); + } + if (payload["type"] == "SERVER_MESSAGE") { + Anchor_DisplayMessage({ + .prefix = "Server:", + .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f), + .message = payload["message"].get(), + }); + } + if (payload["type"] == "DISABLE_ANCHOR") { + GameInteractor::Instance->isRemoteInteractorEnabled = false; + GameInteractorAnchor::Instance->isEnabled = false; + } + if (payload["type"] == "RESET") { + std::reinterpret_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch("reset"); + } +} + +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; + gSaveContext.adultTradeItems = loadedData.adultTradeItems; + gSaveContext.triforcePiecesCollected = loadedData.triforcePiecesCollected; + + 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 < SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT; i++) { + gSaveContext.sohStats.entrancesDiscovered[i] = loadedData.sohStats.entrancesDiscovered[i]; + } + + CheckTracker::ClearAreaTotals(); + for (int i = 2; i < RC_MAX; i++) { + if (loadedData.checkTrackerData[i].status == RCSHOW_SAVED || loadedData.checkTrackerData[i].skipped) { + CheckTracker::AddToChecksCollected(static_cast(i)); + } + gSaveContext.checkTrackerData[i].status = loadedData.checkTrackerData[i].status; + gSaveContext.checkTrackerData[i].skipped = loadedData.checkTrackerData[i].skipped; + gSaveContext.checkTrackerData[i].price = loadedData.checkTrackerData[i].price; + gSaveContext.checkTrackerData[i].hintItem = loadedData.checkTrackerData[i].hintItem; + } + + CheckTracker::UpdateAllOrdering(); + + 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; + Anchor_DisplayMessage({ .message = "State loaded from remote!" }); +}; + +AnchorClient* Anchor_GetClientByActorIndex(uint32_t actorIndex) { + if (actorIndex < GameInteractorAnchor::ActorIndexToClientId.size()) { + uint32_t clientId = GameInteractorAnchor::ActorIndexToClientId[actorIndex]; + if (GameInteractorAnchor::AnchorClients.find(clientId) != GameInteractorAnchor::AnchorClients.end()) { + return &GameInteractorAnchor::AnchorClients[clientId]; + } + } + + return nullptr; +} + +uint8_t Anchor_GetClientScene(uint32_t actorIndex) { + AnchorClient* client = Anchor_GetClientByActorIndex(actorIndex); + if (client == nullptr) return SCENE_ID_MAX; + + return client->sceneNum; +} + +PosRot Anchor_GetClientPosition(uint32_t actorIndex) { + AnchorClient* client = Anchor_GetClientByActorIndex(actorIndex); + if (client == nullptr) return { -9999.0, -9999.0, -9999.0, 0, 0, 0 }; + + return client->posRot; +} + +uint8_t Anchor_GetClientRoomIndex(uint32_t actorIndex) { + AnchorClient* client = Anchor_GetClientByActorIndex(actorIndex); + if (client == nullptr) return 0xFF; + + return client->roomIndex; +} + +Color_RGB8 Anchor_GetClientColor(uint32_t actorIndex) { + AnchorClient* client = Anchor_GetClientByActorIndex(actorIndex); + if (client == nullptr) return { 100, 255, 100 }; + + return client->color; +} + +void Anchor_RefreshClientActors() { + if (!GameInteractor::IsSaveLoaded()) return; + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_ITEMACTION].head; + while (actor != NULL) { + if (gEnPartnerId == actor->id) { + Actor_Kill(actor); + } + actor = actor->next; + } + + GameInteractorAnchor::ActorIndexToClientId.clear(); + + uint32_t i = 0; + for (auto [clientId, client] : GameInteractorAnchor::AnchorClients) { + GameInteractorAnchor::ActorIndexToClientId.push_back(clientId); + auto fairy = Actor_Spawn( + &gPlayState->actorCtx, gPlayState, gEnPartnerId, + client.posRot.pos.x, client.posRot.pos.y, client.posRot.pos.z, + client.posRot.rot.x, client.posRot.rot.y, client.posRot.rot.z, + 3 + i, false + ); + NameTag_RegisterForActor(fairy, client.name.c_str()); + i++; + } +} + +static uint32_t lastSceneNum = SCENE_ID_MAX; + +void Anchor_RegisterHooks() { + GameInteractor::Instance->RegisterGameHook([]() { + if (gPlayState == NULL || !GameInteractor::Instance->isRemoteInteractorConnected) return; + + // Moved to a new scene + if (lastSceneNum != gPlayState->sceneNum) { + Anchor_SendClientData(); + } + + if (GameInteractor::Instance->IsSaveLoaded()) { + // Player loaded into file + if (lastSceneNum == SCENE_ID_MAX) { + Anchor_RequestSaveStateFromRemote(); + } + + Anchor_RefreshClientActors(); + } + + lastSceneNum = gPlayState->sceneNum; + }); + GameInteractor::Instance->RegisterGameHook([]() { + lastSceneNum = SCENE_ID_MAX; + if (!GameInteractor::Instance->isRemoteInteractorConnected) return; + + Anchor_SendClientData(); + }); + + GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { + if (itemEntry.modIndex == MOD_NONE && ((itemEntry.itemId >= ITEM_KEY_BOSS && itemEntry.itemId <= ITEM_KEY_SMALL) || itemEntry.itemId == ITEM_SWORD_MASTER)) { + return; + } + + // If the item exists in receivedItems, remove it from the list and don't emit the packet + auto it = std::find_if(receivedItems.begin(), receivedItems.end(), [itemEntry](std::pair pair) { + return pair.first == itemEntry.tableId && pair.second == itemEntry.getItemId; + }); + if (it != receivedItems.end()) { + receivedItems.erase(it); + 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; + payload["quiet"] = true; + + 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; + payload["quiet"] = true; + + 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; + payload["quiet"] = true; + + 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; + payload["quiet"] = true; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([]() { + static uint32_t lastPlayerCount = 0; + uint32_t currentPlayerCount = 0; + for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { + if (client.sceneNum == gPlayState->sceneNum) { + currentPlayerCount++; + } + } + 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["roomIndex"] = gPlayState->roomCtx.curRoom.num; + payload["entranceIndex"] = gSaveContext.entranceIndex; + payload["posRot"] = player->actor.world; + payload["quiet"] = true; + + lastPosition = currentPosition; + lastPlayerCount = currentPlayerCount; + + for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { + if (client.sceneNum == gPlayState->sceneNum) { + payload["targetClientId"] = clientId; + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + } + } + }); +} + +void Anchor_EntranceDiscovered(uint16_t entranceIndex) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + // If the entrance exists in discoveredEntrances, remove it from the list and don't emit the packet + auto it = std::find(discoveredEntrances.begin(), discoveredEntrances.end(), entranceIndex); + if (it != discoveredEntrances.end()) { + discoveredEntrances.erase(it); + return; + } + + nlohmann::json payload; + + payload["type"] = "ENTRANCE_DISCOVERED"; + payload["entranceIndex"] = entranceIndex; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_UpdateCheckData(uint32_t locationIndex) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "UPDATE_CHECK_DATA"; + payload["locationIndex"] = locationIndex; + payload["checkData"] = gSaveContext.checkTrackerData[locationIndex]; + if (gSaveContext.checkTrackerData[locationIndex].status == RCSHOW_COLLECTED) { + payload["checkData"]["status"] = RCSHOW_SAVED; + } + + 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_UpdateBeansCount(uint8_t amount) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "UPDATE_BEANS_COUNT"; + 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); +} + +void Anchor_GiveDungeonItem(int16_t sceneNum, uint16_t itemId) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "GIVE_DUNGEON_ITEM"; + payload["sceneNum"] = sceneNum; + payload["itemId"] = itemId; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_GameComplete() { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "GAME_COMPLETE"; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); + Anchor_SendClientData(); +} + +void Anchor_RequestTeleport(uint32_t clientId) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->IsSaveLoaded()) return; + + nlohmann::json payload; + + payload["type"] = "REQUEST_TELEPORT"; + payload["targetClientId"] = clientId; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +void Anchor_TeleportToPlayer(uint32_t clientId) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::Instance->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; + + GameInteractorAnchor::Instance->TransmitJsonToRemote(payload); +} + +const ImVec4 GRAY = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); +const ImVec4 WHITE = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); +const ImVec4 GREEN = ImVec4(0.5f, 1.0f, 0.5f, 1.0f); + +void AnchorPlayerLocationWindow::DrawElement() { + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::Begin("AnchorPlayerLocationWindow", &mIsVisible, + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoScrollbar + ); + + ImGui::TextColored(gSaveContext.sohStats.gameComplete ? GREEN : WHITE, "%s", CVarGetString("gRemote.AnchorName", "")); + if (GameInteractor::Instance->IsSaveLoaded()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5, 0.5, 0.5, 1), "%s", SohUtils::GetSceneName(gPlayState->sceneNum).c_str()); + } + for (auto& [clientId, client] : GameInteractorAnchor::AnchorClients) { + ImGui::PushID(clientId); + ImGui::TextColored(client.gameComplete ? GREEN : WHITE, "%s", client.name.c_str()); + if (client.clientVersion != GameInteractorAnchor::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", GameInteractorAnchor::clientVersion.c_str()); + ImGui::Text("Theirs: %s", client.clientVersion.c_str()); + ImGui::EndTooltip(); + } + } + if (client.seed != gSaveContext.finalSeed && client.fileNum != 0xFF && GameInteractor::Instance->IsSaveLoaded()) { + 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", gSaveContext.finalSeed); + ImGui::Text("Theirs: %u", client.seed); + ImGui::EndTooltip(); + } + } + if (client.sceneNum < SCENE_ID_MAX && client.fileNum != 0xFF) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5, 0.5, 0.5, 1), "%s", SohUtils::GetSceneName(client.sceneNum).c_str()); + if (GameInteractor::Instance->IsSaveLoaded() && client.sceneNum != SCENE_GROTTOS && client.sceneNum != SCENE_ID_MAX) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button(ICON_FA_CHEVRON_RIGHT, ImVec2(ImGui::GetFontSize() * 1.0f, ImGui::GetFontSize() * 1.0f))) { + Anchor_RequestTeleport(clientId); + } + ImGui::PopStyleVar(); + } + } + ImGui::PopID(); + } + + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +void AnchorLogWindow::DrawElement() { + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + ImGui::Begin("AnchorLogWindow", &mIsVisible, + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoScrollbar + ); + + // Options to stack notifications on top or bottom, and left or right + if (ImGui::Button(CVarGetInteger("gRemote.AnchorLogWindowX", 1) ? ICON_FA_CHEVRON_RIGHT : ICON_FA_CHEVRON_LEFT, ImVec2(20, 20))) { + CVarSetInteger("gRemote.AnchorLogWindowX", !CVarGetInteger("gRemote.AnchorLogWindowX", 1)); + } + ImGui::SameLine(); + if (ImGui::Button(CVarGetInteger("gRemote.AnchorLogWindowY", 1) ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP, ImVec2(20, 20))) { + CVarSetInteger("gRemote.AnchorLogWindowY", !CVarGetInteger("gRemote.AnchorLogWindowY", 1)); + } + + // Store x/y position of window + ImVec2 anchorPos = ImGui::GetWindowPos(); + ImVec2 anchorSize = ImGui::GetWindowSize(); + + for (int index = 0; index < anchorMessages.size(); ++index) { + auto& message = anchorMessages[index]; + int inverseIndex = -ABS(index - (anchorMessages.size() - 1)); + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); + if (message.remainingTime < 4.0f) { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, (message.remainingTime - 1) / 3.0f); + } else { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); + } + ImGui::Begin(("anchorLog" + std::to_string(message.id)).c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar + ); + ImGui::SetWindowPos(ImVec2( + // X position should take into account both the alignment and the width of the message window + anchorPos.x + (CVarGetInteger("gRemote.AnchorLogWindowX", 1) ? 0 : -(ImGui::GetWindowSize().x - anchorSize.x)), + // Y Position should take into account the stack direction and index of the message + anchorPos.y + (CVarGetInteger("gRemote.AnchorLogWindowY", 1) ? (anchorSize.y + (ImGui::GetWindowSize().y * inverseIndex)) : -(ImGui::GetWindowSize().y * (inverseIndex + 1))) + )); + ImGui::SetWindowFontScale(1.8f); + + if (message.itemIcon != nullptr) { + ImGui::Image(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(message.itemIcon), ImVec2(24, 24)); + ImGui::SameLine(); + } + if (!message.prefix.empty()) { + ImGui::TextColored(message.prefixColor, "%s", message.prefix.c_str()); + ImGui::SameLine(); + } + ImGui::TextColored(message.messageColor, "%s", message.message.c_str()); + if (!message.suffix.empty()) { + ImGui::SameLine(); + ImGui::TextColored(message.suffixColor, "%s", message.suffix.c_str()); + } + ImGui::End(); + ImGui::PopStyleVar(); + + // decrement remainingTime + message.remainingTime -= ImGui::GetIO().DeltaTime; + + // remove message if it has expired + if (message.remainingTime <= 0) { + anchorMessages.erase(anchorMessages.begin() + index); + --index; + } + } + + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +#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..9be4ba62f52 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Anchor.h @@ -0,0 +1,99 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "z64actor.h" +#include + +#ifdef __cplusplus +#include "z64item.h" +#include "./GameInteractor.h" + +typedef struct { + uint32_t clientId; + std::string clientVersion; + std::string name; + Color_RGB8 color; + uint32_t seed; + uint8_t fileNum; + bool gameComplete; + uint8_t sceneNum; + uint8_t roomIndex; + uint32_t entranceIndex; + PosRot posRot; +} AnchorClient; + +class GameInteractorAnchor { + private: + bool isEnabled; + + void HandleRemoteJson(nlohmann::json payload); + public: + static GameInteractorAnchor* Instance; + static std::map AnchorClients; + static std::vector ActorIndexToClientId; + static std::string clientVersion; + + void Enable(); + void Disable(); + + void TransmitJsonToRemote(nlohmann::json payload); +}; + +class AnchorPlayerLocationWindow : public LUS::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override {}; + void DrawElement() override; + void UpdateElement() override {}; +}; + +struct AnchorMessage { + uint32_t id = 0; + const char* itemIcon = nullptr; + std::string prefix = ""; + ImVec4 prefixColor = ImVec4(0.5f, 0.5f, 1.0f, 1.0f); + std::string message = ""; + ImVec4 messageColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + std::string suffix = ""; + ImVec4 suffixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + float remainingTime = 10.0f; +}; + +class AnchorLogWindow : public LUS::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override {}; + void DrawElement() override; + void UpdateElement() override {}; +}; + +#endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +void Anchor_RegisterHooks(); +void Anchor_PushSaveStateToRemote(); +void Anchor_RequestSaveStateFromRemote(); +uint8_t Anchor_GetClientScene(uint32_t actorIndex); +PosRot Anchor_GetClientPosition(uint32_t actorIndex); +uint8_t Anchor_GetClientRoomIndex(uint32_t actorIndex); +Color_RGB8 Anchor_GetClientColor(uint32_t actorIndex); +void Anchor_RefreshClientActors(); +void Anchor_EntranceDiscovered(uint16_t entranceIndex); +void Anchor_UpdateCheckData(uint32_t locationIndex); +void Anchor_UpdateBeansBought(uint8_t amount); +void Anchor_UpdateBeansCount(uint8_t amount); +void Anchor_ConsumeAdultTradeItem(uint8_t itemId); +void Anchor_UpdateKeyCount(int16_t sceneNum, int8_t amount); +void Anchor_GiveDungeonItem(int16_t sceneNum, uint16_t itemId); +void Anchor_GameComplete(); +void Anchor_RequestTeleport(uint32_t clientId); +void Anchor_TeleportToPlayer(uint32_t clientId); + +#ifdef __cplusplus +} +#endif diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_RawAction.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_RawAction.cpp index 54a3b8e7bd8..1a4b85183ff 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" @@ -122,6 +123,23 @@ void GameInteractor::RawAction::ElectrocutePlayer() { func_80837C0C(gPlayState, player, 4, 0, 0, 0, 0); } +void GameInteractor::RawAction::GiveItem(uint16_t modId, uint16_t itemId) { + 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/game-interactor/GameInteractor_Remote.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp index 2cbd6b379f0..eea9cd457ff 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp @@ -73,12 +73,14 @@ void GameInteractor::DisableRemoteInteractor() { } void GameInteractor::TransmitDataToRemote(const char* payload) { - SDLNet_TCP_Send(remoteSocket, payload, strlen(payload) + 1); + SDLNet_TCP_Send(remoteSocket, payload, strlen(payload)); } // Appends a newline character to the end of the json payload and sends it to the remote void GameInteractor::TransmitJsonToRemote(nlohmann::json payload) { - TransmitDataToRemote(payload.dump().c_str()); + // TODO: Migrate anchor server to use null terminators instead of newlines + std::string payloadString = payload.dump() + '\n'; + TransmitDataToRemote(payloadString.c_str()); } // MARK: - Private @@ -134,7 +136,8 @@ void GameInteractor::ReceiveFromServer() { receivedData.append(remoteDataReceived, len); // Proess all complete packets - size_t delimiterPos = receivedData.find('\0'); + // TODO: Migrate anchor server to use null terminators instead of newlines + size_t delimiterPos = receivedData.find('\n'); while (delimiterPos != std::string::npos) { // Extract the complete packet until the delimiter std::string packet = receivedData.substr(0, delimiterPos); @@ -142,7 +145,7 @@ void GameInteractor::ReceiveFromServer() { receivedData.erase(0, delimiterPos + 1); HandleRemoteJson(packet); // Find the next delimiter - delimiterPos = receivedData.find('\0'); + delimiterPos = receivedData.find('\n'); } } diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index a6e5a47e84d..39842691110 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -14,6 +14,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 @@ -436,7 +439,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 0bbeff8ca0f..a47ec210558 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -9,6 +9,9 @@ #include "soh/Enhancements/cosmetics/authenticGfxPatches.h" #include #include "soh/Enhancements/nametag.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #include "src/overlays/actors/ovl_En_Bb/z_en_bb.h" #include "src/overlays/actors/ovl_En_Dekubaba/z_en_dekubaba.h" @@ -1081,4 +1084,7 @@ void InitMods() { RegisterRandomizerSheikSpawn(); RegisterRandomizedEnemySizes(); NameTag_RegisterHooks(); + #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 6123c1a2797..6bcc65e7c26 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -12,6 +12,9 @@ #include "3drando/item_location.hpp" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "z64item.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif extern "C" { #include "variables.h" @@ -258,6 +261,10 @@ void SetCheckCollected(RandomizerCheck rc) { } SaveManager::Instance->SaveSection(gSaveContext.fileNum, sectionId, true); +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateCheckData(rc); +#endif + doAreaScroll = true; UpdateOrdering(rcObj.rcArea); UpdateInventoryChecks(); @@ -369,6 +376,20 @@ bool EvaluateCheck(RandomizerCheckObject rco) { return false; } +void AddItemReceived(GetItemEntry giEntry) { + itemsReceived.push_back(giEntry); +} + +void AddToChecksCollected(RandomizerCheck rc) { + areaChecksGotten[RandomizerCheckObjects::GetAllRCObjects().find(rc)->second.rcArea]++; +} + +void ClearAreaTotals() { + for (auto& [rcArea, vec] : checksByArea) { + areaChecksGotten[rcArea] = 0; + } +} + bool CheckByArea(RandomizerCheckArea area = RCAREA_INVALID) { if (area == RCAREA_INVALID) { area = checkAreas.front(); @@ -393,6 +414,9 @@ void SetShopSeen(uint32_t sceneNum, bool prices) { for (int i = start; i < start + 8; i++) { if (gSaveContext.checkTrackerData[i].status == RCSHOW_UNCHECKED) { gSaveContext.checkTrackerData[i].status = RCSHOW_SEEN; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateCheckData(i); +#endif statusChanged = true; } } @@ -529,6 +553,9 @@ void CheckTrackerShopSlotChange(uint8_t cursorSlot, int16_t basePrice) { gSaveContext.checkTrackerData[slot].status = RCSHOW_IDENTIFIED; gSaveContext.checkTrackerData[slot].price = basePrice; SaveManager::Instance->SaveSection(gSaveContext.fileNum, sectionId, true); +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateCheckData(slot); +#endif } } @@ -1294,6 +1321,9 @@ void DrawLocation(RandomizerCheckObject rcObj) { gSaveContext.checkTrackerData[rcObj.rc].skipped = true; areaChecksGotten[rcObj.rcArea]++; } +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateCheckData(rcObj.rc); +#endif UpdateOrdering(rcObj.rcArea); UpdateInventoryChecks(); SaveManager::Instance->SaveSection(gSaveContext.fileNum, sectionId, true); diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h index b2193f696e0..4e5eba253f9 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h @@ -2,6 +2,7 @@ #include #include "randomizerTypes.h" #include "randomizer_check_objects.h" +#include "soh/Enhancements/item-tables/ItemTableTypes.h" #include @@ -49,6 +50,9 @@ void UpdateAllOrdering(); bool IsVisibleInCheckTracker(RandomizerCheckObject rcObj); void InitTrackerData(bool isDebug); void SetLastItemGetRC(RandomizerCheck rc); +void AddToChecksCollected(RandomizerCheck rc); +void AddItemReceived(GetItemEntry giEntry); +void ClearAreaTotals(); RandomizerCheckArea GetCheckArea(); void CheckTrackerDialogClosed(); void ToggleShopRightChecks(); diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index f2f1a8a3f35..03cbbaac92b 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -10,6 +10,9 @@ #include "randomizer_entrance.h" #include "randomizer_grotto.h" #include +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #include "global.h" @@ -791,6 +794,10 @@ void Entrance_SetEntranceDiscovered(u16 entranceIndex) { return; } +#ifdef ENABLE_REMOTE_CONTROL + Anchor_EntranceDiscovered(entranceIndex); +#endif + u16 bitsPerIndex = sizeof(u32) * 8; u32 idx = entranceIndex / bitsPerIndex; if (idx < SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT) { diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp index fe335c2b6d9..0d8df9edcc9 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp @@ -867,7 +867,7 @@ void EntranceTrackerWindow::DrawElement() { (override->reverseIndex == lastEntranceIndex && OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_DECOUPLED_ENTRANCES) == RO_GENERIC_OFF)) && CVarGetInteger("gEntranceTrackerHighlightPrevious", 0)) { color = COLOR_ORANGE; - } else if (LinkIsInArea(original) != -1) { + } else if (LinkIsInArea(destToggle ? override : original) != -1) { if (CVarGetInteger("gEntranceTrackerHighlightAvailable", 0)) { color = COLOR_GREEN; } @@ -885,16 +885,30 @@ void EntranceTrackerWindow::DrawElement() { // Use a non-breaking space to keep the arrow from wrapping to a newline by itself auto nbsp = u8"\u00A0"; if (original->srcGroup != ENTRANCE_GROUP_ONE_WAY) { - ImGui::TextWrapped("%s to %s%s->", origSrcName, origDstName, nbsp); + if (!destToggle) ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + ImGui::TextWrapped("%s", origSrcName); + if (!destToggle) ImGui::PopStyleColor(); + ImGui::SameLine(); + if (destToggle) ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + ImGui::TextWrapped("%s %s%s->", destToggle ? "from" : "to", origDstName, nbsp); + if (destToggle) ImGui::PopStyleColor(); } else { ImGui::TextWrapped("%s%s->", origSrcName, nbsp); } // Indent the destination - ImGui::SetCursorPosX(ImGui::GetCursorPosX() * 2); + // ImGui::SetCursorPosX(ImGui::GetCursorPosX() * 2); if (!showOverride || (showOverride && (!override->oneExit && override->srcGroup != ENTRANCE_GROUP_ONE_WAY))) { - ImGui::TextWrapped("%s from %s", rplcDstName, rplcSrcName); + ImGui::SameLine(); + if (destToggle) ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + ImGui::TextWrapped("%s", rplcDstName); + if (destToggle) ImGui::PopStyleColor(); + ImGui::SameLine(); + if (!destToggle) ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY); + ImGui::TextWrapped("%s %s", destToggle ? "to" : "from", rplcSrcName); + if (!destToggle) ImGui::PopStyleColor(); } else { + ImGui::SameLine(); ImGui::TextWrapped("%s", rplcDstName); } diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 0cabc06cbb6..771e7210547 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -80,8 +80,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" @@ -1071,6 +1073,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; @@ -1102,6 +1105,9 @@ extern "C" void InitOTR() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Enable(); break; + case GI_SCHEME_ANCHOR: + GameInteractorAnchor::Instance->Enable(); + break; } } #endif @@ -1128,6 +1134,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/SohGui.cpp b/soh/soh/SohGui.cpp index 77fe2f0a6dc..0979a4a3739 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -34,6 +34,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 #include "Enhancements/game-interactor/GameInteractor.h" @@ -126,6 +127,10 @@ namespace SohGui { std::shared_ptr mItemTrackerSettingsWindow; std::shared_ptr mItemTrackerWindow; std::shared_ptr mRandomizerSettingsWindow; +#ifdef ENABLE_REMOTE_CONTROL + std::shared_ptr mAnchorPlayerLocationWindow; + std::shared_ptr mAnchorLogWindow; +#endif void SetupGuiElements() { auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); @@ -184,6 +189,12 @@ namespace SohGui { gui->AddGuiWindow(mItemTrackerSettingsWindow); mRandomizerSettingsWindow = std::make_shared("gRandomizerSettingsEnabled", "Randomizer Settings"); gui->AddGuiWindow(mRandomizerSettingsWindow); +#ifdef ENABLE_REMOTE_CONTROL + mAnchorPlayerLocationWindow = std::make_shared("gRemote.AnchorPlayerLocationWindow", "Anchor Player Location Window"); + gui->AddGuiWindow(mAnchorPlayerLocationWindow); + mAnchorLogWindow = std::make_shared("gRemote.AnchorLogWindow", "Anchor Log"); + gui->AddGuiWindow(mAnchorLogWindow); +#endif } void Destroy() { @@ -205,5 +216,9 @@ namespace SohGui { mStatsWindow = nullptr; mConsoleWindow = nullptr; mSohMenuBar = nullptr; +#ifdef ENABLE_REMOTE_CONTROL + mAnchorPlayerLocationWindow = nullptr; + mAnchorLogWindow = nullptr; +#endif } } diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index bad50d40401..5dbb99e680c 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 @@ -1396,6 +1397,10 @@ extern std::shared_ptr mSaveEditorWindow; extern std::shared_ptr mColViewerWindow; extern std::shared_ptr mActorViewerWindow; extern std::shared_ptr mDLViewerWindow; +#ifdef ENABLE_REMOTE_CONTROL +extern std::shared_ptr mAnchorPlayerLocationWindow; +extern std::shared_ptr mAnchorLogWindow; +#endif void DrawDeveloperToolsMenu() { if (ImGui::BeginMenu("Developer Tools")) { @@ -1493,9 +1498,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"); @@ -1508,10 +1521,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()); @@ -1524,8 +1543,33 @@ void DrawRemoteControlMenu() { CVarSetInteger("gRemote.Port", port); LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); } - ImGui::PopItemWidth(); + } else { + ImGui::Text("Fairy Color & Name"); + static Color_RGBA8 color = CVarGetColor("gRemote.AnchorColor", { 100, 255, 100, 255 }); + 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; + + CVarSetColor("gRemote.AnchorColor", color); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + 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"); + int flags = 0; + if (GameInteractor::Instance->isRemoteInteractorEnabled) flags = ImGuiInputTextFlags_Password; + if (ImGui::InputText("##gRemote.AnchorRoomId", (char*)anchorRoomId.c_str(), anchorRoomId.capacity() + 1, flags)) { + CVarSetString("gRemote.AnchorRoomId", anchorRoomId.c_str()); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + } ImGui::EndDisabled(); ImGui::Spacing(); @@ -1543,6 +1587,15 @@ void DrawRemoteControlMenu() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Disable(); break; + case GI_SCHEME_ANCHOR: + if (CVarGetInteger("gRemote.AnchorLogWindow", 0) && mAnchorLogWindow) { + mAnchorLogWindow->ToggleVisibility(); + } + if (CVarGetInteger("gRemote.AnchorPlayerLocationWindow", 0) && mAnchorPlayerLocationWindow) { + mAnchorPlayerLocationWindow->ToggleVisibility(); + } + GameInteractorAnchor::Instance->Disable(); + break; } } else { CVarSetInteger("gRemote.Enabled", 1); @@ -1554,6 +1607,11 @@ void DrawRemoteControlMenu() { case GI_SCHEME_CROWD_CONTROL: CrowdControl::Instance->Enable(); break; + case GI_SCHEME_ANCHOR: + if (mAnchorLogWindow) mAnchorLogWindow->ToggleVisibility(); + if (mAnchorPlayerLocationWindow) mAnchorPlayerLocationWindow->ToggleVisibility(); + GameInteractorAnchor::Instance->Enable(); + break; } } } @@ -1561,13 +1619,62 @@ void DrawRemoteControlMenu() { if (GameInteractor::Instance->isRemoteInteractorEnabled) { ImGui::Spacing(); - if (GameInteractor::Instance->isRemoteInteractorConnected) { - ImGui::Text("Connected"); - } else { + if (!GameInteractor::Instance->isRemoteInteractorConnected) { ImGui::Text("Connecting..."); } } + if (GameInteractor::Instance->isRemoteInteractorConnected && CVarGetInteger("gRemote.Scheme", GI_SCHEME_BUILT_IN) == GI_SCHEME_ANCHOR) { + if (ImGui::Button("Request State", ImVec2(ImGui::GetContentRegionAvail().x, 0.0f))) { + Anchor_RequestSaveStateFromRemote(); + } + + 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, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Incompatible version! Will not work together!"); + ImGui::Text("Yours: %s", GameInteractorAnchor::clientVersion.c_str()); + ImGui::Text("Theirs: %s", client.clientVersion.c_str()); + ImGui::EndTooltip(); + } + } + if (client.seed != gSaveContext.finalSeed && client.fileNum != 0xFF && gSaveContext.fileNum != 0xFF) { + 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", gSaveContext.finalSeed); + ImGui::Text("Theirs: %u", client.seed); + ImGui::EndTooltip(); + } + } + } + + ImGui::Spacing(); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + if (mAnchorPlayerLocationWindow) { + if (ImGui::Button(GetWindowButtonText("Player Location", CVarGetInteger("gRemote.AnchorPlayerLocationWindow", 0)).c_str(), ImVec2(-1.0f, 0.0f))) { + mAnchorPlayerLocationWindow->ToggleVisibility(); + } + } + if (mAnchorLogWindow) { + if (ImGui::Button(GetWindowButtonText("Anchor Log", CVarGetInteger("gRemote.AnchorLogWindow", 0)).c_str(), ImVec2(-1.0f, 0.0f))) { + mAnchorLogWindow->ToggleVisibility(); + } + } + ImGui::PopStyleVar(3); + } + ImGui::Dummy(ImVec2(0.0f, 0.0f)); ImGui::EndMenu(); } diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index 62e1d2dce23..8ef7cc5416f 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -1,7 +1,10 @@ #include "util.h" #include +#include +#include "Enhancements/randomizer/randomizerTypes.h" #include +#include std::vector sceneNames = { "Inside the Deku Tree", @@ -27,7 +30,7 @@ std::vector sceneNames = { "Phantom Ganon's Lair", "Volvagia's Lair", "Morpha's Lair", - "Twinrova's Lair & Nabooru's Mini-Boss Room", + "Twinrova's Lair", "Bongo Bongo's Lair", "Ganondorf's Lair", "Tower Collapse Exterior", @@ -76,34 +79,34 @@ std::vector sceneNames = { "Castle Hedge Maze (Day)", "Castle Hedge Maze (Night)", "Cutscene Map", - "Damp�'s Grave & Windmill", + "Dampe's Grave & Windmill", "Fishing Pond", "Castle Courtyard", "Bombchu Bowling Alley", "Ranch House & Silo", "Guard House", "Granny's Potion Shop", - "Ganon's Tower Collapse & Battle Arena", + "Ganon's Tower Collapse & Arena", "House of Skulltula", - "Spot 00 - Hyrule Field", - "Spot 01 - Kakariko Village", - "Spot 02 - Graveyard", - "Spot 03 - Zora's River", - "Spot 04 - Kokiri Forest", - "Spot 05 - Sacred Forest Meadow", - "Spot 06 - Lake Hylia", - "Spot 07 - Zora's Domain", - "Spot 08 - Zora's Fountain", - "Spot 09 - Gerudo Valley", - "Spot 10 - Lost Woods", - "Spot 11 - Desert Colossus", - "Spot 12 - Gerudo's Fortress", - "Spot 13 - Haunted Wasteland", - "Spot 15 - Hyrule Castle", - "Spot 16 - Death Mountain Trail", - "Spot 17 - Death Mountain Crater", - "Spot 18 - Goron City", - "Spot 20 - Lon Lon Ranch", + "Hyrule Field", + "Kakariko Village", + "Graveyard", + "Zora's River", + "Kokiri Forest", + "Sacred Forest Meadow", + "Lake Hylia", + "Zora's Domain", + "Zora's Fountain", + "Gerudo Valley", + "Lost Woods", + "Desert Colossus", + "Gerudo's Fortress", + "Haunted Wasteland", + "Hyrule Castle", + "Death Mountain Trail", + "Death Mountain Crater", + "Goron City", + "Lon Lon Ranch", "Ganon's Castle Exterior", "Jungle Gym", "Ganondorf Test Room", @@ -302,6 +305,341 @@ std::vector questItemNames = { "Gold Skulltula Token", }; +#define ITEM_ICON(id) \ + { id, #id } + +// Maps items ids to info for use in ImGui +std::map itemIcons = { + ITEM_ICON(ITEM_STICK), + ITEM_ICON(ITEM_NUT), + ITEM_ICON(ITEM_BOMB), + ITEM_ICON(ITEM_BOW), + ITEM_ICON(ITEM_ARROW_FIRE), + ITEM_ICON(ITEM_DINS_FIRE), + ITEM_ICON(ITEM_SLINGSHOT), + ITEM_ICON(ITEM_OCARINA_FAIRY), + ITEM_ICON(ITEM_OCARINA_TIME), + ITEM_ICON(ITEM_BOMBCHU), + ITEM_ICON(ITEM_HOOKSHOT), + ITEM_ICON(ITEM_LONGSHOT), + ITEM_ICON(ITEM_ARROW_ICE), + ITEM_ICON(ITEM_FARORES_WIND), + ITEM_ICON(ITEM_BOOMERANG), + ITEM_ICON(ITEM_LENS), + ITEM_ICON(ITEM_BEAN), + ITEM_ICON(ITEM_HAMMER), + ITEM_ICON(ITEM_ARROW_LIGHT), + ITEM_ICON(ITEM_NAYRUS_LOVE), + ITEM_ICON(ITEM_BOTTLE), + ITEM_ICON(ITEM_POTION_RED), + ITEM_ICON(ITEM_POTION_GREEN), + ITEM_ICON(ITEM_POTION_BLUE), + ITEM_ICON(ITEM_FAIRY), + ITEM_ICON(ITEM_FISH), + ITEM_ICON(ITEM_MILK_BOTTLE), + ITEM_ICON(ITEM_LETTER_RUTO), + ITEM_ICON(ITEM_BLUE_FIRE), + ITEM_ICON(ITEM_BUG), + ITEM_ICON(ITEM_BIG_POE), + ITEM_ICON(ITEM_MILK_HALF), + ITEM_ICON(ITEM_POE), + ITEM_ICON(ITEM_WEIRD_EGG), + ITEM_ICON(ITEM_CHICKEN), + ITEM_ICON(ITEM_LETTER_ZELDA), + ITEM_ICON(ITEM_MASK_KEATON), + ITEM_ICON(ITEM_MASK_SKULL), + ITEM_ICON(ITEM_MASK_SPOOKY), + ITEM_ICON(ITEM_MASK_BUNNY), + ITEM_ICON(ITEM_MASK_GORON), + ITEM_ICON(ITEM_MASK_ZORA), + ITEM_ICON(ITEM_MASK_GERUDO), + ITEM_ICON(ITEM_MASK_TRUTH), + ITEM_ICON(ITEM_SOLD_OUT), + ITEM_ICON(ITEM_POCKET_EGG), + ITEM_ICON(ITEM_POCKET_CUCCO), + ITEM_ICON(ITEM_COJIRO), + ITEM_ICON(ITEM_ODD_MUSHROOM), + ITEM_ICON(ITEM_ODD_POTION), + ITEM_ICON(ITEM_SAW), + ITEM_ICON(ITEM_SWORD_BROKEN), + ITEM_ICON(ITEM_PRESCRIPTION), + ITEM_ICON(ITEM_FROG), + ITEM_ICON(ITEM_EYEDROPS), + ITEM_ICON(ITEM_CLAIM_CHECK), + ITEM_ICON(ITEM_BOW_ARROW_FIRE), + ITEM_ICON(ITEM_BOW_ARROW_ICE), + ITEM_ICON(ITEM_BOW_ARROW_LIGHT), + ITEM_ICON(ITEM_SWORD_KOKIRI), + ITEM_ICON(ITEM_SWORD_MASTER), + ITEM_ICON(ITEM_SWORD_BGS), + ITEM_ICON(ITEM_SHIELD_DEKU), + ITEM_ICON(ITEM_SHIELD_HYLIAN), + ITEM_ICON(ITEM_SHIELD_MIRROR), + ITEM_ICON(ITEM_TUNIC_KOKIRI), + ITEM_ICON(ITEM_TUNIC_GORON), + ITEM_ICON(ITEM_TUNIC_ZORA), + ITEM_ICON(ITEM_BOOTS_KOKIRI), + ITEM_ICON(ITEM_BOOTS_IRON), + ITEM_ICON(ITEM_BOOTS_HOVER), + ITEM_ICON(ITEM_BULLET_BAG_30), + ITEM_ICON(ITEM_BULLET_BAG_40), + ITEM_ICON(ITEM_BULLET_BAG_50), + ITEM_ICON(ITEM_QUIVER_30), + ITEM_ICON(ITEM_QUIVER_40), + ITEM_ICON(ITEM_QUIVER_50), + ITEM_ICON(ITEM_BOMB_BAG_20), + ITEM_ICON(ITEM_BOMB_BAG_30), + ITEM_ICON(ITEM_BOMB_BAG_40), + ITEM_ICON(ITEM_BRACELET), + ITEM_ICON(ITEM_GAUNTLETS_SILVER), + ITEM_ICON(ITEM_GAUNTLETS_GOLD), + ITEM_ICON(ITEM_SCALE_SILVER), + ITEM_ICON(ITEM_SCALE_GOLDEN), + ITEM_ICON(ITEM_SWORD_KNIFE), + ITEM_ICON(ITEM_WALLET_ADULT), + ITEM_ICON(ITEM_WALLET_GIANT), + ITEM_ICON(ITEM_SEEDS), + ITEM_ICON(ITEM_FISHING_POLE), + ITEM_ICON(ITEM_SONG_MINUET), + ITEM_ICON(ITEM_SONG_BOLERO), + ITEM_ICON(ITEM_SONG_SERENADE), + ITEM_ICON(ITEM_SONG_REQUIEM), + ITEM_ICON(ITEM_SONG_NOCTURNE), + ITEM_ICON(ITEM_SONG_PRELUDE), + ITEM_ICON(ITEM_SONG_LULLABY), + ITEM_ICON(ITEM_SONG_EPONA), + ITEM_ICON(ITEM_SONG_SARIA), + ITEM_ICON(ITEM_SONG_SUN), + ITEM_ICON(ITEM_SONG_TIME), + ITEM_ICON(ITEM_SONG_STORMS), + ITEM_ICON(ITEM_MEDALLION_FOREST), + ITEM_ICON(ITEM_MEDALLION_FIRE), + ITEM_ICON(ITEM_MEDALLION_WATER), + ITEM_ICON(ITEM_MEDALLION_SPIRIT), + ITEM_ICON(ITEM_MEDALLION_SHADOW), + ITEM_ICON(ITEM_MEDALLION_LIGHT), + ITEM_ICON(ITEM_KOKIRI_EMERALD), + ITEM_ICON(ITEM_GORON_RUBY), + ITEM_ICON(ITEM_ZORA_SAPPHIRE), + ITEM_ICON(ITEM_STONE_OF_AGONY), + ITEM_ICON(ITEM_GERUDO_CARD), + ITEM_ICON(ITEM_SKULL_TOKEN), + ITEM_ICON(ITEM_HEART_CONTAINER), + ITEM_ICON(ITEM_HEART_PIECE), + ITEM_ICON(ITEM_KEY_BOSS), + ITEM_ICON(ITEM_COMPASS), + ITEM_ICON(ITEM_DUNGEON_MAP), + ITEM_ICON(ITEM_KEY_SMALL), + ITEM_ICON(ITEM_MAGIC_SMALL), + ITEM_ICON(ITEM_MAGIC_LARGE), + ITEM_ICON(RG_TRIFORCE_PIECE), +}; + +std::map randomizerGetToItemIdIcon = { + { RG_NONE, ITEM_NONE }, + { RG_KOKIRI_SWORD, ITEM_SWORD_KOKIRI }, + { RG_GIANTS_KNIFE, ITEM_SWORD_KNIFE }, + { RG_BIGGORON_SWORD, ITEM_SWORD_BGS }, + { RG_DEKU_SHIELD, ITEM_SHIELD_DEKU }, + { RG_HYLIAN_SHIELD, ITEM_SHIELD_HYLIAN }, + { RG_MIRROR_SHIELD, ITEM_SHIELD_MIRROR }, + { RG_GORON_TUNIC, ITEM_TUNIC_GORON }, + { RG_ZORA_TUNIC, ITEM_TUNIC_ZORA }, + { RG_IRON_BOOTS, ITEM_BOOTS_IRON }, + { RG_HOVER_BOOTS, ITEM_BOOTS_HOVER }, + { RG_BOOMERANG, ITEM_BOOMERANG }, + { RG_LENS_OF_TRUTH, ITEM_LENS }, + { RG_MEGATON_HAMMER, ITEM_HAMMER }, + { RG_STONE_OF_AGONY, ITEM_STONE_OF_AGONY }, + { RG_DINS_FIRE, ITEM_DINS_FIRE }, + { RG_FARORES_WIND, ITEM_FARORES_WIND }, + { RG_NAYRUS_LOVE, ITEM_NAYRUS_LOVE }, + { RG_FIRE_ARROWS, ITEM_ARROW_FIRE }, + { RG_ICE_ARROWS, ITEM_ARROW_ICE }, + { RG_LIGHT_ARROWS, ITEM_ARROW_LIGHT }, + { RG_GERUDO_MEMBERSHIP_CARD, ITEM_GERUDO_CARD }, + { RG_MAGIC_BEAN, ITEM_BEAN }, + { RG_MAGIC_BEAN_PACK, ITEM_BEAN }, + { RG_DOUBLE_DEFENSE, ITEM_DOUBLE_DEFENSE }, + { RG_WEIRD_EGG, ITEM_WEIRD_EGG }, + { RG_ZELDAS_LETTER, ITEM_LETTER_ZELDA }, + { RG_POCKET_EGG, ITEM_POCKET_EGG }, + { RG_COJIRO, ITEM_COJIRO }, + { RG_ODD_MUSHROOM, ITEM_ODD_MUSHROOM }, + { RG_ODD_POTION, ITEM_ODD_POTION }, + { RG_POACHERS_SAW, ITEM_SAW }, + { RG_BROKEN_SWORD, ITEM_SWORD_BROKEN }, + { RG_PRESCRIPTION, ITEM_PRESCRIPTION }, + { RG_EYEBALL_FROG, ITEM_FROG }, + { RG_EYEDROPS, ITEM_EYEDROPS }, + { RG_CLAIM_CHECK, ITEM_CLAIM_CHECK }, + { RG_GOLD_SKULLTULA_TOKEN, ITEM_SKULL_TOKEN }, + { RG_PROGRESSIVE_HOOKSHOT, ITEM_HOOKSHOT }, + { RG_PROGRESSIVE_STRENGTH, ITEM_BRACELET }, + { RG_PROGRESSIVE_BOMB_BAG, ITEM_BOMB_BAG_20 }, + { RG_PROGRESSIVE_BOW, ITEM_BOW }, + { RG_PROGRESSIVE_SLINGSHOT, ITEM_SLINGSHOT }, + { RG_PROGRESSIVE_WALLET, ITEM_WALLET_ADULT }, + { RG_PROGRESSIVE_SCALE, ITEM_SCALE_SILVER }, + { RG_PROGRESSIVE_NUT_UPGRADE, ITEM_NUT }, + { RG_PROGRESSIVE_STICK_UPGRADE, ITEM_STICK }, + { RG_PROGRESSIVE_BOMBCHUS, ITEM_BOMBCHU }, + { RG_PROGRESSIVE_MAGIC_METER, ITEM_MAGIC_SMALL }, + { RG_MAGIC_SINGLE, ITEM_MAGIC_SMALL }, + { RG_MAGIC_DOUBLE, ITEM_MAGIC_LARGE }, + { RG_PROGRESSIVE_OCARINA, ITEM_OCARINA_FAIRY }, + { RG_PROGRESSIVE_GORONSWORD, ITEM_SWORD_BGS }, + { RG_EMPTY_BOTTLE, ITEM_BOTTLE }, + { RG_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE }, + { RG_BOTTLE_WITH_RED_POTION, ITEM_POTION_RED }, + { RG_BOTTLE_WITH_GREEN_POTION, ITEM_POTION_GREEN }, + { RG_BOTTLE_WITH_BLUE_POTION, ITEM_POTION_BLUE }, + { RG_BOTTLE_WITH_FAIRY, ITEM_FAIRY }, + { RG_BOTTLE_WITH_FISH, ITEM_FISH }, + { RG_BOTTLE_WITH_BLUE_FIRE, ITEM_BLUE_FIRE }, + { RG_BOTTLE_WITH_BUGS, ITEM_BUG }, + { RG_BOTTLE_WITH_POE, ITEM_POE }, + { RG_RUTOS_LETTER, ITEM_LETTER_RUTO }, + { RG_BOTTLE_WITH_BIG_POE, ITEM_BIG_POE }, + { RG_ZELDAS_LULLABY, ITEM_SONG_LULLABY }, + { RG_EPONAS_SONG, ITEM_SONG_EPONA }, + { RG_SARIAS_SONG, ITEM_SONG_SARIA }, + { RG_SUNS_SONG, ITEM_SONG_SUN }, + { RG_SONG_OF_TIME, ITEM_SONG_TIME }, + { RG_SONG_OF_STORMS, ITEM_SONG_STORMS }, + { RG_MINUET_OF_FOREST, ITEM_SONG_MINUET }, + { RG_BOLERO_OF_FIRE, ITEM_SONG_BOLERO }, + { RG_SERENADE_OF_WATER, ITEM_SONG_SERENADE }, + { RG_REQUIEM_OF_SPIRIT, ITEM_SONG_REQUIEM }, + { RG_NOCTURNE_OF_SHADOW, ITEM_SONG_NOCTURNE }, + { RG_PRELUDE_OF_LIGHT, ITEM_SONG_PRELUDE }, + { RG_DEKU_TREE_MAP, ITEM_DUNGEON_MAP }, + { RG_DODONGOS_CAVERN_MAP, ITEM_DUNGEON_MAP }, + { RG_JABU_JABUS_BELLY_MAP, ITEM_DUNGEON_MAP }, + { RG_FOREST_TEMPLE_MAP, ITEM_DUNGEON_MAP }, + { RG_FIRE_TEMPLE_MAP, ITEM_DUNGEON_MAP }, + { RG_WATER_TEMPLE_MAP, ITEM_DUNGEON_MAP }, + { RG_SPIRIT_TEMPLE_MAP, ITEM_DUNGEON_MAP }, + { RG_SHADOW_TEMPLE_MAP, ITEM_DUNGEON_MAP }, + { RG_BOTTOM_OF_THE_WELL_MAP, ITEM_DUNGEON_MAP }, + { RG_ICE_CAVERN_MAP, ITEM_DUNGEON_MAP }, + { RG_DEKU_TREE_COMPASS, ITEM_COMPASS }, + { RG_DODONGOS_CAVERN_COMPASS, ITEM_COMPASS }, + { RG_JABU_JABUS_BELLY_COMPASS, ITEM_COMPASS }, + { RG_FOREST_TEMPLE_COMPASS, ITEM_COMPASS }, + { RG_FIRE_TEMPLE_COMPASS, ITEM_COMPASS }, + { RG_WATER_TEMPLE_COMPASS, ITEM_COMPASS }, + { RG_SPIRIT_TEMPLE_COMPASS, ITEM_COMPASS }, + { RG_SHADOW_TEMPLE_COMPASS, ITEM_COMPASS }, + { RG_BOTTOM_OF_THE_WELL_COMPASS, ITEM_COMPASS }, + { RG_ICE_CAVERN_COMPASS, ITEM_COMPASS }, + { RG_FOREST_TEMPLE_BOSS_KEY, ITEM_KEY_BOSS }, + { RG_FIRE_TEMPLE_BOSS_KEY, ITEM_KEY_BOSS }, + { RG_WATER_TEMPLE_BOSS_KEY, ITEM_KEY_BOSS }, + { RG_SPIRIT_TEMPLE_BOSS_KEY, ITEM_KEY_BOSS }, + { RG_SHADOW_TEMPLE_BOSS_KEY, ITEM_KEY_BOSS }, + { RG_GANONS_CASTLE_BOSS_KEY, ITEM_KEY_BOSS }, + { RG_FOREST_TEMPLE_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_FIRE_TEMPLE_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_WATER_TEMPLE_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_SPIRIT_TEMPLE_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_SHADOW_TEMPLE_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_BOTTOM_OF_THE_WELL_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_GERUDO_TRAINING_GROUNDS_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_GERUDO_FORTRESS_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_GANONS_CASTLE_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_TREASURE_GAME_SMALL_KEY, ITEM_KEY_SMALL }, + { RG_FOREST_TEMPLE_KEY_RING, ITEM_KEY_SMALL }, + { RG_FIRE_TEMPLE_KEY_RING, ITEM_KEY_SMALL }, + { RG_WATER_TEMPLE_KEY_RING, ITEM_KEY_SMALL }, + { RG_SPIRIT_TEMPLE_KEY_RING, ITEM_KEY_SMALL }, + { RG_SHADOW_TEMPLE_KEY_RING, ITEM_KEY_SMALL }, + { RG_BOTTOM_OF_THE_WELL_KEY_RING, ITEM_KEY_SMALL }, + { RG_GERUDO_TRAINING_GROUNDS_KEY_RING, ITEM_KEY_SMALL }, + { RG_GERUDO_FORTRESS_KEY_RING, ITEM_KEY_SMALL }, + { RG_GANONS_CASTLE_KEY_RING, ITEM_KEY_SMALL }, + { RG_KOKIRI_EMERALD, ITEM_KOKIRI_EMERALD }, + { RG_GORON_RUBY, ITEM_GORON_RUBY }, + { RG_ZORA_SAPPHIRE, ITEM_ZORA_SAPPHIRE }, + { RG_FOREST_MEDALLION, ITEM_MEDALLION_FOREST }, + { RG_FIRE_MEDALLION, ITEM_MEDALLION_FIRE }, + { RG_WATER_MEDALLION, ITEM_MEDALLION_WATER }, + { RG_SPIRIT_MEDALLION, ITEM_MEDALLION_SPIRIT }, + { RG_SHADOW_MEDALLION, ITEM_MEDALLION_SHADOW }, + { RG_LIGHT_MEDALLION, ITEM_MEDALLION_LIGHT }, + { RG_RECOVERY_HEART, ITEM_HEART }, + { RG_GREEN_RUPEE, ITEM_RUPEE_GREEN }, + { RG_GREG_RUPEE, ITEM_RUPEE_GREEN }, + { RG_BLUE_RUPEE, ITEM_RUPEE_BLUE }, + { RG_RED_RUPEE, ITEM_RUPEE_RED }, + { RG_PURPLE_RUPEE, ITEM_RUPEE_PURPLE }, + { RG_HUGE_RUPEE, ITEM_RUPEE_GOLD }, + { RG_PIECE_OF_HEART, ITEM_HEART_PIECE }, + { RG_HEART_CONTAINER, ITEM_HEART_CONTAINER }, + { RG_ICE_TRAP, ITEM_NONE }, + { RG_MILK, ITEM_MILK_BOTTLE }, + { RG_BOMBS_5, ITEM_BOMB }, + { RG_BOMBS_10, ITEM_BOMB }, + { RG_BOMBS_20, ITEM_BOMB }, + { RG_BOMBCHU_5, ITEM_BOMBCHU }, + { RG_BOMBCHU_10, ITEM_BOMBCHU }, + { RG_BOMBCHU_20, ITEM_BOMBCHU }, + { RG_BOMBCHU_DROP, ITEM_BOMBCHU }, + { RG_ARROWS_5, ITEM_ARROWS_SMALL }, + { RG_ARROWS_10, ITEM_ARROWS_MEDIUM }, + { RG_ARROWS_30, ITEM_ARROWS_LARGE }, + { RG_DEKU_NUTS_5, ITEM_NUT }, + { RG_DEKU_NUTS_10, ITEM_NUT }, + { RG_DEKU_SEEDS_30, ITEM_SEEDS }, + { RG_DEKU_STICK_1, ITEM_STICK }, + { RG_RED_POTION_REFILL, ITEM_POTION_RED }, + { RG_GREEN_POTION_REFILL, ITEM_POTION_GREEN }, + { RG_BLUE_POTION_REFILL, ITEM_POTION_BLUE }, + { RG_TREASURE_GAME_HEART, ITEM_HEART_PIECE }, + { RG_TREASURE_GAME_GREEN_RUPEE, ITEM_RUPEE_GREEN }, + { RG_BUY_DEKU_NUT_5, ITEM_NUT }, + { RG_BUY_ARROWS_30, ITEM_ARROWS_MEDIUM }, + { RG_BUY_ARROWS_50, ITEM_ARROWS_LARGE }, + { RG_BUY_BOMBS_525, ITEM_BOMB }, + { RG_BUY_DEKU_NUT_10, ITEM_NUT }, + { RG_BUY_DEKU_STICK_1, ITEM_STICK }, + { RG_BUY_BOMBS_10, ITEM_BOMB }, + { RG_BUY_FISH, ITEM_FISH }, + { RG_BUY_RED_POTION_30, ITEM_POTION_RED }, + { RG_BUY_GREEN_POTION, ITEM_POTION_GREEN }, + { RG_BUY_BLUE_POTION, ITEM_POTION_BLUE }, + { RG_BUY_HYLIAN_SHIELD, ITEM_SHIELD_HYLIAN }, + { RG_BUY_DEKU_SHIELD, ITEM_SHIELD_DEKU }, + { RG_BUY_GORON_TUNIC, ITEM_TUNIC_GORON }, + { RG_BUY_ZORA_TUNIC, ITEM_TUNIC_ZORA }, + { RG_BUY_HEART, ITEM_HEART }, + { RG_BUY_BOMBCHU_10, ITEM_BOMBCHU }, + { RG_BUY_BOMBCHU_20, ITEM_BOMBCHU }, + { RG_BUY_DEKU_SEEDS_30, ITEM_SEEDS }, + { RG_SOLD_OUT, ITEM_SOLD_OUT }, + { RG_BUY_BLUE_FIRE, ITEM_BLUE_FIRE }, + { RG_BUY_BOTTLE_BUG, ITEM_BUG }, + { RG_BUY_POE, ITEM_POE }, + { RG_BUY_FAIRYS_SPIRIT, ITEM_FAIRY }, + { RG_BUY_ARROWS_10, ITEM_ARROWS_SMALL }, + { RG_BUY_BOMBS_20, ITEM_BOMB }, + { RG_BUY_BOMBS_30, ITEM_BOMB }, + { RG_BUY_BOMBS_535, ITEM_BOMB }, + { RG_BUY_RED_POTION_40, ITEM_POTION_RED }, + { RG_BUY_RED_POTION_50, ITEM_POTION_RED }, + { RG_TYCOON_WALLET, ITEM_WALLET_GIANT }, + { RG_TRIFORCE_PIECE, RG_TRIFORCE_PIECE }, +}; + +int32_t SohUtils::GetItemIdIconFromRandomizerGet(int32_t randomizerGet) { + return randomizerGetToItemIdIcon.contains(randomizerGet) ? randomizerGetToItemIdIcon[randomizerGet] : ITEM_NONE; +} + +const char* SohUtils::GetIconNameFromItemID(int32_t itemId) { + return itemIcons.contains(itemId) ? itemIcons[itemId] : nullptr; +} + +// To be used with SceneID enum (SCENE_ prefix) const std::string& SohUtils::GetSceneName(int32_t scene) { return sceneNames[scene]; } diff --git a/soh/soh/util.h b/soh/soh/util.h index bdbfcd777fb..143a71b26b1 100644 --- a/soh/soh/util.h +++ b/soh/soh/util.h @@ -3,6 +3,10 @@ #include namespace SohUtils { + int32_t GetItemIdIconFromRandomizerGet(int32_t randomizerGet); + + const char* GetIconNameFromItemID(int32_t itemId); + const std::string& GetSceneName(int32_t scene); const std::string& GetItemName(int32_t item); diff --git a/soh/src/code/z_en_item00.c b/soh/src/code/z_en_item00.c index 42ea78f90ba..69240cae6fa 100644 --- a/soh/src/code/z_en_item00.c +++ b/soh/src/code/z_en_item00.c @@ -776,6 +776,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) || (this->actor.params >= ITEM00_ARROWS_SMALL && this->actor.params <= ITEM00_SMALL_KEY) || diff --git a/soh/src/code/z_map_exp.c b/soh/src/code/z_map_exp.c index c4db0098b23..c581329a06f 100644 --- a/soh/src/code/z_map_exp.c +++ b/soh/src/code/z_map_exp.c @@ -5,6 +5,10 @@ #include "textures/map_i_static/map_i_static.h" #include "textures/map_grand_static/map_grand_static.h" #include +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif MapData* gMapData; @@ -602,6 +606,8 @@ void Map_Init(PlayState* play) { } } +extern s16 gEnPartnerId; + void Minimap_DrawCompassIcons(PlayState* play) { s32 pad; Player* player = GET_PLAYER(play); @@ -684,6 +690,12 @@ void Minimap_DrawCompassIcons(PlayState* play) { G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); gDPSetPrimColor(OVERLAY_DISP++, 0, 0xFF, currentPositionColor.r, currentPositionColor.g, currentPositionColor.b, 255); +#ifdef ENABLE_REMOTE_CONTROL + if (CVarGetInteger("gRemote.Scheme", 0) == GI_SCHEME_ANCHOR) { + Color_RGB8 myColor = CVarGetColor24("gRemote.AnchorColor", (Color_RGB8){ 100, 255, 100 }); + gDPSetPrimColor(OVERLAY_DISP++, 0, 0xFF, myColor.r, myColor.g, myColor.b, 255); + } +#endif gSPDisplayList(OVERLAY_DISP++, gCompassArrowDL); //Player map entry (red arrow) @@ -718,6 +730,72 @@ void Minimap_DrawCompassIcons(PlayState* play) { gDPSetPrimColor(OVERLAY_DISP++, 0, 0xFF, lastEntranceColor.r, lastEntranceColor.g, lastEntranceColor.b, 255); gSPDisplayList(OVERLAY_DISP++, gCompassArrowDL); + +#ifdef ENABLE_REMOTE_CONTROL + // Other Anchor Players Arrow + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_ITEMACTION].head; + while (actor != NULL) { + if (gEnPartnerId == actor->id && Anchor_GetClientRoomIndex(actor->params - 3) == gPlayState->roomCtx.curRoom.num) { + if (actor->world.pos.x != -9999.0 && Anchor_GetClientScene(actor->params - 3) == gPlayState->sceneNum) { + Color_RGB8 playerColor = Anchor_GetClientColor(actor->params - 3); + + tempX = actor->world.pos.x; + tempZ = actor->world.pos.z; + tempX /= R_COMPASS_SCALE_X * (CVarGetInteger("gMirroredWorld", 0) ? -1 : 1); + tempZ /= R_COMPASS_SCALE_Y; + + s16 tempXOffset = R_COMPASS_OFFSET_X + (CVarGetInteger("gMirroredWorld", 0) ? mirrorOffset : 0); + if (CVarGetInteger("gMinimapPosType", 0) != 0) { + if (CVarGetInteger("gMinimapPosType", 0) == 1) { // Anchor Left + if (CVarGetInteger("gMinimapUseMargins", 0) != 0) { + X_Margins_Minimap = Left_MM_Margin; + }; + Matrix_Translate( + OTRGetDimensionFromLeftEdge((tempXOffset + (X_Margins_Minimap * 10) + tempX + + (CVarGetInteger("gMinimapPosX", 0) * 10)) / + 10.0f), + (R_COMPASS_OFFSET_Y + ((Y_Margins_Minimap * 10) * -1) - tempZ + + ((CVarGetInteger("gMinimapPosY", 0) * 10) * -1)) / + 10.0f, + 0.0f, MTXMODE_NEW); + } else if (CVarGetInteger("gMinimapPosType", 0) == 2) { // Anchor Right + if (CVarGetInteger("gMinimapUseMargins", 0) != 0) { + X_Margins_Minimap = Right_MM_Margin; + }; + Matrix_Translate( + OTRGetDimensionFromRightEdge((tempXOffset + (X_Margins_Minimap * 10) + tempX + + (CVarGetInteger("gMinimapPosX", 0) * 10)) / + 10.0f), + (R_COMPASS_OFFSET_Y + ((Y_Margins_Minimap * 10) * -1) - tempZ + + ((CVarGetInteger("gMinimapPosY", 0) * 10) * -1)) / + 10.0f, + 0.0f, MTXMODE_NEW); + } else if (CVarGetInteger("gMinimapPosType", 0) == 3) { // Anchor None + Matrix_Translate((tempXOffset + tempX + (CVarGetInteger("gMinimapPosX", 0) * 10) / 10.0f), + (R_COMPASS_OFFSET_Y + ((Y_Margins_Minimap * 10) * -1) - tempZ + + ((CVarGetInteger("gMinimapPosY", 0) * 10) * -1)) / + 10.0f, + 0.0f, MTXMODE_NEW); + } + } else { + Matrix_Translate( + OTRGetDimensionFromRightEdge((tempXOffset + (X_Margins_Minimap * 10) + tempX) / 10.0f), + (R_COMPASS_OFFSET_Y + ((Y_Margins_Minimap * 10) * -1) - tempZ) / 10.0f, 0.0f, MTXMODE_NEW); + } + Matrix_Scale(0.4f, 0.4f, 0.4f, MTXMODE_APPLY); + Matrix_RotateX(-1.6f, MTXMODE_APPLY); + tempX = ((0x7FFF - actor->shape.rot.y) / 0x400) * (CVarGetInteger("gMirroredWorld", 0) ? -1 : 1); + Matrix_RotateY(tempX / 10.0f, MTXMODE_APPLY); + gSPMatrix(OVERLAY_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), + G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + + gDPSetPrimColor(OVERLAY_DISP++, 0, 0xFF, playerColor.r, playerColor.g, playerColor.b, 255); + gSPDisplayList(OVERLAY_DISP++, gCompassArrowDL); + } + } + actor = actor->next; + } +#endif } CLOSE_DISPS(play->state.gfxCtx); diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 77e4617c3af..c58102701ad 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -21,6 +21,9 @@ #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/randomizer/randomizer_grotto.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define DO_ACTION_TEX_WIDTH() 48 #define DO_ACTION_TEX_HEIGHT() 16 @@ -1804,6 +1807,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); } + // TODO: Need this upstream - 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 = RetrieveGetItemIDFromItemID(itemID); if (getItemID != GI_MAX) { @@ -1949,15 +1957,20 @@ u8 Item_Give(PlayState* play, u8 item) { return Return_Item(item, MOD_NONE, ITEM_NONE); } else if ((item == ITEM_KEY_BOSS) || (item == ITEM_COMPASS) || (item == ITEM_DUNGEON_MAP)) { gSaveContext.inventory.dungeonItems[gSaveContext.mapIndex] |= gBitFlags[item - ITEM_KEY_BOSS]; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_GiveDungeonItem(gSaveContext.mapIndex, item); +#endif return Return_Item(item, MOD_NONE, ITEM_NONE); } else if (item == ITEM_KEY_SMALL) { if (gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] < 0) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] = 1; - return Return_Item(item, MOD_NONE, ITEM_NONE); } else { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]++; - return Return_Item(item, MOD_NONE, ITEM_NONE); } +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateKeyCount(gSaveContext.mapIndex, gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]); +#endif + return Return_Item(item, MOD_NONE, ITEM_NONE); } else if ((item == ITEM_QUIVER_30) || (item == ITEM_BOW)) { if (CUR_UPG_VALUE(UPG_QUIVER) == 0) { Inventory_ChangeUpgrade(UPG_QUIVER, 1); @@ -2262,6 +2275,16 @@ 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; + 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); + } + } return Return_Item(item, MOD_NONE, ITEM_NONE); } else if (item == ITEM_HEART_CONTAINER) { gSaveContext.healthCapacity += 0x10; 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 a2f2d241ce8..6f7f8ea940b 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 58f11a7f3c6..6cd9868c06d 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,9 @@ 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 || IS_BOSS_RUSH) { + // #region SOH [Co-op] + if (this->collider.base.acFlags & 2 || blueFireArrowHit || IS_BOSS_RUSH || 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 1f0c98f0424..957910362d0 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 cae8037c643..014cd8542bd 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_Firewall/z_bg_hidan_firewall.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Firewall/z_bg_hidan_firewall.c index 6c1ce1e8fa0..3350ea560fe 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Firewall/z_bg_hidan_firewall.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Firewall/z_bg_hidan_firewall.c @@ -6,6 +6,10 @@ #include "z_bg_hidan_firewall.h" #include "objects/object_hidan_objects/object_hidan_objects.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #define FLAGS 0 @@ -21,6 +25,8 @@ void BgHidanFirewall_Erupt(BgHidanFirewall* this, PlayState* play); void BgHidanFirewall_Collide(BgHidanFirewall* this, PlayState* play); void BgHidanFirewall_ColliderFollowPlayer(BgHidanFirewall* this, PlayState* play); +extern s16 gEnPartnerId; + const ActorInit Bg_Hidan_Firewall_InitVars = { ACTOR_BG_HIDAN_FIREWALL, ACTORCAT_BG, @@ -88,6 +94,20 @@ s32 BgHidanFirewall_CheckProximity(BgHidanFirewall* this, PlayState* play) { player = GET_PLAYER(play); func_8002DBD0(&this->actor, &distance, &player->actor.world.pos); +#ifdef ENABLE_REMOTE_CONTROL + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_ITEMACTION].head; + while (actor != NULL) { + if (gEnPartnerId == actor->id && Anchor_GetClientRoomIndex(actor->params - 3) == gPlayState->roomCtx.curRoom.num && Anchor_GetClientScene(actor->params - 3) == gPlayState->sceneNum) { + Vec3f actorDistance; + func_8002DBD0(&this->actor, &actorDistance, &actor->world.pos); + if (fabsf(actorDistance.x) < 100.0f && fabsf(actorDistance.z) < 120.0f) { + return 1; + } + } + actor = actor->next; + } +#endif + if (fabsf(distance.x) < 100.0f && fabsf(distance.z) < 120.0f) { return 1; } 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 c0ffd1c8bf9..c7911907c91 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 ee303a1787c..8bd0e78e7ce 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 36666ed1551..76a0d0ca7af 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 @@ -332,10 +332,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 06c2381e923..3d1fdcd3af1 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 af09141fde5..6311d8aa0f8 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 11241004a2a..11580b62c56 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 @@ -134,8 +134,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 08f8e36b51b..73251148eb6 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 9a38cfa5efa..b8bae4968ea 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); func_80078884(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 b7c988cdab7..da655776e67 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_Boss_Ganon2/z_boss_ganon2.c b/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c index cb00dbee83d..771bcf33814 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c @@ -8,6 +8,9 @@ #include "objects/object_geff/object_geff.h" #include "soh/frame_interpolation.h" #include "soh/Enhancements/boss-rush/BossRush.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #include @@ -1691,6 +1694,9 @@ void func_8090120C(BossGanon2* this, PlayState* play) { gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME; BossRush_HandleCompleteBoss(play); gSaveContext.sohStats.gameComplete = true; +#ifdef ENABLE_REMOTE_CONTROL + Anchor_GameComplete(); +#endif this->unk_39E = Play_CreateSubCamera(play); Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_WAIT); Play_ChangeCameraStatus(play, this->unk_39E, CAM_STAT_ACTIVE); 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 290e809f6bd..c1fd9eed618 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); @@ -640,6 +646,12 @@ void DoorShutter_Update(Actor* thisx, PlayState* play) { if (!(player->stateFlags1 & 0x100004C0) || (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 f0442821a5b..ef7a4c7876e 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); } @@ -230,6 +236,12 @@ 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_Md/z_en_md.c b/soh/src/overlays/actors/ovl_En_Md/z_en_md.c index 14d9cbb7bec..6860f244231 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 @@ -786,6 +786,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_KOKIRI_FOREST)) { + 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 8c4f78a0cc5..954409b5727 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 8780ab10caa..24c48c1c460 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) @@ -80,6 +83,18 @@ void EnPartner_Init(Actor* thisx, PlayState* play) { this->outerColor.b = 0.0f; this->outerColor.a = 255.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; + this->innerColor.r = MIN(color.r + 100.0f, 255.0f); + this->innerColor.g = MIN(color.g + 100.0f, 255.0f); + this->innerColor.b = MIN(color.b + 100.0f, 255.0f); + } +#endif + this->usedItemButton = 0xFF; Collider_InitCylinder(play, &this->collider); @@ -131,11 +146,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); } @@ -578,6 +601,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 49e88f90345..62c9263808b 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 @@ -198,6 +198,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 0a6825c0e0e..5a8537101cb 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 @@ -915,6 +915,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 abb65c1914e..e6a8978ef91 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..9fbeb9130ca 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; 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 497935a6958..ad14cbcd260 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -30,6 +30,9 @@ #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/randomizer/randomizer_grotto.h" #include "soh/frame_interpolation.h" +#ifdef ENABLE_REMOTE_CONTROL +#include "soh/Enhancements/game-interactor/GameInteractor_Anchor.h" +#endif #include #include @@ -5087,6 +5090,9 @@ s32 func_8083B040(Player* this, PlayState* play) { ((this->exchangeItemId != EXCH_ITEM_BEAN) || (this->itemAction == PLAYER_IA_MAGIC_BEAN))) { if (this->exchangeItemId == EXCH_ITEM_BEAN) { Inventory_ChangeAmmo(ITEM_BEAN, -1); +#ifdef ENABLE_REMOTE_CONTROL + Anchor_UpdateBeansCount(AMMO(ITEM_BEAN)); +#endif func_80835DE4(play, this, func_8084279C, 0); this->stateFlags1 |= PLAYER_STATE1_IN_CUTSCENE; this->unk_850 = 0x50;