106 lines
4.0 KiB
Lua
Executable File
106 lines
4.0 KiB
Lua
Executable File
-- Pure save validation: no engine, JSON, filesystem, or global game state.
|
|
local S = {}
|
|
local needs = {food = 85, water = 85, warmth = 80, wet = 0, stamina = 100}
|
|
local kinds = {needs = true, claims = true, homes = true, access = true, location = true, spawn = true}
|
|
|
|
local function invalid()
|
|
error("Last Hearth: invalid save schema or data", 0)
|
|
end
|
|
|
|
local function record(value, fields)
|
|
if type(value) ~= "table" or getmetatable(value) ~= nil then invalid() end
|
|
for key in pairs(value) do
|
|
if not fields[key] then invalid() end
|
|
end
|
|
end
|
|
|
|
local function number(value, low, high, integer)
|
|
if type(value) ~= "number" or value ~= value or value < low or value > high
|
|
or (integer and value % 1 ~= 0) then invalid() end
|
|
return value
|
|
end
|
|
|
|
local function position(value, integer)
|
|
record(value, {x = true, y = true, z = true})
|
|
return {x = number(value.x, -31000, 31000, integer),
|
|
y = number(value.y, -31000, 31000, integer),
|
|
z = number(value.z, -31000, 31000, integer)}
|
|
end
|
|
|
|
local function player_name(value)
|
|
return type(value) == "string" and #value > 0 and #value <= 32
|
|
and value:match("^[%w_%-]+$") ~= nil
|
|
end
|
|
|
|
local function normalize(kind, value)
|
|
if kind == "needs" then
|
|
record(value, needs)
|
|
local result = {}
|
|
for key in pairs(needs) do result[key] = number(value[key], 0, 100) end
|
|
return result
|
|
elseif kind == "location" then
|
|
record(value, {latitude = true, longitude = true})
|
|
return {latitude = number(value.latitude, -90, 90),
|
|
longitude = number(value.longitude, -180, 180)}
|
|
elseif kind == "spawn" then
|
|
return position(value, false)
|
|
elseif kind == "access" then
|
|
if type(value) ~= "table" or getmetatable(value) ~= nil then invalid() end
|
|
local result = {}
|
|
for owner, members in pairs(value) do
|
|
if not player_name(owner) or type(members) ~= "table" or getmetatable(members) ~= nil then invalid() end
|
|
local normalized, count = {}, 0
|
|
for member, role in pairs(members) do
|
|
if not player_name(member) or member == owner
|
|
or (role ~= "resident" and role ~= "storekeeper") then invalid() end
|
|
count = count + 1
|
|
if count > 16 then invalid() end
|
|
normalized[member] = role
|
|
end
|
|
result[owner] = normalized
|
|
end
|
|
return result
|
|
end
|
|
-- JSON objects and empty arrays can both become {} in the engine. Nonempty
|
|
-- arrays are rejected by the string-key rule; an empty table is valid maps.
|
|
if type(value) ~= "table" or getmetatable(value) ~= nil then invalid() end
|
|
local result = {}
|
|
for owner, pos in pairs(value) do
|
|
if type(owner) ~= "string" or owner == "" then invalid() end
|
|
result[owner] = position(pos, true)
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- value is already parsed JSON, NOT JSON text. Missing storage is the adapter's
|
|
-- responsibility. Legacy records must also be complete and valid: never clamp,
|
|
-- coerce strings, silently fill fields, or downgrade an unrecognized envelope.
|
|
function S.decode(kind, value)
|
|
if type(kind) ~= "string" or not kinds[kind] or type(value) ~= "table"
|
|
or getmetatable(value) ~= nil then invalid() end
|
|
local migrated = true
|
|
local envelope = value.schema ~= nil or value.kind ~= nil or value.data ~= nil
|
|
if kind == "claims" or kind == "homes" or kind == "access" then
|
|
-- Players may legitimately be named "schema", "kind", or "data".
|
|
-- Their legacy claim values are positions, never envelope scalars.
|
|
envelope = (value.schema ~= nil and type(value.schema) ~= "table")
|
|
or (value.kind ~= nil and type(value.kind) ~= "table")
|
|
or (value.data ~= nil and type(value.data) ~= "table")
|
|
end
|
|
if envelope then
|
|
record(value, {schema = true, kind = true, data = true})
|
|
if type(value.schema) ~= "number" or value.schema ~= 1 or value.kind ~= kind then invalid() end
|
|
value, migrated = value.data, false
|
|
end
|
|
return {schema = 1, kind = kind, data = normalize(kind, value)}, migrated
|
|
end
|
|
|
|
-- Each call owns its copy; callers cannot mutate future defaults.
|
|
function S.defaults()
|
|
local result = {}
|
|
for key, value in pairs(needs) do result[key] = value end
|
|
return result
|
|
end
|
|
|
|
return S
|