diff --git a/src/core/Checkpoint.lua b/src/core/Checkpoint.lua index bded2027..3f530f53 100644 --- a/src/core/Checkpoint.lua +++ b/src/core/Checkpoint.lua @@ -313,6 +313,16 @@ local function equalData(a, b) return okA and okB and encodedA == encodedB end +local function normalizeVerificationMetadata(actual, expected) + if type(actual) == "table" and type(actual.identity) == "table" + and type(expected) == "table" and type(expected.identity) == "table" then + -- RFC 0004 treats engineVersion as compatibility metadata, not runtime + -- state. A fresh recapture stamps the running engine, so normalize only + -- that metadata before differential verification. + actual.identity.engineVersion = expected.identity.engineVersion + end +end + local function firstDifference(a, b, path) path = path or "$" if type(a) ~= type(b) then return path .. " (type)" end @@ -394,6 +404,7 @@ function Checkpoint.restore(game, checkpoint) if ok then local restored, verifyCode = Checkpoint.capture(game) if restored and validated.rng == nil then restored.rng = nil end + normalizeVerificationMetadata(restored, validated) if restored and equalData(restored, validated) then emitRestored(game, validated) return true @@ -474,6 +485,7 @@ function Checkpoint.resume(game, checkpoint) if ok then local restored, verifyCode = Checkpoint.capture(game) if restored and validated.rng == nil then restored.rng = nil end + normalizeVerificationMetadata(restored, validated) if restored and equalData(restored, validated) then emitRestored(game, validated) return true diff --git a/tests/modkit/cases/title_playthrough_context.lua b/tests/modkit/cases/title_playthrough_context.lua index ab40f26d..54857a53 100644 --- a/tests/modkit/cases/title_playthrough_context.lua +++ b/tests/modkit/cases/title_playthrough_context.lua @@ -207,6 +207,11 @@ if type(storage) == "table" then T.check(type(normalBytes) == "string" and normalBytes ~= "", "first checkpoint anchor is durably represented before restart") local anchoredAt = SaveSerializer.decode(normalBytes).meta.savedAt + + -- Model a durable checkpoint captured by the previous shipped engine. + -- RFC 0004 treats engineVersion as compatibility metadata, not runtime state. + checkpoint.identity.engineVersion = "0.1.79" + SaveData.resetSlotState() local titleRuntime = makeRuntime(SaveData.newGame({ version = "red" }), true) titleRuntime.save.options = { volume = 9, bindings = {} } @@ -224,7 +229,14 @@ if type(storage) == "table" then version = "red", meta = { playthroughId = originalId }, }, fs).savedAt, anchoredAt, "title bootstrap never rewrites the first normal save") - T.same(checkpoints:capture(titleRuntime), checkpoint, + local recaptured = checkpoints:capture(titleRuntime) + T.eq(recaptured and recaptured.identity + and recaptured.identity.engineVersion, Version.engine, + "cross-version resume recaptures the running engine version") + if recaptured and recaptured.identity then + recaptured.identity.engineVersion = checkpoint.identity.engineVersion + end + T.same(recaptured, checkpoint, "bootstrapped overworld differentially recaptures the selected checkpoint") T.eq(_G.MOD_TITLE_RESTORE_COUNT, 1, "a successfully verified title resume emits checkpoint.restored exactly once")