mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
fix: reject invalid checkpoint content
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user