feat: validate schema-v2 map tables with layer-name whitelist

Adds validate_map_table_v2 — enforces schema_version=2, required
fields (id, size, atlases), non-empty atlases[], per-layer tile-count
check against w*h, and a VALID_LAYER_NAMES whitelist that logs a
warning for unknown layers instead of rejecting them.

Moves require_field before the v2 validator so it is in scope at
call time. engine.warn replaced with engine.print (no warn binding).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Axel Meyer
2026-05-21 15:26:48 +02:00
parent 0985141fb9
commit 1562ee0068

View File

@@ -77,6 +77,83 @@ local function upgrade_v1_to_v2(v1)
} }
end end
-- =====================================================================
-- Schema-validation helper (used by both v1 and v2 validators)
-- =====================================================================
-- Schema-validation helper: reads required field with type-check.
local function require_field(t, key, expected_type, source)
local v = t[key]
if v == nil then
error(string.format("maps.load: schema violation in %s: missing required field '%s'",
source, key))
end
if type(v) ~= expected_type then
error(string.format("maps.load: schema violation in %s: field '%s' must be %s, got %s",
source, key, expected_type, type(v)))
end
return v
end
-- =====================================================================
-- Schema-v2 Layer-Whitelist + Validation
-- =====================================================================
local VALID_LAYER_NAMES = {
foundation = true,
subsurface = true,
surface = true,
topsurface = true,
lower_wall = true,
wall = true,
upper_wall = true,
canopy = true,
}
local function validate_map_table_v2(t, source)
require_field(t, "schema_version", "number", source)
if t.schema_version ~= 2 then
error(string.format("maps.load: schema_version %d not supported (expected 2)",
t.schema_version))
end
require_field(t, "id", "string", source)
local size = require_field(t, "size", "table", source)
require_field(size, "w", "number", source .. ".size")
require_field(size, "h", "number", source .. ".size")
local expected = size.w * size.h
local atlases = require_field(t, "atlases", "table", source)
if #atlases == 0 then
error(string.format("maps.load: schema violation in %s: atlases[] must be non-empty", source))
end
if #atlases > 256 then
error(string.format("maps.load: schema violation in %s: atlases[] has %d entries, max 256", source, #atlases))
end
local layers = t.layers or {}
for layer_name, layer_data in pairs(layers) do
if not VALID_LAYER_NAMES[layer_name] then
engine.print(string.format("maps.load: %s: ignoring unknown layer '%s'", source, layer_name))
else
local tiles = require_field(layer_data, "tiles", "table",
source .. ".layers." .. layer_name)
if #tiles ~= expected then
error(string.format("maps.load: schema violation in %s.layers.%s: tiles length %d != %d",
source, layer_name, #tiles, expected))
end
end
end
if t.roof ~= nil then
if type(t.roof) ~= "table" then
error(string.format("maps.load: %s: roof must be array", source))
end
if #t.roof ~= expected then
error(string.format("maps.load: %s: roof length %d != %d", source, #t.roof, expected))
end
end
end
-- ===================================================================== -- =====================================================================
-- Internal helpers -- Internal helpers
-- ===================================================================== -- =====================================================================
@@ -103,20 +180,6 @@ local function resolve_tilemap_id(ref)
return engine.module.id() .. "." .. ref return engine.module.id() .. "." .. ref
end end
-- Schema-validation helper: reads required field with type-check.
local function require_field(t, key, expected_type, source)
local v = t[key]
if v == nil then
error(string.format("maps.load: schema violation in %s: missing required field '%s'",
source, key))
end
if type(v) ~= expected_type then
error(string.format("maps.load: schema violation in %s: field '%s' must be %s, got %s",
source, key, expected_type, type(v)))
end
return v
end
local function validate_map_table(t, source) local function validate_map_table(t, source)
require_field(t, "id", "string", source) require_field(t, "id", "string", source)
require_field(t, "tilemap", "string", source) require_field(t, "tilemap", "string", source)
@@ -389,5 +452,6 @@ end
M.encode_gid = encode_gid M.encode_gid = encode_gid
M.decode_gid = decode_gid M.decode_gid = decode_gid
M.upgrade_v1_to_v2 = upgrade_v1_to_v2 M.upgrade_v1_to_v2 = upgrade_v1_to_v2
M.validate_map_table_v2 = validate_map_table_v2
return M return M