summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGarrett Cox <garrettjcox@gmail.com>2026-01-24 17:52:29 -0600
committerGitHub <noreply@github.com>2026-01-24 17:52:29 -0600
commit16ef59baeaf9f6955281b54d2f75ecc7c968a6c3 (patch)
treef12d58ffca6e2212be42be29b2ee2bc16dc0e1e7
parent5229a531c2336349e12a4985dc25ebf5e13fc63a (diff)
WIP Tweaks to fix compatibility with 0th/4th day glitches (#1462)
-rw-r--r--mm/2s2h/BenGui/BenMenu.cpp7
-rw-r--r--mm/2s2h/BenPort.cpp1
-rw-r--r--mm/2s2h/Enhancements/Enhancements.cpp11
-rw-r--r--mm/2s2h/Enhancements/Enhancements.h5
-rw-r--r--mm/2s2h/Enhancements/Saving/AutoSave.cpp94
-rw-r--r--mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp286
-rw-r--r--mm/2s2h/Enhancements/Saving/SavingEnhancements.h6
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h16
-rw-r--r--mm/src/code/z_play.c2
-rw-r--r--mm/src/code/z_sram_NES.c21
-rw-r--r--mm/src/overlays/actors/ovl_En_Test4/z_en_test4.c36
-rw-r--r--mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope_NES.c11
12 files changed, 300 insertions, 196 deletions
diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp
index 218d19ed6..2ad9c4c9f 100644
--- a/mm/2s2h/BenGui/BenMenu.cpp
+++ b/mm/2s2h/BenGui/BenMenu.cpp
@@ -1125,7 +1125,6 @@ void BenMenu::AddEnhancements() {
.Options(CheckboxOptions().Tooltip("When loading a save, places Link at the last entrance he went through."));
AddWidget(path, "Autosave", WIDGET_CVAR_CHECKBOX)
.CVar("gEnhancements.Saving.Autosave")
- .Callback([](WidgetInfo& info) { RegisterAutosave(); })
.Options(CheckboxOptions().Tooltip(
"Automatically create a persistent Owl Save on the chosen interval.\n\nWhen loading "
"back into the game, you will be placed either at the entrance of the dungeon you "
@@ -1173,12 +1172,6 @@ void BenMenu::AddEnhancements() {
AddWidget(path, "Oceanside wallet any day", WIDGET_CVAR_CHECKBOX)
.CVar("gEnhancements.Cycle.OceansideWalletAnyDay")
.Options(CheckboxOptions().Tooltip("Allows the wallet reward to be collected on any day."));
- AddWidget(path, "Unstable", WIDGET_SEPARATOR_TEXT).Options(WidgetOptions().Color(Colors::Orange));
- AddWidget(path, "Disable Save Delay", WIDGET_CVAR_CHECKBOX)
- .CVar("gEnhancements.Saving.DisableSaveDelay")
- .Options(CheckboxOptions().Tooltip(
- "Removes the arbitrary 2 second timer for saving from the original game. This is known to "
- "cause issues when attempting the 0th Day Glitch."));
//// Graphics Enhancements
path = { "Enhancements", "Graphics", SECTION_COLUMN_1 };
diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp
index 6f0146094..c9a5ad156 100644
--- a/mm/2s2h/BenPort.cpp
+++ b/mm/2s2h/BenPort.cpp
@@ -713,7 +713,6 @@ extern "C" void InitOTR() {
LoadGuiTextures();
BenGui::SetupGuiElements();
ShipInit::InitAll();
- InitEnhancements();
Rando::Init();
GfxPatcher_ApplyNecessaryAuthenticPatches();
DebugConsole_Init();
diff --git a/mm/2s2h/Enhancements/Enhancements.cpp b/mm/2s2h/Enhancements/Enhancements.cpp
deleted file mode 100644
index 4ecd24439..000000000
--- a/mm/2s2h/Enhancements/Enhancements.cpp
+++ /dev/null
@@ -1,11 +0,0 @@
-#include "Enhancements.h"
-
-void InitEnhancements() {
- // Cycle & Saving
- RegisterSavingEnhancements();
- RegisterAutosave();
-
- // Uncomment to enable the demo behavior, this shows of different modding capabilities
- // void RegisterDemoBehavior();
- // RegisterDemoBehavior();
-}
diff --git a/mm/2s2h/Enhancements/Enhancements.h b/mm/2s2h/Enhancements/Enhancements.h
index 13d8a5884..0df8845a0 100644
--- a/mm/2s2h/Enhancements/Enhancements.h
+++ b/mm/2s2h/Enhancements/Enhancements.h
@@ -1,8 +1,6 @@
#ifndef ENHANCEMENTS_H
#define ENHANCEMENTS_H
-#include "Saving/SavingEnhancements.h"
-
enum AlwaysWinDoggyRaceOptions {
ALWAYS_WIN_DOGGY_RACE_OFF,
ALWAYS_WIN_DOGGY_RACE_MASKOFTRUTH,
@@ -64,7 +62,4 @@ enum GoronRaceDifficultyOptions {
GORON_RACE_DIFFICULTY_SKIP,
};
-// Old Entry Point
-void InitEnhancements();
-
#endif // ENHANCEMENTS_H
diff --git a/mm/2s2h/Enhancements/Saving/AutoSave.cpp b/mm/2s2h/Enhancements/Saving/AutoSave.cpp
new file mode 100644
index 000000000..1495069f5
--- /dev/null
+++ b/mm/2s2h/Enhancements/Saving/AutoSave.cpp
@@ -0,0 +1,94 @@
+#include "SavingEnhancements.h"
+#include <libultraship/bridge/consolevariablebridge.h>
+#include "BenPort.h"
+#include "2s2h/GameInteractor/GameInteractor.h"
+#include "2s2h/ShipInit.hpp"
+#include "2s2h/CustomMessage/CustomMessage.h"
+
+extern "C" {
+#include <variables.h>
+#include <functions.h>
+}
+
+#define CVAR_AUTOSAVE_NAME "gEnhancements.Saving.Autosave"
+#define CVAR_AUTOSAVE CVarGetInteger(CVAR_AUTOSAVE_NAME, 0)
+
+static uint32_t autosaveInterval = 0;
+static uint32_t iconTimer = 0;
+static uint64_t currentTimestamp = 0;
+static uint64_t lastSaveTimestamp = GetUnixTimestamp();
+
+void DrawAutosaveIcon() {
+ // 5 seconds (100 frames) of showing the owl save icon to signify autosave has happened.
+ if (iconTimer != 0) {
+ float opacity = 255.0;
+ // Fade in icon
+ if (iconTimer > 80) {
+ opacity = 255.0 - (((iconTimer - 80.0) / 20.0) * 255);
+ // Fade out icon
+ } else if (iconTimer < 20) {
+ opacity = (iconTimer / 20.0) * 255.0;
+ }
+ Interface_DrawAutosaveIcon(gPlayState, uint16_t(opacity));
+ iconTimer--;
+ }
+}
+
+void HandleAutoSave() {
+ // Check if the interval has passed in minutes.
+ autosaveInterval = CVarGetInteger("gEnhancements.Saving.AutosaveInterval", 5) * 60000;
+ currentTimestamp = GetUnixTimestamp();
+ if ((currentTimestamp - lastSaveTimestamp) < autosaveInterval) {
+ return;
+ }
+
+ Player* player = GET_PLAYER(gPlayState);
+ if (player == NULL) {
+ return;
+ }
+
+ // If owl save available to create, do it and reset the interval.
+ if (SavingEnhancements_CanSave() && gPlayState->pauseCtx.state == 0) {
+
+ // Reset timestamp, set icon timer to show autosave icon for 5 seconds (100 frames)
+ lastSaveTimestamp = GetUnixTimestamp();
+ iconTimer = 100;
+
+ // Persist this in case the user is 0th daying
+ bool currentOwlSaveState = gSaveContext.save.isOwlSave;
+
+ // Create owl save
+ gSaveContext.save.isOwlSave = true;
+ SavingEnhancements_PersistSaveEntranceInfo();
+ SavingEnhancements_AdvancePlaytime();
+ Play_SaveCycleSceneFlags(gPlayState);
+ gSaveContext.save.saveInfo.playerData.savedSceneId = gPlayState->sceneId;
+ func_8014546C(&gPlayState->sramCtx);
+ Sram_SetFlashPagesOwlSave(&gPlayState->sramCtx,
+ gFlashOwlSaveStartPages[gSaveContext.fileNum * FLASH_SAVE_MAIN_MULTIPLIER],
+ gFlashOwlSaveNumPages[gSaveContext.fileNum * FLASH_SAVE_MAIN_MULTIPLIER]);
+ Sram_StartWriteToFlashOwlSave(&gPlayState->sramCtx);
+ gSaveContext.save.isOwlSave = currentOwlSaveState;
+ SavingEnhancements_ClearSaveEntranceInfo();
+ }
+}
+
+static RegisterShipInitFunc registerAutosave(
+ []() {
+ COND_HOOK(OnGameStateUpdate, CVAR_AUTOSAVE, []() {
+ if (gPlayState == nullptr) {
+ return;
+ }
+
+ HandleAutoSave();
+ });
+
+ COND_HOOK(OnGameStateDrawFinish, CVAR_AUTOSAVE, []() {
+ if (gPlayState == nullptr) {
+ return;
+ }
+
+ DrawAutosaveIcon();
+ });
+ },
+ { CVAR_AUTOSAVE_NAME });
diff --git a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp
index e725da230..f6181ea78 100644
--- a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp
+++ b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp
@@ -2,6 +2,7 @@
#include "BenPort.h"
#include "2s2h/GameInteractor/GameInteractor.h"
#include "2s2h/ShipInit.hpp"
+#include "2s2h/CustomMessage/CustomMessage.h"
extern "C" {
#include <variables.h>
@@ -11,20 +12,14 @@ extern "C" {
#define CVAR_REMEMBER_SAVE_LOCATION_NAME "gEnhancements.Saving.RememberSaveLocation"
#define CVAR_REMEMBER_SAVE_LOCATION CVarGetInteger(CVAR_REMEMBER_SAVE_LOCATION_NAME, 0)
-static uint32_t autosaveInterval = 0;
-static uint32_t iconTimer = 0;
-static uint64_t currentTimestamp = 0;
-static uint64_t lastSaveTimestamp = GetUnixTimestamp();
static int lastEntrance = -1;
static int entranceToSave = -1;
-static HOOK_ID autosaveGameStateUpdateHookId = 0;
-static HOOK_ID autosaveGameStateDrawFinishHookId = 0;
static HOOK_ID skipEntranceCutsceneHookId = 0;
static HOOK_ID gameplayStartHookId = 0;
// Used for saving through Autosaves and Pause Menu saves.
-extern "C" int SavingEnhancements_GetSaveEntrance() {
+extern "C" void SavingEnhancements_PersistSaveEntranceInfo() {
if (CVAR_REMEMBER_SAVE_LOCATION) {
// Maintain respawn information, used for grottos
for (int i = 0; i < RESPAWN_MODE_MAX; i++) {
@@ -32,34 +27,46 @@ extern "C" int SavingEnhancements_GetSaveEntrance() {
}
// Daytelop on new game, with Time Shuffle, makes it possible for entranceToSave to be -1. Given that the player
// must be at this entrance in that scenario, just use it as a fallback.
- return entranceToSave < 0 ? ENTRANCE(SOUTH_CLOCK_TOWN, 0) : entranceToSave;
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance =
+ entranceToSave < 0 ? ENTRANCE(SOUTH_CLOCK_TOWN, 0) : entranceToSave;
} else {
switch (gPlayState->sceneId) {
// Woodfall Temple + Odolwa
case SCENE_MITURIN:
case SCENE_MITURIN_BS:
- return ENTRANCE(WOODFALL_TEMPLE, 0);
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(WOODFALL_TEMPLE, 0);
+ break;
// Snowhead Temple + Goht
case SCENE_HAKUGIN:
case SCENE_HAKUGIN_BS:
- return ENTRANCE(SNOWHEAD_TEMPLE, 0);
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(SNOWHEAD_TEMPLE, 0);
+ break;
// Great Bay Temple + Gyorg
case SCENE_SEA:
case SCENE_SEA_BS:
- return ENTRANCE(GREAT_BAY_TEMPLE, 0);
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(GREAT_BAY_TEMPLE, 0);
+ break;
// Stone Tower Temple
case SCENE_INISIE_N:
- return ENTRANCE(STONE_TOWER_TEMPLE, 0);
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(STONE_TOWER_TEMPLE, 0);
+ break;
// Stone Tower Temple (inverted) + Twinmold
case SCENE_INISIE_R:
case SCENE_INISIE_BS:
- return ENTRANCE(STONE_TOWER_TEMPLE_INVERTED, 0);
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(STONE_TOWER_TEMPLE_INVERTED, 0);
+ break;
default:
- return ENTRANCE(SOUTH_CLOCK_TOWN, 0);
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(SOUTH_CLOCK_TOWN, 0);
+ break;
}
}
}
+extern "C" void SavingEnhancements_ClearSaveEntranceInfo() {
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance = -1;
+ memset(&gSaveContext.save.shipSaveInfo.respawn, 0, sizeof(gSaveContext.save.shipSaveInfo.respawn));
+}
+
extern "C" bool SavingEnhancements_CanSave() {
// Game State
if (gPlayState == NULL || GET_PLAYER(gPlayState) == NULL) {
@@ -113,67 +120,16 @@ extern "C" void SavingEnhancements_AdvancePlaytime() {
}
void DeleteOwlSave() {
- // Remove Owl Save on time cycle reset, needed when persisting owl saves and/or when
- // creating owl saves without the player being send back to the file select screen.
+ // Persist this in case the user is 0th daying
+ bool currentOwlSaveState = gSaveContext.save.isOwlSave;
- // Delete Owl Save
+ // Typically called when loading into an owl save
func_80147314(&gPlayState->sramCtx, gSaveContext.fileNum);
// Set it to not be an owl save so after reloading the save file it doesn't try to load at the owl's position in
// clock town
- gSaveContext.save.isOwlSave = false;
-}
-
-void DrawAutosaveIcon() {
- // 5 seconds (100 frames) of showing the owl save icon to signify autosave has happened.
- if (iconTimer != 0) {
- float opacity = 255.0;
- // Fade in icon
- if (iconTimer > 80) {
- opacity = 255.0 - (((iconTimer - 80.0) / 20.0) * 255);
- // Fade out icon
- } else if (iconTimer < 20) {
- opacity = (iconTimer / 20.0) * 255.0;
- }
- Interface_DrawAutosaveIcon(gPlayState, uint16_t(opacity));
- iconTimer--;
- }
-}
-
-void HandleAutoSave() {
- // Check if the interval has passed in minutes.
- autosaveInterval = CVarGetInteger("gEnhancements.Saving.AutosaveInterval", 5) * 60000;
- currentTimestamp = GetUnixTimestamp();
- if ((currentTimestamp - lastSaveTimestamp) < autosaveInterval) {
- return;
- }
-
- Player* player = GET_PLAYER(gPlayState);
- if (player == NULL) {
- return;
- }
- // If owl save available to create, do it and reset the interval.
- if (SavingEnhancements_CanSave() && gPlayState->pauseCtx.state == 0) {
-
- // Reset timestamp, set icon timer to show autosave icon for 5 seconds (100 frames)
- lastSaveTimestamp = GetUnixTimestamp();
- iconTimer = 100;
-
- // Create owl save
- gSaveContext.save.isOwlSave = true;
- gSaveContext.save.shipSaveInfo.pauseSaveEntrance = SavingEnhancements_GetSaveEntrance();
- SavingEnhancements_AdvancePlaytime();
- Play_SaveCycleSceneFlags(gPlayState);
- gSaveContext.save.saveInfo.playerData.savedSceneId = gPlayState->sceneId;
- func_8014546C(&gPlayState->sramCtx);
- Sram_SetFlashPagesOwlSave(&gPlayState->sramCtx,
- gFlashOwlSaveStartPages[gSaveContext.fileNum * FLASH_SAVE_MAIN_MULTIPLIER],
- gFlashOwlSaveNumPages[gSaveContext.fileNum * FLASH_SAVE_MAIN_MULTIPLIER]);
- Sram_StartWriteToFlashOwlSave(&gPlayState->sramCtx);
- gSaveContext.save.isOwlSave = false;
- gSaveContext.save.shipSaveInfo.pauseSaveEntrance = -1;
- }
+ gSaveContext.save.isOwlSave = currentOwlSaveState;
}
/*
@@ -183,7 +139,7 @@ void HandleAutoSave() {
* ENTR_LOAD_OPENING, which in turn would lead to a crash if the save is within a grotto and the player dies before
* leaving.
*/
-void loadRespawnData(s16 fileNum) {
+void LoadRespawnData(s16 fileNum) {
for (int i = 0; i < RESPAWN_MODE_MAX; i++) {
gSaveContext.respawn[i] = gSaveContext.save.shipSaveInfo.respawn[i];
}
@@ -208,7 +164,7 @@ static void UnregisterEntranceCutsceneSkip() {
}
}
-void skipEntranceCutsceneOnLoad(s16 fileNum) {
+void SkipEntranceCutsceneOnLoad(s16 fileNum) {
// Clean up any existing hooks first
UnregisterEntranceCutsceneSkip();
// Register hook to skip entrance cutscenes - may skip multiple if they chain
@@ -229,92 +185,126 @@ void skipEntranceCutsceneOnLoad(s16 fileNum) {
});
}
-void RegisterSavingEnhancements() {
- REGISTER_VB_SHOULD(VB_DELETE_OWL_SAVE, {
- if (CVarGetInteger("gEnhancements.Saving.PersistentOwlSaves", 0) ||
- gSaveContext.save.shipSaveInfo.pauseSaveEntrance != -1) {
- *should = false;
- }
- });
-
- COND_HOOK(OnSaveLoad, true, [](s16 fileNum) {
- if (gSaveContext.save.shipSaveInfo.fileCreatedAt == 0) {
- gSaveContext.save.shipSaveInfo.fileCreatedAt = GetUnixTimestamp();
- }
- gSaveContext.shipSaveContext.lastTimeLog = GetUnixTimestamp();
- lastEntrance = entranceToSave = gSaveContext.save.shipSaveInfo.pauseSaveEntrance;
- });
-
- // Owl statue prompt
- COND_ID_HOOK(OnOpenText, 0xC01, true,
- [](u16* textId, bool* loadFromMessageTable) { SavingEnhancements_AdvancePlaytime(); });
-
- // Finished the game, mark fileCompletedAt accordingly
- COND_HOOK(OnGameCompletion, true, []() {
- if (gSaveContext.save.shipSaveInfo.fileCompletedAt == 0) {
- SavingEnhancements_AdvancePlaytime();
- gSaveContext.save.shipSaveInfo.fileCompletedAt = GetUnixTimestamp();
- }
- });
+static RegisterShipInitFunc registerSavingEnhancements(
+ []() {
+ // Prevent deletion of owl saves based on cvar or if this was a pause/auto save
+ COND_VB_SHOULD(VB_DELETE_OWL_SAVE, true, {
+ if (CVarGetInteger("gEnhancements.Saving.PersistentOwlSaves", 0) ||
+ gSaveContext.save.shipSaveInfo.pauseSaveEntrance != -1) {
+ *should = false;
+ }
+ });
- GameInteractor::Instance->RegisterGameHook<GameInteractor::BeforeEndOfCycleSave>([]() {
- SavingEnhancements_AdvancePlaytime();
- DeleteOwlSave();
- });
+ COND_HOOK(OnSaveLoad, true, [](s16 fileNum) {
+ if (gSaveContext.save.shipSaveInfo.fileCreatedAt == 0) {
+ gSaveContext.save.shipSaveInfo.fileCreatedAt = GetUnixTimestamp();
+ }
+ gSaveContext.shipSaveContext.lastTimeLog = GetUnixTimestamp();
+ lastEntrance = entranceToSave = gSaveContext.save.shipSaveInfo.pauseSaveEntrance;
+ });
- GameInteractor::Instance->RegisterGameHook<GameInteractor::BeforeMoonCrashSaveReset>([]() { DeleteOwlSave(); });
+ // Owl statue prompt
+ COND_ID_HOOK(OnOpenText, 0xC01, true,
+ [](u16* textId, bool* loadFromMessageTable) { SavingEnhancements_AdvancePlaytime(); });
- GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSaveLoad>(loadRespawnData);
-}
+ // Finished the game, mark fileCompletedAt accordingly
+ COND_HOOK(OnGameCompletion, true, []() {
+ if (gSaveContext.save.shipSaveInfo.fileCompletedAt == 0) {
+ SavingEnhancements_AdvancePlaytime();
+ gSaveContext.save.shipSaveInfo.fileCompletedAt = GetUnixTimestamp();
+ }
+ });
-void RegisterAutosave() {
- if (autosaveGameStateUpdateHookId) {
- GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnGameStateUpdate>(autosaveGameStateUpdateHookId);
- autosaveGameStateUpdateHookId = 0;
- }
+ // When resetting the time cycle or letting the moon crash, an owl save would normally not even
+ // be present because they are deleted when loading. However, with persistent owl saves or
+ // pause/auto saves, we need to explicitly delete the owl save here, to let the player load
+ // into the new cycle or lose their progress on moon crash.
+ COND_HOOK(BeforeEndOfCycleSave, true, []() {
+ SavingEnhancements_AdvancePlaytime();
+ DeleteOwlSave();
+ });
+ COND_HOOK(BeforeMoonCrashSaveReset, true, []() { DeleteOwlSave(); });
+
+ // Vanilla has an arbitrary 2 second delay when saving, we can't remove it entirely because
+ // it's used to pull off certain 0th Day glitches (specifically Any Item as Any Form and Goron Missile),
+ // because they required you to change forms before the SaveContext is restored in that window. We can
+ // determine if they are trying to pull this trick off if they are playing song of time and isOwlSave is
+ // set (which normally shouldn't be). Otherwise remove the delay.
+ COND_VB_SHOULD(VB_SAVE_DELAY, true, {
+ if (gPlayState == NULL || gPlayState->msgCtx.msgMode != MSGMODE_NEW_CYCLE_1 ||
+ !gSaveContext.save.isOwlSave) {
+ *should = true;
+ }
+ });
- if (autosaveGameStateDrawFinishHookId) {
- GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnGameStateDrawFinish>(
- autosaveGameStateDrawFinishHookId);
- autosaveGameStateDrawFinishHookId = 0;
- }
+ // In vanilla, isOwlSave determines if the owl save sram path is taken (which restores the SaveContext N seconds
+ // later) We only want this path to be taken for actual owl saves and when playing Song of Time with isOwlSave
+ // set (0th Daying). This might seem redundant, but isOwlSave can be set and the player can autosave/pause save.
+ COND_VB_SHOULD(VB_SAVE_USE_OWL_SAVE_TIMING, true, {
+ *should = gSaveContext.save.isOwlSave && (gPlayState->msgCtx.msgMode == MSGMODE_NEW_CYCLE_1 ||
+ gPlayState->msgCtx.msgMode == MSGMODE_OWL_SAVE_1);
+ });
- if (CVarGetInteger("gEnhancements.Saving.Autosave", 0)) {
- autosaveGameStateUpdateHookId =
- GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameStateUpdate>([]() {
- if (gPlayState == nullptr) {
- return;
+ // These hooks modify the SceneTitleCard messages for day/night on the 0th and 4th days
+ COND_ID_HOOK(OnOpenText, 0x1BB4, true, [](u16* textId, bool* loadFromMessageTable) {
+ if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
+ auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId);
+
+ // Weird edge case where dawn of the Zeroth day pulls the night entry
+ if (CURRENT_DAY == 0 && gSaveContext.save.time == CLOCK_TIME(6, 0) - 1) {
+ CustomMessage::Replace(&entry.msg, "Night of the First", "Dawn of the Zeroth");
+ } else {
+ switch (CURRENT_DAY) {
+ case 0:
+ CustomMessage::Replace(&entry.msg, "First", "Zeroth");
+ break;
+ case 4:
+ CustomMessage::Replace(&entry.msg, "First", "Fourth");
+ break;
+ }
}
- HandleAutoSave();
- });
+ CustomMessage::LoadCustomMessageIntoFont(entry);
+ *loadFromMessageTable = false;
+ }
+ });
- autosaveGameStateDrawFinishHookId =
- GameInteractor::Instance->RegisterGameHook<GameInteractor::OnGameStateDrawFinish>([]() {
- if (gPlayState == nullptr) {
- return;
+ COND_ID_HOOK(OnOpenText, 0x1BB2, true, [](u16* textId, bool* loadFromMessageTable) {
+ if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
+ auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId);
+
+ switch (CURRENT_DAY) {
+ case 0:
+ CustomMessage::Replace(&entry.msg, "First", "Zeroth");
+ break;
+ case 4:
+ CustomMessage::Replace(&entry.msg, "First", "Fourth");
+ break;
}
- DrawAutosaveIcon();
- });
- }
-}
-
-void RegisterRememberSaveLocation() {
- COND_VB_SHOULD(VB_PLAY_TRANSITION_CS, CVAR_REMEMBER_SAVE_LOCATION, {
- /*
- * Update the entrance to save, unless we're leaving a grotto. Grottos exit to entrance 0 of the destination
- * scene and adjust the position manually. In effect, there is no real entrance to target for loading purposes,
- * so we just load into the last grotto instead under those circumstances.
- */
- if (lastEntrance != -1 && !(Entrance_GetSceneIdAbsolute(gSaveContext.save.entrance) != SCENE_KAKUSIANA &&
- Entrance_GetSceneIdAbsolute(lastEntrance) == SCENE_KAKUSIANA)) {
- entranceToSave = gSaveContext.save.entrance;
- }
- lastEntrance = gSaveContext.save.entrance;
- });
-
- COND_HOOK(OnSaveLoad, CVAR_REMEMBER_SAVE_LOCATION, skipEntranceCutsceneOnLoad);
-}
+ CustomMessage::LoadCustomMessageIntoFont(entry);
+ *loadFromMessageTable = false;
+ }
+ });
+ },
+ {});
+
+static RegisterShipInitFunc registerRememberSaveLocation(
+ []() {
+ COND_VB_SHOULD(VB_PLAY_TRANSITION_CS, CVAR_REMEMBER_SAVE_LOCATION, {
+ /*
+ * Update the entrance to save, unless we're leaving a grotto. Grottos exit to entrance 0 of the destination
+ * scene and adjust the position manually. In effect, there is no real entrance to target for loading
+ * purposes, so we just load into the last grotto instead under those circumstances.
+ */
+ if (lastEntrance != -1 && !(Entrance_GetSceneIdAbsolute(gSaveContext.save.entrance) != SCENE_KAKUSIANA &&
+ Entrance_GetSceneIdAbsolute(lastEntrance) == SCENE_KAKUSIANA)) {
+ entranceToSave = gSaveContext.save.entrance;
+ }
+ lastEntrance = gSaveContext.save.entrance;
+ });
-static RegisterShipInitFunc initFunc(RegisterRememberSaveLocation, { CVAR_REMEMBER_SAVE_LOCATION_NAME });
+ COND_HOOK(OnSaveLoad, CVAR_REMEMBER_SAVE_LOCATION, SkipEntranceCutsceneOnLoad);
+ COND_HOOK(OnSaveLoad, CVAR_REMEMBER_SAVE_LOCATION, LoadRespawnData);
+ },
+ { CVAR_REMEMBER_SAVE_LOCATION_NAME });
diff --git a/mm/2s2h/Enhancements/Saving/SavingEnhancements.h b/mm/2s2h/Enhancements/Saving/SavingEnhancements.h
index 4e7261aa7..960cc3dd5 100644
--- a/mm/2s2h/Enhancements/Saving/SavingEnhancements.h
+++ b/mm/2s2h/Enhancements/Saving/SavingEnhancements.h
@@ -1,14 +1,12 @@
#ifndef SAVING_ENHANCEMENTS_H
#define SAVING_ENHANCEMENTS_H
-void RegisterSavingEnhancements();
-void RegisterAutosave();
-
#ifdef __cplusplus
extern "C" {
#endif
-int SavingEnhancements_GetSaveEntrance();
+void SavingEnhancements_PersistSaveEntranceInfo();
+void SavingEnhancements_ClearSaveEntranceInfo();
bool SavingEnhancements_CanSave();
void SavingEnhancements_AdvancePlaytime();
diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
index a1c657d1b..9f8bd8bb1 100644
--- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
+++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
@@ -1722,6 +1722,22 @@ typedef enum {
// #### `result`
// ```c
+ // OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >= SECONDS_TO_TIMER(2))
+ // ```
+ // #### `args`
+ // - None
+ VB_SAVE_DELAY,
+
+ // #### `result`
+ // ```c
+ // gSaveContext.save.isOwlSave
+ // ```
+ // #### `args`
+ // - None
+ VB_SAVE_USE_OWL_SAVE_TIMING,
+
+ // #### `result`
+ // ```c
// false
// ```
// #### `args`
diff --git a/mm/src/code/z_play.c b/mm/src/code/z_play.c
index a0a3b3a99..1b568b522 100644
--- a/mm/src/code/z_play.c
+++ b/mm/src/code/z_play.c
@@ -1101,7 +1101,7 @@ void Play_UpdateMain(PlayState* this) {
this->state.gfxCtx);
if (this->sramCtx.status != 0) {
- if (gSaveContext.save.isOwlSave) {
+ if (GameInteractor_Should(VB_SAVE_USE_OWL_SAVE_TIMING, gSaveContext.save.isOwlSave)) {
Sram_UpdateWriteToFlashOwlSave(&this->sramCtx);
} else {
Sram_UpdateWriteToFlashDefault(&this->sramCtx);
diff --git a/mm/src/code/z_sram_NES.c b/mm/src/code/z_sram_NES.c
index 03c3c301e..bbeb24e70 100644
--- a/mm/src/code/z_sram_NES.c
+++ b/mm/src/code/z_sram_NES.c
@@ -1401,7 +1401,14 @@ void Sram_OpenSave(FileSelectState* fileSelect, SramContext* sramCtx) {
if (gSaveContext.save.shipSaveInfo.pauseSaveEntrance != -1) {
gSaveContext.save.entrance = gSaveContext.save.shipSaveInfo.pauseSaveEntrance;
} else {
- gSaveContext.save.entrance = sOwlWarpEntrances[(void)0, gSaveContext.save.owlWarpId];
+ // @bug When the player saves at the extra Owl statue in west clock town
+ // their owlWarpId is set to 0xF. On hardware this results in the entrance being set to 0
+ // 2S2H [Port] We opt to fix this by hardcoding entrance to 0 when owlWarpId is out of bounds
+ if (gSaveContext.save.owlWarpId > OWL_WARP_MAX) {
+ gSaveContext.save.entrance = 0;
+ } else {
+ gSaveContext.save.entrance = sOwlWarpEntrances[(void)0, gSaveContext.save.owlWarpId];
+ }
}
if ((gSaveContext.save.entrance == ENTRANCE(SOUTHERN_SWAMP_POISONED, 10)) &&
CHECK_WEEKEVENTREG(WEEKEVENTREG_CLEARED_WOODFALL_TEMPLE)) {
@@ -2079,10 +2086,8 @@ void Sram_UpdateWriteToFlashDefault(SramContext* sramCtx) {
sramCtx->status = 4;
}
}
- } else if (OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >=
- SECONDS_TO_TIMER(CVarGetInteger("gEnhancements.Saving.DisableSaveDelay", 0) ? 0 : 2)) {
- // 2S2H [Port] Some tricks require a save delay so we can't just force it to zero
- // Finished status is hardcoded to 2 seconds instead of when the task finishes
+ } else if (GameInteractor_Should(VB_SAVE_DELAY,
+ OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >= SECONDS_TO_TIMER(2))) {
sramCtx->status = 0;
}
}
@@ -2119,10 +2124,8 @@ void Sram_UpdateWriteToFlashOwlSave(SramContext* sramCtx) {
sramCtx->status = 4;
}
}
- } else if (OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >=
- SECONDS_TO_TIMER(CVarGetInteger("gEnhancements.Saving.DisableSaveDelay", 0) ? 0 : 2)) {
- // 2S2H [Port] Some tricks require a save delay so we can't just force it to zero
- // Finished status is hardcoded to 2 seconds instead of when the task finishes
+ } else if (GameInteractor_Should(VB_SAVE_DELAY,
+ OSTIME_TO_TIMER(osGetTime() - sramCtx->startWriteOsTime) >= SECONDS_TO_TIMER(2))) {
sramCtx->status = 0;
memset(sramCtx->saveBuf, 0, SAVE_BUFFER_SIZE);
gSaveContext.save.isOwlSave = false;
diff --git a/mm/src/overlays/actors/ovl_En_Test4/z_en_test4.c b/mm/src/overlays/actors/ovl_En_Test4/z_en_test4.c
index 257ff3449..365e12db7 100644
--- a/mm/src/overlays/actors/ovl_En_Test4/z_en_test4.c
+++ b/mm/src/overlays/actors/ovl_En_Test4/z_en_test4.c
@@ -58,13 +58,27 @@ void EnTest4_HandleDayNightSwapFromInit(EnTest4* this, PlayState* play) {
if (this->daytimeIndex != THREEDAY_DAYTIME_NIGHT) {
// Previously day, turning night
- Message_DisplaySceneTitleCard(play, sNightOfTextIds[CURRENT_DAY - 1]);
+ // @bug On 0th/4th day, this will read OOB, on console this results in an empty message
+ // 2S2H [Port] Opting to fix this, displaying the first day message on invalid days
+ // which we alter in SavingEnhancements.cpp
+ if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
+ Message_DisplaySceneTitleCard(play, 0x1BB4);
+ } else {
+ Message_DisplaySceneTitleCard(play, sNightOfTextIds[CURRENT_DAY - 1]);
+ }
} else if ((sCsIdList[this->daytimeIndex] <= CS_ID_NONE) || (play->actorCtx.flags & ACTORCTX_FLAG_TELESCOPE_ON)) {
// Previously night, turning day, without a cutscene
if (play->actorCtx.flags & ACTORCTX_FLAG_TELESCOPE_ON) {
Sram_IncrementDay();
gSaveContext.save.time = CLOCK_TIME(6, 0);
- Message_DisplaySceneTitleCard(play, sDawnOfTextIds[CURRENT_DAY - 1]);
+ // @bug On 0th/4th day, this will read OOB, on console this results in an empty message
+ // 2S2H [Port] Opting to fix this, displaying the first day message on invalid days
+ // which we alter in SavingEnhancements.cpp
+ if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
+ Message_DisplaySceneTitleCard(play, 0x1BB2);
+ } else {
+ Message_DisplaySceneTitleCard(play, sDawnOfTextIds[CURRENT_DAY - 1]);
+ }
} else {
this->daytimeIndex = THREEDAY_DAYTIME_NIGHT;
gSaveContext.save.time += CLOCK_TIME_MINUTE;
@@ -119,13 +133,27 @@ void EnTest4_HandleDayNightSwap(EnTest4* this, PlayState* play) {
if (this->daytimeIndex != THREEDAY_DAYTIME_NIGHT) {
// Previously day, turning night
- Message_DisplaySceneTitleCard(play, sNightOfTextIds[CURRENT_DAY - 1]);
+ // @bug On 0th/4th day, this will read OOB, on console this results in an empty message
+ // 2S2H [Port] Opting to fix this, displaying the first day message on invalid days
+ // which we alter in SavingEnhancements.cpp
+ if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
+ Message_DisplaySceneTitleCard(play, 0x1BB4);
+ } else {
+ Message_DisplaySceneTitleCard(play, sNightOfTextIds[CURRENT_DAY - 1]);
+ }
} else if ((sCsIdList[this->daytimeIndex] <= CS_ID_NONE) || (play->actorCtx.flags & ACTORCTX_FLAG_TELESCOPE_ON)) {
// Previously night, turning day, without a cutscene
Sram_IncrementDay();
gSaveContext.save.time = CLOCK_TIME(6, 0);
Interface_NewDay(play, CURRENT_DAY);
- Message_DisplaySceneTitleCard(play, sDawnOfTextIds[CURRENT_DAY - 1]);
+ // @bug On 0th/4th day, this will read OOB, on console this results in an empty message
+ // 2S2H [Port] Opting to fix this, displaying the first day message on invalid days
+ // which we alter in SavingEnhancements.cpp
+ if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
+ Message_DisplaySceneTitleCard(play, 0x1BB2);
+ } else {
+ Message_DisplaySceneTitleCard(play, sDawnOfTextIds[CURRENT_DAY - 1]);
+ }
gSceneSeqState = SCENESEQ_MORNING;
Environment_PlaySceneSequence(play);
Environment_NewDay(&play->envCtx);
diff --git a/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope_NES.c b/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope_NES.c
index e2581adfc..dcb6258d5 100644
--- a/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope_NES.c
+++ b/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope_NES.c
@@ -3574,12 +3574,11 @@ void KaleidoScope_Update(PlayState* play) {
pauseCtx->savePromptState = PAUSE_SAVEPROMPT_STATE_RETURN_TO_MENU;
} else {
Audio_PlaySfx(NA_SE_SY_PIECE_OF_HEART);
+ // 2S2H [Enhancement] Persist this in case the user is 0th daying
+ bool currentOwlSaveState = gSaveContext.save.isOwlSave;
if (CVarGetInteger("gEnhancements.Saving.PauseSave", 0)) {
gSaveContext.save.isOwlSave = true;
- // 2S2H [Enhancement] Eventually we might allow them to load from their last entrance,
- // but we need to first identify and fix edge cases where that doesn't work properly
- // like grottos and cutscenes
- gSaveContext.save.shipSaveInfo.pauseSaveEntrance = SavingEnhancements_GetSaveEntrance();
+ SavingEnhancements_PersistSaveEntranceInfo();
SavingEnhancements_AdvancePlaytime();
}
Play_SaveCycleSceneFlags(play);
@@ -3596,8 +3595,8 @@ void KaleidoScope_Update(PlayState* play) {
gFlashOwlSaveStartPages[gSaveContext.fileNum * FLASH_SAVE_MAIN_MULTIPLIER],
gFlashOwlSaveNumPages[gSaveContext.fileNum * FLASH_SAVE_MAIN_MULTIPLIER]);
Sram_StartWriteToFlashOwlSave(sramCtx);
- gSaveContext.save.isOwlSave = false;
- gSaveContext.save.shipSaveInfo.pauseSaveEntrance = -1;
+ gSaveContext.save.isOwlSave = currentOwlSaveState;
+ SavingEnhancements_ClearSaveEntranceInfo();
} else {
Sram_SetFlashPagesDefault(sramCtx, gFlashSaveStartPages[gSaveContext.fileNum],
gFlashSaveNumPages[gSaveContext.fileNum]);