fix: reject invalid checkpoint content

This commit is contained in:
MaxTomahawk
2026-08-07 15:09:16 +02:00
parent bbca4e8e39
commit 9d6ea845d7
3 changed files with 41 additions and 7 deletions
+5 -1
View File
@@ -79,10 +79,14 @@ format, kind, internal identity consistency, current game/playthrough identity,
map availability, integral in-bounds tile, facing, surfing, and synchronized save
position.
Validation codes are `invalid_checkpoint`, `unsupported_format`,
Validation codes are `invalid_checkpoint`, `invalid_content`, `unsupported_format`,
`unsupported_runtime_kind`, `wrong_game`, `wrong_playthrough`, `invalid_map`, and
`invalid_position`, in addition to the capability refusal reasons.
The canonical save validator runs against the detached record. Unlike ordinary
CONTINUE, a checkpoint never accepts a quarantine, remap, reclaim, clamp, or
repair: any such content change returns `invalid_content` before live mutation.
The engine captures an in-memory rollback checkpoint, preserves current global
options, then reconstructs semantic overworld state through
`Game:restoreCheckpointSave`. Checkpoint entry suppresses normal map exit/entry
+13
View File
@@ -195,6 +195,19 @@ local function validate(game, checkpoint)
or runtime.x >= width * 2 or runtime.y >= height * 2 then
return nil, "invalid_position", "Checkpoint position is outside the map."
end
-- A checkpoint is a strict restoration record, not an ordinary CONTINUE
-- migration. Reuse the canonical save validator on the detached copy, but
-- reject any quarantine, remap, reclaim, clamp, or content repair it would
-- perform instead of silently changing the state the caller selected.
local beforeContent = SaveSerializer.encode(copy.save)
local validOk, report = pcall(SaveData.validate, copy.save, game.data)
local afterOk, afterContent = pcall(SaveSerializer.encode, copy.save)
if not validOk or not afterOk or not SaveData.emptyReport(report)
or afterContent ~= beforeContent then
return nil, "invalid_content",
"Checkpoint references unavailable or invalid game content."
end
return copy
end
+23 -6
View File
@@ -56,7 +56,8 @@ local function baseSave()
name = "RED", rival = "BLUE", id = 7,
},
money = 3000,
party = { { species = "BULBASAUR", hp = 19, moves = { "TACKLE" } } },
party = { { species = "BULBASAUR", level = 5, hp = 19,
moves = { "TACKLE" } } },
flags = { GOT_STARTER = true },
inventory = { POTION = 1 },
pcItems = {}, box = {}, boxes = {}, defeatedTrainers = {},
@@ -99,11 +100,18 @@ local function makeGame()
end
game = setmetatable({
save = baseSave(), stack = stack, overworld = ow,
data = { maps = {
PALLET_TOWN = { id = "PALLET_TOWN", width = 10, height = 9 },
ROUTE_1 = { id = "ROUTE_1", width = 10, height = 18 },
BROKEN = { id = "BROKEN", width = 10, height = 9 },
} },
data = {
pokemon = { BULBASAUR = { dex = 1 } },
moves = { TACKLE = { pp = 35 } },
items = { POTION = {} },
constants = { fallbackMove = "TACKLE" },
field = { boot = { startMap = "PALLET_TOWN", startX = 5, startY = 6 } },
maps = {
PALLET_TOWN = { id = "PALLET_TOWN", width = 10, height = 9 },
ROUTE_1 = { id = "ROUTE_1", width = 10, height = 18 },
BROKEN = { id = "BROKEN", width = 10, height = 9 },
},
},
}, { __index = GameMethods })
stack.states[1] = ow
return game, ow
@@ -261,6 +269,15 @@ T.check(not restored and restoreCode == "invalid_map",
T.same(checkpoints:capture(game), beforeRejected,
"validation failures leave the live state unchanged")
local invalidGame = makeGame()
local badSpecies = checkpoints:capture(invalidGame)
badSpecies.save.party[1].species = "MISSING_SPECIES"
restored, restoreCode = checkpoints:restore(invalidGame, badSpecies)
T.check(not restored and restoreCode == "invalid_content",
"unknown Pokemon content is rejected before reconstruction")
T.eq(invalidGame.save.party[1].species, "BULBASAUR",
"invalid Pokemon content leaves the live party unchanged")
-- A reconstruction exception rolls back to the exact pre-operation state.
local target = checkpoints:capture(game)
target.runtime.overworld.map = "BROKEN"