diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index f0ab0363b..59e322ed9 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -149,9 +149,11 @@ public: DEFINE_HOOK(OnTransitionEnd, void(int16_t sceneNum)); DEFINE_HOOK(OnSceneInit, void(int16_t sceneNum)); DEFINE_HOOK(OnPlayerUpdate, void()); + DEFINE_HOOK(OnOcarinaSongAction, void()); + DEFINE_HOOK(OnActorUpdate, void(void* actor)); DEFINE_HOOK(OnPlayerBonk, void()); - + DEFINE_HOOK(OnSaveFile, void(int32_t fileNum)); DEFINE_HOOK(OnLoadFile, void(int32_t fileNum)); DEFINE_HOOK(OnDeleteFile, void(int32_t fileNum)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 7bc85e017..90074f4ff 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -34,6 +34,10 @@ void GameInteractor_ExecuteOnPlayerUpdate() { GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerUpdate>(); } +void GameInteractor_ExecuteOnOcarinaSongAction() { + GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaSongAction>(); +} + void GameInteractor_ExecuteOnActorUpdate(void* actor) { GameInteractor::Instance->ExecuteHooks<GameInteractor::OnActorUpdate>(actor); } diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 6c0a003b1..7c9661cf0 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -9,8 +9,10 @@ extern "C" void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry); extern "C" void GameInteractor_ExecuteOnTransitionEndHooks(int16_t sceneNum); extern "C" void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum); extern "C" void GameInteractor_ExecuteOnPlayerUpdate(); +extern "C" void GameInteractor_ExecuteOnOcarinaSongAction(); extern "C" void GameInteractor_ExecuteOnActorUpdate(void* actor); extern "C" void GameInteractor_ExecuteOnPlayerBonk(); +extern "C" void GameInteractor_ExecuteOnOcarinaSongAction(); // MARK: - Save Files extern "C" void GameInteractor_ExecuteOnSaveFile(int32_t fileNum); diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index 6dc595f00..cffd00ffe 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -6,14 +6,28 @@ extern "C" { #include <z64.h> #include "macros.h" +#include "functions.h" #include "variables.h" #include "functions.h" extern SaveContext gSaveContext; extern PlayState* gPlayState; +extern void Play_PerformSave(PlayState* play); +extern s32 Health_ChangeBy(PlayState* play, s16 healthChange); +extern void Rupees_ChangeBy(s16 rupeeChange); +extern void Inventory_ChangeEquipment(s16 equipment, u16 value); } bool performDelayedSave = false; bool performSave = false; +// TODO: When there's more uses of something like this, create a new GI::RawAction? +void ReloadSceneTogglingLinkAge() { + gPlayState->nextEntranceIndex = gSaveContext.entranceIndex; + gPlayState->sceneLoadFlag = 0x14; + gPlayState->fadeTransition = 11; + gSaveContext.nextTransitionType = 11; + gPlayState->linkAgeOnLoad ^= 1; // toggle linkAgeOnLoad +} + void RegisterInfiniteMoney() { GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameFrameUpdate>([]() { if (CVarGetInteger("gInfiniteMoney", 0) != 0) { @@ -155,11 +169,7 @@ void RegisterSwitchAge() { playerPos = GET_PLAYER(gPlayState)->actor.world.pos; playerYaw = GET_PLAYER(gPlayState)->actor.shape.rot.y; - gPlayState->nextEntranceIndex = gSaveContext.entranceIndex; - gPlayState->sceneLoadFlag = 0x14; - gPlayState->fadeTransition = 11; - gSaveContext.nextTransitionType = 11; - gPlayState->linkAgeOnLoad ^= 1; + ReloadSceneTogglingLinkAge(); warped = true; } @@ -172,6 +182,58 @@ void RegisterSwitchAge() { }); } +/// Switches Link's age and respawns him at the last entrance he entered. +void RegisterOcarinaTimeTravel() { + GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameFrameUpdate>([]() { + if (!gPlayState) return; + + // For the gTimeTravel: Don't give child Link a Kokiri Sword if we don't have one + if (LINK_AGE_IN_YEARS == 5 && CVarGetInteger("gTimeTravel", 0)) { + uint32_t kokiriSwordBitMask = 1 << 0; + if (!(gSaveContext.inventory.equipment & kokiriSwordBitMask)) { + Player* player = GET_PLAYER(gPlayState); + player->currentSwordItemId = ITEM_NONE; + gSaveContext.equips.buttonItems[0] = ITEM_NONE; + Inventory_ChangeEquipment(EQUIP_SWORD, PLAYER_SWORD_NONE); + } + } + + // Switches Link's age and respawns him at the last entrance he entered. + if (CVarGetInteger("gTimeTravel", 0) && CVarGetInteger("gSwitchTimeline", 0)) { + CVarSetInteger("gSwitchTimeline", 0); + ReloadSceneTogglingLinkAge(); + } + }); + + GameInteractor::Instance->RegisterGameHook<GameInteractor::OnOcarinaSongAction>([]() { + if (!gPlayState) { + return; + } + + Actor* player = &GET_PLAYER(gPlayState)->actor; + Actor* nearbyTimeBlockEmpty = Actor_FindNearby(gPlayState, player, ACTOR_OBJ_WARP2BLOCK, ACTORCAT_ITEMACTION, 300.0f); + Actor* nearbyTimeBlock = Actor_FindNearby(gPlayState, player, ACTOR_OBJ_TIMEBLOCK, ACTORCAT_ITEMACTION, 300.0f); + Actor* nearbyOcarinaSpot = Actor_FindNearby(gPlayState, player, ACTOR_EN_OKARINA_TAG, ACTORCAT_PROP, 120.0f); + Actor* nearbyDoorOfTime = Actor_FindNearby(gPlayState, player, ACTOR_DOOR_TOKI, ACTORCAT_BG, 500.0f); + Actor* nearbyFrogs = Actor_FindNearby(gPlayState, player, ACTOR_EN_FR, ACTORCAT_NPC, 300.0f); + uint8_t hasMasterSword = (gBitFlags[ITEM_SWORD_MASTER - ITEM_SWORD_KOKIRI] << gEquipShifts[EQUIP_SWORD]) & gSaveContext.inventory.equipment; + uint8_t hasOcarinaOfTime = (INV_CONTENT(ITEM_OCARINA_TIME) == ITEM_OCARINA_TIME); + // If TimeTravel + Player have the Ocarina of Time + Have Master Sword + is in proper range + // TODO: Once Swordless Adult is fixed: Remove the Master Sword check + if (CVarGetInteger("gTimeTravel", 0) && hasOcarinaOfTime && hasMasterSword && + gPlayState->msgCtx.lastPlayedSong == OCARINA_SONG_TIME && !nearbyTimeBlockEmpty && !nearbyTimeBlock && + !nearbyOcarinaSpot && !nearbyFrogs) { + if (gSaveContext.n64ddFlag) { + CVarSetInteger("gSwitchTimeline", 1); + } else if (!gSaveContext.n64ddFlag && !nearbyDoorOfTime) { + // This check is made for when Link is learning the Song Of Time in a vanilla save file that load a + // Temple of Time scene where the only object present is the Door of Time + CVarSetInteger("gSwitchTimeline", 1); + } + } + }); +} + void AutoSave(GetItemEntry itemEntry) { u8 item = itemEntry.itemId; // Don't autosave immediately after buying items from shops to prevent getting them for free! @@ -386,6 +448,7 @@ void InitMods() { RegisterUnrestrictedItems(); RegisterFreezeTime(); RegisterSwitchAge(); + RegisterOcarinaTimeTravel(); RegisterAutoSave(); RegisterRupeeDash(); RegisterHyperBosses(); diff --git a/soh/soh/GameMenuBar.cpp b/soh/soh/GameMenuBar.cpp index ab2a20b5c..075ea4134 100644 --- a/soh/soh/GameMenuBar.cpp +++ b/soh/soh/GameMenuBar.cpp @@ -356,6 +356,15 @@ namespace GameMenuBar { UIWidgets::Tooltip("Greatly decreases cast time of Farore's Wind magic spell."); UIWidgets::PaddedEnhancementCheckbox("Dampe Appears All Night", "gDampeAllNight", true, false); UIWidgets::Tooltip("Makes Dampe appear anytime during it's night, not just his usual working hours."); + UIWidgets::PaddedEnhancementCheckbox("Time Travel with the Song of Time", "gTimeTravel", true, false); + UIWidgets::Tooltip("Allows Link to freely change age by playing the Song of Time.\n" + "Time Blocks can still be used properly.\n\n" + "Requirements:\n" + "- Obtained the Ocarina of Time\n" + "- Obtained the Song of Time\n" + "- Obtained the Master Sword\n" + "- Not within range of Time Block\n" + "- Not within range of Ocarina playing spots"); ImGui::EndMenu(); } diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index ec3eff08d..1a8c777a2 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -2557,6 +2557,7 @@ void Message_DrawMain(PlayState* play, Gfx** p) { osSyncPrintf(VT_RST); osSyncPrintf("→ OCARINA_MODE=%d\n", play->msgCtx.ocarinaMode); } + GameInteractor_ExecuteOnOcarinaSongAction(); } break; case MSGMODE_DISPLAY_SONG_PLAYED: