From 6a3c9b760456c27ed8a0ac9ffdb580fdf105829c Mon Sep 17 00:00:00 2001 From: David Racine Date: Mon, 17 Aug 2026 13:22:34 -0400 Subject: [PATCH] Add a Skip Warp Cutscenes enhancement (#7059) Warp song cutscenes were skipped for randomizer players whether they wanted it or not: Demo_Kankyo cut the departure animation short from a hardcoded IS_RANDO branch, and entrance rando cleared respawnFlag so the arrival never played. There was no way to get the vanilla warp back, and no way for a vanilla playthrough to skip it. Drop both and put the behavior behind a Skip Warp Cutscenes toggle, defaulting on in randomizer so existing rando warps are unchanged. It kills the departure actor as it spawns and puts the arrival spawn mode back to IDLE, which covers both halves of the warp. Entrance rando's destination override moves off Demo_Kankyo's update onto a new OnWarpSongLeave hook fired from Environment_WarpSongLeave. That is where every warp path -- the cutscene, the skip toggle, and the spawn failure fallback -- commits its destination, so the override no longer depends on the departure actor being alive to see it, and a warp song shuffled onto a grotto return keeps its grotto respawn. Co-Authored-By: Claude Opus 5 --- .../Enhancements/QoL/SkipWarpAnimation.cpp | 37 +++++++++++++++++++ .../GameInteractor_HookTable.h | 1 + .../game-interactor/GameInteractor_Hooks.cpp | 4 ++ .../game-interactor/GameInteractor_Hooks.h | 1 + .../Enhancements/randomizer/hook_handlers.cpp | 13 +++++-- .../randomizer/randomizer_entrance.c | 3 -- soh/soh/SohGui/SohMenuEnhancements.cpp | 4 ++ soh/src/code/z_kankyo.c | 4 ++ .../actors/ovl_Demo_Kankyo/z_demo_kankyo.c | 8 +--- 9 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 soh/soh/Enhancements/QoL/SkipWarpAnimation.cpp diff --git a/soh/soh/Enhancements/QoL/SkipWarpAnimation.cpp b/soh/soh/Enhancements/QoL/SkipWarpAnimation.cpp new file mode 100644 index 0000000000..23c6198dca --- /dev/null +++ b/soh/soh/Enhancements/QoL/SkipWarpAnimation.cpp @@ -0,0 +1,37 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "functions.h" +#include "macros.h" +#include "variables.h" +#include "z64save.h" + +extern PlayState* gPlayState; +} + +#define CVAR_SKIP_WARP_NAME CVAR_ENHANCEMENT("WarpSongSkipAnimation") +#define CVAR_SKIP_WARP_VALUE CVarGetInteger(CVAR_SKIP_WARP_NAME, IS_RANDO) + +// DEMOKANKYO_WARP_OUT = 0x0F; the warp-song departure animation spawned by Player_Action_8084E3C4 +static constexpr s16 DEMOKANKYO_WARP_OUT_PARAM = 0x0F; + +static void RegisterSkipWarpHooks() { + COND_ID_HOOK(ShouldActorInit, ACTOR_DEMO_KANKYO, CVAR_SKIP_WARP_VALUE, [](void* refActor, bool* should) { + Actor* actor = static_cast(refActor); + if (actor->params != DEMOKANKYO_WARP_OUT_PARAM) { + return; + } + // Leaves right away, and settles the destination: entrance rando redirects from inside it. + Environment_WarpSongLeave(gPlayState); + // Switch arrival spawn mode from WARP_SONG to IDLE so DEMO_KANKYO WARP_IN is never spawned. + // Another mode means something else owns the arrival (a rando grotto return), with no cutscene. + RespawnData* respawn = &gSaveContext.respawn[RESPAWN_MODE_RETURN]; + if (((respawn->playerParams & 0xF00) >> 8) == PLAYER_START_MODE_WARP_SONG) { + respawn->playerParams = (respawn->playerParams & ~0xF00) | (PLAYER_START_MODE_IDLE << 8); + } + *should = false; + }); +} + +static RegisterShipInitFunc initFunc(RegisterSkipWarpHooks, { CVAR_SKIP_WARP_NAME, "IS_RANDO" }); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index a86375d413..a3ee672f99 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -29,6 +29,7 @@ DEFINE_HOOK(OnPlayerUpdate, ()); DEFINE_HOOK(OnSetDoAction, (uint16_t action)); DEFINE_HOOK(OnPlayerSfx, (u16 sfxId)); DEFINE_HOOK(OnOcarinaSongAction, ()); +DEFINE_HOOK(OnWarpSongLeave, ()); DEFINE_HOOK(OnOcarinaNote, (uint8_t note, float modulator, int8_t bend)); DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price)); DEFINE_HOOK(OnDungeonKeyUsed, (uint16_t mapIndex)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 711186c7ad..3b5d1f74f4 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -114,6 +114,10 @@ void GameInteractor_ExecuteOnOcarinaSongAction() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnWarpSongLeave() { + GameInteractor::Instance->ExecuteHooks(); +} + void GameInteractor_ExecuteOnOcarinaNote(uint8_t note, float modulator, int8_t bend) { GameInteractor::Instance->ExecuteHooks(note, modulator, bend); } diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 8f54e288a6..7ce3037b88 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -31,6 +31,7 @@ void GameInteractor_ExecuteOnPlayerUpdate(); void GameInteractor_ExecuteOnSetDoAction(uint16_t action); void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId); void GameInteractor_ExecuteOnOcarinaSongAction(); +void GameInteractor_ExecuteOnWarpSongLeave(); void GameInteractor_ExecuteOnOcarinaNote(uint8_t note, float modulator, int8_t bend); bool GameInteractor_ShouldActorInit(void* actor); void GameInteractor_ExecuteOnActorInit(void* actor); diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index fc4c7b9068..da334769be 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -2639,10 +2639,12 @@ void RandomizerOnActorUpdateHandler(void* refActor) { Flags_UnsetRandomizerInf(RAND_INF_SPIRIT_BIG_MIRROR_STATUE_TURNED); } } +} - // In ER, override the warp song locations. Also removes the warp song cutscene - if (RAND_GET_OPTION(RSK_SHUFFLE_ENTRANCES) && actor->id == ACTOR_DEMO_KANKYO && - actor->params == 0x000F) { // Warp Song particles +// In ER, warp songs lead to their shuffled entrance rather than their warp pad. Every warp path commits its +// destination through Environment_WarpSongLeave, so that is the one place the override has to happen. +void RandomizerOnWarpSongLeaveHandler() { + if (RAND_GET_OPTION(RSK_SHUFFLE_ENTRANCES)) { Entrance_SetWarpSongEntrance(); } } @@ -2809,6 +2811,7 @@ static void RandomizerRegisterHooks() { static uint32_t afterSceneCommandsHook = 0; static uint32_t onActorInitHook = 0; static uint32_t onActorUpdateHook = 0; + static uint32_t onWarpSongLeaveHook = 0; static uint32_t onPlayerUpdateHook = 0; static uint32_t onGameFrameUpdateHook = 0; static uint32_t onSceneSpawnActorsHook = 0; @@ -2841,6 +2844,7 @@ static void RandomizerRegisterHooks() { GameInteractor::Instance->UnregisterGameHook(afterSceneCommandsHook); GameInteractor::Instance->UnregisterGameHook(onActorInitHook); GameInteractor::Instance->UnregisterGameHook(onActorUpdateHook); + GameInteractor::Instance->UnregisterGameHook(onWarpSongLeaveHook); GameInteractor::Instance->UnregisterGameHook(onPlayerUpdateHook); GameInteractor::Instance->UnregisterGameHook(onGameFrameUpdateHook); GameInteractor::Instance->UnregisterGameHook(onSceneSpawnActorsHook); @@ -2859,6 +2863,7 @@ static void RandomizerRegisterHooks() { afterSceneCommandsHook = 0; onActorInitHook = 0; onActorUpdateHook = 0; + onWarpSongLeaveHook = 0; onPlayerUpdateHook = 0; onGameFrameUpdateHook = 0; onSceneSpawnActorsHook = 0; @@ -2900,6 +2905,8 @@ static void RandomizerRegisterHooks() { GameInteractor::Instance->RegisterGameHook(RandomizerOnActorInitHandler); onActorUpdateHook = GameInteractor::Instance->RegisterGameHook(RandomizerOnActorUpdateHandler); + onWarpSongLeaveHook = GameInteractor::Instance->RegisterGameHook( + RandomizerOnWarpSongLeaveHandler); onPlayerUpdateHook = GameInteractor::Instance->RegisterGameHook(RandomizerOnPlayerUpdateHandler); onGameFrameUpdateHook = GameInteractor::Instance->RegisterGameHook( diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index 3e1d5486ca..34302bf72c 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -464,9 +464,6 @@ void Entrance_SetWarpSongEntrance(void) { if (gSaveContext.gameMode != GAMEMODE_NORMAL) { // During DHWW the cutscene must play at the destination gSaveContext.respawnFlag = -3; - } else if (gSaveContext.respawnFlag == -3) { - // Unset Zoneout Type -3 to avoid cutscene at destination (technically it's not needed) - gSaveContext.respawnFlag = 0; } } diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index 5672a45f79..a11e2679df 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -453,6 +453,10 @@ void SohMenu::AddMenuEnhancements() { AddWidget(path, "Skip Song Cutscenes", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.LearnSong")) .Options(CheckboxOptions().DefaultValue(IS_RANDO)); + AddWidget(path, "Skip Warp Cutscenes", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("WarpSongSkipAnimation")) + .Options(CheckboxOptions().DefaultValue(IS_RANDO).Tooltip( + "Warp songs skip the departure and arrival cutscenes, fading immediately to the destination.")); AddWidget(path, "Skip Boss Introductions", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro")) .Options(CheckboxOptions().DefaultValue(IS_RANDO)); diff --git a/soh/src/code/z_kankyo.c b/soh/src/code/z_kankyo.c index dc25416a67..a790176034 100644 --- a/soh/src/code/z_kankyo.c +++ b/soh/src/code/z_kankyo.c @@ -7,6 +7,7 @@ #include "soh/OTRGlobals.h" #include "soh/ResourceManagerHelpers.h" #include "soh/Enhancements/savestate_serialize.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" typedef enum { /* 0 */ LENS_FLARE_CIRCLE0, @@ -2534,6 +2535,9 @@ void Environment_WarpSongLeave(PlayState* play) { play->transitionType = TRANS_TYPE_FADE_WHITE; gSaveContext.nextTransitionType = TRANS_TYPE_FADE_WHITE; + // Where entrance rando redirects the warp: every warp song path decides its destination here. + GameInteractor_ExecuteOnWarpSongLeave(); + switch (play->nextEntranceIndex) { case ENTR_DEATH_MOUNTAIN_CRATER_UPPER_EXIT: Flags_SetEventChkInf(EVENTCHKINF_ENTERED_DEATH_MOUNTAIN_CRATER); diff --git a/soh/src/overlays/actors/ovl_Demo_Kankyo/z_demo_kankyo.c b/soh/src/overlays/actors/ovl_Demo_Kankyo/z_demo_kankyo.c index a14d591d35..971aa8d07c 100644 --- a/soh/src/overlays/actors/ovl_Demo_Kankyo/z_demo_kankyo.c +++ b/soh/src/overlays/actors/ovl_Demo_Kankyo/z_demo_kankyo.c @@ -794,13 +794,7 @@ void DemoKankyo_DrawWarpSparkles(Actor* thisx, PlayState* play) { this->unk_150[i].unk_0.y = (s16)((Rand_ZeroOne() - 0.5f) * 16.0f * temp_f22); this->unk_150[i].unk_0.z = (s16)((Rand_ZeroOne() - 0.5f) * 16.0f * temp_f22); this->unk_150[i].unk_23 = 0; - - // Skip the first part of warp song cutscenes in rando - if (IS_RANDO && this->actor.params == DEMOKANKYO_WARP_OUT) { - this->unk_150[i].unk_22 = 2; - } else { - this->unk_150[i].unk_22++; - } + this->unk_150[i].unk_22++; case 1: if (this->actor.params == DEMOKANKYO_WARP_OUT) {