maps v0.5.1 — opaque-flag + manifest fields + override format
Three small follow-ups bundled into a single version bump:
1. Real tile.opaque consumption (cell_is_opaque_on_layer)
v0.5.0e shipped a slot-13 heuristic for vertex-painted layers
because the atlas-baker wasn't computing real opacity. Atlas-baker
v0.2.0 now sets tile.opaque via alpha-analysis (all-alpha-255
detection). Both vertex-painted and tiles[] paths now resolve the
actual slot and read its tile.opaque from atlas metadata. Pre-
v0.2.0 atlases without the flag report not-opaque (safe-
conservative: extra draws, no missed cells).
2. Manifest-schema patch (reserved fields, accept-but-ignore)
Type-checked acceptance of forthcoming manifest fields the
validator will need before consumers can ship them in real
manifests without breaking changes:
map-level: z_level (int), z_below (string)
per-layer: base_color (string #RRGGBB), tint_override (table),
collision_policy (table), traversal_modes (table),
foundation_mode (array)
Future slices (lib-core.maps v0.6 multi-z-level, lib-core.actor
movement-modes, etc.) wire the fields into actual behaviour.
3. Override format extension {slot, rot, flip} (design paper §15.1)
Override entries now accept EITHER a bare integer slot_id (compact
canonical-orientation form, backwards-compat with 0.5.0d) OR an
object `{slot, rot?, flip?}` for explicit-transform placement
(unblocks map-editor v0.2's Direct-mode Transform controls).
New normalize_override_entry helper converts disk-form to runtime
{slot, rot, flip}. Validator (validate_map_table_v3) type-checks
per-entry. set_override accepts either form and auto-stores as
compact (bare-int) when rot+flip both 0, object form otherwise —
minimises disk diff for the common canonical case. get_override
returns normalized form regardless of how the entry is stored.
draw_layer + cell_is_opaque_on_layer updated to read the
normalized form.
Closure-gate: milestone-check.sh ctest + test-all-modules GREEN.
SPOREL_CI=1 vagrant-skeleton rc=0 with 60 render_frame_ok; no
magenta-placeholders, no render-hook errors. Existing override-cells
(vagrant's 3 wall pillars) still render correctly through the new
normalize path; opaque-ceiling cache now reads real atlas data.
Consumer dep-bumps for v0.5.1 land in their own commits per repo:
sporel-lib-core.render, sporel-module-vagrant-skeleton, sporel-
module-map-editor[-test].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
218
init.lua
218
init.lua
@@ -295,30 +295,61 @@ local function atlas_slot_index(atlas)
|
||||
return atlas._slot_index
|
||||
end
|
||||
|
||||
-- 0.5.0e: predicate "is cell (x, y) opaque on layer_name". Used by the
|
||||
-- opaque-ceiling cache. Heuristics for now:
|
||||
-- * Vertex-painted layer: opaque iff cell is material AND its bitmask
|
||||
-- resolves to slot 13 (full solid). An override that forces slot 13
|
||||
-- also counts. Other slots have transparent regions so they
|
||||
-- can't be a ceiling.
|
||||
-- * Legacy tiles[] path: opaque iff the resolved atlas tile carries
|
||||
-- an explicit `opaque == true` flag. Atlases don't ship this flag
|
||||
-- today (deferred to atlas-baker E2 alpha-analysis), so this path
|
||||
-- defaults to non-opaque. Safe-conservative: extra draws, never
|
||||
-- missing draws.
|
||||
-- v0.5.1 override-format helper: normalize a stored override entry to
|
||||
-- {slot, rot, flip}. Disk-compact form is a bare integer slot_id (back-
|
||||
-- wards-compat with 0.5.0d); object form `{slot, rot?, flip?}` ships
|
||||
-- rotation+flip explicitly for direct-mode terrain-override placement
|
||||
-- with non-canonical orientation. Returns nil for malformed input.
|
||||
local function normalize_override_entry(entry)
|
||||
if type(entry) == "number" then
|
||||
return { slot = entry, rot = 0, flip = 0 }
|
||||
end
|
||||
if type(entry) == "table" and type(entry.slot) == "number" then
|
||||
return {
|
||||
slot = entry.slot,
|
||||
rot = entry.rot or 0,
|
||||
flip = entry.flip or 0,
|
||||
}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Predicate "is cell (x, y) opaque on layer_name". Used by the
|
||||
-- opaque-ceiling cache. Reads the actual atlas tile.opaque flag set
|
||||
-- by atlas-baker v0.2.0+ alpha-analysis (was previously a slot-13
|
||||
-- heuristic in 0.5.0e — replaced now that the real data is available).
|
||||
--
|
||||
-- Pre-v0.2.0 atlases without the opaque flag set will report all-
|
||||
-- not-opaque, harmlessly forcing the renderer to draw layers it could
|
||||
-- have skipped. Safe-conservative.
|
||||
local function cell_is_opaque_on_layer(map, layer_name, x, y)
|
||||
local layer = map.layers[layer_name]
|
||||
if not layer then return false end
|
||||
if layer.vertices then
|
||||
local override_slot = layer.overrides and layer.overrides[x .. ":" .. y]
|
||||
if override_slot ~= nil then
|
||||
return override_slot == 13
|
||||
-- Resolve the slot that would render at this cell (override
|
||||
-- first, else bitmask), then look up the actual tile.opaque
|
||||
-- flag in the layer's material atlas.
|
||||
local atlas = layer.material and map.atlas_by_alias
|
||||
and map.atlas_by_alias[layer.material]
|
||||
if not atlas then return false end
|
||||
local override_entry = layer.overrides and layer.overrides[x .. ":" .. y]
|
||||
local slot
|
||||
if override_entry ~= nil then
|
||||
local norm = normalize_override_entry(override_entry)
|
||||
if not norm then return false end
|
||||
slot = norm.slot
|
||||
else
|
||||
if not cell_has_material(layer.vertices, map.size.w, map.size.h, x, y) then
|
||||
return false
|
||||
end
|
||||
local bitmask = compute_cell_bitmask_v3(layer.vertices, map.size.w, map.size.h, x, y)
|
||||
local rec = SLOT_LOOKUP[bitmask]
|
||||
if not rec then return false end
|
||||
slot = rec.slot
|
||||
end
|
||||
if not cell_has_material(layer.vertices, map.size.w, map.size.h, x, y) then
|
||||
return false
|
||||
end
|
||||
local bitmask = compute_cell_bitmask_v3(layer.vertices, map.size.w, map.size.h, x, y)
|
||||
return bitmask == 0xFF
|
||||
local tile_id = atlas_slot_index(atlas)[slot]
|
||||
local tile = tile_id and atlas.tiles[tile_id]
|
||||
return tile and tile.opaque == true or false
|
||||
end
|
||||
if layer.tiles then
|
||||
local gid = layer.tiles[y * map.size.w + x + 1] or 0
|
||||
@@ -476,12 +507,77 @@ local function validate_map_table_v3(t, source)
|
||||
if layer_data.vertices ~= nil and type(layer_data.vertices) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: vertices must be array", lsrc))
|
||||
end
|
||||
if layer_data.overrides ~= nil and type(layer_data.overrides) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: overrides must be table", lsrc))
|
||||
if layer_data.overrides ~= nil then
|
||||
if type(layer_data.overrides) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: overrides must be table", lsrc))
|
||||
end
|
||||
-- v0.5.1: each override entry is either a bare int (compact
|
||||
-- canonical-orientation form) or an object {slot, rot?, flip?}
|
||||
-- for explicit-transform placement. Validate per-entry.
|
||||
for k, v in pairs(layer_data.overrides) do
|
||||
if type(v) == "number" then
|
||||
if v ~= math.floor(v) or v < 0 or v > 13 then
|
||||
error(string.format(
|
||||
"maps.load: schema violation in %s.overrides[%s]: bare slot %s must be integer 0..13",
|
||||
lsrc, tostring(k), tostring(v)))
|
||||
end
|
||||
elseif type(v) == "table" then
|
||||
if type(v.slot) ~= "number" or v.slot ~= math.floor(v.slot)
|
||||
or v.slot < 0 or v.slot > 13 then
|
||||
error(string.format(
|
||||
"maps.load: schema violation in %s.overrides[%s]: object must have slot integer 0..13",
|
||||
lsrc, tostring(k)))
|
||||
end
|
||||
if v.rot ~= nil and (type(v.rot) ~= "number" or v.rot < 0 or v.rot > 3) then
|
||||
error(string.format(
|
||||
"maps.load: schema violation in %s.overrides[%s]: rot must be 0..3",
|
||||
lsrc, tostring(k)))
|
||||
end
|
||||
if v.flip ~= nil and v.flip ~= 0 and v.flip ~= 1 then
|
||||
error(string.format(
|
||||
"maps.load: schema violation in %s.overrides[%s]: flip must be 0 or 1",
|
||||
lsrc, tostring(k)))
|
||||
end
|
||||
else
|
||||
error(string.format(
|
||||
"maps.load: schema violation in %s.overrides[%s]: value must be integer slot or {slot, rot?, flip?} object",
|
||||
lsrc, tostring(k)))
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Reserved per-layer fields (design paper §8 + §14b + §15.1).
|
||||
-- Accept-but-ignore today; future slices wire them into the
|
||||
-- renderer / collision-resolver / movement-mode system.
|
||||
-- Type-checks here catch malformed values early instead of
|
||||
-- letting them survive as silent garbage.
|
||||
if layer_data.base_color ~= nil and type(layer_data.base_color) ~= "string" then
|
||||
error(string.format("maps.load: schema violation in %s: base_color must be string (#RRGGBB)", lsrc))
|
||||
end
|
||||
if layer_data.tint_override ~= nil and type(layer_data.tint_override) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: tint_override must be table", lsrc))
|
||||
end
|
||||
if layer_data.collision_policy ~= nil and type(layer_data.collision_policy) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: collision_policy must be table", lsrc))
|
||||
end
|
||||
if layer_data.traversal_modes ~= nil and type(layer_data.traversal_modes) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: traversal_modes must be table", lsrc))
|
||||
end
|
||||
if layer_data.foundation_mode ~= nil and type(layer_data.foundation_mode) ~= "table" then
|
||||
error(string.format("maps.load: schema violation in %s: foundation_mode must be array (per-cell 0/1)", lsrc))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Reserved map-level fields for §14c multi-z-level support.
|
||||
-- Accept-but-ignore today; future lib-core.maps v0.6 wires them
|
||||
-- into a recursive load+link + foundation-reveal-composite pipeline.
|
||||
if t.z_level ~= nil and type(t.z_level) ~= "number" then
|
||||
error(string.format("maps.load: schema violation in %s: z_level must be integer", source))
|
||||
end
|
||||
if t.z_below ~= nil and type(t.z_below) ~= "string" then
|
||||
error(string.format("maps.load: schema violation in %s: z_below must be string map-id reference", source))
|
||||
end
|
||||
|
||||
if t.roof ~= nil then
|
||||
if type(t.roof) ~= "table" then
|
||||
error(string.format("maps.load: %s: roof must be array", source))
|
||||
@@ -790,16 +886,21 @@ local function draw_layer(m, layer_name)
|
||||
if ceiling and layer_z and ceiling[idx] > layer_z then
|
||||
goto continue -- higher opaque layer covers this cell
|
||||
end
|
||||
local override_slot = overrides and overrides[x .. ":" .. y]
|
||||
local has_material = override_slot ~= nil
|
||||
local override_entry = overrides and overrides[x .. ":" .. y]
|
||||
local has_material = override_entry ~= nil
|
||||
or cell_has_material(layer.vertices, sz.w, sz.h, x, y)
|
||||
if not has_material then
|
||||
goto continue
|
||||
end
|
||||
local slot_index, rot, flip
|
||||
if override_slot ~= nil then
|
||||
-- Forced slot: render in canonical orientation, no transform.
|
||||
slot_index, rot, flip = override_slot, 0, 0
|
||||
if override_entry ~= nil then
|
||||
-- v0.5.1: override entry can be bare int (canonical
|
||||
-- orientation, backwards-compat) or {slot, rot, flip}
|
||||
-- object for explicit transform placement.
|
||||
local norm = normalize_override_entry(override_entry)
|
||||
if norm then
|
||||
slot_index, rot, flip = norm.slot, norm.rot, norm.flip
|
||||
end
|
||||
else
|
||||
local bitmask = compute_cell_bitmask_v3(layer.vertices, sz.w, sz.h, x, y)
|
||||
local rec = SLOT_LOOKUP[bitmask]
|
||||
@@ -1192,10 +1293,14 @@ function M.set_cell_gid(layer_name, x, y, gid, map_id)
|
||||
map._opaque_ceiling = nil
|
||||
end
|
||||
|
||||
-- v3 vertex-painted-layer override APIs (0.5.0d). Override forces a
|
||||
-- specific canonical slot (0..13) at a cell, bypassing the bitmask
|
||||
-- autotile result. Override implies cell-has-material regardless of
|
||||
-- the vertex-grid state. Stored as sparse map keyed "x:y" -> slot_id.
|
||||
-- v3 vertex-painted-layer override APIs.
|
||||
-- 0.5.0d: override forces a specific canonical slot (0..13) at a cell,
|
||||
-- bypassing the bitmask autotile result. Override implies
|
||||
-- cell-has-material regardless of the vertex-grid state.
|
||||
-- 0.5.1: override entry can be bare integer slot_id (canonical orientation,
|
||||
-- compact-form, backwards-compat) OR object {slot, rot?, flip?}
|
||||
-- for explicit-transform direct-mode placement.
|
||||
-- Stored as sparse map keyed "x:y" -> entry.
|
||||
|
||||
local function require_v3_layer(map, layer_name, fn_name)
|
||||
if map.schema_version < 3 then
|
||||
@@ -1221,17 +1326,50 @@ local function check_coords(map, x, y, fn_name)
|
||||
end
|
||||
end
|
||||
|
||||
function M.set_override(layer_name, x, y, slot_id, map_id)
|
||||
-- 0.5.1: accepts either bare slot_id (compact form) or table
|
||||
-- {slot, rot?, flip?} (explicit transform). Auto-stores as compact form
|
||||
-- when rot+flip are both 0, else stores as object — minimises disk diff
|
||||
-- for the common canonical-orientation case.
|
||||
function M.set_override(layer_name, x, y, slot_or_entry, map_id)
|
||||
local map = require_map(map_id)
|
||||
check_coords(map, x, y, "maps.set_override")
|
||||
local layer = require_v3_layer(map, layer_name, "maps.set_override")
|
||||
if type(slot_id) ~= "number" or slot_id < 0 or slot_id > 13
|
||||
or slot_id ~= math.floor(slot_id) then
|
||||
error(string.format("maps.set_override: slot_id %s must be integer 0..13",
|
||||
tostring(slot_id)))
|
||||
local stored
|
||||
if type(slot_or_entry) == "number" then
|
||||
if slot_or_entry < 0 or slot_or_entry > 13
|
||||
or slot_or_entry ~= math.floor(slot_or_entry) then
|
||||
error(string.format("maps.set_override: slot %s must be integer 0..13",
|
||||
tostring(slot_or_entry)))
|
||||
end
|
||||
stored = slot_or_entry
|
||||
elseif type(slot_or_entry) == "table" then
|
||||
local slot = slot_or_entry.slot
|
||||
local rot = slot_or_entry.rot or 0
|
||||
local flip = slot_or_entry.flip or 0
|
||||
if type(slot) ~= "number" or slot < 0 or slot > 13
|
||||
or slot ~= math.floor(slot) then
|
||||
error(string.format("maps.set_override: object.slot %s must be integer 0..13",
|
||||
tostring(slot)))
|
||||
end
|
||||
if type(rot) ~= "number" or rot < 0 or rot > 3 or rot ~= math.floor(rot) then
|
||||
error(string.format("maps.set_override: object.rot %s must be integer 0..3",
|
||||
tostring(rot)))
|
||||
end
|
||||
if flip ~= 0 and flip ~= 1 then
|
||||
error(string.format("maps.set_override: object.flip %s must be 0 or 1",
|
||||
tostring(flip)))
|
||||
end
|
||||
if rot == 0 and flip == 0 then
|
||||
stored = slot -- canonical orientation -> compact form
|
||||
else
|
||||
stored = { slot = slot, rot = rot, flip = flip }
|
||||
end
|
||||
else
|
||||
error(string.format("maps.set_override: must be integer slot or {slot, rot?, flip?} object, got %s",
|
||||
type(slot_or_entry)))
|
||||
end
|
||||
if layer.overrides == nil then layer.overrides = {} end
|
||||
layer.overrides[x .. ":" .. y] = slot_id
|
||||
layer.overrides[x .. ":" .. y] = stored
|
||||
map._dirty = true
|
||||
if map._layer_has_content then
|
||||
map._layer_has_content[layer_name] = true
|
||||
@@ -1250,12 +1388,18 @@ function M.clear_override(layer_name, x, y, map_id)
|
||||
end
|
||||
end
|
||||
|
||||
-- 0.5.1: returns normalized form {slot, rot, flip} regardless of how
|
||||
-- the entry is stored on disk (bare-int compact OR object form). Returns
|
||||
-- nil when no override is set. Callers that need the on-disk shape
|
||||
-- should consult layer.overrides directly.
|
||||
function M.get_override(layer_name, x, y, map_id)
|
||||
local map = require_map(map_id)
|
||||
check_coords(map, x, y, "maps.get_override")
|
||||
local layer = require_v3_layer(map, layer_name, "maps.get_override")
|
||||
if layer.overrides == nil then return nil end
|
||||
return layer.overrides[x .. ":" .. y]
|
||||
local entry = layer.overrides[x .. ":" .. y]
|
||||
if entry == nil then return nil end
|
||||
return normalize_override_entry(entry)
|
||||
end
|
||||
|
||||
function M.set_roof(x, y, value, map_id)
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"id":"lib-core.maps","version":"0.5.0","api_min":"0.1"}
|
||||
{"id":"lib-core.maps","version":"0.5.1","api_min":"0.1"}
|
||||
|
||||
Reference in New Issue
Block a user