From a67ee2ab15ebad916d88f55719ad8c24d950aa80 Mon Sep 17 00:00:00 2001 From: Eblo <7004497+Eblo@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:55:00 -0500 Subject: [PATCH 1/2] Persist entrance and respawn information in saves --- mm/2s2h/BenJsonConversions.hpp | 42 +++++++++++++ .../Saving/SavingEnhancements.cpp | 62 ++++++++++++------- mm/2s2h/GameInteractor/GameInteractor.cpp | 4 ++ mm/2s2h/GameInteractor/GameInteractor.h | 3 + mm/include/z64save.h | 1 + mm/src/code/z_eventmgr.c | 5 ++ .../ovl_file_choose/z_file_choose_NES.c | 3 + .../ovl_kaleido_scope/z_kaleido_scope_NES.c | 7 ++- 8 files changed, 103 insertions(+), 24 deletions(-) diff --git a/mm/2s2h/BenJsonConversions.hpp b/mm/2s2h/BenJsonConversions.hpp index 190f5202f4..34f91db4b2 100644 --- a/mm/2s2h/BenJsonConversions.hpp +++ b/mm/2s2h/BenJsonConversions.hpp @@ -20,16 +20,58 @@ void from_json(const json& j, DpadSaveInfo& dpadEquips) { } } +void to_json(json& j, const Vec3f& vec) { + j = json{ + { "x", vec.x }, + { "y", vec.y }, + { "z", vec.z }, + }; +} + +void from_json(const json& j, Vec3f& vec) { + j.at("x").get_to(vec.x); + j.at("y").get_to(vec.y); + j.at("z").get_to(vec.z); +} + +void to_json(json& j, const RespawnData& respawnData) { + j = json{ + { "pos", respawnData.pos }, + { "yaw", respawnData.yaw }, + { "playerParams", respawnData.playerParams }, + { "entrance", respawnData.entrance }, + { "roomIndex", respawnData.roomIndex }, + { "data", respawnData.data }, + { "tempSwitchFlags", respawnData.tempSwitchFlags }, + { "unk_18", respawnData.unk_18 }, + { "tempCollectFlags", respawnData.tempCollectFlags }, + }; +} + +void from_json(const json& j, RespawnData& respawnData) { + j.at("pos").get_to(respawnData.pos); + j.at("yaw").get_to(respawnData.yaw); + j.at("playerParams").get_to(respawnData.playerParams); + j.at("entrance").get_to(respawnData.entrance); + j.at("roomIndex").get_to(respawnData.roomIndex); + j.at("data").get_to(respawnData.data); + j.at("tempSwitchFlags").get_to(respawnData.tempSwitchFlags); + j.at("unk_18").get_to(respawnData.unk_18); + j.at("tempCollectFlags").get_to(respawnData.tempCollectFlags); +} + void to_json(json& j, const ShipSaveInfo& shipSaveInfo) { j = json { { "dpadEquips", shipSaveInfo.dpadEquips }, { "pauseSaveEntrance", shipSaveInfo.pauseSaveEntrance }, + { "respawn", shipSaveInfo.respawn}, }; } void from_json(const json& j, ShipSaveInfo& shipSaveInfo) { j.at("dpadEquips").get_to(shipSaveInfo.dpadEquips); j.at("pauseSaveEntrance").get_to(shipSaveInfo.pauseSaveEntrance); + j.at("respawn").get_to(shipSaveInfo.respawn); } void to_json(json& j, const ItemEquips& itemEquips) { diff --git a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp index 22fa326f72..c1bb2f0d01 100644 --- a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp +++ b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp @@ -14,30 +14,11 @@ static uint64_t lastSaveTimestamp = GetUnixTimestamp(); static HOOK_ID autosaveGameStateUpdateHookId = 0; static HOOK_ID autosaveGameStateDrawFinishHookId = 0; +static HOOK_ID loadCutsceneHookId = 0; // Used for saving through Autosaves and Pause Menu saves. extern "C" int SavingEnhancements_GetSaveEntrance() { - switch (gPlayState->sceneId) { - // Woodfall Temple + Odolwa - case SCENE_MITURIN: - case SCENE_MITURIN_BS: - return ENTRANCE(WOODFALL_TEMPLE, 0); - // Snowhead Temple + Goht - case SCENE_HAKUGIN: - case SCENE_HAKUGIN_BS: - return ENTRANCE(SNOWHEAD_TEMPLE, 0); - // Great Bay Temple + Gyorg - case SCENE_SEA: - case SCENE_SEA_BS: - return ENTRANCE(GREAT_BAY_TEMPLE, 0); - // Stone Tower Temple (+ inverted) + Twinmold - case SCENE_INISIE_N: - case SCENE_INISIE_R: - case SCENE_INISIE_BS: - return ENTRANCE(STONE_TOWER_TEMPLE, 0); - default: - return ENTRANCE(SOUTH_CLOCK_TOWN, 0); - } + return gSaveContext.save.entrance; } extern "C" bool SavingEnhancements_CanSave() { @@ -146,6 +127,34 @@ void HandleAutoSave() { } } +/* + * This respawn data is used for multiple things. Beyond the obvious usage for handling player respawns, this structure + * also maintains state information when entering shared grottos. This code executes from OnSaveLoad, which runs after + * save data is populated. This must run after that, otherwise the RESPAWN_MODE_DOWN entrance would get set to + * 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) { + for (int i = 0; i < RESPAWN_MODE_MAX; i++) { + gSaveContext.respawn[i] = gSaveContext.save.shipSaveInfo.respawn[i]; + } +} + +/* + * Upon loading a save, skip any cutscenes that would play if save is from a cutscene entrance (e.g. owl warps, Link + * bowing at Mikau's grave, etc.). An OnPassPlayerInputs hook is then used to unregister the entrance cutscene skip + * hook so that subsequent cutscenes are not also skipped. + */ +void skipEntranceCutsceneOnLoad(s16 fileNum) { + if (!loadCutsceneHookId) { + loadCutsceneHookId = REGISTER_VB_SHOULD(VB_START_CUTSCENE, { + if (CVarGetInteger("gEnhancements.Saving.PauseSave", 0) && loadCutsceneHookId) { + *should = false; + } + }); + } +} + void RegisterSavingEnhancements() { REGISTER_VB_SHOULD(VB_DELETE_OWL_SAVE, { if (CVarGetInteger("gEnhancements.Saving.PersistentOwlSaves", 0) || @@ -157,6 +166,17 @@ void RegisterSavingEnhancements() { GameInteractor::Instance->RegisterGameHook([]() { DeleteOwlSave(); }); GameInteractor::Instance->RegisterGameHook([]() { DeleteOwlSave(); }); + + GameInteractor::Instance->RegisterGameHook(loadRespawnData); + + GameInteractor::Instance->RegisterGameHook(skipEntranceCutsceneOnLoad); + + GameInteractor::Instance->RegisterGameHook([](Input* input) { + if (loadCutsceneHookId) { + GameInteractor::Instance->UnregisterGameHook(loadCutsceneHookId); + loadCutsceneHookId = 0; + } + }); } void RegisterAutosave() { diff --git a/mm/2s2h/GameInteractor/GameInteractor.cpp b/mm/2s2h/GameInteractor/GameInteractor.cpp index 78963a3e2d..864eee96ba 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.cpp +++ b/mm/2s2h/GameInteractor/GameInteractor.cpp @@ -42,6 +42,10 @@ void GameInteractor_ExecuteOnSaveInit(s16 fileNum) { GameInteractor::Instance->ExecuteHooks(fileNum); } +void GameInteractor_ExecuteOnSaveLoad(s16 fileNum) { + GameInteractor::Instance->ExecuteHooks(fileNum); +} + void GameInteractor_ExecuteBeforeEndOfCycleSave() { GameInteractor::Instance->ExecuteHooks(); } diff --git a/mm/2s2h/GameInteractor/GameInteractor.h b/mm/2s2h/GameInteractor/GameInteractor.h index b41a87e50b..ddc8fc8eef 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.h +++ b/mm/2s2h/GameInteractor/GameInteractor.h @@ -87,6 +87,7 @@ typedef enum { VB_HONEY_AND_DARLING_MINIGAME_FINISH, VB_MINIMAP_TOGGLE, VB_MONKEY_WAIT_TO_TALK_AFTER_APPROACH, + VB_START_CUTSCENE, } GIVanillaBehavior; typedef enum { @@ -308,6 +309,7 @@ class GameInteractor { DEFINE_HOOK(BeforeKaleidoDrawPage, (PauseContext * pauseCtx, u16 pauseIndex)); DEFINE_HOOK(AfterKaleidoDrawPage, (PauseContext * pauseCtx, u16 pauseIndex)); DEFINE_HOOK(OnSaveInit, (s16 fileNum)); + DEFINE_HOOK(OnSaveLoad, (s16 fileNum)); DEFINE_HOOK(BeforeEndOfCycleSave, ()); DEFINE_HOOK(AfterEndOfCycleSave, ()); DEFINE_HOOK(BeforeMoonCrashSaveReset, ()); @@ -358,6 +360,7 @@ void GameInteractor_ExecuteOnKaleidoUpdate(PauseContext* pauseCtx); void GameInteractor_ExecuteBeforeKaleidoDrawPage(PauseContext* pauseCtx, u16 pauseIndex); void GameInteractor_ExecuteAfterKaleidoDrawPage(PauseContext* pauseCtx, u16 pauseIndex); void GameInteractor_ExecuteOnSaveInit(s16 fileNum); +void GameInteractor_ExecuteOnSaveLoad(s16 fileNum); void GameInteractor_ExecuteBeforeEndOfCycleSave(); void GameInteractor_ExecuteAfterEndOfCycleSave(); void GameInteractor_ExecuteBeforeMoonCrashSaveReset(); diff --git a/mm/include/z64save.h b/mm/include/z64save.h index 73cc19e1f4..41e6c8b83b 100644 --- a/mm/include/z64save.h +++ b/mm/include/z64save.h @@ -330,6 +330,7 @@ typedef struct DpadSaveInfo { typedef struct ShipSaveInfo { DpadSaveInfo dpadEquips; s32 pauseSaveEntrance; + RespawnData respawn[RESPAWN_MODE_MAX]; } ShipSaveInfo; // #endregion diff --git a/mm/src/code/z_eventmgr.c b/mm/src/code/z_eventmgr.c index d682ed4f44..35c4cae632 100644 --- a/mm/src/code/z_eventmgr.c +++ b/mm/src/code/z_eventmgr.c @@ -7,6 +7,7 @@ #include "global.h" #include "z64shrink_window.h" #include "libc/string.h" +#include "2s2h/GameInteractor/GameInteractor.h" ActorCutscene sGlobalCutsceneList[] = { // CS_ID_GLOBAL_78 @@ -375,6 +376,10 @@ s16 CutsceneManager_StartWithPlayerCsAndSetFlag(s16 csId, Actor* actor) { } s16 CutsceneManager_Start(s16 csId, Actor* actor) { + if (!GameInteractor_Should(VB_START_CUTSCENE, true, &csId, actor)) { + return CS_ID_NONE; + } + ActorCutscene* csEntry; Camera* subCam; Camera* retCam; diff --git a/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c b/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c index 6befa39484..733f252b57 100644 --- a/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c +++ b/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c @@ -13,6 +13,7 @@ #include "interface/parameter_static/parameter_static.h" #include "misc/title_static/title_static.h" #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" +#include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h_assets.h" #include #include "BenPort.h" @@ -2231,6 +2232,8 @@ void FileSelect_LoadGame(GameState* thisx) { gSaveContext.hudVisibilityTimer = 0; gSaveContext.save.saveInfo.playerData.tatlTimer = 0; + + GameInteractor_ExecuteOnSaveLoad(gSaveContext.fileNum); } void (*sSelectModeUpdateFuncs[])(GameState*) = { 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 6df5ba5f16..7b46f4a958 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 @@ -3561,10 +3561,11 @@ void KaleidoScope_Update(PlayState* play) { Audio_PlaySfx(NA_SE_SY_PIECE_OF_HEART); 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 + // 2S2H [Enhancement] Maintain respawn information, used for grottos gSaveContext.save.shipSaveInfo.pauseSaveEntrance = SavingEnhancements_GetSaveEntrance(); + for (int i = 0; i < RESPAWN_MODE_MAX; i++) { + gSaveContext.save.shipSaveInfo.respawn[i] = gSaveContext.respawn[i]; + } } Play_SaveCycleSceneFlags(&play->state); gSaveContext.save.saveInfo.playerData.savedSceneId = play->sceneId; From 13261aceeb04cb0e4aef0df75227b0539f2b0f8f Mon Sep 17 00:00:00 2001 From: Eblo <7004497+Eblo@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:16:29 -0500 Subject: [PATCH 2/2] Add save migration for respawn data --- mm/2s2h/SaveManager/Migrations/6.cpp | 28 ++++++++++++++++++++++++++++ mm/2s2h/SaveManager/SaveManager.cpp | 5 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 mm/2s2h/SaveManager/Migrations/6.cpp diff --git a/mm/2s2h/SaveManager/Migrations/6.cpp b/mm/2s2h/SaveManager/Migrations/6.cpp new file mode 100644 index 0000000000..2061f75563 --- /dev/null +++ b/mm/2s2h/SaveManager/Migrations/6.cpp @@ -0,0 +1,28 @@ +#include "2s2h/SaveManager/SaveManager.h" +#include "z64.h" + +// Add respawn info to saves +void SaveManager_Migration_6(nlohmann::json& j) { + if (!j["save"]["shipSaveInfo"].contains("respawn")) { + j["save"]["shipSaveInfo"]["respawn"] = {}; + } + + for (int i = 0; i < RESPAWN_MODE_MAX; i++) { + j["save"]["shipSaveInfo"]["respawn"][i] = { + { "pos", + { + { "x", 0.0 }, + { "y", 0.0 }, + { "z", 0.0 }, + } }, + { "yaw", 0 }, + { "playerParams", 0 }, + { "entrance", 0 }, + { "roomIndex", 0 }, + { "data", 0 }, + { "tempSwitchFlags", 0 }, + { "unk_18", 0 }, + { "tempCollectFlags", 0 }, + }; + } +} diff --git a/mm/2s2h/SaveManager/SaveManager.cpp b/mm/2s2h/SaveManager/SaveManager.cpp index a841ca4396..f0dd88ada3 100644 --- a/mm/2s2h/SaveManager/SaveManager.cpp +++ b/mm/2s2h/SaveManager/SaveManager.cpp @@ -49,13 +49,14 @@ const std::filesystem::path savesFolderPath(Ship::Context::GetPathRelativeToAppD // - Create the migration file in the Migrations folder with the name `{CURRENT_SAVE_VERSION}.cpp` // - Add the migration function definition below and add it to the `migrations` map with the key being the previous // version -const uint32_t CURRENT_SAVE_VERSION = 5; +const uint32_t CURRENT_SAVE_VERSION = 6; void SaveManager_Migration_1(nlohmann::json& j); void SaveManager_Migration_2(nlohmann::json& j); void SaveManager_Migration_3(nlohmann::json& j); void SaveManager_Migration_4(nlohmann::json& j); void SaveManager_Migration_5(nlohmann::json& j); +void SaveManager_Migration_6(nlohmann::json& j); const std::unordered_map> migrations = { // Pre-1.0.0 Migrations, deprecated @@ -65,6 +66,8 @@ const std::unordered_map> migrati { 3, SaveManager_Migration_4 }, // Base Migration { 4, SaveManager_Migration_5 }, + // Persist respawn data + { 5, SaveManager_Migration_6 }, }; int SaveManager_MigrateSave(nlohmann::json& j) {