feat: add SaveGame.fnv1a32, migrateForward and Scavenging.herbFor

This commit is contained in:
dualfroz committed 2026-09-07 21:47:00 +02:00
1 parent 35b97b6c04
commit f920e8d57f
2 files changed
+176

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
-- A reusable versioned, checksummed save container laid on top of
-- serialize.lua's plain Lua-expression encoding. serialize.lua only knows how
-- to turn a value into text and back; it has no notion of "this text might be
-- truncated, hand-edited, or written by a codebase three versions ago". This
-- module adds that layer: an envelope carrying a schema version and an
-- FNV-1a32 checksum of its payload, plus a migration chain that walks old
-- data forward one version at a time.
--
-- A file written before this module existed - a bare Serialize.saveFile dump
-- with no envelope at all - is treated as implicit version 1, so an existing
-- save on disk keeps loading instead of being rejected outright.
--
-- SaveGame.new(migrations) builds one codec for one call site. `migrations`
-- is a 1-indexed array where migrations[v] transforms data from version v to
-- v+1; the codec's CURRENT_VERSION is #migrations + 1. Two call sites with
-- differently shaped data (a run's game state, a player's meta progression)
-- each get their own codec with their own migration chain - this module
-- knows nothing about either shape.
local Serialize = require("serialize")
local SaveGame = {}
local MAGIC = "WARRENSAVE"
local MASK32 = 0xFFFFFFFF
-- FNV-1a, 32-bit. Verified below against the algorithm's published test
-- vectors (fnv1a32("") and fnv1a32("a")), not just self-consistency.
function SaveGame.fnv1a32(str)
local hash = 2166136261
for i = 1, #str do
hash = (hash ~ str:byte(i)) & MASK32
hash = (hash * 16777619) & MASK32
end
return hash
end
local function migrateForward(data, fromVersion, migrations, currentVersion)
for v = fromVersion, currentVersion - 1 do
data = migrations[v](data)
end
return data
end
-- Builds a codec bound to one migration chain. `migrations` may be empty
-- (CURRENT_VERSION == 1); a version-1-only codec still gets checksumming and
-- corruption detection for free.
function SaveGame.new(migrations)
migrations = migrations or {}
local currentVersion = #migrations + 1
local codec = { CURRENT_VERSION = currentVersion }
function codec.encode(data)
local payload = Serialize.encode(data)
local envelope = {
magic = MAGIC,
version = currentVersion,
checksum = SaveGame.fnv1a32(payload),
payload = payload,
}
return Serialize.encode(envelope)
end
-- Returns (data) on success, or (nil, reason) on any failure. `reason` is
-- always one of: "missing" (only from .load, never from .decode), a string
-- starting with "corrupt:", or a string starting with "future version:".
function codec.decode(text)
local ok, value = pcall(Serialize.decode, text)
if not ok or type(value) ~= "table" then
return nil, "corrupt: cannot parse save data"
end
if value.magic ~= MAGIC then
-- No envelope at all: a save from before this codec existed. The
-- decoded value IS the raw data, at implicit version 1.
return migrateForward(value, 1, migrations, currentVersion)
end
if type(value.version) ~= "number" or type(value.payload) ~= "string"
or type(value.checksum) ~= "number" then
return nil, "corrupt: malformed save envelope"
end
if value.version > currentVersion then
return nil, "future version: save is v" .. value.version
.. ", this build reads up to v" .. currentVersion
end
if SaveGame.fnv1a32(value.payload) ~= value.checksum then
return nil, "corrupt: checksum mismatch"
end
local ok2, data = pcall(Serialize.decode, value.payload)
if not ok2 or type(data) ~= "table" then
return nil, "corrupt: cannot parse save payload"
end
return migrateForward(data, value.version, migrations, currentVersion)
end
function codec.save(path, data)
local file = assert(io.open(path, "w"))
file:write(codec.encode(data))
file:close()
end
function codec.load(path)
local file = io.open(path, "r")
if not file then return nil, "missing" end
local text = file:read("a")
file:close()
return codec.decode(text)
end
return codec
end
return SaveGame