From 09ca2ad383c84212d3c60661d1c6f8673a64cb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philip=20Dub=C3=A9?= <159546+serprex@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:48:47 +0000 Subject: [PATCH] Maintain correct position/camera for Master Sword cutscene skip (#7184) --- .../Story/SkipPullMasterSword.cpp | 106 ++++++++++++++++++ .../TimeSavers/timesaver_hook_handlers.cpp | 18 --- 2 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipPullMasterSword.cpp diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipPullMasterSword.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipPullMasterSword.cpp new file mode 100644 index 0000000000..6bb054b96c --- /dev/null +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipPullMasterSword.cpp @@ -0,0 +1,106 @@ +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "z64.h" +#include "z64save.h" +#include "macros.h" +#include "functions.h" +extern SaveContext gSaveContext; +extern PlayState* gPlayState; +extern void Player_StartMode_Idle(PlayState* play, Player* player); +extern Vec3f* Camera_Vec3fVecSphGeoAdd(Vec3f* dest, Vec3f* a, VecSph* b); +} + +// Where cutscene leaves Link after Sheik appears +static const Vec3f sMasterSwordEndPos = { 0.0f, 40.0f, 214.0f }; +static const int16_t sMasterSwordEndYaw = 30037; + +// Set flag before giving medallion. Setting flag queues the medallion as a get-item, +// and giving it takes it back out of the queue, so Link doesn't hold it up. +static void SkipPullMasterSword_GiveLightMedallion() { + Flags_SetEventChkInf(EVENTCHKINF_TIME_TRAVELED_TO_ADULT); + if (GameInteractor_Should(VB_GIVE_ITEM_LIGHT_MEDALLION, true)) { + Item_Give(gPlayState, ITEM_MEDALLION_LIGHT); + } +} + +// Outside rando, the skip leaves EVENTCHKINF_TIME_TRAVELED_TO_ADULT unset. +// Like vanilla, it gets set once adult Link reaches the Temple of Time. +static bool SkipPullMasterSword_IsFirstAdultArrival() { + return gPlayState->sceneNum == SCENE_TEMPLE_OF_TIME && LINK_IS_ADULT && + !Flags_GetEventChkInf(EVENTCHKINF_TIME_TRAVELED_TO_ADULT); +} + +// Runs after the scene sets up its camera, which uses the time travel spawn's fixed camera +static void SkipPullMasterSword_OnSceneSpawnActors() { + if (!SkipPullMasterSword_IsFirstAdultArrival()) { + return; + } + SkipPullMasterSword_GiveLightMedallion(); + + Player* player = GET_PLAYER(gPlayState); + Camera* camera = Play_GetCamera(gPlayState, CAM_ID_MAIN); + + // Coming back as an adult uses a fixed camera aimed at the pedestal. + // Switch to the normal camera that follows Link. + Camera_InitPlayerSettings(camera, player); + Camera_RequestMode(camera, CAM_MODE_NORMAL); + + // The normal camera starts behind Link. Start it in front of him, + // facing him. It moves back behind him on its own. + VecSph offset; + offset.r = camera->dist; + offset.pitch = camera->inputDir.x; + offset.yaw = sMasterSwordEndYaw; + Camera_Vec3fVecSphGeoAdd(&camera->eyeNext, &camera->at, &offset); + camera->eye = camera->eyeNext; + camera->inputDir.y = BINANG_ROT180(sMasterSwordEndYaw); + camera->camDir = camera->inputDir; +} + +void RegisterSkipPullMasterSword() { + COND_VB_SHOULD(VB_PLAY_PULL_MASTER_SWORD_CS, + CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO), { + if (!Flags_GetEventChkInf(EVENTCHKINF_PULLED_MASTER_SWORD_FROM_PEDESTAL)) { + // Normally, these would be done in the cutscene, but we're skipping it + Flags_SetEventChkInf(EVENTCHKINF_PULLED_MASTER_SWORD_FROM_PEDESTAL); + Flags_SetEventChkInf(EVENTCHKINF_ENTERED_MASTER_SWORD_CHAMBER); + Flags_SetEventChkInf(EVENTCHKINF_SHEIK_SPAWNED_AT_MASTER_SWORD_PEDESTAL); + if (IS_RANDO) { + SkipPullMasterSword_GiveLightMedallion(); + } else { + gSaveContext.dayTime = gSaveContext.skyboxTime = 0x8000; + } + } + *should = false; + }); + + // After the skip, Link appears at pedestal and raises sword. Move to where cutscene ends instead + COND_VB_SHOULD(VB_EXECUTE_PLAYER_STARTMODE_FUNC, + !IS_RANDO && CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), 0), { + int32_t startMode = va_arg(args, int32_t); + + // Only change the first trip to adult, not later trips through time + if (startMode == PLAYER_START_MODE_TIME_TRAVEL && SkipPullMasterSword_IsFirstAdultArrival()) { + Player* player = GET_PLAYER(gPlayState); + player->actor.world.pos = sMasterSwordEndPos; + player->actor.prevPos = sMasterSwordEndPos; + player->actor.home.pos = sMasterSwordEndPos; + player->actor.world.rot.y = sMasterSwordEndYaw; + player->actor.shape.rot.y = sMasterSwordEndYaw; + player->yaw = sMasterSwordEndYaw; + + // Just stand there instead of raising the sword + Player_StartMode_Idle(gPlayState, player); + *should = false; + } + }); + + COND_HOOK(OnSceneSpawnActors, !IS_RANDO && CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), 0), + SkipPullMasterSword_OnSceneSpawnActors); +} + +static RegisterShipInitFunc initFunc(RegisterSkipPullMasterSword, + { CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), "IS_RANDO" }); diff --git a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp index 29ed3c4dd4..e69c9e9a41 100644 --- a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp +++ b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp @@ -529,24 +529,6 @@ void TimeSaverOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_li *should = false; } break; - case VB_PLAY_PULL_MASTER_SWORD_CS: - if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) { - if (!Flags_GetEventChkInf(EVENTCHKINF_PULLED_MASTER_SWORD_FROM_PEDESTAL)) { - // Normally, these would be done in the cutscene, but we're skipping it - Flags_SetEventChkInf(EVENTCHKINF_PULLED_MASTER_SWORD_FROM_PEDESTAL); - Flags_SetEventChkInf(EVENTCHKINF_ENTERED_MASTER_SWORD_CHAMBER); - Flags_SetEventChkInf(EVENTCHKINF_SHEIK_SPAWNED_AT_MASTER_SWORD_PEDESTAL); - Flags_SetEventChkInf(EVENTCHKINF_TIME_TRAVELED_TO_ADULT); - if (!IS_RANDO) { - gSaveContext.dayTime = gSaveContext.skyboxTime = 0x8000; - } - if (GameInteractor_Should(VB_GIVE_ITEM_LIGHT_MEDALLION, true)) { - Item_Give(gPlayState, ITEM_MEDALLION_LIGHT); - } - } - *should = false; - } - break; case VB_PLAY_DISPEL_BARRIER_CS: { if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story"), IS_RANDO)) { static s16 trialEntrances[] = {