assets source

← assets · API reference · version 0.5.0

7 file(s), 1289 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.

Public items

A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.

ItemDefinedWorked exampleCall sites
BlobKind enum30—44 196 577
DefKind enum34—100
Blob struct42—196 202 213 288 540 590 … (29 total)
PageInfo struct54—158 340 366 423 448 559 … (15 total)
Cell struct68—159 341 366 424 448 570 … (15 total)
Seq struct82—160 590 59 17
Def struct98—161 590 59 19 21 23 … (7 total)
Anim struct114—162 590 59 25 27 74
Scene struct123—163 590 59 29 38 21
Placed struct134—164 590 59 30 32 34 … (10 total)
Pack struct157—173 174 213 230 244 257 … (12 total)
new_pack fn173—52 57 88 113 128 142 … (21 total)
meta_path fn183—215 231 232 43 44 165 … (8 total)
blobs_path fn186—216 43 44 67 97 106 … (14 total)
blob_put fn196—577 31 32 33 174 175 … (8 total)
pack_write fn213PAK-00155 91 116 131 162 38 … (7 total)
pack_read fn230—58 143 40 60
anim_of fn244PAK-00346 47 50 51 52 75 … (8 total)
keys_near fn257—58 65 70 71 83 61
prefetch fn288PAK-00297 106 120 147 43 64 … (7 total)
PROXY_ALPHA_ON const314—350 392 428
PROXY_BANDS const323—449
opaque_texels fn423—78 96 114 129
cell_proxy fn448—573 63 81 97 115 130
proxy_overshoot fn462—82 87 102 132
texels fn478—26 91 79 45
Tile struct494—541 37 38 54 55 67 … (13 total)
page_grid fn540—36 53 66 83 98 102 … (7 total)
layout_descriptor fn588—612 43 51 55 70 71
layout_fingerprint fn610—42 43 50

src/assets.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// assets — a game's content pack, which IS a loft store.
//
// There is no codec here, and no parse step: the struct definitions below ARE the
// file layout. That is what lets a pack sit on any dumb file server and be read by
// HTTP range, with no server-side code — a browser fetches the pages one lookup
// touches and nothing else.
//
// A pack is TWO stores, split by how each half is READ:
//
//   * `Pack` — the metadata. Small, read whole at boot, and a wrapper struct
//     holding seven keyed collections, so one file carries all of them.
//   * `hash<Blob[bl_key]>` — the bulk bytes: pixels, audio, fonts. Rooted on the
//     hash itself, never reached through a field, because a collection reached
//     through one records the WRAPPER as the store's type and the paged loaders
//     refuse such a store.
//
// Mixing the two would make the boot load pay for the art.

// --- What a pack holds ---

use shapes::*;

/// What a blob's bytes are, so a loader knows what to do with them without
/// sniffing the content. `Rgba` is a texture page's own pixels — premultiplied,
/// row-major, four bytes per texel — already decoded by the packer, so the runtime
/// never learns how the art was made.
pub enum BlobKind { Bytes, Rgba, Png, Ogg, Wav, Ttf, Woff2 }

/// Whether a definition draws or lights. A light is a placed instance like any
/// other, and this is what says which.
pub enum DefKind { Sprite, Light }

/// Raw bytes under a key — a texture page's pixels, audio, a font, a source PNG.
///
/// One blob is the **paging unit**: `store_load_key_text` fetches the pages this
/// record touches and nothing else. So size a blob like an atlas page rather than
/// like one sprite — a store's minimum image is 8 KiB, and a finer key fetches a
/// page's worth either way.
pub struct Blob {
  pub bl_key: text,
  pub bl_kind: BlobKind = Bytes,
  pub bl_bytes: vector<u8>,
}

/// A texture page's SHAPE, and the blob key its pixels live under.
///
/// The dimensions are metadata and the pixels are bulk, so they live in different
/// stores. A loader that needs to know a page is 64x32 — to build UVs, to size a GL
/// texture, to decide what to prefetch — gets that from the boot load without
/// fetching a single texel.
pub struct PageInfo {
  pub pi_name: text,
  pub pi_w: integer,
  pub pi_h: integer,
  pub pi_blob: text,          // the `Rgba` blob holding this page's pixels
}

/// One sprite's rect within a page, plus the origin a stage places it by.
///
/// `ce_proxy` is the collision proxy DERIVED from this cell's own alpha at pack
/// time (see `cell_proxy`), in cell-local coordinates. It is empty when nothing
/// derived one — a pack built before proxies, or a cell with no opaque texels —
/// and an empty proxy hits nothing, so a consumer that finds one absent falls back
/// to the cell rect rather than colliding with everything.
pub struct Cell {
  pub ce_key: text,
  pub ce_page: text,
  pub ce_x: integer,
  pub ce_y: integer,
  pub ce_w: integer,
  pub ce_h: integer,
  pub ce_ox: float = 0.0,
  pub ce_oy: float = 0.0,
  pub ce_proxy: vector<shapes::Rect>,
}

/// An animation: a run of cells laid out in one page's grid. Mirrors `stage`'s
/// `Sequence`, so a loader hands it straight over.
pub struct Seq {
  pub sq_key: text,
  pub sq_page: text,
  pub sq_first: integer,
  pub sq_count: integer,
  pub sq_fps: float = 12.0,
  pub sq_mode: integer = 0,
  pub sq_cols: integer = 1,
  pub sq_rows: integer = 1,
}

/// An object definition — GameMaker's *object*, the thing a room places.
///
/// A walk cycle is asset data and not code, so the animation table hangs off the
/// definition (see `anim_of`) rather than living in a game's source. A light
/// carries its radius, colour and power here and is placed exactly like a sprite.
pub struct Def {
  pub df_name: text,
  pub df_kind: DefKind = Sprite,
  pub df_cell: text = "",          // the still sprite, when it has no sequence
  pub df_w: float = 0.0,
  pub df_h: float = 0.0,
  pub df_ox: float = 0.0,
  pub df_oy: float = 0.0,
  pub df_layer: integer = 0,
  pub df_radius: float = 0.0,      // light only: reach is 0 at exactly this distance
  pub df_colour: integer = 0,
  pub df_power: float = 0.0,
}

/// One row of a definition's animation table: `(action, facing) -> sequence`.
/// Build `an_key` as `"{def}/{action}/{facing}"`, which is what `anim_of` looks up.
pub struct Anim {
  pub an_key: text,
  pub an_def: text,
  pub an_action: text,
  pub an_facing: integer,
  pub an_seq: text,
}

/// A room: a named space that placed instances live in.
pub struct Scene {
  pub sc_name: text,
  pub sc_w: integer,
  pub sc_h: integer,
}

/// A placed instance — GameMaker's *room instance*.
///
/// Definitions plus these are what a scene IS; a flat node dump cannot say that two
/// goblins are the same goblin. The fields mirror `stage::Place`, so loading a scene
/// is a copy rather than a translation.
pub struct Placed {
  pub pd_scene: text,
  pub pd_order: integer,
  pub pd_def: text,
  pub pd_x: float = 0.0,
  pub pd_y: float = 0.0,
  pub pd_depth: float = 0.0,
  pub pd_layer: integer = 0,
  pub pd_rot: float = 0.0,
  pub pd_sx: float = 1.0,
  pub pd_sy: float = 1.0,
  pub pd_alpha: float = 1.0,
  pub pd_colour: integer = 0,
  pub pd_visible: boolean = true,
}

/// A pack's METADATA half — every collection a game reads at boot.
///
/// Binding through this container is what puts all seven collections in one file: a
/// keyed field shares its container's store, so one persist call writes one file and
/// a reader that declares the same type gets all of it. Read it WHOLE; it is small,
/// every part is needed from the first frame, and being a wrapper struct it cannot be
/// paged anyway. The bytes are the other store's job.
pub struct Pack {
  pub pages: hash<PageInfo[pi_name]>,
  pub cells: hash<Cell[ce_key]>,
  pub seqs: hash<Seq[sq_key]>,
  pub defs: hash<Def[df_name]>,
  pub anims: hash<Anim[an_key]>,
  pub scenes: hash<Scene[sc_name]>,
  pub placed: index<Placed[pd_scene, pd_order]>,
}

// --- Writing a pack, and reading one back ---

/// An empty pack — every collection present and none of them holding anything.
///
/// Both a packer and a loader start here: a loader declares the pack it is about to
/// fill, then hands it to `pack_read`.
pub fn new_pack() -> Pack {
  Pack { pages: [], cells: [], seqs: [], defs: [], anims: [], scenes: [],
         placed: [] }
}

/// Where a pack's metadata half lives, given the name the pack is known by.
///
/// `base` is a local path or an `http(s)://` URL — the two halves are named the same
/// way either side of the wire, which is what lets a game develop against a file and
/// ship against a CDN with only the base changed.
pub fn meta_path(base: text) -> text { "{base}.meta.store" }

/// Where a pack's bulk half lives. Pass this to `prefetch` to page a blob out of it.
pub fn blobs_path(base: text) -> text { "{base}.blobs.store" }

/// Is this base a URL rather than a path on this machine?
fn is_url(base: text) -> boolean {
  base.starts_with("http://") || base.starts_with("https://")
    || base.starts_with("file://")
}

/// Put a blob under its key, ready to be written.  A blob already under the key is
/// replaced, which is what a packer re-running over changed art wants.
pub fn blob_put(b: &hash<Blob[bl_key]>, key: text, kind: BlobKind,
                bytes: const vector<u8>) {
  // The old blob is removed first rather than replaced in one assignment: a loft before
  // the fix of keyed replacement under a TEXT key kept both records (`len` 2 under one
  // key).  A removal by key was always exact, so this is right on every loft.
  b[key] = null;
  b[key] = Blob { bl_key: key, bl_kind: kind, bl_bytes: bytes };
}

/// Write both halves of a pack, answering whether both landed.
///
/// A pack is final by the time it ships, so this copies rather than binds: a copied
/// image lays each collection out in its own order, which is what makes one keyed
/// lookup one RUN of pages instead of a scatter — the difference between a range
/// read that pays for a page and one that pays for the file. Writing needs a
/// desktop; a browser reads packs and does not build them.
// Example: @PAK-001
pub fn pack_write(p: const Pack, blobs: const hash<Blob[bl_key]>,
                  base: text) -> boolean {
  if !store_persist_copy(p, meta_path(base)) { return false }
  store_persist_copy(blobs, blobs_path(base))
}

/// Read a pack's metadata half in full, answering whether it arrived.
///
/// Give it a pack from `new_pack`. `base` is a local path or an `http(s)://` /
/// `file://` URL, and that is the only thing that changes between developing against
/// a file and shipping against a CDN. The bytes are NOT read here — ask for those by
/// key, through `prefetch`, at a load or level boundary.
///
/// A URL is fetched from a source you TRUST: the image is structurally validated, so
/// a corrupt file is refused rather than adopted, but its contents are not
/// authenticated. Serve packs from your own origin, or verify the download yourself
/// with `store_load_url` before reading it.
pub fn pack_read(p: Pack, base: text) -> boolean {
  if is_url(base) { return store_load_url_trusted(p, meta_path(base)) }
  store_load(p, meta_path(base))
}

// --- Asking a pack what a scene needs ---

/// The sequence an action and a facing map to, or `""` when the definition has none.
///
/// This is the animation table's whole point: `anim_of(pack, "goblin", "walk", 2)`
/// answers `"goblin_walk_n"` with nothing in a game's source holding the mapping, and
/// an action the definition does not have answers empty rather than a plausible wrong
/// sequence.
// Example: @PAK-003
pub fn anim_of(p: const Pack, def: text, action: text, facing: integer) -> text {
  row = p.anims["{def}/{action}/{facing}"];
  if row == null { return "" }
  row.an_seq
}

/// Every blob key a scene's instances draw from within `radius` of `(cx, cy)`.
///
/// Ask for this at a load or a level boundary, never inside a frame: a range read is
/// a round trip and a frame is 16 ms, so a frame that DISCOVERS it needs an asset
/// stutters visibly. A negative `radius` means the whole scene — what a small room
/// and a loading screen both want. Duplicates are dropped, because a page shared by
/// forty instances is still one fetch.
pub fn keys_near(p: const Pack, scene: text, cx: float, cy: float,
                 radius: float) -> vector<text> {
  out: vector<text> = [];
  for i in p.placed {
    if i.pd_scene != scene { continue }
    if radius >= 0.0 {
      dx = i.pd_x - cx;
      dy = i.pd_y - cy;
      if dx * dx + dy * dy > radius * radius { continue }
    }
    d = p.defs[i.pd_def];
    if d == null { continue }
    if d.df_cell == "" { continue }        // a light draws nothing
    c = p.cells[d.df_cell];
    if c == null { continue }
    page = p.pages[c.ce_page];
    if page == null { continue }
    seen = false;
    for k in out { if k == page.pi_blob { seen = true } }
    if !seen { out += [page.pi_blob]; }
  }
  out
}

/// Pull `keys` into `into`, and answer how many actually COST something.
///
/// `source` is `blobs_path(base)`. A key already resident is skipped, so the answer is
/// the number of round trips this call paid for: what a level boundary wants to log,
/// and what a frame must be able to assert is zero. A key the pack does not hold is
/// skipped too — an absent asset costs a lookup, not a record.
// Example: @PAK-002
pub fn prefetch(into: hash<Blob[bl_key]>, source: text,
                keys: const vector<text>) -> integer {
  // A `file://` base names a file on this machine, and the paged loader takes the
  // path — it refuses the URL spelling outright.  Dropping the scheme here is what
  // lets ONE base drive both halves of a pack; the metadata half reads a `file://`
  // URL directly, so without this the same string would work for one and not the
  // other.
  src = source;
  // Byte offsets, because a slice is bytes: `len` counts CHARACTERS and a path
  // outside ASCII would be cut short by it.
  if src.starts_with("file://") { src = src[7..size(src)]; }
  n = 0;
  for k in keys {
    if into[k] != null { continue }
    if store_load_key_text(into, src, k) { n += 1 }
  }
  n
}

// --- Deriving a collision proxy from the art ---

/// A texel at or below this alpha is not part of the silhouette.
///
/// Not 128. Antialiased art has a fringe of low-alpha texels that visibly belong
/// to the sprite, and a proxy that excludes them stops short of the outline a
/// player sees. 8 is the value @PLN146 F7a measured the corpus with.
pub const PROXY_ALPHA_ON = 8;

/// How many bands a derived proxy is cut into.
///
/// Measured over 36 sprites (@PLN146 F7a): one tight box overshoots the art by
/// +150.6 % on average, a convex hull by +54.9 %, 8 bands by +52.2 % and 16 by
/// +36.7 % — and 16 beats the hull on mean, median AND worst case while needing no
/// shape kind `shapes` does not have. 32 buys another 10 points for twice the
/// boxes; 16 is where the curve flattens.
pub const PROXY_BANDS = 16;

/// The alpha of one texel of an `Rgba`/`Bytes` page, or 0 when it is off the page.
///
/// Off-page reads 0 — TRANSPARENT — rather than null, because the only caller is a
/// containment scan and a texel that is not there cannot be part of a silhouette.
fn texel_alpha(ta_px: const vector<u8>, ta_stride: integer, ta_x: integer,
               ta_y: integer) -> integer {
  ta_i = (ta_y * ta_stride + ta_x) * 4 + 3;
  if ta_i < 0 || ta_i >= len(ta_px) { return 0 }
  ta_px[ta_i]?
}

/// The along-axis extent of a cell's opaque mask: `(lo, hi)`, or `(-1, -1)` when
/// nothing in the cell is opaque.
///
/// `swap` reads the mask transposed, which is how one routine serves both axes.
fn opaque_extent(oe_px: const vector<u8>, oe_page: const PageInfo,
                 oe_c: const Cell, oe_swap: boolean) -> (integer, integer) {
  oe_lo = -1;
  oe_hi = -1;
  oe_vn = if oe_swap { oe_c.ce_w } else { oe_c.ce_h };
  oe_un = if oe_swap { oe_c.ce_h } else { oe_c.ce_w };
  for v in 0..oe_vn {
    for u in 0..oe_un {
      oe_ax = oe_c.ce_x + (if oe_swap { v } else { u });
      oe_ay = oe_c.ce_y + (if oe_swap { u } else { v });
      if texel_alpha(oe_px, oe_page.pi_w, oe_ax, oe_ay) > PROXY_ALPHA_ON {
        if oe_lo < 0 { oe_lo = v; }
        oe_hi = v;
        break;
      }
    }
  }
  (oe_lo, oe_hi)
}

/// Band the opaque texels along ONE axis and take each band's tight box.
///
/// `swap` runs the identical decomposition on the other axis by reading the mask
/// transposed and un-transposing the boxes, which is how one routine answers both
/// without a second copy of the arithmetic. Boxes come back in CELL-LOCAL
/// coordinates.
fn band_boxes(bb_px: const vector<u8>, bb_page: const PageInfo, bb_c: const Cell,
              bb_k: integer, bb_swap: boolean) -> vector<shapes::Rect> {
  bb_out: vector<shapes::Rect> = [];
  (bb_lo, bb_hi) = opaque_extent(bb_px, bb_page, bb_c, bb_swap);
  if bb_lo < 0 { return bb_out }
  bb_span = bb_hi - bb_lo + 1;
  bb_un = if bb_swap { bb_c.ce_h } else { bb_c.ce_w };

  // One accumulator per band.  Parallel vectors rather than a keyed collection:
  // the index IS the key, and a hash here would allocate a record per band for a
  // number the loop already has.
  bmin_u: vector<integer> = [];
  bmax_u: vector<integer> = [];
  bmin_v: vector<integer> = [];
  bmax_v: vector<integer> = [];
  for _ in 0..bb_k {
    bmin_u += [-1]; bmax_u += [-1]; bmin_v += [-1]; bmax_v += [-1];
  }
  for v in bb_lo..=bb_hi {
    // `/` types nullable (a zero divisor answers null); `bb_span` is `bb_hi - bb_lo + 1`
    // behind the `bb_lo < 0` return above, so it is at least 1 and `?? 0` is unreachable.
    bb_band = ((v - bb_lo) * bb_k) / bb_span ?? 0;
    if bb_band > bb_k - 1 { bb_band = bb_k - 1; }
    for u in 0..bb_un {
      bb_ax = bb_c.ce_x + (if bb_swap { v } else { u });
      bb_ay = bb_c.ce_y + (if bb_swap { u } else { v });
      if texel_alpha(bb_px, bb_page.pi_w, bb_ax, bb_ay) <= PROXY_ALPHA_ON { continue }
      if bmin_u[bb_band]? < 0 || u < bmin_u[bb_band]? { bmin_u[bb_band] = u; }
      if u > bmax_u[bb_band]? { bmax_u[bb_band] = u; }
      if bmin_v[bb_band]? < 0 || v < bmin_v[bb_band]? { bmin_v[bb_band] = v; }
      if v > bmax_v[bb_band]? { bmax_v[bb_band] = v; }
    }
  }
  for b in 0..bb_k {
    if bmin_u[b]? < 0 { continue }
    bb_uu = bmin_u[b]? as float;
    bb_vv = bmin_v[b]? as float;
    bb_uw = (bmax_u[b]? - bmin_u[b]? + 1) as float;
    bb_vh = (bmax_v[b]? - bmin_v[b]? + 1) as float;
    if bb_swap {
      bb_out += [shapes::Rect{rx: bb_vv, ry: bb_uu, rw: bb_vh, rh: bb_uw}];
    } else {
      bb_out += [shapes::Rect{rx: bb_uu, ry: bb_vv, rw: bb_uw, rh: bb_vh}];
    }
  }
  bb_out
}

/// Total area of a box set — the numerator of the overshoot the packer gates on.
fn boxes_area(ba: const vector<shapes::Rect>) -> float {
  ba_t = 0.0;
  for r in ba { ba_t += r.rw * r.rh; }
  ba_t
}

/// How many texels of this cell are opaque — the thing a proxy stands for, and the
/// denominator of `proxy_overshoot`.
pub fn opaque_texels(ot_px: const vector<u8>, ot_page: const PageInfo,
                     ot_c: const Cell) -> integer {
  ot_n = 0;
  for y in 0..ot_c.ce_h {
    for x in 0..ot_c.ce_w {
      if texel_alpha(ot_px, ot_page.pi_w, ot_c.ce_x + x, ot_c.ce_y + y) > PROXY_ALPHA_ON {
        ot_n += 1;
      }
    }
  }
  ot_n
}

/// Derive `c`'s collision proxy from the page's own alpha, in cell-local
/// coordinates.
///
/// Banded on the TIGHTER axis: the derivation runs both ways and keeps the smaller
/// total area. 17 of the 36 corpus sprites band better by column than by row, so
/// choosing per sprite rather than fixing an axis takes the median overshoot from
/// +29.2 % to +22.8 % — for the cost of running a pass twice at pack time, which
/// is a cost nothing at runtime pays.
///
/// A cell with no opaque texels derives an EMPTY proxy, and an empty proxy hits
/// nothing. That is the answer a fully transparent cell should give; a single box
/// around it would collide with whatever stood there.
pub fn cell_proxy(c: const Cell, page: const PageInfo, px: const vector<u8>,
                  bands: integer = PROXY_BANDS) -> vector<shapes::Rect> {
  rows = band_boxes(px, page, c, bands, false);
  cols = band_boxes(px, page, c, bands, true);
  if len(cols) > 0 && boxes_area(cols) < boxes_area(rows) { return cols }
  rows
}

/// How much bigger a derived proxy is than the art it stands for.
///
/// The number F7 gates on: `<= 1.0` is at most twice the art. Reported per sprite
/// rather than asserted in aggregate, because one adversarial shape — three thin
/// diagonal arrows — misses the bound at every band count, and a bound nothing can
/// violate is not a bound.
pub fn proxy_overshoot(po_boxes: const vector<shapes::Rect>, po_solid: integer) -> float {
  if po_solid <= 0 { return 0.0 }
  ((boxes_area(po_boxes) - (po_solid as float)) / (po_solid as float))?
}

// --- Building a page out of sprites ---

/// The RGBA bytes of a row-major buffer of `0xAARRGGBB` words.
///
/// Both routes into a `Tile` end here. A `graphics::Canvas` holds exactly these
/// words in `data`, and a PNG decoded by `imaging` answers them one pixel at a time
/// through `Pixel.rgba()` — so a sprite rendered in this process and the same sprite
/// read back off disk become the same texels by the same arithmetic, rather than by
/// two conversions that agree until one of them is edited.
///
/// Alpha is carried STRAIGHT, not premultiplied: see `page_grid`.
pub fn texels(words: const vector<integer>) -> vector<u8> {
  tx_out: vector<u8> = [];
  for w in words {
    tx_out += [((w >> 16) & 255) as u8, ((w >> 8) & 255) as u8,
               (w & 255) as u8, ((w >> 24) & 255) as u8];
  }
  tx_out
}

/// One sprite on its way into a page: the key it will be known by, its size, and
/// its texels.
///
/// `ti_px` is `ti_w * ti_h * 4` bytes of STRAIGHT-alpha RGBA, row-major. Where
/// those bytes came from is deliberately not this package's business — a decoded
/// PNG and a canvas rendered in this process are the same tile here, which is what
/// lets a sprite authored as text skip the file it used to have to become.
pub struct Tile {
  pub ti_key: text,
  pub ti_w: integer,
  pub ti_h: integer,
  pub ti_px: vector<u8>,
  /// Where the stage places the sprite BY, in cell-local texels — the feet of a
  /// character rather than its top-left corner.
  pub ti_ox: float = 0.0,
  pub ti_oy: float = 0.0,
}

/// `n` zero bytes.
///
/// Built by DOUBLING: a page buffer is megabytes, and appending one element at a
/// time costs three bytecodes each, while appending a vector to itself is one
/// block copy.
fn zero_bytes(n: integer) -> vector<u8> {
  zb: vector<u8> = [];
  if n <= 0 { return zb }
  zb += [0];
  while len(zb) < n { zb += zb; }
  if len(zb) == n { return zb }
  zt = zb[0..n];
  zt
}

/// The smallest square-ish column count for `n` tiles.
fn grid_cols(n: integer) -> integer {
  gc = 1;
  while gc * gc < n { gc += 1; }
  gc
}

/// Lay tiles out in a grid page, blit their texels into it, and record one `Cell`
/// per tile — with each cell's collision proxy DERIVED from the alpha as it lands.
///
/// The page is sized from the widest and tallest tile, so every cell shares one
/// grid step; `pad` puts transparent texels between them, which is what a sampler
/// that filters needs to stop a neighbour bleeding in. Answers false — writing
/// nothing — when a tile's byte count disagrees with the size it declares, because
/// a packer that quietly places a truncated sprite is the failure this exists to
/// prevent.
///
/// The pixels land under `blob` as `Bytes`: STRAIGHT alpha, not premultiplied,
/// because that is what a blend of SRC_ALPHA / ONE_MINUS_SRC_ALPHA expects.
/// `BlobKind::Rgba` promises premultiplied texels and these are not.
pub fn page_grid(p: Pack, blobs: &hash<Blob[bl_key]>, page: text, blob: text,
                 tiles: const vector<Tile>, cols: integer = 0,
                 pad: integer = 0) -> boolean {
  if len(tiles) == 0 { return false }
  for t in tiles {
    if t.ti_w <= 0 || t.ti_h <= 0 { return false }
    if len(t.ti_px) != t.ti_w * t.ti_h * 4 { return false }
  }
  pg_cols = if cols > 0 { cols } else { grid_cols(len(tiles)) };
  pg_rows = ((len(tiles) + pg_cols - 1) / pg_cols)?;
  pg_cw = 0;
  pg_ch = 0;
  for t in tiles {
    if t.ti_w > pg_cw { pg_cw = t.ti_w; }
    if t.ti_h > pg_ch { pg_ch = t.ti_h; }
  }
  pg_w = pg_cols * (pg_cw + pad) - pad;
  pg_h = pg_rows * (pg_ch + pad) - pad;
  pg_px = zero_bytes(pg_w * pg_h * 4);
  pg_info = PageInfo { pi_name: page, pi_w: pg_w, pi_h: pg_h, pi_blob: blob };

  for i in 0..len(tiles) {
    pg_t = tiles[i]?;
    pg_x = ((i % pg_cols)?) * (pg_cw + pad);
    pg_y = ((i / pg_cols)?) * (pg_ch + pad);
    for y in 0..pg_t.ti_h {
      pg_src = y * pg_t.ti_w * 4;
      pg_dst = ((pg_y + y) * pg_w + pg_x) * 4;
      for k in 0..(pg_t.ti_w * 4) { pg_px[pg_dst + k] = pg_t.ti_px[pg_src + k]?; }
    }
    pg_c = Cell { ce_key: pg_t.ti_key, ce_page: page, ce_x: pg_x, ce_y: pg_y,
                  ce_w: pg_t.ti_w, ce_h: pg_t.ti_h,
                  ce_ox: pg_t.ti_ox, ce_oy: pg_t.ti_oy, ce_proxy: [] };
    pg_c.ce_proxy = cell_proxy(pg_c, pg_info, pg_px);
    p.cells += [pg_c];
  }
  p.pages += [pg_info];
  blob_put(blobs, blob, BlobKind.Bytes, pg_px);
  true
}

// --- Checking the layout before anything reads a pack ---

/// The pack's own types, field by field, as the store records them.
///
/// Read this when the fingerprints below disagree: it names the type and the field
/// whose position moved, which the single number cannot. Derived by reflection from
/// the schema itself, so it cannot drift from it.
pub fn layout_descriptor() -> text {
  d = "";
  for n in ["Blob", "PageInfo", "Cell", "Seq", "Def", "Anim", "Scene", "Placed", "Pack"] {
    t = type_named(n);
    if t == null { d += "{n} MISSING\n"; continue }
    d += "{t.name} size={t.size}\n";
    for f in t.fields {
      d += "  {f.name} {f.type_name} @{f.position} {f.kind} null={f.nullable}\n";
    }
  }
  d
}

/// One number naming this build's pack layout — compare it across targets before a
/// pack is read.
///
/// The struct definition IS the file layout, so a field that sits four bytes further
/// along on one target reads a NEIGHBOUR's value on the other, silently, and the file
/// looks corrupt rather than the layout looking wrong. A game that reads packs built
/// elsewhere should compare this once at boot and refuse rather than draw garbage.
/// (A 32-bit FNV-1a over `layout_descriptor`, masked at every step because an
/// unbounded multiply would reach loft's null.)
pub fn layout_fingerprint() -> integer {
  h = 2166136261;
  for c in layout_descriptor() {
    h = (h ^ (c as integer)) & 4294967295;
    h = (h * 16777619) & 4294967295;
  }
  h
}

tests/layout.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// The pack's layout IS its file format, so this suite is the format's regression
// test: the struct definitions decide where every byte of a written pack lands, and
// a field that moves reads a neighbour's value out of packs already in the wild.
use assets::*;

/// What the layout of `assets` 0.5.0 hashes to.
///
/// A CHANGE HERE IS A FORMAT CHANGE.  Packs written by an earlier release are read at
/// the offsets this number stands for, so if this test fails, one of two things
/// happened and each has its own answer:
///
///   * the schema was edited — every existing pack now reads garbage at some field,
///     so `data_compatible_with` in `loft.toml` moves to this release and the
///     CHANGELOG says so;
///   * loft's own layout rules moved under us — the same breakage, arriving without
///     an edit, which is exactly why this is pinned rather than merely computed.
///
/// 0.1.0 hashed to 21440911.  It moved at 0.2.0 because `Cell` gained `ce_proxy`
/// (@PLN146 F7) — the FIRST answer above, an edit we made, so `data_compatible_with`
/// moved with it.  0.2.0 to 0.3.1 hashed to 2433656147.  It moved at 0.4.0 with no
/// edit here — the SECOND answer: loft's C138 rounds a record's size up to its
/// alignment (`(L-Align)`), so `Blob` went 9 -> 12, `Cell` 60 -> 64, `Def` 73 -> 80,
/// `Scene` 20 -> 24 and `Placed` 98 -> 104 bytes, every field at the position it had.
/// A collection's element stride is its record's size, so a 0.3.x pack is refused by
/// a loft with C138 (it names those five types) rather than misread.  0.4.0 to 0.4.2
/// hashed to 3641078007.  It moved at 0.5.0, again with no edit here: loft's C147 makes
/// an `index` a B+tree kept in an arena of its own, so `Placed`, the element of the
/// pack's `placed` index, no longer carries the red-black tree's links (`#left_1` at
/// 88, `#right_1` at 92, `#color_1` at 96).  `pd_visible` moved from 97 to 88 and
/// `Placed` went 104 -> 96 bytes; no other record changed.  A 0.4.x pack is refused by
/// a loft with C147 (it names `Placed` and its index) rather than misread: `pack_read`
/// answers false.  The old numbers stay written down here: a pin nobody can compare
/// against does not say a format changed, only that one is different.
///
/// Measured identical on the interpreter and `--native`.
LAYOUT_0_5_0 = 755486205;

fn test_the_layout_of_this_release_is_the_one_it_promises() {
  assert(layout_fingerprint() == LAYOUT_0_5_0,
         "the pack layout changed: {layout_fingerprint()} (was {LAYOUT_0_5_0}) — read the note on LAYOUT_0_5_0, and layout_descriptor() for the field that moved");
}

fn test_the_fingerprint_is_taken_over_real_content() {
  // A hash of nothing is a stable number too, so the pin above would survive a
  // descriptor that had gone empty.  2166136261 is FNV-1a's offset basis: the answer
  // for empty input, and what this must not be.
  assert(layout_fingerprint() != 2166136261, "the fingerprint consumed something");
  assert(size(layout_descriptor()) > 1000, "the descriptor is a description");
}

fn test_every_pack_type_resolves() {
  d = layout_descriptor();
  // Reflection answering MISSING means a type was renamed out from under the
  // descriptor's own list — the fingerprint would change, but not say why.
  assert(!d.contains("MISSING"), "every pack type resolved:\n{d}");
  for t in ["Blob", "PageInfo", "Cell", "Seq", "Def", "Anim", "Scene", "Placed", "Pack"] {
    assert(d.contains("{t} size="), "{t} is described");
  }
}

fn test_the_descriptor_carries_positions_not_declaration_order() {
  // The load-bearing property: a field's POSITION is what a stored pack is read at,
  // and it is not the order the field was written in.  `ce_key` is declared first
  // and sits at 48, after the six integers and two floats — so a descriptor that
  // reported declaration order (or all zeroes) would hash to a stable number and
  // gate nothing.
  assert(layout_descriptor().contains("ce_key text @48"),
         "a text field placed after the fixed-width ones:\n{layout_descriptor()}");
}

tests/pack.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// The pack round-trip: what is written comes back, byte for byte, and a key can be
// read out of the bulk half without reading the rest of it.
use assets::*;

/// A deterministic byte pattern.  A run of zeroes would pass a loader that returned
/// nothing, so every fixture byte depends on its own index.
fn pattern(n: integer, salt: integer) -> vector<u8> {
  out: vector<u8> = [];
  for i in 0..n { out += [((i * 37 + salt * 11 + i / 251) % 251) as u8 ?? 0]; }
  out
}

fn same_bytes(a: const vector<u8>, b: const vector<u8>) -> boolean {
  if len(a) != len(b) { return false }
  for i in 0..len(a) { if a[i] != b[i] { return false } }
  true
}

/// A pack with both halves populated: two texture pages, a sprite cut out of each,
/// and an audio blob that is not a page at all.
fn fixture(p: Pack, b: &hash<Blob[bl_key]>) {
  p.pages += [PageInfo { pi_name: "mobs", pi_w: 64, pi_h: 32, pi_blob: "page/mobs" }];
  p.pages += [PageInfo { pi_name: "items", pi_w: 32, pi_h: 32, pi_blob: "page/items" }];
  p.cells += [Cell { ce_key: "goblin", ce_page: "mobs", ce_x: 0, ce_y: 0,
                     ce_w: 16, ce_h: 16, ce_ox: 8.0, ce_oy: 16.0 }];
  p.cells += [Cell { ce_key: "potion", ce_page: "items", ce_x: 0, ce_y: 0,
                     ce_w: 16, ce_h: 16 }];
  blob_put(b, "page/mobs", Rgba, pattern(64 * 32 * 4, 1));
  blob_put(b, "page/items", Rgba, pattern(32 * 32 * 4, 2));
  blob_put(b, "hit.ogg", Ogg, pattern(2048, 3));
}

/// A base path nothing else in this suite writes to.
fn base_for(name: text) -> text {
  d = temp_dir() ?? ".";
  "{d}/loft_assets_test_{name}"
}

fn clean(base: text) {
  for f in [meta_path(base), blobs_path(base),
            "{meta_path(base)}.dschema", "{blobs_path(base)}.dschema"] {
    if file(f).exists() { _ = delete(f); }
  }
}

fn test_a_written_pack_reads_back_identical() {
  base = base_for("roundtrip");
  clean(base);
  p = new_pack();
  b: hash<Blob[bl_key]> = [];
  fixture(p, b);
  assert(pack_write(p, b, base), "both halves written");

  q = new_pack();
  assert(pack_read(q, base), "metadata read back");
  page = q.pages["mobs"];
  assert(page != null, "page mobs present");
  assert(page.pi_w == 64 && page.pi_h == 32, "page shape");
  cell = q.cells["goblin"];
  assert(cell != null, "cell goblin present");
  assert(cell.ce_oy == 16.0, "the origin came back, not its default");

  qb: hash<Blob[bl_key]> = [];
  assert(store_load(qb, blobs_path(base)), "bulk half read back");
  out = qb["page/mobs"];
  src = b["page/mobs"];
  assert(out != null and src != null, "page pixels present");
  // A keyed read answers `Blob?`, so the bytes are `vector<u8>?`.  Every `?? []` in
  // this file sits behind a non-null assert on the line above it: the default is what
  // an already-failed test would have read, never a comparison of two empty vectors.
  assert(same_bytes(out.bl_bytes ?? [], src.bl_bytes ?? []), "pixels byte-identical");
  assert(out.bl_kind is Rgba, "the kind came back too");
  // Length alone cannot witness the bytes: two blobs of the same size differ here.
  items = qb["page/items"];
  clip = qb["hit.ogg"];
  assert(items != null and clip != null, "both fixture blobs are resident");
  assert(!same_bytes(items.bl_bytes ?? [], clip.bl_bytes ?? []),
         "the fixture's blobs are distinguishable at all");
  clean(base);
}

fn test_one_key_reads_without_the_rest_of_the_pack() {
  base = base_for("paged");
  clean(base);
  p = new_pack();
  b: hash<Blob[bl_key]> = [];
  fixture(p, b);
  assert(pack_write(p, b, base), "written");

  // The whole point of the bulk half: a reader starts EMPTY and ends holding only
  // what it asked for.  A loader that pulled the file would pass a value check and
  // fail this one.
  res: hash<Blob[bl_key]> = [];
  got = prefetch(res, blobs_path(base), ["hit.ogg"]);
  assert(got == 1, "the fetch cost one round trip");
  clip = res["hit.ogg"];
  assert(clip != null && len(clip.bl_bytes ?? []) == 2048, "the clip arrived whole");
  n = 0;
  for _r in res { n += 1; }
  assert(n == 1, "only the requested key is resident, not the two pages");

  // Asking again costs nothing — which is what lets a frame assert zero.
  assert(prefetch(res, blobs_path(base), ["hit.ogg"]) == 0, "a resident key is free");
  clean(base);
}

fn test_a_key_the_pack_does_not_have_answers_false() {
  base = base_for("absent");
  clean(base);
  p = new_pack();
  b: hash<Blob[bl_key]> = [];
  fixture(p, b);
  assert(pack_write(p, b, base), "written");

  // A paged reader that cannot say "absent" is one a caller has to guess about.
  res: hash<Blob[bl_key]> = [];
  assert(prefetch(res, blobs_path(base), ["no-such-blob"]) == 0, "nothing was fetched");
  assert(res["no-such-blob"] == null, "and no empty record was invented");
  clean(base);
}

fn test_one_base_drives_both_halves_as_a_url() {
  base = base_for("url");
  clean(base);
  p = new_pack();
  b: hash<Blob[bl_key]> = [];
  fixture(p, b);
  assert(pack_write(p, b, base), "written");

  // The plan's whole premise: develop against a file, ship against a CDN, with ONE
  // string changed.  `file://` is the scheme this suite can drive without a server,
  // and it takes the same branch an `http(s)://` base does.
  //
  // ⚠ The two halves do NOT accept the same spellings underneath: the metadata half
  // reads a `file://` URL and the paged half REFUSES it (a path or an http URL, and
  // nothing else).  A test that only read the metadata would pass while a game's
  // art silently loaded nothing.
  url = "file://{base}";
  q = new_pack();
  assert(pack_read(q, url), "the metadata half read through a URL");
  assert(q.pages["mobs"] != null, "and carries the same pages");

  res: hash<Blob[bl_key]> = [];
  got = prefetch(res, blobs_path(url), ["hit.ogg"]);
  assert(got == 1, "the bulk half fetched through the SAME base, got {got}");
  clip = res["hit.ogg"];
  fix = b["hit.ogg"];
  assert(clip != null and fix != null, "a record, not a refusal");
  assert(same_bytes(clip.bl_bytes ?? [], fix.bl_bytes ?? []), "the same bytes as the path read");
  clean(base);
}

fn test_the_two_halves_are_separate_files() {
  base = base_for("split");
  clean(base);
  p = new_pack();
  b: hash<Blob[bl_key]> = [];
  fixture(p, b);
  assert(pack_write(p, b, base), "written");
  // The split is the design, and it is observable: the metadata a boot load pays
  // for is a different file from the art, so it can stay small beside it.
  assert(file(meta_path(base)).exists(), "metadata half on disk");
  assert(file(blobs_path(base)).exists(), "bulk half on disk");
  assert(meta_path(base) != blobs_path(base), "two names, not one");
  clean(base);
}

// A second put under one key replaces the first: one blob, the newer bytes.
fn test_a_blob_put_twice_under_one_key_is_one_blob() {
  b: hash<Blob[bl_key]> = [];
  blob_put(b, "page/a", Bytes, [1 as u8]);
  blob_put(b, "page/a", Png, [2 as u8, 3 as u8]);
  blob_put(b, "page/b", Bytes, [9 as u8]);
  n = 0;
  for _x in b { n += 1; }
  assert(len(b) == 2 && n == 2, "two keys, two blobs: len {len(b)}, walk {n}");
  got = b["page/a"].bl_bytes ?? [];
  assert(len(got) == 2 && b["page/a"].bl_kind == Png, "the newer blob");
}

tests/page.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Building a page out of sprites: where each cell lands, what the texels between
// them are, and what happens to a tile that lies about its size.
//
// Every expected value here is hand-computed from the grid arithmetic rather than
// read off a run — a placer that agrees with itself is exactly the failure a
// self-comparison cannot see.
use assets::*;

/// `w * h` texels of one colour, as straight-alpha RGBA bytes.
fn solid(w: integer, h: integer, r: integer, g: integer, b: integer, a: integer) -> vector<u8> {
  out: vector<u8> = [];
  for _ in 0..(w * h) { out += [(r & 255) as u8, (g & 255) as u8, (b & 255) as u8, (a & 255) as u8]; }
  out
}

fn byte_at(px: const vector<u8>, w: integer, x: integer, y: integer, c: integer) -> integer {
  px[(y * w + x) * 4 + c]?
}

fn test_a_word_becomes_four_bytes_in_rgba_order() {
  // 0xAARRGGBB in, R G B A out — the order a texture page holds and the order a
  // decoded PNG arrives in.
  got = texels([0x804090C0, 0xFF000000]);
  assert(len(got) == 8, "two words, eight bytes, got {len(got)}");
  assert(got[0]? == 0x40 && got[1]? == 0x90 && got[2]? == 0xC0 && got[3]? == 0x80,
         "R G B A, got {got[0]?} {got[1]?} {got[2]?} {got[3]?}");
  assert(got[7]? == 255, "an opaque black stays opaque");
}

fn test_two_tiles_share_one_page() {
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  ok = page_grid(p, blobs, "atlas", "page/atlas",
                 [Tile { ti_key: "a", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 200, 10, 10, 255) },
                  Tile { ti_key: "b", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 10, 10, 200, 255) }], 2);
  assert(ok, "two well-formed tiles are placed");
  info = p.pages["atlas"];
  assert(info.pi_w == 4 && info.pi_h == 2, "two 2x2 cells side by side: 4x2, got {info.pi_w}x{info.pi_h}");
  assert(p.cells["a"].ce_x == 0 && p.cells["a"].ce_y == 0, "the first cell is at the origin");
  assert(p.cells["b"].ce_x == 2 && p.cells["b"].ce_y == 0, "the second is one grid step along");
  px = blobs["page/atlas"].bl_bytes?;
  assert(len(px) == 4 * 2 * 4, "the page is width*height*4 bytes, got {len(px)}");
  assert(byte_at(px, 4, 0, 1, 0) == 200, "a's second ROW is a's, not b's");
  assert(byte_at(px, 4, 2, 1, 2) == 200, "and b's second row is b's — the blit is row-major");
}

fn test_padding_puts_transparent_texels_between_cells() {
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  _ = page_grid(p, blobs, "atlas", "page/atlas",
                [Tile { ti_key: "a", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 200, 10, 10, 255) },
                 Tile { ti_key: "b", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 10, 10, 200, 255) }], 2, 1);
  info = p.pages["atlas"];
  assert(info.pi_w == 5, "2*(2+1)-1 = 5 — the gutter is BETWEEN cells, not after the last");
  assert(p.cells["b"].ce_x == 3, "b starts past the gutter, got {p.cells[\"b\"].ce_x}");
  px = blobs["page/atlas"].bl_bytes?;
  assert(byte_at(px, 5, 2, 0, 3) == 0, "the gutter column is transparent");
}

fn test_a_smaller_tile_keeps_its_own_rect() {
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  _ = page_grid(p, blobs, "atlas", "page/atlas",
                [Tile { ti_key: "big", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 200, 10, 10, 255) },
                 Tile { ti_key: "small", ti_w: 1, ti_h: 1, ti_px: solid(1, 1, 10, 200, 10, 255) }], 2);
  info = p.pages["atlas"];
  assert(info.pi_w == 4 && info.pi_h == 2, "the grid step is the WIDEST tile, got {info.pi_w}x{info.pi_h}");
  small = p.cells["small"];
  assert(small.ce_x == 2 && small.ce_w == 1 && small.ce_h == 1,
         "the cell is the sprite's own size, not the step's");
  px = blobs["page/atlas"].bl_bytes?;
  assert(byte_at(px, 4, 2, 0, 1) == 200, "the small tile landed");
  assert(byte_at(px, 4, 3, 0, 3) == 0, "and the rest of its cell stayed transparent");
}

fn test_the_grid_squares_itself_when_no_column_count_is_given() {
  // Three tiles: the smallest square-ish grid is 2 wide, so the third wraps.
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  _ = page_grid(p, blobs, "atlas", "page/atlas",
                [Tile { ti_key: "a", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 200, 10, 10, 255) },
                 Tile { ti_key: "b", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 10, 200, 10, 255) },
                 Tile { ti_key: "c", ti_w: 2, ti_h: 2, ti_px: solid(2, 2, 10, 10, 200, 255) }]);
  info = p.pages["atlas"];
  assert(info.pi_w == 4 && info.pi_h == 4, "2x2 cells of 2x2: 4x4, got {info.pi_w}x{info.pi_h}");
  assert(p.cells["c"].ce_x == 0 && p.cells["c"].ce_y == 2, "the third tile wraps to the next row");
  px = blobs["page/atlas"].bl_bytes?;
  assert(byte_at(px, 4, 0, 2, 2) == 200, "and its texels went with it");
  assert(byte_at(px, 4, 2, 2, 3) == 0, "the unused fourth cell stays transparent");
}

fn test_a_tile_that_lies_about_its_size_is_refused() {
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  ok = page_grid(p, blobs, "atlas", "page/atlas",
                 [Tile { ti_key: "t", ti_w: 2, ti_h: 2, ti_px: [1, 2, 3] }], 1);
  assert(!ok, "a truncated tile is refused");
  assert(len(p.pages) == 0 && len(p.cells) == 0, "and nothing is written");
  assert(page_grid(p, blobs, "atlas", "page/atlas", [], 1) == false, "so is an empty list");
}

fn test_a_proxy_is_derived_as_the_cell_lands() {
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  _ = page_grid(p, blobs, "atlas", "page/atlas",
                [Tile { ti_key: "solid", ti_w: 4, ti_h: 4, ti_px: solid(4, 4, 200, 10, 10, 255) },
                 Tile { ti_key: "clear", ti_w: 4, ti_h: 4, ti_px: solid(4, 4, 0, 0, 0, 0) }], 2);
  assert(p.cells["solid"] != null && p.cells["clear"] != null, "both tiles have a cell");
  solid_c = p.cells["solid"];
  clear_c = p.cells["clear"];
  assert(len(solid_c.ce_proxy?) > 0, "opaque art derives a proxy with no hand edit");
  area = 0.0;
  for r in solid_c.ce_proxy? { area += r.rw * r.rh; }
  assert(area == 16.0, "a full 4x4 square is covered exactly once, got {area}");
  assert(len(clear_c.ce_proxy?) == 0,
         "a transparent cell derives NOTHING — a box around it would collide with the air");
}

tests/proxy.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// @PLN146 F7 — the collision proxy derived from a sprite's own alpha.
//
// The contract has two halves and they pull against each other: the proxy must
// CONTAIN every opaque texel (so substituting it for the art can over-report a
// hit but never miss one), and its OVERSHOOT must be bounded (or a screen-sized
// box would satisfy containment perfectly and be useless).  Every test below is
// one of those two.
use assets::*;
use shapes::*;

// A blank RGBA page, `w * h` texels, every one fully transparent.
fn blank_page(bw: integer, bh: integer) -> vector<u8> {
  bp: vector<u8> = [];
  // `0 as u8` and not `0 as u8?`: a checked cast is required only where the value
  // could overflow the target width at run time, and a literal 0 cannot.  The
  // nullable form would put an `u8?` into a `vector<u8>`, where it becomes null.
  for _ in 0..(bw * bh * 4) { bp += [0 as u8]; }
  bp
}

// Paint one texel opaque (or to a given alpha).  The alpha is a `u8`, so its range is
// the parameter's type and not a cast at the store: `pu_a as u8?` put a nullable into a
// `vector<u8>`, where an alpha that did not fit became 0 in silence (loft#1660).
fn put(pu_px: &vector<u8>, pu_stride: integer, pu_x: integer, pu_y: integer,
       pu_a: u8) {
  pu_i = (pu_y * pu_stride + pu_x) * 4;
  pu_px[pu_i] = 200 as u8;
  pu_px[pu_i + 1] = 100 as u8;
  pu_px[pu_i + 2] = 50 as u8;
  pu_px[pu_i + 3] = pu_a;
}

fn a_cell(ac_w: integer, ac_h: integer) -> assets::Cell {
  assets::Cell{ce_key: "c", ce_page: "p", ce_x: 0, ce_y: 0, ce_w: ac_w, ce_h: ac_h}
}

fn a_page(ap_w: integer, ap_h: integer) -> assets::PageInfo {
  assets::PageInfo{pi_name: "p", pi_w: ap_w, pi_h: ap_h, pi_blob: "b"}
}

// Is (x, y) inside any box?  The containment check, written here rather than
// reused, so the test does not depend on the predicate it is checking.
fn covered(cv: const vector<shapes::Rect>, cx: integer, cy: integer) -> boolean {
  for r in cv {
    if (cx as float) >= r.rx && (cx as float) < r.rx + r.rw
      && (cy as float) >= r.ry && (cy as float) < r.ry + r.rh { return true }
  }
  false
}

// CONTAINMENT is the half that makes substitution safe: a system validated
// against the proxy stays valid when the art changes, because the proxy can only
// ever be bigger than the art.  Checked texel by texel on a diagonal — the shape
// a single box is worst at.
fn test_every_opaque_texel_is_inside_the_proxy() {
  px = blank_page(32, 32);
  for i in 0..32 { put(px, 32, i, i, 255); }
  page = a_page(32, 32);
  cell = a_cell(32, 32);
  boxes = assets::cell_proxy(cell, page, px);
  assert(len(boxes) > 1, "a diagonal derives more than one band");
  for i in 0..32 {
    assert(covered(boxes, i, i), "texel ({i},{i}) is inside the proxy");
  }
}

// And the BOUND is what stops containment being satisfied by a screen-sized box.
// A 32x32 diagonal is 32 opaque texels; its single tight box is 1024, which is
// +3100 %.  The banded proxy has to come in far under that, and under F7's +100 %.
fn test_the_proxy_is_bounded_where_a_single_box_is_not() {
  px = blank_page(32, 32);
  for i in 0..32 { put(px, 32, i, i, 255); }
  page = a_page(32, 32);
  cell = a_cell(32, 32);
  solid = assets::opaque_texels(px, page, cell);
  assert(solid == 32, "32 opaque texels");

  boxes = assets::cell_proxy(cell, page, px);
  over = assets::proxy_overshoot(boxes, solid);
  assert(over <= 1.0, "the derived proxy is inside F7's +100% bound, at {over}");

  // The single box it replaces is not, by a wide margin — that is the gain.
  one: vector<shapes::Rect> = [shapes::Rect{rx: 0.0, ry: 0.0, rw: 32.0, rh: 32.0}];
  assert(assets::proxy_overshoot(one, solid) > 30.0, "one box is +3100%");
}

// A cell with nothing opaque derives NOTHING, and an empty proxy hits nothing.
// A single box around an empty cell would collide with whatever stood there.
fn test_a_transparent_cell_derives_an_empty_proxy() {
  px = blank_page(16, 16);
  page = a_page(16, 16);
  cell = a_cell(16, 16);
  assert(assets::opaque_texels(px, page, cell) == 0, "nothing opaque");
  boxes = assets::cell_proxy(cell, page, px);
  assert(len(boxes) == 0, "and nothing derived");
  p = shapes::Proxy{boxes: boxes};
  assert(!p.proxy_hits(shapes::Rect{rx: 0.0, ry: 0.0, rw: 16.0, rh: 16.0}),
         "an empty proxy hits nothing");
  assert(assets::proxy_overshoot(boxes, 0) == 0.0, "and no art to be a multiple of");
}

// The alpha threshold is a threshold, not `== 255`: antialiased art has a fringe
// of partly-transparent texels that belong to the silhouette.  At or below
// PROXY_ALPHA_ON a texel is not art.
fn test_the_alpha_cutoff_decides_what_is_art() {
  px = blank_page(8, 8);
  put(px, 8, 1, 1, 255);   // solid
  put(px, 8, 6, 6, 4);     // below the cutoff — a stray almost-clear texel
  page = a_page(8, 8);
  cell = a_cell(8, 8);
  assert(assets::opaque_texels(px, page, cell) == 1, "only the solid one counts");
  boxes = assets::cell_proxy(cell, page, px);
  assert(covered(boxes, 1, 1), "the solid texel is in");
  assert(!covered(boxes, 6, 6), "the near-transparent one is not");
}

// The axis is chosen PER SPRITE, not fixed.  A wide, short bar bands badly by row
// (one band holds the whole bar) and well by column, so the derivation must run
// both and keep the smaller — 17 of 36 corpus sprites are tighter by column.
fn test_the_tighter_axis_is_chosen_per_sprite() {
  // A horizontal staircase: wide, only a few rows tall, with big empty corners.
  px = blank_page(32, 32);
  for i in 0..32 { put(px, 32, i, 10 + (i / 8), 255); }
  page = a_page(32, 32);
  cell = a_cell(32, 32);
  solid = assets::opaque_texels(px, page, cell);
  boxes = assets::cell_proxy(cell, page, px);
  for i in 0..32 { assert(covered(boxes, i, 10 + (i / 8)), "contains texel {i}"); }
  assert(assets::proxy_overshoot(boxes, solid) <= 1.0,
         "the chosen axis keeps it inside the bound");
}

tests/scene.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// What a pack can be ASKED: which sequence an action maps to, and which blobs a
// scene is about to draw from.
use assets::*;

/// A room with two goblins drawn from one page, a potion from another, and a light
/// that draws nothing — so every clause of `keys_near` has something to act on.
fn fixture(p: Pack) {
  p.pages += [PageInfo { pi_name: "mobs", pi_w: 64, pi_h: 32, pi_blob: "page/mobs" }];
  p.pages += [PageInfo { pi_name: "items", pi_w: 32, pi_h: 32, pi_blob: "page/items" }];
  p.cells += [Cell { ce_key: "goblin_0", ce_page: "mobs", ce_x: 0, ce_y: 0,
                     ce_w: 16, ce_h: 16 }];
  p.cells += [Cell { ce_key: "potion", ce_page: "items", ce_x: 0, ce_y: 0,
                     ce_w: 16, ce_h: 16 }];
  p.seqs += [Seq { sq_key: "goblin_walk_s", sq_page: "mobs", sq_first: 0,
                   sq_count: 4, sq_fps: 8.0, sq_cols: 4, sq_rows: 2 }];
  p.defs += [Def { df_name: "goblin", df_kind: Sprite, df_cell: "goblin_0",
                   df_w: 16.0, df_h: 16.0, df_layer: 1 }];
  p.defs += [Def { df_name: "potion", df_kind: Sprite, df_cell: "potion",
                   df_w: 16.0, df_h: 16.0, df_layer: 1 }];
  p.defs += [Def { df_name: "torch", df_kind: Light, df_radius: 96.0,
                   df_colour: 16755200, df_power: 1.25 }];
  p.anims += [Anim { an_key: "goblin/walk/0", an_def: "goblin", an_action: "walk",
                     an_facing: 0, an_seq: "goblin_walk_s" }];
  p.anims += [Anim { an_key: "goblin/walk/2", an_def: "goblin", an_action: "walk",
                     an_facing: 2, an_seq: "goblin_walk_n" }];
  p.scenes += [Scene { sc_name: "cave", sc_w: 640, sc_h: 360 }];
  p.placed += [Placed { pd_scene: "cave", pd_order: 0, pd_def: "goblin",
                        pd_x: 0.0, pd_y: 0.0 }];
  p.placed += [Placed { pd_scene: "cave", pd_order: 1, pd_def: "goblin",
                        pd_x: 10.0, pd_y: 0.0 }];
  p.placed += [Placed { pd_scene: "cave", pd_order: 2, pd_def: "potion",
                        pd_x: 500.0, pd_y: 0.0 }];
  p.placed += [Placed { pd_scene: "cave", pd_order: 3, pd_def: "torch",
                        pd_x: 5.0, pd_y: 0.0 }];
  p.scenes += [Scene { sc_name: "hall", sc_w: 320, sc_h: 200 }];
  p.placed += [Placed { pd_scene: "hall", pd_order: 0, pd_def: "potion",
                        pd_x: 0.0, pd_y: 0.0 }];
}

fn test_an_action_and_a_facing_name_a_sequence() {
  p = new_pack();
  fixture(p);
  assert(anim_of(p, "goblin", "walk", 0) == "goblin_walk_s", "facing 0");
  assert(anim_of(p, "goblin", "walk", 2) == "goblin_walk_n", "facing 2");
  // Absent answers EMPTY, not a plausible wrong sequence — a game that draws the
  // south walk for a swim it does not have looks correct and is not.
  assert(anim_of(p, "goblin", "swim", 0) == "", "an action it does not have");
  assert(anim_of(p, "goblin", "walk", 3) == "", "a facing it does not have");
  assert(anim_of(p, "nobody", "walk", 0) == "", "a definition it does not have");
}

fn test_a_scene_names_the_blobs_it_draws_from() {
  p = new_pack();
  fixture(p);
  all = keys_near(p, "cave", 0.0, 0.0, -1.0);
  // Two pages, not four instances: a page shared by two goblins is one fetch, and
  // the light draws nothing at all.
  assert(len(all) == 2, "the whole scene is two pages, got {len(all)}");
  assert(all[0] == "page/mobs" && all[1] == "page/items", "in placement order");

  // A radius takes only what is within it.  The potion is 500 away.
  near = keys_near(p, "cave", 0.0, 0.0, 100.0);
  assert(len(near) == 1 && near[0] == "page/mobs", "only the near page");

  // A scene nobody placed anything in wants nothing, and an unknown scene is not
  // an error — it is a scene with no instances.
  assert(len(keys_near(p, "hall", 0.0, 0.0, -1.0)) == 1, "the other room");
  assert(len(keys_near(p, "nowhere", 0.0, 0.0, -1.0)) == 0, "an unknown scene");
}

fn test_a_light_is_placed_but_never_fetched() {
  p = new_pack();
  fixture(p);
  // Centred ON the light, with a radius that also reaches both goblins (they stand
  // 5 away): the light is the NEAREST instance in the answer's window, so its
  // absence is about what it IS and not about where it is.
  torch = p.defs["torch"];
  assert(torch != null && torch.df_kind is Light, "the fixture placed a light");
  assert(torch.df_radius == 96.0, "with its own reach");
  keys = keys_near(p, "cave", 5.0, 0.0, 6.0);
  assert(len(keys) == 1, "the light contributed no blob, got {len(keys)}");
  assert(keys[0] == "page/mobs", "and the goblins beside it still did");
}

tests/worked-examples.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for `assets` (@PLN141): each `test_*` is a running demonstration of a
// contract the signatures cannot state, cited from the function it explains with
// `// Example: @PAK-00N`.
use assets::*;

fn base_for(name: text) -> text { "{temp_dir() ?? "."}/loft_assets_example_{name}" }

fn clean(base: text) {
  for f in [meta_path(base), blobs_path(base), "{meta_path(base)}.dschema", "{blobs_path(base)}.dschema"] {
    if file(f).exists() { _ = delete(f); }
  }
}

fn small_pack(p: Pack, blobs: &hash<Blob[bl_key]>) {
  p.pages += [PageInfo { pi_name: "mobs", pi_w: 4, pi_h: 2, pi_blob: "page/mobs" }];
  p.cells += [Cell { ce_key: "goblin", ce_page: "mobs", ce_x: 0, ce_y: 0, ce_w: 2, ce_h: 2 }];
  p.defs += [Def { df_name: "goblin", df_cell: "goblin" }];
  p.scenes += [Scene { sc_name: "hall", sc_w: 320, sc_h: 200 }];
  p.placed += [Placed { pd_scene: "hall", pd_order: 0, pd_def: "goblin", pd_x: 10.0, pd_y: 10.0 }];
  p.placed += [Placed { pd_scene: "hall", pd_order: 1, pd_def: "goblin", pd_x: 300.0, pd_y: 150.0 }];
  pixels: vector<u8> = [];
  for i in 0..4 * 2 * 4 { pixels += [(i * 7 % 251) as u8 ?? 0]; }
  blob_put(blobs, "page/mobs", Rgba, pixels);
}

// @PAK-001 — a pack is TWO files read two ways.  `pack_read` reads the metadata whole and
// brings back a page's shape without one texel; the bytes come out of the other file one key
// at a time, through `prefetch`.  A loader that looks for pixels in the metadata finds names.
fn test_a_pack_is_two_files_read_two_ways() {
  base = base_for("halves");
  clean(base);
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  small_pack(p, blobs);
  assert(pack_write(p, blobs, base), "written");
  q = new_pack();
  assert(pack_read(q, base), "the metadata, whole");
  assert(q.pages["mobs"].pi_w == 4 && q.pages["mobs"].pi_blob == "page/mobs", "the page's shape, and the KEY of its bytes");
  res: hash<Blob[bl_key]> = [];
  assert(prefetch(res, blobs_path(base), ["page/mobs"]) == 1, "the bytes, by key");
  bytes = res["page/mobs"].bl_bytes ?? [];
  assert(len(bytes) == 32, "4 x 2 texels x 4 bytes");
  clean(base);
}

// @PAK-002 — ask at a boundary, and a frame can assert zero.  `keys_near` answers each page
// once however many instances share it; `prefetch` answers the round trips it paid, and skips
// what is resident, so the second call answers 0.
fn test_prefetch_answers_the_round_trips_it_paid() {
  base = base_for("boundary");
  clean(base);
  p = new_pack();
  blobs: hash<Blob[bl_key]> = [];
  small_pack(p, blobs);
  assert(pack_write(p, blobs, base), "written");
  q = new_pack();
  assert(pack_read(q, base), "read");
  keys = keys_near(q, "hall", 0.0, 0.0, -1.0);
  assert(len(keys) == 1, "two instances, one shared page, one key");
  res: hash<Blob[bl_key]> = [];
  assert(prefetch(res, blobs_path(base), keys) == 1, "the boundary pays one fetch");
  assert(prefetch(res, blobs_path(base), keys) == 0, "a frame afterwards pays none");
  clean(base);
}

// @PAK-003 — an animation is a row of data, and a missing one answers EMPTY.  `anim_of` reads
// the `"{def}/{action}/{facing}"` row; an action the definition lacks answers "" rather than a
// plausible wrong sequence, so a game can tell "no such animation" from "this one".
fn test_a_missing_animation_answers_empty() {
  p = new_pack();
  p.anims += [Anim { an_key: "goblin/walk/2", an_def: "goblin", an_action: "walk", an_facing: 2, an_seq: "goblin_walk_n" }];
  assert(anim_of(p, "goblin", "walk", 2) == "goblin_walk_n", "the row answers its sequence");
  assert(anim_of(p, "goblin", "walk", 3) == "", "another facing is another row");
  assert(anim_of(p, "goblin", "jump", 2) == "", "an action the def lacks answers empty");
}