# Module (asset type)
A module is a reusable, stateless (or globally-stateful) Luau library
that other code pulls in with `require()`. Modules are the unit of
factored-out logic in a world: helpers, math libraries, format
parsers, registries, shared algorithms.
## When to use one
- You have logic you want to share across multiple components,
services, scenes, or tools.
- You have a registry, lookup table, or shared cache that callers all
read from / write to.
- You want a stable API surface (`M.fn`) other code can depend on.
If the logic is bound to one entity, use a `.component`. If you want a
named UI of fields rather than functions, use a `.preset`.
## Where it lives
- Source: `/zero/source/.../<name>.module/`
- Identity: the source path with `/` written as `.` and each segment's
type suffix dropped — `/zero/source/demo/beats.module` is
`demo.beats`, and `/zero/source/kit.package/beats.module` is
`kit.beats`, the container's name kept and its `.package` suffix
gone. `asset.create` returns this string, `asset.resolve(...).identity`
reads it back, and the `require` forms below write it as
`<identity>`.
- Folder shape:
- `init.luau` (or `init.lua`) — module body. **Required.** Returns
a table (conventionally named `M`).
- `README.md` — instance-level documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
## How to create one
```luau
asset.create("module", "<name>")
-- Creates: /zero/source/<name>.module/
-- init.luau (canonical `local M = {} … return M`)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source, and that subfolder
-- leads the identity: `{ folder = "lib" }` produces `lib.<name>`.
-- `into` authors it inside a resolved container (a package, a toolbox),
-- whose own identity leads the same way:
asset.create("module", "<name>", { folder = "lib" })
asset.create("module", "<name>", { into = asset.resolve("myPkg.package") })
```
## How it operates
1. **Registration.** Writing `init.luau` into a `.module/` folder
indexes the asset and registers the module path with the resolver.
2. **Loading.** The first `require("<identity>")` runs
the file's body and caches the returned table. Subsequent requires
from anywhere return the **same table** — this is the
shared-state lever for "module-local" registries.
3. **Hot reload.** Editing the file re-runs the module body and folds
what it returns into the table already loaded, so that table keeps
its identity: a caller that captured it with `require` reads the new
source's functions and values through the reference it already
holds, and a later `require` returns that same table. A key the new
source drops goes away; a key something set at runtime stays. The
body's own `local`s start again from their initial values, so state
that must outlive an edit lives in `modules.state()`
(`guides { path: "core/scripting-and-tasks" }`).
4. **Identity resolution.** A module is reached by its `<identity>`,
and where the caller lives decides which spelling of it resolves.
For a module in a **world**, from a caller in that same world —
another module, a component, a scene entrypoint, or an `execute`
chunk:
- `require("<identity>")` — the identity on its own, resolved
against the caller's own root. It carries every folder segment,
so a module created with `{ folder = "demo" }` is
`require("demo.beats")`.
- `require("@root::<identity>")` — the same root-relative
resolution, written out.
- `require(".sibling")` — a module in the same folder, by its bare
name; `..name` steps up one folder, `...name` two.
Logs, stack traces and hot-reload notices name that same module
`@local.source.<identity>` — its key under the world source root.
The `<identity>` tail of that key is the form above, so a trace
reading `@local.source.demo.beats` is `require("demo.beats")`.
For a module in a **library** — content under
`/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's
root from anywhere: `require("@builtin::modules.transform")`.
Mounting a world as a library moves its files under such a root and
flips the caller root to `@<lib>::`, which is what keeps the
root-relative forms above resolving across the move.
## Discovery
- `asset.list("module")` — every registered module.
- `asset.inspect("<name>")` — public functions, source path, this
type README.
- `ls /zero/source/<name>.module` lists the files the folder holds.
## Authoring conventions
- Return a single `M` table from `init.luau`. Top-level statements
with side effects run on first require — useful for one-time
initialization, dangerous if they touch the engine before it's
ready.
- Write exported functions as `typed function`, not `function`: the
argument types are enforced at the call and the checker knows the
signature, so a caller's mistake is reported where they made it.
- Annotate exported functions with `--!desc` / `--!arg` / `--!return`
/ `--!example` so the LSP, `tools.list`, and `asset.inspect`
surface them. A function with no `--!desc` is reachable only by
someone already reading this file.
- Declare a VALUE with `Field.<kind>(default, mode, description)`, and
a table whose members are reached through a metatable with
`--!members <TypeName>` — the latter gives the surface its accepted
member set, its type, and its documentation from one table. See
`guides { path: "core/authoring" }` for all four rules and
`modules/api/engine/entity.module/members.module` for a worked
member table.
- Keep module state in module-local upvalues or in `modules.state()`.
Globals leak across reloads. Module-locals start again from their
initial values with each hot reload; the table `modules.state()`
returns is the same table on both sides of every reload.
- Prefer focused modules over kitchen-sink modules. If two halves of a
module have no shared state, split them.
## Common pitfalls
- **Cyclic requires.** A `require` chain that loops will return the
partial table (the half built before the cycle was detected).
Design for one-way dependencies.
- **Engine-time side effects.** Don't `entity.spawn` at module
top-level — the engine may not be ready. Expose an `M.init` and
call it from a scene entrypoint.
- **Hot-reload + module-local cache.** A module that caches
expensive computation in upvalues loses that cache on reload —
fine for dev, but be aware in performance work.
- **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical.
- **Writing one while play runs.** A `/source` write made while play is
running lands on the play shadow, and a guarded play-exit discards it
unless it is kept. One `.module` write shadows `init.luau`, the
`README.md` and the `.metadata` together.
`vfs.write(path, bytes, { durable = true })` skips the shadow, and
`vfs.promotePlayShadow` keeps what is already on it — the `core/vfs`
guide has the model and what each route costs.
## Related types
- `.component` — for entity-bound state + lifecycle hooks.
- `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider.
- `.tool` — for a single agent-callable function with a YAML schema.
- `.package` — to group several modules + components + scenes into
one shippable folder.
## In the Inspector
Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
# profiler
Find where the frame time goes — autonomously, in any scenario, without a human reading a graph.
The engine's raw profiling surface is flat (`getProfilingDataTable`, `getLuauVmDataTable`, `profiler.stopCapture`). These tools compose it into the honest, self-accounting hierarchy a profiler should give you: every node shows its **self** time (own cost, excluding children), its **total**, call count, and share of the frame; the frame time no schedule covers shows as an explicit **present / idle** (vsync) bucket instead of vanishing; and each heavy component's `update(dt)` is attributed down to the exact **component + entity** so the cost lands on something you can open and fix.
The intended call site is the `use_tool` MCP surface — `use_tool { toolbox = "profiler", tool = "...", args = [...] }`.
## The tools
- **`summary`** — the engine's own one-line performance summary, the string that rides a tool response. A *different measurement* from everything below: this is the published smoothed frame, while the rest report the profiler's effective frame time (wall clock minus the cost of the call doing the asking). Read it when you want the figure other callers are being told; read `frame` when you want to know where the time goes.
- **`frame`** — the full attributed tree (self / total / calls / % per node). The deep view; read from the top row down.
- **`hotspots`** — a flat, ranked list of the costliest nodes by self time. The fast "what's slow" triage.
- **`scripts`** — per-component `update(dt)` attribution: last / avg / max / calls, by `component @ entity`. The "which of *my* content is slow" view.
- **`record`** — start / stop / status a background recording that spans a play session. Play across as many turns as you like, then stop to get avg / p50 / p90 / p99 / max frametime and the **spike-cluster distribution**. The path for "it hitches while I play", which a single live frame can't catch.
- **`ring`** + **`retro`** — the latency-immune path. `ring on` keeps an always-recording history of the last N seconds; `retro` reads it back AFTER the fact. Because the data is historical, it does not matter that a tool call lands seconds after the spike — the flaw that makes `frame`/live sampling unreliable for an agent (which can't control when its calls land).
- **`watch`** + **`hits`** — the conditional path. `watch <ceilingMs>` arms a frame-time watchdog that either records every bad frame (review with `hits`) or pauses gameplay the instant one hits (freeze + inspect with `retro`). Runs in the engine's per-frame hook, so it catches stalls an agent poll would miss.
- **`gpu`** — GPU pass timings from timestamp queries, label-aggregated (`compute.<shader>`, `scene.*`, `post.<effect>`, `feature.*`). The other side of the frame: when the CPU tree shows everything under **present / idle**, the frame is GPU-bound and this is the view that says which passes the GPU spends it on.
Everything an agent reads is on **effective** frame time — wall-clock minus the agent's own injected `execute` cost — so your profiling and authoring calls never masquerade as game hitches. Spike reports are **distributions** (every spike frame grouped by its dominant hotspot), so one capture shows the whole picture rather than a single anecdotal worst frame. The ring and watchdog are **editor-profile only** and off until enabled, so a shipped runtime never pays for them.
## The autonomous loop
1. `hotspots` or `frame` — baseline, what's expensive now (best when the scene is steady; live sampling races the ~seconds of tool-call latency).
2. For anything that hitches: `ring on` → play / test → `retro` — latency-immune, and the clustered distribution of every spike. Or `watch <ceilingMs>` in `pause` mode to freeze the exact bad frame.
3. `scripts` — attribute script cost to the component + entity.
4. Fix, then `retro` / `record` again and `compare`.
Every read tool accepts a recording label as its `source` — including a `retro` or `hits` snapshot — so anything you can see live you can also drill into for a recorded window's clusters, worst, or typical frame.
# MaterialAuthor
Stand up new materials and textures fast — the go-to surface for making materials at design time, each in one call instead of a hand-rolled asset-create-and-wire dance.
Every tool writes a source material asset and hands back a live `AssetRef<material>` you can apply straight to a Model. Wrap a texture (a proxy, a `.texture` asset, or an image path) into a new material with its PBR slot inferred from the texture name; make a flat-colour material from an RGB(A) value; clone an existing material under a new name with property or texture overrides; or scan a folder of textures — loose images and `.texture` assets — inferring each one's slot from its name and composing them into a single material. It pairs with the SVG → texture importer: drop a `.svg`, get a `.texture`, wrap it.
**This is authoring, not runtime.** Every tool creates source assets, which is a design-time action — never call it from gameplay code (components, controllers, per-frame updates, spawn logic). At runtime you consume an already-authored material by name or ref through the lower-level `Material` surface (create / apply / get / set), which is the gameplay-safe layer this one is built on. Reach here to *make* materials; reach for `Material` to *apply and tweak* them.
## Material properties
Beyond the keys a tool reads itself (`name`, `shader`, `overwrite`, and `slot` on `fromTexture`), every option names a property of the shader the material is authored against — so anything `pbr` declares is reachable in one call: `roughness`, `metallic`, `clearcoat`, `transmission`, `anisotropy`, `ior`, `uv_scale`, and the texture slots. A custom shader's own properties work the same way, since the vocabulary is read from the shader.
Spell a property in `snake_case` or `camelCase` (`uv_scale` or `uvScale`); `tiling` names `uv_scale` and `color` names `base_color`. A single number fills a vector property's components, so `tiling = 300` tiles 300×300. An option the shader doesn't declare is refused, naming the closest accepted spelling — the material is not written.
```lua
tools.use("MaterialAuthor", "fromTexture", "cliff_diff.texture",
{ name = "Cliff", tiling = 300, roughness = 0.9 })
```
## Slot inference
When no slot is given, the texture stem picks the PBR slot: `albedo` / `basecolor` / `base_color` / `diffuse` / `col` → base colour; `normal` / `nrm` → normal; `roughness` / `metallic` / `metalness` / `metallic_roughness` / `orm` → roughness-metallic; `emissive` / `emission` / `emit` → emissive; `ao` / `occlusion` → occlusion. Pass an explicit slot to override.
# api
Discover the engine's own documentation when you don't yet know the path — browse the docs tree and search it by keyword.
Browse the documentation under `/zero/docs`: with no argument it lists the top-level categories (api, tools, guides, components), a partial path lists a directory's entries, and a full path returns the document. Searching matches a keyword against entry names across the whole docs tree. When you already know the path, reading it directly is quicker; these are for finding your way when you don't.
# localContent
`/source/local/` is the **runtime write path**: where a running session puts data it produces and wants back later. A recorded gesture, a generated texture, a downloaded pack, a computed cache too big to be worth rebuilding — anything that does not fit the small JSON of `runtime_data` but should still be there after a restart.
Two properties define it, and they are the reason it exists:
- **It never syncs.** Nothing written there reaches a world, a manifest row, or another player. That is what keeps unbounded runtime output off the backend — there is no size cliff to police, because nothing here can reach one.
- **It survives the process.** The machine keeps it, per world and per user. It is also the only durable write a shipped runtime build has: that profile never leaves play, and a play-mode write anywhere else under `/source/` lands on the play shadow and dies with the process.
It is still ordinary source — an asset folder written there registers, and `asset.list` finds it — so a session reads its own cache the way it reads anything else.
## Which durable store to write to
Both survive a restart. They differ in how far the data travels, and that is the whole choice:
| | `runtime_data` | `/source/local/` |
|---|---|---|
| shape | JSON leaves, path-keyed, size-bounded | files and asset folders, any bytes |
| survives a restart | yes | yes |
| follows the user to another machine | **yes** — it replicates | **no** — this machine only |
| scope | per world, and per user for `runtime_data.player` | per world **and** per user, always |
So a player's settings, progress, and small state go in `runtime_data`, where a new device picks them up. Bulk that a device can produce again or does not need elsewhere — caches, recordings, generated assets — goes in `/source/local/`, where it costs the backend nothing. A fresh device starts with the first and without the second.
## Nothing outside it may reference into it
A world asset that depends on something under `/source/local/` resolves for exactly one machine and dangles for every collaborator and every player. The dependency is refused where it would be recorded: it is dropped from the `.refs` and replaced with an error-severity `dep.local_content` problem, which refuses the publish.
The other direction is free. Local content names world content as much as it likes, because the world has everything it names.
To make something local into something shared, promote it with `persist` and reference the copy that lands under `/source/`.
## The three tools
- **`list`** — every path held, with sizes and a total.
- **`persist`** — copy one path into the world, at the same relative path under `/source/` unless told otherwise. The local copy stays, so a failed publish never costs the only copy. Edit mode, because that is the mode whose writes persist.
- **`forget`** — stop holding a path. The file goes and the record goes with it, so the next boot does not restore it.
## The shape of the work
A world owner runs the head, the session writes what it produces, and later the same world is opened in an editor to decide what becomes part of the world:
```
zero localContent list
zero localContent persist gestures/wave.json
zero localContent forget cache/scratch-run
```
# lighting
The canonical scene-lighting toolbox — ambient light, directional sun, sky, clear color, and point / spot / area lights, each with modify-or-spawn semantics: every call leaves the scene in the requested lighting state, spawning the canonical `sun` / `ambient` entities (or switching the renderer-level sky) when the scene has none.
Set the whole environment in one call (sun, ambient, sky, and clear color, all optional), or set each independently: the directional sun, the ambient light, or the sky — swapped from a preset word, a project material matched by name, or a material ref. Add a point light as an entity at a position. Update any point, spot, or area light by name — its color, intensity, reach, shadows, and its shape controls (spot cone, area rect). Read the full lighting state back — every light with its values, clear color, authored sky, live sky config — in the same shape the whole-scene setup accepts.
These are the interactive authoring surface, for setting a scene's lighting from `execute()`. The behaviour lives in the runtime lighting capability (`modules.api.engine.lighting`); a component, entrypoint, or the scene saver that adjusts lighting at runtime uses that capability directly, not a tool.
# camera
Author and manage the cameras in a scene: create one, list every camera, read its full parameters, change them, aim it, frame a subject, or make it the primary camera. A physical camera on the machine (a webcam, a phone camera) is `cameraInput`, covered by the `topics/camera-input` guide.
## Create & inspect
- **create** — spawn a camera entity at a position, with any starting parameters in one call: fov, near/far, priority, render layers, a follow target, a behavior, a `lookAt`, and `target` (render-to-texture — `true` or `{ width, height, name }` renders the camera offscreen instead of to the viewport). Returns the new camera's full parameter record.
- **list** — every camera in the scene, enumerated from the live ECS so a just-spawned, hidden, or runtime camera is never missed, sorted by descending priority, each row marking whether it is the on-screen (`active`) camera, whether it is the `player` camera (the one the PlayerPrototype uses), and its main/editor role.
- **get** — the full parameter record for one camera (position, rotation, fov, near, far, priority, render layers, debug channel, render target, behavior, follow). Defaults to the active/main camera when you name none.
## Modify
- **set** — change any of these camera parameters in one call: `fov`, `near`, `far`, `priority`, `debugPass` (the diagnostic view this camera renders, by name), `debugChannel` (the same as a raw numeric channel), `renderLayers` (the layer-name spec, e.g. `"all !ui"`), `behavior` (a `cameraBehavior`-tagged component), and `follow` (the entity the behavior tracks). Pass no camera to modify the active one. A camera's render target is set when it is created (`create` with `opts.target`).
- **lookAt** — aim a camera at a world position or at another entity.
- **frame** — position and aim a camera to frame a target entity from a distance and angle in one step.
- **bindToPlayer** — make the joining player use a camera in play by binding it to the scene's PlayerPrototype: it re-parents the camera into the prototype (so it is cloned with the player) and sets the prototype's `camera` role to it. Pair it with a static behavior (`set { behavior = "@builtin::controller.menu" }`) for a fixed PS1-style camera the player moves within, or a follow behavior (`orbital_follow`, `third_person_follow`, `first_person`, …) for a tracking one. A player-less scene has no prototype — there the rendering camera is simply the highest-priority one, ordered with `set { priority = N }`.
## Related surfaces
- Capturing an image from a specific camera is the `capture` toolbox (`capture` with a camera, or `oneshot`) — it renders any camera's authored pose offscreen without changing what is on screen.
- Putting geometry ON a render layer is the `renderLayer` toolbox (`renderLayer.set`); `renderLayer.camera` is the layer-system-side way to set a camera's include filter, the same field `camera.set { renderLayers = ... }` writes.
- The scene's primary camera is normally spawned for you from the scene's PlayerPrototype; `camera.create` adds additional cameras (a security view, a cutscene angle, a render-texture feed).
# services
Generate real content — 3D meshes, textures, audio — from a prompt, by spending credits. This is the whole surface for it: discover what you can make, start a generation, track it, and find what you've made. You never touch the underlying asset type or task plumbing.
When a build needs a recognizable thing — a treasure chest, a snowboard, a character, a sound — you **generate** it. You don't assemble it from boxes and spheres.
## The loop
Discover what you can make: each service lists its operations, and each operation is self-describing: its inputs, the kind of asset it produces, and its credit cost. Check your credit balance if you want. Start a generation with a service and a prompt; this returns immediately with a handle and runs in the background, because a generation takes minutes and blocking would trip the execute time limit. When the run ends it posts a notice, `generation finished` naming its id and the job's `asset` (the spawnable asset path, which you can drop straight into a blocked-out slot) or `generation failed` naming its id and the error, and the notice reaches you on your next tool call; `services.status` reads a run's progress by its id meanwhile. Alongside the live per-session job view, a durable outputs record survives restarts by reading the provenance stamped on each generated asset.
## Describing your assets
`describe` is the one tool here that makes text, not content. It finds the assets in your world that lack the description their type asks for (for the built-in types that ask, a README, or the `about.yaml` texts of a type) and fills them in: `{ mode = "find" }` lists what is missing and what filling it costs, `{ mode = "all" }` fills it, and `{ assets = { ... } }` fills the assets you name. A call costs 1 credit and covers many assets, and `find` reports the number of calls before anything is spent. What it writes is an ordinary file in the asset, there for you to edit.
# show
Open a file, an asset or text you wrote in the editor so the user can read
or review it. Each call opens a new, closable editor panel beside whatever
the user has open, in the viewer they would get by navigating there
themselves: code and text in an editor, markdown rendered, an image drawn,
an asset with a preview (bundle, mesh, material, effect, avatar, …) rendered
live so they can orbit it.
- **file** — a VFS file, folder or asset (by path, name, identity or guid).
- **text** — text you produced: a write-up (`format = "md"`), code, or JSON.
A table is pretty-printed as JSON. The text is written to a scratch file
under `/zero/source/tmp/show/` and the result names it.
- **list** — the panels open now, and which ones hold unsaved edits.
- **close** — close one you opened.
**Show it, then work on it together.** A shown panel is editable where the
file is text and follows the file on disk: write the file and the user sees
the change in place; read the file back to see what they changed. When they
have unsaved edits and you write the file, their edits are kept and they are
asked whether to reload.
Needs the editor dock, so it answers in the windowed editor. When the dock
is not up, the error names the actual cause: a headless engine mounts no
editor shell, the runtime profile mounts none either, and a windowed editor
whose shell has not mounted yet says to call again in a moment.
# inputSim
Every simulated input, across every device a player might hold. Drive the
world as a keyboard-and-mouse player, as a phone, or as someone with a
controller — and read back what actually reached the game.
## Why all three, always
A world checked only from a keyboard is a world whose keyboard experience
is the only one anybody verified. That is how a game ships playable on a
desktop and dead on a phone: not because the touch path was written badly,
but because nothing ever drove it.
So this toolbox covers the three classes symmetrically, and `device` makes
a desktop session present as any of them.
## Driving the game: actions
A game reads its input through the controls its input maps declare —
`move`, `jump`, `sprint`. `action` drives one by name with the value the
game should read, which is how a test or an agent plays a game: the same
call works whatever device the player would hold, and nothing about a
device — which key a map binds, where the pointer is — has to be worked out.
```lua
tools.use("inputSim", "bindings") -- the live controls and their kinds
tools.use("inputSim", "action", "jump") -- tap a button
tools.use("inputSim", "action", "move", { 0, 1 }, 2) -- walk forward for 2 s
tools.use("inputSim", "action", "sprint", true, nil, true) -- hold until `release`
tools.use("inputSim", "fired") -- reported under class "driven"
```
A button takes `true`, a 1-D axis a number, a 2-D axis `{ x, y }`. The
value joins the control like another device: its shaping applies, its
press and release fire, and a context or a suppression that stands the
control down stands it down too. `release` lets go of every driven control.
The device verbs below prove the devices themselves: that a key, a pad or
the touch overlay reaches the control a player would press it for.
## The loop
```lua
tools.use("inputSim", "device", "touch") -- present as a phone
tools.use("inputSim", "bindings") -- what controls are live?
tools.use("inputSim", "layout") -- what is on screen?
tools.use("inputSim", "tapButton", "Jump") -- tap it where a player would
tools.use("inputSim", "fired") -- did it reach the game?
```
`bindings` first, always. Nothing is live until a component activates an
input map, so an empty list means no awake component asked for input —
which is a different problem from a control that fires and does nothing.
`fired` is what turns a simulated input into an assertion. A capture can
look identical whether or not the input landed; this cannot. Its `count` is
the number of engine frames the control was active, so a tap that holds a
button across two frames reads 2 for one press. To count presses, subscribe
with `control:onPressed`.
## One argument shape
Every verb here takes its arguments POSITIONALLY, in one order across the
whole toolbox: **what** first — the button, key, stick, trigger, label or
contact id — then **where** as flat `x, y` (or `dx, dy`), then **how long**
in seconds or **how far spread** in frames, then **which pad slot**. Every
optional argument trails. No verb wraps its arguments in an options table,
so a verb's shape is known from having called its neighbour.
```lua
tools.use("inputSim", "click", 0, 400, 300) -- what, where
tools.use("inputSim", "mouseDown", 0) -- the same what
tools.use("inputSim", "touch", 400, 300, 0.5) -- where, how long
tools.use("inputSim", "padStick", "left", 0, -1) -- what, where
```
Every surface that names arguments resolves those names against the same
positional list, so one call is written four ways and reaches the same
slots: `click(0, 400, 300)` in Luau, `{ button = 0, x = 400, y = 300 }`
through `tools.use`, `args` over `use_tool`, and `--button 0 --x 400 --y
300` in the shell. A verb whose every argument is optional takes `{}` for
"no arguments at all", and a name the verb does not declare is refused with
the ones it does.
## Who the input is for
What this toolbox simulates is the game's input: a key, click, motion, wheel
or touch reaches the scene alone. It reaches the scene whatever a person at
the window has focused — a panel they clicked, a terminal they are typing a
message into — and it reaches no UI widget, so a run driving the game never
types into the person's panel and the person's typing never stands the run
down. Every key, pointer and touch verb takes a trailing `target`:
`"game"`, the default, or `"ui"`, which reaches the UI the way a person's
device does. Driving a UI by its elements — clicking a button, typing into a
field — is the `gui` toolbox's work.
```lua
tools.use("inputSim", "key", "KeyW", 1) -- walks the player
tools.use("inputSim", "key", "KeyA", nil, "ui") -- types into the focused field
```
A mouse button is written either way, index or name: `0` / `"left"`,
`1` / `"right"`, `2` / `"middle"` — the same pair `Zin.state.mouseButtonDown`
reads. A pad button is always a canonical position name (`"south"`,
`"dpad_up"`, …), because a pad has no index a player could recognise.
## Where a point is measured from
`mouseMove` and `click` take their point in physical pixels, measured in
the space the input's receiver reads positions in, so `target` decides it.
With `target` `"game"` (the default) the point is in the space `capture`
reports: the scene's own pixels, measured from the top-left of the picture
a capture returns. That is the space `Zin.state.get().mouse_position` reads
a pointer back in and the space `worldToScreen` answers in, so a point read
off any of the three is a point the other two take.
```lua
tools.use("inputSim", "mouseMove", 300, 200)
Zin.state.get().mouse_position -- 300, 200
```
The window can hold the scene inside a rect with editor chrome above and
to the left of it; the offset is those two verbs' to carry, not the
caller's.
With `target` `"ui"` the point is on the window: the space every UI widget
is recorded in, wherever its screen draws (the window itself, or the
picture a Scene panel presents). A widget's `gui.bounds` rect is in logical
points, and times `ui.pixelRatio()` it is the point to aim at:
```lua
local b = tools.use("gui", "bounds", "play-button")
local r = ui.pixelRatio()
tools.use("inputSim", "click", 0, b.centerX * r, b.centerY * r, nil, "ui")
```
`gui.clickElement` does the same by id and refuses a widget the last frame
did not draw where its rect claims, so it is the call for pressing a named
button.
A `macro`'s ops are those same verbs, so an absolute point written for one
is the point the other takes.
`touch`, `touchDown` and `touchMove` place their contact on the window.
`camera.viewData().viewport` states where the scene's rect sits on that
window.
## Keyboard and mouse
| Tool | What it does |
|---|---|
| `key` | Tap a key — down, optional hold, up |
| `keyDown` / `keyUp` | Hold and release |
| `click` | A full click, optionally at a point |
| `mouseDown` / `mouseUp` | Hold and release a button |
| `mouseMove` | Move the cursor to a point; the delta is what look bindings read |
| `mouseMoveBy` | Move the pointer by a motion, over `steps` frames — repeat it to hold a look axis |
| `scroll` | Turn the wheel |
| `lockPointer` | Lock or release the pointer — mouse-look is usually gated on it |
`keyDown` and `mouseDown` hold until something releases them, and a hold
outlives the call that made it — including a driver task that was cancelled
mid-press. `held` says what is down; `release` puts all of it down at once.
## Touch
| Tool | What it does |
|---|---|
| `touch` | A whole tap at a point |
| `touchDown` / `touchMove` / `touchUp` | One finger, by contact id — several at once |
| `stick` | Push the on-screen movement stick |
| `drag` | Drag across the look zone, spread over frames |
| `pinch` | Pinch or spread two fingers |
| `tapButton` | Tap an on-screen button **by the label a player reads** |
`tapButton` aims at the button's real centre and raises when no live button
carries that label, listing the ones that do. That is deliberate: a tap
that silently lands on nothing looks exactly like a control that works.
## Gamepad
| Tool | What it does |
|---|---|
| `pad` / `padOff` | Connect or disconnect a simulated pad |
| `padPress` | Press a button — down, optional hold, up |
| `padDown` / `padUp` | Hold and release |
| `padStick` | Push a thumbstick |
| `padTrigger` | Squeeze a trigger to an analog travel |
Buttons are named by POSITION, never by the letter printed on them:
`south`, `east`, `west`, `north`, `left_shoulder`, `right_shoulder`,
`left_trigger`, `right_trigger`, `left_stick`, `right_stick`, `select`,
`start`, `guide`, `dpad_up`, `dpad_down`, `dpad_left`, `dpad_right`. The
same call works whatever pad is connected.
The name passed to `pad` decides which legends a prompt draws, so
connecting `"Sony DualSense Wireless Controller"` is how a scheme's
PlayStation prompts get checked.
## Reading back
| Tool | What it answers |
|---|---|
| `device` | Which class the session presents as — and forces one |
| `bindings` | Which controls are live, and which map contributed each |
| `layout` | What the on-screen controls are actually drawing |
| `fired` | Which controls were active since last asked, for how many engine frames, and from which device |
| `held` | What the session is holding down right now, across every device |
`layout` is how a phantom control is caught: a label on screen that no
entry in `bindings` explains is a button nothing consumes.
`held` is how a leftover hold is caught: keys, mouse buttons, fingers, the
stick and connected pads in one answer, with `atRest` true when the session
holds nothing. A control that keeps firing after a take ended reads here as
the key that is still down, so recovering from it is a `release` rather than
a guess at what was pressed.
## Replaying a sequence
A run of events worth repeating becomes a `.inputMacro` asset, so the
same sequence drives the same world on every later check.
| Tool | What it does |
|---|---|
| `recordMacro` | Capture real input for `duration` seconds |
| `saveMacro` | Write an event list to a `.inputMacro` asset |
| `macro` | Play an event list back through the input surface |
| `listMacros` | Every `.inputMacro` asset in the project |
An event is `{ t, op, arg }`. `op` names one of the verbs above that drives
input and `arg` carries that verb's arguments — the value itself when the
verb takes one, a table keyed by the verb's parameter names when it takes
several — so every device class a verb reaches is a device class a macro
reaches. A `touchDown` event's `arg.id` is the name its contact goes under,
and the `touchMove` and `touchUp` events carrying that `id` drive it.
```lua
tools.use("inputSim", "macro", {
{ t = 0.0, op = "pad" },
{ t = 0.4, op = "padDown", arg = "south" },
{ t = 0.6, op = "padUp", arg = "south" },
{ t = 0.8, op = "touchDown", arg = { x = 714, y = 984, id = 1 } },
{ t = 1.2, op = "touchUp", arg = 1 },
})
```
`recordMacro` reads what every device is HOLDING each frame — keys, mouse
buttons, pad buttons, the pad itself, fingers on the glass — and what each
one is MEASURING every `sampleInterval` seconds — mouse motion, the wheel,
thumbsticks, triggers, a sliding contact.
`release` is the reset between takes — it lifts every contact, drops the
on-screen stick, releases every held key and mouse button, and disconnects
every connected pad, so one take never leaks a held control into the next.
It reports how many of each it let go, and `held` reads the same set before
or after.
# cutscene
Drive the cutscene playing now — put one on screen without waiting for whatever
would have triggered it, land on an exact moment inside it, freeze it while you
work on the frame, and read where its clock is.
A cutscene is written to be watched from its start, which is the whole reason
this toolbox exists: reaching the shot at 0:48 by watching costs 48 seconds
every iteration, and `cutscene.seek 48` puts it on screen in one call.
## The loop this is for
```
cutscene.play mygame.scenes.intro -- put it on screen
cutscene.seek 6.5 -- land on the shot you are working on
cutscene.pause -- stop the clock and the scene; the frame stays put
… edit the cutscene module via the VFS …
cutscene.play mygame.scenes.intro -- play it again on the edit
capture -- look at the result
```
`cutscene.rate 0.25` runs a fast move slowly enough to read; `cutscene.rate -1`
runs it backwards.
## The tools
| Tool | What it does |
|---|---|
| `play <module> [paused]` | Play a cutscene from the module its definition is returned from. |
| `status` | Name, clock, shot, progress, paused, rate. |
| `seek <t>` | Land on an exact moment. The frame is re-derived, not fast-forwarded to. |
| `pause` / `resume` | Freeze the clock and start it again. |
| `rate <r>` | 1 real time, 0.25 slow motion, -1 backwards. |
| `skip` | Skip to the closing fade, the way a player does. Cues past here still fire. |
| `stop` | End it and give back the camera, the overlay and the player's controls. |
## Reaches the cutscene you are watching
The clock belongs to whichever scope started the cutscene — a scene entrypoint,
a `CutsceneTrigger`, an `execute`. These tools reach it through engine state
rather than through a module handle, so `cutscene.pause` freezes the cutscene on
your screen rather than a private copy of it.
## Requires play mode
A cutscene runs in play. In edit there is no clock to drive, and every tool here
says so rather than pretending it moved something.
# lib
Browse the engine's library content registry — what assets are registered, and whether a given one exists.
Two operations: list the available library assets (everything registered, or narrowed to a single asset type such as model, shader, material, or component), and check whether a library asset exists at a given path. Both hand back raw values — an asset-descriptor array for the listing, a boolean for the existence check.
# diagnostics
Ask the engine what's wrong with a scene — triage broken materials, missing assets, and component errors before guessing.
Three read-only inspectors over the active world's canonical state: the current component and entity errors (all of them, or narrowed to one entity), an at-a-glance summary of the scene (entity count, component-type breakdown, error count, cameras, culling stats), and a validation pass that checks scene integrity — broken materials, missing model assets, entity errors. Nothing here mutates the scene; it only reports.
# notices
Manage the notice keys that surface on your tool calls.
Notices are event records the engine and world content post so a side effect
you did not trigger yourself — a component disabled on an awake error, an
import that finished, a hot-reload that failed — reaches you on your next tool
call. Each notice is addressed to an audience: the caller whose work raised
it, a list of callers, or every caller. Every caller has its own read
position, so you are shown each notice addressed to you once, and reading it
removes nothing for anyone else. Most of the time you just read them. This
toolbox is for the case where one key repeats and you have already acted on
it: `suppress` mutes a key for you so it stops rendering, `unsuppress` brings
it back, and `list` shows the notices addressed to you and what you have
muted.
`list` first when a key is noisy: it prints each key exactly as you pass it to
`suppress`. A key you suppressed keeps counting while muted, so `list` also
tells you how many times it fired at you after you silenced it. Your mute is
your own — every other caller keeps receiving the key. Suppression lasts for
the session and resets when the engine restarts.
`drain` is for a caller that does not read notices off its tool results at all.
It hands back your unread notices as the same delivery text a tool result
would have carried, which is how a harness that composes its own context
collects a notice posted while nothing was mid-call — the case a tool result
cannot reach. If you read notices normally, you do not need it.
Notices are what the engine and the world tell you. To tell another agent or
a person something yourself, message them with the `messages` toolbox.
# capture
Render a screenshot of the scene from anywhere — an explicit position, a named camera station around an entity, a specific camera, or a mirror of the active viewport — and a grid of them in one image; or read back the frame the engine actually presented, exactly as the player saw it.
The picture of the machine's own camera (a webcam, a phone camera) is `cameraInput`, covered by the `topics/camera-input` guide; this toolbox photographs the scene.
Every source but `presented` and `window` (see [The presented frame](#the-presented-frame)) spawns an ephemeral offscreen camera, triggers one render, and returns the picture as an image value — returned from a tool, a probe or `execute`, it reaches you as a picture, with what the capture has to say about it (its size, the frame it is of, the cells of a grid) beside it. A PNG is written as well when you pass a `path`. The convenience wrappers render from a world-space position (with optional look-at), frame a target entity, mirror the active viewport camera including its layers, or render a specific camera by name from its exact authored pose and settings (`fromCamera`, with optional per-capture overrides that never touch the camera); they run as a background task that waits for the GPU readback and cleans up — which only works in play mode, since the task scheduler ticks there. The low-level oneshot spawns the camera, triggers one render, and returns the handles synchronously — the only path that works from edit mode, and the one the `capture` MCP tool is built on. A cleanup tool clears the capture output directory.
## Where a shot lands
A capture given a `path` writes the PNG there and says where those bytes
stand, beside the path it wrote: `durable` is true when the PNG is on the path
it names and the next session reads it back, and false when the play shadow
took it (live in this session and discarded on a guarded play-exit unless kept,
with `survivesRestart` saying whether a restart keeps it and `playShadow`
naming the routes that keep it) or when the path is in `/source/tmp/` scratch
(gone when the engine stops or restarts, with a `warning` opening `SCRATCH`).
Every capture carries it on the picture it returns as `durability`. Shooting
gameplay means shooting in play, which is exactly when a `/source` destination
is the shadow's, so this is the field that says whether a shot survives leaving
play — `vfs.promotePlayShadow(path)` keeps it with play still running.
## The presented frame
`source = "presented"` reads back the frame the engine presented to the
player instead of rendering one. Nothing is drawn for it: it is the frame the
viewport drew, copied out as it was finished, after the post chain, the
upscale, the display transform, the content UI and every post-scene pass, at
the size it was composed at. So it is the one capture whose temporal
antialiasing, motion blur, camera motion, auto exposure and render scale are
the real ones, and the source to verify any of them with. `source = "window"`
is the whole presented surface (the window, the browser canvas or the
headless framebuffer) with everything painted on the window: the editor's
chrome and every window-level UI pass. Where no Scene panel shows the
viewport, the two are the same image: the scene fills the window and the
editor's chrome is painted over it, and each such frame says so in its
`note` (a cell of a `presented` capture, a frame `renderer.presentedFrames`
returns) for as long as that chrome is on the window. Show the
Scene panel (the editor toolbox's `layout` tool) to read the game view alone.
`frames = n` (1-32) takes n presented frames, one every `interval` frames
(default 1: consecutive), at real gameplay speed, and lays them out as a grid
in the order they were presented. Each cell states what its frame was drawn
with: `frame` (the renderer's count, so consecutive frames carry consecutive
numbers), `rasterWidth` / `rasterHeight` (the size the scene rasterized at),
`compositeWidth` / `compositeHeight`, `renderScale` and `scaleMode`, `jitter`
(the projection's sub-pixel offset in raster pixels), `exposure` (what the
display transform multiplied the scene by) and `historyReset` (the viewport
camera's first frame, a cut, or a hand-over to another camera). A single frame
comes back at the size it was presented at; `width` / `height` resample it, or
size the sheet a grid is laid into.
A presented frame is drawn through the camera, layers, lens and chain that
drew the screen, so the options that aim, light or shape a rendered frame
(`camera`, `entity`, `position`, `viewpoint`, `fov`, `pass`, `renderLayers`,
`postProcessing`, `environment`, `mode = "collage"` and the collage axes) are
refused for it; aim the camera holding the viewport instead, or take an
offscreen capture. A source that renders refuses `frames` and `interval` the
same way, since they count presented frames. `deterministic` and `clearAir`
hold the clock and the air for the whole engine, so they reach the presented
frame too.
A diagnostic buffer of the viewport itself, under its real camera motion and
history, is read the same way: put the camera holding the viewport on that
pass (the camera toolbox's `set` with `debugPass = "motion_vectors"`) and take
`source = "presented"`. The viewport's velocity buffer is the frame's travel
against the frame before it, so a turning camera reads in it, where an
offscreen `pass = "motion_vectors"` holds the objects' own motion alone.
The `motion_vectors` picture is a visualization: each channel is the square
root of 100 times the stored displacement's size on its axis (R horizontal, G
vertical, B the length), the sign is dropped, and R reads 255 from
`width / 200` px of horizontal travel, G from `height / 200` px of vertical
travel (raster pixels). The rendering guide's "What the
`motion_vectors` picture encodes" states it in full.
The same frames, with their pixels as `TextureCpuHandle`s, come from
`renderer.presentedFrames({ scope = "viewport" | "window", frames, interval })`,
and `velocity = true` adds the viewport's velocity buffer from each frame as
numbers (`frame.velocity`, a `VelocityField`): each pixel's travel in pixels
per frame, x right and y down, signed and unclamped, read with
`field:at(x, y)`, `field:row(y)` and `field:region(rect)`.
## The feed
Every capture publishes the frame it made to `@builtin::tools.capture.feed`,
so a viewer can show the newest one without taking a shot of its own.
`Feed.latest()` is that frame: a `src` an image widget draws (the render
target's guid while the engine still holds it, the written path when the
capture produced a file), with the size, the source and the pass beside it.
`Feed.watch(fn)` calls `fn` as each new frame arrives. The editor's Capture
panel is built on it, and the `watch` layout puts that panel under the scene.
A viewer calls `Feed.shown()` to say it is drawing frames from the feed. From
then on the feed holds the newest capture's render target so the picture stays
up after the capture that made it is finished with it, and lets it go when a
later frame replaces it; `Feed.shown(false)` stops. With nobody drawing, a
capture's target is released the moment its capture is done, and nothing is
held.
## Viewpoints
A **viewpoint** names where the camera stands and which way is up in the image: `front`, `back`, `left`, `right`, `top`, `bottom`, `iso`. It replaces reverse-engineering a `{yaw, pitch}` pair, and names the seven stations that answer "is this built correctly".
`basis` says which axes the name is measured against, and it is **required** alongside a viewpoint when a subject is being framed:
- **`"local"`** — the subject's own axes. `front` is the side it faces however it is turned, and the frame is fitted to its own extents rather than to the world-axis box around them.
- **`"world"`** — the world axes. `front` is whatever faces world +Z.
For a crate turned 40 degrees those are different pictures, so the call says which one it wants. Framing a world position rather than an entity, both values mean the world axes and either is accepted.
## Passes
`pass` names **what** the frame holds, by name. `"final"` is the lit picture; a built-in diagnostic pass (`albedo`, `normal`, `depth`, `motion_vectors`, …) replaces the shaded image with the buffer behind it; and any capture **view** a render feature has published (`renderer.captureView.list()` enumerates them) is selectable by its own name. A name that is neither is refused against the list of what is registered.
The name carries two decisions: which channel the camera renders, and which render layers that channel is legible under. A diagnostic buffer is read flat, so the sky behind it and the post-process chain over it come off; a view that draws its own geometry declares the layers it needs and is rendered under them. Stating `renderLayers` answers that second question yourself, for the one call.
`passes` spreads the same vocabulary across the cells of a collage, and each cell reports the layers it was read under.
## Projection
`projection = "orthographic"` covers a fixed world-space height at every distance. Parallel edges stay parallel and two equal-size objects at different depths cover equal pixels, which is what makes it the projection to check a shape or compare sizes under — a perspective frame's convergence hides both. `orthoHeight` sets that height; omit it and it is fitted to what is being framed.
Any camera can be orthographic, not only a capture: it is a field on the `Camera` component. A capture through a camera the scene holds (`fromCamera`, `viewport`, and `source = "camera"`, `"screen"` or the scene camera) takes that camera's `projection` and `orthoHeight` with the rest of its lens, and its lighting `environment` with an `isolated` camera's background and ambient; a `projection` or `orthoHeight` stated for the call stands over the camera's.
## Which entity a capture framed
A capture names an entity, or a camera to render from, by its id or by its name.
A name exactly one entity carries means that entity. A name several entities
carry means none of them on its own, so the capture is refused, and the refusal
says how many entities carry it and names each by name and id; pass the id of
the one you mean. A player is drawn as its body, so a capture of a player
identity (an entity carrying `UserIdentity`, named `user`) frames the body its
`avatar` names. A player prototype is absent from the live scene while it
plays, so its name reaches no entity there: a capture of that name is refused,
and the refusal names each player spawned from the prototype, by its identity
and its body, with their ids.
Every cell of a capture aimed at an entity reports what it framed under
`subjects`: one `{ id, name }` per subject, carrying
`playerIdentity = { id, name }` when the picture is the body of the identity
the capture named.
## Isolate
Framing a subject fits the **frame** to it; it does not decide what is drawn in that
frame. Everything else standing there is still rendered — and a scene's default player
spawn sits at the world **origin**, which is exactly where a prop usually gets built, so
a tight shot of a new prop at the origin can come back showing the player instead.
`isolate = true` draws the subject without the other geometry. It changes exactly that — lighting, sky and post-process are untouched, so a final frame stays a final frame, and a flat read of the geometry is a `pass`. Excluded geometry still casts shadows onto the subject and still bounces light into it, the same as any render-layer exclusion. In a `setups` collage each cell shows the entity its own set-up frames alone, and a set-up can state `isolate` for its own cell; a set-up aimed from a position or a camera frames no entity and is refused when it is to be isolated.
## The tone the frame carries
Every capture reports `luma` on its cell record: the tone of the pixels it just
encoded, in code values on the 0-255 scale the image is delivered at.
```
luma = { pixels, min, max, mean, p1, p5, p50, p95, p99, span, spread, crushed, clipped }
```
`span` is `max - min` — the whole range, a single stray pixel included. `spread`
is `p95 - p5` — the range the body of the picture occupies, which is the reading
that answers whether a shot is legible. `crushed` and `clipped` are the shares of
the picture sitting at code 0 and at code 255.
A frame's mean cannot answer that question: half a picture at 40 and half at 200
carries the same mean as a flat field at 120, and only one of them has anything
in it. A subject can be modelled, shaded, lit and drawn and still arrive inside a
handful of code values, in which case the image looks like a wash and every other
field here reports success. `spread` is the number that says so.
The same reading is available anywhere pixels are: `cpu:tone()` on a
`TextureCpuHandle` — from `renderer.texture.readback(rtHandle)` on a capture
handle, from a render-to-texture camera's target, from a CPU canvas — and
`cpu:histogram()` under it, the per-code-value counts for luminance and for each
of the three channels, which every reading above is a sum over.
`cpu:histogram(x, y, w, h)` counts one rectangle of the picture alone, so a
reading per region of the frame — how much of each cell is dark, lit or a
given colour — is one call per region.
`cpu:flip(reference)` holds the picture against another of its size by NVIDIA
FLIP, the comparison a golden is judged by; `@builtin::modules.postMetrics`
carries it with the golden runner, the pixel metrics (banding, temporal
variance, halo overshoot, ring energy, hue drift) and the scopes, each
reading a presented frame or a capture result as it comes.
## The grade an offscreen frame carries
A capture spawns its own camera and renders offscreen, and the post-process
chain runs over that frame the way it runs over the viewport's — for a station,
for an orbit around an entity, for a named camera, for a render-to-texture
camera alike. Inside an effect, `engine.view_proj` / `engine.prev_view_proj` /
`engine.inv_view_proj` are the camera THAT render was drawn from and
`engine.resolution` is that target's own size, so a grade that reconstructs
world space from scene depth reconstructs against the station and lens the
capture asked for. That makes an offscreen capture an oracle for an authored
look: it photographs a chosen station and lens without taking the on-screen
camera away from whoever else is driving the scene, and moving that camera
somewhere else does not move the frame this one returns.
An offscreen render keeps no view history of its own, so `engine.prev_view_proj`
there holds that same matrix rather than the frame before it, and a pass taking
camera motion from the two reads none. On the viewport the pair is a frame
apart, so a camera's motion (and the blur and temporal reconstruction built on
it) is read from the presented frame: `source = "presented"`.
A render feature's passes reach an offscreen render the same way, against the
same camera — the `renderFeature` assetType README carries that half, including
`@frame.camera`, its own per-target camera input. `render(ctx)` itself runs once
a frame for the main viewport, so `ctx.viewport` is the window's size rather
than the captured target's; a feature sizes its scratch with `screen = true` and
lets the engine track the drawn target.
`postProcessing = false` takes the chain off the frame a capture returns, and
with it the film a render feature draws — the flare and grain, a short focus's
bokeh, the shutter's smear. It is a `Camera` property rather than a render
layer, so `renderLayers` does not reach it. A capture naming a diagnostic `pass`
drops the chain by default, so the buffer comes back as the value it encodes.
## Collage
`collage` renders a grid of frames into one image. What varies across its cells is what you pass — `setups` for a cell per whole camera set-up, `viewpoints` for a cell per view of one subject (or `"sides"` for all six), `passes` for a cell per render pass, `duration` for a cell per sample over time. A collage with no axis is refused rather than quietly sampling time.
`setups` is the shot list: each entry is written the way a single capture is aimed (`source`, `camera`, `entity`/`entities`, `position`, `lookAt`, `rotation`, `distance`, `angle`, `margin`, `viewpoint`, `basis`, `projection`, `orthoHeight`, `fov`, `near`, `far`), plus `label` to name the shot on the result, and a field an entry leaves out comes from the collage's own options. `setups` and `viewpoints` are one axis — both say where the camera stands — so a request names one of them; either crosses with `passes` into a 2D grid, a row per station and a column per pass.
The shot most of this exists for is one call: six sides of one object, each an orthographic elevation, nothing else in frame.
Authoring the cameras themselves — creating, listing, aiming, framing, and setting their parameters — is the `camera` toolbox.
# discovery
Content search in this engine. One question — `discovery.search { q, assetType,
match, mode, limit }` — answered in one shape, in one of two modes. `match`
belongs to keyword mode.
**Keyword** reads the files each asset type's `type.yaml` names under
`indexing.data` for the slot `match` selects: `identity` is the identity role
(a README, usually); `capability` is the capability, interface and
implementation roles together (code, guide bodies); `any` is either and `all`
is both. It matches the query's words against those files verbatim and each hit
says which role and file matched. It runs on this engine's own files, so
unpushed content and a private world's own content are in it.
**Semantic** is ZeroMind's ranking over what ZeroMind may embed: the builtin
library, the origin worlds of installed libraries and imported assets, and this
world itself when it is public. Say what you need in `q`, in plain words. The
search decides which asset types the words are about, and a judge reads every
candidate against the query; the hits it keeps come back best first. `match`
does not apply to it. Leave `assetType` out: naming one replaces the search's
own choice of types, so only the named types are searched and a type guessed
wrong answers nothing even when the library holds what you asked for. Name a
type only when the task fixes it. A private world's own content is out of its
reach, and the answer's note says what it searched and that keyword mode
searches the world's own files.
The two modes never fall back to each other; the answer names the other one
when it cannot answer.
**The library corpus.** `library = true` asks the semantic ranking over the
whole published library instead of the content this engine can reach
(`discovery.libraryScope()` names no world, so the request carries no world
filter). Each hit carries `local_` when this world already holds it. The same
question is then asked of this world's own content by keyword mode
(`keyword { own = true }`, which reads only assets `discovery.worldOwn` holds to
be this world's: not the builtin library, not an installed library, not a
dependency, not anything `world.installedAssets()` lists), and those matches are
appended after the library's hits (`discovery.withWorldMatches`): a hit the
library already returned stays where the library put it, every other one is
listed after, marked `from = "world"`. The library's hits keep the order the
search core gave them; no score is changed or combined. When ZeroMind cannot be
asked (`resting`, `unavailable`), the world's own matches still answer and the
note says the library was not searched and why. An engine bound to no world
answers as before. Keyword mode refuses `library`.
`discovery.searchGuides` asks the same question the way a `guides { query }`
call does: the documentation types `DOC_TYPES` names, `guide`, `agentskill`
and `assettype`. `discovery.searchTools` asks it the way a
`search_tools { query }` call does: the types `TOOL_TYPES` names, `tool`,
`toolbox` and `workflow`. Three surfaces mean three things (`guides` is the
docs, `search_tools` is what an agent runs, `discovery.search` is the content),
so a `guides` query answers out of the docs alone, and an answer holding no hit
carries `NO_DOCS_NOTE`, which names `discovery.search` as the surface that reads
modules, components, render features, shaders and the rest. Both MCP tools ask
with `library = true` whenever the engine is bound to a world.
Naming no `assetType`, keyword mode walks `discovery.guideTypes()` — the types
`discovery.types()` lists that `DOC_TYPES` names — and semantic mode asks
ZeroMind over them (`discovery.semanticKinds`, each type by the guid of its
`.assetType`) for `SEMANTIC_FETCH_MAX` hits (100, the most one call answers)
and keeps the first `limit` of those kinds (`discovery.onlyKinds`). Naming an `assetType`, it searches exactly those types; a
list naming `tool`, `toolbox` or `workflow` is refused with a sentence that points to
`search_tools`, and one naming any other type `discovery.types()` lists with a
sentence that points to `discovery.search`. `searchTools` refuses a documentation
type by pointing to `guides`, and a content type by pointing to
`discovery.search`. The hits stay in the search's own rank order.
## The answer
```
{
mode: "keyword" | "semantic", -- the mode that ran; absent on a refusal
match: "identity" | "capability" | "any" | "all", -- keyword answers only
from: "bound" | "library", -- the scope that ran
status: "answered" | "private_world" | "resting" | "unavailable" | "refused",
results: { { q, hits } }, -- one entry per query, in the order asked
world: { guid, title, private }, -- the world this ran from, when one is bound
searched: { { guid, title, why } }, -- the worlds it covered
note: string, -- the reach this answer had, and where else to look
scanned: number, -- keyword: the most assets one query read
cut: boolean, -- keyword: whether a walk stopped at KEYWORD_SCAN_MAX
}
```
`private` is stated once the world's visibility has been read. Semantic mode
reads it — through `discovery.visibility`, which asks ZeroMind once per world
and holds the answer — because it decides what ZeroMind is asked over. A keyword
answer and a refusal ask ZeroMind nothing: they name the world from what the
engine holds (`discovery.worldHere()`), so an unreachable backend never delays
them. `private_world` is the status of an answered semantic search from a
private world; a `resting` or `unavailable` one keeps that status and its
`retry_in_s` or `error`.
`note` is what an answered search says about its own reach.
`PRIVATE_WORLD_NOTE` is what a private world's semantic search covered and
which mode reads that world's own files; `ALL_KINDS_NOTE` is the semantic
search that named no kind, so ZeroMind ranked over every kind it holds and a
row can be of any of them; `NO_DOCS_NOTE` is the `guides` query the
documentation held nothing for, and names the surface that searches every other
kind. They stand in the one `note` when more than one applies, the
private-world sentence first — `status` reads `private_world` there, so the
sentence that status names opens what the answer carries.
`why` is `builtin`, `this world`, `library` or `import`. A semantic answer's
`searched` is the scope ZeroMind was asked over; a keyword answer's is the
corpus it actually read — the builtin library when builtin assets were in it,
this world when the world's own were. A `library` or `import` entry's `title`
is read from the install record that brought its guid into this world; an
install whose record carries none names that origin by guid alone.
A hit carries `guid`, `assetType` (the server's wire field `kind`, renamed where
a hit is read), `name`, `summary` — the first line of the asset's
README that is neither blank nor a heading — and `local_`, the form of the
asset this engine holds: `path`, plus `tool` for a tool (the identity a call
names, from the tool registry through `tools.callable`), `toolbox` for a tool
and for a toolbox (the name a drill-in lists the toolbox by), or `guide` for a
guide or an assetType (the identity `guides { path }` reads), `workflow` for a
workflow (the identity `workflow.start` runs), `skill` for an agent skill (the
identity `agent_skill` opens), and `readme`, the VFS path of the asset's README
when it holds one. A tool or toolbox hit also
carries `local_.registered`, which says whether the registry holds what the hit
names: the function a call to the tool would run (`tools.callable`), or the
toolbox a drill-in would list (`tools.registeredToolbox`). A `.tool` folder
whose source registered none has `tool = nil` and `registered = false`; a
toolbox folder the registry holds nothing for keeps its `toolbox` name and has
`registered = false`. `discovery.identityOf(hit)` is the one name the surface
acting on a tool-family hit takes: a tool's `<toolbox>.<tool>`, a toolbox's
drill-in name. A hit from keyword mode carries
`matched = { role, file, terms, words, together }`: the role and file that
carried the query's words best, how many of those words the asset carried
anywhere in its text, how many it carried as words of their own, and the most
it carried side by side in the query's order. A hit from semantic mode carries
`score`, ZeroMind's own rank for it, and what the search core stated: `p` (the
judge's probability, which is the order of a library answer), `type_key` and
`system_key` (keys into the result's `types` and `systems`, which a library
result carries beside its hits), `owning_world`, and `preview_description` when
the server sends one.
A semantic request names its types through `discovery.kindParam`: every
registered type by the guid of its `.assetType`, builtin types included, which
is the identity ZeroMind files a type under. A name no registered type answers
to travels as written, so ZeroMind can say which name it does not know.
`discovery.semanticKinds(names)` spells the ones of `names` that resolve. A
search that names no kind where its surface documents some — none of them
resolving — answers with `ALL_KINDS_NOTE`, so a caller reads the ranking's
reach off the answer rather than off the rows.
`discovery.shape(answer, "guides", query)` lists a guide or assetType hit with
its `assetType`, the `path` `guides { path }` reads and its `summary`; a hit of
any other type with its `assetType`, `name`, a one-line `purpose` and
`vfs_path`, the README `read_file` opens. `discovery.shape(answer, "tools")`
lists each hit with its `assetType` (tool, toolbox or workflow), its `identity`
(`discovery.identityOf`), its `toolbox`, a one-line `purpose` and `registered`;
a tool row also carries `tool`, the `use_tool` identity while the registry
holds it and empty while it does not. Every row of both forms carries `guid`,
`installed` (whether this world holds it), `use` (`discovery.nextCall`: the one
call that acts on it, `use_tool { toolbox, tool }`, `search_tools { toolbox }`,
the `workflow` toolbox's `start`, `guides { path }`, `agent_skill { name }` or
`read_file { path }` for a hit this world holds, `zeromind_install { guid }`
for one it does not), `p` when the search core judged it, and `from = "world"`
on a match from this world's own content.
## Ranking
Hits fall by `matched.weight`, what the query's words the asset carried as
words of their own weigh together: `hand` in "hand it over" and `spawn` in
"spawns" or "spawning" count, `off` inside "offline" does not (`matched.words`
is how many of them there were). A word weighs `ln((read + 1) / (carriers +
0.5))`, where `read` is how many assets the walk read and `carriers` how many of
them carry the word, so a word few assets carry outweighs several that nearly
every asset carries: in "roster who is here" the tool saying "roster" ranks above
the hundreds saying "who is here". Then by `matched.together`,
the most of the query's words the asset carried side by side in the query's
order (separated by spaces or hyphens), so "hand off" ranks above "hand" and
"off" apart. Then by `matched.terms`, how many of the words the asset carried
anywhere in its text, inside longer words included. Each word counts once
however many of the asset's files carry it. Then by the order the type declares
the role that matched in its `indexing.data`, then by name. `match: "all"` keeps only assets whose identity files and whose
capability files each carried at least one of the words.
A keyword answer's `results[i].total` is how many assets matched that query,
of which `hits` are the best `limit`.
## The frames a search takes
A keyword walk reads the files of up to `KEYWORD_SCAN_MAX` assets, which on a
cold cache is thousands of reads. Called from code that can yield (a task, an
`execute`, the engine shell, `search_tools`), it gives the frame back every
`SLICE_MS` (8 ms) of reading, so the engine keeps stepping and drawing while it
searches. `discovery.newSlice()` is that rule as a function, for any other walk
to call between its pieces. Called from code that cannot yield, the walk reads
everything within the one call.
## What it reads, what it holds, and what it reads twice
A keyword query reads the files the recipe names for every asset of every type
it covers. One copy of each file is kept — lowercased, which is what the match
reads, plus the one line a README answers for — against the checksum the asset
index reports for its asset (`asset.meta(ref).checksum`, the tree hash over the
asset's own files). So a second identical query reads nothing, and an asset
whose bytes changed is read again on the next query that reaches it.
What is held is bounded by `TEXT_BUDGET_BYTES`, **16 MiB**; past it the least
recently read asset's text is dropped, and a query that reaches it again reads
it again. `discovery.stats()` reports `reads`, the number of `vfs.read` calls
the module has made, and `bytes`, what it is holding right now.
`discovery.forget()` drops all of it.
Past the budget, the least recently read text is dropped until
`TEXT_KEEP_FRACTION` (three quarters) of it is held, so the ordering that picks
what goes is paid once per pass rather than once per read.
## How much one query reads
A keyword query reads at most `KEYWORD_SCAN_MAX` assets, **500**. The walk
takes this world's own assets first and the library's after them, since no
other search answers for what the world holds; within each, it takes the types'
assets in turn — the first of each type, then the second of each — so a bound
walk samples every type instead of reading the first few types whole. A
`tools.search` drilled into a toolbox (`{ toolbox = ... }`) walks the tools
under that toolbox's folder and no others (`within`). The answer states
`scanned`, how many it read, and `cut`, whether the types held more. The bound holds whatever the query names: `assetType`
narrows which assets the 500 are, so a type holding more than 500 is cut too.
`search_tools` asks for `tool,toolbox` (440 builtin assets), which fits inside
the bound on the builtin library. A `guides` query that names no `assetType`
covers `guideTypes()` — `guide` and `assettype`, 125 builtin assets — so its
walk reads both types whole. A name no registered type answers to is refused,
naming it and the types that surface reads, and so is a name whose type
declares no indexing recipe, since no search reads its assets;
`discovery.types()` is the whole list a `discovery.search` can read.
Measured on a native headless offline engine, `dev` profile (optimized +
debuginfo), Windows, `--dev` over the checkout, 2026-09-14. "Cold" follows
`discovery.forget()`; "warm" repeats the same query. Every row but the
fresh-engine one ran on an engine whose earlier walks had already read the
same files once, so its cold figure is the module's cost without the first
read of each file from disk. The builtin library holds
2,172 assets across the 42 types that declare a recipe and more than 46 MB of
lowercased text; `module` (15.6 MB) and `testSuite` (past the 16 MiB budget on
its own) are the largest.
| Query | Assets read | Cold | Warm |
|---|---|---|---|
| builtin, no `assetType`, no bound | 2,172 | 28,558 ms, 6,051 reads | 19,309 ms, 6,002 reads (the text passes the budget, so it is read again) |
| builtin, no `assetType`, bound 500, first query of a fresh engine | 500, cut | 8,328 ms, 1,283 reads | 159 ms, 0 reads |
| builtin, `tool,toolbox` | 440 | 383 ms, 1,224 reads | 38 ms, 0 reads |
| builtin, `guide,assettype` | 125 | 541 ms, 348 reads | 11 ms, 0 reads |
| builtin, `searchGuides` naming `guide`, after `forget()` | 76 | 436 ms, 152 reads | 34 ms, 0 reads |
| builtin + 1,500 world guides, no `assetType` | 500, cut | 8,320 ms, 1,192 reads | 136 ms, 0 reads |
| builtin + 1,500 world guides, `guide` | 500 of 1,576, cut | 607 ms, 1,000 reads | 27 ms, 0 reads |
The world guides were authored into the session's `/source` with
`asset.create("guide", name)`, each carrying a two-line `README.md` and
`guide.md`. What a read costs depends on the file: a guide's two small files
read in 0.6 to 1.6 ms each, while an `animation`, `font` or `texture` asset's
recipe files cost 17 to 42 ms each, which is why the bounded unfiltered walk — which reaches
every type — costs more per asset than a family walk.
# debug
Surface recent problems, inspect entity component state, and control the ambient debug-visualization overlay.
- **problems** — return the errors and warnings the engine has logged since your last call (contract violations, `awake`/`update` throws, auto-disabled components, unresolved requires, bridged renderer/asset errors). Advances a read-cursor, so repeated calls report only what is new. **When something you built does not behave, call this first.**
- **inspect** — component state for every entity matching a target (id, name, glob, proxy, or an array): script components as serialized public data, native ECS components as field values, or both — selected by `include`, filterable by `types`.
In the editor the overlay draws gizmos for the whole scene — mesh bounds, camera frustums, skeleton bones, physics colliders, icons for meshless entities (audio sources, cameras, lights), and mesh wireframes. It is ambient (no per-entity setup) and agent-driven:
- **set** — show or hide a category (`bounds`, `frustum`, `bones`, `colliders`, `icons`, `wireframe`), or `all`.
- **scope** — focus the overlay on specific entities (by name or id); call with nothing to clear the focus and draw for everything.
- **playSession** — show the overlay in play mode for a temporary, never-saved session, so bones/colliders/bounds stay visible on a running, animating scene.
- **state** — report which categories draw, the current scope, the play session, and the engine mode.
# viewport
The size the engine draws at — read it, and change it while the engine runs.
Everything a screen is laid out against comes from this one number pair: the
pixels `getViewportSize()` reports, the logical points `ui.screenSize()`
reports over them, each camera's aspect, and the raster a `capture` encodes.
Until an engine can be told to draw at another size, a layout can be proven at
exactly one shape per boot, and the second shape has to be read off a picture
taken at the first.
- **size** — what the engine is drawing at right now, in pixels and in the
logical points a UI screen is laid out in, with the factor between them and
the largest surface this device will draw.
- **setSize** — draw at another size. The surface is resized and every reader
of it follows within a frame, so the UI tree is *rebuilt* at the new shape
rather than scaled from the old one.
## Why a resized capture is not this
`capture { width, height }` re-runs layout at the size it encodes, so a shot
comes back looking like a genuine render of the other shape. It is not one. The
widget tree was built one frame earlier from the live screen size, so every
absolute number in it — font sizes, padding, a fixed `maxWidth` — was computed
against the old short edge, and any branch the tree took on the live size (a
one-column portrait layout against a two-column landscape one) is the branch
the old size chose. `setSize` changes the size the tree is built from, which is
the half a resized capture cannot reach.
## Who honours the size
A headless engine and a browser canvas own the surface they draw into, so the
size is honoured exactly. Where an OS window owns it, the window manager is
asked and has the last word — a tiled or maximized window keeps the size it
has. Either way `size` afterwards reports what was realized. A size larger than
the device draws is refused at the call, naming the bound.
## Two spaces, one surface
`size` reports the surface in pixels and in the logical points a widget is
positioned in, plus the ratio between them. The logical canvas is normalised to
about 1280 points wide, so a resize to a size of the SAME aspect moves the ratio
and leaves the logical shape where it was, while a resize that changes the
aspect changes the logical shape too. Both are real resizes; the second is the
one an orientation branch reacts to.
`sceneWidth`/`sceneHeight` is the rect the 3D scene is drawn into. It is the
whole surface in a runtime window, a browser canvas and a headless engine, and
the editor's viewport panel — smaller than the surface — in an editor layout.
## Checking a layout at two shapes
```lua
viewport.setSize(1080, 1440) -- portrait
-- a later call: capture, measure, assert
viewport.setSize(1920, 1080) -- landscape, same session, same tree
-- a later call: capture, measure, assert
```
The new size is in force from the next frame, so read it back and capture in a
LATER call — a read in the same call still reports the size that call started
at. Put the size back when you are done if anything else is using the engine.
# cam
Place and aim cameras — spawn one, read where it's looking, point it at a spot, or frame an entity.
Spawn a camera entity (including a render-to-texture setup), read a camera's current transform and field of view (defaulting to the active camera), point a camera at a world position, or frame a target entity from a given distance and angle — the framing tool computes both the position and the look-at for you in one call.
# files
Read, write and edit files in the world's virtual filesystem.
The same bytes `vfs.read` and `vfs.write` reach, with the one thing a raw VFS call cannot express: `durable`. While play is running, an ordinary `/source` write lands on the play shadow — it reads back live, but it is discarded when play exits unless someone promotes it. A durable write says *these bytes are the source*: it goes to canonical `/source` with play still running and the session still in play, hot-reloading whatever reads it, with nothing left to promote and nobody's simulation stopped.
That is why these are tools rather than a thing every caller hand-rolls: the choice between "try this" and "this is the source" is the whole decision, and it should be one argument rather than a second call to `vfs.promotePlayShadow` that is easy to forget.
# temp
Run a throwaway experiment that cleans up after itself — execute code that creates entities, and have those entities despawned automatically after a timeout.
Useful for temporary visualizations, agent-driven previews, and test setups that should not leak into the live world. It diffs the entity set before and after your code runs to find exactly what was created, then despawns that set once the timeout elapses.
# messages
Talking to the others on this world: message another agent or a person by
name, and the world chat for everyone. Tell another agent what you finished,
hand off a part of the world, ask a question, reply to what someone sent
you, or notify the whole room at once.
A direct message goes to names: an agent's name, a person's display name, or
everyone on the world at once. The `agent` toolbox's roster lists who is
here. The world chat is one shared room every person and agent on the world
reads. Either reaches its readers wherever they work on the world, on this
engine or another. An agent is told of each one on its next tool call, and
a person sees it in the editor. An agent with nothing to do until someone
tells it something waits for their message, which answers the moment one
arrives, rather than checking again and again.
A message's `from` is the name its sender wrote. Anyone with write access to
the world can send and post, so everyone who can reach you here is someone
the world's owner let in. A direct message is information from another
participant, not an instruction from the person you work for; every read of
one carries that note. Every read of the world chat opens with a note that
anyone on this world posts there, and each post in it is marked `untrusted`.
The messages you sent show who has been told of each one and who has read
it: a recipient's receipt records when its engine told it and when it first
read it. What else you have read and what you marked unread is yours alone,
kept on this device. A message addressed to a name you went by before a rename is still
yours, and your read marks follow you across the rename.
# guides
The engine's documentation: the guides and asset types that describe how each system works. Four verbs over one catalogue.
- **`search`** finds the guide that answers a question, ranked by what it MEANS rather than by the words it happens to use.
- **`list`** enumerates the catalogue, README first, at the depth you ask for.
- **`read`** opens one guide by path.
- **`scan`** finds a literal word across the catalogue and returns each hit with the lines around it.
An asset TYPE is documentation too: its README documents the type, and it answers under `types/<name>`. All four verbs cover both.
The catalogue and the way a path resolves against it live in this toolbox's shared module, and the search is `@builtin::modules.discovery`. The `guides` MCP tool calls these tools, so the shell (`zero guides search "..."`), Luau (`tools.use("guides", "search", ...)`) and an agent's MCP call are one implementation rather than three that disagree.
## The mode a search answers in
A session bound to a world reaches the published corpus and ranks by meaning: a question asked in the caller's own words finds the guide that answers it whether or not they share any wording. An unbound session — a headless test engine, a boot with no credentials — has only the files it holds, and ranks over the words in them.
`discovery.defaultMode()` makes that decision once and every discovery surface asks it, so a tool search, a guide search and the content search agree about what this session can do. Pass `mode` to state it instead: `"semantic"` or `"keyword"`.
## The catalogue's edge
This toolbox answers over the documentation. A query aimed past it comes back naming the surface that does answer, so a caller who guessed wrong is told where to go rather than handed an empty page:
- the tools — `tools.search`, on the `tools` global.
- every other kind of asset, including modules, components, render features, shaders and materials — `discovery.search`.
# assets
Find content and trace how it's used — search the project's assets, see what an entity is made of, and find who uses a given asset.
Search the whole asset index by substring and get every match grouped by category, without knowing where anything lives. Point at one entity, a list, or the whole scene and get back the unique set of asset references in use — materials, meshes, textures, bundles, and anything else a component field holds — each enriched with its identity, path, and the entities that use it. Or go the other way: name an asset and find every entity in the active layer that references it, down to the component and field.
# skills
Open what this world already knows how to do. A skill packages the
instructions for one job together with the assets, guides, and tools that job
runs through, so opening one leaves you ready to act rather than ready to go
looking.
Skills advertise themselves — every skill's name and one-line description
rides your tool responses. `list` gives the roster; `invoke` opens one;
`release` puts one down. The `agent_skill` tool reaches all three.
A skill with distinct branches carries subskills, addressed `parent/sub`.
They are reachable only through their parent, so a skill can go several
procedures deep while costing the roster a single line — `invoke` on the
parent names the ones available.
## A skill you open stays open
Opening a skill holds it. A held skill leads every advertisement with its own
line and the subskills under it, each marked opened or not, and it stays there
until you release it.
That is what carries a job across the calls it takes. A skill's later passes —
the validation after the build, the publish after the validation — come due
long after the roster line that leads to them stops looking relevant, because
by then the scene is built and a skill about building scenes reads as spent.
The held section keeps the passes you have left in front of you instead.
`release` is the other half: it says the job is finished and takes the lines
back. Releasing a parent releases the subskills opened under it.
Write a new skill by creating a `<name>.agentSkill/` folder anywhere under
`/zero/source/`, and it joins the roster as soon as it exists.
`guides { path = "types/agentSkill" }` is the authoring reference.
# gui
Build and drive on-screen UI in one call — stand up a screen, change what it shows, react to it, and read its state back.
Create and show a screen from a widget list in a single step, then show, hide, or toggle it. Update a widget's properties in place by id. Switch the active theme or list the registered themes. Drive interaction synthetically the way a test or an agent would: **press**, **type** and **set** use an element by its id — an editor panel's or the game's UI — without moving the pointer or anyone's keyboard focus; **clickElement**, **hoverElement** and **dragElement** aim the pointer at it; **click** fires a callback id; **scroll** turns the wheel. And introspect: read a screen's widget tree, list the available widget types, or get the property definitions for a given type. The inspection operations hand back tables; the rest act on the live UI.
# animation toolbox
Test-play animation clips on skinned bodies. `animation.play` is the quick way
to see a `.animation` clip on a character without authoring a component: it
resolves the clip and the entity, finds the SkinnedModel to drive, plays the
clip through the canonical AnimGraph, and tears everything down when the window
ends.
```lua
animation.play("A_Walk_F_Masc", "hero") -- play once, auto-cleanup
animation.play("Idle", "hero", { offset = "5s" }) -- wait 5s, then play
animation.play("Wave", "hero", { loop = true, duration = 8 }) -- loop for 8s
```
It plays while the world runs (play mode), the same as any component-driven
animation. Durable playback belongs on a `ClipPlayer` component; this tool is for
throwaway testing and leaves no component behind.
On a body that already animates (a ClipPlayer, the character controller) the clip
overrides for the window and the body's own animation resumes untouched
afterwards — the tool reuses the running graph and restores its output on cleanup.
# inputAuthor
Write control schemes whose every control already carries keyboard,
gamepad and touch.
## Why a toolbox for this
A binding must cover all three devices — that rule is what stops a world
shipping playable on a desktop and dead on a phone. A rule like that only
survives if obeying it is cheap; otherwise authors reach for a canonical
name that half-fits, or bind the keyboard and mean to come back.
So the catalog fills in the common controls. Ask for `jump` and get Space,
the south face button, and a lower-right touch button, already complete.
Ask for six controls and get a whole scheme in one call.
```lua
tools.use("inputAuthor", "map", "shooter",
{ "move", "look", "attack", "aim", "reload", "jump" })
```
Where each control lands is a judgement about the device rather than a
translation of the keyboard binding: `sprint` is a stick click on a pad
because that is where a thumb already rests, and a lower-right button on
touch for the same reason. `aim` is a trigger, not a face button.
## Overriding
A control is either a catalog name or a table naming it with overrides:
```lua
tools.use("inputAuthor", "map", "racer", {
"move",
"brake",
-- a sprint, under the name this game calls it
{ name = "boost", from = "sprint", label = "Nitro" },
{ name = "horn", -- unknown: supply all three
kind = "button",
kbm = '{ B.key("KeyH") }',
gamepad = '{ B.padButton("north") }',
touch = '{ B.touchButton({ zone = "right-lower" }) }' },
})
```
`from` is for the common case where a game's control IS a canonical one
under its own name — a racer's boost is a sprint, a shooter's fire is an
attack. It keeps the control named what the game calls it without retyping
three bindings.
A name the catalog does not know, and that does not say what it is based
on, needs all three classes from you. It cannot guess where a control
nobody has named before belongs on a pad or a phone screen, and guessing
would produce exactly the half-right layout this whole model exists to
prevent.
Nothing is written until every control resolves, so a scheme with one
unknown control names it rather than leaving half a map on disk.
## Tools
| Tool | What it does |
|---|---|
| `map` | Write a scheme: the map folder and every control it declares |
| `control` | Add one control to a scheme that already exists |
| `catalog` | List the controls that come pre-filled, and what each is bound to |
| `check` | Report every problem across a scheme's controls, without activating it |
`check` reports all faults at once and activates nothing, so a scheme can
be inspected before a world runs it. To drive the result on each device,
use the `inputSim` toolbox.
# appearance
Change how entities look across a whole hierarchy in one call — hand it a material, a mesh, or a shader and a scope, and it finds the right entities and fields and applies it.
The common situation is that you have a material and a rough idea of where the thing lives, and want the assignment done across the whole subtree at once. Batch-assign a material (by name or ref) to every material field under a scope, swap the mesh on the Model / SkinnedModel entities under a root, or swap a material's shader while preserving its texture links and property values by canonical role.
Targeting is schema-driven: for the material and shader operations, each targeted entity's components are enumerated and every component — builtin or user-authored — that declares a public material asset-ref field participates. The scope is one entity, a list, or the whole scene by default; options can stop at the first match or filter candidates by name pattern. This is the hierarchy-aware assignment layer — creating materials and editing registry-wide properties live on the lower-level `Material` surface, and per-ref edits live on the resolved asset refs themselves.
# zm
Version control for the world you are working in. Each tool is the git command of the same name and means what git means by it; the shell `zm <verb>` runs the same command.
Inspect the working state: `status` lists the staged, changed and untracked paths and the installed content that is ahead of its upstream, `log` the history of a revision (merged branches and pulled commits included), `revParse` the commit a revision names. Stage paths with `add` (`.` stages every changed path), then `commit` the staged tree — a commit takes only what is staged. `restore` takes working edits back to their staged or committed version, or unstages with `{ staged = true }`; `reset` moves the branch head in git's soft, mixed or hard mode; `stash` sets your changes aside and brings them back.
Branches: `branch` lists, creates, deletes and renames them; `checkout` switches the session's branch (your uncommitted changes come with you), takes one side of an unmerged path, or writes paths from a revision; `merge` merges a branch into the session's (or aborts the merge in progress). Synchronise with origin through `fetch`, `pull` and `push`. `trash` lists what destructive operations threw away, restores an entry by handle, or empties the trash.
Pull requests: `prCreate` sends your changes to installed content upstream; `prOpen`, `prList`, `prView`, `prConflicts` and `prMerge` propose and land a branch's work.
Accounts, worlds, installing and publishing content — the ZeroMind library, world metadata, ratings and reviews — are the `zeromind` toolbox.
# workflow
Run a workflow, and answer what it asks.
A workflow is a program that holds the shape of a job: its phases, its branches,
what runs at the same time as what, and what each step is given. It executes in
the engine and it decides all of that itself. You do not drive it — you answer
the questions it stops to ask, and it carries on.
That inversion is the point. A job with eight steps written as instructions
becomes eight things an agent has to remember at once, and by the last one the
first is gone. Written as a workflow, the shape lives in the program and no
agent has to hold it.
## The loop
workflow.start begin a run — returns immediately, the run is already going
workflow.next take the next request it is waiting on, claimed as you read it
workflow.renew keep a claim while its work goes on
workflow.answer hand back a result; the run resumes from where it parked
workflow.correct replace an answer the run has not read yet
workflow.fail say why a request cannot be answered
workflow.status what every run is doing, waiting on, and returned
A request that carries a `schema` takes only an answer of that shape:
`workflow.answer` refuses any other, naming the field and the shape it needs,
and the request stays yours to answer again. Give the schema to the subagent
with the prompt.
An answer is final once the run has read it, which it does within a moment
of it being given. `workflow.correct` replaces an answer the run has not read
yet, held to the same schema; once the run has read it, the call refuses and
says the answer was already consumed.
`workflow.answer`, `workflow.correct`, `workflow.fail` and `workflow.renew`
each refuse a request they cannot take by naming it and its state: an id no
run holds (with a pointer at `workflow.status`), an answer already given (and
whether the run has read it), a run that has ended, a request that failed or
was cancelled. The shell exits non-zero on a refusal.
A request nobody has taken announces itself on your tool responses until it
is taken, so there is nothing to remember and nothing to poll. A request you
have claimed is yours to work on in peace: your own calls keep it, and you are
told once when it is about to run out, and once if it does. A run that ends
says so once: a failed run names why, and every request it was still waiting
on is ended with it, so nothing is left to take or answer.
## Every request is a subagent's job
The prompt `workflow.next` hands over is written to be one agent's entire
task. It comes as a file, `prompt_path`, so a claim stays small however long
the job is: give a fresh subagent that file as its task, and answer with what
that subagent returns.
Doing the work yourself instead defeats the workflow: the whole run's context —
every phase, every result, every prompt — ends up in one conversation, which is
the exact failure a workflow exists to prevent. Your context should hold the
prompts you dispatched and the results you got back, and nothing else.
When several requests are waiting at once, the workflow opened them together
because they do not depend on each other. Dispatch them together.
## Tell the subagent how to find things
A request says what to do, not how to work here. Put that in the task you dispatch:
- **Open the skill for the job first.** `agent_skill` with no argument lists what this world knows
how to do. Skills advertise themselves on every tool response — one that covers the job is a job
somebody already worked out, and an agent that builds around it produces something the engine
never loads.
- **Then the tools.** `search_tools` finds one, `describe_tool` gives its exact signature,
`use_tool` runs it. `bash` is the engine's own shell, carrying the `zero` CLI over the same
toolboxes and `zm` for committing. `execute` runs Luau for what no tool covers.
- **What it builds is content the world loads**, not a script somebody runs by hand. A file that
only does something when executed manually is a note about the world rather than part of it.
Every one of those was learned by leaving it out: subagents given a plausible-sounding but wrong
discovery flow wrote loose scripts, never opened the skill describing their exact task, and never
had the shell.
## The subagents share one engine
Every subagent works in the same engine, the same world, the same Luau VM and
the same viewport as the others and as you. Nothing about running in parallel
gives any of them a private copy.
So each request owns its own files and writes nobody else's. None of them stops
or restarts the engine, clears the scene, or deletes what it did not create —
the run is not over when one step is, and the next step inherits whatever the
last one left. Anything a step starts in the background it also stops before it
returns, because a leftover driver loop makes the next step's measurements
wrong.
## A run outlives whoever started it, and the engine
Runs live in the engine, not in a session. Close the tab, lose the connection,
hand over to somebody else: the run is still parked exactly where it was, and
whoever asks `workflow.next` gets the request that was waiting. Each run keeps
its record under `/zero/source/local/workflow_runs/`, which this machine keeps
for the world and the user, so when the engine itself restarts on the world the
run comes back in the phase it had reached, with every answer it was given and
every claim under the same request id, and carries on. `workflow.status`
counts how many times an engine read it back as `restored`.
A claim is a lease. `workflow.next` states how long it holds (`lease_seconds`,
and `expires_in` for what is left), counted from the last tool call the session
that claimed it makes on the engine: a session working through one long turn
keeps its claims by working, whatever tools the work calls. `workflow.renew`
starts the lease over before a stretch with no engine calls, and every
`workflow.next` you make renews the claims you already hold. A claim whose
session makes no call for the length of the lease returns to the queue, so a
runner that disappears mid-request costs that request and not the run; its
holder takes it back with `workflow.renew` while nobody else has taken it.
# editor
What an editor action committed, and why it committed less than it was asked
for. A gizmo drag, a delete, a duplicate, a command dispatch and a selection
gesture each publish one record when they close; this toolbox reads them.
- **layout** — the editor's dock arrangement: which layouts can be applied,
and switching to one by name. Every layout is an `.editorLayout` asset: the
standard ones the editor ships (`watch`, `default`, `recording`, `agents`,
`minimal`) and the ones saved on the world through Window > Save layout.
With no name it lists them. `editor.layout { name = "recording" }` before
a recorded agent run puts the scene beside the agent's terminal, and
`editor.layout { name = "watch" }` puts the newest frame an agent captured
under the scene, which is the arrangement an editor comes up in.
- **observe** — the whole record: the last action of each kind, the history
behind them, and how many of each this session ran. Each record names its
outcome (`committed`, `partial`, `refused`, `cancelled`, `noop`) and, when it
is not a clean commit, one reason from a closed set.
**When something you moved did not go where you put it, call this.** The drag
record separates what the pointer asked for from what the gizmo applied after
snapping, and both from the transform the engine actually holds — read back from
the engine per entity, not recomputed from the drag. An entity the engine
refused, converted, or lost to a despawn mid-drag says so by name, and a press
that landed on a handle and started no drag at all says why it started none.
`editor.observe` also answers why a menu action did nothing: a command's record
tells a missing id from a disabled command from a broken enabled-predicate from
a body that raised, each with the error text.
The editor's own per-frame cost is named in the profiler alongside this:
`script.editor.gizmo.tick`, `script.editor.viewport.select`,
`script.editor.selection.highlight`, and `script.editor.panel.<id>` per dock
panel. Read them with `profiler.stats()`.
# renderLayer
Render layers end to end — put entities on a layer, choose which layers a camera or UI screen draws, and see what layers exist, all by layer name.
This is how a wall, prop or overlay stays out of a shot while it still
exists for lighting, shadows and physics. Everything here speaks layer
**names**; no caller ever builds a bitmask.
A render layer is a name: a shared coordination namespace. An entity is a
member of one or more layers; a camera renders a filter over them
(`"all !shell !ui"`). The two sides let geometry exist for lighting and
physics while staying out of a given shot — a near wall that still
occludes and bounces light in a bake, but does not block the view.
## Membership
- **set** — put one or many entities on named layers, optionally the
whole subtree.
- **get** — read which layers entities are on, optionally the whole
subtree.
Both report the extent of what they covered: `set` returns `moved` and
`skipped` per target, `get` returns `skipped` per entity read. A call on a
root of 46 children reports `moved = 1, skipped = 46` on its own and
`moved = 47, skipped = 0` with `tree = true`, so the size of the write is
in the reply.
## Visibility
- **camera** — set what a camera renders, as a name filter.
- **screen** — set which layers a UI screen draws into.
## Introspection
- **list** — every layer that exists, which entities are on each, and
which cameras include or exclude it.
Referencing a layer name that does not exist yet creates it. That means a
typo produces a real, empty layer that renders nothing and reports no
error — `list` is how you see that.
Scene layers (`wld.layers`) are a **different concept**: those are
additive scene composition, the mechanism for merging one scene's content
into another. These are render layers, which govern what draws.
# phys
Set up and control physics on entities — spawn bodies, attach and change them, join them, constrain them, and filter their collisions — each in one ergonomic call.
Spawn an entity with a Model and a dynamic body or a static collider in one step. On an existing entity, attach or strip a body and collider, or change a body's kind (dynamic, kinematic, static) at runtime while preserving its mass, colliders, and joints — the move behind ragdoll activation and freeze/unfreeze. Join two entities with a validated physics joint, or remove an entity's joints; either end of a joint may be a soft body, anchored on a region of its particles. Hold a region of a soft body to a rigid body (a flag's edge on its pole, a body shell on a car's chassis) with `attachSoftRegion`, and release it with `detachSoftRegion`. Add transform constraints (position, rotation, scale, look-at, parent, aim) that run every frame and work even on entities without a physics body, and remove them by index. Lock rotation or translation on chosen axes, ignore collisions between a specific pair, or set an entity's collision groups and filter. Each op adds a viz flash so the change is visible from any camera.
Two verbs read rather than write. `worldState` reports what the simulation is holding — bodies by type, awake and asleep counts, colliders, joints, contact pairs and points, gravity, the timestep and the last step's cost — and `whyStill` answers why the solver is not moving one body, naming the nearest cause from a closed set. Both read the solver rather than the `Physics` component, and both answer in edit mode as well as play mode.
# agent
Who you are on this world, and who else is here.
Other people and other agents may be working on the same world as you, each
agent under a name. An agent connected through ZeroMind is listed on every
engine of the world; one connected directly to an engine's own MCP port is
listed on that engine only. `whoami` gives the name you go by, your caller key
and the person you work for. `roster` lists everyone this engine knows is on
the world: each person, the engines they have open, and the agents working for
them with each one's name, harness and last call. `rename` sets the name you go
by — pick one that says what you are working on, so the people and agents
around you can tell your work apart from theirs and address notices to you by
it. Talking to the others on the world, by a direct message or in the world
chat, is the `messages` toolbox.
The `agent` block of every response's `state` carries the same answer: your
name, and who else is on the world whenever that changes.
# importers
See and drive the importer system — the pipeline that turns a raw source file written into the world (`.fbx`, `.glb`, `.png`, …) into its engine asset (`.bundle`, `.texture`, …).
Imports run asynchronously off the write signal, so this toolbox is the observability and control surface: `status` lists recent import jobs with their state (running / imported / failed / skipped / unclaimed) and errors, `explain` dry-runs the dispatch gates for a path and names exactly what would block an import right now, `run` imports a path synchronously and returns the produced asset, `await` blocks until an in-flight import for a path settles, and `list` enumerates the registered importers.
Start with `status` when "I wrote a file and nothing happened" — the job record carries the state, the winning importer, the produced output, or the error. If the path has no job at all, `explain` tells you which gate stopped it before a claimant ran.
# baking
Bake a scene's lighting so it stops being recomputed every frame. This is the optimization surface: turn static lighting into baked lightmaps, place irradiance light probes for the movers, place reflection probes for shiny surfaces, and detect what still needs baking — each in one call, with automatic placement and sane defaults.
The whole point is to reach optimized content without hand-placing anything. You do not scatter probes one by one or attach a component to every surface. You name a scope (or the whole scene) and the tool resolves the set, places what is needed, and bakes it.
## Mobility decides who bakes
`Model.mobility` ("auto" | "static" | "movable") is the single selection axis. Static geometry receives lightmaps and occludes baked light; movable geometry (and every player) samples probe volumes. "auto" derives the answer from the entity's components (physics bodies, skinned meshes, animation, movers). `baking.mobility` shows every derivation and batch-overrides the field where the derivation is wrong.
Static surfaces whose UVs tile or overlap cannot hold a per-texel lightmap; the lightmap pass binds them to the covering probe volume automatically and reports them as `probeLit`.
## The flow
1. Build the scene and set up its lighting (the `lighting` toolbox).
2. `baking.detect` — see what is unbaked and why (static surfaces with no lightmap, shadow-casting lights re-rendering every frame, movers with no probe coverage, no reflection probe).
3. `baking.all` — the whole GI pass in one call: an auto-placed irradiance probe volume, static lightmaps (tiled-UV surfaces routed to that probe field), and auto-placed reflection probes.
4. Verify: `baking.detect` again should come back clean.
## When to reach for each
- `baking.all` — the finish move. Run it once a static scene is dressed.
- `baking.mobility` — inspect or batch-set geometry mobility (the exclusion switch when a derivation is wrong).
- `baking.lightmaps` — bake static surfaces in a scope to per-surface lightmaps (full irradiance: direct + multi-bounce indirect).
- `baking.probes` — auto-place one irradiance probe volume over the region the lights actually reach, so moving entities pick up baked indirect light.
- `baking.reflections` — auto-place reflection probes across the scene so metallic/reflective surfaces mirror their surroundings.
- `baking.showProbes` — visualize the baked irradiance field with color-coded markers.
- `baking.detect` — the coverage check. What needs baking / re-baking and why.
- `baking.clear` — tear baked GI down to re-bake from a clean slate.
## Placement is automatic — do not hand-place
Light probes are placed from where light actually is: the tool gathers the scene's point/spot lights, bounds the union of their influence, and lays a grid over it. Reflection probes are spread across the scene's geometry bounds. You steer with typed knobs (`resolution`, `count`, `padding`, `samples`), not coordinates. Placing probes by hand, one at a time, is the pattern this toolbox exists to replace — a scene full of hand-dropped probes is unoptimized content that took more effort to make.
Baking is a static-content step: bake once the geometry and static lights are settled, and re-bake (or `baking.detect`) when they change. Things that genuinely move every frame stay realtime; everything else should end up baked.
# detail
What a world has resident of its detail right now, and why a chunk, tile, cell
or LOD level is not on screen.
Four systems decide how much of a world stands at any moment — terrain's LOD
cut, a voxel world's chunk meshes, spatial streaming's cell store, and mesh
LOD's level selection. `resident` reports all four in one reading: the totals,
the per-terrain LOD cut, every voxel chunk with what became of its mesh build,
every streaming cell with what it costs, and every LOD receiver with the level
it draws at.
`whyNotDrawn` answers about one piece: an entity, a voxel chunk, a mesh-LOD
level, or a streaming cell. The answer is one reason from a closed set with a
detail line naming what it is about — for a chunk whose mesh build the engine
dropped, the engine's own message.
Both read the systems' own state rather than the components' fields, so a voxel
world that stands with the right block count and draws nothing reads as what it
is.
# entityOps
Operate on scene entities — spawn, instantiate content, change state,
swap, and navigate the hierarchy. Every tool resolves targets by entity
**name or id** (single, a list, or the records `scene.find` returns) or
by a **`scene.find` query** (`{ name = { "wheel" } }`,
`{ component = { "Light" } }`, `{ under = { "car" } }`) that names a
selection without looking it up first, and asset inputs by a name / path
/ identity string that the tool resolves and reports back canonically —
so the caller never needs to know an asset's exact identity up front.
Finding and reading entities is the `scene` toolbox (`scene.find`,
`scene.tree`, `scene.summary`); this toolbox is the operating surface.
## Spawn + instantiate
- **spawn** — spawn entities from a template: name the components they
carry and their field values, plus a transform. Authored (serializing)
components only. Pass a `{ defaults, items }` list to spawn MANY in one
call with shared defaults and per-item overrides — a six-walled room is
one call, not six — validated whole before any entity is created.
- **fromAsset** — instantiate an existing asset (bundle / avatar / mesh /
material / texture, or anything with an `:instantiate()`): resolves a
name/path/identity string, dispatches by category, and reports the
canonical asset it hit. Controls the live `link` and `synced` flags.
## State + hierarchy
- **modify** — delete / enable / disable / hide / show / temporary /
reparent / rename, over one or many entities.
- **swap** — replace an entity with another entity that already exists in
the scene (moves it into the target's slot).
- **addComponents** — attach component(s) with setup data to one or many
entities; `create` spawns missing ones.
- **removeComponents** — detach component(s) from one or many entities,
reporting per entity what was `removed` and what was `absent`.
- **component** — inspect a component TYPE's schema: its public fields
(name + type + current value), methods, and lifecycle hooks — so you
set fields that exist instead of guessing. Reach for it before
`addComponents` / `spawn`.
- **roots** — find the root ancestor of one or many entities.
## Transform + copy
- **transform** — move, rotate and scale: absolute sets, relative
offset / rotate / scaleBy, and grid snapping. With no ops it is a pure
read of the current transform.
- **duplicate** — copy entities, one at a time or as a numbered run
stepped along an accumulating `offset`.
## Addressing a set you built
Every tool here resolves targets by **query** as well as by name or id, so
a set can be operated on without retaining a single id. Tag the set as you
build it, then address it by that tag:
```lua
-- Build the set in one call, tagged.
entityOps.spawn({
defaults = { components = { Model = { model = "cube" } },
attributes = { group = "roomA" } },
items = { { name = "wall_n", position = { 0, 1, -2 } },
{ name = "wall_s", position = { 0, 1, 2 } } },
})
local set = { attribute = { name = "group", value = "roomA" } }
entityOps.transform(set, { offset = { 0, 5, 0 } }) -- move the whole set
renderLayer.set(set, "shell") -- re-layer the whole set
entityOps.modify(set, { delete = true }) -- remove the whole set
```
The tag is an ordinary entity attribute, so it persists with the scene and
is as much a part of the content as a name is.
To see what is already present rather than what you made, `scene.find`
with no search axis returns the whole live population, and
`include = { "transform", "componentValues", "attributes" }` annotates
each hit — one call, no follow-up per entity.
# pp
Layer screen-space post-processing on the scene's final image — add, remove, toggle, and inspect effects.
Add an effect by naming a `.shader` asset — a shipped short name, an identity, a path, a guid, or a scene-authored `.shader` asset. Every property the shader declares is settable by its own name in the `add` call that registers the effect, and the shader's schema is what decides which option keys the call takes. A shipped effect may own several passes, drawn in order through its own scratch targets — the shader itself declares them, so `add` needs no pass-shaped option to register one. `postprocess.describe(name)` names that schema and the value each property holds once the effect is registered, and `asset.resolve(shader, "shader"):getProperties()` names it before. Every name `pp.presets` returns is one `add` accepts and one whose schema is readable at `@builtin::shaders.post.<name>`, the identity it returns beside each name. List the currently-active effects or every shipped short name with `pp.presets`, remove an effect by name, or enable and disable one without removing it.
# json
JSON encode/decode library for Luau. Encodes Lua values to JSON
strings and decodes JSON strings back to Lua values. Used for
communication with the Rust side of the engine, the VFS read/write
bridge, and any wire-format that needs JSON.
Pure Luau, no engine dependencies. Compact and pretty-printed
encoders, plus a hand-rolled decoder that streams the input by position
so it works under WASM as well as native.
## Exports
- `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`.
- `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly).
- `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array.
- `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure.
## Usage
```luau
local Json = require("@builtin::modules.json")
local widget = { type = "button", text = "Click Me" }
local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}'
local pretty = Json.encodePretty(widget, " ")
local decoded = Json.decode(compact)
local v, err = Json.decode("oops") -- v = nil, err = error message
```
## Notes
- Object keys are sorted alphabetically in both encoders for consistent
output across runs.
- Numeric keys on objects are stringified at encode time (JSON has no
numeric keys). Pure-integer key sets get detected as arrays via
`isArray` and encoded with brackets.
- NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round
trips through `decode` recover `null` (Lua `nil`), so they don't
preserve.
- Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe.
Only the BMP is covered; supplementary planes via surrogate pairs
are not.
- Functions encode as `null`.
- Decode is character-streamed — no regex, no `string.match` patterns
on the whole input — so the line-and-column information needs to be
reconstructed from the position offset.
# sc (deprecated)
> **Deprecated — superseded by the `entityOps` / `scene` / `appearance` toolboxes.** To spawn an entity that displays a mesh, use the entity API directly:
>
> ```luau
> local e = entity.spawn({ name = "name" })
> e.component.add("Model", { model = "<mesh>" }) -- model = a mesh name, asset.ref, or MeshHandle
> e.position = { x, y, z }
> ```
>
> The tools below remain for existing content that still calls them.
Scene-construction shortcuts — the operations you reach for constantly while authoring a scene: spawn things, find them, move them, tag them, and tear them down. Each tool composes several lower-level engine calls into one authoring step.
## Spawn primitives (entity + components in one call)
- **spawnModel** — an entity with a Model (or asset bundle) plus a Collider at a position; static physics by default.
- **spawnPhysics** — the same as spawnModel but dynamic physics.
- **spawnCamera** — an entity carrying a Camera component plus a nameplate gizmo.
- **spawnText** — a 3D text entity with a brief pop-in.
## Spawn layouts (batched placement)
- **spawnGrid** — N entities laid out in a grid on the ground plane.
- **spawnCircle** — N entities arranged around a circle.
- **spawnLine** — N entities along an arbitrary direction.
Each layout hands back the array of generated ids, so a placement feeds straight into the batch operations below.
Finding entities and reading the scene structure is the `scene` toolbox
(`scene.tree`, `scene.find`, `scene.findAll`, `scene.findByComponent`,
`scene.summary`); component-state inspection is the `debug` toolbox's
`inspect` tool.
## Transform, hierarchy, components, visuals — single or batch
Set position / rotation / scale, add a positional delta, or snap to grid steps. Parent or unparent one or many entities. Add or remove a component across a selection. Blend a colour tint, set an outline glow, or clear both. Every batch operation accepts one entity or many, each written as an id, a display name, or an entity proxy, and the record array that find returns pipes in as-is — the uniformity is what lets a query pipe straight into a mutation. A name or id matching no entity is refused, naming it.
## Despawn / scene
Despawn one or many entities, clear every non-camera entity in the active scene, or read a scene overview (root count, roots, active camera).
## Position arguments
The single-entity spawn tools accept a position as individual numbers, a vector table, or fields inside the options table. The layout tools take their origin through the options table.
# wld
Manage the live world while you work — flip between edit and play, and compose additive scene layers.
Flip into play mode — which snapshots the edit state, then starts scripts and physics — and back to edit, which restores that snapshot and stops scripts; query the current mode any time. Load a scene as an additive layer alongside the current one, unload it, or show and hide layers without losing their entities. List the worlds saved on disk, and read the current world state: name, mode, active layer, and available scenes. To list the loaded scene layers or the scenes available to load, use `scene.loadedLayers` and `scene.list`.
Saving and loading scene snapshots is the `scene` toolbox (`scene.save` / `scene.load` / `scene.list`).
# primitives
Spawn common procedural shapes with sensible defaults — the quick stand-ins and ground you drop while building a scene.
A visual cube at a position (no physics unless you add it), a flat ground plane scaled to size with static physics and an optional tint, a hexagonal ground tile with static physics, and a procedural tree from a preset (oak, pine, birch, willow, palm, baobab, dead, bush) with a static collider. Each spawns an entity backed by a builtin primitive mesh; the ground and tree variants wire in static physics so dynamic entities rest on them.
The primitive meshes these spawn are each centred on their own local origin, so a `scale` or `size` argument reads directly as the size the shape covers in the world. `types/mesh` lists each one's extent: the cube, sphere, cylinder and cone span one unit, and the capsule, torus and the two ground tiles carry their own.
# sceneAuthoring
The scene-change contract — the scene only ever changes by reviewing the pending changes and accepting them.
You build live — spawn entities, edit existing ones, create assets in play, register effects and UI — and the scene stays untouched until you review and accept. There is no silent save anywhere in this flow. A review is composed within the frame it is called in, so it describes that frame, and the session keeps running — on a world several agents share, one agent reading its own pending changes leaves every other agent working. A verdict reached later reconciles what it writes against the scene as it stands then. Accepting writes the reviewed items into the world's source; rejecting keeps them live for the session but never enters them into the scene. A review can be accepted or rejected whole, or narrowed to numbered items, entity names, name parts, or group labels.
The review reports, per numbered item:
- **added entities** — clustered so one spawn batch reads as one decision, with component shape, spatial span, and materials on the line.
- **updated entities** — field-level diffs against the scene.
- **assets the changes use** — play-created ones are saved into the world's source on accept; references with no backing asset are flagged as broken-on-reload.
- **edited source files**: code and assets modified during play, saved over the originals on accept. A folder asset is one item, its edited files kept or dropped together.
- **session effects and UI screens** — runtime-only registrations that will not come back on reload.
Accepting also saves the play-created assets the accepted entities reference — a scene must never point at files that vanish when play ends.
# opts
The contract an options table is held to. A call that reads a fixed set of
keys acts on those keys and no others, so a key it does not know names a
change the caller asked for and never got — a cube asked for `size = 3` that
comes back one unit wide, reporting success. `Opts.check` makes that a refusal
carrying what to write instead: the key, the accepted set, and the nearest
accepted spelling.
The same three parts answer for a name of any kind — `tools.use` names the
near-miss toolbox, `MaterialAuthor` names the near-miss shader property — so
the distance search lives here too, and every caller ranks candidates the same
way.
Pure Luau, no engine dependencies.
## Exports
- `Opts.check(where: string, value: any, accepted: { string }, inTable?: string)` — raise unless every key of `value` is named by `accepted`. `nil` accepts.
- `Opts.forward(where: string, own: { string }, fn, ...) -> ...` — call `fn(...)`, and when it refuses an option, append the vocabulary `where` itself takes.
- `Opts.unknown(value, accepted) -> { string }` — the keys `value` carries that `accepted` does not name, sorted.
- `Opts.nearest(key: string, candidates: { string }) -> string?` — the candidate `key` is most plausibly a misspelling of, or nil.
- `Opts.editDistance(a: string, b: string, limit: number) -> number` — single-character edits between two names, capped at `limit + 1`.
## Usage
```luau
local Opts = require("@builtin::modules.opts")
-- The keys this call reads, named once.
local CUBE_OPTS = { "animate", "scale" }
function M.cube(name, x, y, z, opts)
Opts.check("primitives.cube", opts, CUBE_OPTS)
-- primitives.cube: unknown option 'scail'. Accepted: animate, scale. Did you mean 'scale'?
-- The spawn target is handed to `entity.spawn`, so it is that call's
-- contract a key is checked against — `own` says what this one takes.
local e = Opts.forward("primitives.cube", { "opts.scale", "opts.animate" }, entity.spawn, name)
-- entity.spawn: unknown option 'size'. Valid options: name, hidden, …
-- (primitives.cube itself takes: opts.scale, opts.animate)
end
```
A call taking more than one table names which one the key was written in:
```luau
Opts.check("primitives.ground", color, { "r", "g", "b", "a" }, "color")
-- primitives.ground: unknown option 'red' in color. Accepted: r, g, b, a. Did you mean 'r'?
```
## Notes
- Refusals raise at level 0: the message is the whole error, so a caller reads
the contract rather than the line inside the library that checked it.
- A key of any type is reported by how it is written, so the positional form of
a named shape (`{ 200, 50, 50 }` for `{ r = , g = , b = }`) names its indices
rather than passing as a table with nothing the call reads.
- `nearest` is case-insensitive, and a candidate containing the key scores by
how much longer it is, so `roughnes` prefers `roughness` over
`clearcoat_roughness`. How far a plain misspelling may stray scales with the
key's own length — a four-letter key admits two edits, a longer one three —
so a short key is not answered with an unrelated short name.
- Equal scores go to the shorter candidate, then the alphabetically earlier
one, so the answer never depends on the order candidates were listed in.
- `forward` matches the phrase both this module and the engine's own option
checks use for the condition (`unknown option '<key>'`). Every other error
passes through as raised.
# logs
A toolbox. Its tools read what the engine logged.
Everything the engine records lands in one in-memory ring — Rust log lines,
bridged renderer and asset errors from third-party crates, Rust panics, and
every `print` and `log.*` a script made. These tools read it back from inside
the engine, on every platform — a browser has no host filesystem to reach for
and no console to scroll.
## The tools
- **search** — the one to reach for. Filters by severity, subsystem, substring
or regex, the script or entity that logged a line, and a time window. Can
return the lines around each match, or collapse a repeating message into a
single row.
- **tail** — the most recent lines of any level, oldest first. What is
happening right now.
- **errors** — recent errors and warnings, newest first.
- **summary** — how many lines, of what severity, from which subsystems and
entities, and which messages repeat. The shape of the noise before you go
looking inside it.
- **clear** — drops what is buffered. Lifetime counts survive.
## The ring as files
The same ring is mounted read-only on the VFS — the engine's own filesystem,
which is why these need no host disk behind them — and `cat` and `grep` work on
them like any other file:
| Path | What it holds |
|------|---------------|
| `/zero/runtime/logs/engine` | every level, most recent 500 lines |
| `/zero/runtime/logs/errors` | the ERROR-and-above subset of the same |
| `/zero/runtime/logs/script` | what scripts logged |
| `/zero/runtime/logs/previous` | the previous session of this world: how it ended, then the last lines it wrote |
The ring starts empty at every boot, so it cannot say why the engine before
this one stopped. `previous` can: its first line names how the previous
session of the world this engine booted on ended (a clean shutdown and its
reason, an exit with no reason recorded, or no exit record at all because the
process was killed or faulted), with the panic and the crash record that
explain an ending that was not clean, and the lines that session wrote follow.
Those lines are as the engine wrote them to its log file, each opening with its
`[seconds.millis]` stamp, where the live `engine` and `errors` files open each
line with its number in this session's ring. A boot whose previous session ended without a
clean shutdown also says so in a notice and a WARN line. Each engine on the machine keeps
its own record of each world, so an engine that comes back reads the session it ran last,
whatever other engines on the same world did meanwhile.
Neither surface is the right one — they answer different questions. A message
you can already name is quicker to grep than to query. A question with a shape
to it — this entity's lines, this severity in this window, what surrounded the
failure, which message repeats and how often — is what the tools are for, and a
file cannot answer it. The files also stop at 500 lines and include the record
of your own tool calls, so grepping for a marker string can return the call that
went looking for it; a query reaches the whole ring and leaves those out unless
`include_mcp` asks for them.
## Finding out why something misbehaved
The `problems` tool in the `debug` toolbox is the first call when something you
built does not behave: it drains the errors and warnings you have not read yet,
oldest first, so the root-cause failure reads ahead of the cascade it set off.
This toolbox is the general view over the same ring — every level, searchable,
countable, and filterable by origin and by caller. Reach for it when you need
more than the unread problems: a specific message, a specific entity, a
specific minute, one agent's failures on an engine several of them share, or
the lines that led up to a failure.
## What a line carries
| field | meaning |
|---|---|
| `seq` | monotonic, unique for the process lifetime |
| `level` | `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR` |
| `type` | subsystem — `RENDERER`, `SCRIPT`, `PHYSICS`, `UI`, … |
| `message` | the line itself |
| `timestamp` | epoch seconds |
| `origin` | `local`, or `remote` for a line raised by content another session on this world authored |
| `caller` | which caller's work raised it, as the key the caller answers by on the engine that raised it (`agent:<name>` while the caller holds a name, else `client:<session>` / `actor:<sub>`), on the lines a caller's work raised |
| `script` | the script that logged it, when a script did |
| `entity` | the entity that script was running on, when there was one |
| `context` | present only on a line returned as context around another line's match |
`script` and `entity` are why asking for one entity's output is a filter rather
than a guess at what its messages look like.
`caller` is the same move one axis over: one engine answers several agents at
once and runs all of their content, so its log holds all of their failures.
Pass `caller = "mine"` for your own lines, `"unowned"` for the engine's own,
or a caller key spelled out for somebody else's.
## Reading incrementally
**summary** reports a `cursor` — the highest sequence number the ring has
assigned. Read it before an action and hand it back to **search** as `since`
afterwards, and you see only what that action logged. `engine_cursor` is the
same figure with your own tool traffic left out, so it stands still on an
engine whose loop has stopped however many times you poll it.
## Your own tool traffic
Each tool call is itself recorded, with `type` of `MCP`. A search leaves those
out, because the call carrying the search is one of them and an unqualified
search for a marker would otherwise return the search. `include_mcp` sees them
alongside everything else; a `type` of `MCP` sees only them.
# deprecation (module)
Answers whether an asset is deprecated, why, and what replaces it. An asset
states it in its own `.metadata`:
```json
{ "deprecated": { "reason": "one line why", "replacedBy": "@builtin::assetTypes.effect" } }
```
The marker works for any asset, builtin or published. It is passed down:
- an asset is deprecated when a folder holding it at any depth (a package, a
toolbox, any folder asset) carries the marker, so marking a package covers
everything inside it;
- an asset is deprecated when its asset type carries the marker, so marking a
type covers every instance of it.
Assets stay resolvable and loadable by identity and guid. Search surfaces do
not index or offer them.
## Reading it
```luau
local deprecation = require("modules.deprecation")
local d = deprecation.of("@builtin::deprecated.deprecatedParticles.DeprecatedParticleEmitter")
if d then
print(d.via, d.reason, d.replacedBy) -- "container", "...", "@builtin::assetTypes.effect"
end
```
`deprecation.of(ref, memo?)` is the one function that answers. `via` says where
the marker is held: `"asset"`, `"container"` or `"type"`; `source` is the folder
holding it. Pass one `memo` table across the calls of a pass so each folder's
`.metadata` is read once.
`deprecation.indexed(ref, memo?)` is true unless the asset is deprecated; every
search surface asks it before it reads an asset.
`deprecation.warnOnce(handle)` logs one warning per asset per session naming the
asset, the reason and the replacement; `asset.resolve`, `asset.tryResolve` and
`asset.ref` call it. `deprecation.describe(subject, found)` renders the sentence
and `deprecation.forget()` resets what has warned.
`asset.deprecation(ref)` answers the same as `of` from the `asset` namespace.
# probe
Get told when something happens in the running world, without editing anything.
A probe attaches to code that is already running. You say what to watch, what
condition matters, and what to compute at that instant; you get a notice on a
later tool call saying it happened and carrying the value. You never wait, you
never guess a delay, and there is nothing to remember to undo — no source file
is touched, nothing reloads, and cancelling removes every trace.
This is what to reach for instead of adding a log line to a component and
reloading it, and instead of running a script, waiting, and capturing in the
hope the timing was right.
## The three things you can watch
| Tool | Fires when |
|---|---|
| `onCall` | A named function runs. Sees its arguments and what it returned. |
| `onLine` | Execution reaches a file and a line. Sees that frame's locals. |
| `onChange` | A value becomes different from what it was. |
And three to manage them: `list` (what is armed and what it has seen), `hits`
(the detail behind the notices), `cancel` (take one off, or all).
## What a probe tells you
Three parts, every time:
1. **Where it came from** — which probe, and what it is attached to. Written by
the probe, so several armed at once never blur together.
2. **What it is about** — your `notice` text. Required. A value with no idea
what it is about is not a report.
3. **What your body returned** — a string becomes the line, a table becomes the
values beside it.
```lua
probe.onCall("modules.combat.applyDamage", {
notice = "a hit killed something",
when = "ret.killed",
run = "return ret.target .. ' at ' .. tostring(ret.hp) .. 'hp'",
})
```
That reads back as its source, then "a hit killed something", then
`goblin at -40hp`. Repeat hits collapse into one line with a count and the
values that differed, which is why the text stays fixed and the value varies.
## Seeing the moment
Pass `capture` to have a hit bring back a picture as well: Luau, read against
the hit's context the way `run` is, that aims a capture tool at what the hit is
about and returns the picture.
```lua
probe.onCall("modules.combat.applyDamage", {
notice = "a hit killed something",
when = "ret.killed",
capture = "return tools.use('capture', 'fromEntity', { entity = args[1], angle = { 30, 20 } })",
freeze = true,
})
```
A capture waits for the renderer, so it runs as the hit is recorded rather
than inside the code the probe is attached to; the hit's notice goes out once
the picture is back and carries it under `shot`, delivered to you as a picture
beside the line, and the picture says which frame it is of. `probe.hits` holds
the same picture on the hit's row. On `onCall`, `freeze` also stops the world
at the hit. A capture that fails still reports the hit, with `captureError`
saying why.
## Picking a target
- A function on a required module — `onCall`, by `module.field`.
- A local inside a module, or a helper a component keeps to itself — `onLine`,
by chunk and line. This is the one that reaches code no name can address.
- A value: a component field, a light's intensity, a position — `onChange`.
Pair any of them with `when` to name the moment rather than every step toward
it. `when = "value >= 5"` fires the first time it reaches five.
A line probe's `when` is evaluated inside the frame it stopped in, so it reads
that frame's variables by their own names and with their own types —
`doubled >= 6`. The other two read the context: `ret.killed`, `value >= 5`.
## What it costs
- `onCall` costs one extra call on the wrapped function, and nothing anywhere
else.
- `onLine` costs nothing on an unarmed line, but a breakpoint drops the
enclosing function out of native code until that script reloads. On a
function that runs every frame that is real, so cancel it when done.
- `onChange` reads its expression once per frame while armed, which is also
its limit: something that changes and changes back within a frame is not a
change it can see. Probe the code that sets it with `onLine` instead.
Every probe carries a hit limit and a cooldown, both defaulted low. A probe
that reaches its limit disarms and says so; hits the cooldown held back are
counted and shown by `list`, so a busy probe never leaves you believing it
fired only as often as it spoke.
## Where it stops
Nothing about a probe is written to disk and nothing survives the engine
stopping. A body runs inside a protected call and must not yield; three errors
in a row disarm the probe and report why.
Globals are sealed read-only, so `onCall` cannot wrap one. A component's own
method is not a field on a module either — reach it with `onLine`.
# tests
Run the engine test suite from inside a live engine and triage the results — the baseline-then-regression loop, without leaving the session.
List every registered test suite (optionally with test counts) to see exactly what the runner and CI will execute. Run everything, one suite, or a subset — each suite runs through its own cooperative coroutine so multi-frame tests yield correctly — and get back structured results plus Markdown and JSON reports written to the VFS. Compare two JSON reports to surface the failures that are new since the baseline, which is how you catch regressions a change introduced. The lower-level framework for authoring suites lives in the test module and the `testSuite` asset type.
# worldValidation
Cargo-check-style validation for a Zero world. Walks every script and
every asset under `/source/` and reports problems in one structured
report. The world and its imported libraries are validated as cleanly
separated buckets so authoring problems in user content can be spotted
without being drowned out by issues in `@builtin` or any other
installed library — and vice versa.
The system is pure Luau on top of `vfs.*` and `asset.*`. It does not
spawn entities, allocate GPU resources, or mutate world state — the
report is read-only.
## Two-bucket separation
```
/source/ ← user-authored content (this world)
foo.component/
main.scene/
libs/
@builtin/ ← imported library: @builtin
...
@mylib/ ← imported library: @mylib
...
```
Everything under `/source/libs/<name>/` is treated as belonging to the
library `<name>`. Everything else under `/source/` is world content.
Reports always carry the bucket split so a quick `report.world.ok`
check tells you whether YOUR code is clean independent of library
health.
## Contents
- `validator.module/` — orchestrator, public API entry point
(`check`, `validateWorld`, `validateLibraries`, `validateLibrary`,
`filter`, `format`, `summary`, `saveReport`, `placeholders`).
- `scriptValidator.module/` — per-file Luau script validation.
Includes a placeholder syntax-check hook for the future when a
runtime parser is available.
- `assetValidator.module/` — asset-folder validation. Delegates to
`asset.validate`, the engine primitive the `world.push` gate runs,
and maps its problems into the report — structure, content
constraints, semantic hooks, and the private-subasset exemption all
behave exactly as they do at publish time.
- `vfsScanner.module/` — recursive VFS scanner that enumerates scripts
and asset roots under any path.
- `reportFormatter.module/` — filtering + formatting helpers
(`human`, `markdown`, `json`, `summary`).
- `tools/worldValidation.toolbox/` — agent-callable surface:
- `worldValidation.check` — full validation pass.
- `worldValidation.checkWorld` — world bucket only.
- `worldValidation.checkLibraries` — every imported library.
- `worldValidation.checkLibrary` — one library by name.
- `worldValidation.report` — generate a report with filtering /
formatting options; can write to a file.
- `worldValidation.summary` — one-line health summary.
## Portability
The package's library modules use the `~` package-relative require
form — drop the package under another library, rename the parent
directory, or install it into a world under a different identity
and every internal lookup keeps resolving. The package is also
self-contained: no file outside `worldValidation.package/` is
needed for the validator to run.
The toolbox at `tools/worldValidation.toolbox/` is the one
exception: code-mode tools are mounted under the synthetic
`@<lib>::tools.<ns>` namespace, which has no `.package` ancestor
in its chunkname, so `~` can't walk back to a package alias from
a tool source. Tools therefore use the absolute
`@builtin::systems.worldValidation.validator` form to reach the
validator module, matching the pattern other package-shipped
toolboxes (`characterController`, `volumeProbe`) already follow.
Asset validation goes through `asset.validate`, which resolves each
asset's registered type (built-in or world-authored) and its
`type.yaml` from the live registry — the validator hardcodes no VFS
paths.
## Quick start
```luau
local V = require("@builtin::~worldValidation.validator")
-- Full validation
local report = V.check()
if report.ok then
print("clean")
else
print(V.format(report, "human"))
end
-- World only, errors only, saved as markdown
local r = V.validateWorld({ severity = "error" })
V.saveReport(r, "/source/.validation/report.md", { format = "markdown" })
```
From an agent / tools:
```luau
tools.call("worldValidation.check", {})
tools.call("worldValidation.report", {
scope = "world",
severity = "error",
format = "markdown",
savePath = "/source/.validation/report.md",
})
tools.call("worldValidation.summary", {})
```
## Problem schema
Every issue surfaced by the validator follows the same shape:
```luau
{
severity = "error" | "warning" | "info",
category = "structure" | "syntax" | "semantic" | "reference"
| "schema" | "placeholder",
code = "<short.code>", -- e.g. "structure.missing_required_file"
path = "<vfs path>", -- where the problem lives
message = "<details>", -- one-line human reason
source = "world" | "library:<name>",
placeholder = boolean, -- true if produced by a placeholder check
}
```
## What's actually validated
| Layer | Implementation | Covers |
|---|---|---|
| Luau code | `lsp.check(path)` per script — the engine's embedded LSP | parser errors, type errors, every registered lint, including diagnostics inside asset folders (component / module / scene init.luau, etc.). |
| Asset folders | `asset.validate` per asset — the same primitive the `world.push` gate runs | missing required files / one_of_group alternatives (`error`), content constraints like an empty README or tag-less `.metadata` (`error`), unexpected entries when `allow_unlisted: false` (`warning` — the publish gate doesn't block on them), the type's own semantic `validate` hook, unknown/unresolved types (`warning`). Private subassets (a bundle's interior meshes / materials) are exempt from the describe-this-asset requirements, exactly as at publish time. |
## Notes
- The validator is read-only. It never writes to `/source/` except
when `saveReport` is invoked with an explicit destination path.
- Validation is single-frame, synchronous — no `await` boundaries.
- Hot reload: editing this package's modules reloads them on next
require. Running validation again picks up the new logic.
# zeromind
Find, reuse and publish content on ZeroMind — the shared library every Zero world draws from.
**Start here when a world needs something it does not have.** `zeromind.search` queries the library by what you need, in plain words, and chooses the asset types itself (leave `assetType` out unless the task fixes the type), or searches worlds only; `zeromind.inspect` reads the full record behind a hit — capabilities, schema, review scores, and who already depends on it — so you can vet it before committing; `zeromind.preview` shows exactly what an install would write without writing anything; `zeromind.installAsset` wires it in. Reusing published work beats authoring it from scratch, and this is the only way to find out what exists.
Installed content stays connected upstream: `zeromind.updates` lists the libraries and installed assets that have moved on, `zeromind.reviewUpdate` shows what one update changes (the commits it brings with their messages, then its files), and `zeromind.update` applies one, several or all of them. `zm status` says how far your copy has drifted (its "ahead of upstream" section), and `zm pr create` sends improvements back as a pull request.
The other half is your own work as other people see it. `zeromind.create` makes a world, `zeromind.list` shows the ones you can reach, `zeromind.swap` switches this engine over to one, and `zeromind.setMetadata` / `zeromind.setCover` decide how a world is presented in the library — its title, description, tags, visibility and cover image. `zeromind.worldInfo` reads that record back.
Community verbs act on the same guids a search returns: `vote`, `rate`, `review`, `comment`, `listComments`, `bookmark`, `follow`, and `report` for content that breaks the rules. `zeromind.issue` is different — it files a bug about the Zero platform itself, not about anything published.
Every call needs the engine linked to ZeroMind: `zeromind.login` starts the link, `zeromind.authStatus` reports it and the ZeroMind it points at, `zeromind.logout` revokes it and `zeromind.setTarget` points the engine at another ZeroMind. The credential stays in the trusted VM: a tool names the operation it wants and never holds a bearer.
Version control of the world you are working in — commits, branches, pull requests — is the `zm` toolbox.
# scene
Work with the live scene across its whole lifecycle: start one from a
template, understand its structure, reshape its player, clean it, and set
it as the world's entry point.
## The player model
A scene is either **playerless** (`"none"`) or **spawns a player per
joining user** (`"spawns"`). The player is defined by a **PlayerPrototype**
subtree — a body (the avatar a joining user becomes) and a camera rig. Zero
is like Roblox here: the default character is a socket a joining player's
own avatar replaces, so **reuse the canonical player setup rather than
hand-rolling one** — a divergent player breaks multiplayer. `scene.summary`
and `scene.tree` always report the scene's player intent, so you can see a
scene's type without knowing this tool exists.
## Start a scene
- **create** — create a NEW scene from a typed template (`empty`, `flat`,
`player`, `menu`) and load it. Omit the template to get the catalog of
what each contains. `opts.startup = true` also makes it the world's entry
scene. The starting point is a choice, not a scene handed to you silently.
- **setStartup** — make the currently active scene the world's startup
scene, so it survives world reloads. No argument, no clear.
## Understand a scene
- **tree** — the entity hierarchy as an indented tree, led by a one-line
player-state header. The quick "what is the current structure" view.
- **find** — the single entity-discovery tool over the whole live
population. Search by `name`, `component`, or `attribute` (each a list),
scope by hierarchy or scene, and add projections with `include`:
`transform` (`position`, `rotation`, `eulerAngles`, `scale` as arrays,
read `position[1]`), `componentValues` (each component's live field
values), `attributes`, `scriptComponents`. Every match carries
`components` (the component names) without one. Returns
`{ matches, total, truncated, searchedAxes }`.
- **summary**: entity + root counts, the top-level rows (at most `limit`, 20
when omitted), the active camera, the **player** setup (intent + counts +
the verb to change it), and the world's **startup** scene. `under` scopes
the counts and rows to the subtrees of named entities, and `fields` keeps
only the named fields, so `{ fields = { "entityCount" } }` answers the count
alone.
- **list** — the SCENES you can load, so you find one by name instead of
guessing. `scope` filters `world` (default, this world's own scenes),
`library` (builtin `@builtin::` templates / demos / canonical setups),
or `all`; each entry says whether it is currently loaded / active.
- **loadedLayers** — the scene LAYERS composing the live scene right now:
the root scene plus any additive overlays (editor UI, effect passes),
each with its backing scene, status, visibility, and additive flag. A
layer whose load produced failures reads `status = "partial"`.
## See what a load did, and what a scene costs
- **observe** — what every load did and what each loaded scene costs.
`lastLoad` is the last load's report: what it produced, the root it
replaced and the overlays that went with it, the entity counts on each
side, the milliseconds each phase took, and every failure with the
entity, component or lifecycle hook it is about. `layers` counts what
the engine attributes to each layer, `cost` is each layer's per-frame
entrypoint tick summed across the window `window` reports.
- **whyPartial** — why a layer is not whole: the nearest cause from a
closed set, the engine's own words for it, and the full failure list.
The answer to "the scene loaded but something in it is missing".
- **cost** — which loaded scene is eating the frame time, ranked by what
each layer's entrypoint tick costs. A scene entrypoint's per-frame
`update` / `editorUpdate` runs on the scheduler rather than as a
component, so `profiler.scripts` does not list it; this does. Pass
`true` to open a fresh window, let frames pass, then call again.
## Reshape the player
- **player** — read the setup, flip the typed intent between `"spawns"` and
`"none"`, and reshape: `body` adopts an entity or an avatar asset ref,
`camera` wires an entity carrying a Camera, `behavior` sets the player
camera's behavior to any `cameraBehavior` component. `"none"` removes the
prototype and spawns; `"spawns"` authors the canonical setup when missing.
- **spawns** — the spawn POINTS: where joining players arrive, as opposed to
`player`, which decides whether they arrive at all and what they become. No
argument reports every PlayerSpawn with its transform, the prototype it
instantiates (and whether that reference still resolves), and its team /
role / maxPlayers / spawnPolicy / placement. `"add"` places one, `"set"`
reconfigures one (omitted fields keep their value), `"remove"` deletes one;
`opts.spawn` picks WHICH when the scene has several. A spawn's transform is
where a body lands and its forward axis is the heading it faces — the
edit-mode overlay draws that ring, arrow and standing capsule.
## Change the content
- **clean** — clear the scene down to a protected keep-set. By default only
the player setup survives (never orphaned); `keep` protects a `scene.find`
selection (e.g. `{ component = { "Light" } }`); `keepPlayer = false` wipes
the player too; `dryRun` previews. Only the active scene layer is touched.
- **remove** — delete entities by name, id, array, or a `scene.find` query,
subtree-deep. Removing the default floor is `scene.remove("ground")`.
- **save** — save a loaded scene back to its own `scene.json`; a no-arg call
reuses the last scene name.
- **load** — load a scene snapshot into the live world.
- **reload** — re-run a loaded scene's build after editing its entrypoint.
`load` of an already-loaded, unchanged scene is an idempotent no-op; this
rebuilds it, in edit mode. Omit the name to reload the active scene.
A rebuild saves the scene, as a write to `build.luau` does, so `scene.json`
holds what the reload built and the next play loads it. A library scene
(under `/libs/`) is rebuilt in the live scene alone.
- **replace** — replace a loaded scene's content with another scene's
content, keeping the target's identity. Supports `dryRun`.
## Maintenance
- **migrate_v6_to_v7** — convert a v6 scene's `scene.json` to the v7 format
in place. User-initiated only.
World mode (play/edit) and additive layers are `wld`; accepting or
rejecting play-mode changes is `sceneAuthoring`.
# assetType (asset type)
The `assetType` is the **type of types** — the meta-type that every
`<typename>.assetType/` folder is an instance of, including itself. A
`<typename>.assetType/` folder declares a new asset type called
`<typename>`: its on-disk shape, its identity rule, and the structural
contract instances of the type must satisfy.
This type is self-hosting. `assetType.assetType/` conforms to its own
schema — a `type.yaml` plus a `README.md` — so the type system is
defined in terms of itself rather than a hidden engine special-case
(the same way a C compiler is written in C). Every asset in the engine,
including each `.assetType` definition, now resolves to a registered
assetType: a `.material` resolves to `material.assetType`, and
`material.assetType` resolves to `assetType.assetType`, which resolves
to itself.
## Why it exists
Assets used to be mapped to their type *implicitly*, by matching the
folder suffix against a name-keyed registry. That match carried no
stored link: nothing on a `Foo.material` recorded *which*
`material.assetType` it was written against, so two worlds shipping a
same-named type could silently disagree. Making `assetType` a real,
resolvable asset closes that gap — the link from an asset to its type is
now an explicit reference pinned in the asset's `.refs` sidecar
(`via: "asset_type"`), exactly like every other dependency.
## Where it lives
- Source: `/zero/source/.../<typename>.assetType/`
- Identity: `<typename>` (the `.assetType` suffix strips from the
identity; the folder retains the suffix on disk).
- Folder shape:
- `type.yaml` — the structural spec the validator reads. **Required.**
- `README.md` — type-level documentation. **Required.**
- `about.yaml` — three short plain-language texts saying what the type
is for (see "Say what your type is for" below). Every built-in type
ships one.
- `behavior.luau` — optional behavior/scaffolding module.
- `template/` — optional canonical placeholder body the templater
copies into a new asset of the type.
- `<name>.module/` — optional shared code the type ships to **every
instance** of it, conventionally `shared.module/` (see below).
## Shared code its instances reach (`asset.containing` + `.modules`)
A type ships code every instance of it uses by declaring the modules in
its own `behavior.luau`, as a map of tracked `require`s:
```luau
-- inside <typename>.assetType/behavior.luau
M.modules = { shared = require(".shared") } -- the sibling shared.module/
```
An instance reaches it with one location-independent line:
```luau
-- inside any foo.<typename>/init.luau
local api = asset.containing(__FILE__).modules.shared
```
`__FILE__` is the chunk's own VFS path; `asset.containing` resolves the
calling asset, and `.modules.<name>` follows the instance's pinned
`typeRef` (the type's **guid**, recorded in the instance's `.refs` as
`via = "asset_type"`) to the module its type declares. Because
resolution follows that link rather than a category name, the same line
in an instance of a different type resolves to that type's module, and
identically-named modules in two types never collide. `asset.typeRef(target)`
reads that guid for any target; `asset.resolve(asset.typeRef(target))`
gives the full `AssetRef<assetType>`. See `assetTypes/README.md`
§ "Shared code: type-owned modules" for the full mechanism.
## How to create one
Authoring a new asset type is just creating a `<typename>.assetType/`
folder under `/zero/source/`. The engine registers `<typename>` as a
category **the moment the folder is written** — no engine restart, no
Rust change, and it works for built-in (`@builtin/assetTypes/`) and
user-defined types identically. Once `<typename>` is registered,
`<name>.<typename>/` folders are recognised as assets of that type and
resolve their `asset_type` link back to this definition.
Creating a `<name>.<typename>/` folder *before* its
`<typename>.assetType/` exists fails fast: the suffix has no registered
type, so the folder can't be a valid asset. Author the type first.
## Say what your type is for (`about.yaml`)
Every type carries a small `about.yaml` beside its `type.yaml`, and the README
you write for the type documents how to author an asset of it. Both are
required, and both are meant to be written as you make the type, while you know
what it is for. `about.yaml` answers three questions in plain words, one key
each:
```yaml
intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering."
function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text."
usage: "You choose it by name for your UI or your text, and the text appears in that typeface."
```
- **`intent`** is what the type is for in someone's game. Name the whole range
of things it covers, in the words a game maker would use for them.
- **`function`** is how it works today: what an asset of the type is made of and
what the engine does with it.
- **`usage`** is what a person or an agent does with one: how they pick it, place
it, call it, or hand it to something else.
A few habits keep it useful. Write about the type as a whole, so the text holds
for every asset of it, and leave out any one asset's name. Say what the type
does right now. Stay with words a game maker would say, and mention a file name
or a call only where a game maker would know it. The limits are in bytes:
`intent` 800, `function` 1500, `usage` 1200. Each is generous, and a few
sentences usually say it.
Why bother: agents working in a world read these three texts to learn what your
type is for, so they reach for it when it fits and use it the way you meant. People
searching find the type by what they want to make, and the three texts let them
tell at a glance whether it is the one they need.
`asset.create("assetType", name)` scaffolds the file with all three keys, and a
README whose first paragraph is a `[[describe]]` note. Each value starts with
`[[describe]]` followed by a note on what to write; replace the whole value with
your own words. A value or a README line that still starts with `[[describe]]`
counts as not written yet, which the push's check of each asset it carries against
its type reports, naming the file. A type you leave as it was is not asked, so
older types stand.
Many types at once, or one you would rather not write by hand: `services
describe` lists the types that lack these texts and what filling them costs, and
`{ mode = "all" }` writes them into the same files, where you edit them like
anything else you wrote.
Where the texts show up: `asset.inspect("<typename>", "assetType")` returns them
as `detail.about`, `assetTypeRef:getAbout()` returns them as a table with the
keys `intent`, `function` and `usage`, the type's `types/<typename>` guide page
ends with an "About this type" section holding them, and the Inspector's Type
section lists them at the top. The guide page is this README plus that section,
so a README that explains the type well and an `about.yaml` that says what it is
for read well together.
## Ask your assets to say what they are
A type can ask each of its assets to carry a description: a file, written by the
author or generated, that says what that asset is and what it is for. The people
and agents who find an asset read it to decide whether it fits, and the author
knows what it is for at the moment of making it.
**1. Declare the file with a `described` check.** This is the whole requirement:
```yaml
files:
required:
- path: "README.md" # any file name works
kind: "file"
content:
- { check: "described", message: "Say what this widget is for and where it goes." }
```
With that in place:
- An asset whose file is missing, empty, or still has a line starting with
`[[describe]]` does not meet the type's requirements. `asset.validate` names it,
`zm status` and `world.publishBlockers()` list it, and a push that carries it is
refused with `message`.
- `services describe` finds it (`mode = "find"` lists it and the price) and can
write the description for it (`mode = "all"`). It writes only this file.
For a YAML file that holds several texts, name the keys and their limits in bytes,
the way this type does for `about.yaml`:
```yaml
content:
- check: "described"
keys:
- { key: "intent", max_bytes: 800 }
- { key: "usage", max_bytes: 1200 }
message: "Write intent and usage."
```
`headings` names sections a Markdown file must have. `assetTypes/README.md` has the
full check.
**2. Optionally, steer what gets generated with a `describe:` block:**
```yaml
describe:
guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI."
examples:
- "<identity of an asset of this type whose description reads well>"
```
`guidance` says what a description of your type covers, and `examples` names assets
whose descriptions show the shape you want. Without the block, generation still
works from the general describing guide and the check's `message`.
An example is an asset of your type in the library whose description is a true,
written one, never a scaffold's placeholder, and never the asset being described.
When the library has none, leave `examples` empty rather than name one that is not.
A type whose assets the model cannot perceive (a sound it cannot hear, a motion it
cannot see) says so in `guidance`: the description comes only from what the asset's
own files state, such as its name and the `.metadata` where a generation service
records the prompt that made it, and when they state nothing the model answers
`cannot` with that reason. `services describe` then fails that one asset with the
reason and leaves its file as it was.
**3. Start the file unwritten in your template.** Put a heading and one line that
starts with `[[describe]]` and a short note on what to write, in
`template/<file>`, so a new asset starts unwritten rather than with text that only
looks written. Do not put example content in it: every new asset carries the
template verbatim, and the same text on many assets makes them impossible to tell
apart in search.
Types whose assets say what they are through their own content (a mesh, a texture,
a sound) leave this out: declare nothing and nothing is asked.
## The `behavior.luau` contract
`behavior.luau` is the type's code. It returns ONE table, and the keys below
are the ones the framework reads off it for EVERY type — the surface you get
by declaring them. Anything else on the table is your own: reachable by name
through `require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("<typename>")` for a
system that knows your type (`computeShader` publishes `dispatchKey` that
way), and never called by the framework.
```luau
local M = {}
M.ref = { ... } -- methods on every AssetRef of this type
M.modules = { ... } -- shared code the instances reach
M.global = {} -- reserved key; ship the empty table
function M.onCreate(name, opts) ... end
return M
```
### The tables
| Key | Shape | What the engine does with it |
|-----|-------|------------------------------|
| `M.ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every `AssetRef` of this type: `ref:name(...)`, with `self` the ref. This is how a type gives its instances an API. Two names in here are contracted — see below. |
| `M.modules` | `{ [name] = require(".sibling") }` | Shared code the type's instances reach through `asset.containing(__FILE__).modules.<name>`, resolved by the instance's pinned `typeRef` guid. See the section above. |
| `M.refShapes` | `{ [method] = "TypeName" }` | Declares the result type of a `M.ref` method so the LSP can check what a call site does with it. See the `M.refShapes` section below. |
| `M.events` | an events schema | The events an ASSET of this type fires, declared the way a component declares its own. Readers subscribe through `ref.events.<name>:connect(...)`; firing authority stays with the type. Keyed by the asset's guid, so every resolver of the same asset shares the signals and a re-resolved ref re-attaches to subscriptions already there. |
| `M.namePattern` | a Lua pattern string | The name shape `asset.create` enforces for instances of this type, replacing the default `^[A-Za-z][A-Za-z0-9_]*$`. |
| `M.global` | `{}` | Reserved. Ship the empty table. |
### The lifecycle hooks
Each is optional; a type that omits one costs nothing. The engine calls them:
| Hook | Signature | When it fires |
|------|-----------|---------------|
| `M.onCreate` | `(name, opts) -> { [filename] = contents }` | On `asset.create("<type>", name, opts)`. Returns the files that scaffold the new instance, as a map of relative path to contents; the framework writes them. The `opts` type annotation is the schema `asset.create` validates the caller's arguments against, so annotate it. |
| `M.onReplace` | `(dest, files) -> { [filename] = contents }` | On an `asset.create` that writes over an asset already at `dest`, after `onCreate`. Receives the files `onCreate` produced and returns the files to write, so a type that re-derives its content (a rig re-imported from its source file) can carry what an author added to the asset it replaces. Optional. |
| `M.onRegister` | `(self)` | Once per instance, the first time it registers: on `engine.onWorldLoaded` for instances already in the world, immediately on a live `asset.create`, and when an instance arrives after that sweep (installed, pulled, or synced from a peer). Guarded by the instance's path, so a double trigger never double-registers. An instance that leaves the runtime releases the guard, so one arriving at the same path again registers. This is what makes "write an asset into the world and it takes effect live" work for content that registers into a runtime registry. The `@builtin` library is excluded from the sweep. |
| `M.onChange` | `(ref, change)` | On every VFS write to a file INSIDE one of this type's instances. `change` is `{ path, asset, type, kind, origin }`: `kind` is `"edited"` or `"seeded"`, `origin` is `"local"` or `"remote"` (an importer runs on the originator only). **Filter on `change.path`**: the hook fires for any file under the folder, so act on the one you care about. It runs as its own task and may wait on what it reads: a `vfs.read` of bytes only the blob store holds (a binary file on the web build) fetches them. The writes to one asset reach the hook in the order they were made, each once the hook before it has finished. A re-entrancy guard suppresses writes back into the same asset routed inline from the hook, and a write the hook makes is routed on a later frame like any other, so the hook must be convergent on its own: diff the meaningful state and short-circuit while your own regeneration is in flight. |
| `M.onDelete` | `(ref)` | When an instance of this type leaves the runtime: it is deleted, or the engine swaps away from the world binding that held it. Its files are gone from `/zero/source` by then. A removal routed while the instance's `onChange` is waiting reaches `onDelete` once that hook, and every write queued behind it, has finished. On a swap the instance still exists in the world it belongs to, and a VFS write the hook makes reaches the world now bound. |
| `M.validate` | `(assetRef) -> { { code, message, severity? } }` | On `asset.validate`, after the structural `type.yaml` check, for the type's own SEMANTIC validation. `severity` defaults to `"error"`; error-severity problems flip `ok` to false, warnings do not. A hook that raises or returns a non-table is itself reported as a `validate.hook_failed` error. `world.push` runs this per user asset, so declaring it enforces your type's rules at publish time with no further wiring. |
### The two contracted `M.ref` names
Most `M.ref` methods are your type's own surface: name them what you like,
return what you like. Two are read by the engine and mean the same thing for
every type, so their shape is fixed:
- **`instantiate(self, target?, opts?) -> (root, idMap)`** — makes an instance
part of the scene. Defining it is the whole opt-in: `ref:canInstantiate()`
is true exactly when it exists. Its **return is a contract** — see the next
section.
- **`inspect(self) -> detail`** — the type-specific half of `asset.inspect`.
See its section below.
### Names a type cannot shadow
Some keys resolve on every `AssetRef` before per-type dispatch, so an `M.ref`
entry of the same name is never reached: `canInstantiate`, `getSource` /
`getBytes` / `getText`, `exists`, `deps`, `meta`, `runtime`, `events`,
`modules`, `typeRef`, and the residency flags `has_backing_asset`,
`has_runtime_changes`, `cpu_resident`, `gpu_resident`. These are the
behaviours every asset must expose identically, which is why they win.
### Reloading
A `behavior.luau` edit ripples to existing refs with no restart: dispatch
reads `mod.ref` fresh each time and `require`'s cache is re-run in place. A
type whose folder appears at runtime is picked up on the next dispatch —
negative results are not cached either.
## The names your type's assets hold (`references:`)
An asset that names another asset by a string depends on it: the named asset
travels with it into a pack or a pull, and a name nothing answers to is an
unresolved dependency `worldValidation` reports and the push gate refuses. The
engine reads `require(...)` and the asset lookups on its own; a name your type's
format or API carries anywhere else is one you declare, in `type.yaml`:
```yaml
references:
# A top-level key of this type's own files that holds a name (or a table of them).
keys:
- { key: "shader", type: "shader" }
# A call's argument, counting from 1, wherever the call is written.
args:
- { call: "g:node", position: 2, type: "procNode" }
# A field of the options table a call takes.
fields:
- { call: "fx.beam", field: "material", type: "material" }
```
`type` is the category the name resolves in, one of `asset.categories()`. An
argument declared once per category, at the same `call` and `position`, takes
the name from whichever category answers. A key or a field holds one category:
declared again with another `type`, it takes the later one. The block is
declared as the type registers, when the library or the world holding it is
laid into the VFS, so every reference table reads it whatever script has or has
not run. `asset.declareReferenceKey`, `asset.declareReferenceArg` and
`asset.declareReferenceField` declare the same things from a script, for a
surface that is no type's.
## What search does with your type (`indexing:`)
A type declares what search embeds for its instances. **This is not optional
and there is no useful default**: ZeroMind runs your declaration and adds
nothing of its own, so anything you do not point at is absent from the index —
silently, and for every instance of the type forever. Twenty types once shipped
with no `indexing:` block and every instance of them was unfindable.
A type whose instances are never searchable (a run record, a log) says so with
`indexing: none`; every other type declares a recipe.
A type without an `about.yaml` beside its `type.yaml` refuses every push that
carries it, whether or not the push edited the type. Texts in it that are not
written yet follow the rule every `described` file follows: they refuse the
push that edits, stages or commits the type.
Two words carry the whole thing, and they are the same two the search tool
exposes:
- **`identity`** — what an instance IS. For anything you can look at, that is
the picture (`content:` + `modality: image`, matched directly by a text
query). For everything else it is the authored text that says what it is.
- **`capability`** — what it DOES and how it is made. Code, settings, the
model-written summary of them.
A type with no behaviour has no `capability` entry. A type with nothing to look
at has no image. Leaving a slot empty is a statement; filling it with whatever
happens to be lying around is not.
The one you have to decide when authoring a type is: *for an instance of this,
what is the thing a person would recognise it by, and what is the thing it
does?* A texture answers "the image" and "its compression settings". A module
answers "its README" and "its code". Write those two answers into `indexing:`
and the rest follows.
Three specifics worth knowing before you write one:
- **`derive` is instructed by this type's own `README.md`.** A model reads an
instance's source and writes a description; what it embeds is the
description, not the source. It follows your README to know what it is
looking at, so a type whose README says what its instances are gets good
derivations for free. There is no prompt to name.
- **`derive` loses detail.** It keeps the main technique and drops secondary
ones. If the source is code you want searchable by its own vocabulary,
declare the same files a second time as `capability` with
`extractor: verbatim`. Two entries, same role, different failure modes.
- **Never `source: { field: name }`.** A filename is one or two words with no
usable embedding, and a corpus indexed that way answers "language runtime"
with `say_runtime_2.soundClip`.
`facets:` is the second stage: a `file:` embedded whole (`.metadata`, so keys
nobody declared still get indexed) plus computed keys that state a value for
every instance including `false` — the raw file can imply "not rigged" and can
never say it.
The full schema, every extractor and the facet sources are in
[`assetTypes/README.md`](../README.md); the scaffolded block with inline
guidance is in [`template/type.yaml`](template/type.yaml).
## A container whose contents work together (`system: true`)
A type that is a container can say its contents are one working whole by
declaring `system: true` at the top level of its `type.yaml`. `package` declares
it: the components, modules, scenes and tools a package ships are parts of one
system. The declaration is plain data, read from the decoded `type.yaml`
(`require("@builtin::assetTypes.assetType.shared.declaration").of("package").system`),
and a type that leaves it out is an ordinary type.
## What your type looks like (`preview:`)
A type declares how its instances are pictured, and the engine makes the
picture when something needs it: staging an instance whose `preview.png` is
missing or pictures content it no longer holds queues a render, a view that
shows the instance asks for one, and `ref:preview()` renders one on demand. An
edit renders nothing on its own. Each `preview.png` records the content it
pictured, which is how a stale one is told from a current one. The preview studio owns the light, the
backdrop, the framing and the camera, and nothing in the world reaches it, so
an instance pictures the same in every world. Declare one of four recipes:
| `kind` | The picture is | Your type provides |
|---|---|---|
| `instantiate` | the asset stood in the studio through its own `instantiate` | `ref.instantiate` |
| `image` | a file the instance holds, fitted to the frame, nothing rendered | `source.files`, the candidate files in order |
| `surface` | the asset painted on a builtin mesh | `mesh`, and `ref.previewMaterial(self) -> { material }` |
| `custom` | whatever `ref.preview(self, opts)` returns | `ref.preview` |
```yaml
preview:
kind: surface
mesh: sphere
```
A scene-instantiable type that declares nothing still answers `ref:preview()`
through its `instantiate`, but persists no picture: the declaration is what
makes the engine keep one. List `preview.png` among the type's files so an
instance may hold it.
The picture is findable only when `indexing.content` names it. The two are
separate statements, and neither is derived from the other:
```yaml
indexing:
content:
- role: identity
modality: image
source: { file: "preview.png" }
```
`@builtin::assetTypes.assetType.shared.previewRecipe` reads the declaration;
the scaffolded block with inline guidance is in
[`template/type.yaml`](template/type.yaml).
## Typing a result from the instance (`M.refShapes`)
Every method on `M.ref` is checked against the type its `--!return` names,
and every asset of the type gets the same one. That is right for most
methods and wrong for the ones whose result is shaped by the ASSET: a
method answering one entry per child folder, per row of a config, per
binding an input map declares. The widest true annotation for those is
`{ [string]: Thing }`, and an indexer accepts every key — so a caller's
typo reads as valid and nothing reports.
`M.refShapes` lets the type state the result per instance. Each entry is
`function(self) -> (typeExpression, source?)`: a Luau type expression for
THIS asset, and the module whose type vocabulary the expression uses.
Read the asset; never run it — stating a type must not take effect.
```lua
-- inputMap.assetType/behavior.luau — the shipped example.
-- `activate()` hands back one handle per control the map declares.
M.refShapes = {
activate = function(self): (string, string)
local names = {}
for _, control in ipairs(M.ref.controls(self)) do
table.insert(names, control.name .. ": Handle")
end
if #names == 0 then return "", "" end
return "{ " .. table.concat(names, ", ") .. " }",
"@builtin::modules.zinput.scheme"
end,
}
```
With it, `map.jump:onPressed(fn)` resolves and `map.noexisting` is
reported with the map's real controls. Without it, both pass silently.
### What the call site has to say
A per-instance type is matched by the asset's IDENTITY, so it applies only
where the reference the method is called on says WHICH asset. Two spellings
do:
```lua
-- a component field, whose declared default names the asset
public = { map = Field.assetRef("inputMap", "@builtin::inputMaps.default", Sync) }
local controls = public.map:activate() -- typed for THAT map
-- an annotation, naming category and identity
local m: AssetRef<"inputMap", "@builtin::inputMaps.default">
```
`asset.resolve("<identity>", "<category>")` carries the CATEGORY, so the
methods the category defines are checked on the result — but not which
asset it found, so a per-instance result keeps the category's declared
return. A reference that names no asset (`asset.resolve(someVariable)`, a
value passed in as a parameter) carries neither, and calls on it are
unchecked. That is the same rule everywhere: the checker states what the
code states, and a name computed at runtime states nothing.
### The result is a value, not a binding
The type belongs to the expression, so it travels the way any inferred type
travels. Indexing the call's result is checked; stashing it in a module
local and indexing it from another function is not, because the local's
declared type is what carries across — and `any` (or no annotation) carries
nothing:
```lua
-- checked: `d.greting` is reported
function awake()
local d = public.dialogue:lines()
d.greeting:say()
end
-- NOT checked: `lines` is a module local typed `any`
local lines: any = nil
function awake() lines = public.dialogue:lines() end
function start() lines.greting:say() end
```
Bind what you need at the call site and the names stay checked in both
places — the asset-derived type at the boundary, ordinary scoping after it:
```lua
local lineGreeting = nil
function awake()
local d = public.dialogue:lines()
lineGreeting = d.greeting -- `d.greting` is reported here
end
function start() lineGreeting:say() end
```
### When nothing is reported and you expected something
From a call site, a type that is correct and a type that was never published
look the same: no diagnostic either way.
`require("@builtin::assetTypes.assetType.shared.refShapes").published()` answers which it
is. It returns two maps — the category surfaces, and the per-asset results
keyed by identity and then by method:
```lua
local shapes = require("@builtin::assetTypes.assetType.shared.refShapes").published()
shapes.categories.dialogue
--> "{ lines: () -> any, lineNames: () -> { string } }"
shapes.returns.Shopkeeper.lines
--> "{ browsing: Line, farewell: Line, greeting: Line, wares: Line }"
```
An identity absent from `returns` was never published for, which is a
different thing from published-and-correct — and the reason to look here
rather than at the call site.
The answers are recomputed when an instance is written, so a control added
to a map reaches the checker with no restart. Only types that declare
`refShapes` are read per instance, and at most 64 assets of one type are —
past that the type keeps its declared return and the engine log names what
was dropped.
## The instantiate hook (`M.ref.instantiate`) — a contracted return
A type opts into becoming part of the scene by defining `instantiate` on its
`ref` table. That is the whole opt-in: `ref:canInstantiate()` is true exactly
when the hook exists, so consumers offer a scene path — an `Asset.source`
field, a viewport drop, a tool argument — by CAPABILITY rather than by a list
of type names.
Unlike `inspect`, this hook's **return is a contract**. A caller writes one
piece of code against every instantiable type, so what comes back cannot vary
by type:
```lua
-- behavior.luau
local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")
local M = {}
M.ref = {
instantiate = function(self, target, opts)
local root = Instantiable.root(self, target, opts) -- IN: the base opts
-- ... compose whatever this type is, under `root`, NOW ...
return Instantiable.result(self, root, idMap) -- OUT: the contract
end,
}
return M
```
**IN.** `target` is an owning entity ref: the instance lands under (or, for a
hierarchy type, onto) that owner. With no target the type spawns a fresh root.
`position`, `rotation`, `scale`, `name` and `temporary` mean the same for every
type — `Instantiable.root` applies them to a root you mint, `Instantiable.place`
to one you adopt, so you never re-read the spec. Honour more opts of your own if
your type needs them, and document them in the hook's `--!arg` docs.
**OUT.** Return `(root, idMap)` through `Instantiable.result`:
- **`root`** — the composed root, as an `EntityRef`. Composition is
**synchronous**: everything your type builds is live when you return, so the
caller can parent to it and read its components in the same statement. Do not
defer the work to a component's `awake` and return an empty shell — a root
that is not live is refused.
- **`idMap`** — the `originalId -> runtimeId` map naming what you spawned, or
nil for a type with no addressable children (`result` normalises it to `{}`).
**A caller never receives nil.** A component that re-composes your asset on
every load keeps this map and passes it back in, which is how a cross-entity
reference into the composition survives a reload.
`AssetRef` runs every `instantiate` through `result` on the way out whether or
not your type called it, so returning something else fails at your own call
rather than handing a caller a nil root. Calling it yourself is still how you
say what you return, and running it twice changes nothing.
`@builtin::assetTypes.assetType.shared.instantiable`'s README is the full reference for both halves.
## The inspect hook (`M.ref.inspect`)
`asset.inspect(ref)` resolves `ref`, builds a common envelope shared by
every asset (`identity`, `name`, `guid`, `source`, `typeName`,
`typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then
calls the resolved type's own inspect hook — `M.ref.inspect(self)` on
`behavior.luau` — for the type-specific `detail`. The hook returns
*only* `detail`; the framework fills in everything else:
```lua
-- behavior.luau
local M = {}
M.ref = {
inspect = function(self)
-- self.path is the asset's own folder. Parse the asset's OWN
-- files — source text, a native payload header, its `.metadata`
-- — never runtime state (a live component instance, a GPU
-- upload, a compiled schema): inspect must work on an asset that
-- hasn't been compiled, uploaded, or registered yet.
return {
-- type-specific fields, whatever this type wants to surface
}
end,
}
return M
```
A type that omits `M.ref.inspect` yields the generic record: the
envelope alone, `detail = nil`. That's a valid, working state — not an
error — but it means agents calling `asset.inspect` /
`tools.use("assets","describe", ...)` on an instance of the type see
only identity and scope, none of what makes an instance of it
meaningful. Declaring the hook is what makes a type's instances
inspectable for what they actually are.
A hook that raises leaves `detail = nil` and sets `record.warning` to
the error text — `asset.inspect` itself never raises for a resolved
ref.
## Discovery
- `asset.list("assetType")` — every registered asset type.
- `asset.inspect("<typename>")` — the type's identity, source path, and
this README, plus its `about.yaml` texts as `detail.about`.
- `asset.resolve("<typename>", "assetType")` — the `<typename>.assetType`
asset itself (used by the validator to find the type's `type.yaml`).
## Related
- `assetTypes/README.md` — the full `type.yaml` schema and validator
rules.
- `@builtin::assetTypes.assetType.shared.instantiable` — the instantiation contract's shared
implementation, both halves.
- Every other `*.assetType/` here — the concrete types built on this
meta-type.
## The Inspector a type ships
Every type names the file its Inspector sections come from with `inspector: { type: "inspector.luau" }` in its type.yaml. `asset.create("assetType", name)` scaffolds the block and an `inspector.luau` whose Contents section lists an asset's files, built from `ctx.parts`. `asset.validate` on a type that names none reports `assetType.inspector_required` (an error for a `@builtin` type the engine's library authors; a warning for a world's own, and for one the library carries by reference, which the world that published it answers for), and one whose file does not load `assetType.inspector_invalid`.
## In the Inspector
Type: what the type.yaml declares (suffix, container, primary files, the settings and inspector blocks), whether the type ships a behavior, how many instances it has, and **Open** for its files. Schema: the settings every instance carries, as read-only typed controls at their defaults (a type whose schema is per instance says where it is declared). Instances: a link to each of the first 20 assets of the type, and how many there are when there are more.
·voxeltemplate · born here
❒asset # bookshelf
Voxel template `bookshelf` (demo asset). Live instances spawned via `Voxel.fromTemplate("bookshelf")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # bot_seated
Voxel template `bot_seated` (demo asset). Live instances spawned via `Voxel.fromTemplate("bot_seated")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # bot_standing
Voxel template `bot_standing` (demo asset). Live instances spawned via `Voxel.fromTemplate("bot_standing")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # cabinet
Voxel template `cabinet` (demo asset). Live instances spawned via `Voxel.fromTemplate("cabinet")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # chair
Voxel template `chair` (demo asset). Live instances spawned via `Voxel.fromTemplate("chair")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # clock
Voxel template `clock` (demo asset). Live instances spawned via `Voxel.fromTemplate("clock")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # desk
Voxel template `desk` (demo asset). Live instances spawned via `Voxel.fromTemplate("desk")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # keyboard
Voxel template `keyboard` (demo asset). Live instances spawned via `Voxel.fromTemplate("keyboard")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # lamp
Voxel template `lamp` (demo asset). Live instances spawned via `Voxel.fromTemplate("lamp")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # linked_workstation_baked
Voxel template `linked_workstation_baked` (demo asset). Live instances spawned via `Voxel.fromTemplate("linked_workstation_baked")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # monitor
Voxel template `monitor` (demo asset). Live instances spawned via `Voxel.fromTemplate("monitor")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # mug
Voxel template `mug` (demo asset). Live instances spawned via `Voxel.fromTemplate("mug")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # plant
Voxel template `plant` (demo asset). Live instances spawned via `Voxel.fromTemplate("plant")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # printer
Voxel template `printer` (demo asset). Live instances spawned via `Voxel.fromTemplate("printer")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # trash
Voxel template `trash` (demo asset). Live instances spawned via `Voxel.fromTemplate("trash")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # whiteboard
Voxel template `whiteboard` (demo asset). Live instances spawned via `Voxel.fromTemplate("whiteboard")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # blade
Voxel template `blade` (demo asset). Live instances spawned via `Voxel.fromTemplate("blade")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # house
Voxel template `house` (demo asset). Live instances spawned via `Voxel.fromTemplate("house")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # rock
Voxel template `rock` (demo asset). Live instances spawned via `Voxel.fromTemplate("rock")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # tree
Voxel template `tree` (demo asset). Live instances spawned via `Voxel.fromTemplate("tree")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # windmill_base
Voxel template `windmill_base` (demo asset). Live instances spawned via `Voxel.fromTemplate("windmill_base")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # cabin
Voxel template `cabin` (demo asset). Live instances spawned via `Voxel.fromTemplate("cabin")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # chassis
Voxel template `chassis` (demo asset). Live instances spawned via `Voxel.fromTemplate("chassis")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # door
Voxel template `door` (demo asset). Live instances spawned via `Voxel.fromTemplate("door")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # front_bumper
Voxel template `front_bumper` (demo asset). Live instances spawned via `Voxel.fromTemplate("front_bumper")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # headlight
Voxel template `headlight` (demo asset). Live instances spawned via `Voxel.fromTemplate("headlight")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # hood
Voxel template `hood` (demo asset). Live instances spawned via `Voxel.fromTemplate("hood")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # mirror
Voxel template `mirror` (demo asset). Live instances spawned via `Voxel.fromTemplate("mirror")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # rear_bumper
Voxel template `rear_bumper` (demo asset). Live instances spawned via `Voxel.fromTemplate("rear_bumper")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # rear_window
Voxel template `rear_window` (demo asset). Live instances spawned via `Voxel.fromTemplate("rear_window")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # seat
Voxel template `seat` (demo asset). Live instances spawned via `Voxel.fromTemplate("seat")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # spoiler
Voxel template `spoiler` (demo asset). Live instances spawned via `Voxel.fromTemplate("spoiler")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # steering_wheel
Voxel template `steering_wheel` (demo asset). Live instances spawned via `Voxel.fromTemplate("steering_wheel")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # taillight
Voxel template `taillight` (demo asset). Live instances spawned via `Voxel.fromTemplate("taillight")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # trunk
Voxel template `trunk` (demo asset). Live instances spawned via `Voxel.fromTemplate("trunk")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # wheel
Voxel template `wheel` (demo asset). Live instances spawned via `Voxel.fromTemplate("wheel")` reload automatically when this template's `template.voxbin` payload is rewritten.
·voxeltemplate · born here
❒asset # windshield
Voxel template `windshield` (demo asset). Live instances spawned via `Voxel.fromTemplate("windshield")` reload automatically when this template's `template.voxbin` payload is rewritten.
·computeshader · born here
❒asset # greedy_mesh
Turns a voxel chunk into a mesh, merging coplanar faces of one colour into the
largest rectangle that covers them so a solid wall costs two triangles instead
of two per voxel. One invocation per slice; the vertex and index counts are
claimed atomically, so the pass writes both buffers in a single dispatch.
## Bindings
| name | kind | what it is |
|------|------|------------|
| `params` | `buffer` (read, u32) | chunk size, padding and the origin the mesh is built around |
| `solid_padded` | `buffer` (read, u32) | the chunk's occupancy bitmask, one voxel of padding on every side so a face test at the edge reads a real neighbour |
| `colors` | `buffer` (read, u32) | one packed colour per voxel |
| `vertex_buf` | `buffer` (read_write, u32) | the vertices produced, in the engine's vertex layout |
| `index_buf` | `buffer` (read_write, u32) | the indices produced |
| `counters` | `buffer` (read_write, atomic u32) | how many vertices and indices have been claimed |
## Params
| name | type | what it is |
|------|------|------------|
| `vertStride` | `u32` | one vertex in u32 words, so the writes follow the engine's vertex layout |
# voxelConfig
Read and write the voxel engine's configuration — the path defaults and engine knobs the voxel system runs on (chunk size, default block size, default mesher, CPU fallback, play-mode write permission, and the path defaults).
Get a value by key, get the whole merged config, set a runtime override, or reset one or all overrides. Values resolve in order: a runtime override wins, then the world's voxel settings, then the built-in defaults.
# voxelEngine
Build and edit voxel worlds — spawn worlds and shapes, mutate blocks, query them, transform them, manage palettes, persist them, and kitbash composites.
Spawn worlds, shapes, and templates, clone handles, or scatter and stamp templates across regions, and destroy them. Mutate blocks — set, clear, fill boxes, spheres, cylinders, and lines, mirror, translate, and paste regions. Read the world back — a block, its neighbours, a region, counts by name, raycasts, sphere and line queries, region copies, and bounds. Apply entity-level transforms (position, rotation, scale, parent, snap-to-world) and read them. Manage the palette. Persist to voxbin and templates — save, bake, save and copy templates, and capture an area. Kitbash composite props from existing templates — new, add child, save baked or linked, explode, save as a bundle. And read or override the voxel config. A target — a handle, `"world"`, an entity id, or an entity name — is accepted uniformly across the toolbox.
# voxelEngine
Public API for the voxel system. Single entry point for creating and
editing voxel worlds and shapes. Tools and custom code call into here;
nobody touches the `VoxelWorld` / `VoxelShape` components directly.
Two top-level concepts:
- **World** — singleton entity carrying a chunked `VoxelWorld` grid.
Created lazily on the first `createWorld` call, found by name later
via `world()` / `hasWorld()`, destroyed with `destroyWorld()`.
- **Shapes** — per-entity dense or chunked `VoxelShape` grids. Spawned
freely; each carries its own voxbin path on disk under
`Config.get("paths.shapes")/<entityId>.voxbin`.
Templates are stored under `Config.get("paths.templates")/<name>.voxbin`
and instantiated via `fromTemplate`. Captures (snapshots of a world
region) land at `Config.get("paths.captures")/<name>.voxbin`.
## Exports
Module-level:
- `M.world() -> Handle?` — handle to the singleton VoxelWorld, or nil.
- `M.createWorld(opts: CreateWorldOpts?) -> Handle` — create or reuse the singleton.
- `M.destroyWorld()` — despawn the singleton world if it exists.
- `M.hasWorld() -> boolean` — true when the singleton exists.
- `M.shape(name_or_id) -> Handle?` — handle to an existing shape entity.
- `M.createShape(opts: CreateShapeOpts) -> Handle` — spawn a new shape entity.
- `M.destroyShape(name_or_id)` — despawn the shape entity if it exists.
- `M.fromTemplate(name: string, opts: FromTemplateOpts?) -> Handle` — instantiate from `paths.templates/<name>.voxbin`. Supports live-link, detached, and memory-only modes.
- `M.saveAsTemplate(shape: Handle, name: string, opts: SaveAsTemplateOpts?) -> string` — write a shape's grid as a named template.
- `M.listTemplates() -> { string }` — enumerate template names (no extension).
- `M.deleteTemplate(name: string)` — remove a template file.
- `M.copyTemplate(srcName: string, dstName: string) -> string` — duplicate a template file.
- `M.captureArea(opts: CaptureAreaOpts) -> any` — flatten a region of world + shapes into a dense voxbin record (and optionally save it).
- `M.scatter(templateName: string, opts: ScatterOpts) -> { Handle }` — bulk-spawn instances across a region.
- `M.stamp(templateName: string, opts: StampOpts) -> Handle` — single placement, either as a new entity (`mode = "instance"`, default) or rasterised into the world grid (`mode = "merge"`).
- `M.composite` — re-export of the composite-prop module.
- `M._resolvePivot(value) -> { number, number, number }?` — internal pivot helper, exposed for composite reuse.
Handle methods (returned by `createWorld` / `createShape` / `fromTemplate` / `world` / `shape` / `scatter` / `stamp`):
- `h:setBlock(x, y, z, name)` / `h:getBlock(x, y, z)`
- `h:fillBox(min, max, name)` / `h:fillSphere(center, radius, name)`
- `h:fillCylinder(p1, p2, radius, name)` / `h:fillLine(p1, p2, name)`
- `h:clear()` — clear every cell.
- `h:setPalette(palette)` / `h:addPaletteEntry(name, color)`
- `h:bounds() -> Bounds?` — inclusive bounds of every filled cell.
- `h:flush()` — flush pending dirty-chunk remeshes.
- `h:save()` — write the grid to the component's dataPath.
- `h:dirtyChunks() -> { any }`
- `h:isLive() -> boolean` / `h:templatePath() -> string?`
- `h:bake() -> Handle` — break the live-link, copy into a private dataPath. Shape-only.
- `h:saveAsTemplate(name, opts) -> string`
- `h:exists() -> boolean` / `h:destroy()`
- `h:setPosition(x, y, z) -> Handle` / `h:getPosition() -> Vec3Map?`
- `h:setRotation(r) -> Handle` / `h:getRotation() -> Vec3Map?` (Euler radians, ZYX)
- `h:setParent(parentEntityId) -> Handle`
- `h:copyRegion(min, max) -> any` / `h:pasteRegion(offset, regionData, opts)`
- `h:mirror(axis)` / `h:translate(dx, dy, dz)`
- `h:getNeighbors(x, y, z) -> any`
- `h:queryRegion(min, max, opts) -> { any }` / `h:countByName(min, max) -> { [string]: number }`
- `h:raycast(origin, dir, maxDist) -> any` / `h:sphereQuery(center, radius) -> { any }` / `h:lineQuery(p1, p2) -> { any }`
- `h:clone(opts) -> Handle` — shape-only. Detached duplicate.
- `h:snapToWorld(opts) -> Handle` — quantise transform or shift voxel data to the world grid.
Types:
- `Vec3Map = { x: number?, y: number?, z: number? }`
- `Vec3Array = { number }`
- `Vec3Like = Vec3Map | Vec3Array`
- `ColorMap = { r: number?, g: number?, b: number? }`
- `ColorArray = { number }`
- `ColorLike = ColorMap | ColorArray`
- `PaletteEntry = { name: string, color: ColorLike, material: any?, atlas: any?, faces: any? }`
- `Palette = { PaletteEntry } | { [string]: ColorLike }`
- `Bounds = { min: Vec3Map, max: Vec3Map }`
- `Handle = { entityId: any, componentName: string, [string]: any }`
- `CreateWorldOpts` / `CreateShapeOpts` / `FromTemplateOpts` / `SaveAsTemplateOpts`
- `CaptureAreaOpts` / `CaptureAreaBoxSelector` / `CaptureAreaSphereSelector`
- `CloneOpts` / `SnapToWorldOpts`
- `RegionSpec` / `ScatterOpts` / `StampOpts`
- `QueryRegionOpts` / `PasteRegionOpts`
## Usage
```luau
local Voxel = require("@builtin::systems.voxelEngine")
-- Create the singleton world, write a couple of cells.
local world = Voxel.createWorld({
size = { x = 64, y = 32, z = 64 },
palette = { stone = { r = 0.5, g = 0.5, b = 0.5 } },
})
world:setBlock(0, 0, 0, "stone")
world:fillBox({ 1, 0, 0 }, { 3, 0, 3 }, "stone")
-- Spawn a shape from a template, live-linked to the template file.
local tree = Voxel.fromTemplate("tree", {
position = { x = 10, y = 0, z = 5 },
})
-- Scatter 20 copies inside a region.
local hs = Voxel.scatter("tree", {
region = { box = { min = { 0, 0, 0 }, max = { 50, 0, 50 } } },
count = 20,
snap = true,
})
-- Capture a region of world + shapes into a dense voxbin record.
local rec = Voxel.captureArea({
box = { min = { 0, 0, 0 }, max = { 15, 15, 15 } },
})
```
## Notes
- Handles are thin wrappers over an `entityId` + a component name. Each
method re-resolves the underlying component on every call, so the
handle stays usable across hot-reload cycles.
- All `h:setBlock` / `h:fillBox` / … calls forward to the component via
colon-call (`c:method(...)`) — dot-call would drop `self` and the
strict typed-function checker would read args from the wrong slots.
- `fromTemplate` defaults to `live = true` + `persist = true` —
live-linked instance that auto-bakes private edits to disk. Pass
`live = false` for a detached copy with its own dataPath, or
`persist = false` for a memory-only instance.
- `Voxel.scatter` / `Voxel.stamp` (mode = "merge") obey the play-mode
write gate enforced by `assertModeAllowsWrite`. In play mode, callers
must toggle `Config.set("allowPlayWrites", true)` (or set the
`[voxel].allowPlayWrites` WORLD_SETTINGS override) to unlock voxbin
writes.
- `Voxel.stamp(mode = "merge")` only supports yaw rotations of
0 / 90 / 180 / 270 degrees — voxel grids require axis-aligned stamps.
Non-axis-aligned yaws raise.
- The `composite` sub-module is reachable as `Voxel.composite`. It
builds kitbashed `.voxcomp` props that re-resolve their children at
instantiation time; `fromTemplate` transparently dispatches to it
when the named template resolves to a `.voxcomp` manifest.
# Voxel Template (asset type)
A voxel template is a reusable, named voxel grid that live instances
link to. It is the source of truth for a family of `VoxelShape`
instances spawned via `Voxel.fromTemplate(<name>)` — every live
instance reloads its grid + rebuilds its mesh automatically when the
template's payload is rewritten, and a `Voxel.deleteTemplate(<name>)`
flips every live instance's `staleTemplate` flag.
## When to use one
- You want a reusable voxel prop (`tree`, `rock`, `barrel`) that many
entities share, with edits to the template propagating to every
live instance.
- You want a stable identity (a guid) for a voxel grid so the engine's
asset-reload machinery can drive the live-link instead of a
poll loop.
If you just need a one-off shape on a single entity, use
`Voxel.createShape` — that writes a private `dataPath` voxbin and never
becomes a template.
## Where it lives
- Source: `Config.get("paths.templates")/<Name>.voxelTemplate/`
- Identity: `<Name>` (the `.voxelTemplate` suffix strips).
- Folder shape:
- `template.voxbin` — the binary grid payload (size, blockSize,
palette, flat grid array). Written by `saveAsTemplate` /
`copyTemplate`. **Optional** in the structural spec only so the
freshly-scaffolded (empty) folder validates; every real template
carries it.
- `.metadata` — agent-editable tags + free-form fields. **Required.**
## How to create one
From a live shape you authored:
```luau
local Voxel = require("@builtin::systems.voxelEngine.voxelEngine")
local shape = Voxel.createShape({ size = { 8, 8, 8 } })
shape:fillBox({0,0,0}, {7,7,7}, "stone")
shape:saveAsTemplate("my_prop")
-- Writes: <paths.templates>/my_prop.voxelTemplate/
-- template.voxbin (the grid payload)
-- .metadata
```
Or scaffold an empty one to fill in later:
```luau
asset.create("voxelTemplate", "MyProp")
-- Creates: /zero/source/MyProp.voxelTemplate/
-- .metadata
```
## How it operates
1. **Live-link.** `Voxel.fromTemplate(<name>)` resolves the template to
an `AssetRef<voxelTemplate>` and stores it in
`VoxelShape.templateRef`. The engine indexes that subscription by
guid, so a write to the template's `template.voxbin` fires
`onAssetReload("templateRef")` on every live instance, which
re-reads the payload and rebuilds the mesh.
2. **Delete.** `Voxel.deleteTemplate(<name>)` removes the folder and
directly flips `staleTemplate = true` on every live instance —
deletes do not emit a content-change event, so the engine notifies
instances synchronously rather than waiting on a reload hook.
3. **Copy-on-write.** Editing a live instance (`setBlock`, `fillBox`,
…) auto-bakes a private `dataPath` copy and clears `templateRef`,
detaching the instance from the template. The original template is
untouched.
## In the Inspector
Selecting a voxel template opens its **Grid** section, decoded from `template.voxbin` once per version of the file: the template's size in voxels, its block size, whether it is chunked (and, when it is, its chunk size and chunk count), how many palette entries it holds, and whether it carries custom data.
# Package (asset type)
A package is an asset — a `<Name>.package/` folder that bundles
components, modules, scenes, presets, tools, and any other typed
sub-assets under one identity, installed into a world as one unit.
Packages are how authoring sets get distributed across worlds.
A **library** is the addressable root scope content resolves under
(`@builtin`, `@<name>`) — a whole world mounted into this one under a
namespace, via `zeromind.installLib` / `world.installLibrary`. A package is
an asset that lives inside such a root, and one library holds as many
packages as its author put there. A package is installed by guid the
way every asset is (`zeromind.install`, `zeromind.installAsset`).
## When to use one
- You're authoring a coherent feature suite (character controller
stack, voxel engine, UI kit, animation library) and want to ship
it as one install target.
- You want a stable identity worlds can install and resolve sub-assets
against (`@<lib>::pkg.sub_module`).
- You want versioning + a per-package metadata layer that travels with
the contents.
If you're authoring inside a single world and don't need a separate
install lifecycle, you don't need a package — drop your `.component`
/ `.module` / etc. directly in `/zero/source/`. Packages are for
cross-world distribution.
## Where it lives
- Source: `/zero/source/.../<Name>.package/` (or, for legacy
packages, a folder containing `package.yaml` at its root).
- Identity: `<Name>` (the `.package` suffix collapses in identity —
mirrors `.module`). A file at `<Name>.package/<sub>/init.luau`
resolves under the same identity it would at `<Name>/<sub>/init.luau`.
- Folder shape (container-style — open child set):
- `package.yaml` — metadata (name, version, description).
**Required.**
- `README.md` — instance documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
- Sub-asset folders: any combination of `.component/`,
`.module/`, `.scene/`, `.preset/`, `.tool/`, `.toolbox/`,
`.material/`, `.shader/`, etc.
The validator permits unlisted children (`allow_unlisted: true`) by
design — packages are open containers.
## How to create one
```luau
asset.create("package", "<Name>")
-- Creates: /zero/source/<Name>.package/
-- package.yaml (metadata scaffold)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source instead of the root:
asset.create("package", "<Name>", { folder = "systems" })
```
Then scaffold sub-assets inside it:
```luau
local pkg = asset.resolve("<Name>.package")
asset.create("component", "Spinner", { into = pkg })
asset.create("module", "util", { into = pkg })
```
## How it operates
1. **Registration.** Writing `package.yaml` indexes the package with
the package registry.
2. **Install.** A package is installed by guid, the way every asset
is: `zeromind.install` from the shared library, or
`tools.use("zeromind", "installAsset", <guid>)` inside the engine. The
install writes the package's files (and the deps its `.refs`
record) into this world's source, so its sub-assets resolve here.
`tools.use("zeromind", "preview", <guid>)` reports what an install would
write before it writes anything.
3. **Sub-asset resolution.** Because `.package` collapses in
identity, sub-asset paths work whether the package folder is
suffixed (`<Name>.package/<sub>`) or not (`<Name>/<sub>`).
4. **Updates.** `tools.use("zeromind", "updates")` lists installed content
with upstream changes available; re-running the install pulls them.
5. **Metadata.** `package.yaml::name` overrides the folder stem if
present (rare); `version` + `description` are surfaced in the
package index.
6. **Hot reload.** Edits to sub-assets hot-reload as normal; edits to
`package.yaml` reload package metadata.
## Discovery
- `asset.list("package")` — every registered package.
- `asset.inspect("<name>")` — sub-asset catalog, source, this type
README.
- `ls /zero/source/<Name>.package` lists the files the folder holds.
- `library.list()` — the library assets registered in this world,
including the ones packages under an installed library contribute.
## Authoring conventions
- Name packages by their feature (`voxels`, `cameraRigs`,
`dialogSystem`), not by their owner — the host library is the
`@<lib>::` scope prefix.
- Cluster cohesive sub-assets together. A "character controller"
package should ship the controller component, its preset configs,
its supporting modules, and a demo scene — not just the component.
- Document the package's surface in `README.md` — what install
installs, what each sub-asset does, how they relate. Agents reading
`cat /zero/source/<Name>.package/README.md` get the full picture in one shot.
- Version conservatively — breaking a package means breaking every
world that installed it. Use semantic version bumps.
## Common pitfalls
- **Missing `package.yaml`.** Without it the folder isn't recognized
as a package even if it has a `.package/` suffix.
- **Dependency installs.** A package that depends on another package
must declare it (in `package.yaml::dependencies` once supported)
and the parent world must install dependencies in order.
- **Identity collapse.** Sub-asset paths skip the `.package` suffix.
Don't include `.package` in `require` strings.
## Referencing inside a package: the `~` rule
`~` means **this package**, resolved structurally from the referring file's own
location. It is the only package-specific syntax. It is rename-safe (no package
name is ever written) and library-agnostic (it never names the host library, so
it survives `@builtin` being renamed or the package being mounted elsewhere).
`~.tail` maps to `<pkg>.package/tail` from anywhere inside the package.
| Referencing from | Use | Example |
|---|---|---|
| An asset/module **inside this package** | `~.tail` | `require("~.modules.myThing")`, `shader: "~.shaders.myThingBlend"` |
| A **different package** under the same root | root-relative identity | `require("@root::systems.otherPkg.modules.x")` |
| A **different library's** public API (a deliberate external dependency) | `@otherlib::…` | `require("@physics::modules.rigidbody")` |
**Never write `@builtin::` (or any host-library name) to reference your own
package** — that couples the package to the library it currently lives in and
breaks if the library is renamed or the files move. Use `~.`. It works from
Luau `require()` and from asset-reference strings in data files (a material's
`mat.yaml` `shader:` field, a component's default asset ref); the engine
resolves it against the referring file's own package. `asset.resolve` takes a
`~.` name the same way, against the package of the file that calls it:
`asset.resolve("~.materials.myThingStandard", "material")`.
`~.` is the package-relative self-reference; `@root::` is the root-relative one
(the library root, or the world source tree for non-library content). The
`core/requiring-modules` guide covers the root model in full.
A package that ships substantial functionality should include a `.guide` asset
(a `<name>.guide/guide.md` folder anywhere under the package); the `guides` tool
discovers it live, so collaborators find the system the way they find built-in
guides. A package's test suite reaches the shared `Test` framework through its
enclosing asset: `local Test = asset.containing(__FILE__).modules.shared`
(`asset.containing` returns the deepest enclosing typed asset, the `.testSuite`,
not the outer `.package`).
## Related types
- Any of `.component`, `.module`, `.scene`, `.preset`, `.tool`,
`.toolbox`, `.material`, `.shader`, `.style`, `.service`,
`.importer` — packages bundle these.
## In the Inspector
Package: the name, version and description package.yaml declares, how many things the package holds and what they are, and **Open** for package.yaml. A package.yaml that does not decode is listed under Problems with the decoder's error.
# profiler.compare
Diff two recordings made with `record` — the "did my optimization help, and where" view. Reports the change in avg / p90 frametime between a baseline (A) and a comparison (B), plus the systems that moved the most between them. A positive frametime delta means B is slower than A. Record a baseline, make a change, record again, then compare — this closes the autonomous optimize loop.
# profiler.flamegraph
Call-stack depth, for when attributing cost to a component isn't enough. The sampling profiler walks the Luau stack at kHz and shows the hottest call stacks — across update loops and coroutines alike — so "which line inside this update is slow" gets an answer. Reach for it after `frame` / `scripts` or `task_scheduler` shows a component hot and you need to know exactly what it's doing.
# profiler.frame
The current frame's time as a self-accounting tree — the deep view. Every schedule, system, and sub-timing shows its SELF time (own cost, excluding children), TOTAL, call count, and % of the frame; the frame-limiter / vsync wait shows as an explicit `present / idle` bucket instead of vanishing; and each heavy component's `update(dt)` is attributed by component + entity under `lua_update.vm_call`. Read the live frame with source `now`, or a stopped recording by its label (its worst or typical frame). Read from the top: the row with the largest SELF time is where the frame time actually goes.
# profiler.gpu
GPU pass timings of the most recent resolved frame, measured with GPU timestamp queries. Rows are label-aggregated — every render/compute pass recorded under one label sums into a row with its call count: `compute.<shader>` per compute dispatch, `scene.*` for the scene passes (shadow, sky, gbuffer, main, transparent, deferred_light, oit), `post.<effect>` per post-process effect, `feature.*` for render-feature geometry/draw passes, plus `skinning`, `culling`, `compose`, `raytrace`, and `ui.egui`. The header shows the frame's GPU span (first pass begin to last pass end) next to the summed pass time — the sum can exceed the span when the GPU overlaps passes. The readback is asynchronous, so the report lags the live frame by a few frames. Use it when `frame` shows the time under `present / idle` (GPU-bound) and you need to know which passes the GPU spends it on. Takes `n` (rows, default 20). Reports `supported = false` cleanly on devices without timestamp queries. Steady per-frame workloads read reliably; a lone one-shot dispatch submitted in isolation can under-report on some drivers.
# profiler.hits
Drain and review the frames a record-mode `watch` caught. The report is a DISTRIBUTION — every caught frame grouped by its dominant hotspot — so a scene that hitched twenty times over twenty seconds reads as "these three causes", not twenty separate anecdotes. All times are EFFECTIVE (the agent's own `execute` cost is excluded). Draining clears the buffer, so a subsequent `hits` returns only what was caught since. The snapshot is stored under a label, so you can drill into any cluster afterward with `frame <label>` or `hotspots <label>`. Arm the watchdog first with `watch <ceilingMs>`.
# profiler.hotspots
The fastest triage — a flat, ranked list of the frame's most expensive nodes by SELF time (own cost, not because of children). One glance answers "what is eating the frame": a system's own work, or a specific component's `update(dt)`. Follow a hit down with `frame`. Reads the live frame (`now`) or a stopped recording's worst / typical frame by its label. Takes `n` — how many rows to return (default 12).
# profiler.memory
The Luau VM's memory — total heap + GC state, then the components retaining the most bytes per instance. Frame time isn't the only budget: a script that grows its memory every frame is a leak that eventually stalls GC or crashes. Take it, play / test, take it again, and watch which component's retained bytes climb. Complements the frame-time tools — memory pressure shows up as GC cost in `frame` (the `gc` node) and as crashes under load, not as one slow system. Takes `n` — how many components to list (default 20).
# profiler.record
Start / stop / check a background profiling recording that spans a play session — the path for "it hitches while I play", which a single live frame can never catch. `record("start", label)` begins capturing every frame; play or test across as many turns as you like; `record("stop", label)` ends it and returns the windowed breakdown — avg / p50 / p90 / p99 / max frametime, spike count, and the worst frame's attributed tree. `record("status", label)` reports an in-progress recording. Every read tool (`frame`, `hotspots`, `scripts`, `tasks`) accepts a recording's label as its `source`.
# profiler.retro
Read the ring buffer's recent history RETROACTIVELY — the latency-immune profiler. Because the ring is always recording (once enabled with `ring on`), it does not matter that this call lands seconds after the spike: the data is historical. The report is a DISTRIBUTION — every spike frame grouped by its dominant hotspot — so one call shows the whole picture, not a single anecdotal worst frame that might be a random hiccup. All times are EFFECTIVE (the agent's own `execute` cost is excluded, so your profiling calls never masquerade as game hitches). The snapshot is stored under a label, so you can drill into any cluster afterward with `frame <label>` or `hotspots <label>`.
# profiler.ring
Control the retroactive ring buffer — an always-recording, bounded history of the last N seconds of per-frame data. It exists because an agent's tool calls land seconds after anything happens: with the ring on, you read the frames AROUND a spike AFTER the fact instead of having to start a recording before it (which you can't, because you don't know when the spike will come). `ring("on", seconds)` starts it (default 20s), `ring("off")` stops and clears it, `ring("status")` reports how many frames and seconds it holds. Then read the history with `retro`. Editor profile only, and off until enabled — so it costs nothing until you ask for it.
# profiler.scripts
Per-component `update(dt)` attribution — the "which of MY content is slow" view. Where `frame` / `hotspots` show engine systems, this drills the one system that runs authored code (component update loops) down to the exact component instance and entity. Rolled up per component type by default (many instances collapse to `Type xN` with summed cost; a lone instance keeps its `@ entity`); pass a `type` to drill into that type's individual instances. Use it after `hotspots` / `frame` point at `lua_update.vm_call`. Takes `n` (rows, default 20) and `minMs` (drop rows below this many ms per update, default 0.05).
# profiler.summary
The engine's own one-line performance summary, exactly as it rides a tool
response. The headline is the published smoothed frame time; when the frame is
slow, or CPU moved since the last edit, the line expands with the severity
marker, the `▲ from` delta and the systems the time went to. Takes no
arguments. Returns nil before the engine has published its first frame.
Read this when you want the figure other callers are reading. It is not the
same measurement as the rest of this toolbox: `frame`, `hotspots` and
`flamegraph` report the profiler's *effective* frame time, wall clock minus the
cost of the call doing the asking, while this reports the frame the engine
publishes to the agent surface, smoothed across frames. Composing your own line
from the other tools produces a number that disagrees with what every other
caller is told about the same engine.
It exists for callers that compose their own context. A tool response normally
carries this line, but it is deliberately omitted for a harness assembling its
own prompt, since for that caller it is per-call overhead on every answer. That
left those callers unable to read the frame at all, so a host appending its own
engine-state block either went without or measured the frame a second way.
Reading it does not move the baseline the delta is measured against. That
baseline brackets exactly one agent edit; a reader that moved it would make the
next edit's delta come out as zero, which drops the delta and the hotspot
breakdown from the line together, so a real regression would stop announcing
itself.
# profiler.tasks
Per-component coroutine-resume attribution — the content view of the task scheduler. Where `scripts` shows where component `update(dt)` time goes, this shows where the scheduler's time goes, attributed to the component whose tasks (`task.spawn` / `task.wait` / awaited promises) it resumed. Rolled up per component type (many instances collapse to `Type xN`; a lone instance keeps its `@ entity`). Use it when `frame` / `hotspots` show `task_scheduler` hot and you need to know whose task work is behind it. Takes `n` (rows, default 20) and `minMs` (drop owners below this many ms of resume time, default 0.05).
# profiler.watch
A frame-time watchdog: set a ceiling, run the scene, and the engine catches the bad frames for you. `watch(50)` records every frame whose EFFECTIVE time (the agent's own `execute` cost excluded) is at or above 50ms; `watch(50, "pause")` instead pauses gameplay the instant one crosses the ceiling, freezing the bad state for inspection (then read it with `retro`). `watch("off")` disarms, `watch("status")` reports state and hit count, and the `hits` tool reviews what record mode caught. It runs inside the engine's per-frame hook, so it is frame-accurate and immune to the very stalls it watches for — unlike an agent poll, which always lands seconds late. Editor profile only: a shipped runtime can't pause, so it is a no-op there.
# Toolbox (asset type)
A toolbox is a namespace folder that groups related `.tool/` children
plus shared helpers. Toolboxes are the addressing layer for tools —
every tool's identity is `<toolbox>.<name>`, so the toolbox name
appears in every callsite. Toolboxes also own a `shared.module/`
helper module that the toolbox's tools `require(".shared")` for
common utilities (result envelopes, validators, formatters).
## When to use one
- You have two or more related tools that belong together (an `anim`
toolbox with `play` / `stop` / `list`; an `assets` toolbox with
`compose` / `validate`).
- You need a shared helper module exclusively used by a tool set.
- You want a clean dot-namespaced identity for your tools
(`my_tools.foo`, `my_tools.bar`).
If you only have one tool and no shared helpers, you still need a
toolbox — it's the required parent.
## Where it lives
- Source: `/zero/source/.../<Name>.toolbox/`
- Identity: `<Name>` (the `.toolbox` suffix strips).
- Folder shape (container-style — open child set):
- `README.md` — instance documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
- `shared.module/` — optional shared helper module
(`init.luau` + `README.md`).
- `<x>.tool/` — any number of `.tool/` children.
The validator permits unlisted children (`allow_unlisted: true`) so
tool subfolders don't need to be declared per-name.
## How to create one
```luau
tools.createToolbox({ name = "<Name>", description = "..." })
-- or the generic scaffold:
asset.create("toolbox", "<Name>")
-- Creates: /zero/source/<Name>.toolbox/
-- README.md (instance README template)
-- shared.module/ (ok / fail result-envelope constructors)
-- `folder` places it in a subfolder of /source; `into` nests it in a
-- resolved `.package`:
asset.create("toolbox", "<Name>", { folder = "tools" })
```
Then add tools under it:
```luau
local box = asset.resolve("<Name>.toolbox")
asset.create("tool", "<toolName>", { into = box })
```
## How it operates
1. **Registration.** Writing into a `.toolbox/` folder indexes the
toolbox. It becomes the parent namespace for any `.tool/`
children.
2. **Tool identity composition.** Each child `.tool/`'s identity is
`<toolbox>.<tool>`. Listing the toolbox's tools enumerates the
children.
3. **Shared helpers.** Tools inside the toolbox pull
`shared.module/` in via `require(".shared")` (relative require
from the tool's `init.luau`).
4. **Hot reload.** Editing `shared.module/init.luau` reloads every
tool in the toolbox (they all `require` it). Editing the toolbox
folder structure (adding / removing tools) reflects in
`tools.list` immediately.
## Discovery
- `asset.list("toolbox")` — every registered toolbox.
- `asset.inspect("<name>")` — child tools, source, this type
README.
- `ls /zero/source/<Name>.toolbox` lists the files the folder holds.
- `tools.list()` — every tool across every toolbox.
## Authoring conventions
- **Per-tool implementation lives in each `<x>.tool/init.luau`.**
The tool's full function body — argument validation, the call
into engine APIs, the return value — goes in `init.luau` with
`--!desc` / `--!arg` / `--!return` / `--!example` annotations
above the function. **Every exported function is declared
`typed function`** — the keyword compiles to the runtime argument
check, so a plain `function` accepts any input and reports success
on garbage. Do NOT make `init.luau` a one-line forwarder onto a
function defined in `shared.module/`.
- Keep `shared.module/` thin: result envelope constructors
(`shared.ok(value, formatted)`, `shared.fail(message, exitCode)`),
argument validators, common formatters — anything genuinely used
by more than one tool. Per-tool logic does NOT belong here.
- Group by domain, not by surface — `assets` toolbox holds asset
operations, `entity` toolbox holds entity operations. Tools that
feel unrelated probably want different toolboxes.
- Document the toolbox's surface in `README.md` — what problem space
it covers, who calls into it, any shared invariants.
## Common pitfalls
- **Missing parent toolbox.** Tools cannot exist outside a toolbox.
Always scaffold the toolbox first.
- **Cross-toolbox shared.** Each toolbox has its own
`shared.module/`. Don't try to share helpers across toolboxes; if
you need cross-toolbox helpers, factor them into a standalone
`.module`.
- **Toolbox identity.** Strip the `.toolbox` suffix in identity
references (`my_tools`, not `my_tools.toolbox`).
## Related types
- `.tool` — the required child(ren) of a toolbox.
- `.module` — for cross-toolbox utilities.
## In the Inspector
Tools: how many tools the box holds, whether it ships a shared module, what each tool does, and a link to each tool.
# MaterialAuthor.composeFolder
Composes a whole folder of textures into a single material. It scans the folder for texture entries — loose image files and `.texture` assets alike — infers each one's PBR slot from its name (base colour, normal, roughness/metallic, occlusion, emissive), and binds them all into one new material, with the first match per slot winning. It errors when nothing maps. Returns a live material ref, named after the folder unless overridden.
This is an authoring action performed at design time: it writes a new source material asset. Never call it from gameplay code — at runtime you apply an already-authored material through the lower-level Material surface.
# MaterialAuthor.duplicate
Clones an existing material under a new name and layers overrides on top. The clone is a verbatim copy of the source — its shader, every property, and every texture binding — so it renders identically until the overrides (property values and per-slot texture swaps) are applied, making it the quick way to derive a variant like a wetter, glossier version of an existing surface. The source is given by material ref or name, and the result is returned as a live material ref.
This is an authoring action performed at design time: it writes a new source material asset. Never call it from gameplay code — at runtime you apply an already-authored material through the lower-level Material surface.
# MaterialAuthor.fromColor
Makes a flat-colour material from an RGB(A) value in one call, with options for its name, shader, roughness, metallic, and emissive so a plain tint or a simple metal is a single line. Returns a live material ref.
This is an authoring action performed at design time: it writes a new source material asset. Never call it from gameplay code — at runtime you apply an already-authored material through the lower-level Material surface.
# MaterialAuthor.fromTexture
Wraps a single texture into a new material — the fastest path from having a texture to having a material you can apply. The texture may be a texture proxy, a `.texture` asset ref or identity, or a VFS image path. Its PBR slot can be given explicitly or is inferred from the texture's name, defaulting to base colour. Returns a live material ref.
This is an authoring action performed at design time: it writes a new source material asset. Never call it from gameplay code — at runtime you apply an already-authored material through the lower-level Material surface.
# MaterialAuthor.toolbox / shared.module
> ⚠️ **Authoring-only.** Helpers for an agent-facing, design-time toolbox.
> Never used from gameplay/runtime code.
Cross-tool helpers required by each authoring tool via `require(".shared")`:
- `ok(value, formatted)` / `fail(message, exitCode)` — result envelopes.
- `createMaterial(name, opts)` — create a material via the generic
`asset.create("material", …)` path and return its live `AssetRef`.
- `SLOTS` — canonical `pbr` texture-slot names.
- `slotForName(stem)` — infer a PBR slot from a texture filename/asset stem
(`albedo`/`basecolor`/`diffuse` → base colour, `normal`, `roughness`/
`metallic`/`orm`, `ao`/`occlusion`, `emissive`).
- `textureRef(texture)` — coerce a texture proxy / AssetRef / identity / path
to the slot ref string + a display name.
- `resolveMaterial(source)` — resolve a material AssetRef / identity / name to
a live `AssetRef<material>`.
- `stripExt(name)` / `IMAGE_EXTS` — naming + image-extension helpers.
Per convention, per-tool logic stays in each `<x>.tool/init.luau`; only
genuinely shared helpers live here.
# api.docs
Browse the engine's built-in documentation. With no path it lists the top-level categories — `api`, `tools`, `guides`, `components`; with a category or namespace it lists the entries there; with a full path it returns the document content. Within the API category the namespaces cover the engine surface — entity, scene, world, physics, camera, mesh, material, asset, library, compute, postprocess, profiler, texture, video, ui, color, input, cursor, vfs, task, log, and more. Reach for this to discover documentation when you don't yet know the path.
# api.searchDocs
Search all of the engine's built-in documentation — api, tools, guides, and components — by keyword, returning the matching paths. Matching is a case-insensitive substring test on entry names. Use it to locate documentation across every category from a single term.
# shared Module
Cross-tool helpers for the `api` toolbox. Every tool inside this
toolbox can pull these in with `require(".shared")`.
## Purpose
This module holds the small set of helpers that more than one tool in
the toolbox needs — typically the `ok(value, formatted?)` /
`fail(message, exitCode?)` constructors that produce the canonical
`apiToolResult` shape, plus any toolbox-wide parsers,
validators, or shared state. Keep it focused: per-tool implementation
stays inside each `<x>.tool/init.luau` (with its own `--!desc` /
`--!arg` / `--!return` / `--!example` annotations).
## Usage
```luau
local shared = require(".shared")
function M.someTool(...)
if not ok then
return shared.fail("reason", 1)
end
return shared.ok(value, "")
end
```
## Exports
See `init.luau` for the full export surface. Typical exports include
`shared.ok(value, formatted?)` and `shared.fail(message, exitCode?)`
for building the toolbox's standard result envelope.
# localContent.forget
Stop holding a path under `/source/local/` on this machine.
The file leaves the VFS and the record of it leaves with it, which is the part that matters: a record that outlived its file would put the content back at the next boot. A folder takes its contents with it.
Content already promoted with `localContent.persist` is untouched — that copy belongs to the world, and forgetting the local one is how a session cleans up after a promotion.
```
zero localContent forget gestures/misfire.json
zero localContent forget recorded
```
# localContent.list
Every path this machine holds under `/source/local/` for the signed-in user of the bound world, with each file's size and the total.
Walks the tree with `vfs.list` and reads each leaf for its length, so what it reports is what is actually there rather than what a record claims. Paths come back in path order, so two calls are comparable.
It is the survey half of the pair: `list` to see what a session made, `persist` to move a piece of it into the world.
```
zero localContent list
```
# localContent.persist
Promote one path from `/source/local/` into the world, where it persists for everyone.
`/source/local/` is the machine's own content and never reaches a world on its own — that is what keeps a device's recordings off the backend and out of every other player's session. This is the deliberate step that says one particular piece of it should cross over. It writes to the same relative path under `/source/` unless a destination says otherwise, and takes a file or a whole folder.
The local copy stays where it is. Persisting is a promotion, not a move, so a publish that goes wrong never costs the only copy; drop the local one with `localContent.forget` once the world's copy is what you want.
Refused while play is running, and the refusal says why: a `/source` write during play lands on the play shadow, which is the exact non-persistence this tool exists to end. Open the world in edit mode first.
```
zero localContent persist gestures/wave.json
zero localContent persist gestures/wave.json --destination /source/protogen/gestures/wave.json
```
# lighting.addLight
Spawns a point or spot light entity at a position. Point is the default; pass `kind = "spot"` for a cone light with `angle`/`innerAngle`. An editor-only viz radius gizmo shows the light's range while authoring — it never leaks into play mode. Use `setLight` to update it afterwards by name, and `removeLight` to delete it.
# lighting.get
Reads the scene's full lighting state: every light (entity name, id, kind, current values), and the sky — its entity, component type, controls, and for a material sky, the material's editable parameters (what `setSky`'s `params` accepts). `sky.source = "fallback"` means the scene has no explicit sky entity and renders the engine fallback; `setSky` adds one. The shape round-trips into `setup`.
The scene's sun is one field set with one holder, so each directional light
reports `holdsSun`: the light marked `true` is the one whose direction, colour,
intensity and `castsShadows` the geometry is lit and shadowed by.
`directionals` and `ambients` are the scoped collections: one record per light
entity, each carrying `entityId`, `layerMask` (the render layer mask of the
entity carrying it — cameras whose include mask intersects it are lit by it),
`color` and `intensity`, plus `direction` and `castsShadows` on a directional
record. A light on an entity with no explicit render layer records the default
layer. A punctual light (point, spot, area, distant) carries `layerMask` on its
`lights` record instead, read from the row the renderer holds for it.
# lighting.removeLight
Removes a light entity by name — exact match first, then substring (ambiguity errors and lists the scene's lights). Despawns the entity.
# lighting.setAmbient
Sets the scene's ambient light — the flat fill that lifts everything out of pure black — with an optional color and intensity. It configures the entity carrying the scene's ambient light, spawning one when the scene has none, so the call always leaves the scene at the requested ambient level, and reports the entity it wrote.
# lighting.setLight
Updates a light entity by name — exact match first, then substring (ambiguity errors and lists the scene's lights). Only the fields you pass change; the rest stay as authored. Works on point/spot lights spawned by `addLight`, and on the sun/ambient entities. `position` moves the entity.
# lighting.setSky
Set or reconfigure the scene's sky in one call, routed through the scene's explicit sky entity (`ProceduralSky` / `Skybox` components) so every change persists, replicates, and survives reload. Finds the scene's sky entity, creating a "sky" entity when the scene has none — this is also how a scene carrying the missing-sky warning heals.
A preset word (`"clear_day"`, `"sunset"`, `"sunrise"`, `"overcast"`, `"night"`) applies that look via a `ProceduralSky` component. `"studio"` is the solid gray sky (`Skybox` over `sky_solid`). `"none"` turns the sky off explicitly (`Skybox` with `kind = "none"`). Any other word is resolved against the project's materials FILTERED to sky-domain shaders (exact identity → leaf name → substring — the name finds, the shader decides) and the sky renders that material via a `Skybox`. Omit `sky` to reconfigure the current sky with `opts` only.
`opts.params` edits a material sky's own parameters (discoverable via the `get` tool); editing a builtin material clones it into the world first and repoints the sky at the clone.
# lighting.setSun
Sets the scene's directional sun — the primary light that casts the main shadows — with an optional direction, color, and intensity. It configures the entity carrying the scene's directional light, spawning one when the scene has none, so the call always leaves the scene lit by the requested sun, and reports the entity it wrote.
# lighting.setup
Sets the whole lighting environment in one call — directional sun, ambient light, sky, and clear color — with every field optional so only the ones you provide change. It spawns the canonical sun and ambient entities when the scene has none and swaps the sky and clear color to match, leaving the scene in the requested lighting state.
# shared Module
Cross-tool resolution + introspection engine for the `lighting` toolbox.
Every tool inside this toolbox pulls these in with `require(".shared")`.
## Purpose
Tools have unlimited perf budget — full entity scans and per-candidate
asset reads live here, never in `modules/`. This module owns: finding the
scene's sky entity, resolving a word to a sky material by shader domain
(`@domain: sky` in the shader's WGSL header — the name finds, the shader
decides), reading and writing a material's editable parameters, and
resolving a light by name (exact, then substring, with an ambiguity
error that lists every candidate).
## Usage
```luau
local shared = require(".shared")
local found = shared.findSkyEntity()
local pick, alternatives = shared.resolveSkyMaterial("sunset")
local target = shared.resolveLight("torch1")
```
## Exports
See `init.luau` for the full export surface: `normalizeColor`, `vec3`,
`findSkyEntity`, `ensureSkyComponent`, `isSkyMaterial`, `listSkyMaterials`,
`resolveSkyMaterial`, `materialParams`, `applySkyMaterialParams`,
`listLightEntities`, `resolveLight`.
# camera.bindToPlayer
Make the joining player use this camera in play by binding it to the active scene's PlayerPrototype: it re-parents the camera into the prototype subtree (so it is cloned with the player on spawn), sets the prototype's `camera` role to it, and detaches + disables any displaced camera (parked idle, not deleted). Pair it with a static behavior (`@builtin::controller.menu`) for a fixed PS1-style camera the player moves within, or a follow behavior for a tracking one. A player-less scene has no prototype — there the rendering camera is simply the highest-priority one.
# camera.create
Spawn a new camera entity at a position, with any starting parameters — fov, near/far, priority, render layers, a follow target, a behavior, a look-at, or a render-to-texture setup — in one call. The scene's primary camera comes from the PlayerPrototype; use this for additional cameras (a security view, a cutscene angle, a render-texture feed). Returns the new camera's full parameter record.
# camera.frame
Position and aim a camera to frame a target entity in one step: place the camera at a distance from the target on a yaw/pitch angle, then look at it.
# camera.get
The full parameter record for one camera — position, rotation, fov, near, far, priority, render-layer spec, debug channel, render target, behavior, and follow — plus whether it is the active and/or player camera. Defaults to the active/main camera when none is named.
# camera.list
Every camera in the scene, enumerated from the live ECS so a just-spawned, hidden, or runtime camera is never missed, sorted by descending priority. Each row marks whether it is the on-screen (`active`) camera, whether it is the `player` camera (the one the PlayerPrototype uses), and its main/editor role.
# camera.lookAt
Aim a camera at a world position or at another entity. Pass a single target to aim the active camera, or a camera plus a target to aim a specific one.
# camera.set
Change one or more camera parameters in a single call: `fov`, `near`, `far`, `priority`, `debugChannel`, `renderLayers` (a layer-name spec like `"all !ui"`), `behavior` (a `cameraBehavior`-tagged component), and `follow` (the entity the behavior tracks). Pass no camera to change the active one.
# camera shared
Cross-tool helpers for the `camera` toolbox — camera resolution (by name/id/proxy, defaulting to the active camera), live-ECS camera enumeration, the parameter snapshot every tool reads or returns, PlayerPrototype and prototype-camera lookup, and flexible position parsing. Referenced only by the `camera` toolbox's own tools.
# services.attach
Reads a generation's gateway job by the `jobId` a run recorded, and returns its state and, once succeeded, its result — the provider's response, the URL or bytes the run would have downloaded. A submitted job is kept on the gateway with its result for hours after it finishes, longer than the engine that started it is guaranteed to last, so this reaches work that was already paid for once the run that started it is gone. A job whose provider refused the request (a 429 while it rate limits) reads `failed`, with the provider's HTTP status in `upstreamStatus`, its reason in `error`, and its answer in `result`. Costs nothing; the charge happened at submit.
# services.balance
Reads your current credit balance — the pool every generation spends from. Check it against an operation's cost before starting a generation. Returns the credit count when signed in, or a reason when not.
# services.describe
Describe your assets: finds the descriptions (READMEs) they are missing and
writes them, many at a call. A description is the text an asset's type asks it
to carry: for the built-in types that ask, its README, or the `intent`,
`function` and `usage` texts of a type's `about.yaml`. Each call costs 1 credit
and covers many assets. What it writes is an ordinary file in the asset, there
for you to edit.
```luau
tools.use("services", "describe", { mode = "find" }) -- what is missing, how many calls, what they cost
tools.use("services", "describe", { mode = "all" }) -- fill what that listing names
tools.use("services", "describe", { assets = { "Spinner", "Door" } })
```
## Modes
- `find` (the default) lists the assets that lack the description their type
asks for, the file and keys each needs, the number of calls filling them makes
and the total price. It writes nothing.
- `all` fills every asset `find` lists.
- `assets` fills the assets you name.
By default the assets are the ones that block the current push. `scope = "world"`
takes every asset your world holds that a type asks to describe.
## What it does
The assets are packed whole into as few calls as fit, in the order of their
names, and the calls run several at once. Each asset comes back `written`,
`failed` (with the reason) or `skipped` (with the reason). An answer that is
missing or unusable for one asset fails that asset and no other. The text is
written into the description file or keys the type declares. Text you wrote is
never replaced: it fills only what is missing or still starts with `[[describe]]`.
Library and installed assets, `/tmp` and `/local` are never touched, and a
sub-asset is left to its enclosing asset unless that asset's type makes its
sub-assets discoverable.
Add `instructions` to say more to the model, and `examples` to name assets whose
descriptions show the shape you want. The model also reads the type's own
guidance and example descriptions, and each asset's files, whole.
## Price
A call costs 1 credit, whatever it carries, and a call covers many assets.
`find` reports the number of calls the fill would make, which is also the total
in credits, before anything is spent. The credits for the whole fill are checked
before the first call, and a fill you cannot cover starts nothing. An asset whose
files alone are more than one call carries is answered `failed`, since the files
are never cut down.
# services.generate
Starts a generation on a service: the step that turns a prompt into a real asset (a 3D mesh, a texture, a sound) by spending credits. You name a service and pass that operation's inputs, including the prompt. It returns immediately with a handle because a generation takes minutes and runs in the background; when the run ends it posts a notice, `generation finished` with the handle's id and the asset to spawn, or `generation failed` with the id and the error, which reaches you on your next tool call. `services.status` reads the run's progress in the meantime.
# services.jobs
Lists the generation jobs this session has started, active and finished, newest first: the live view of what is running and where the finished ones landed. The answer is bounded. It carries at most `limit` rows (20 by default; `limit = 0` answers the counts alone) and says how many jobs matched (`total`), whether rows were left out (`truncated`) and how to read them (`more`). `status` keeps the jobs in a status (`"active"` for the ones still running, `"finished"` for completed and failed, or an exact status) and `service` keeps one service's jobs (a name that is no service and ran no job is refused, naming the services); `byStatus` and `byService` count the matching jobs.
Each row carries the job's id, service, operation, status, progress, resulting asset path and any error, with `promptPreview` (the prompt's first 60 characters) in place of the full prompt. `prompts = true` carries the full prompt on every row, and the status tool reads one job with its full prompt. For the durable record that survives restarts, use the outputs listing instead.