mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +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
|
||||
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
|
||||
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
|
||||
another mod namespace. Resolution is read-only; no selected mapping means
|
||||
`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)`
|
||||
|
||||
|
||||
+44
-3
@@ -630,6 +630,15 @@ local function tryMigrateLegacy(version, fs)
|
||||
local opts = SaveData.loadOptions(fs)
|
||||
opts.saveSlots = opts.saveSlots or {}
|
||||
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)
|
||||
return id
|
||||
end
|
||||
@@ -655,9 +664,9 @@ end
|
||||
|
||||
-- (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.
|
||||
function saveNames(version)
|
||||
function saveNames(version, injectedFs)
|
||||
version = version or GameVersion.get()
|
||||
local fs = persistFs(nil)
|
||||
local fs = persistFs(injectedFs)
|
||||
ensureVersionSlots(version, fs)
|
||||
local slot = activeSlotCache[version]
|
||||
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
|
||||
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 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
|
||||
return nil, "no_selected_playthrough",
|
||||
"The selected playthrough has no durable tool state."
|
||||
@@ -994,6 +1007,34 @@ function SaveData.selectedPlaythroughId(save, injectedFs)
|
||||
return id
|
||||
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
|
||||
|
||||
-- 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,
|
||||
playthroughId = playthroughId,
|
||||
}
|
||||
local normal = SaveData.selectedNormalSaveInfo(save, self.injectedFs)
|
||||
if type(normal) == "table" and normal.savedAt ~= nil then
|
||||
context.normalSavedAt = normal.savedAt
|
||||
end
|
||||
return {
|
||||
context = function() return {
|
||||
engineVersion = context.engineVersion,
|
||||
gameVersion = context.gameVersion,
|
||||
playthroughId = context.playthroughId,
|
||||
} end,
|
||||
context = function()
|
||||
local copy = {
|
||||
engineVersion = context.engineVersion,
|
||||
gameVersion = context.gameVersion,
|
||||
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,
|
||||
write = function(_, key, value) return self:write(selectedGame, key, value) 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 Runtime = require("src.mods.Runtime")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local SaveSerializer = require("src.core.SaveSerializer")
|
||||
local Version = require("src.core.Version")
|
||||
local GameMethods = require("src.core.Game")
|
||||
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")
|
||||
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 freshContext = storage:context({ save = explicitNewGame })
|
||||
T.check(freshContext and freshContext.playthroughId ~= originalId,
|
||||
|
||||
Reference in New Issue
Block a user