mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
feat: anchor first checkpoint for cold restart
This commit is contained in:
+15
-1
@@ -238,6 +238,11 @@ if capability.canCapture then
|
||||
end
|
||||
|
||||
local ok, code, message = mod.checkpoints:restore(game, checkpoint)
|
||||
|
||||
-- After the tool has durably committed its first checkpoint, make a
|
||||
-- never-saved playthrough reachable through ordinary title boot exactly once.
|
||||
local anchored, anchorCode, anchorMessage =
|
||||
mod.checkpoints:ensureNormalSave(game, checkpoint)
|
||||
```
|
||||
|
||||
Checkpoint format 1 supports settled overworld control and proven battle
|
||||
@@ -287,8 +292,17 @@ engine-selected existing playthrough, reconstructs only after all validation
|
||||
passes, preserves current options, and verifies by recapture. A title session
|
||||
has no live gameplay rollback state: if reconstruction or verification fails,
|
||||
the engine rebuilds a usable title session and returns `false, code, message`.
|
||||
It never writes a normal Pokémon save. It is unavailable outside title and does
|
||||
It never rewrites a normal Pokémon save. It is unavailable outside title and does
|
||||
not broaden capture or arbitrary-frame support.
|
||||
|
||||
`mod.checkpoints:ensureNormalSave(game, checkpoint)` is a separate live-runtime
|
||||
operation for durable checkpoint tools. It creates ordinary progress only when
|
||||
none exists, only after validating that the supplied checkpoint is the exact
|
||||
current safe runtime, and through the normal atomic save lifecycle. Once an
|
||||
ordinary save exists it returns `true, "already_exists"` without writing, so
|
||||
subsequent checkpoints and the player's later SAVE commands remain independent.
|
||||
Call it only after the tool's own checkpoint/index commit; treat an anchoring
|
||||
failure as a failed first checkpoint rather than claiming restart safety.
|
||||
See RFC 0003, RFC 0004, RFC 0005, and RFC 0006 for exact contracts and error
|
||||
codes.
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@ checkpoint, title, save-slot, and no-mod parity suites.
|
||||
A tool checkpoint may be the first durable record of a new playthrough. The
|
||||
engine intentionally keeps normal Pokémon SAVE independent: before the first
|
||||
normal write, identity is retained by the engine-owned selected-slot mapping,
|
||||
while title starts with a fresh New Game skeleton. Calling ordinary active
|
||||
while title starts with a fresh New Game skeleton. A durable checkpoint tool may
|
||||
explicitly create one ordinary progress anchor after its first checkpoint has
|
||||
committed; later tool writes must remain independent. Calling ordinary active
|
||||
`mod.storage` there would allocate/adopt an identity, and live
|
||||
`mod.checkpoints:restore` correctly refuses title because it has no gameplay
|
||||
rollback state. A generic title capability is required; a tool must not use
|
||||
private storage paths, slot ids, or a hidden normal SAVE.
|
||||
rollback state. Generic public capabilities are required; a tool must not use
|
||||
private storage paths, slot ids, or simulate the player's SAVE menu flow.
|
||||
|
||||
## Additive public API
|
||||
|
||||
@@ -40,6 +42,21 @@ 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:ensureNormalSave(game, checkpoint)`
|
||||
|
||||
Available only at a live checkpoint-safe boundary. After a tool has durably
|
||||
committed the supplied current checkpoint, it may request an ordinary progress
|
||||
anchor for a playthrough that has never had one. The engine validates the
|
||||
checkpoint, proves it exactly matches a fresh capture of the live runtime, and
|
||||
uses the normal atomic save path including `save.write` lifecycle/veto hooks.
|
||||
|
||||
The operation is idempotent. It returns `true, "already_exists"` without writing
|
||||
when matching normal progress already exists, so later checkpoints never move
|
||||
the vanilla CONTINUE target. A stale/non-current checkpoint, unsafe runtime,
|
||||
write veto/failure, or failed readback returns a structured failure. A tool
|
||||
should call it only after its own checkpoint and index are durable and must not
|
||||
report that first checkpoint as successful if the required anchor fails.
|
||||
|
||||
### `mod.checkpoints:resume(game, checkpoint)`
|
||||
|
||||
Available only from title. It validates format, data-only structure, selected
|
||||
@@ -50,7 +67,7 @@ recaptures before committing. On success it emits `checkpoint.restored` once.
|
||||
|
||||
Title has no live runtime rollback. A reconstruction or verification failure
|
||||
therefore rebuilds a clean title session from the pre-operation title save and
|
||||
RNG; it emits no success event and never writes normal progress. Validation
|
||||
RNG; it emits no success event and never rewrites normal progress. Validation
|
||||
failure leaves the existing title session untouched. Stable errors include
|
||||
`not_at_title`, `no_selected_playthrough`, normal checkpoint validation codes,
|
||||
`resume_failed`, and `title_recovery_failed`.
|
||||
@@ -65,9 +82,11 @@ does not rewind with a checkpoint; canonical `game.save` / `mod.save` does.
|
||||
|
||||
## Verification
|
||||
|
||||
The public SDK test starts a fresh playthrough, stores tool history without a
|
||||
normal SAVE, simulates title/restart, reads the selected binding without
|
||||
The public SDK test starts a fresh playthrough, stores tool history, creates and
|
||||
readback-verifies exactly one normal anchor, proves subsequent calls do not
|
||||
rewrite it, simulates title/restart, reads the selected binding without
|
||||
allocating title identity, resumes an overworld checkpoint, preserves options,
|
||||
performs no normal SAVE, differentially recaptures, and confirms a later
|
||||
explicit NEW GAME receives another identity. Existing no-mod, storage,
|
||||
checkpoint, battle, and title suites prove additive parity.
|
||||
differentially recaptures, and confirms a later explicit NEW GAME receives
|
||||
another identity. A separate two-process disk test proves cold-start routing and
|
||||
reconstruction. Existing no-mod, storage, checkpoint, battle, and title suites
|
||||
prove additive parity.
|
||||
|
||||
@@ -77,6 +77,8 @@ run_tier "T0 NX Yellow/Blue boot (dynamic paths)" "$LUA" tests/engine/nx_yellow_
|
||||
run_tier "T0 touch-controls pad cursor" "$LUA" tests/engine/touch_controls_pad_cursor_test.lua
|
||||
run_tier "T1/T2 engine invariants + parity gates" "$LUA" tests/run_engine.lua
|
||||
run_tier "T4 mod-SDK" "$LUA" tests/run_modkit.lua
|
||||
run_tier "T4 title checkpoint cold restart" \
|
||||
bash tests/integration/title_checkpoint_cold_start.sh
|
||||
|
||||
# The modded-link desync suite (symmetric mod, handshake fail-closed,
|
||||
# extra-bag round trip) is ROM-free and runs inside the T4 tier above, as
|
||||
|
||||
@@ -388,6 +388,47 @@ end
|
||||
|
||||
local emitRestored
|
||||
|
||||
-- Persist the current verified checkpoint as the ordinary progress anchor only
|
||||
-- when this playthrough has never had one. This is intentionally idempotent:
|
||||
-- durable checkpoint tools can make a first session resumable without turning
|
||||
-- every later checkpoint into a hidden normal SAVE. The live runtime must still
|
||||
-- match the supplied checkpoint, and the ordinary save.write veto/lifecycle
|
||||
-- remains authoritative through Game:writeSave().
|
||||
function Checkpoint.ensureNormalSave(game, checkpoint, injectedFs)
|
||||
local capability = Checkpoint.inspect(game)
|
||||
if not capability.canCapture then
|
||||
return false, capability.reason, capability.message
|
||||
end
|
||||
local validated, code, message = validate(game, checkpoint)
|
||||
if not validated then return false, code, message end
|
||||
|
||||
local info, infoCode, infoMessage =
|
||||
SaveData.selectedNormalSaveInfo(game.save, injectedFs)
|
||||
if not info then return false, infoCode, infoMessage end
|
||||
if info.exists then return true, "already_exists" end
|
||||
|
||||
local current, captureCode, captureMessage = Checkpoint.capture(game)
|
||||
if not current then return false, captureCode, captureMessage end
|
||||
if not equalData(current, validated) then
|
||||
return false, "checkpoint_not_current",
|
||||
"The active runtime changed after this checkpoint was captured."
|
||||
end
|
||||
if type(game.writeSave) ~= "function" then
|
||||
return false, "save_unavailable",
|
||||
"The active runtime cannot persist ordinary progress."
|
||||
end
|
||||
local ok, saved = pcall(game.writeSave, game)
|
||||
if not ok or saved == false then
|
||||
return false, "save_failed", "Could not create the first ordinary progress save."
|
||||
end
|
||||
local verified = SaveData.selectedNormalSaveInfo(game.save, injectedFs)
|
||||
if type(verified) ~= "table" or not verified.exists then
|
||||
return false, "save_verify_failed",
|
||||
"The first ordinary progress save could not be verified."
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function Checkpoint.restore(game, checkpoint)
|
||||
local capability = Checkpoint.inspect(game)
|
||||
if not capability.canRestore then
|
||||
|
||||
+14
-4
@@ -941,7 +941,17 @@ local function rememberPlaythroughId(save, opts, injectedFs)
|
||||
if type(id) ~= "string" or id == "" then return opts, false end
|
||||
local version = save.version or GameVersion.get()
|
||||
local scope = playthroughScope(version, injectedFs)
|
||||
opts = opts or SaveData.loadOptions(injectedFs)
|
||||
local persisted = SaveData.loadOptions(injectedFs)
|
||||
if opts then
|
||||
-- Slot selection and opaque playthrough routing are engine-owned launcher
|
||||
-- state. A live game may carry an options snapshot from before a legacy
|
||||
-- save was promoted to slot1; writing that stale snapshot must not erase
|
||||
-- the freshly persisted routing and strand tool storage on next boot.
|
||||
opts.saveSlots = deepCopy(persisted.saveSlots)
|
||||
opts.playthroughIds = deepCopy(persisted.playthroughIds)
|
||||
else
|
||||
opts = persisted
|
||||
end
|
||||
opts.playthroughIds = opts.playthroughIds or {}
|
||||
opts.playthroughIds[version] = opts.playthroughIds[version] or {}
|
||||
local changed = opts.playthroughIds[version][scope] ~= id
|
||||
@@ -1021,18 +1031,18 @@ function SaveData.selectedNormalSaveInfo(save, injectedFs)
|
||||
or readTable(fs, staged)
|
||||
or readTable(fs, backup)
|
||||
if type(normal) ~= "table" or normal.version ~= version then
|
||||
return { savedAt = nil }
|
||||
return { exists = false, savedAt = nil }
|
||||
end
|
||||
local normalId = normal.meta and normal.meta.playthroughId
|
||||
if type(normalId) == "string" and normalId ~= "" and normalId ~= playthroughId then
|
||||
return { savedAt = nil }
|
||||
return { exists = false, 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 }
|
||||
return { exists = true, savedAt = savedAt }
|
||||
end
|
||||
|
||||
-- ------- meta
|
||||
|
||||
@@ -683,6 +683,9 @@ function Loader:_api(mod)
|
||||
resume = function(_, game, checkpoint)
|
||||
return Checkpoint.resume(game, checkpoint)
|
||||
end,
|
||||
ensureNormalSave = function(_, game, checkpoint)
|
||||
return Checkpoint.ensureNormalSave(game, checkpoint, loader.fs)
|
||||
end,
|
||||
},
|
||||
options = {
|
||||
define = function(_, schema)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local phase, root = arg[1], arg[2]
|
||||
assert(phase == "capture" or phase == "resume", "phase must be capture or resume")
|
||||
assert(type(root) == "string" and root ~= "", "test needs a persistence root")
|
||||
|
||||
local function quote(value)
|
||||
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function full(path) return root .. "/" .. path end
|
||||
|
||||
local fs = {}
|
||||
function fs.createDirectory(path)
|
||||
return os.execute("mkdir -p " .. quote(full(path))) == 0
|
||||
end
|
||||
function fs.write(path, body)
|
||||
local parent = path:match("^(.*)/[^/]+$")
|
||||
if parent then assert(fs.createDirectory(parent)) end
|
||||
local handle = assert(io.open(full(path), "wb"))
|
||||
handle:write(body)
|
||||
handle:close()
|
||||
return true
|
||||
end
|
||||
function fs.read(path)
|
||||
local handle = io.open(full(path), "rb")
|
||||
if not handle then return nil end
|
||||
local body = handle:read("*a")
|
||||
handle:close()
|
||||
return body
|
||||
end
|
||||
function fs.remove(path)
|
||||
os.remove(full(path))
|
||||
return true
|
||||
end
|
||||
function fs.getInfo(path)
|
||||
if os.execute("test -d " .. quote(full(path))) == 0 then
|
||||
return { type = "directory" }
|
||||
end
|
||||
local handle = io.open(full(path), "rb")
|
||||
if handle then handle:close(); return { type = "file" } end
|
||||
return nil
|
||||
end
|
||||
function fs.load(path)
|
||||
local body = fs.read(path)
|
||||
if not body then return nil, "no file: " .. path end
|
||||
return load(body, "@" .. path)
|
||||
end
|
||||
function fs.getDirectoryItems(path)
|
||||
local items = {}
|
||||
local pipe = io.popen("find " .. quote(full(path))
|
||||
.. " -mindepth 1 -maxdepth 1 -printf '%f\\n' 2>/dev/null")
|
||||
if pipe then
|
||||
for item in pipe:lines() do items[#items + 1] = item end
|
||||
pipe:close()
|
||||
end
|
||||
table.sort(items)
|
||||
return items
|
||||
end
|
||||
function fs.getSaveDirectory() return root end
|
||||
|
||||
love = require("tests.love_stub")
|
||||
love.filesystem = fs
|
||||
|
||||
local Loader = require("src.mods.Loader")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local SaveSerializer = require("src.core.SaveSerializer")
|
||||
local GameMethods = require("src.core.Game")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
|
||||
local function writeProbe()
|
||||
fs.write("mods/cold_start_probe/manifest.json",
|
||||
'{"id":"cold_start_probe","name":"cold start probe","version":"1.0.0",'
|
||||
.. '"entry":"main.lua","api":2,"profile":"content"}')
|
||||
fs.write("mods/cold_start_probe/main.lua", [[
|
||||
return function(mod)
|
||||
_G.COLD_STORAGE = mod.storage
|
||||
_G.COLD_CHECKPOINTS = mod.checkpoints
|
||||
end
|
||||
]])
|
||||
end
|
||||
|
||||
local function runtime(save, title)
|
||||
local stack = setmetatable({ states = {} }, { __index = StateStack })
|
||||
local game
|
||||
local overworld = {
|
||||
map = { id = "PALLET_TOWN" },
|
||||
player = { cellX = 3, cellY = 6, facing = "down", surfing = false },
|
||||
scriptMoves = {}, pendingScripts = {}, parallelRunners = {}, parallelQueue = {},
|
||||
runner = { isRunning = function() return false end },
|
||||
}
|
||||
function overworld:captureSave(target)
|
||||
target.player.map, target.player.x, target.player.y = self.map.id,
|
||||
self.player.cellX, self.player.cellY
|
||||
target.player.facing, target.player.surfing = self.player.facing,
|
||||
self.player.surfing and true or false
|
||||
end
|
||||
function overworld:enter(mapId, x, y, facing)
|
||||
self.map = { id = mapId }
|
||||
self.player = { cellX = x, cellY = y, facing = facing,
|
||||
surfing = game.save.player.surfing and true or false }
|
||||
self.scriptMoves, self.pendingScripts = {}, {}
|
||||
self.parallelRunners, self.parallelQueue = {}, {}
|
||||
self.runner = { isRunning = function() return false end }
|
||||
end
|
||||
game = setmetatable({
|
||||
save = save, stack = stack, overworld = overworld,
|
||||
data = {
|
||||
pokemon = {}, moves = { TACKLE = { pp = 35 } }, items = { POTION = {} },
|
||||
constants = { fallbackMove = "TACKLE" },
|
||||
field = { boot = { startMap = "PALLET_TOWN", startX = 3, startY = 6 } },
|
||||
maps = { PALLET_TOWN = { id = "PALLET_TOWN", width = 10, height = 9 } },
|
||||
},
|
||||
}, { __index = GameMethods })
|
||||
stack.states[1] = title and { screenId = "TitleState" } or overworld
|
||||
if title then function game:makeTitleState() return { screenId = "TitleState" } end end
|
||||
return game
|
||||
end
|
||||
|
||||
writeProbe()
|
||||
SaveData.resetSlotState()
|
||||
local loader = Loader.new({ fs = fs })
|
||||
|
||||
if phase == "capture" then
|
||||
local game = runtime(SaveData.newGame({ version = "red" }), false)
|
||||
loader.game = game
|
||||
assert(loader:load({}) == true)
|
||||
assert(_G.COLD_STORAGE:write(game, "history/index", { newest = "q0001" }))
|
||||
local checkpoint = assert(_G.COLD_CHECKPOINTS:capture(game))
|
||||
assert(_G.COLD_STORAGE:write(game, "history/q0001", checkpoint))
|
||||
local id = assert(game.save.meta.playthroughId)
|
||||
assert(_G.COLD_CHECKPOINTS:ensureNormalSave(game, checkpoint))
|
||||
local normal = assert(SaveData.load("red"))
|
||||
assert(normal.meta.playthroughId == id)
|
||||
fs.write("cold-start-witness.lua", SaveSerializer.encode({ playthroughId = id }))
|
||||
print("cold-start capture persisted")
|
||||
else
|
||||
local title = runtime(SaveData.newGame({ version = "red" }), true)
|
||||
title.save.options = { volume = 7, bindings = {} }
|
||||
loader.game = title
|
||||
assert(loader:load({}) == true)
|
||||
local selected = assert(_G.COLD_STORAGE:selected(title))
|
||||
local witness = assert(SaveSerializer.decode(assert(fs.read("cold-start-witness.lua"))))
|
||||
assert(selected:context().playthroughId == witness.playthroughId)
|
||||
assert(selected:read("history/index").newest == "q0001")
|
||||
local checkpoint = assert(selected:read("history/q0001"))
|
||||
assert(_G.COLD_CHECKPOINTS:resume(title, checkpoint))
|
||||
assert(title.save.meta.playthroughId == witness.playthroughId)
|
||||
assert(title.save.options.volume == 7)
|
||||
assert(SaveSerializer.encode(_G.COLD_CHECKPOINTS:capture(title))
|
||||
== SaveSerializer.encode(checkpoint))
|
||||
local normal = assert(SaveData.load("red"))
|
||||
assert(normal.meta.playthroughId == witness.playthroughId)
|
||||
print("cold-start resume reconstructed")
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
test_root="$(mktemp -d)"
|
||||
trap 'rm -rf "$test_root"' EXIT
|
||||
|
||||
"${LUA:-luajit}" tests/integration/title_checkpoint_cold_start.lua capture "$test_root"
|
||||
"${LUA:-luajit}" tests/integration/title_checkpoint_cold_start.lua resume "$test_root"
|
||||
|
||||
@@ -19,6 +19,7 @@ local GameMethods = require("src.core.Game")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
|
||||
local savedEvents, savedHooks = Runtime.events, Runtime.hooks
|
||||
local realFs = love.filesystem
|
||||
|
||||
local function memfs(files)
|
||||
return {
|
||||
@@ -68,6 +69,7 @@ end
|
||||
]],
|
||||
}
|
||||
local fs = memfs(files)
|
||||
love.filesystem = fs
|
||||
-- Production storage and checkpoint resume share the same engine persistence
|
||||
-- backend. Route the test's default SaveData lookup to this fixture backend so
|
||||
-- the restart path exercises that shared mapping rather than host test files.
|
||||
@@ -181,6 +183,30 @@ if type(storage) == "table" then
|
||||
local checkpoint = checkpoints and checkpoints:capture(runtime)
|
||||
T.check(type(checkpoint) == "table",
|
||||
"a fresh playthrough can capture a stable overworld checkpoint")
|
||||
T.check(type(checkpoints and checkpoints.ensureNormalSave) == "function",
|
||||
"public checkpoints expose an idempotent first-save anchor")
|
||||
local normalWrites = 0
|
||||
local writeSave = runtime.writeSave
|
||||
function runtime:writeSave()
|
||||
normalWrites = normalWrites + 1
|
||||
return writeSave(self)
|
||||
end
|
||||
local anchored, anchorCode, anchorMessage =
|
||||
checkpoints:ensureNormalSave(runtime, checkpoint)
|
||||
T.check(anchored == true,
|
||||
"first persisted checkpoint can anchor normal progress: "
|
||||
.. tostring(anchorCode or anchorMessage))
|
||||
T.eq(normalWrites, 1,
|
||||
"first checkpoint creates exactly one normal Pokemon save")
|
||||
local anchoredAgain, againCode = checkpoints:ensureNormalSave(runtime, checkpoint)
|
||||
T.check(anchoredAgain == true and againCode == "already_exists",
|
||||
"later checkpoints leave the established normal save independent")
|
||||
T.eq(normalWrites, 1,
|
||||
"idempotent anchor never rewrites the established normal save")
|
||||
local normalBytes = files["save.lua"]
|
||||
T.check(type(normalBytes) == "string" and normalBytes ~= "",
|
||||
"first checkpoint anchor is durably represented before restart")
|
||||
local anchoredAt = SaveSerializer.decode(normalBytes).meta.savedAt
|
||||
SaveData.resetSlotState()
|
||||
local titleRuntime = makeRuntime(SaveData.newGame({ version = "red" }), true)
|
||||
titleRuntime.save.options = { volume = 9, bindings = {} }
|
||||
@@ -194,8 +220,10 @@ if type(storage) == "table" then
|
||||
"title bootstrap retains the checkpoint's original playthrough identity")
|
||||
T.eq(titleRuntime.save.options.volume, 9,
|
||||
"title bootstrap preserves current options rather than rewinding them")
|
||||
T.check(files["save.lua"] == nil,
|
||||
"title bootstrap never creates a normal Pokémon save as a side effect")
|
||||
T.eq(SaveData.selectedNormalSaveInfo({
|
||||
version = "red", meta = { playthroughId = originalId },
|
||||
}, fs).savedAt, anchoredAt,
|
||||
"title bootstrap never rewrites the first normal save")
|
||||
T.same(checkpoints:capture(titleRuntime), checkpoint,
|
||||
"bootstrapped overworld differentially recaptures the selected checkpoint")
|
||||
T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1,
|
||||
@@ -223,8 +251,10 @@ if type(storage) == "table" then
|
||||
"failed title reconstruction restores the unbound title skeleton")
|
||||
T.eq(failingTitle.save.options.volume, 7,
|
||||
"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(SaveData.selectedNormalSaveInfo({
|
||||
version = "red", meta = { playthroughId = originalId },
|
||||
}, fs).savedAt, anchoredAt,
|
||||
"failed title reconstruction never rewrites the normal Pokémon save")
|
||||
T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1,
|
||||
"failed title reconstruction emits no additional restored lifecycle event")
|
||||
end
|
||||
@@ -234,7 +264,8 @@ if type(storage) == "table" then
|
||||
-- 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))
|
||||
T.check(SaveData.save(active.save) == true,
|
||||
"fixture updates the selected normal save chronology")
|
||||
SaveData.resetSlotState()
|
||||
local titleWithNormalSave = {
|
||||
save = SaveData.newGame({ version = "red" }),
|
||||
@@ -265,5 +296,6 @@ _G.MOD_TITLE_RESTORE_COUNT = nil
|
||||
_G.MOD_TITLE_RESTORE_KIND = nil
|
||||
SaveData.resetSlotState()
|
||||
SaveData.loadOptions = originalLoadOptions
|
||||
love.filesystem = realFs
|
||||
|
||||
T.finish()
|
||||
|
||||
Reference in New Issue
Block a user