diff options
| author | Garrett Cox <garrettjcox@gmail.com> | 2026-01-24 17:52:29 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-24 17:52:29 -0600 |
| commit | 16ef59baeaf9f6955281b54d2f75ecc7c968a6c3 (patch) | |
| tree | f12d58ffca6e2212be42be29b2ee2bc16dc0e1e7 | |
| parent | 5229a531c2336349e12a4985dc25ebf5e13fc63a (diff) | |
WIP Tweaks to fix compatibility with 0th/4th day glitches (#1462)
| -rw-r--r-- | mm/2s2h/BenGui/BenMenu.cpp | 7 | ||||
| -rw-r--r-- | mm/2s2h/BenPort.cpp | 1 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Enhancements.cpp | 11 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Enhancements.h | 5 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Saving/AutoSave.cpp | 94 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp | 286 | ||||
| -rw-r--r-- | mm/2s2h/Enhancements/Saving/SavingEnhancements.h | 6 | ||||
| -rw-r--r-- | mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h | 16 | ||||
| -rw-r--r-- | mm/src/code/z_play.c | 2 | ||||
| -rw-r--r-- | mm/src/code/z_sram_NES.c | 21 | ||||
| -rw-r--r-- | mm/src/overlays/actors/ovl_En_Test4/z_en_test4.c | 36 | ||||
| -rw-r--r-- | mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope_NES.c | 11 |
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]); |
