mirror of
https://github.com/HarbourMasters/Shipwright
synced 2026-08-27 00:44:04 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Actor*>(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" });
|
||||
@@ -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));
|
||||
|
||||
@@ -114,6 +114,10 @@ void GameInteractor_ExecuteOnOcarinaSongAction() {
|
||||
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaSongAction>();
|
||||
}
|
||||
|
||||
void GameInteractor_ExecuteOnWarpSongLeave() {
|
||||
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnWarpSongLeave>();
|
||||
}
|
||||
|
||||
void GameInteractor_ExecuteOnOcarinaNote(uint8_t note, float modulator, int8_t bend) {
|
||||
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaNote>(note, modulator, bend);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<GameInteractor::AfterSceneCommands>(afterSceneCommandsHook);
|
||||
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorInit>(onActorInitHook);
|
||||
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorUpdate>(onActorUpdateHook);
|
||||
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnWarpSongLeave>(onWarpSongLeaveHook);
|
||||
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnPlayerUpdate>(onPlayerUpdateHook);
|
||||
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnGameFrameUpdate>(onGameFrameUpdateHook);
|
||||
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneSpawnActors>(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<GameInteractor::OnActorInit>(RandomizerOnActorInitHandler);
|
||||
onActorUpdateHook =
|
||||
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorUpdate>(RandomizerOnActorUpdateHandler);
|
||||
onWarpSongLeaveHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnWarpSongLeave>(
|
||||
RandomizerOnWarpSongLeaveHandler);
|
||||
onPlayerUpdateHook =
|
||||
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPlayerUpdate>(RandomizerOnPlayerUpdateHandler);
|
||||
onGameFrameUpdateHook = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameFrameUpdate>(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user