From b8c71d2a8e451ad7999c7cf65016acdb744c049d Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 1 Sep 2026 15:08:06 +0300 Subject: [PATCH] =?UTF-8?q?feat(stdlib):=20Tiled=20P4=20=E2=80=94=20object?= =?UTF-8?q?s,=20custom=20props/types,=20templates,=20opt-in=20spawn=20(#72?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Object layers: all shapes (rectangle/ellipse/point/polygon/polyline/text) and custom properties parse and are queryable — Tiled.object_count / Tiled.object / Tiled.object_shape, and Tiled.prop / prop_int / prop_type over any object / tile / layer / map. - Custom types: Tiled.load_types reads an objecttypes.xml project custom-type table so a class property resolves its default; enum values resolve as strings. - Templates: Tiled.template reads a .tx/.tj, and Tiled.load merges each object's `template` reference under the instance's overrides (field inheritance). - Opt-in spawning: Tiled.spawn / Tiled.spawn_layer map an object's class + properties onto Ludic components through the reflection ABI (Position from x/y, each property to a like-named field). Off by default — no gameplay coupling in the core load. Split into tiled_spawn.ludic, spliced only for a Tiled *game* (the world table exists only under has_ecs), so a plain map-reading tool never links against the reflection ABI. Proven by library/tiled_p4.ludic (18 assertions) incl. the design's named orthogonal-outside.tmx object shapes. x test: 95 passed. Co-Authored-By: Claude Opus 4.8 --- assets/tiled-fixtures/p4_enemy.tx | 8 + assets/tiled-fixtures/p4_map.tmx | 19 + assets/tiled-fixtures/p4_types.xml | 8 + changes/tiled-maps.md | 1 + docs/language/tiled/tiled-load_types.md | 14 + docs/language/tiled/tiled-object.md | 14 + docs/language/tiled/tiled-object_count.md | 14 + docs/language/tiled/tiled-object_shape.md | 14 + docs/language/tiled/tiled-prop.md | 14 + docs/language/tiled/tiled-prop_int.md | 14 + docs/language/tiled/tiled-prop_type.md | 14 + docs/language/tiled/tiled-spawn.md | 14 + docs/language/tiled/tiled-spawn_layer.md | 14 + docs/language/tiled/tiled-template.md | 14 + examples/library/tiled_p4.ludic | 78 + runtime/native/tiled.ludic | 193 + runtime/native/tiled_spawn.ludic | 55 + selfhost/backend/emit_call.ludic | 11 + selfhost/frontend/parse.ludic | 5 + selfhost/ludicc.seed.ll | 34828 ++++++++++---------- tools/docgen/inventory.json | 12 +- tools/x/test.ludic | 1 + 22 files changed, 18278 insertions(+), 17081 deletions(-) create mode 100644 assets/tiled-fixtures/p4_enemy.tx create mode 100644 assets/tiled-fixtures/p4_map.tmx create mode 100644 assets/tiled-fixtures/p4_types.xml create mode 100644 docs/language/tiled/tiled-load_types.md create mode 100644 docs/language/tiled/tiled-object.md create mode 100644 docs/language/tiled/tiled-object_count.md create mode 100644 docs/language/tiled/tiled-object_shape.md create mode 100644 docs/language/tiled/tiled-prop.md create mode 100644 docs/language/tiled/tiled-prop_int.md create mode 100644 docs/language/tiled/tiled-prop_type.md create mode 100644 docs/language/tiled/tiled-spawn.md create mode 100644 docs/language/tiled/tiled-spawn_layer.md create mode 100644 docs/language/tiled/tiled-template.md create mode 100644 examples/library/tiled_p4.ludic create mode 100644 runtime/native/tiled_spawn.ludic diff --git a/assets/tiled-fixtures/p4_enemy.tx b/assets/tiled-fixtures/p4_enemy.tx new file mode 100644 index 00000000..d8f39a8e --- /dev/null +++ b/assets/tiled-fixtures/p4_enemy.tx @@ -0,0 +1,8 @@ + + diff --git a/assets/tiled-fixtures/p4_map.tmx b/assets/tiled-fixtures/p4_map.tmx new file mode 100644 index 00000000..e963f586 --- /dev/null +++ b/assets/tiled-fixtures/p4_map.tmx @@ -0,0 +1,19 @@ + + + + + + + + + + Hello + + + + + + + + + diff --git a/assets/tiled-fixtures/p4_types.xml b/assets/tiled-fixtures/p4_types.xml new file mode 100644 index 00000000..57a877e9 --- /dev/null +++ b/assets/tiled-fixtures/p4_types.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/changes/tiled-maps.md b/changes/tiled-maps.md index ff0a1c57..9602c17d 100644 --- a/changes/tiled-maps.md +++ b/changes/tiled-maps.md @@ -8,3 +8,4 @@ type: feat - **P1 core load + render (#69)** — the runtime `rt_tmap` model (heap-allocated to `w·h`, lifting the old `96×64` cap), the GID resolver (`Tiled.resolve` → tileset / local id / H·V·D flips), image-backed rendering (`Tiled.draw`, flips applied at blit), and the legacy-tilemap compatibility projection so `Grid.*`/`Path.*`/`esys_move` keep working. `Tiled.load` reads either format, resolves external tilesets + images, and auto-projects a `collision` layer. The Kenney sample loads and renders pixel-identically from `.tmx` and `.tmj`; the `grid` and `physics_tiles` demos now run off a loaded map. - **P2 collision & grid (#70)** — normalise three collision sources into the byte tilemap `esys_move`/`Grid.*`/`Path.*` read, in the design's priority order: per-tile `` hitboxes, the `solid`/`oneway`/`trigger` property convention (`Tiled.collision_kind`/`Tiled.tile_shapes`), and the designated collision layer (`Tiled.project`, any non-zero GID solid) — or drive collision from a visual layer's per-tile metadata alone (`Tiled.collide`). The property convention and the collision-layer fallback produce the same feed; `Path.a_star` over a loaded map matches the hand-authored baseline. - **P3 animated tiles + tile objects (#71)** — a tileset `` advances deterministically as a pure function of the fixed 60/s engine frame clock (`Tiled.frame_gid`/`Tiled.animated`), so an animated GID resolves at draw to the current frame's GID with its flip flags preserved and reproduces frame-for-frame across runs; `Tiled.draw_anim` draws a map with animations advanced. Object-layer entries with a `gid` render the tile image (with their own flips), bottom-anchored, as placeable sprites. +- **P4 objects, properties, templates, spawning (#72)** — all object shapes (rectangle / ellipse / point / polygon / polyline / text) and custom properties parse and are queryable (`Tiled.object`, `Tiled.object_shape`, `Tiled.prop`/`Tiled.prop_int`/`Tiled.prop_type`); class properties resolve their defaults against a project custom-type table (`Tiled.load_types` over `objecttypes.xml`); template instances inherit their `.tx`/`.tj` template's fields; and an object maps onto Ludic components on demand (`Tiled.spawn`/`Tiled.spawn_layer`, off by default, via the reflection ABI). diff --git a/docs/language/tiled/tiled-load_types.md b/docs/language/tiled/tiled-load_types.md new file mode 100644 index 00000000..552da742 --- /dev/null +++ b/docs/language/tiled/tiled-load_types.md @@ -0,0 +1,14 @@ +--- +id: tiled-load_types +name: Tiled.load_types +category: tiled +kind: namespace-method +tokens: Tiled.load_types +sig: Tiled.load_types(path: string) -> int +tip: Load a project custom-type table. +order: 26 +ns: Tiled +member: load_types +--- + +Loads an objecttypes.xml custom-type table so class/enum properties resolve their defaults; returns the number of types loaded. Each <objecttype> maps to its default <property> list. diff --git a/docs/language/tiled/tiled-object.md b/docs/language/tiled/tiled-object.md new file mode 100644 index 00000000..de23f667 --- /dev/null +++ b/docs/language/tiled/tiled-object.md @@ -0,0 +1,14 @@ +--- +id: tiled-object +name: Tiled.object +category: tiled +kind: namespace-method +tokens: Tiled.object +sig: Tiled.object(map, layer: int, index: int) -> value +tip: An object on an object layer by index. +order: 21 +ns: Tiled +member: object +--- + +Returns the object Value at index on object layer — its x/y/width/height, shape, gid, type (class) and properties. diff --git a/docs/language/tiled/tiled-object_count.md b/docs/language/tiled/tiled-object_count.md new file mode 100644 index 00000000..3010dfee --- /dev/null +++ b/docs/language/tiled/tiled-object_count.md @@ -0,0 +1,14 @@ +--- +id: tiled-object_count +name: Tiled.object_count +category: tiled +kind: namespace-method +tokens: Tiled.object_count +sig: Tiled.object_count(map, layer: int) -> int +tip: How many objects an object layer holds. +order: 20 +ns: Tiled +member: object_count +--- + +Returns the number of objects on object layer (0 for a non-object layer). diff --git a/docs/language/tiled/tiled-object_shape.md b/docs/language/tiled/tiled-object_shape.md new file mode 100644 index 00000000..2ec21d28 --- /dev/null +++ b/docs/language/tiled/tiled-object_shape.md @@ -0,0 +1,14 @@ +--- +id: tiled-object_shape +name: Tiled.object_shape +category: tiled +kind: namespace-method +tokens: Tiled.object_shape +sig: Tiled.object_shape(object) -> string +tip: Classify an object's shape. +order: 22 +ns: Tiled +member: object_shape +--- + +Returns an object's shape: tile (a gid object), point, ellipse, polygon, polyline, text, or rectangle (the default). diff --git a/docs/language/tiled/tiled-prop.md b/docs/language/tiled/tiled-prop.md new file mode 100644 index 00000000..c6b3148d --- /dev/null +++ b/docs/language/tiled/tiled-prop.md @@ -0,0 +1,14 @@ +--- +id: tiled-prop +name: Tiled.prop +category: tiled +kind: namespace-method +tokens: Tiled.prop +sig: Tiled.prop(container, name: string) -> string +tip: A custom property's value, with class-default fallback. +order: 23 +ns: Tiled +member: prop +--- + +Returns the string value of custom property name on container (an object / tile / layer / map), falling back to the container class's default from the project custom-type table (Tiled.load_types). "" when absent everywhere. diff --git a/docs/language/tiled/tiled-prop_int.md b/docs/language/tiled/tiled-prop_int.md new file mode 100644 index 00000000..46a6b7b8 --- /dev/null +++ b/docs/language/tiled/tiled-prop_int.md @@ -0,0 +1,14 @@ +--- +id: tiled-prop_int +name: Tiled.prop_int +category: tiled +kind: namespace-method +tokens: Tiled.prop_int +sig: Tiled.prop_int(container, name: string) -> int +tip: A custom property parsed as an integer. +order: 24 +ns: Tiled +member: prop_int +--- + +Like Tiled.prop but parsed as an integer (true/false → 1/0), with the same class-default fallback. diff --git a/docs/language/tiled/tiled-prop_type.md b/docs/language/tiled/tiled-prop_type.md new file mode 100644 index 00000000..0b65db82 --- /dev/null +++ b/docs/language/tiled/tiled-prop_type.md @@ -0,0 +1,14 @@ +--- +id: tiled-prop_type +name: Tiled.prop_type +category: tiled +kind: namespace-method +tokens: Tiled.prop_type +sig: Tiled.prop_type(container, name: string) -> string +tip: A custom property's declared type. +order: 25 +ns: Tiled +member: prop_type +--- + +Returns the declared type of custom property name (string/int/float/bool/color/file/object/class/an enum), with the class-default fallback. diff --git a/docs/language/tiled/tiled-spawn.md b/docs/language/tiled/tiled-spawn.md new file mode 100644 index 00000000..7d57f55a --- /dev/null +++ b/docs/language/tiled/tiled-spawn.md @@ -0,0 +1,14 @@ +--- +id: tiled-spawn +name: Tiled.spawn +category: tiled +kind: namespace-method +tokens: Tiled.spawn +sig: Tiled.spawn(map, object, model: string) -> int +tip: Spawn a Ludic entity from an object. +order: 28 +ns: Tiled +member: spawn +--- + +Opt-in: spawns an entity of model from an object — its Position (if the model has one) takes the object's x/y, and each object custom property sets a like-named int/bool component field through the reflection ABI. Returns the entity, or -1 if the model is unknown. The core load never calls this. diff --git a/docs/language/tiled/tiled-spawn_layer.md b/docs/language/tiled/tiled-spawn_layer.md new file mode 100644 index 00000000..2f952b77 --- /dev/null +++ b/docs/language/tiled/tiled-spawn_layer.md @@ -0,0 +1,14 @@ +--- +id: tiled-spawn_layer +name: Tiled.spawn_layer +category: tiled +kind: namespace-method +tokens: Tiled.spawn_layer +sig: Tiled.spawn_layer(map, layer: int, model: string) -> int +tip: Spawn every object on a layer. +order: 29 +ns: Tiled +member: spawn_layer +--- + +Spawns every object on object layer as model (see Tiled.spawn); returns how many were spawned. diff --git a/docs/language/tiled/tiled-template.md b/docs/language/tiled/tiled-template.md new file mode 100644 index 00000000..8d8b9974 --- /dev/null +++ b/docs/language/tiled/tiled-template.md @@ -0,0 +1,14 @@ +--- +id: tiled-template +name: Tiled.template +category: tiled +kind: namespace-method +tokens: Tiled.template +sig: Tiled.template(path: string) -> value +tip: Read a template file into an object value. +order: 27 +ns: Tiled +member: template +--- + +Reads a .tx/.tj template file and returns its object Value — the reusable object definition an instance inherits. Tiled.load resolves each object's template reference automatically, merging the template's fields under the instance's overrides. diff --git a/examples/library/tiled_p4.ludic b/examples/library/tiled_p4.ludic new file mode 100644 index 00000000..8ccc4de6 --- /dev/null +++ b/examples/library/tiled_p4.ludic @@ -0,0 +1,78 @@ +# tiled_p4.ludic — Tiled P4 objects, custom properties/types, templates, opt-in +# spawning (issue #72). All object shapes (rect/ellipse/point/polygon/polyline/ +# text) parse and are queryable; class properties resolve their defaults against +# the project custom-type table (objecttypes.xml); a template instance inherits +# its template's fields; and an object maps onto Ludic components on demand. +# +# A full run prints: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 +program TiledP4 { + property Position { x: int = 0, y: int = 0 } + property Health { hp: int = 0 } + model Enemy { Position, Health } + + handler Boot phase Start { + let m = Tiled.load("assets/tiled-fixtures/p4_map.tmx") + Tiled.load_types("assets/tiled-fixtures/p4_types.xml") + + # --- all object shapes parse and are queryable (layer 0 "shapes") --- + if Tiled.object_count(m, 0) == 6 { print(1) } + if Tiled.object_shape(Tiled.object(m, 0, 0)) == "rectangle" { print(2) } + if Tiled.object_shape(Tiled.object(m, 0, 1)) == "ellipse" { print(3) } + if Tiled.object_shape(Tiled.object(m, 0, 2)) == "point" { print(4) } + if Tiled.object_shape(Tiled.object(m, 0, 3)) == "polygon" { print(5) } + if Tiled.object_shape(Tiled.object(m, 0, 4)) == "polyline" { print(6) } + if Tiled.object_shape(Tiled.object(m, 0, 5)) == "text" { print(7) } + # polygon points + text content, via the value tree + let poly = Tiled.object(m, 0, 3) + if Value.count(Value.get(poly, "polygon")) == 4 { print(8) } + let txt = Tiled.object(m, 0, 5) + if Value.as_str(Value.get(Value.get(txt, "text"), "text")) == "Hello" { print(9) } + + # --- custom properties + class defaults (layer 1 "spawns") --- + let e0 = Tiled.object(m, 1, 0) # id 10, hp override = 99 + if Tiled.prop_int(e0, "hp") == 99 { print(10) } + let e1 = Tiled.object(m, 1, 1) # id 11, no hp -> Enemy default 10 + if Tiled.prop_int(e1, "hp") == 10 { print(11) } + if Tiled.prop_int(e1, "speed") == 3 { print(12) } # class default + if Tiled.prop_type(e1, "hp") == "int" { print(13) } + + # --- template instance inherits its template's fields (id 12) --- + let e2 = Tiled.object(m, 1, 2) + if Value.as_str(Value.get(e2, "type")) == "Enemy" { print(14) } # type inherited from p4_enemy.tx + if Tiled.prop_int(e2, "hp") == 25 { print(15) } # hp inherited from p4_enemy.tx + + # --- opt-in spawn maps an object's class/properties -> components --- + let ent = Tiled.spawn(m, e0, "Enemy") + let pp = World.prop_id("Position") + let ph = World.prop_id("Health") + let ok_pos = World.get(ent, pp, World.field_id(pp, "x")) == 32 + let ok_hp = World.get(ent, ph, World.field_id(ph, "hp")) == 99 + if ok_pos and ok_hp { print(16) } + + # --- the design's named fixture: orthogonal-outside.tmx object shapes --- + let og = Tiled.load("assets/tiled-fixtures/orthogonal-outside.tmx") + var olayer = 0 - 1 + var oli = 0 + while oli < Tiled.layer_count(og) { + if Tiled.object_count(og, oli) > 0 { olayer = oli } + oli = oli + 1 + } + var nell = 0 + var npt = 0 + var npg = 0 + var npl = 0 + var oi = 0 + while oi < Tiled.object_count(og, olayer) { + let sh = Tiled.object_shape(Tiled.object(og, olayer, oi)) + if sh == "ellipse" { nell = nell + 1 } + if sh == "point" { npt = npt + 1 } + if sh == "polygon" { npg = npg + 1 } + if sh == "polyline" { npl = npl + 1 } + oi = oi + 1 + } + if nell == 1 and npt == 1 and npg == 1 { print(17) } + if npl == 2 { print(18) } + } + + handler Run phase Update { quit() } +} diff --git a/runtime/native/tiled.ludic b/runtime/native/tiled.ludic index fb586337..3eca6191 100644 --- a/runtime/native/tiled.ludic +++ b/runtime/native/tiled.ludic @@ -106,6 +106,7 @@ function tmx_object_to_value(ob: Xml) -> Val { 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))) @@ -807,6 +808,7 @@ function tmap_add_layer(m: Tmap, lv: Val) -> void { 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 @@ -916,3 +918,194 @@ function tmap_draw(m: Tmap, camx: int, camy: int) -> void { tmap_draw_full(m, ca # 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) #