From 4e20f4585f6ad634c55b2661f294c5843b6c7f99 Mon Sep 17 00:00:00 2001 From: MaxTomahawk Date: Mon, 10 Aug 2026 20:39:01 +0200 Subject: [PATCH] fix(mods): emit restore lifecycle after title resume --- src/core/Checkpoint.lua | 19 +++++++++++++------ .../cases/title_playthrough_context.lua | 11 +++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/core/Checkpoint.lua b/src/core/Checkpoint.lua index 8698c7ee..c210bbd1 100644 --- a/src/core/Checkpoint.lua +++ b/src/core/Checkpoint.lua @@ -386,6 +386,8 @@ local function firstDifference(a, b, path) return nil end +local emitRestored + function Checkpoint.restore(game, checkpoint) local capability = Checkpoint.inspect(game) if not capability.canRestore then @@ -403,12 +405,7 @@ function Checkpoint.restore(game, checkpoint) local restored, verifyCode = Checkpoint.capture(game) if restored and validated.rng == nil then restored.rng = nil end if restored and equalData(restored, validated) then - if ModRuntime.wants("checkpoint.restored") then - ModRuntime.emit("checkpoint.restored", { - game = game, - kind = validated.kind, - }) - end + emitRestored(game, validated) return true end err = restored and ("restored state differed at " @@ -424,6 +421,15 @@ function Checkpoint.restore(game, checkpoint) return false, "restore_failed", "Checkpoint restoration failed: " .. tostring(err) end +emitRestored = function(game, checkpoint) + if ModRuntime.wants("checkpoint.restored") then + ModRuntime.emit("checkpoint.restored", { + game = game, + kind = checkpoint.kind, + }) + end +end + local function isTitleSession(game) local states = game and game.stack and game.stack.states if type(states) ~= "table" then return false end @@ -479,6 +485,7 @@ function Checkpoint.resume(game, checkpoint) local restored, verifyCode = Checkpoint.capture(game) if restored and validated.rng == nil then restored.rng = nil end if restored and equalData(restored, validated) then + emitRestored(game, validated) return true end err = restored and ("resumed state differed at " diff --git a/tests/modkit/cases/title_playthrough_context.lua b/tests/modkit/cases/title_playthrough_context.lua index bff297b3..8adb87e5 100644 --- a/tests/modkit/cases/title_playthrough_context.lua +++ b/tests/modkit/cases/title_playthrough_context.lua @@ -60,6 +60,10 @@ local files = { return function(mod) _G.MOD_TITLE_STORAGE = mod.storage _G.MOD_TITLE_CHECKPOINTS = mod.checkpoints + mod.events:on("checkpoint.restored", function(ev) + _G.MOD_TITLE_RESTORE_COUNT = (_G.MOD_TITLE_RESTORE_COUNT or 0) + 1 + _G.MOD_TITLE_RESTORE_KIND = ev.kind + end) end ]], } @@ -194,6 +198,10 @@ if type(storage) == "table" then "title bootstrap never creates a normal Pokémon save as a side effect") T.same(checkpoints:capture(titleRuntime), checkpoint, "bootstrapped overworld differentially recaptures the selected checkpoint") + T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1, + "a successfully verified title resume emits checkpoint.restored exactly once") + T.eq(_G.MOD_TITLE_RESTORE_KIND, "overworld", + "title resume lifecycle reports the reconstructed checkpoint kind") -- Force a failure after restoreCheckpointSave has already installed the -- checkpoint's canonical save and overworld. Title has no live checkpoint @@ -217,6 +225,8 @@ if type(storage) == "table" then "failed title reconstruction retains current title options") T.check(files["save.lua"] == nil, "failed title reconstruction never writes a normal Pokémon save") + T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1, + "failed title reconstruction emits no additional restored lifecycle event") end -- A title policy may compare its own durable checkpoint chronology with the @@ -252,6 +262,7 @@ Runtime.currentMod = nil _G.MOD_TITLE_STORAGE = nil _G.MOD_TITLE_CHECKPOINTS = nil _G.MOD_TITLE_RESTORE_COUNT = nil +_G.MOD_TITLE_RESTORE_KIND = nil SaveData.resetSlotState() SaveData.loadOptions = originalLoadOptions