mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-16 16:21:30 +02:00
feat(mods): resume selected checkpoints from title
This commit is contained in:
+83
-3
@@ -246,7 +246,7 @@ end
|
||||
|
||||
local FACINGS = { up = true, down = true, left = true, right = true }
|
||||
|
||||
local function validate(game, checkpoint)
|
||||
local function validate(game, checkpoint, expectedIdentity)
|
||||
if type(checkpoint) ~= "table" then
|
||||
return nil, "invalid_checkpoint", "Checkpoint root must be a table."
|
||||
end
|
||||
@@ -264,13 +264,16 @@ local function validate(game, checkpoint)
|
||||
end
|
||||
local identity = copy.identity
|
||||
local current = game and game.save
|
||||
local currentId = current and current.meta and current.meta.playthroughId
|
||||
local currentId = expectedIdentity and expectedIdentity.playthroughId
|
||||
or (current and current.meta and current.meta.playthroughId)
|
||||
local currentVersion = expectedIdentity and expectedIdentity.gameVersion
|
||||
or (current and current.version)
|
||||
if type(identity) ~= "table" or type(identity.engineVersion) ~= "string"
|
||||
or type(identity.gameVersion) ~= "string"
|
||||
or type(identity.playthroughId) ~= "string" then
|
||||
return nil, "invalid_checkpoint", "Checkpoint identity is missing or corrupt."
|
||||
end
|
||||
if identity.gameVersion ~= current.version then
|
||||
if identity.gameVersion ~= currentVersion then
|
||||
return nil, "wrong_game", "Checkpoint belongs to another game version."
|
||||
end
|
||||
if identity.playthroughId ~= currentId then
|
||||
@@ -421,4 +424,81 @@ function Checkpoint.restore(game, checkpoint)
|
||||
return false, "restore_failed", "Checkpoint restoration failed: " .. tostring(err)
|
||||
end
|
||||
|
||||
local function isTitleSession(game)
|
||||
local states = game and game.stack and game.stack.states
|
||||
if type(states) ~= "table" then return false end
|
||||
for _, state in ipairs(states) do
|
||||
if type(state) == "table" and state.screenId == "TitleState" then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function emitRestored(game, checkpoint)
|
||||
if ModRuntime.wants("checkpoint.restored") then
|
||||
ModRuntime.emit("checkpoint.restored", { game = game, kind = checkpoint.kind })
|
||||
end
|
||||
end
|
||||
|
||||
local function rebuildTitle(game, savedTitle, rng)
|
||||
local save, err = dataCopy(savedTitle)
|
||||
if not save then error("title rollback decode failed: " .. tostring(err), 0) end
|
||||
game.save = save
|
||||
if type(game.adoptSave) == "function" then game:adoptSave(save) end
|
||||
restoreRng(rng)
|
||||
if not (game.stack and game.stack.top and game.stack.pop and game.stack.push
|
||||
and type(game.makeTitleState) == "function") then
|
||||
error("title recovery is unavailable", 0)
|
||||
end
|
||||
while game.stack:top() do game.stack:pop() end
|
||||
game.stack:push(game:makeTitleState())
|
||||
end
|
||||
|
||||
-- Reconstruct a validated persistent checkpoint from the title session. This
|
||||
-- is intentionally separate from restore(): title has no live gameplay state
|
||||
-- to capture for rollback. Validation happens before any mutation; a failed
|
||||
-- reconstruction rebuilds a fresh usable title session instead of exposing a
|
||||
-- half-installed overworld or battle.
|
||||
function Checkpoint.resume(game, checkpoint)
|
||||
if not isTitleSession(game) then
|
||||
return false, "not_at_title",
|
||||
"Checkpoint resume is available only from the title session."
|
||||
end
|
||||
local save = game and game.save
|
||||
local playthroughId, identityCode, identityMessage =
|
||||
SaveData.selectedPlaythroughId(save)
|
||||
if type(playthroughId) ~= "string" or playthroughId == "" then
|
||||
return false, identityCode, identityMessage
|
||||
end
|
||||
local expected = { gameVersion = save and save.version, playthroughId = playthroughId }
|
||||
local validated, code, message = validate(game, checkpoint, expected)
|
||||
if not validated then return false, code, message end
|
||||
|
||||
local titleSave, titleErr = dataCopy(save)
|
||||
if not titleSave then
|
||||
return false, "title_recovery_unavailable",
|
||||
"Could not preserve the title session: " .. tostring(titleErr)
|
||||
end
|
||||
local titleRng = captureRng()
|
||||
local currentOptions = save.options
|
||||
local ok, err = pcall(apply, game, validated, currentOptions)
|
||||
if ok then
|
||||
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 "
|
||||
.. tostring(firstDifference(validated, restored) or "canonical encoding"))
|
||||
or ("resumed state could not be captured: " .. tostring(verifyCode))
|
||||
end
|
||||
|
||||
local recovered, recoveryErr = pcall(rebuildTitle, game, titleSave, titleRng)
|
||||
if not recovered then
|
||||
return false, "title_recovery_failed",
|
||||
"Checkpoint resume failed and title recovery failed: " .. tostring(recoveryErr)
|
||||
end
|
||||
return false, "resume_failed", "Checkpoint resume failed: " .. tostring(err)
|
||||
end
|
||||
|
||||
return Checkpoint
|
||||
|
||||
@@ -967,6 +967,33 @@ function SaveData.ensurePlaythroughId(save, injectedFs)
|
||||
return id
|
||||
end
|
||||
|
||||
-- Resolve the already-selected playthrough without changing the supplied save
|
||||
-- or allocating a replacement id. Title tools use this before a normal SAVE:
|
||||
-- Game:load intentionally owns a fresh skeleton there, while the selected
|
||||
-- launcher slot's durable tool data remains bound by the engine-owned mapping.
|
||||
-- This does not make arbitrary identities addressable; callers still need a
|
||||
-- higher-level engine capability that decides when selected resolution is safe.
|
||||
function SaveData.selectedPlaythroughId(save, injectedFs)
|
||||
if type(save) ~= "table" then
|
||||
return nil, "not_in_playthrough", "No selected playthrough is available."
|
||||
end
|
||||
local version = save.version or GameVersion.get()
|
||||
if not knownVersion(version) then
|
||||
return nil, "unknown_game", "The selected game version is unavailable."
|
||||
end
|
||||
local id = save.meta and save.meta.playthroughId
|
||||
if type(id) == "string" and id ~= "" then return id end
|
||||
|
||||
local opts = SaveData.loadOptions(injectedFs)
|
||||
local byVersion = opts.playthroughIds and opts.playthroughIds[version]
|
||||
id = byVersion and byVersion[playthroughScope(version, injectedFs)] or nil
|
||||
if type(id) ~= "string" or id == "" then
|
||||
return nil, "no_selected_playthrough",
|
||||
"The selected playthrough has no durable tool state."
|
||||
end
|
||||
return id
|
||||
end
|
||||
|
||||
-- ------- meta
|
||||
|
||||
-- the version/engine/mod-set stamp every v2 save carries; mods is the
|
||||
|
||||
Reference in New Issue
Block a user