# ============================================================================ # 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 >= 48 and c <= 57 { cur = cur * 10 + (c - 48); have = 1 } else { if c == 44 or c == 32 or c == 9 or c == 10 or c == 13 { if have == 1 { push(out.kids, value_int(cur)); cur = 0; have = 0 } } } i = 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(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(comp, clen, d, outcap); raw = d; rawlen = dn } else { if compression == "gzip" { let d = bytes(outcap); let dn = z_gunzip(comp, clen, d, outcap); raw = d; rawlen = dn } else { if compression == "zstd" { let d = bytes(outcap); let dn = z_zstd(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 = i + 4 } return out } # decode the text of `node` under encoding `enc` / compression `comp` -> GID list. function tiled_decode_enc(node: Xml, enc: pointer, comp: pointer, count: int) -> Val { if enc == "base64" { return tiled_b64_list(xml_text(node), comp, count) } return tiled_csv_list(xml_text(node)) # csv (or the tag-per-tile form) } # decode a `` element (child of a ``) into a Value list of GIDs. function tiled_data_list(data: Xml, count: int) -> Val { return tiled_decode_enc(data, xml_attr(data, "encoding"), xml_attr(data, "compression"), count) } # flatten a chunked (infinite-map) `` — its `` # children — into a dense GID list; sets `o`'s width/height/data (#74). function tiled_chunked_layer(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 = 0 - 1000000000 var maxy = 0 - 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 = 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 = 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(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 = xx + 1 } yy = yy + 1 } } i = i + 1 } value_put(o, "width", value_int(W)) value_put(o, "height", value_int(H)) value_put(o, "data", gids) } # ---- custom 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 = i + 1 } return out } # ---- objects (shapes) ------------------------------------------------------ # one `` -> 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] == 32 or s[i] == 9) { i = i + 1 } if i >= n { break } # read "x,y" var xv = 0; var xn = 0; var xs = 0 if s[i] == 45 { xs = 1; i = i + 1 } while i < n and s[i] >= 48 and s[i] <= 57 { xv = xv * 10 + (s[i] - 48); xn = 1; i = i + 1 } if xs == 1 { xv = 0 - xv } if i < n and s[i] == 44 { i = i + 1 } var yv = 0; var ys = 0 if i < n and s[i] == 45 { ys = 1; i = i + 1 } while i < n and s[i] >= 48 and s[i] <= 57 { yv = yv * 10 + (s[i] - 48); i = i + 1 } if ys == 1 { yv = 0 - 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 `` 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 = 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 = 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 `` 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 = i + 1 } if len(tiles.kids) > 0 { value_put(o, "tiles", tiles) } } # a `` child of a `` -> 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(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(data, o) } else { value_put(o, "width", value_int(w)) value_put(o, "height", value_int(h)) value_put(o, "data", tiled_data_list(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 = i + 1 } value_put(o, "objects", objs) return o } # ---- map ------------------------------------------------------------------- # a `` root -> the intermediate map Value tree (Tiled JSON schema). function tmx_to_value(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(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(ch, mapw, maph)) } } } } } i = 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 } # `` -> 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 } # `` -> a Value object carrying its nested layers (P5 renders recursively). function tmx_group_to_value(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(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(ch, mapw, maph)) } } } } i = 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(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(value_at(ls, i)); 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(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(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(chunk: Val, enc: pointer, comp: pointer, count: int) -> Val { let d = value_get(chunk, "data") if value_kind(d) == 4 { return tiled_b64_list(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(layer: Val, chunks: Val, enc: pointer, comp: pointer) -> void { var minx = 1000000000 var miny = 1000000000 var maxx = 0 - 1000000000 var maxy = 0 - 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 = 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 = 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(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 = xx + 1 } yy = yy + 1 } i = i + 1 } value_put(layer, "width", value_int(W)) value_put(layer, "height", value_int(H)) value_put(layer, "data", gids) } function tmj_normalize(m: Val) -> Val { let ls = value_get(m, "layers") var i = 0 while i < value_count(ls) { tmj_normalize_layer(value_at(ls, i)); 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 == 32 or c == 9 or c == 10 or c == 13 { i = 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(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(xml_parse(text)) } return tmj_normalize(json_parse(text)) # '{' -> JSON } # read a tileset file (TSX or TSJ) -> a tileset Value object. function tiled_read_tsx(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(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 = 0 - 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 = 0 - 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 = 0 - 1 # designated collision layer index, or -1 } # ---- GID resolver ---------------------------------------------------------- property GidInfo { tileset: int = 0 - 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 = 0 - 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 = 0 - 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 = 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) -> pointer { 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 = 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 = j + 1 } return 0 } # does the tile a GID resolves to carry a per-tile 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 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(m: Tmap, layer: int, whole_layer_solid: int) -> void { m.coll = layer rt_map_size(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_map[y * 96 + x] = 35 } # '#' if k == 2 { rt_map[y * 96 + x] = 61 } # '=' } } x = x + 1 } } y = 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(m: Tmap, layer: int) -> void { tmap_project_layer(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(m: Tmap, layer: int) -> void { tmap_project_layer(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 = i + 1 } return 0 - 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(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 = img_px[imgid] let iw = 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(dx + i, dy + j, argb) i = i + 1 } j = j + 1 } } # draw one tile GID at map cell (x,y) with the camera offset already applied. function tmap_draw_gid(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(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 = 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) -> pointer { 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(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(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(tiled_join(imgdir, img)) } push(m.tilesets, ts) i = 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 = i + 1 } # image layers (#73): load each 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(tiled_join(basedir, img)) } } i = 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 = 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 = 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(path: pointer) -> Tmap { let tree = tiled_read(path) let m = tmap_build(tree, Path.dir(path)) tmap_resolve_templates(m, Path.dir(path)) # #72: fill template-instance objects let c = tmap_find_collision(m) if c >= 0 { tmap_project(m, c) } return m } # ============================================================================ # P3 (#71) — animated tiles + tile objects. # # Animated tiles: a tileset's 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 # 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 = 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 = 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 )? 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(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(m, gid, tmap_cell_sx(m, x, y) + lox - camx, tmap_cell_sy(m, x, y) + loy - camy) } x = x + 1 } y = y + 1 } } if l.kind == 2 and l.visible != 0 { # image layer (parallax / repeat) tmap_draw_imagelayer(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(m, g, ox - camx, oy - m.tileh - camy) } oi = oi + 1 } } li = li + 1 } } # static draw (no animation) — the P1 entry point. function tmap_draw(m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(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(m: Tmap, camx: int, camy: int, frame: int) -> void { tmap_draw_full(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) -> pointer { 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 = 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(container: Val, name: pointer) -> pointer { 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(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(container: Val, name: pointer) -> int { let s = tiled_prop_str(container, name) if s == "true" { return 1 } if s == "false" { return 0 } return xml_atoi(s) } function tiled_prop_type(container: Val, name: pointer) -> pointer { 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(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. var tiled_type_table: Val = null function tiled_types() -> Val { if tiled_type_table == null { tiled_type_table = value_object() } return tiled_type_table } # load an objecttypes.xml file into the type table. Each maps # to its list. function tiled_load_types(path: pointer) -> int { let text = Fs.read_text(path) if text == null { return 0 } if text == "" { return 0 } let root = xml_parse(text) let tbl = tiled_types() 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 = j + 1 } value_put(tbl, xml_attr(ot, "name"), props) n = n + 1 } i = i + 1 } return n } # the default {name,type,value} property node for a custom type, or a null node. function tiled_type_default(typename: pointer, propname: pointer) -> Val { let tbl = tiled_types() 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 = i + 1 } return value_null() } # ---- templates (.tx / .tj) ------------------------------------------------- # read a template file -> its object Value (the a .tx wraps, or the # "object" of a .tj). External reusable object definitions. function tiled_read_template(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(text) #