summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp102
-rw-r--r--soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp3
-rw-r--r--soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp32
-rw-r--r--soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp7
-rw-r--r--soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h41
-rw-r--r--soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c22
-rw-r--r--soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c6
7 files changed, 190 insertions, 23 deletions
diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp
new file mode 100644
index 000000000..1f634a842
--- /dev/null
+++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp
@@ -0,0 +1,102 @@
+#include <algorithm>
+
+#include <libultraship/bridge/consolevariablebridge.h>
+
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/ShipInit.hpp"
+
+extern "C" {
+#include <objects/object_ganon2/object_ganon2.h>
+#include <objects/object_ganon_anime3/object_ganon_anime3.h>
+#include "src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.h"
+#include "src/overlays/actors/ovl_En_Zl3/z_en_zl3.h"
+extern PlayState* gPlayState;
+void BossGanon2_SetObjectSegment(BossGanon2* ganon, PlayState* play, s32 objectId, u8 setRSPSegment);
+}
+
+// Ganon's intro state where he knocks the sword out of Link's hand
+constexpr s16 GANON_KNOCK_SWORD_STATE = 23;
+// The timer is not reset going into that state. It has counted since Ganon landed and started flailing.
+constexpr u32 GANON_KNOCK_SWORD_TIMER = 215;
+
+#define CVAR_NAME CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro")
+#define CVAR_VALUE CVarGetInteger(CVAR_NAME, IS_RANDO)
+
+void RegisterGanonIntros() {
+ COND_HOOK(OnSceneInit, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, [](int16_t sceneNum) {
+ if (sceneNum == SCENE_GANONDORF_BOSS) {
+ Flags_SetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE);
+ }
+ });
+
+ COND_VB_SHOULD(VB_PLAY_GANONDORF_INTRO_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; });
+
+ // Skip Ganon rising from the rubble, transforming and landing. Skip to knocking sword away.
+ COND_VB_SHOULD(VB_PLAY_GANON_INTRO_CS, CVAR_VALUE, {
+ BossGanon2* ganon = va_arg(args, BossGanon2*);
+ EnZl3* zelda = va_arg(args, EnZl3*);
+ Player* player = GET_PLAYER(gPlayState);
+
+ // The rubble watches unk_314: pieces fly off when Ganondorf rises (1),
+ // then get removed or become breakable rubble once he floats (2).
+ // Run their update once for each, with Ganondorf where he bursts out, so they push away from him.
+ ganon->actor.world.pos.x = ganon->actor.world.pos.z = -200.0f;
+ for (u8 mode : { 1, 2 }) {
+ ganon->unk_314 = mode;
+ for (Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_PROP].head; actor != NULL;
+ actor = actor->next) {
+ if (actor->id == ACTOR_DEMO_GJ && actor->update != NULL) {
+ actor->update(actor, gPlayState);
+ }
+ }
+ }
+ // Set when he finishes transforming. Stops the smoke.
+ ganon->unk_314 = 3;
+
+ // Swap Ganondorf's skeleton for Ganon's
+ BossGanon2_SetObjectSegment(ganon, gPlayState, OBJECT_GANON2, false);
+ SkelAnime_Free(&ganon->skelAnime, gPlayState);
+ SkelAnime_InitFlex(gPlayState, &ganon->skelAnime, (FlexSkeletonHeader*)gGanonSkel, NULL, NULL, NULL, 0);
+ BossGanon2_SetObjectSegment(ganon, gPlayState, OBJECT_GANON_ANIME3, false);
+ Animation_PlayOnce(&ganon->skelAnime, (AnimationHeader*)gGanonUncurlAndFlailAnim);
+ ganon->skelAnime.curFrame = std::min((float)GANON_KNOCK_SWORD_TIMER, ganon->skelAnime.endFrame);
+
+ ganon->actor.world.pos.x = 50.0f;
+ ganon->actor.world.pos.y = 1099.0f;
+ ganon->actor.world.pos.z = -200.0f;
+ ganon->actor.world.rot.y = 0x4000;
+ ganon->actor.velocity.y = 0.0f;
+ ganon->actor.shape.yOffset = 7000.0f;
+
+ // Values the skipped states leave behind
+ ganon->unk_337 = 2;
+ ganon->unk_336 = 2;
+ ganon->unk_324 = 255.0f;
+ ganon->unk_228 = 1.0f;
+ ganon->unk_224 = 0.0f;
+ ganon->unk_30C = 0.0f;
+ ganon->unk_394 = 0.0f;
+ gPlayState->envCtx.unk_D8 = 0.0f;
+ gPlayState->envCtx.unk_BE = gPlayState->envCtx.unk_BD = 0;
+
+ player->actor.world.pos.x = 250.0f;
+ player->actor.world.pos.y = 1086.0f;
+ player->actor.world.pos.z = -266.0f;
+ player->actor.shape.rot.y = -0x4000;
+ Player_SetCsActionWithHaltedActors(gPlayState, &ganon->actor, 0x55);
+
+ zelda->actor.world.pos.x = 724.0f;
+ zelda->actor.world.pos.y = 1086.0f;
+ zelda->actor.world.pos.z = -186.0f;
+ zelda->actor.shape.rot.y = -0x5000;
+ zelda->unk_3C8 = 5;
+
+ Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_GANON_BOSS);
+
+ ganon->csState = GANON_KNOCK_SWORD_STATE;
+ ganon->csTimer = GANON_KNOCK_SWORD_TIMER;
+ *should = false;
+ });
+}
+
+static RegisterShipInitFunc initFunc(RegisterGanonIntros, { CVAR_NAME, "IS_RANDO", "IS_BOSS_RUSH" });
diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp
index 9c92d220c..afaf001c5 100644
--- a/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp
+++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp
@@ -111,6 +111,9 @@ void RegisterQuickBossDeaths() {
}
});
+ // Skip Ganondorf dying, go to the window shattering before the escape
+ COND_VB_SHOULD(VB_GANONDORF_DEATH_SCENE, CVAR_VALUE || IS_BOSS_RUSH, { *should = false; });
+
COND_VB_SHOULD(VB_VOLVAGIA_DEATH_SCENE, CVAR_VALUE, {
if (*should) {
BossFd2* fd2 = va_arg(args, BossFd2*);
diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp
new file mode 100644
index 000000000..ec818241b
--- /dev/null
+++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp
@@ -0,0 +1,32 @@
+#include <libultraship/bridge/consolevariablebridge.h>
+
+#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/ShipInit.hpp"
+
+extern "C" {
+#include "functions.h"
+#include "variables.h"
+}
+
+#define CVAR_NAME CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story")
+#define CVAR_VALUE CVarGetInteger(CVAR_NAME, IS_RANDO)
+
+void RegisterSkipGanonsTowerCollapse() {
+ // Zelda coming down in her crystal after Ganondorf dies
+ COND_VB_SHOULD(VB_PLAY_ZELDA_CRYSTAL_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; });
+
+ // The tower falling after the escape, same as what Skip Tower Escape does on arrival
+ COND_VB_SHOULD(VB_PLAY_TRANSITION_CS, CVAR_VALUE, {
+ if (gEntranceTable[gSaveContext.entranceIndex].scene == SCENE_GANON_BOSS &&
+ !Flags_GetEventChkInf(EVENTCHKINF_WATCHED_GANONS_CASTLE_COLLAPSE_CAUGHT_BY_GERUDO)) {
+ Flags_SetEventChkInf(EVENTCHKINF_WATCHED_GANONS_CASTLE_COLLAPSE_CAUGHT_BY_GERUDO);
+ gSaveContext.entranceIndex = ENTR_GANON_BOSS_0;
+ *should = false;
+ }
+ });
+
+ // Zelda talking once the tower has fallen
+ COND_VB_SHOULD(VB_PLAY_ESCAPED_TOWER_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; });
+}
+
+static RegisterShipInitFunc initFunc(RegisterSkipGanonsTowerCollapse, { CVAR_NAME, "IS_RANDO", "IS_BOSS_RUSH" });
diff --git a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp
index 6167206bc..010c409d9 100644
--- a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp
+++ b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp
@@ -1225,13 +1225,6 @@ void TimeSaverOnSceneInitHandler(int16_t sceneNum) {
}
}
break;
- case SCENE_GANONDORF_BOSS:
- if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) {
- if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE)) {
- Flags_SetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE);
- }
- }
- break;
}
}
diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
index cfaef048a..41f34ca4e 100644
--- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
+++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
@@ -989,6 +989,14 @@ typedef enum {
// true
// ```
// #### `args`
+ // - `*BossGanon`
+ VB_GANONDORF_DEATH_SCENE,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
// - None
VB_GANON_HEAL_BEFORE_FIGHT,
@@ -2182,6 +2190,14 @@ typedef enum {
// true
// ```
// #### `args`
+ // - `*BossGanon2`
+ VB_PLAY_ESCAPED_TOWER_CS,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
// ##### When called from `z_en_ds.c`
// - `*EnDs`
// ##### When called from `z_en_mk.c`
@@ -2217,6 +2233,23 @@ typedef enum {
// true
// ```
// #### `args`
+ // - `*BossGanon`
+ VB_PLAY_GANONDORF_INTRO_CS,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
+ // - `*BossGanon2`
+ // - `*EnZl3`
+ VB_PLAY_GANON_INTRO_CS,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
// - `*EnHeishi2`
// - `bool` (clearCamera - true if the code clears a sub-camera, false otherwise)
VB_PLAY_GATE_OPENING_OR_CLOSING_CS,
@@ -2439,6 +2472,14 @@ typedef enum {
// ```c
// true
// ```
+ // #### `args`
+ // - `*BossGanon`
+ VB_PLAY_ZELDA_CRYSTAL_CS,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
// Whether the left stick aims in first person. Off when it moves Link instead.
// #### `args`
// - `*Player`
diff --git a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c
index 11146cf69..6311e0bdb 100644
--- a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c
+++ b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c
@@ -585,7 +585,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) {
Play_ChangeCameraStatus(play, this->csCamIndex, CAM_STAT_ACTIVE);
this->csCamFov = 60.0f;
- if (Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE) || IS_RANDO || IS_BOSS_RUSH) {
+ if (Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE)) {
// watched cutscene already, skip most of it
this->csState = 17;
this->csTimer = 0;
@@ -918,7 +918,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) {
this->csTimer = 0;
this->csCamFov = 60.0f;
BossGanon_SetIntroCsCamera(this, 12);
- if (!IS_RANDO && !IS_BOSS_RUSH) {
+ if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) {
Message_StartTextbox(play, 0x70CB, NULL);
}
}
@@ -942,7 +942,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) {
this->csState = 19;
this->csTimer = 0;
- if (!IS_BOSS_RUSH) {
+ if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) {
Message_StartTextbox(play, 0x70CC, NULL);
}
Animation_MorphToPlayOnce(&this->skelAnime, &gGanondorfRaiseHandStartAnim, -5.0f);
@@ -985,8 +985,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) {
}
if ((this->csTimer > 80) && (Message_GetState(&play->msgCtx) == TEXT_STATE_NONE)) {
- // In rando, skip past dark waves section straight to title card phase of the cutscene.
- if (IS_RANDO || IS_BOSS_RUSH) {
+ if (!GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) {
this->timers[2] = 30;
this->csCamAt.x = this->unk_1FC.x - 10.0f;
this->csCamAt.y = this->unk_1FC.y + 30.0f;
@@ -1293,10 +1292,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) {
this->actor.shape.yOffset = -7000.0f;
this->actor.shape.rot.y = 0;
- // Skip Ganondorf dying and go straight to next scene.
- // The cutscene skip met a mixed reaction, so until we figure out a better way of doing it,
- // it will stay not-skipped outside of Boss Rush (originally implemented for randomizer).
- if (!IS_BOSS_RUSH) {
+ if (GameInteractor_Should(VB_GANONDORF_DEATH_SCENE, true, this)) {
this->csState = 1;
this->csTimer = 0;
} else {
@@ -1570,7 +1566,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) {
sZelda = (EnZl3*)Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_ZL3, 0.0f, 6000.0f, 0.0f,
0, 0, 0, 0x2000);
- if (!IS_RANDO && !IS_BOSS_RUSH) {
+ if (GameInteractor_Should(VB_PLAY_ZELDA_CRYSTAL_CS, true, this)) {
this->csState = 101;
} else {
this->skelAnime.playSpeed = 1.0f;
@@ -1697,8 +1693,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) {
}
// fallthrough
case 104:
- // In rando, fade out the white here as the earlier part is skipped.
- if (IS_RANDO || IS_BOSS_RUSH) {
+ if (!GameInteractor_Should(VB_PLAY_ZELDA_CRYSTAL_CS, true, this)) {
Math_ApproachZeroF(&this->whiteFillAlpha, 1.0f, 10.0f);
}
@@ -1719,8 +1714,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) {
}
if (this->csTimer == 50) {
- // In rando, skip the rest of the cutscene after the crystal around Zelda dissapears.
- if (!IS_RANDO && !IS_BOSS_RUSH) {
+ if (GameInteractor_Should(VB_PLAY_ZELDA_CRYSTAL_CS, true, this)) {
sZelda->unk_3C8 = 4;
} else {
this->csState = 108;
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 f6ae2d062..fee03b755 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
@@ -259,8 +259,7 @@ void func_808FD5F4(BossGanon2* this, PlayState* play) {
sZelda->actor.world.pos.z = -214.0f;
sZelda->actor.shape.rot.y = -0x7000;
- // In rando, skip past the cutscene to the part where the player takes control again.
- if (!IS_RANDO && !IS_BOSS_RUSH) {
+ if (GameInteractor_Should(VB_PLAY_ESCAPED_TOWER_CS, true, this)) {
this->csState = 1;
this->csTimer = 0;
} else {
@@ -397,6 +396,9 @@ void func_808FD5F4(BossGanon2* this, PlayState* play) {
this->subCamId = Play_CreateSubCamera(play);
Play_ChangeCameraStatus(play, CAM_ID_MAIN, CAM_STAT_WAIT);
Play_ChangeCameraStatus(play, this->subCamId, CAM_STAT_ACTIVE);
+ if (!GameInteractor_Should(VB_PLAY_GANON_INTRO_CS, true, this, sZelda)) {
+ break;
+ }
} else {
break;
}