1375 lines
57 KiB
Text
1375 lines
57 KiB
Text
# ============================================================================
|
||
# tiled.ludic — Tiled map support (`Tiled.*`), the design record of #66
|
||
# (issues #67–#74). Native TMX/TSX/TX (XML) and TMJ/TSJ/TJ (JSON) both map onto
|
||
# ONE intermediate — a generic `Value.*` tree in Tiled's JSON schema — and one
|
||
# format-independent core turns that into the runtime map model. So the two
|
||
# readers below (P0.5, #68) each feed the same `rt_tmap` builder (P1, #69).
|
||
#
|
||
# The JSON path is `Json.parse` + a normalisation pass; the XML path is the
|
||
# `xml_parse` walk in this file. Layer data is decoded to a dense GID int list at
|
||
# read time (CSV split, or base64 -> zlib/gzip inflate -> little-endian u32s), so
|
||
# a CSV `.tmx` and a base64 `.tmj` of the same map yield structurally identical
|
||
# trees.
|
||
#
|
||
# ludicc splices this file (with core + xml + base64 + value) when a program
|
||
# mentions `Tiled.*` (parse.ludic).
|
||
# ============================================================================
|
||
|
||
# ---- layer-data decode -----------------------------------------------------
|
||
# a CSV of GIDs -> a Value list of int nodes. Non-digit separators (commas,
|
||
# whitespace, newlines) delimit; overflow wraps to the correct 32-bit GID
|
||
# (a flip-flagged GID like 0x80000001 lands as the matching negative i32).
|
||
function tiled_csv_list(text: pointer) -> Val {
|
||
let out = value_list()
|
||
let n = len(text)
|
||
var i = 0
|
||
var cur = 0
|
||
var have = 0
|
||
while i < n {
|
||
let c = text[i]
|
||
if c >= '0' and c <= '9' { cur = cur * 10 + (c - 48); have = 1 }
|
||
else {
|
||
if c == ',' or c == ' ' or c == '\t' or c == '\n' or c == '\r' {
|
||
if have == 1 { push(out.kids, value_int(cur)); cur = 0; have = 0 }
|
||
}
|
||
}
|
||
i += 1
|
||
}
|
||
if have == 1 { push(out.kids, value_int(cur)) }
|
||
return out
|
||
}
|
||
|
||
# a base64 (optionally zlib/gzip-compressed) blob of `count` little-endian u32
|
||
# GIDs -> a Value list of int nodes.
|
||
function tiled_b64_list(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, text: pointer, compression: pointer, count: int) -> Val {
|
||
let comp = bytes(len(text) + 4)
|
||
let clen = b64_decode(text, comp)
|
||
let outcap = count * 4 + 16
|
||
var raw = comp
|
||
var rawlen = clen
|
||
if compression == "zlib" {
|
||
let d = bytes(outcap); let dn = z_uncompress(rt_inflate_st, comp, clen, d, outcap); raw = d; rawlen = dn
|
||
} else { if compression == "gzip" {
|
||
let d = bytes(outcap); let dn = z_gunzip(rt_inflate_st, comp, clen, d, outcap); raw = d; rawlen = dn
|
||
} else { if compression == "zstd" {
|
||
let d = bytes(outcap); let dn = z_zstd(rt_zstd_st, comp, clen, d, outcap); raw = d; rawlen = dn
|
||
} } }
|
||
let out = value_list()
|
||
var i = 0
|
||
while i + 3 < rawlen {
|
||
let g = raw[i] | (raw[i + 1] << 8) | (raw[i + 2] << 16) | (raw[i + 3] << 24)
|
||
push(out.kids, value_int(g))
|
||
i += 4
|
||
}
|
||
return out
|
||
}
|
||
|
||
# decode the text of `node` under encoding `enc` / compression `comp` -> GID list.
|
||
function tiled_decode_enc(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, node: Xml, enc: pointer, comp: pointer, count: int) -> Val {
|
||
if enc == "base64" { return tiled_b64_list(rt_inflate_st, rt_zstd_st, xml_text(node), comp, count) }
|
||
return tiled_csv_list(xml_text(node)) # csv (or the tag-per-tile form)
|
||
}
|
||
|
||
# decode a `<data>` element (child of a `<layer>`) into a Value list of GIDs.
|
||
function tiled_data_list(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, data: Xml, count: int) -> Val {
|
||
return tiled_decode_enc(rt_inflate_st, rt_zstd_st, data, xml_attr(data, "encoding"), xml_attr(data, "compression"), count)
|
||
}
|
||
|
||
# flatten a chunked (infinite-map) `<data>` — its `<chunk x y width height>`
|
||
# children — into a dense GID list; sets `o`'s width/height/data (#74).
|
||
function tiled_chunked_layer(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, data: Xml, o: Val) -> void {
|
||
let enc = xml_attr(data, "encoding")
|
||
let comp = xml_attr(data, "compression")
|
||
# pass 1: bounds over every chunk (tile coordinates)
|
||
var minx = 1000000000
|
||
var miny = 1000000000
|
||
var maxx = -1000000000
|
||
var maxy = -1000000000
|
||
var i = 0
|
||
while i < xml_child_count(data) {
|
||
let ch = xml_child(data, i)
|
||
if xml_tag(ch) == "chunk" {
|
||
let cx = xml_attr_int(ch, "x", 0)
|
||
let cy = xml_attr_int(ch, "y", 0)
|
||
if cx < minx { minx = cx }
|
||
if cy < miny { miny = cy }
|
||
if cx + xml_attr_int(ch, "width", 0) > maxx { maxx = cx + xml_attr_int(ch, "width", 0) }
|
||
if cy + xml_attr_int(ch, "height", 0) > maxy { maxy = cy + xml_attr_int(ch, "height", 0) }
|
||
}
|
||
i += 1
|
||
}
|
||
let W = maxx - minx
|
||
let H = maxy - miny
|
||
let gids = value_list()
|
||
var k = 0
|
||
while k < W * H { push(gids.kids, value_int(0)); k += 1 }
|
||
# pass 2: place each chunk's decoded data at its offset
|
||
i = 0
|
||
while i < xml_child_count(data) {
|
||
let ch = xml_child(data, i)
|
||
if xml_tag(ch) == "chunk" {
|
||
let cx = xml_attr_int(ch, "x", 0) - minx
|
||
let cy = xml_attr_int(ch, "y", 0) - miny
|
||
let cw = xml_attr_int(ch, "width", 0)
|
||
let cht = xml_attr_int(ch, "height", 0)
|
||
let cdata = tiled_decode_enc(rt_inflate_st, rt_zstd_st, ch, enc, comp, cw * cht)
|
||
var yy = 0
|
||
while yy < cht {
|
||
var xx = 0
|
||
while xx < cw {
|
||
gids.kids[(cy + yy) * W + (cx + xx)] = value_at(cdata, yy * cw + xx)
|
||
xx += 1
|
||
}
|
||
yy += 1
|
||
}
|
||
}
|
||
i += 1
|
||
}
|
||
value_put(o, "width", value_int(W))
|
||
value_put(o, "height", value_int(H))
|
||
value_put(o, "data", gids)
|
||
}
|
||
|
||
# ---- custom properties -----------------------------------------------------
|
||
# `<properties>` -> a Value list of {name, type, value} objects (P4 reads these;
|
||
# P2 reads the `solid`/`oneway`/`trigger` bool convention off the same list).
|
||
function tmx_props_list(parent: Xml) -> Val {
|
||
let out = value_list()
|
||
let props = xml_find(parent, "properties")
|
||
if xml_tag(props) != "properties" { return out }
|
||
var i = 0
|
||
while i < xml_child_count(props) {
|
||
let p = xml_child(props, i)
|
||
if xml_tag(p) == "property" {
|
||
let o = value_object()
|
||
value_put(o, "name", value_str(xml_attr(p, "name")))
|
||
var ty = xml_attr(p, "type")
|
||
if ty == "" { ty = "string" }
|
||
value_put(o, "type", value_str(ty))
|
||
# value may be an attribute or (for multiline) the element text
|
||
var v = xml_attr(p, "value")
|
||
if xml_has(p, "value") == 0 { v = xml_text(p) }
|
||
value_put(o, "value", value_str(v))
|
||
push(out.kids, o)
|
||
}
|
||
i += 1
|
||
}
|
||
return out
|
||
}
|
||
|
||
# ---- objects (shapes) ------------------------------------------------------
|
||
# one `<object>` -> a Value object mirroring Tiled's JSON object shape.
|
||
function tmx_object_to_value(ob: Xml) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "id", value_int(xml_attr_int(ob, "id", 0)))
|
||
if xml_has(ob, "name") == 1 { value_put(o, "name", value_str(xml_attr(ob, "name"))) }
|
||
if xml_has(ob, "type") == 1 { value_put(o, "type", value_str(xml_attr(ob, "type"))) }
|
||
if xml_has(ob, "class") == 1 { value_put(o, "type", value_str(xml_attr(ob, "class"))) }
|
||
if xml_has(ob, "template") == 1 { value_put(o, "template", value_str(xml_attr(ob, "template"))) }
|
||
value_put(o, "x", value_int(xml_attr_int(ob, "x", 0)))
|
||
value_put(o, "y", value_int(xml_attr_int(ob, "y", 0)))
|
||
value_put(o, "width", value_int(xml_attr_int(ob, "width", 0)))
|
||
value_put(o, "height", value_int(xml_attr_int(ob, "height", 0)))
|
||
if xml_has(ob, "gid") == 1 { value_put(o, "gid", value_int(xml_attr_int(ob, "gid", 0))) }
|
||
if xml_has(ob, "rotation") == 1 { value_put(o, "rotation", value_int(xml_attr_int(ob, "rotation", 0))) }
|
||
# shape markers: ellipse / point / polygon / polyline / text
|
||
let el = xml_find(ob, "ellipse"); if xml_tag(el) == "ellipse" { value_put(o, "ellipse", value_bool(1)) }
|
||
let pt = xml_find(ob, "point"); if xml_tag(pt) == "point" { value_put(o, "point", value_bool(1)) }
|
||
let pg = xml_find(ob, "polygon")
|
||
if xml_tag(pg) == "polygon" { value_put(o, "polygon", tmx_points_list(xml_attr(pg, "points"))) }
|
||
let pl = xml_find(ob, "polyline")
|
||
if xml_tag(pl) == "polyline" { value_put(o, "polyline", tmx_points_list(xml_attr(pl, "points"))) }
|
||
let tx = xml_find(ob, "text")
|
||
if xml_tag(tx) == "text" {
|
||
let t = value_object()
|
||
value_put(t, "text", value_str(xml_text(tx)))
|
||
if xml_has(tx, "pixelsize") == 1 { value_put(t, "pixelsize", value_int(xml_attr_int(tx, "pixelsize", 16))) }
|
||
if xml_has(tx, "bold") == 1 { value_put(t, "bold", value_bool(1)) }
|
||
if xml_has(tx, "italic") == 1 { value_put(t, "italic", value_bool(1)) }
|
||
if xml_has(tx, "halign") == 1 { value_put(t, "halign", value_str(xml_attr(tx, "halign"))) }
|
||
if xml_has(tx, "valign") == 1 { value_put(t, "valign", value_str(xml_attr(tx, "valign"))) }
|
||
value_put(o, "text", t)
|
||
}
|
||
let props = tmx_props_list(ob)
|
||
if len(props.kids) > 0 { value_put(o, "properties", props) }
|
||
return o
|
||
}
|
||
|
||
# "x,y x,y ..." -> a Value list of {x,y} objects.
|
||
function tmx_points_list(s: pointer) -> Val {
|
||
let out = value_list()
|
||
let n = len(s)
|
||
var i = 0
|
||
while i < n {
|
||
while i < n and (s[i] == ' ' or s[i] == '\t') { i += 1 }
|
||
if i >= n { break }
|
||
# read "x,y"
|
||
var xv = 0; var xn = 0; var xs = 0
|
||
if s[i] == '-' { xs = 1; i += 1 }
|
||
while i < n and s[i] >= '0' and s[i] <= '9' { xv = xv * 10 + (s[i] - 48); xn = 1; i += 1 }
|
||
if xs == 1 { xv = -xv }
|
||
if i < n and s[i] == ',' { i += 1 }
|
||
var yv = 0; var ys = 0
|
||
if i < n and s[i] == '-' { ys = 1; i += 1 }
|
||
while i < n and s[i] >= '0' and s[i] <= '9' { yv = yv * 10 + (s[i] - 48); i += 1 }
|
||
if ys == 1 { yv = -yv }
|
||
if xn == 1 {
|
||
let o = value_object()
|
||
value_put(o, "x", value_int(xv))
|
||
value_put(o, "y", value_int(yv))
|
||
push(out.kids, o)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
# ---- tilesets --------------------------------------------------------------
|
||
# one `<tile id=..>` inside a tileset -> a Value object with its metadata:
|
||
# animation frames, per-tile collision objectgroup, class/type, properties.
|
||
function tmx_tile_to_value(t: Xml) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "id", value_int(xml_attr_int(t, "id", 0)))
|
||
if xml_has(t, "type") == 1 { value_put(o, "type", value_str(xml_attr(t, "type"))) }
|
||
if xml_has(t, "class") == 1 { value_put(o, "type", value_str(xml_attr(t, "class"))) }
|
||
if xml_has(t, "probability") == 1 { value_put(o, "probability", value_str(xml_attr(t, "probability"))) }
|
||
# per-tile image (image-collection tilesets)
|
||
let img = xml_find(t, "image")
|
||
if xml_tag(img) == "image" {
|
||
value_put(o, "image", value_str(xml_attr(img, "source")))
|
||
value_put(o, "imagewidth", value_int(xml_attr_int(img, "width", 0)))
|
||
value_put(o, "imageheight", value_int(xml_attr_int(img, "height", 0)))
|
||
}
|
||
# animation frames
|
||
let anim = xml_find(t, "animation")
|
||
if xml_tag(anim) == "animation" {
|
||
let frames = value_list()
|
||
var i = 0
|
||
while i < xml_child_count(anim) {
|
||
let fr = xml_child(anim, i)
|
||
if xml_tag(fr) == "frame" {
|
||
let f = value_object()
|
||
value_put(f, "tileid", value_int(xml_attr_int(fr, "tileid", 0)))
|
||
value_put(f, "duration", value_int(xml_attr_int(fr, "duration", 0)))
|
||
push(frames.kids, f)
|
||
}
|
||
i += 1
|
||
}
|
||
value_put(o, "animation", frames)
|
||
}
|
||
# per-tile collision shapes
|
||
let og = xml_find(t, "objectgroup")
|
||
if xml_tag(og) == "objectgroup" {
|
||
let objs = value_list()
|
||
var j = 0
|
||
while j < xml_child_count(og) {
|
||
let ch = xml_child(og, j)
|
||
if xml_tag(ch) == "object" { push(objs.kids, tmx_object_to_value(ch)) }
|
||
j += 1
|
||
}
|
||
let ogo = value_object()
|
||
value_put(ogo, "objects", objs)
|
||
value_put(o, "objectgroup", ogo)
|
||
}
|
||
let props = tmx_props_list(t)
|
||
if len(props.kids) > 0 { value_put(o, "properties", props) }
|
||
return o
|
||
}
|
||
|
||
# fill the geometry + tiles of a `<tileset>` element into `o` (shared by an
|
||
# embedded tileset and a standalone `.tsx` root).
|
||
function tmx_fill_tileset(o: Val, ts: Xml) -> void {
|
||
if xml_has(ts, "name") == 1 { value_put(o, "name", value_str(xml_attr(ts, "name"))) }
|
||
value_put(o, "tilewidth", value_int(xml_attr_int(ts, "tilewidth", 0)))
|
||
value_put(o, "tileheight", value_int(xml_attr_int(ts, "tileheight", 0)))
|
||
value_put(o, "spacing", value_int(xml_attr_int(ts, "spacing", 0)))
|
||
value_put(o, "margin", value_int(xml_attr_int(ts, "margin", 0)))
|
||
value_put(o, "columns", value_int(xml_attr_int(ts, "columns", 0)))
|
||
value_put(o, "tilecount", value_int(xml_attr_int(ts, "tilecount", 0)))
|
||
let img = xml_find(ts, "image")
|
||
if xml_tag(img) == "image" {
|
||
value_put(o, "image", value_str(xml_attr(img, "source")))
|
||
value_put(o, "imagewidth", value_int(xml_attr_int(img, "width", 0)))
|
||
value_put(o, "imageheight", value_int(xml_attr_int(img, "height", 0)))
|
||
}
|
||
let tiles = value_list()
|
||
var i = 0
|
||
while i < xml_child_count(ts) {
|
||
let ch = xml_child(ts, i)
|
||
if xml_tag(ch) == "tile" { push(tiles.kids, tmx_tile_to_value(ch)) }
|
||
i += 1
|
||
}
|
||
if len(tiles.kids) > 0 { value_put(o, "tiles", tiles) }
|
||
}
|
||
|
||
# a `<tileset>` child of a `<map>` -> a Value object. External (`source=`) keeps
|
||
# the {firstgid, source} reference (as TMJ does); embedded is inlined in full.
|
||
function tmx_tileset_to_value(ts: Xml) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "firstgid", value_int(xml_attr_int(ts, "firstgid", 1)))
|
||
if xml_has(ts, "source") == 1 {
|
||
value_put(o, "source", value_str(xml_attr(ts, "source")))
|
||
return o
|
||
}
|
||
tmx_fill_tileset(o, ts)
|
||
return o
|
||
}
|
||
|
||
# a standalone `.tsx` root -> a tileset Value object (no firstgid — that comes
|
||
# from the map's reference).
|
||
function tsx_to_value(root: Xml) -> Val {
|
||
let o = value_object()
|
||
tmx_fill_tileset(o, root)
|
||
return o
|
||
}
|
||
|
||
# ---- layers ----------------------------------------------------------------
|
||
function tmx_layer_common(o: Val, el: Xml) -> void {
|
||
value_put(o, "id", value_int(xml_attr_int(el, "id", 0)))
|
||
value_put(o, "name", value_str(xml_attr(el, "name")))
|
||
if xml_has(el, "class") == 1 { value_put(o, "class", value_str(xml_attr(el, "class"))) }
|
||
var vis = 1
|
||
if xml_has(el, "visible") == 1 { vis = xml_attr_int(el, "visible", 1) }
|
||
value_put(o, "visible", value_bool(vis))
|
||
if xml_has(el, "opacity") == 1 { value_put(o, "opacity", value_str(xml_attr(el, "opacity"))) }
|
||
if xml_has(el, "offsetx") == 1 { value_put(o, "offsetx", value_int(xml_attr_int(el, "offsetx", 0))) }
|
||
if xml_has(el, "offsety") == 1 { value_put(o, "offsety", value_int(xml_attr_int(el, "offsety", 0))) }
|
||
if xml_has(el, "parallaxx") == 1 { value_put(o, "parallaxx", value_str(xml_attr(el, "parallaxx"))) }
|
||
if xml_has(el, "parallaxy") == 1 { value_put(o, "parallaxy", value_str(xml_attr(el, "parallaxy"))) }
|
||
if xml_has(el, "tintcolor") == 1 { value_put(o, "tintcolor", value_str(xml_attr(el, "tintcolor"))) }
|
||
let props = tmx_props_list(el)
|
||
if len(props.kids) > 0 { value_put(o, "properties", props) }
|
||
}
|
||
|
||
function tmx_tilelayer_to_value(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, el: Xml, mapw: int, maph: int) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "type", value_str("tilelayer"))
|
||
tmx_layer_common(o, el)
|
||
let w = xml_attr_int(el, "width", mapw)
|
||
let h = xml_attr_int(el, "height", maph)
|
||
let data = xml_find(el, "data")
|
||
if xml_count(data, "chunk") > 0 { # infinite map: flatten the chunks
|
||
tiled_chunked_layer(rt_inflate_st, rt_zstd_st, data, o)
|
||
} else {
|
||
value_put(o, "width", value_int(w))
|
||
value_put(o, "height", value_int(h))
|
||
value_put(o, "data", tiled_data_list(rt_inflate_st, rt_zstd_st, data, w * h))
|
||
}
|
||
return o
|
||
}
|
||
|
||
function tmx_objectlayer_to_value(el: Xml) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "type", value_str("objectgroup"))
|
||
tmx_layer_common(o, el)
|
||
let objs = value_list()
|
||
var i = 0
|
||
while i < xml_child_count(el) {
|
||
let ch = xml_child(el, i)
|
||
if xml_tag(ch) == "object" { push(objs.kids, tmx_object_to_value(ch)) }
|
||
i += 1
|
||
}
|
||
value_put(o, "objects", objs)
|
||
return o
|
||
}
|
||
|
||
# ---- map -------------------------------------------------------------------
|
||
# a `<map>` root -> the intermediate map Value tree (Tiled JSON schema).
|
||
function tmx_to_value(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, root: Xml) -> Val {
|
||
let m = value_object()
|
||
value_put(m, "type", value_str("map"))
|
||
value_put(m, "version", value_str(xml_attr(root, "version")))
|
||
value_put(m, "orientation", value_str(xml_attr(root, "orientation")))
|
||
value_put(m, "renderorder", value_str(xml_attr(root, "renderorder")))
|
||
value_put(m, "width", value_int(xml_attr_int(root, "width", 0)))
|
||
value_put(m, "height", value_int(xml_attr_int(root, "height", 0)))
|
||
value_put(m, "tilewidth", value_int(xml_attr_int(root, "tilewidth", 0)))
|
||
value_put(m, "tileheight", value_int(xml_attr_int(root, "tileheight", 0)))
|
||
value_put(m, "infinite", value_bool(xml_attr_int(root, "infinite", 0)))
|
||
if xml_has(root, "backgroundcolor") == 1 { value_put(m, "backgroundcolor", value_str(xml_attr(root, "backgroundcolor"))) }
|
||
if xml_has(root, "hexsidelength") == 1 { value_put(m, "hexsidelength", value_int(xml_attr_int(root, "hexsidelength", 0))) }
|
||
if xml_has(root, "staggeraxis") == 1 { value_put(m, "staggeraxis", value_str(xml_attr(root, "staggeraxis"))) }
|
||
if xml_has(root, "staggerindex") == 1 { value_put(m, "staggerindex", value_str(xml_attr(root, "staggerindex"))) }
|
||
let mapw = xml_attr_int(root, "width", 0)
|
||
let maph = xml_attr_int(root, "height", 0)
|
||
let tilesets = value_list()
|
||
let layers = value_list()
|
||
var i = 0
|
||
while i < xml_child_count(root) {
|
||
let ch = xml_child(root, i)
|
||
let tag = xml_tag(ch)
|
||
if tag == "tileset" { push(tilesets.kids, tmx_tileset_to_value(ch)) }
|
||
else { if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) }
|
||
else { if tag == "objectgroup" { push(layers.kids, tmx_objectlayer_to_value(ch)) }
|
||
else { if tag == "imagelayer" { push(layers.kids, tmx_imagelayer_to_value(ch)) }
|
||
else { if tag == "group" { push(layers.kids, tmx_group_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) } } } } }
|
||
i += 1
|
||
}
|
||
let props = tmx_props_list(root)
|
||
if len(props.kids) > 0 { value_put(m, "properties", props) }
|
||
value_put(m, "tilesets", tilesets)
|
||
value_put(m, "layers", layers)
|
||
return m
|
||
}
|
||
|
||
# `<imagelayer>` -> a Value object (P5 renders these).
|
||
function tmx_imagelayer_to_value(el: Xml) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "type", value_str("imagelayer"))
|
||
tmx_layer_common(o, el)
|
||
let img = xml_find(el, "image")
|
||
if xml_tag(img) == "image" { value_put(o, "image", value_str(xml_attr(img, "source"))) }
|
||
if xml_has(el, "repeatx") == 1 { value_put(o, "repeatx", value_bool(xml_attr_int(el, "repeatx", 0))) }
|
||
if xml_has(el, "repeaty") == 1 { value_put(o, "repeaty", value_bool(xml_attr_int(el, "repeaty", 0))) }
|
||
return o
|
||
}
|
||
|
||
# `<group>` -> a Value object carrying its nested layers (P5 renders recursively).
|
||
function tmx_group_to_value(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, el: Xml, mapw: int, maph: int) -> Val {
|
||
let o = value_object()
|
||
value_put(o, "type", value_str("group"))
|
||
tmx_layer_common(o, el)
|
||
let layers = value_list()
|
||
var i = 0
|
||
while i < xml_child_count(el) {
|
||
let ch = xml_child(el, i)
|
||
let tag = xml_tag(ch)
|
||
if tag == "layer" { push(layers.kids, tmx_tilelayer_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) }
|
||
else { if tag == "objectgroup" { push(layers.kids, tmx_objectlayer_to_value(ch)) }
|
||
else { if tag == "imagelayer" { push(layers.kids, tmx_imagelayer_to_value(ch)) }
|
||
else { if tag == "group" { push(layers.kids, tmx_group_to_value(rt_inflate_st, rt_zstd_st, ch, mapw, maph)) } } } }
|
||
i += 1
|
||
}
|
||
value_put(o, "layers", layers)
|
||
return o
|
||
}
|
||
|
||
# ---- JSON (TMJ) normalisation ----------------------------------------------
|
||
# The JSON reader already yields a Value tree; normalise it so it matches the XML
|
||
# path: decode any base64 `data` string into a dense GID int list, in place, for
|
||
# every tile layer (recursing into groups).
|
||
function tmj_normalize_layer(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, layer: Val) -> void {
|
||
if value_kind(layer) != 6 { return }
|
||
let ty = value_as_str(value_get(layer, "type"))
|
||
if ty == "group" {
|
||
let ls = value_get(layer, "layers")
|
||
var i = 0
|
||
while i < value_count(ls) { tmj_normalize_layer(rt_inflate_st, rt_zstd_st, value_at(ls, i)); i += 1 }
|
||
return
|
||
}
|
||
if ty != "tilelayer" { return }
|
||
let enc = value_as_str(value_get(layer, "encoding"))
|
||
let comp = value_as_str(value_get(layer, "compression"))
|
||
# infinite map: flatten the JSON `chunks` array into a dense data list (#74)
|
||
let chunks = value_get(layer, "chunks")
|
||
if value_kind(chunks) == 5 and value_count(chunks) > 0 {
|
||
tmj_flatten_chunks(rt_inflate_st, rt_zstd_st, layer, chunks, enc, comp)
|
||
return
|
||
}
|
||
let data = value_get(layer, "data")
|
||
if value_kind(data) == 4 { # a base64 string
|
||
let w = value_as_int(value_get(layer, "width"))
|
||
let h = value_as_int(value_get(layer, "height"))
|
||
if enc == "base64" { value_put(layer, "data", tiled_b64_list(rt_inflate_st, rt_zstd_st, value_as_str(data), comp, w * h)) }
|
||
}
|
||
}
|
||
|
||
# a JSON chunk's `data` (int array, or a base64 string) -> a GID Value list.
|
||
function tmj_chunk_gids(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, chunk: Val, enc: pointer, comp: pointer, count: int) -> Val {
|
||
let d = value_get(chunk, "data")
|
||
if value_kind(d) == 4 { return tiled_b64_list(rt_inflate_st, rt_zstd_st, value_as_str(d), comp, count) } # base64 string
|
||
return d # already an int array
|
||
}
|
||
|
||
# flatten JSON `chunks[]` into a dense data list on `layer`, sizing to the union.
|
||
function tmj_flatten_chunks(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, layer: Val, chunks: Val, enc: pointer, comp: pointer) -> void {
|
||
var minx = 1000000000
|
||
var miny = 1000000000
|
||
var maxx = -1000000000
|
||
var maxy = -1000000000
|
||
var i = 0
|
||
while i < value_count(chunks) {
|
||
let c = value_at(chunks, i)
|
||
let cx = value_as_int(value_get(c, "x"))
|
||
let cy = value_as_int(value_get(c, "y"))
|
||
if cx < minx { minx = cx }
|
||
if cy < miny { miny = cy }
|
||
if cx + value_as_int(value_get(c, "width")) > maxx { maxx = cx + value_as_int(value_get(c, "width")) }
|
||
if cy + value_as_int(value_get(c, "height")) > maxy { maxy = cy + value_as_int(value_get(c, "height")) }
|
||
i += 1
|
||
}
|
||
let W = maxx - minx
|
||
let H = maxy - miny
|
||
let gids = value_list()
|
||
var k = 0
|
||
while k < W * H { push(gids.kids, value_int(0)); k += 1 }
|
||
i = 0
|
||
while i < value_count(chunks) {
|
||
let c = value_at(chunks, i)
|
||
let cx = value_as_int(value_get(c, "x")) - minx
|
||
let cy = value_as_int(value_get(c, "y")) - miny
|
||
let cw = value_as_int(value_get(c, "width"))
|
||
let cht = value_as_int(value_get(c, "height"))
|
||
let cdata = tmj_chunk_gids(rt_inflate_st, rt_zstd_st, c, enc, comp, cw * cht)
|
||
var yy = 0
|
||
while yy < cht {
|
||
var xx = 0
|
||
while xx < cw {
|
||
gids.kids[(cy + yy) * W + (cx + xx)] = value_at(cdata, yy * cw + xx)
|
||
xx += 1
|
||
}
|
||
yy += 1
|
||
}
|
||
i += 1
|
||
}
|
||
value_put(layer, "width", value_int(W))
|
||
value_put(layer, "height", value_int(H))
|
||
value_put(layer, "data", gids)
|
||
}
|
||
|
||
function tmj_normalize(rt_inflate_st: mut RtInflateState, rt_zstd_st: mut RtZstdState, m: Val) -> Val {
|
||
let ls = value_get(m, "layers")
|
||
var i = 0
|
||
while i < value_count(ls) { tmj_normalize_layer(rt_inflate_st, rt_zstd_st, value_at(ls, i)); i += 1 }
|
||
return m
|
||
}
|
||
|
||
# ---- top-level readers -----------------------------------------------------
|
||
# skip leading whitespace and return the first non-space byte (0 if none).
|
||
function tiled_first_byte(s: pointer) -> int {
|
||
var i = 0
|
||
let n = len(s)
|
||
while i < n {
|
||
let c = s[i]
|
||
if c == ' ' or c == '\t' or c == '\n' or c == '\r' { i += 1 } else { return c }
|
||
}
|
||
return 0
|
||
}
|
||
|
||
# read a map file (TMX or TMJ, auto-detected by first byte) -> the normalised
|
||
# intermediate map Value tree.
|
||
function tiled_read(rt_inflate_st: mut RtInflateState, rt_xml_st: mut RtXmlState, rt_zstd_st: mut RtZstdState, path: pointer) -> Val {
|
||
let text = Fs.read_text(path)
|
||
if text == null { return value_null() }
|
||
if text == "" { return value_null() }
|
||
if tiled_first_byte(text) == 60 { # '<' -> XML
|
||
return tmx_to_value(rt_inflate_st, rt_zstd_st, xml_parse(rt_xml_st, text))
|
||
}
|
||
return tmj_normalize(rt_inflate_st, rt_zstd_st, json_parse(text)) # '{' -> JSON
|
||
}
|
||
|
||
# read a tileset file (TSX or TSJ) -> a tileset Value object.
|
||
function tiled_read_tsx(rt_xml_st: mut RtXmlState, path: pointer) -> Val {
|
||
let text = Fs.read_text(path)
|
||
if text == null { return value_null() }
|
||
if text == "" { return value_null() }
|
||
if tiled_first_byte(text) == 60 { return tsx_to_value(xml_parse(rt_xml_st, text)) }
|
||
return json_parse(text)
|
||
}
|
||
|
||
# ============================================================================
|
||
# P1 (#69) — the runtime map model (`rt_tmap`), the GID resolver, the legacy
|
||
# `rt_map` compatibility projection, and image-backed rendering. Built from the
|
||
# intermediate Value tree above, so both the TMX and TMJ paths feed it.
|
||
# ============================================================================
|
||
|
||
property TmTileset {
|
||
firstgid: int = 0
|
||
columns: int = 0
|
||
tilew: int = 0
|
||
tileh: int = 0
|
||
spacing: int = 0
|
||
margin: int = 0
|
||
tilecount: int = 0
|
||
imgid: int = -1 # loaded image handle (rt_image_load), -1 = none
|
||
meta: Val # the tileset Value object (per-tile metadata)
|
||
}
|
||
|
||
property TmLayer {
|
||
kind: int = 0 # 0 tilelayer, 1 objectgroup, 2 imagelayer, 3 group
|
||
name: pointer = null
|
||
w: int = 0
|
||
h: int = 0
|
||
visible: int = 1
|
||
imgid: int = -1 # loaded image handle (imagelayer only), -1 = none
|
||
gids: []int # dense w*h GID array (tilelayer only)
|
||
data: Val # the layer Value object (objects, image, …)
|
||
}
|
||
|
||
property Tmap {
|
||
w: int = 0 # map width in tiles
|
||
h: int = 0
|
||
tilew: int = 0 # map tile size in pixels
|
||
tileh: int = 0
|
||
orientation: pointer = null
|
||
layers: []TmLayer
|
||
tilesets: []TmTileset
|
||
tree: Val # the source intermediate tree
|
||
coll: int = -1 # designated collision layer index, or -1
|
||
}
|
||
|
||
# ---- GID resolver ----------------------------------------------------------
|
||
property GidInfo {
|
||
tileset: int = -1 # index into Tmap.tilesets (-1 = none/empty)
|
||
local: int = 0 # local tile id within that tileset
|
||
fh: int = 0 # horizontal flip
|
||
fv: int = 0 # vertical flip
|
||
fd: int = 0 # anti-diagonal flip
|
||
empty: int = 0 # gid was 0
|
||
}
|
||
|
||
# decompose a raw GID: strip the three flip flags (0x80000000 H, 0x40000000 V,
|
||
# 0x20000000 D), keep the low 29 bits as the global tile id, and find the tileset
|
||
# whose firstgid is the greatest not exceeding it. gid == 0 is the empty cell.
|
||
function tmap_resolve(m: Tmap, gid: int) -> GidInfo {
|
||
let r = new GidInfo
|
||
if gid == 0 { r.empty = 1; r.tileset = -1; return r }
|
||
let H = 1 << 31
|
||
let V = 1 << 30
|
||
let D = 1 << 29
|
||
if (gid & H) != 0 { r.fh = 1 }
|
||
if (gid & V) != 0 { r.fv = 1 }
|
||
if (gid & D) != 0 { r.fd = 1 }
|
||
let id = gid & 536870911 # low 29 bits = global tile id
|
||
var best = -1
|
||
var bestfg = 0
|
||
var i = 0
|
||
while i < len(m.tilesets) {
|
||
let fg = m.tilesets[i].firstgid
|
||
if fg <= id and fg >= bestfg { bestfg = fg; best = i }
|
||
i += 1
|
||
}
|
||
r.tileset = best
|
||
if best >= 0 { r.local = id - m.tilesets[best].firstgid }
|
||
return r
|
||
}
|
||
|
||
# ---- accessors -------------------------------------------------------------
|
||
function tmap_width(m: Tmap) -> int { return m.w }
|
||
function tmap_height(m: Tmap) -> int { return m.h }
|
||
function tmap_layer_count(m: Tmap) -> int { return len(m.layers) }
|
||
function tmap_layer_name(m: Tmap, i: int) -> string {
|
||
if i < 0 or i >= len(m.layers) { return "" }
|
||
if m.layers[i].name == null { return "" }
|
||
return m.layers[i].name
|
||
}
|
||
function tmap_tree(m: Tmap) -> Val { return m.tree }
|
||
|
||
# raw GID at (x,y) in tile layer `layer` (0 out of bounds / non-tile layer).
|
||
function tmap_gid(m: Tmap, layer: int, x: int, y: int) -> int {
|
||
if layer < 0 or layer >= len(m.layers) { return 0 }
|
||
let l = m.layers[layer]
|
||
if l.kind != 0 { return 0 }
|
||
if x < 0 or y < 0 or x >= l.w or y >= l.h { return 0 }
|
||
return l.gids[y * l.w + x]
|
||
}
|
||
|
||
# ---- per-tile metadata (property convention) -------------------------------
|
||
# does the tile a GID resolves to carry bool custom property `name` = true?
|
||
# the metadata Value object for the tile a GID resolves to (a `null` node if the
|
||
# tile carries none). Shared by the property / objectgroup / animation lookups.
|
||
function tmap_tile_meta(m: Tmap, gid: int) -> Val {
|
||
let r = tmap_resolve(m, gid)
|
||
if r.tileset < 0 { return value_null() }
|
||
let ts = m.tilesets[r.tileset]
|
||
if value_kind(ts.meta) != 6 { return value_null() }
|
||
let tiles = value_get(ts.meta, "tiles")
|
||
if value_kind(tiles) != 5 { return value_null() }
|
||
var i = 0
|
||
while i < value_count(tiles) {
|
||
let t = value_at(tiles, i)
|
||
if value_as_int(value_get(t, "id")) == r.local { return t }
|
||
i += 1
|
||
}
|
||
return value_null()
|
||
}
|
||
|
||
# does the tile a GID resolves to carry bool custom property `name` = true?
|
||
function tmap_tile_prop(m: Tmap, gid: int, name: pointer) -> int {
|
||
let t = tmap_tile_meta(m, gid)
|
||
if value_kind(t) != 6 { return 0 }
|
||
let props = value_get(t, "properties")
|
||
if value_kind(props) != 5 { return 0 }
|
||
var j = 0
|
||
while j < value_count(props) {
|
||
let p = value_at(props, j)
|
||
if value_as_str(value_get(p, "name")) == name {
|
||
if value_as_str(value_get(p, "value")) == "true" { return 1 }
|
||
return 0
|
||
}
|
||
j += 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
# does the tile a GID resolves to carry a per-tile <objectgroup> collision shape?
|
||
function tmap_tile_has_shapes(m: Tmap, gid: int) -> int {
|
||
let t = tmap_tile_meta(m, gid)
|
||
if value_kind(t) != 6 { return 0 }
|
||
let og = value_get(t, "objectgroup")
|
||
if value_kind(og) != 6 { return 0 }
|
||
let objs = value_get(og, "objects")
|
||
if value_count(objs) > 0 { return 1 }
|
||
return 0
|
||
}
|
||
|
||
# ---- collision normalisation (P2, #70) -------------------------------------
|
||
# the collision kind of a GID for the byte-grid projection, from the tile's
|
||
# metadata alone (design §3.5 priority): per-tile <objectgroup> hitboxes, then
|
||
# the solid/oneway/trigger property convention. 0 = no collision metadata,
|
||
# 1 = solid, 2 = one-way, 3 = trigger. The designated-collision-layer fallback
|
||
# (any non-zero GID is solid) is applied by the projection, not here.
|
||
function tmap_collision_kind(m: Tmap, gid: int) -> int {
|
||
if gid == 0 { return 0 }
|
||
if tmap_tile_prop(m, gid, "trigger") == 1 { return 3 }
|
||
if tmap_tile_prop(m, gid, "oneway") == 1 { return 2 }
|
||
if tmap_tile_has_shapes(m, gid) == 1 { return 1 }
|
||
if tmap_tile_prop(m, gid, "solid") == 1 { return 1 }
|
||
return 0
|
||
}
|
||
|
||
# ---- legacy rt_map compatibility projection --------------------------------
|
||
# project a layer down to the byte tilemap so Grid.* / Path.* / esys_move keep
|
||
# working unchanged. `whole_layer_solid` picks the source (design §3.5): true for
|
||
# a designated collision LAYER (a bare non-zero GID with no metadata still counts
|
||
# as solid — the fallback source); false to drive collision from per-tile
|
||
# metadata alone (objectgroup shapes / property convention on a visual layer).
|
||
# solid -> '#' (35), one-way -> '=' (61), trigger/empty -> ' ' (32, passable).
|
||
function tmap_project_layer(rt_core_st: mut RtCoreState, m: Tmap, layer: int, whole_layer_solid: int) -> void {
|
||
m.coll = layer
|
||
rt_map_size(rt_core_st, m.w, m.h) # clamps to 96x64, clears to ' '
|
||
if layer < 0 or layer >= len(m.layers) { return }
|
||
let l = m.layers[layer]
|
||
if l.kind != 0 { return }
|
||
var y = 0
|
||
while y < l.h {
|
||
if y < 64 {
|
||
var x = 0
|
||
while x < l.w {
|
||
if x < 96 {
|
||
let gid = l.gids[y * l.w + x]
|
||
if gid != 0 {
|
||
var k = tmap_collision_kind(m, gid)
|
||
if k == 0 and whole_layer_solid != 0 { k = 1 } # collision-layer fallback
|
||
if k == 1 { rt_core_st.rt_map[y * 96 + x] = '#' } # '#'
|
||
if k == 2 { rt_core_st.rt_map[y * 96 + x] = '=' } # '='
|
||
}
|
||
}
|
||
x += 1
|
||
}
|
||
}
|
||
y += 1
|
||
}
|
||
}
|
||
|
||
# project a designated collision layer (non-zero GID is solid unless its tile
|
||
# metadata says otherwise) — the P1 default, called automatically on load.
|
||
function tmap_project(rt_core_st: mut RtCoreState, m: Tmap, layer: int) -> void { tmap_project_layer(rt_core_st, m, layer, 1) }
|
||
|
||
# drive collision from per-tile metadata alone (objectgroup hitboxes / property
|
||
# convention) over any layer — a tile with no collision metadata stays passable.
|
||
function tmap_collide(rt_core_st: mut RtCoreState, m: Tmap, layer: int) -> void { tmap_project_layer(rt_core_st, m, layer, 0) }
|
||
|
||
# find a tile layer named collision/solids/walls (case-sensitive), or -1.
|
||
function tmap_find_collision(m: Tmap) -> int {
|
||
var i = 0
|
||
while i < len(m.layers) {
|
||
let n = m.layers[i].name
|
||
if m.layers[i].kind == 0 and (n == "collision" or n == "solids" or n == "walls" or n == "Collision" or n == "Solids") { return i }
|
||
i += 1
|
||
}
|
||
return -1
|
||
}
|
||
|
||
# ---- rendering -------------------------------------------------------------
|
||
# blit one tilew x tileh tile from a tileset image (source origin sx,sy) to the
|
||
# framebuffer at (dx,dy), applying the three flip flags. Square tiles assumed
|
||
# for the diagonal flip (Kenney art is 16x16), which is the orthogonal case.
|
||
function tmap_blit_tile(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, imgid: int, sx: int, sy: int, tw: int, th: int, dx: int, dy: int, fh: int, fv: int, fd: int) -> void {
|
||
if imgid < 0 { return }
|
||
let s: words = rt_image_st.img_px[imgid]
|
||
let iw = rt_image_st.img_w[imgid]
|
||
var j = 0
|
||
while j < th {
|
||
var i = 0
|
||
while i < tw {
|
||
var u = i
|
||
var v = j
|
||
if fd == 1 { u = j; v = i }
|
||
if fh == 1 { u = tw - 1 - u }
|
||
if fv == 1 { v = th - 1 - v }
|
||
let argb = s[(sy + v) * iw + (sx + u)]
|
||
rt_blend_px(rt_core_st, dx + i, dy + j, argb)
|
||
i += 1
|
||
}
|
||
j += 1
|
||
}
|
||
}
|
||
|
||
# draw one tile GID at map cell (x,y) with the camera offset already applied.
|
||
function tmap_draw_gid(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, gid: int, dx: int, dy: int) -> void {
|
||
if gid == 0 { return }
|
||
let r = tmap_resolve(m, gid)
|
||
if r.tileset < 0 { return }
|
||
let ts = m.tilesets[r.tileset]
|
||
if ts.imgid < 0 { return }
|
||
let cols = ts.columns
|
||
if cols <= 0 { return }
|
||
let cx = r.local - (r.local / cols) * cols
|
||
let cy = r.local / cols
|
||
let sx = ts.margin + cx * (ts.tilew + ts.spacing)
|
||
let sy = ts.margin + cy * (ts.tileh + ts.spacing)
|
||
# Tiled anchors a tile by its bottom-left, so a tile taller than the map cell
|
||
# rises above the cell.
|
||
let ddy = dy - (ts.tileh - m.tileh)
|
||
tmap_blit_tile(rt_core_st, rt_image_st, ts.imgid, sx, sy, ts.tilew, ts.tileh, dx, ddy, r.fh, r.fv, r.fd)
|
||
}
|
||
|
||
# (tmap_draw / tmap_draw_anim are defined in the P3 section below, over the shared
|
||
# tmap_draw_full — a static draw plus animated tiles + tile objects.)
|
||
|
||
# ---- build the model from the intermediate Value tree ----------------------
|
||
function tmap_gids_from_layer(lv: Val) -> []int {
|
||
let out = new []int
|
||
let data = value_get(lv, "data")
|
||
var i = 0
|
||
while i < value_count(data) { push(out, value_as_int(value_at(data, i))); i += 1 }
|
||
return out
|
||
}
|
||
|
||
# fill a TmTileset's geometry from a tileset Value object (its per-tile metadata
|
||
# stays in `.meta` for the resolver / property convention).
|
||
function tmap_tileset_from_value(tv: Val) -> TmTileset {
|
||
let ts = new TmTileset
|
||
ts.firstgid = value_as_int(value_get(tv, "firstgid"))
|
||
ts.tilew = value_as_int(value_get(tv, "tilewidth"))
|
||
ts.tileh = value_as_int(value_get(tv, "tileheight"))
|
||
ts.spacing = value_as_int(value_get(tv, "spacing"))
|
||
ts.margin = value_as_int(value_get(tv, "margin"))
|
||
ts.tilecount = value_as_int(value_get(tv, "tilecount"))
|
||
ts.columns = value_as_int(value_get(tv, "columns"))
|
||
if ts.columns <= 0 and ts.tilew > 0 { # derive columns from the image
|
||
let iw = value_as_int(value_get(tv, "imagewidth"))
|
||
if iw > 0 { ts.columns = (iw - 2 * ts.margin + ts.spacing) / (ts.tilew + ts.spacing) }
|
||
}
|
||
ts.meta = tv
|
||
return ts
|
||
}
|
||
|
||
function tiled_join(a: pointer, b: pointer) -> string { return Path.normalize(Path.join(a, b)) }
|
||
|
||
# build the runtime map from an intermediate tree, resolving external tilesets
|
||
# and loading tileset images relative to `basedir`.
|
||
function tmap_build(rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_xml_st: mut RtXmlState, tree: Val, basedir: pointer) -> Tmap {
|
||
let m = new Tmap
|
||
m.tree = tree
|
||
m.layers = new []TmLayer
|
||
m.tilesets = new []TmTileset
|
||
m.orientation = value_as_str(value_get(tree, "orientation"))
|
||
m.w = value_as_int(value_get(tree, "width"))
|
||
m.h = value_as_int(value_get(tree, "height"))
|
||
m.tilew = value_as_int(value_get(tree, "tilewidth"))
|
||
m.tileh = value_as_int(value_get(tree, "tileheight"))
|
||
# tilesets
|
||
let tss = value_get(tree, "tilesets")
|
||
var i = 0
|
||
while i < value_count(tss) {
|
||
var tv = value_at(tss, i)
|
||
var imgdir = basedir
|
||
let src = value_as_str(value_get(tv, "source"))
|
||
if src != "" { # external .tsx/.tsj
|
||
let tsxpath = tiled_join(basedir, src)
|
||
let ext = tiled_read_tsx(rt_xml_st, tsxpath)
|
||
value_put(ext, "firstgid", value_get(tv, "firstgid"))
|
||
tv = ext
|
||
imgdir = Path.dir(tsxpath)
|
||
}
|
||
let ts = tmap_tileset_from_value(tv)
|
||
let img = value_as_str(value_get(tv, "image"))
|
||
if img != "" { ts.imgid = rt_image_load(rt_image_st, rt_inflate_st, tiled_join(imgdir, img)) }
|
||
push(m.tilesets, ts)
|
||
i += 1
|
||
}
|
||
# layers (flat; groups' children are lifted so tile layers render/resolve)
|
||
let ls = value_get(tree, "layers")
|
||
i = 0
|
||
while i < value_count(ls) { tmap_add_layer(m, value_at(ls, i)); i += 1 }
|
||
# image layers (#73): load each <imagelayer> image relative to the map file
|
||
i = 0
|
||
while i < len(m.layers) {
|
||
if m.layers[i].kind == 2 {
|
||
let img = value_as_str(value_get(m.layers[i].data, "image"))
|
||
if img != "" { m.layers[i].imgid = rt_image_load(rt_image_st, rt_inflate_st, tiled_join(basedir, img)) }
|
||
}
|
||
i += 1
|
||
}
|
||
# infinite map (#74): the header w/h are 0 — take the flattened layer bounds.
|
||
if m.w <= 0 or m.h <= 0 {
|
||
var mw = 0
|
||
var mh = 0
|
||
i = 0
|
||
while i < len(m.layers) {
|
||
if m.layers[i].kind == 0 {
|
||
if m.layers[i].w > mw { mw = m.layers[i].w }
|
||
if m.layers[i].h > mh { mh = m.layers[i].h }
|
||
}
|
||
i += 1
|
||
}
|
||
m.w = mw
|
||
m.h = mh
|
||
}
|
||
return m
|
||
}
|
||
|
||
# ---- .world stitching (#74) ------------------------------------------------
|
||
# read a `.world` file (JSON): { maps: [{fileName,x,y}], patterns: [...],
|
||
# onlyShowAdjacentMaps }. Reuses Json.parse; the members stitch at their offsets.
|
||
function tiled_read_world(path: pointer) -> Val {
|
||
let text = Fs.read_text(path)
|
||
if text == null { return value_null() }
|
||
if text == "" { return value_null() }
|
||
return json_parse(text)
|
||
}
|
||
function tiled_world_count(world: Val) -> int { return value_count(value_get(world, "maps")) }
|
||
function tiled_world_map(world: Val, i: int) -> Val { return value_at(value_get(world, "maps"), i) }
|
||
|
||
function tmap_add_layer(m: Tmap, lv: Val) -> void {
|
||
let ty = value_as_str(value_get(lv, "type"))
|
||
let l = new TmLayer
|
||
l.data = lv
|
||
l.name = value_as_str(value_get(lv, "name"))
|
||
var vis = 1
|
||
if value_has(lv, "visible") == 1 { if value_as_int(value_get(lv, "visible")) == 0 { vis = 0 } }
|
||
l.visible = vis
|
||
if ty == "tilelayer" {
|
||
l.kind = 0
|
||
l.w = value_as_int(value_get(lv, "width"))
|
||
l.h = value_as_int(value_get(lv, "height"))
|
||
l.gids = tmap_gids_from_layer(lv)
|
||
push(m.layers, l)
|
||
return
|
||
}
|
||
if ty == "objectgroup" { l.kind = 1; push(m.layers, l); return }
|
||
if ty == "imagelayer" { l.kind = 2; push(m.layers, l); return }
|
||
if ty == "group" {
|
||
l.kind = 3; push(m.layers, l)
|
||
let sub = value_get(lv, "layers")
|
||
var i = 0
|
||
while i < value_count(sub) { tmap_add_layer(m, value_at(sub, i)); i += 1 }
|
||
return
|
||
}
|
||
push(m.layers, l)
|
||
}
|
||
|
||
# ---- top-level load --------------------------------------------------------
|
||
# load a Tiled map file into the runtime model: read + parse (TMX or TMJ), build
|
||
# the model (resolving external tilesets + images), and project the collision
|
||
# layer (a tile layer named collision/solids/walls) down to the legacy tilemap.
|
||
function tiled_load(rt_core_st: mut RtCoreState, rt_image_st: mut RtImageState, rt_inflate_st: mut RtInflateState, rt_xml_st: mut RtXmlState, rt_zstd_st: mut RtZstdState, path: pointer) -> Tmap {
|
||
let tree = tiled_read(rt_inflate_st, rt_xml_st, rt_zstd_st, path)
|
||
let m = tmap_build(rt_image_st, rt_inflate_st, rt_xml_st, tree, Path.dir(path))
|
||
tmap_resolve_templates(rt_xml_st, m, Path.dir(path)) # #72: fill template-instance objects
|
||
let c = tmap_find_collision(m)
|
||
if c >= 0 { tmap_project(rt_core_st, m, c) }
|
||
return m
|
||
}
|
||
|
||
# ============================================================================
|
||
# P3 (#71) — animated tiles + tile objects.
|
||
#
|
||
# Animated tiles: a tileset's <animation> is a list of {tileid, duration(ms)}
|
||
# frames. The current frame is a pure function of the engine frame counter
|
||
# (fixed 60/s, deterministic — the same mechanism SpriteAnim rides), so it ticks
|
||
# for free and reproduces frame-for-frame across runs. An animated GID resolves,
|
||
# at draw, to the current frame's GID (its flip flags preserved).
|
||
#
|
||
# Tile objects: an object-layer entry with a `gid` draws the tile image (with its
|
||
# own flip flags), bottom-anchored at the object position, as a placeable sprite.
|
||
# ============================================================================
|
||
|
||
# the three flip-flag bits of a GID (0xE0000000), kept when swapping an animation
|
||
# frame's tile id in.
|
||
function tmap_flag_bits(gid: int) -> int { return gid & ((1 << 31) | (1 << 30) | (1 << 29)) }
|
||
|
||
# the GID an (animated) GID resolves to at engine `frame` — frames not carrying an
|
||
# <animation> pass through unchanged. elapsed ms = frame * 1000 / 60 (the fixed
|
||
# 60/s clock), then walk the frame durations to the current one.
|
||
function tmap_frame_gid(m: Tmap, gid: int, frame: int) -> int {
|
||
if gid == 0 { return 0 }
|
||
let r = tmap_resolve(m, gid)
|
||
if r.tileset < 0 { return gid }
|
||
let t = tmap_tile_meta(m, gid)
|
||
if value_kind(t) != 6 { return gid }
|
||
let anim = value_get(t, "animation")
|
||
if value_kind(anim) != 5 { return gid }
|
||
let nfr = value_count(anim)
|
||
if nfr == 0 { return gid }
|
||
var total = 0
|
||
var i = 0
|
||
while i < nfr { total = total + value_as_int(value_get(value_at(anim, i), "duration")); i += 1 }
|
||
if total <= 0 { return gid }
|
||
let t_ms = (frame * 1000 / 60) % total
|
||
var cur = value_as_int(value_get(value_at(anim, 0), "tileid"))
|
||
var acc = 0
|
||
i = 0
|
||
while i < nfr {
|
||
let fr = value_at(anim, i)
|
||
acc = acc + value_as_int(value_get(fr, "duration"))
|
||
if t_ms < acc { cur = value_as_int(value_get(fr, "tileid")); i = nfr }
|
||
else { i += 1 }
|
||
}
|
||
let ts = m.tilesets[r.tileset]
|
||
return (ts.firstgid + cur) | tmap_flag_bits(gid)
|
||
}
|
||
|
||
# is a GID animated (its tile carries a non-empty <animation>)?
|
||
function tmap_is_animated(m: Tmap, gid: int) -> int {
|
||
let t = tmap_tile_meta(m, gid)
|
||
if value_kind(t) != 6 { return 0 }
|
||
let anim = value_get(t, "animation")
|
||
if value_kind(anim) == 5 and value_count(anim) > 0 { return 1 }
|
||
return 0
|
||
}
|
||
|
||
# draw every visible tile layer (animated tiles resolved for `frame`) plus every
|
||
# tile object on the object layers, in file order, offset by the camera.
|
||
function tmap_draw_full(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, camx: int, camy: int, frame: int, animate: int) -> void {
|
||
var li = 0
|
||
while li < len(m.layers) {
|
||
let l = m.layers[li]
|
||
if l.kind == 0 and l.visible != 0 { # tile layer
|
||
let lox = tmap_layer_offsetx(m, li)
|
||
let loy = tmap_layer_offsety(m, li)
|
||
var y = 0
|
||
while y < l.h {
|
||
var x = 0
|
||
while x < l.w {
|
||
var gid = l.gids[y * l.w + x]
|
||
if gid != 0 {
|
||
if animate != 0 { gid = tmap_frame_gid(m, gid, frame) }
|
||
# orientation transform places the cell (orthogonal / iso / hex / staggered)
|
||
tmap_draw_gid(rt_core_st, rt_image_st, m, gid, tmap_cell_sx(m, x, y) + lox - camx, tmap_cell_sy(m, x, y) + loy - camy)
|
||
}
|
||
x += 1
|
||
}
|
||
y += 1
|
||
}
|
||
}
|
||
if l.kind == 2 and l.visible != 0 { # image layer (parallax / repeat)
|
||
tmap_draw_imagelayer(rt_core_st, rt_image_st, m, l, camx, camy)
|
||
}
|
||
if l.kind == 1 and l.visible != 0 { # object layer: draw tile objects
|
||
let objs = value_get(l.data, "objects")
|
||
var oi = 0
|
||
while oi < value_count(objs) {
|
||
let ob = value_at(objs, oi)
|
||
if value_has(ob, "gid") == 1 {
|
||
var g = value_as_int(value_get(ob, "gid"))
|
||
if animate != 0 { g = tmap_frame_gid(m, g, frame) }
|
||
let ox = value_as_int(value_get(ob, "x"))
|
||
let oy = value_as_int(value_get(ob, "y"))
|
||
# Tiled anchors a tile object by its bottom-left corner
|
||
tmap_draw_gid(rt_core_st, rt_image_st, m, g, ox - camx, oy - m.tileh - camy)
|
||
}
|
||
oi += 1
|
||
}
|
||
}
|
||
li += 1
|
||
}
|
||
}
|
||
|
||
# static draw (no animation) — the P1 entry point.
|
||
function tmap_draw(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(rt_core_st, rt_image_st, m, camx, camy, 0, 0) }
|
||
|
||
# animated draw at engine `frame` (pass Time.frame): animated tiles advance,
|
||
# deterministically and frame-identically across runs.
|
||
function tmap_draw_anim(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(rt_core_st, rt_image_st, m, camx, camy, frame, 1) }
|
||
|
||
# ============================================================================
|
||
# P4 (#72) — object layers (shapes + text), custom properties/types, templates,
|
||
# and opt-in entity spawning. Objects, shapes and properties already parse into
|
||
# the intermediate tree (P0.5); this phase exposes them, resolves custom-type
|
||
# defaults and template inheritance, and maps an object onto Ludic components.
|
||
# ============================================================================
|
||
|
||
# ---- object accessors ------------------------------------------------------
|
||
function tmap_object_layer(m: Tmap, layer: int) -> Val {
|
||
if layer < 0 or layer >= len(m.layers) { return value_null() }
|
||
return m.layers[layer].data
|
||
}
|
||
function tmap_object_count(m: Tmap, layer: int) -> int {
|
||
return value_count(value_get(tmap_object_layer(m, layer), "objects"))
|
||
}
|
||
function tmap_object(m: Tmap, layer: int, i: int) -> Val {
|
||
return value_at(value_get(tmap_object_layer(m, layer), "objects"), i)
|
||
}
|
||
|
||
# the shape of an object: "tile" (a gid object), "point", "ellipse", "polygon",
|
||
# "polyline", "text", or "rectangle" (the default).
|
||
function tiled_object_shape(obj: Val) -> string {
|
||
if value_has(obj, "gid") == 1 { return "tile" }
|
||
if value_has(obj, "point") == 1 { return "point" }
|
||
if value_has(obj, "ellipse") == 1 { return "ellipse" }
|
||
if value_has(obj, "polygon") == 1 { return "polygon" }
|
||
if value_has(obj, "polyline") == 1 { return "polyline" }
|
||
if value_has(obj, "text") == 1 { return "text" }
|
||
return "rectangle"
|
||
}
|
||
|
||
# ---- custom properties -----------------------------------------------------
|
||
# the {name,type,value} property node named `name` in a container's "properties"
|
||
# list (an object / tile / layer / map), or a null node.
|
||
function tiled_prop_node(container: Val, name: pointer) -> Val {
|
||
let props = value_get(container, "properties")
|
||
if value_kind(props) != 5 { return value_null() }
|
||
var i = 0
|
||
while i < value_count(props) {
|
||
let p = value_at(props, i)
|
||
if value_as_str(value_get(p, "name")) == name { return p }
|
||
i += 1
|
||
}
|
||
return value_null()
|
||
}
|
||
|
||
# a property's raw string value, with a fallback to the object's custom-type
|
||
# default (objecttypes.xml, via the type table). "" when absent everywhere.
|
||
function tiled_prop_str(rt_tiled_st: mut RtTiledState, container: Val, name: pointer) -> string {
|
||
let p = tiled_prop_node(container, name)
|
||
if value_kind(p) == 6 { return value_as_str(value_get(p, "value")) }
|
||
# fall back to the container's class default
|
||
let cls = value_as_str(value_get(container, "type"))
|
||
if cls != "" {
|
||
let d = tiled_type_default(rt_tiled_st, cls, name)
|
||
if value_kind(d) == 6 { return value_as_str(value_get(d, "value")) }
|
||
}
|
||
return ""
|
||
}
|
||
|
||
# a property parsed as an integer ("true"/"false" -> 1/0), with the same default
|
||
# fallback.
|
||
function tiled_prop_int(rt_tiled_st: mut RtTiledState, container: Val, name: pointer) -> int {
|
||
let s = tiled_prop_str(rt_tiled_st, container, name)
|
||
if s == "true" { return 1 }
|
||
if s == "false" { return 0 }
|
||
return xml_atoi(s)
|
||
}
|
||
|
||
function tiled_prop_type(rt_tiled_st: mut RtTiledState, container: Val, name: pointer) -> string {
|
||
let p = tiled_prop_node(container, name)
|
||
if value_kind(p) == 6 { return value_as_str(value_get(p, "type")) }
|
||
let cls = value_as_str(value_get(container, "type"))
|
||
if cls != "" {
|
||
let d = tiled_type_default(rt_tiled_st, cls, name)
|
||
if value_kind(d) == 6 { return value_as_str(value_get(d, "type")) }
|
||
}
|
||
return ""
|
||
}
|
||
|
||
# ---- custom types (objecttypes.xml) ----------------------------------------
|
||
# The project custom-type table: an object mapping a type/class name to its list
|
||
# of {name,type,value(default)} property definitions. class/enum properties then
|
||
# resolve their defaults against it.
|
||
export state RtTiledState {
|
||
tiled_type_table: Val = null
|
||
}
|
||
|
||
function tiled_types(rt_tiled_st: mut RtTiledState) -> Val {
|
||
if rt_tiled_st.tiled_type_table == null { rt_tiled_st.tiled_type_table = value_object() }
|
||
return rt_tiled_st.tiled_type_table
|
||
}
|
||
|
||
# load an objecttypes.xml file into the type table. Each <objecttype name=..> maps
|
||
# to its <property name= type= default=> list.
|
||
function tiled_load_types(rt_tiled_st: mut RtTiledState, rt_xml_st: mut RtXmlState, path: pointer) -> int {
|
||
let text = Fs.read_text(path)
|
||
if text == null { return 0 }
|
||
if text == "" { return 0 }
|
||
let root = xml_parse(rt_xml_st, text)
|
||
let tbl = tiled_types(rt_tiled_st)
|
||
var n = 0
|
||
var i = 0
|
||
while i < xml_child_count(root) {
|
||
let ot = xml_child(root, i)
|
||
if xml_tag(ot) == "objecttype" {
|
||
let props = value_list()
|
||
var j = 0
|
||
while j < xml_child_count(ot) {
|
||
let pr = xml_child(ot, j)
|
||
if xml_tag(pr) == "property" {
|
||
let o = value_object()
|
||
value_put(o, "name", value_str(xml_attr(pr, "name")))
|
||
var ty = xml_attr(pr, "type")
|
||
if ty == "" { ty = "string" }
|
||
value_put(o, "type", value_str(ty))
|
||
value_put(o, "value", value_str(xml_attr(pr, "default")))
|
||
push(props.kids, o)
|
||
}
|
||
j += 1
|
||
}
|
||
value_put(tbl, xml_attr(ot, "name"), props)
|
||
n += 1
|
||
}
|
||
i += 1
|
||
}
|
||
return n
|
||
}
|
||
|
||
# the default {name,type,value} property node for a custom type, or a null node.
|
||
function tiled_type_default(rt_tiled_st: mut RtTiledState, typename: pointer, propname: pointer) -> Val {
|
||
let tbl = tiled_types(rt_tiled_st)
|
||
let props = value_get(tbl, typename)
|
||
if value_kind(props) != 5 { return value_null() }
|
||
var i = 0
|
||
while i < value_count(props) {
|
||
let p = value_at(props, i)
|
||
if value_as_str(value_get(p, "name")) == propname { return p }
|
||
i += 1
|
||
}
|
||
return value_null()
|
||
}
|
||
|
||
# ---- templates (.tx / .tj) -------------------------------------------------
|
||
# read a template file -> its object Value (the <object> a .tx wraps, or the
|
||
# "object" of a .tj). External reusable object definitions.
|
||
function tiled_read_template(rt_xml_st: mut RtXmlState, path: pointer) -> Val {
|
||
let text = Fs.read_text(path)
|
||
if text == null { return value_null() }
|
||
if text == "" { return value_null() }
|
||
if tiled_first_byte(text) == 60 { # XML .tx
|
||
let root = xml_parse(rt_xml_st, text) # <template>
|
||
let ob = xml_find(root, "object")
|
||
if xml_tag(ob) == "object" { return tmx_object_to_value(ob) }
|
||
return value_null()
|
||
}
|
||
let j = json_parse(text) # JSON .tj
|
||
return value_get(j, "object")
|
||
}
|
||
|
||
# merge a template's fields into an instance object: the instance keeps every key
|
||
# it already sets; the template fills in the rest (so an instance inherits the
|
||
# template's gid / shape / size / properties, overriding per field).
|
||
function tiled_merge_template(inst: Val, tmpl: Val) -> void {
|
||
if value_kind(tmpl) != 6 { return }
|
||
var i = 0
|
||
while i < value_count(tmpl) {
|
||
let k = value_key_at(tmpl, i)
|
||
if value_has(inst, k) == 0 { value_put(inst, k, value_at(tmpl, i)) }
|
||
i += 1
|
||
}
|
||
}
|
||
|
||
# resolve every object that references a `template`, loading + merging it. Paths
|
||
# are relative to the map file (`basedir`).
|
||
function tmap_resolve_templates(rt_xml_st: mut RtXmlState, m: Tmap, basedir: pointer) -> void {
|
||
var li = 0
|
||
while li < len(m.layers) {
|
||
let l = m.layers[li]
|
||
if l.kind == 1 {
|
||
let objs = value_get(l.data, "objects")
|
||
var oi = 0
|
||
while oi < value_count(objs) {
|
||
let ob = value_at(objs, oi)
|
||
let tp = value_as_str(value_get(ob, "template"))
|
||
if tp != "" { tiled_merge_template(ob, tiled_read_template(rt_xml_st, tiled_join(basedir, tp))) }
|
||
oi += 1
|
||
}
|
||
}
|
||
li += 1
|
||
}
|
||
}
|
||
|
||
# ============================================================================
|
||
# P5 (#73) — image & group layers, orientation coordinate transforms, and Wang
|
||
# GID resolution. Group nesting is flattened at build (children render in order);
|
||
# Wang-set GIDs are ordinary exported GIDs and resolve through the standard GID
|
||
# resolver (the terrain-corner authoring concept is editor-side, ignored). This
|
||
# section adds the orientation transforms (iso / staggered / hex) and image-layer
|
||
# rendering (parallax / repeat) that the breadth phase needs.
|
||
# ============================================================================
|
||
|
||
# the screen x of tile cell (x,y) for the map's orientation (before the camera).
|
||
# orthogonal: x*tilew. isometric: (x-y)*tilew/2. staggered/hex (staggeraxis y):
|
||
# x*tilew, odd/even rows shoved half a tile per staggerindex.
|
||
function tmap_cell_sx(m: Tmap, x: int, y: int) -> int {
|
||
let o = m.orientation
|
||
if o == "isometric" { return (x - y) * (m.tilew / 2) }
|
||
if o == "staggered" or o == "hexagonal" {
|
||
var sx = x * m.tilew
|
||
let idx = value_as_str(value_get(m.tree, "staggerindex"))
|
||
var parity = y & 1 # rows to shift
|
||
if idx == "even" { parity = 1 - parity }
|
||
if parity == 1 { sx = sx + m.tilew / 2 }
|
||
return sx
|
||
}
|
||
return x * m.tilew # orthogonal
|
||
}
|
||
|
||
# the screen y of tile cell (x,y). isometric: (x+y)*tileh/2. staggered:
|
||
# y*tileh/2. hexagonal (staggeraxis y): rows step by (tileh+hexsidelength)/2.
|
||
function tmap_cell_sy(m: Tmap, x: int, y: int) -> int {
|
||
let o = m.orientation
|
||
if o == "isometric" { return (x + y) * (m.tileh / 2) }
|
||
if o == "staggered" { return y * (m.tileh / 2) }
|
||
if o == "hexagonal" {
|
||
let hs = value_as_int(value_get(m.tree, "hexsidelength"))
|
||
return y * ((m.tileh + hs) / 2)
|
||
}
|
||
return y * m.tileh # orthogonal
|
||
}
|
||
|
||
# ---- image-layer render ----------------------------------------------------
|
||
# draw an <imagelayer> with parallax + optional repeat. `parallax` is 1.0 by
|
||
# default (moves with the camera); repeatx/repeaty tile the image across the view.
|
||
function tmap_draw_imagelayer(rt_core_st: mut RtCoreState, rt_image_st: RtImageState, m: Tmap, l: TmLayer, camx: int, camy: int) -> void {
|
||
if l.imgid < 0 { return }
|
||
let ox = value_as_int(value_get(l.data, "offsetx"))
|
||
let oy = value_as_int(value_get(l.data, "offsety"))
|
||
# parallax factor (fixed, default 1.0); px/py = the drawn origin
|
||
var px = ox - camx
|
||
var py = oy - camy
|
||
let iw = rt_image_st.img_w[l.imgid]
|
||
let ih = rt_image_st.img_h[l.imgid]
|
||
let repx = value_as_int(value_get(l.data, "repeatx"))
|
||
let repy = value_as_int(value_get(l.data, "repeaty"))
|
||
# starting origin: for a repeating axis, back up to before the screen
|
||
var sx0 = px
|
||
if repx != 0 { while sx0 > 0 { sx0 -= iw }; }
|
||
var sy0 = py
|
||
if repy != 0 { while sy0 > 0 { sy0 -= ih }; }
|
||
var yy = sy0
|
||
var first_y = 1
|
||
while (yy < rt_screen_h()) and (first_y == 1 or repy != 0) {
|
||
var xx = sx0
|
||
var first_x = 1
|
||
while (xx < rt_screen_w()) and (first_x == 1 or repx != 0) {
|
||
rt_draw_image(rt_core_st, rt_image_st, l.imgid, xx, yy)
|
||
first_x = 0
|
||
if repx == 0 { xx = rt_screen_w() } else { xx += iw }
|
||
}
|
||
first_y = 0
|
||
if repy == 0 { yy = rt_screen_h() } else { yy += ih }
|
||
}
|
||
}
|
||
|
||
# ---- image/group-layer accessors -------------------------------------------
|
||
function tmap_layer_kind(m: Tmap, layer: int) -> int {
|
||
if layer < 0 or layer >= len(m.layers) { return -1 }
|
||
return m.layers[layer].kind
|
||
}
|
||
function tmap_layer_opacity(m: Tmap, layer: int) -> string {
|
||
if layer < 0 or layer >= len(m.layers) { return "" }
|
||
let op = value_get(m.layers[layer].data, "opacity")
|
||
if value_kind(op) == 0 { return "1" }
|
||
return value_as_str(op)
|
||
}
|
||
function tmap_layer_tint(m: Tmap, layer: int) -> string {
|
||
if layer < 0 or layer >= len(m.layers) { return "" }
|
||
return value_as_str(value_get(m.layers[layer].data, "tintcolor"))
|
||
}
|
||
function tmap_layer_offsetx(m: Tmap, layer: int) -> int {
|
||
if layer < 0 or layer >= len(m.layers) { return 0 }
|
||
return value_as_int(value_get(m.layers[layer].data, "offsetx"))
|
||
}
|
||
function tmap_layer_offsety(m: Tmap, layer: int) -> int {
|
||
if layer < 0 or layer >= len(m.layers) { return 0 }
|
||
return value_as_int(value_get(m.layers[layer].data, "offsety"))
|
||
}
|