feat(engine): Tiled P1 — rt_tmap model + GID resolver + render + projection (#69)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 20s
ci / build-and-test (push) Successful in 1m48s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 27s

The heart of Tiled support: load a map and draw it.

- rt_tmap model (Tmap/TmLayer/TmTileset in tiled.ludic): map header + ordered
  layers (dense int32 GID arrays, heap-allocated to w*h, lifting the 96x64 cap)
  + tilesets. Built from the intermediate Value tree, so the TMX and TMJ paths
  both feed it.
- GID resolver (Tiled.resolve): gid -> (tileset, localId, flipH/V/D); the three
  flip flags masked off before the local-id lookup, returned alongside. gid==0
  is empty.
- Image-backed render (Tiled.draw): every visible tile layer in file order,
  blitting each tile from its tileset image with flips applied at draw.
- Compatibility projection (Tiled.project / auto on load): a designated
  collision layer projects to the legacy byte tilemap ('#' solid, '=' one-way
  via the oneway property, ' ' empty) so Grid.*/Path.*/esys_move are unchanged.
- Tiled.load resolves external tilesets + images relative to the map file and
  auto-projects a collision/solids/walls layer.
- The grid and physics_tiles demos now run off a loaded map (grid_maze.tmx /
  physics_map.tmx) instead of hand-authored Map.row strings, byte-identically.

Two compiler fixes fell out of this (see the changeset):
- emit_index_addr set g_addr_ty before evaluating the index, so slice[obj.field]
  came back mis-typed; set it last, like the raw-pointer branches.
- @strcmp was declared by both the world table and the fs prelude; centralise
  it in the head prelude so a game that uses Fs/Path links.

Proven by library/tiled_p1.ludic (14 assertions: loads+renders Kenney map
identically from .tmx and .tmj, flip mirroring) + the converted grid/
physics_tiles demos. x test: 92 passed; self-host bootstrap fixpoint intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-01 14:43:29 +03:00
parent 071c268de7
commit bc301c8d17
29 changed files with 12932 additions and 9582 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Hand-authored collision tileset for the Ludic Tiled demos (issue #69). CC0. -->
<tileset version="1.10" tiledversion="1.10.2" name="collision" tilewidth="16" tileheight="16" tilecount="2" columns="1">
<tile id="1">
<properties>
<property name="oneway" type="bool" value="true"/>
</properties>
</tile>
</tileset>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Hand-authored map for the Ludic Tiled demos (issue #69). CC0. -->
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="8" height="5" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="collision.tsx"/>
<layer id="1" name="collision" width="8" height="5">
<data encoding="csv">
1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,1,
1,0,1,1,1,1,0,1,
1,0,0,0,0,1,0,1,
1,1,1,1,1,1,1,1
</data>
</layer>
</map>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Hand-authored map for the Ludic Tiled demos (issue #69). CC0. -->
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="16" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="collision.tsx"/>
<layer id="1" name="collision" width="16" height="10">
<data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
</map>

View file

@ -0,0 +1,4 @@
bump: patch
type: fix
**Correct the element type of a slice indexed by a member-access expression.** `emit_index_addr` set the global `g_addr_ty` to the slice's element type *before* evaluating the index expression, so an index that was itself a struct-field access (`slice[obj.field]`) overwrote it — the load then came back typed as the field, and a following field access failed with "member access on non-aggregate". The slice branch now sets `g_addr_ty` last, matching the raw-pointer branches. Also de-duplicate the `@strcmp` declaration (centralised in the head prelude) so a program that pulls in both the world table and the filesystem prelude links.

View file

@ -5,3 +5,4 @@ type: feat
- **P0 parsing primitives (#67)** — a minimal pure-Ludic XML reader (`Xml.*`) for the element/attribute/CDATA subset TMX/TSX/TX use; standard base64 decode/encode (`Base64.*`, RFC 4648), whose decoder ignores the whitespace Tiled wraps into `<data>`; and gzip framing (`z_gunzip`, RFC 1952) wrapping the existing DEFLATE inflater. zlib and the JSON reader already shipped. A curated, attributed golden corpus lands under `assets/tiled-fixtures/`. - **P0 parsing primitives (#67)** — a minimal pure-Ludic XML reader (`Xml.*`) for the element/attribute/CDATA subset TMX/TSX/TX use; standard base64 decode/encode (`Base64.*`, RFC 4648), whose decoder ignores the whitespace Tiled wraps into `<data>`; and gzip framing (`z_gunzip`, RFC 1952) wrapping the existing DEFLATE inflater. zlib and the JSON reader already shipped. A curated, attributed golden corpus lands under `assets/tiled-fixtures/`.
- **P0.5 TMX/TSX reader (#68)** — `Tiled.read` / `Tiled.read_tsx` map the native XML formats onto the *same* intermediate the JSON path produces — a `Value` tree in Tiled's JSON schema, with every tile layer's data decoded to a dense GID list (CSV, base64, base64+zlib, base64+gzip). A CSV `.tmx` and a base64+zlib `.tmj` of the same map read structurally identically; the in-repo Kenney `sampleMap.tmx` + external `sampleSheet.tsx` load with no manual JSON re-export. - **P0.5 TMX/TSX reader (#68)** — `Tiled.read` / `Tiled.read_tsx` map the native XML formats onto the *same* intermediate the JSON path produces — a `Value` tree in Tiled's JSON schema, with every tile layer's data decoded to a dense GID list (CSV, base64, base64+zlib, base64+gzip). A CSV `.tmx` and a base64+zlib `.tmj` of the same map read structurally identically; the in-repo Kenney `sampleMap.tmx` + external `sampleSheet.tsx` load with no manual JSON re-export.
- **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.

View file

@ -0,0 +1,14 @@
---
id: tiled-draw
name: Tiled.draw
category: tiled
kind: namespace-method
tokens: Tiled.draw
sig: Tiled.draw(map, camx: int, camy: int) -> void
tip: Draw every visible tile layer.
order: 10
ns: Tiled
member: draw
---
Draws every visible tile layer in file order to the framebuffer, offset by the camera <code>(camx, camy)</code>, blitting each tile from its tileset image and applying the GID's flip flags at draw time.

View file

@ -0,0 +1,14 @@
---
id: tiled-gid
name: Tiled.gid
category: tiled
kind: namespace-method
tokens: Tiled.gid
sig: Tiled.gid(map, layer: int, x: int, y: int) -> int
tip: The raw GID at a cell in a tile layer.
order: 4
ns: Tiled
member: gid
---
Returns the raw global tile id (with its flip flags still set) at cell <code>(x,y)</code> in tile <code>layer</code>, or <code>0</code> (empty) out of bounds or for a non-tile layer.

View file

@ -0,0 +1,14 @@
---
id: tiled-height
name: Tiled.height
category: tiled
kind: namespace-method
tokens: Tiled.height
sig: Tiled.height(map) -> int
tip: The map height in tiles.
order: 7
ns: Tiled
member: height
---
Returns the map's height in tiles.

View file

@ -0,0 +1,14 @@
---
id: tiled-layer_count
name: Tiled.layer_count
category: tiled
kind: namespace-method
tokens: Tiled.layer_count
sig: Tiled.layer_count(map) -> int
tip: How many layers the map has.
order: 8
ns: Tiled
member: layer_count
---
Returns the number of layers (tile, object, image and group layers are all counted; a group's children are flattened in after it).

View file

@ -0,0 +1,14 @@
---
id: tiled-layer_name
name: Tiled.layer_name
category: tiled
kind: namespace-method
tokens: Tiled.layer_name
sig: Tiled.layer_name(map, index: int) -> string
tip: A layer's name by index.
order: 9
ns: Tiled
member: layer_name
---
Returns the name of the layer at <code>index</code>, or <code>""</code> if the index is out of range.

View file

@ -0,0 +1,14 @@
---
id: tiled-load
name: Tiled.load
category: tiled
kind: namespace-method
tokens: Tiled.load
sig: Tiled.load(path: string) -> map
tip: Load a map file into the runtime map model.
order: 3
ns: Tiled
member: load
---
Reads a map file (TMX or TMJ), builds the runtime <code>rt_tmap</code> model — resolving external tilesets and loading their images relative to the map file — and projects the designated collision layer (a tile layer named <code>collision</code>/<code>solids</code>/<code>walls</code>) down to the legacy byte tilemap so <a href="grid"><code>Grid</code></a>/<a href="path"><code>Path</code></a> and <code>esys_move</code> keep working unchanged. Returns the loaded map.

View file

@ -0,0 +1,14 @@
---
id: tiled-project
name: Tiled.project
category: tiled
kind: namespace-method
tokens: Tiled.project
sig: Tiled.project(map, layer: int) -> void
tip: Project a collision layer to the byte tilemap.
order: 11
ns: Tiled
member: project
---
Designates tile <code>layer</code> as the collision layer and projects it onto the legacy byte tilemap: an empty cell is a space (passable), a GID whose tile carries the bool <code>oneway</code> property is <code>'='</code>, any other non-zero GID is <code>'#'</code> (solid). <a href="tiled-load"><code>Tiled.load</code></a> calls this automatically for a layer named <code>collision</code>/<code>solids</code>/<code>walls</code>.

View file

@ -0,0 +1,14 @@
---
id: tiled-resolve
name: Tiled.resolve
category: tiled
kind: namespace-method
tokens: Tiled.resolve
sig: Tiled.resolve(map, gid: int) -> gidinfo
tip: Decode a GID into tileset, local id and flip flags.
order: 5
ns: Tiled
member: resolve
---
Decomposes a raw GID: strips the three flip flags (<code>0x80000000</code> horizontal, <code>0x40000000</code> vertical, <code>0x20000000</code> anti-diagonal), keeps the low 29 bits as the global tile id, and finds the owning tileset. Returns a value with fields <code>tileset</code> (index, -1 if none), <code>local</code> (id within that tileset), <code>fh</code>/<code>fv</code>/<code>fd</code> (the flip flags) and <code>empty</code> (1 when the GID was 0).

View file

@ -0,0 +1,14 @@
---
id: tiled-tile_prop
name: Tiled.tile_prop
category: tiled
kind: namespace-method
tokens: Tiled.tile_prop
sig: Tiled.tile_prop(map, gid: int, name: string) -> int
tip: Whether a GID's tile carries a bool property.
order: 13
ns: Tiled
member: tile_prop
---
Returns <code>1</code> if the tile the GID resolves to carries a custom bool property <code>name</code> set to true, else <code>0</code> — the property convention (<code>solid</code>/<code>oneway</code>/<code>trigger</code>) the collision projection reads.

View file

@ -0,0 +1,14 @@
---
id: tiled-tree
name: Tiled.tree
category: tiled
kind: namespace-method
tokens: Tiled.tree
sig: Tiled.tree(map) -> value
tip: The underlying intermediate value tree.
order: 12
ns: Tiled
member: tree
---
Returns the intermediate <a href="value"><code>Value</code></a> tree the map was built from — the full Tiled JSON schema (layers, objects, tilesets, custom properties), for queries the typed accessors don't cover.

View file

@ -0,0 +1,14 @@
---
id: tiled-width
name: Tiled.width
category: tiled
kind: namespace-method
tokens: Tiled.width
sig: Tiled.width(map) -> int
tip: The map width in tiles.
order: 6
ns: Tiled
member: width
---
Returns the map's width in tiles.

View file

@ -7,13 +7,16 @@ program Grid {
model Marker { Tag } model Marker { Tag }
handler Boot phase Start { handler Boot phase Start {
# The maze is now a loaded Tiled map (issue #69): its `collision` layer —
# 1 = wall, 0 = open — projects to the same byte tilemap Map.row used to fill,
# so every Grid.* / Path.* query below is byte-identical.
# 01234567 # 01234567
Map.size(8, 5) # row 0 ########
Map.row(0, "########") # row 1 #......#
Map.row(1, "#......#") # row 2 #.####.#
Map.row(2, "#.####.#") # row 3 #....#.#
Map.row(3, "#....#.#") # row 4 ########
Map.row(4, "########") Tiled.load("assets/tiled-fixtures/grid_maze.tmx")
# --- Bresenham line --- # --- Bresenham line ---
let hl = Grid.line(1, 1, 6, 1) let hl = Grid.line(1, 1, 6, 1)

View file

@ -35,13 +35,13 @@ program PhysicsTiles {
let f_grd = World.field_id(pb, "on_ground") let f_grd = World.field_id(pb, "on_ground")
let f_hw = World.field_id(pb, "hit_wall") let f_hw = World.field_id(pb, "hit_wall")
# 16px tiles; '#' (35) is solid, '=' (61) is a one-way platform. # 16px tiles; the map is now a loaded Tiled map (issue #69). Its `collision`
Map.size(16, 10) # layer projects to the same byte tilemap: a non-zero GID becomes '#' (35,
Map.row(4, " === ") # one-way platform, cols 10-12 (y 64) # solid), and a GID whose tile carries the bool `oneway` property becomes
Map.row(5, " # ") # wall column, col 6, rows 5-7 # '=' (61), a one-way platform — so esys_move sees the identical grid.
Map.row(6, " # ") # one-way platform, cols 10-12 (y 64); wall column, col 6, rows 5-7;
Map.row(7, " # ") # solid floor, row 8 (top edge y 128).
Map.row(8, "################") # solid floor, row 8 (top edge y 128) Tiled.load("assets/tiled-fixtures/physics_map.tmx")
spawn Config { Solids { tile: 16, wall: 35, oneway: 61 } } spawn Config { Solids { tile: 16, wall: 35, oneway: 61 } }
# ---- T1. fall and land on the tile floor ------------------------------- # ---- T1. fall and land on the tile floor -------------------------------

View file

@ -0,0 +1,100 @@
# tiled_p1.ludic — the Tiled P1 core (issue #69): the rt_tmap model, the GID
# resolver (flip decode), image-backed render, and load-and-draw over the in-repo
# Kenney sampleMap. Proves the map loads and renders identically from both the
# `.tmx` and its `.tmj` export, and that flipped tiles draw mirrored.
#
# A game so the runtime (framebuffer + tilemap) is initialised; the Start handler
# runs the checks and Update quits. A full run prints:
# 1 2 3 4 5 6 7 8 9 10 11 12 13 14
program TiledP1 {
property Tag { v: int = 0 }
model Marker { Tag }
# a rolling hash of the whole framebuffer (deterministic; equal iff identical).
function fb_hash() -> int {
var h = 0
var y = 0
while y < 240 {
var x = 0
while x < 320 {
h = h * 31 + rt_get_px(x, y)
x = x + 4
}
y = y + 4
}
return h
}
handler Boot phase Start {
let m = Tiled.load("assets/kenney/tiny-dungeon/Tiled/sampleMap.tmx")
if Tiled.width(m) == 32 { print(1) }
if Tiled.height(m) == 20 { print(2) }
if Tiled.gid(m, 0, 0, 0) == 14 { print(3) }
# resolve a flip-flagged GID: 1610612787 = 0x60000033 -> V+D, global tile
# 0x33 = 51; firstgid 1 makes the tileset-local id 50; tileset 0
let r = Tiled.resolve(m, 1610612787)
if r.local == 50 { print(4) }
if r.fv == 1 { print(5) }
if r.fd == 1 { print(6) }
if r.fh == 0 { print(7) }
if r.tileset == 0 { print(8) }
let e = Tiled.resolve(m, 0)
if e.empty == 1 { print(9) }
# render the .tmx and hash the framebuffer
rt_clear(0)
Tiled.draw(m, 0, 0)
let hx = fb_hash()
if hx != 0 { print(10) } # something was drawn
# render the .tmj of the same map — must be pixel-identical
let j = Tiled.load("assets/kenney/tiny-dungeon/Tiled/sampleMap.tmj")
rt_clear(0)
Tiled.draw(j, 0, 0)
if fb_hash() == hx { print(11) }
# --- flips draw with the correct orientation ---
# find a horizontally-asymmetric tile so the mirror test is meaningful
var chosen = 0 - 1
var t = 0
while t < 132 {
rt_clear(0)
tmap_draw_gid(m, 1 + t, 0, 0)
var asym = 0
var yy = 0
while yy < 16 {
var xx = 0
while xx < 8 {
if rt_get_px(xx, yy) != rt_get_px(15 - xx, yy) { asym = 1 }
xx = xx + 1
}
yy = yy + 1
}
if asym == 1 and chosen < 0 { chosen = t }
t = t + 1
}
if chosen >= 0 { print(12) }
# draw the chosen tile normal at x=0 and H-flipped at x=16
rt_clear(0)
tmap_draw_gid(m, 1 + chosen, 0, 0)
tmap_draw_gid(m, (1 + chosen) | (1 << 31), 16, 0)
var mirror = 1
var diff = 0
var yy2 = 0
while yy2 < 16 {
var xx2 = 0
while xx2 < 16 {
if rt_get_px(xx2, yy2) != rt_get_px(31 - xx2, yy2) { mirror = 0 } # H-flip mirrors
if rt_get_px(xx2, yy2) != rt_get_px(15 - xx2, yy2) { diff = 1 } # the tile isn't symmetric
xx2 = xx2 + 1
}
yy2 = yy2 + 1
}
if mirror == 1 { print(13) }
if diff == 1 { print(14) }
}
handler Run phase Update { quit() }
}

View file

@ -442,3 +442,349 @@ function tiled_read_tsx(path: pointer) -> Val {
if tiled_first_byte(text) == 60 { return tsx_to_value(xml_parse(text)) } if tiled_first_byte(text) == 60 { return tsx_to_value(xml_parse(text)) }
return json_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
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?
function tmap_tile_prop(m: Tmap, gid: int, name: pointer) -> int {
let r = tmap_resolve(m, gid)
if r.tileset < 0 { return 0 }
let ts = m.tilesets[r.tileset]
if value_kind(ts.meta) != 6 { return 0 }
let tiles = value_get(ts.meta, "tiles")
if value_kind(tiles) != 5 { return 0 }
var i = 0
while i < value_count(tiles) {
let t = value_at(tiles, i)
if value_as_int(value_get(t, "id")) == r.local {
let props = value_get(t, "properties")
if value_kind(props) == 5 {
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
}
i = i + 1
}
return 0
}
# ---- legacy rt_map compatibility projection --------------------------------
# project the designated collision layer down to the byte tilemap so Grid.* /
# Path.* / esys_move keep working unchanged: an empty cell is ' ' (passable), a
# tile whose GID carries the `oneway` property is '=' (61), any other non-zero
# GID is '#' (35, solid). The legacy 96x64 cap clips, so old code is byte-exact.
function tmap_project(m: Tmap, layer: 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 ch = 35 # '#'
if tmap_tile_prop(m, gid, "oneway") == 1 { ch = 61 } # '='
rt_map[y * 96 + x] = ch
}
}
x = x + 1
}
}
y = y + 1
}
}
# 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)
}
# draw every visible tile layer in file order, offset by the camera (camx,camy).
function tmap_draw(m: Tmap, camx: int, camy: int) -> void {
var li = 0
while li < len(m.layers) {
let l = m.layers[li]
if l.kind == 0 and l.visible != 0 {
var y = 0
while y < l.h {
var x = 0
while x < l.w {
let gid = l.gids[y * l.w + x]
if gid != 0 {
tmap_draw_gid(m, gid, x * m.tilew - camx, y * m.tileh - camy)
}
x = x + 1
}
y = y + 1
}
}
li = li + 1
}
}
# ---- 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 }
return m
}
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))
let c = tmap_find_collision(m)
if c >= 0 { tmap_project(m, c) }
return m
}

View file

@ -43,16 +43,16 @@ function emit_index_addr(e: Node) -> pointer {
return r return r
} }
let el = slice_elem(base.ty) let el = slice_elem(base.ty)
g_addr_ty = el
# load the data pointer from the slice header (field 0) # load the data pointer from the slice header (field 0)
let dp = nreg() let dp = nreg()
emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ") emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ")
emit(base.code); emit(", i32 0, i32 0\n") emit(base.code); emit(", i32 0, i32 0\n")
let data = nreg() let data = nreg()
emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n") emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n")
let ix = emit_expr(e.b) let ix = emit_expr(e.b) # (evaluate index BEFORE setting
let r = nreg() let r = nreg() # g_addr_ty — a member-access index
emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(llty(el)) emit(" "); emit(r); emit(" = getelementptr inbounds "); emit(llty(el)) # overwrites it)
emit(", ptr "); emit(data); emit(", i32 "); emit(ix.code); emit("\n") emit(", ptr "); emit(data); emit(", i32 "); emit(ix.code); emit("\n")
g_addr_ty = el # set last so the caller sees the element type
return r return r
} }

View file

@ -613,6 +613,17 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (ns == "Tiled") { if (ns == "Tiled") {
if (meth == "read") { bare = "tiled_read"; push(labels, "path") } if (meth == "read") { bare = "tiled_read"; push(labels, "path") }
if (meth == "read_tsx") { bare = "tiled_read_tsx"; push(labels, "path") } if (meth == "read_tsx") { bare = "tiled_read_tsx"; push(labels, "path") }
if (meth == "load") { bare = "tiled_load"; push(labels, "path") }
if (meth == "gid") { bare = "tmap_gid"; push(labels, "map"); push(labels, "layer"); push(labels, "x"); push(labels, "y") }
if (meth == "resolve") { bare = "tmap_resolve"; push(labels, "map"); push(labels, "gid") }
if (meth == "width") { bare = "tmap_width"; push(labels, "map") }
if (meth == "height") { bare = "tmap_height"; push(labels, "map") }
if (meth == "layer_count") { bare = "tmap_layer_count"; push(labels, "map") }
if (meth == "layer_name") { bare = "tmap_layer_name"; push(labels, "map"); push(labels, "index") }
if (meth == "draw") { bare = "tmap_draw"; push(labels, "map"); push(labels, "camx"); push(labels, "camy") }
if (meth == "project") { bare = "tmap_project"; push(labels, "map"); push(labels, "layer") }
if (meth == "tree") { bare = "tmap_tree"; push(labels, "map") }
if (meth == "tile_prop") { bare = "tmap_tile_prop"; push(labels, "map"); push(labels, "gid"); push(labels, "name") }
} }
# #62: a package-provided namespace (declared with @Namespace(Foo)) that none # #62: a package-provided namespace (declared with @Namespace(Foo)) that none
# of the hardcoded core blocks matched — alias Foo.method to the bare function # of the hardcoded core blocks matched — alias Foo.method to the bare function

View file

@ -52,6 +52,7 @@ function emit_header() -> void {
emith("declare ptr @memcpy(ptr, ptr, i64)\n") emith("declare ptr @memcpy(ptr, ptr, i64)\n")
emith("declare ptr @memset(ptr, i32, i64)\n") emith("declare ptr @memset(ptr, i32, i64)\n")
emith("declare i64 @strlen(ptr)\n") emith("declare i64 @strlen(ptr)\n")
emith("declare i32 @strcmp(ptr, ptr)\n")
emith("declare i32 @strncmp(ptr, ptr, i64)\n") emith("declare i32 @strncmp(ptr, ptr, i64)\n")
emith("declare ptr @strstr(ptr, ptr)\n") emith("declare ptr @strstr(ptr, ptr)\n")
emith("declare i32 @atoi(ptr)\n") emith("declare i32 @atoi(ptr)\n")

View file

@ -15,7 +15,6 @@
# ids are assignment order in the source; field ids are declaration order. # ids are assignment order in the source; field ids are declaration order.
function emit_world_table() -> void { function emit_world_table() -> void {
let me = itoa(MAX_ENT) let me = itoa(MAX_ENT)
emith("declare i32 @strcmp(ptr, ptr)\n")
# EV7 — schema opening: a mod can register a brand-new component at runtime. # EV7 — schema opening: a mod can register a brand-new component at runtime.
# Compile-time components take prop ids 0..NC-1; mod-defined ones take NC.. and # Compile-time components take prop ids 0..NC-1; mod-defined ones take NC.. and

View file

@ -98,7 +98,6 @@ function emit_fs_prelude() -> void {
emith("declare ptr @opendir(ptr)\n") emith("declare ptr @opendir(ptr)\n")
emith("declare ptr @readdir(ptr)\n") emith("declare ptr @readdir(ptr)\n")
emith("declare i32 @closedir(ptr)\n") emith("declare i32 @closedir(ptr)\n")
emith("declare i32 @strcmp(ptr, ptr)\n")
emit_fs_common() emit_fs_common()
emit_fs_path() emit_fs_path()
emit_fs_io() emit_fs_io()

File diff suppressed because it is too large Load diff

View file

@ -684,6 +684,17 @@
], ],
"tiled": [ "tiled": [
"tiled-read", "tiled-read",
"tiled-read_tsx" "tiled-read_tsx",
"tiled-load",
"tiled-gid",
"tiled-resolve",
"tiled-width",
"tiled-height",
"tiled-layer_count",
"tiled-layer_name",
"tiled-draw",
"tiled-project",
"tiled-tree",
"tiled-tile_prop"
] ]
} }

View file

@ -207,6 +207,7 @@ function cmd_test() -> int {
feat_case("library/serialize", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "serialize.ludic (Value tree + Json encode/parse + Reflect.serialize/apply — bit-exact save/load; issue #44)") feat_case("library/serialize", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16", "serialize.ludic (Value tree + Json encode/parse + Reflect.serialize/apply — bit-exact save/load; issue #44)")
feat_case("library/tiled_p0", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21", "tiled_p0.ludic (Tiled P0: Xml reader + Base64 decode/encode + gzip framing over inflate; issue #67)") feat_case("library/tiled_p0", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21", "tiled_p0.ludic (Tiled P0: Xml reader + Base64 decode/encode + gzip framing over inflate; issue #67)")
feat_case("library/tiled_p05", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30", "tiled_p05.ludic (Tiled P0.5: TMX/TSX reader -> same Value-tree intermediate as TMJ; Kenney sampleMap.tmx; issue #68)") feat_case("library/tiled_p05", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30", "tiled_p05.ludic (Tiled P0.5: TMX/TSX reader -> same Value-tree intermediate as TMJ; Kenney sampleMap.tmx; issue #68)")
feat_case("library/tiled_p1", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "tiled_p1.ludic (Tiled P1: rt_tmap model + GID resolver/flip decode + render; loads+draws Kenney map identically from .tmx and .tmj; issue #69)")
feat_case("library/render", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "render.ludic (Screen pixel/oval/camera/clip/blend_mode/measure_text + Camera set/follow/shake, verified by pixel readback)") feat_case("library/render", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "render.ludic (Screen pixel/oval/camera/clip/blend_mode/measure_text + Camera set/follow/shake, verified by pixel readback)")
feat_case("library/lighting", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "lighting.ludic (Light ambient/point radial falloff + occluder hard shadows — 2D light accumulation, verified by pixel readback)") feat_case("library/lighting", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14", "lighting.ludic (Light ambient/point radial falloff + occluder hard shadows — 2D light accumulation, verified by pixel readback)")
feat_case("library/light_tiers", "", "1 1 1 1 1 1 1 1 1", "light_tiers.ludic (Light spot/falloff/soft/gel/normal/time_of_day — render-quality tiers 3-4; issue #49)") feat_case("library/light_tiers", "", "1 1 1 1 1 1 1 1 1", "light_tiers.ludic (Light spot/falloff/soft/gel/normal/time_of_day — render-quality tiers 3-4; issue #49)")