mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
feat(mods): expose selected save chronology
This commit is contained in:
+3
-1
@@ -208,7 +208,9 @@ local deleted, code, message = mod.storage:delete(game, "history/quick/q0001")
|
|||||||
|
|
||||||
`context` returns `{ engineVersion, gameVersion, playthroughId }`. The engine
|
`context` returns `{ engineVersion, gameVersion, playthroughId }`. The engine
|
||||||
version is compatibility metadata; physical launcher-slot and path identity stays
|
version is compatibility metadata; physical launcher-slot and path identity stays
|
||||||
private.
|
private. A title-selected context may additionally contain `normalSavedAt`, the
|
||||||
|
validated matching ordinary-save chronology only; it never exposes normal-save
|
||||||
|
progress or a slot/path handle.
|
||||||
|
|
||||||
At the title screen only, `mod.storage:selected(game)` returns a bound storage
|
At the title screen only, `mod.storage:selected(game)` returns a bound storage
|
||||||
facade for the launcher-selected existing playthrough, or `nil, code, message`.
|
facade for the launcher-selected existing playthrough, or `nil, code, message`.
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ selected existing game-version/playthrough and the calling mod id. It neither
|
|||||||
accepts an arbitrary playthrough id nor reveals a slot id, filesystem path, or
|
accepts an arbitrary playthrough id nor reveals a slot id, filesystem path, or
|
||||||
another mod namespace. Resolution is read-only; no selected mapping means
|
another mod namespace. Resolution is read-only; no selected mapping means
|
||||||
`no_selected_playthrough`, and opening a title browser never mints an identity.
|
`no_selected_playthrough`, and opening a title browser never mints an identity.
|
||||||
|
Its detached context can include only `normalSavedAt` from a matching ordinary
|
||||||
|
save, so title tools can apply their own resume policy without receiving the
|
||||||
|
canonical normal-save record.
|
||||||
|
|
||||||
### `mod.checkpoints:resume(game, checkpoint)`
|
### `mod.checkpoints:resume(game, checkpoint)`
|
||||||
|
|
||||||
|
|||||||
+44
-3
@@ -630,6 +630,15 @@ local function tryMigrateLegacy(version, fs)
|
|||||||
local opts = SaveData.loadOptions(fs)
|
local opts = SaveData.loadOptions(fs)
|
||||||
opts.saveSlots = opts.saveSlots or {}
|
opts.saveSlots = opts.saveSlots or {}
|
||||||
opts.saveSlots[version] = { list = { id }, active = id }
|
opts.saveSlots[version] = { list = { id }, active = id }
|
||||||
|
-- A tool may have allocated the legacy scope before the player made their
|
||||||
|
-- first ordinary SAVE. Promoting that flat save into slot1 must preserve the
|
||||||
|
-- same opaque identity; otherwise title-selected mod storage becomes
|
||||||
|
-- unreachable after the migration even though every durable record exists.
|
||||||
|
local ids = opts.playthroughIds and opts.playthroughIds[version]
|
||||||
|
if type(ids) == "table" and type(ids.legacy) == "string" and ids.legacy ~= "" then
|
||||||
|
if type(ids[id]) ~= "string" or ids[id] == "" then ids[id] = ids.legacy end
|
||||||
|
ids.legacy = nil
|
||||||
|
end
|
||||||
SaveData.saveOptions(opts, fs)
|
SaveData.saveOptions(opts, fs)
|
||||||
return id
|
return id
|
||||||
end
|
end
|
||||||
@@ -655,9 +664,9 @@ end
|
|||||||
|
|
||||||
-- (body for the forward-declared saveNames.) Resolves the ACTIVE slot for
|
-- (body for the forward-declared saveNames.) Resolves the ACTIVE slot for
|
||||||
-- the version, falling back to the flat legacy names when no slot is in use.
|
-- the version, falling back to the flat legacy names when no slot is in use.
|
||||||
function saveNames(version)
|
function saveNames(version, injectedFs)
|
||||||
version = version or GameVersion.get()
|
version = version or GameVersion.get()
|
||||||
local fs = persistFs(nil)
|
local fs = persistFs(injectedFs)
|
||||||
ensureVersionSlots(version, fs)
|
ensureVersionSlots(version, fs)
|
||||||
local slot = activeSlotCache[version]
|
local slot = activeSlotCache[version]
|
||||||
if slot then return slotNames(version, slot) end
|
if slot then return slotNames(version, slot) end
|
||||||
@@ -984,9 +993,13 @@ function SaveData.selectedPlaythroughId(save, injectedFs)
|
|||||||
local id = save.meta and save.meta.playthroughId
|
local id = save.meta and save.meta.playthroughId
|
||||||
if type(id) == "string" and id ~= "" then return id end
|
if type(id) == "string" and id ~= "" then return id end
|
||||||
|
|
||||||
|
-- Resolve the selected scope first. That may perform the one-time legacy
|
||||||
|
-- save-to-slot migration, which also moves the opaque identity mapping; only
|
||||||
|
-- then read options so this lookup never observes the pre-migration table.
|
||||||
|
local scope = playthroughScope(version, injectedFs)
|
||||||
local opts = SaveData.loadOptions(injectedFs)
|
local opts = SaveData.loadOptions(injectedFs)
|
||||||
local byVersion = opts.playthroughIds and opts.playthroughIds[version]
|
local byVersion = opts.playthroughIds and opts.playthroughIds[version]
|
||||||
id = byVersion and byVersion[playthroughScope(version, injectedFs)] or nil
|
id = byVersion and byVersion[scope] or nil
|
||||||
if type(id) ~= "string" or id == "" then
|
if type(id) ~= "string" or id == "" then
|
||||||
return nil, "no_selected_playthrough",
|
return nil, "no_selected_playthrough",
|
||||||
"The selected playthrough has no durable tool state."
|
"The selected playthrough has no durable tool state."
|
||||||
@@ -994,6 +1007,34 @@ function SaveData.selectedPlaythroughId(save, injectedFs)
|
|||||||
return id
|
return id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Read only the chronology of the ordinary selected save for title tools.
|
||||||
|
-- This intentionally returns no canonical progress, slot id, path, or raw
|
||||||
|
-- save handle. A legacy pre-id normal save is valid when the selected scope's
|
||||||
|
-- engine-owned mapping identifies it; a stamped id must match exactly.
|
||||||
|
function SaveData.selectedNormalSaveInfo(save, injectedFs)
|
||||||
|
local playthroughId, code, message = SaveData.selectedPlaythroughId(save, injectedFs)
|
||||||
|
if not playthroughId then return nil, code, message end
|
||||||
|
local version = save and (save.version or GameVersion.get())
|
||||||
|
local fs = persistFs(injectedFs)
|
||||||
|
local main, backup, staged = saveNames(version, injectedFs)
|
||||||
|
local normal = readTable(fs, main)
|
||||||
|
or readTable(fs, staged)
|
||||||
|
or readTable(fs, backup)
|
||||||
|
if type(normal) ~= "table" or normal.version ~= version then
|
||||||
|
return { savedAt = nil }
|
||||||
|
end
|
||||||
|
local normalId = normal.meta and normal.meta.playthroughId
|
||||||
|
if type(normalId) == "string" and normalId ~= "" and normalId ~= playthroughId then
|
||||||
|
return { savedAt = nil }
|
||||||
|
end
|
||||||
|
local savedAt = normal.meta and normal.meta.savedAt
|
||||||
|
if type(savedAt) ~= "number" or savedAt < 0 or savedAt ~= savedAt
|
||||||
|
or savedAt == math.huge or savedAt == -math.huge then
|
||||||
|
savedAt = nil
|
||||||
|
end
|
||||||
|
return { savedAt = savedAt }
|
||||||
|
end
|
||||||
|
|
||||||
-- ------- meta
|
-- ------- meta
|
||||||
|
|
||||||
-- the version/engine/mod-set stamp every v2 save carries; mods is the
|
-- the version/engine/mod-set stamp every v2 save carries; mods is the
|
||||||
|
|||||||
+13
-5
@@ -116,12 +116,20 @@ function Storage:selected(game)
|
|||||||
gameVersion = version,
|
gameVersion = version,
|
||||||
playthroughId = playthroughId,
|
playthroughId = playthroughId,
|
||||||
}
|
}
|
||||||
|
local normal = SaveData.selectedNormalSaveInfo(save, self.injectedFs)
|
||||||
|
if type(normal) == "table" and normal.savedAt ~= nil then
|
||||||
|
context.normalSavedAt = normal.savedAt
|
||||||
|
end
|
||||||
return {
|
return {
|
||||||
context = function() return {
|
context = function()
|
||||||
engineVersion = context.engineVersion,
|
local copy = {
|
||||||
gameVersion = context.gameVersion,
|
engineVersion = context.engineVersion,
|
||||||
playthroughId = context.playthroughId,
|
gameVersion = context.gameVersion,
|
||||||
} end,
|
playthroughId = context.playthroughId,
|
||||||
|
}
|
||||||
|
if context.normalSavedAt ~= nil then copy.normalSavedAt = context.normalSavedAt end
|
||||||
|
return copy
|
||||||
|
end,
|
||||||
read = function(_, key) return self:read(selectedGame, key) end,
|
read = function(_, key) return self:read(selectedGame, key) end,
|
||||||
write = function(_, key, value) return self:write(selectedGame, key, value) end,
|
write = function(_, key, value) return self:write(selectedGame, key, value) end,
|
||||||
list = function(_, prefix) return self:list(selectedGame, prefix) end,
|
list = function(_, prefix) return self:list(selectedGame, prefix) end,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ local T = require("tests.harness").suite("mod title playthrough context")
|
|||||||
local Loader = require("src.mods.Loader")
|
local Loader = require("src.mods.Loader")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local SaveSerializer = require("src.core.SaveSerializer")
|
||||||
local Version = require("src.core.Version")
|
local Version = require("src.core.Version")
|
||||||
local GameMethods = require("src.core.Game")
|
local GameMethods = require("src.core.Game")
|
||||||
local StateStack = require("src.core.StateStack")
|
local StateStack = require("src.core.StateStack")
|
||||||
@@ -227,6 +228,28 @@ if type(storage) == "table" then
|
|||||||
"failed title reconstruction never writes a normal Pokémon save")
|
"failed title reconstruction never writes a normal Pokémon save")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- A title policy may compare its own durable checkpoint chronology with the
|
||||||
|
-- ordinary CONTINUE target, but it must never receive that save's contents,
|
||||||
|
-- slot path, or a way to open another playthrough. This fixture writes the
|
||||||
|
-- canonical normal save directly to model an already-completed vanilla SAVE.
|
||||||
|
active.save.meta.savedAt = 4321
|
||||||
|
fs.write("save.lua", SaveSerializer.encode(active.save))
|
||||||
|
SaveData.resetSlotState()
|
||||||
|
local titleWithNormalSave = {
|
||||||
|
save = SaveData.newGame({ version = "red" }),
|
||||||
|
stack = { states = { { screenId = "TitleState" } } },
|
||||||
|
}
|
||||||
|
local selectedWithNormal, normalCode, normalMessage = storage:selected(titleWithNormalSave)
|
||||||
|
T.check(type(selectedWithNormal) == "table",
|
||||||
|
"legacy-to-slot migration keeps the selected playthrough identity: "
|
||||||
|
.. tostring(normalCode or normalMessage))
|
||||||
|
if type(selectedWithNormal) == "table" then
|
||||||
|
T.eq(selectedWithNormal:context().normalSavedAt, 4321,
|
||||||
|
"title selected context exposes only matching normal-save chronology")
|
||||||
|
end
|
||||||
|
T.check(titleWithNormalSave.save.meta.playthroughId == nil,
|
||||||
|
"normal-save chronology lookup does not bind the fresh title skeleton")
|
||||||
|
|
||||||
local explicitNewGame = SaveData.newGame({ version = "red" })
|
local explicitNewGame = SaveData.newGame({ version = "red" })
|
||||||
local freshContext = storage:context({ save = explicitNewGame })
|
local freshContext = storage:context({ save = explicitNewGame })
|
||||||
T.check(freshContext and freshContext.playthroughId ~= originalId,
|
T.check(freshContext and freshContext.playthroughId ~= originalId,
|
||||||
|
|||||||
Reference in New Issue
Block a user