Compare commits
4 Commits
b8705c0ec7
...
5cf07c8549
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cf07c8549 | ||
|
|
d80c0ecb5f | ||
|
|
84d1980c18 | ||
|
|
45b1f93363 |
14
README.md
14
README.md
@@ -286,6 +286,20 @@ maps.draw_map_post_entities()
|
|||||||
|
|
||||||
**Description:** Draws all post-entity layers of the current map (`lower_wall` through `canopy`). Must be called after entity rendering when using the split draw model. No-op if no current map.
|
**Description:** Draws all post-entity layers of the current map (`lower_wall` through `canopy`). Must be called after entity rendering when using the split draw model. No-op if no current map.
|
||||||
|
|
||||||
|
### Write APIs (v0.4.0+)
|
||||||
|
|
||||||
|
#### `maps.set_cell_gid(layer_name, x, y, gid, map_id?)`
|
||||||
|
|
||||||
|
Writes a single cell into the named layer of the current (or named) map. The layer must be one of `VALID_LAYER_NAMES`; bounds are checked against `map.size`. If the layer does not yet exist on the map it is allocated and initialised to all-zero before the write. Sets an internal `_dirty` flag so callers (e.g. the map-editor) can track unsaved changes.
|
||||||
|
|
||||||
|
#### `maps.set_roof(x, y, value, map_id?)`
|
||||||
|
|
||||||
|
Writes a single roof flag (`0` or `1`) at the named cell. Allocates the roof array on demand if the map did not previously have one. Same bounds-check as `set_cell_gid`. Throws on values other than 0 or 1.
|
||||||
|
|
||||||
|
#### `maps.save_to_disk(map_id, path)`
|
||||||
|
|
||||||
|
Serialises the in-memory map to v2 JSON and writes it to `path`. Output is pretty-printed with 2-space indent and is byte-deterministic for the same map state (sorted object keys, fixed array order). Reverses the internal atlas-resolution back to atlas-ID strings on disk. Resets the `_dirty` flag on success.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Pixel-coords + tile-coords kept distinct: `size`/`tiles` index in tile-units; `tile_size` is the conversion to pixels.
|
- Pixel-coords + tile-coords kept distinct: `size`/`tiles` index in tile-units; `tile_size` is the conversion to pixels.
|
||||||
|
|||||||
198
init.lua
198
init.lua
@@ -12,6 +12,15 @@ local map_registry = {} -- map_id -> Map
|
|||||||
local tilemap_registry = {} -- full_tilemap_id -> Tilemap
|
local tilemap_registry = {} -- full_tilemap_id -> Tilemap
|
||||||
local current_map_id = nil
|
local current_map_id = nil
|
||||||
|
|
||||||
|
-- Internal: resolve and return map by id, or raise.
|
||||||
|
local function require_map(map_id)
|
||||||
|
local id = map_id or current_map_id
|
||||||
|
if not id then error("maps: no current map set") end
|
||||||
|
local m = map_registry[id]
|
||||||
|
if not m then error(string.format("maps: no map registered with id '%s'", id)) end
|
||||||
|
return m
|
||||||
|
end
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- Packed-u32 GID encoding (Schema-v2)
|
-- Packed-u32 GID encoding (Schema-v2)
|
||||||
-- Bit-Layout:
|
-- Bit-Layout:
|
||||||
@@ -628,6 +637,195 @@ function M.cell_gid(layer_name, x, y, map_id)
|
|||||||
return m.layers[layer_name].tiles[y * m.size.w + x + 1] or 0
|
return m.layers[layer_name].tiles[y * m.size.w + x + 1] or 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- Write APIs (v0.4.0)
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- Helper: gather valid layer names for error messages.
|
||||||
|
local function get_valid_layer_names_list()
|
||||||
|
local names = {}
|
||||||
|
for name, _ in pairs(VALID_LAYER_NAMES) do
|
||||||
|
table.insert(names, name)
|
||||||
|
end
|
||||||
|
table.sort(names)
|
||||||
|
return names
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Internal: get-or-allocate a layer table for in-place writes.
|
||||||
|
local function get_or_alloc_layer(map, layer_name)
|
||||||
|
if not VALID_LAYER_NAMES[layer_name] then
|
||||||
|
error(string.format("maps: unknown layer '%s' (valid: %s)",
|
||||||
|
layer_name, table.concat(get_valid_layer_names_list(), ", ")))
|
||||||
|
end
|
||||||
|
local layer = map.layers[layer_name]
|
||||||
|
if not layer then
|
||||||
|
local cell_count = map.size.w * map.size.h
|
||||||
|
local tiles = {}
|
||||||
|
for i = 1, cell_count do tiles[i] = 0 end
|
||||||
|
layer = { tiles = tiles }
|
||||||
|
map.layers[layer_name] = layer
|
||||||
|
end
|
||||||
|
return layer
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.set_cell_gid(layer_name, x, y, gid, map_id)
|
||||||
|
local map = require_map(map_id)
|
||||||
|
if x < 0 or y < 0 or x >= map.size.w or y >= map.size.h then
|
||||||
|
error(string.format("maps.set_cell_gid: cell (%d,%d) out of bounds for size %dx%d",
|
||||||
|
x, y, map.size.w, map.size.h))
|
||||||
|
end
|
||||||
|
local layer = get_or_alloc_layer(map, layer_name)
|
||||||
|
local idx = y * map.size.w + x + 1
|
||||||
|
layer.tiles[idx] = gid
|
||||||
|
map._dirty = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.set_roof(x, y, value, map_id)
|
||||||
|
local map = require_map(map_id)
|
||||||
|
if x < 0 or y < 0 or x >= map.size.w or y >= map.size.h then
|
||||||
|
error(string.format("maps.set_roof: cell (%d,%d) out of bounds for size %dx%d",
|
||||||
|
x, y, map.size.w, map.size.h))
|
||||||
|
end
|
||||||
|
if value ~= 0 and value ~= 1 then
|
||||||
|
error(string.format("maps.set_roof: value %s must be 0 or 1", tostring(value)))
|
||||||
|
end
|
||||||
|
if not map.roof then
|
||||||
|
local cell_count = map.size.w * map.size.h
|
||||||
|
map.roof = {}
|
||||||
|
for i = 1, cell_count do map.roof[i] = 0 end
|
||||||
|
end
|
||||||
|
local idx = y * map.size.w + x + 1
|
||||||
|
map.roof[idx] = value
|
||||||
|
map._dirty = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- Internal JSON pretty-printer (hand-rolled, 2-space indent).
|
||||||
|
-- Supports: nil/null, bool, number (int + float), string, array, object.
|
||||||
|
-- Deterministic key ordering (sorted) so saves are byte-stable.
|
||||||
|
-- =====================================================================
|
||||||
|
local json_value -- forward declaration
|
||||||
|
local json_table -- forward declaration
|
||||||
|
|
||||||
|
local function json_escape_string(s)
|
||||||
|
local replacements = {
|
||||||
|
['"'] = '\\"',
|
||||||
|
['\\'] = '\\\\',
|
||||||
|
['\n'] = '\\n',
|
||||||
|
['\r'] = '\\r',
|
||||||
|
['\t'] = '\\t',
|
||||||
|
['\b'] = '\\b',
|
||||||
|
['\f'] = '\\f',
|
||||||
|
}
|
||||||
|
return '"' .. s:gsub('[%z\1-\31"\\]', function(c)
|
||||||
|
return replacements[c] or string.format('\\u%04x', string.byte(c))
|
||||||
|
end) .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
json_value = function(v, indent)
|
||||||
|
if v == nil then return "null" end
|
||||||
|
local t = type(v)
|
||||||
|
if t == "boolean" then return v and "true" or "false" end
|
||||||
|
if t == "number" then
|
||||||
|
if v ~= v then return "null" end -- NaN
|
||||||
|
if v == math.huge or v == -math.huge then return "null" end
|
||||||
|
if math.type(v) == "integer" or v == math.floor(v) then
|
||||||
|
return string.format("%d", v)
|
||||||
|
end
|
||||||
|
return string.format("%.17g", v)
|
||||||
|
end
|
||||||
|
if t == "string" then return json_escape_string(v) end
|
||||||
|
if t == "table" then
|
||||||
|
return json_table(v, indent)
|
||||||
|
end
|
||||||
|
error("json: cannot serialize value of type " .. t)
|
||||||
|
end
|
||||||
|
|
||||||
|
json_table = function(t, indent)
|
||||||
|
-- Detect array vs object by checking integer keys 1..n
|
||||||
|
local n = #t
|
||||||
|
local is_array = n > 0
|
||||||
|
if is_array then
|
||||||
|
for k, _ in pairs(t) do
|
||||||
|
if type(k) ~= "number" then is_array = false; break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local indent_next = indent .. " "
|
||||||
|
|
||||||
|
if is_array then
|
||||||
|
if n == 0 then return "[]" end
|
||||||
|
local parts = {}
|
||||||
|
for i = 1, n do
|
||||||
|
parts[i] = indent_next .. json_value(t[i], indent_next)
|
||||||
|
end
|
||||||
|
return "[\n" .. table.concat(parts, ",\n") .. "\n" .. indent .. "]"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Object: collect & sort keys for determinism
|
||||||
|
local keys = {}
|
||||||
|
for k, _ in pairs(t) do
|
||||||
|
table.insert(keys, tostring(k))
|
||||||
|
end
|
||||||
|
table.sort(keys)
|
||||||
|
if #keys == 0 then return "{}" end
|
||||||
|
local parts = {}
|
||||||
|
for _, k in ipairs(keys) do
|
||||||
|
parts[#parts + 1] = indent_next .. json_escape_string(k) ..
|
||||||
|
": " .. json_value(t[k], indent_next)
|
||||||
|
end
|
||||||
|
return "{\n" .. table.concat(parts, ",\n") .. "\n" .. indent .. "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- v2-Map serialization: reverse the internal resolved shape back to the
|
||||||
|
-- on-disk JSON schema (atlases as ID strings, layers in canonical order).
|
||||||
|
-- =====================================================================
|
||||||
|
local LAYER_DISK_ORDER = {
|
||||||
|
"foundation", "subsurface", "surface", "topsurface",
|
||||||
|
"lower_wall", "wall", "upper_wall", "canopy",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function serialize_map_v2(map)
|
||||||
|
local out = {
|
||||||
|
schema_version = 2,
|
||||||
|
id = map.id,
|
||||||
|
size = { w = map.size.w, h = map.size.h },
|
||||||
|
atlases = {},
|
||||||
|
layers = {},
|
||||||
|
}
|
||||||
|
-- Use atlas_aliases (raw string IDs from disk) for round-trip fidelity.
|
||||||
|
for i, alias in ipairs(map.atlas_aliases) do
|
||||||
|
out.atlases[i] = alias
|
||||||
|
end
|
||||||
|
for _, layer_name in ipairs(LAYER_DISK_ORDER) do
|
||||||
|
local layer = map.layers[layer_name]
|
||||||
|
if layer then
|
||||||
|
local tiles_copy = {}
|
||||||
|
for i = 1, #layer.tiles do tiles_copy[i] = layer.tiles[i] end
|
||||||
|
out.layers[layer_name] = { tiles = tiles_copy }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if map.roof then
|
||||||
|
local roof_copy = {}
|
||||||
|
for i = 1, #map.roof do roof_copy[i] = map.roof[i] end
|
||||||
|
out.roof = roof_copy
|
||||||
|
end
|
||||||
|
return json_value(out, "") .. "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.save_to_disk(map_id, path)
|
||||||
|
local map = require_map(map_id)
|
||||||
|
local json_str = serialize_map_v2(map)
|
||||||
|
local f, err = io.open(path, "w")
|
||||||
|
if not f then
|
||||||
|
error(string.format("maps.save_to_disk: cannot open '%s' (%s)",
|
||||||
|
path, err or "?"))
|
||||||
|
end
|
||||||
|
f:write(json_str)
|
||||||
|
f:close()
|
||||||
|
map._dirty = false
|
||||||
|
end
|
||||||
|
|
||||||
function M.is_indoor(x, y, map_id)
|
function M.is_indoor(x, y, map_id)
|
||||||
local id = map_id or current_map_id
|
local id = map_id or current_map_id
|
||||||
if not id then error("maps.is_indoor: no current map") end
|
if not id then error("maps.is_indoor: no current map") end
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"id":"lib-core.maps","version":"0.3.0","api_min":"0.1"}
|
{"id":"lib-core.maps","version":"0.4.0","api_min":"0.1"}
|
||||||
|
|||||||
Reference in New Issue
Block a user