Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 07158574-5…●1problem

tools

Public `tools.*` surface — discovery, authoring, and inspection of code-mode tools. Installed as the global `tools` table by the prelude (via `--!global tools`).

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

tools

Public tools.* surface — discovery, authoring, and inspection of code-mode tools. Installed as the global tools table by the prelude (via --!global tools).

Tools in this engine are on-disk assets: a <name>.tool/ folder inside a <box>.toolbox/ parent, callable as tools.<box>.<name>(...). A tool's metadata comes from four sources in the folder — the --!desc/--!arg/ --!return/--!example docstring above its function in init.luau, the typed function signature, the sibling .metadata JSON (tags), and README.md. The engine parses the same four sources to bind each tool and render its docs page, so the filesystem IS the registry — there's no runtime registry to keep in sync.

Exports

  • tools.toolboxes() -> { stdout, value } — the toolbox-first overview. One row per toolbox: { toolbox, purpose, toolCount }, purpose sourced from the toolbox README.md (first descriptive line) or its .metadata description. The "what domains exist" entry point — drill in with tools.search("", { toolbox = "<name>" }).
  • tools.list(tier: (number | string)?, toolbox: string?) -> { stdout, value } — grouped discovery. Tier 1 (default): names only. Tier 2: + one-line descriptions. Tier 3: + the assembled signature / args / returns / examples. A string in either slot is treated as a toolbox name.
  • tools.search(query: string?, opts?) -> { stdout, value, total? }: discovery over the tool family, answered by @builtin::modules.discovery. opts.mode picks how a query ranks: "semantic" by what a tool means, "keyword" by the words it carries, where a hit falls by what the query's words the asset carried weigh (a word few assets carry outweighs several nearly every asset carries), then by the role that matched, then by name. A call naming no mode runs discovery.defaultMode(): semantic wherever a world is bound, keyword where none is. The search is discovery.searchTools, the question search_tools { query } asks: not in keyword mode on a bound engine, the search core ranks the tools, toolboxes and workflows of the whole published library, then this world's own matches follow (from = "world"). opts: toolbox, limit, mode. An empty query lists that toolbox's tools, or the catalogue when none is named. Each row's identity (and name) is the <box>.<tool> call path; assetType says whether a tool, a toolbox or a workflow matched, registered whether the registry holds the function a call would run, and matched the role, file, word counts and weight behind a query hit. total, on a query naming no toolbox, is how many matched in all, of which value holds the best limit. A query row also carries guid, installed (whether this world holds the hit), use (the call that acts on it: use_tool, a search_tools { toolbox } drill-in, the workflow toolbox's start, or zeromind_install for a hit this world does not hold) and p when the search core judged it.
  • tools.createToolbox(args) -> { ok, path?, identity?, error? } — scaffold a new .toolbox/. Required: name, description. Optional: sharedCode (override default shared.module/init.luau), into (a .package ref to nest the toolbox inside).
  • tools.create(args) -> { ok, path?, identity?, signature?, error? } — author a new tool inside an existing toolbox. Required: name, toolbox, code, description. Optional: args, returns, examples, tags.
  • tools.delete(identity: string) -> { ok, path?, error? } — delete the .tool/ folder for "<toolbox>.<name>".
  • tools.get(identity: string) -> ToolMeta? — the tool's assembled metadata ({ name, signature?, description, args, returns?, examples, tags }), or nil if missing.
  • tools.callable(toolbox: string, tool: string) -> string? — the identity a call names, "<toolbox>.<tool>", or nil when the registry carries no such tool. Reads the bound toolbox proxies, so a .tool folder whose source never registered a function answers nil. What a surface holding a tool's ASSET uses to say how it is called.

Usage

-- Discovery
print(tools.list().stdout)         -- summary: toolbox lines, names only
print(tools.list(2).stdout)        -- tier 2: + first-line descriptions
print(tools.list("entityOps").stdout)     -- only the `entityOps` toolbox, summary
print(tools.list("entityOps", 3).stdout)  -- only `entityOps`, full metadata per tool

-- Authoring (two steps — toolbox first, then tools inside it)
local tb = tools.createToolbox({
    name        = "mytools",
    description = "Demo tooling for greeting agents.",
})
assert(tb.ok, tb.error)

local r = tools.create({
    name        = "hello",
    toolbox     = "mytools",
    description = "Greet a name.",
    code        = "return 'hello, ' .. (who or 'world')",
    args = {
        { name = "who", type = "string", description = "Name to greet." },
    },
    returns  = { type = "string", description = "The greeting." },
    examples = { "mytools.hello('agent')" },
    tags     = { "demo" },
})
assert(r.ok, r.error)

-- Inspection — assembled metadata for one tool
print(tools.get("mytools.hello").signature)

-- Removal — deletes the .tool/ folder
tools.delete("mytools.hello")

Notes

  • Auto-installed as the global tools via --!global tools — user code doesn't require this module directly in normal use.
  • Per the project principle "tools in Luau, capabilities in Rust", no FFI calls live here — asset.* and vfs.* already cover the underlying capabilities, and tools never needed a separate runtime registry once they became proper on-disk assets.
  • tools.createToolbox refuses to clobber an existing toolbox. To extend one, edit files via vfs.write and use tools.delete per tool.
  • Every toolbox ships a shared.module/ (template default = ok / fail result-envelope constructors). The init.luau generated by tools.create automatically declares local shared = require(".shared") so the code you pass can reference shared.ok(...) / shared.fail(...) directly. The require is skipped when the toolbox is missing shared.module/, to avoid a guaranteed runtime error.
  • The asset index can lag disk for a frame or two after vfs.remove — tools.list silently skips stale entries to keep discovery clean.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

global tools

tryResolve(identity: string, category: string) → void

Safe wrapper for `asset.resolve` that returns `nil` instead of raising when no asset matches the given identity + category. The engine's binding raises a Lua error rather than the documented nil-on-miss; pcall normalises both branches so the rest of this module can use the documented `if handle == nil` shape.

argtypedescription
identitystring
categorystring

format_summary_line(toolbox: string, names: { string }) → string

Tier-1 summary line for one toolbox: `"<toolbox> (N tools) : t1, t2, …"`.

argtypedescription
toolboxstring
names{ string }

safe_read(path: string) → string

Read a VFS file, returning nil instead of raising when it is missing or unreadable. The asset index can lag disk by a frame after a `vfs.remove`, and a half-written tool may be missing a body file — neither should crash discovery.

argtypedescription
pathstring

trim(s: string) → string

Trim leading and trailing whitespace (including `\r`, so a CRLF checkout parses the same as LF).

argtypedescription
sstring

starts_with(s: string, prefix: string) → boolean

`true` when `s` begins with `prefix`.

argtypedescription
sstring
prefixstring

push_doc_line(buf: string, line: string) → string

Append a continuation line to an accumulating doc field, space-joined.

argtypedescription
bufstring
linestring

continuation_text(t: string) → string

The text a line contributes to the annotation above it, or nil when the line is not a continuation. Two forms are written in the tree. `--! text` marks the continuation with the annotation sigil. The other aligns the text under the annotation's first word: --!desc Apply a preset asset's properties to an already-installed -- character. Reads a `.preset` asset via `preset.values`, ... The alignment is what separates the second form from an ordinary comment: a note written under an annotation starts at `-- ` with a single space, while a continuation is indented to clear the `--!desc ` prefix. Both forms are accepted; a comment with fewer than two spaces ends the annotation.

argtypedescription
tstring

split_once_ws(s: string) → void

Split `s` once on its first run of whitespace into (head, tail). A bare token with no whitespace returns (token, "").

argtypedescription
sstring

strip_line_comment(line: string) → string

Drop a trailing `-- comment` from a line.

argtypedescription
linestring

match_paren(s: string, open: number) → number

Index of the `)` matching the `(` at `open` in `s`, or nil.

argtypedescription
sstring
opennumber

split_top_level_commas(s: string) → void

Split on commas that aren't nested inside `()[]{}<>` — breaks a parameter list into individual params without splitting inside a type.

argtypedescription
sstring

parse_params(inside: string) → void

Parse the inside of a parameter list into typed params plus whether it ends in a `...` variadic tail.

argtypedescription
insidestring

parse_function_line(lineRaw: string) → ParsedSig

Recover the name + signature from a function-definition line. Mirrors the engine's tool-annotation parser: handles `[typed] function [M.]name(...)` and `[M.]name = function(...)`, ignoring `local` (module-internal). Returns nil for non-function lines.

argtypedescription
lineRawstring

leafName(qualified: string) → string

Returns the leaf after the last `.`, or the whole name when unqualified.

argtypedescription
qualifiedstring

fromParen(rest: string) → void

Pull `(params): ret` starting at the first `(` of `rest`.

argtypedescription
reststring

bracket_balance(line: string) → number

The net bracket depth a line contributes — `(`, `{` and `[` against their closers, with a trailing line comment removed. `<` is left out: it opens a generic clause and also writes a comparison, and only the bracket forms close reliably.

argtypedescription
linestring

join_declaration(lines: { string }, start: number) → void

Read the declaration that starts at `lines[start]` as one line, and say how many lines it spans. A signature whose parameter list is written across several lines declares its parameters and its return type exactly as the one-line form does, so it is joined back into one before `parse_function_line` reads it. A line whose brackets already balance is its own declaration and is returned untouched.

argtypedescription
lines{ string }
startnumber

render_signature(callable: string, params: { SigParam }, returns: string?, varargs: boolean) → string

Render `callable(a: T, b?: U, ...) -> Ret` from parsed params.

argtypedescription
callablestring
params{ SigParam }
returnsstring?
varargsboolean

parse_example(raw: string) → ParsedExample

Split an `--!example` annotation into its top-level positional args and an optional trailing `-- comment`. The content is the argument expression list exactly as it appears between a call's parentheses; empty content is a zero-arg call.

argtypedescription
rawstring

with_comment(call: string, comment: string?) → string

Append a preserved trailing comment to a rendered call.

argtypedescription
callstring
commentstring?

render_example_use_tool(raw: string, toolbox: string, leaf: string) → string

Render one `--!example` for the agentic `use_tool { toolbox, tool, args }` surface, composing the tool's context toolbox + leaf with the example args.

argtypedescription
rawstring
toolboxstring
leafstring

render_example_tools_use(raw: string, toolbox: string, leaf: string) → string

Render one `--!example` for the Luau `tools.use("<box>", "<leaf>", …)` surface, composing the tool's context toolbox + leaf with the example args.

argtypedescription
rawstring
toolboxstring
leafstring

strip_line_comments(text: string) → string

Parse a tool's `init.luau` for the canonical function (whose Luau name matches the `.tool` folder stem; falls back to the first exported function) and its preceding doc block. Drop `--` line comments, keeping the shape a type definition describes. Types are documented field by field, and that prose is not part of the shape a caller needs.

argtypedescription
textstring

collect_type_aliases(source: string) → void

Named types declared in a tool's source, as `{ [name] = definition }`. A definition routinely spans several lines, so the capture runs until brackets balance; stopping at the newline would record a bare `{` and leave every field invisible.

argtypedescription
sourcestring

mentions_identifier(haystack: string, needle: string) → boolean

Whether `needle` appears in `haystack` as a whole identifier rather than inside a longer name (`Handle` must not match within `SpawnHandle`).

argtypedescription
haystackstring
needlestring

parse_init_source(source: string, toolName: string, callable: string) → void

argtypedescription
sourcestring
toolNamestring
callablestring

resetDocs( ) → void

read_metadata_sidecar(toolPath: string) → void

Read a tool's `.metadata` sidecar for tool-level `tags`.

argtypedescription
toolPathstring

read_tool_meta(toolboxPath: string, toolName: string, identity: string) → ToolMeta

Assemble a tool's full metadata from its four on-disk sources. `toolboxPath` is the parent `.toolbox/` folder; `toolName` the `.tool` stem; `identity` the tool's `<box>.<name>` identity. The signature renders leaf-only (`<name>(…)`), so the tool leaf drives it.

argtypedescription
toolboxPathstring
toolNamestring
identitystring

toolboxHidden(toolboxDir: string) → boolean

argtypedescription
toolboxDirstring

toolboxSupersededBy(toolboxDir: string) → string

argtypedescription
toolboxDirstring

registry_key(dir: string) → string

The key the engine binds the toolbox folder at `dir` under.

argtypedescription
dirstring

reachable_spelling(identity: string) → string

The spelling that reaches one toolbox by its canonical identity. A toolbox at the world's source root has the shared word itself for an identity, and one under the world's `tools/` folder an identity every tool surface reads with its `tools.` prefix dropped, so each is spelled against the world's root, the way `asset.resolve` names a root asset.

argtypedescription
identitystring

toolbox_folders( ) → ToolboxFolders

Every toolbox folder, by the key the engine binds it under, by the name a call reaches it by, and by its path. Derived from the registered TOOLS, the same index `tools.list` walks, so a folder counts here exactly when the listing shows it.

folders_named(folders: ToolboxFolders, name: string) → void

The toolbox folders `name` reaches: the one it names, every folder a shared key reaches, or the one folder any other name of its asset — its canonical identity, its path — reaches.

argtypedescription
foldersToolboxFolders
namestring

toolbox_path(name: string) → string

The folder of the one toolbox `name` reaches, or nil when it reaches none or more than one.

argtypedescription
namestring

shared_name_reason(name: string, reached: { ToolboxFolder }) → string

The reason a name several toolboxes share reaches none of them alone.

argtypedescription
namestring
reached{ ToolboxFolder }

toolboxesNamed(name: string) →

The toolboxes a toolbox name reaches, each spelled the way a call reaches it alone. A toolbox is bound under its folder's name, so two toolbox folders with the same name — one in each of two packages, say — share it: that name reaches both, and each is then reached by its own canonical identity (`pkgA.blender`, `pkgB.blender`) instead. Every tool surface reads this answer: the listing names each toolbox by it, and a call or lookup through a name reaching more than one is refused naming each (`tools.miss` states the refusal). (`"@lib::ns"`), or any other name the toolbox asset answers to — its canonical identity or its path. name reaches one toolbox, several when it reaches more, none when it reaches no toolbox holding a tool.

argtypedescription
namestringA toolbox name (`"entityOps"`), a library toolbox's scoped key

examples

tools.toolboxesNamed("blender") -- { "pkgA.blender", "pkgB.blender" }

list(tier: (number | string, toolbox: ?) →

Discover registered code-mode tools, grouped by toolbox. Default tier returns just `{ <toolbox> = { name, name, … } }` plus a formatted `stdout` listing every toolbox on one line with its tools — compact enough that listing the whole catalogue doesn't flood agent context. Higher tiers enrich each entry with its one-line description (tier 2) or its full assembled metadata — signature, args, returns, examples (tier 3). Pass a toolbox name to restrict the output to one toolbox. `2` adds one-line descriptions, `3` adds the full assembled metadata. Pass a string instead to restrict to that toolbox at tier 1. (e.g. `"entityOps"`). Passing a string as the first arg also works. For tier 1, `value` is `{ [toolbox] = { toolName, … } }`. For tier ≥ 2, `value` is `{ [toolbox] = { { name, identity, signature?, description, … }, … } }`.

argtypedescription
tier(number | stringVerbosity level: `1` (default) toolbox summary,
toolbox?Optional toolbox name to restrict the output to

examples

tools.list()              -- default: every toolbox, names only
tools.list(2)             -- include first-line descriptions
tools.list("entityOps")     -- only the `entityOps` toolbox at tier 1
tools.list("entityOps", 2)  -- only `entityOps`, with descriptions

toolbox_of(identity: string) → string

The identity's toolbox — everything ahead of the last dot, or the whole identity when it carries none, which is how a toolbox names itself. A tool's own name carries no dot, and a toolbox reached by its canonical identity (`pkg.blender`) carries its own.

argtypedescription
identitystring

leaf_of(identity: string) → string

The leaf a call names inside its toolbox, or nil when the identity names a toolbox rather than a tool.

argtypedescription
identitystring

search_entry_for(tb: string, toolboxPath: string?, leaf: string) → SearchEntry

One tool's `SearchEntry`: its `<toolbox>.<tool>` identity, its owning toolbox, and the documentation the same `read_tool_meta` assembly `M.list` uses reads for it, examples rendered in the agentic `use_tool` call form. `toolboxPath` is the toolbox's folder, nil when no folder answers for it.

argtypedescription
tbstring
toolboxPathstring?
leafstring

build_search_entries( ) → void

Read + parse every tool across every toolbox into a flat list of `SearchEntry` records, enumerated by `discover_toolboxes`. Backs the listing `M.search` answers for an empty query. Called from code that can yield, it gives the frame back between tools (`discovery.newSlice`), since a cold build reads every tool's files.

tool_corpus_signature( ) → string

A change-detecting fingerprint of the current tool corpus, built from every registered tool's `guid:checksum` (read straight off the already- loaded asset refs — no file I/O). A tool authored, deleted, renamed, or edited changes its `.meta` checksum, so the fingerprint moves whenever the assembled corpus would differ. Sorted so ref ordering never matters.

gather_search_entries( ) → void

Gather every tool's `SearchEntry`, reusing the cached corpus when the tool fingerprint is unchanged and rebuilding it otherwise.

cached_search_entries( ) → void

The cached corpus while the tool fingerprint still matches it, else nil.

search_entry_of(identity: string) → SearchEntry

One tool's `SearchEntry` by its identity, read for this one tool, so a query answering a few rows reads those tools' files and no others.

argtypedescription
identitystring

readme_purpose(body: string) → string

Extract a one-line purpose from a toolbox README body: the first non-empty, non-heading line, trimmed. Returns "" when the README has no descriptive line.

argtypedescription
bodystring

toolbox_purpose(toolboxPath: string) → string

Read a toolbox's one-line purpose: the first descriptive line of its `README.md`, falling back to the `.metadata` `description`, else "".

argtypedescription
toolboxPathstring

toolboxes( ) →

Discover the registered toolboxes as a grouped overview — one row per toolbox with its one-line purpose and tool count. The toolbox-first entry point to tool discovery: an agent navigates by DOMAIN (which toolbox), then drills into a toolbox's tools with `tools.search("", { toolbox = "<name>" })`. `purpose` is sourced from the toolbox's `README.md` (its first descriptive line), falling back to the `.metadata` `description`. `value` is toolbox-name-sorted; each entry is `{ toolbox = "<name>", purpose = "<one line>", toolCount = <n> }`.

examples

tools.toolboxes()

search(query: string?, opts: { toolbox: string?, limit: number?, mode: string? }?) →

Search registered code-mode tools, the canonical in-engine tool-discovery entry point — callable from `execute` Luau so an agent can find the tool it needs without leaving the engine. A query is answered by `@builtin::modules.discovery` (`searchTools`), the question `search_tools { query }` asks. In `semantic` mode on an engine bound to a world, the search core ranks the tools, toolboxes and workflows of the whole published library, then this world's own matches follow; a hit falls by how closely it means what was asked, so a tool is found by what it does rather than by the words it happens to use. In `keyword` mode, and on an engine bound to no world, the corpus is the files this engine holds. In `keyword` mode a hit falls by what the query's words the asset carried as words of their own weigh, a word weighing more the fewer of the assets read carry it, then by the most of them it carried side by side in the query's order, then by how many it carried anywhere, then by the order its type declares the role that matched, then by name (`discovery.keyword`). With an empty query the `toolbox` named is listed, or the whole catalogue when none is. Each row's `identity` is the tool's `<toolbox>.<tool>` identity; invoke it with the `use_tool` MCP tool (`toolbox` + `tool` + `args`, an array in signature order or an object naming the parameters), the form each row's `examples` are rendered in. indexing recipe names — for a tool, its README and its source. Empty or nil lists the catalogue, or the toolbox `opts.toolbox` names. `toolbox` — keep only rows whose owning toolbox exactly matches. `limit` — maximum rows to return (default `10`; pass a larger value to `mode` — `"semantic"` ranks by what a tool means, `"keyword"` by the words it carries. Omit it for `discovery.defaultMode()`: `semantic` wherever a world is bound, `keyword` where none is and the only corpus is the files this engine holds. `{ identity, name, assetType, description, toolbox?, registered?, signature?, tags?, examples?, matched?, guid?, installed?, use?, p?, from? }`. A query row says whether this world holds the hit (`installed`) and the one call that acts on it (`use`: `use_tool`, a `search_tools { toolbox }` drill-in, the `workflow` toolbox's `start`, or `zeromind_install` for a hit this world does not hold); `from = "world"` marks a match from this world's own content after a library ranking. A tool row's `identity` is the `<toolbox>.<tool>` a call names; a toolbox row's is the toolbox's name, which `toolbox` repeats and a drill-in takes. `registered` says whether the registry holds what `identity` names: the function a call would run, or the toolbox a drill-in lists. `matched` is the role and file a query hit matched in and what it ranked on (`weight`, `terms`, `words`, `together`); a listing carries none. `stdout` is a one-line summary followed by one line per row. `total`, on a query naming no toolbox, is how many matched in all, of which `value` holds the best `limit`.

argtypedescription
querystring?Free-text search string, matched against every file each type's
opts{ toolbox: string?, limit: number?, mode: string? }?Optional filters table.

examples

tools.search("spawn camera")
tools.search("material", { limit = 5 })
tools.search("", { toolbox = "physics" })   -- list a toolbox
tools.search("")                             -- list everything
tools.search("spawn camera", { mode = "keyword" })

row_for(identity: string, entry: SearchEntry?) → SearchRow

One row: the identity a call names, what the registry holds for it, and the documentation the corpus assembled for it.

argtypedescription
identitystring
entrySearchEntry?

render(summary: string, rows: { SearchRow }) → string

argtypedescription
summarystring
rows{ SearchRow }

entry_of(identity: string) → SearchEntry

argtypedescription
identitystring

emit_doc(parts: { string }, head: string, text: string) → void

Emit a doc field as a first `<head> <text>` line plus `--! ` continuation lines, so the rendered docstring round-trips through the engine's tool-annotation parser. Blank input emits the bare `head`.

argtypedescription
parts{ string }
headstring
textstring

build_metadata_json(tags: { string }) → string

Build the `.metadata` sidecar body — the tool-level `tags` array. The array form is written explicitly so empty tags serialise as `[]`, never an empty object.

argtypedescription
tags{ string }

build_tool_readme(fullName: string, description: string) → string

Build the README.md body for an authored tool. The detailed per-function surface lives in the `init.luau` docstring; the README is the long-form human overview.

argtypedescription
fullNamestring
descriptionstring

build_toolbox_readme(name: string, description: string) → string

Build the toolbox README body for a newly-authored toolbox. The shape mirrors what authors usually write by hand: a heading, the supplied one-paragraph description, and a stable `Tools` section header so future `tools.create` calls can find a known place to list themselves (callers may extend this convention later).

argtypedescription
namestring
descriptionstring

folder_box(folder: ToolboxFolder) → void

argtypedescription
folderToolboxFolder

__index(_: any, key: any) → void

argtypedescription
_any
keyany

resolve_box(name: string) → void

Resolve an installed toolbox to its callable proxy, or nil when none is registered under `name`. Toolboxes live in the private `tools.__scoped` store keyed by the string a caller names them by: an ambient toolbox by its global name (`"entityOps"`), a library toolbox by its scoped identity (`"@lib::ns"`). The proxy's methods return the `ZmToolResult` envelope (`{ ok, value | error }`); the engine's tool-bind path populates the store. This module-local resolver is the only reader of the store, so a toolbox is never a dot-accessible `tools.<ns>` namespace. Several toolbox folders the engine bound under one key share that entry, so a key reaching more than one folder resolves to none, and the second value is the reason, naming each. Each such folder is reached by its own canonical identity, through a toolbox of its own (`folder_box`); a folder no other one shares a key with is reached by its identity too, through the entry the engine bound it under.

argtypedescription
namestring

registered_tool(identity: string) → any

The function a toolbox carries under `identity`'s leaf name, or nil when no toolbox carries it. A scoped toolbox raises on a name it does not hold, so the read is guarded. Authoring a tool's source registers a new function in its place, which is what makes this value stand for the declaration a call would run against.

argtypedescription
identitystring

callable(toolbox: string, tool: string) → string

The identity a registered tool is called by — `<toolbox>.<tool>`, the pair `tools.use` takes — or nil when no toolbox registered under `toolbox` carries `tool`. Reads the bound toolbox proxies, so it answers for what this engine can call rather than for what the asset tree holds: a `.tool` folder whose source never registered a function answers nil. A name several toolboxes share reaches no one tool and answers nil too; `tools.toolboxesNamed` names the toolboxes it reaches. scoped identity `"@lib::ns"`, or a toolbox's canonical identity `"pkg.blender"`).

argtypedescription
toolboxstringThe toolbox's registered name (`"zm"`, a library toolbox's
toolstringThe tool's leaf name (`"status"`).

examples

local callable = tools.callable("zm", "status") -- "zm.status"

registeredToolbox(name: string) → string

The name a drill-in lists a registered toolbox by, or nil when the registry holds no toolbox under `name`. Reads the bound toolbox proxies, the same ones `tools.callable` reads, so it answers for what this engine can call rather than for what the asset tree holds. A name several toolboxes share reaches no one toolbox and answers nil; `tools.toolboxesNamed` names the toolboxes it reaches. identity `"@lib::ns"`, or a toolbox's canonical identity `"pkg.blender"`).

argtypedescription
namestringThe toolbox's registered name (`"zm"`, a library toolbox's scoped

examples

local box = tools.registeredToolbox("zm") -- "zm"

forgetBindSchemas( ) → void

Drop every held schema.

bindSchema(identity: string) → ToolMeta

The schema for `identity`, or nil when no tool carries one. A found schema is held against the tool's function. A miss is asked again on the next call: whether a name reaches a tool changes as the world's content does (a toolbox arriving, or a second one of the same name leaving) while the tool's function stays the same.

argtypedescription
identitystring

createToolbox(args: { [string]: any }) →

Create a new toolbox folder. A toolbox is the namespace container for tools — `.toolbox/` on disk; once tools are authored inside it they are invoked as `tools.use("<name>", "<toolName>", …)`. This call scaffolds the `.toolbox/` folder via `asset.create("toolbox", …)` and overwrites the placeholder `README.md` with a real description so the toolbox doesn't ship the template stub. Optionally seeds `shared.module/` if the caller supplies cross-tool helper code. Authoring a tool inside this toolbox is the separate `tools.create` call. Required: `name` (the toolbox's leaf name, e.g. `"mytools"` — becomes `<name>.toolbox/` on disk and the namespace for tool identities), `description` (one or more sentences describing the surface this toolbox exposes — what problem space the tools cover and who calls them). Optional: `sharedCode` — replacement body for the toolbox's `shared.module/init.luau`. The toolbox template ALWAYS ships a `shared.module/` with default `ok`/`fail` result-envelope constructors so tools in this toolbox can `require(".shared")` from day one. Supply `sharedCode` only when you want to override that default with custom cross-tool helpers (parsers, registries, …); the module body is left untouched if you omit it. `path` — explicit VFS destination (`/zero/source/...` form). Defaults to `/zero/source/tools/<name>` if omitted; pass an explicit path to author a library-scoped or package-internal toolbox. `path` — the new `.toolbox/` folder's VFS path. `identity` — the toolbox's registered identity (`<name>`).

argtypedescription
args{ [string]: any }Structured toolbox definition.

examples

tools.createToolbox({ name = "mytools", description = "Custom tooling for my workflow." })

create(args: { [string]: any }) →

Create a new tool on disk inside an EXISTING toolbox. Scaffolds the `.tool/` folder via `asset.create("tool", …)`, then writes the supplied code wrapped in a documented typed function into `init.luau` (the `--!desc`/`--!arg`/`--!return`/ `--!example` doc block + signature built from the structured metadata), the tags into `.metadata`, and a brief `README.md` — so the authored tool is indistinguishable from a builtin. Errors cleanly if the parent toolbox doesn't exist — call `tools.createToolbox` first so the toolbox starts out with a real description instead of the placeholder template body. The engine's normal hot-reload pipeline picks up the new files and binds the tool's global on the next pass. Every toolbox created via `tools.createToolbox` ships a `shared.module/` (the template default has `ok`/`fail` result-envelope constructors; users can extend or replace it via `sharedCode` at create time or by editing the module later). The generated `init.luau` automatically declares `local shared = require(".shared")` before your function body, so the `code` you pass can reference `shared.ok(...)` / `shared.fail(...)` (or whatever the toolbox's custom helpers expose) directly. If a toolbox doesn't have a `shared.module/` for some reason, the require is skipped so there's no dangling import to fail. Required: `name` (leaf, e.g. `"hello"`), `toolbox` (parent toolbox, e.g. `"mytools"`), `code` (the Luau function body — not a full module, just the statements that will become the function's body), `description` (one or more sentences describing what the tool does). Optional but recommended: `args` (array of `{name, type, description}` argument records), `returns` (`{type, description}` for the return value), `examples` (array of call-site code strings), `tags` (array of strings for search). `path` — the new `.tool/` folder's VFS path. `identity` — the tool's `<toolbox>.<name>` identity. `signature` — the derived `name(args) -> ret` signature.

argtypedescription
args{ [string]: any }Structured tool definition.

examples

tools.create({ name = "hello", toolbox = "mytools", description = "Greet.", code = "return 'hi'" })

delete(identity: string) →

Remove a previously-authored tool by identity. Deletes the `.tool/` folder and its contents from the VFS via `vfs.remove(..., { recursive = true })`. The engine drops the tool's global on the next hot-reload pass. Refuses to operate on a path that doesn't exist (returns `{ ok = false }` with an explanatory error).

argtypedescription
identitystringTool identity (`"<toolbox>.<name>"`).

examples

tools.delete("mytools.hello")

get(identity: string) → ToolMeta

Read back a tool's assembled metadata — description, typed signature, per-argument docs, return, examples, and tags — gathered from the four canonical sources in its `.tool/` folder. Individual tools aren't entries in the asset index (only their parent toolboxes are), so this resolves the toolbox via `asset.resolve(toolbox, "toolbox")` and reads the tool relative to it. Returns `nil` when the toolbox or tool is missing. leading `tools.`). varargs, returns?, examples, typeDefs, tags }`, or nil if not found. Each entry in `args` is `{ name, type, description, optional }` in signature order, so a caller can tell a required parameter from an optional one without parsing the rendered signature. `varargs` is true when the tool accepts trailing arguments beyond the named parameters. `typeDefs` carries `{ name, definition }` for every named type the signature refers to, resolved transitively, so a signature reading `spawn(opts: SpawnOpts)` can be called without opening the tool's source.

argtypedescription
identitystringTool identity (`"<toolbox>.<name>"`, with or without the

examples

local meta = tools.get("entityOps.modify"); print(meta.signature)

admits_table(declared: string?) → boolean

Whether an argument declared with this type could be filled by a table. Read from the declared text: a type written entirely out of the scalar primitives cannot hold one, and everything else — a table literal, a type alias, an argument carrying no annotation — can.

argtypedescription
declaredstring?

unknown_arg(name: string) → BindFailure

A name the tool declares no parameter for, said the same way wherever one turns up: in a payload a `whole` surface handed over, and in the named arguments placed further down.

argtypedescription
namestring

slot_for(name: string) → number

The slot a name fills, or 0. An exact match wins outright; the case-insensitive pass after it is what lets a caller write `--basecolor` for `baseColor` without the shell knowing the tool's spelling.

argtypedescription
namestring

did_you_mean(candidates: { string }, name: string) → string

`name` was not found among `candidates`: the nearest real name when there is one, then a bounded sample of the set, so a refusal carries what to write instead rather than only what was wrong.

argtypedescription
candidates{ string }
namestring

toolbox_names( ) → void

Every registered toolbox name.

global_namespace_hint(toolbox: string, tool: string) → string

What to say when the name is not a toolbox but IS a Luau global namespace. The two surfaces read alike in prose — `logs.errors()` and the `debug` toolbox's `problems` are both "a thing you call by a dotted name" — so a reader following an instruction that names one reaches for the other, and a listing of the 38 toolboxes it is absent from answers a question they did not ask. Naming the global says where the call they already have actually lives. Returns "" when the name is no global, which is every ordinary typo.

argtypedescription
toolboxstring
toolstring

tool_names(toolbox: string) → void

Every tool name a toolbox carries. Read from the listing rather than by walking the toolbox table: a scoped toolbox materialises a tool the first time it is reached, so its own keys are only the ones already used.

argtypedescription
toolboxstring

unknown_toolbox(box: string) → string

Why `box` reaches no toolbox. A tool's name written where the toolbox goes is answered with the toolboxes holding that tool, spelled as the call that reaches it; any other name with the nearest toolbox names.

argtypedescription
boxstring

miss(toolbox: string, tool: string?) → string

What every tool surface says when a toolbox and tool reach no single tool: a toolbox name several toolboxes share names each toolbox it reaches, a toolbox with no tool of that name lists the tools it holds with the nearest one, a tool's name written as the toolbox names the toolboxes holding that tool, and any other name reaching no toolbox lists the nearest toolboxes. `tools.use`, the `use_tool` / `describe_tool` MCP tools and `zero` in the engine shell all refuse with this reason, so the same name is refused the same way wherever it is written.

argtypedescription
toolboxstringThe toolbox name the caller wrote.
toolstring?The tool name the caller wrote, when there is one.

examples

tools.miss("blender", "probe")

invoke_tool(toolbox: string, tool: string, ...: any) → void

Resolve and call one tool, classifying the outcome. Returns `(kind, payload, ...)` where `kind` is `"ok"`, `"no-toolbox"`, `"shared-name"` (a name several toolboxes share), `"no-tool"`, or `"failed"`, `payload` is the tool's value for `"ok"` and the reason otherwise, and everything past it is what the tool stated beyond its first value. The single place a tool call is made, so the raising and the reporting form cannot drift apart. The call runs under `pcall`: a tool that raises rather than returning a failed envelope is a failure of the call like any other, and a caller asking for the outcome must not have the script torn out from under it.

argtypedescription
toolboxstring
toolstring
...any

under_tool(toolbox: string, tool: string, reason: string) → string

A tool's failure said under the tool's name. A tool that already reports under its own qualified name says it once: the reason is returned as it was raised, so the reader is told which tool failed rather than told twice.

argtypedescription
toolboxstring
toolstring
reasonstring

use(toolbox: string, tool: string, ...: any) →

Invoke one code-mode tool by naming its toolbox and tool explicitly — the Luau-code counterpart to the `use_tool` MCP tool. The normal path is the `use_tool` MCP tool; this is the escape hatch for editor panels and shipped modules that script a tool from engine Luau. Resolves the toolbox from the runtime store (builtin, library, and user-authored runtime toolboxes all work), calls the named tool with the remaining args, and envelope and RAISING a Luau error when the tool fails. Because every call names both toolbox and tool, it can never read as a `tools.<box>` namespace. `tools.tryUse` takes the same arguments and reports the outcome as a value instead of raising. or a library toolbox by its scoped identity (`"@lib::ns"`). declaring `(path, reason, frame)` hands back all three, so a second and third value the tool states are read the way the tool's own signature says. Raises on an unknown toolbox / tool or a tool-reported failure. These are the tool's OWN values, not the `{ ok, value }` envelope the `use_tool` MCP surface reports — reach for `tools.tryUse` when the call's outcome is what you want.

argtypedescription
toolboxstringThe owning toolbox — an ambient toolbox by its name (`"entityOps"`)
toolstringThe tool's leaf name within that toolbox (`"spawn"`).
...any

examples

use("entityOps", "spawn", { Model = { model = "cube" } }, { position = {0, 2, 0} })

tryUse(toolbox: string, tool: string, ...: any) → ToolCall

Invoke one code-mode tool and report the outcome as a value. Takes the same arguments as `tools.use` and resolves the toolbox the same way, and unknown toolbox, an unknown tool, a tool that reported failure, and a tool that raised all arrive as `ok = false` with the reason in `error`. The fields are the ones the `use_tool` MCP surface reports, so a script comparing several calls in one pass reads the same names it would over MCP, and reads them without wrapping each call in `pcall`. or a library toolbox by its scoped identity (`"@lib::ns"`). value when `ok` is true; `error` carries the reason when it is false.

argtypedescription
toolboxstringThe owning toolbox — an ambient toolbox by its name (`"entityOps"`)
toolstringThe tool's leaf name within that toolbox (`"spawn"`).
...any

examples

tryUse("entityOps", "spawn", { Model = { model = "cube" } })
⌬ Types
BindFailure = {BindResult = {BindOpts = {ToolCall = {

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing the 199 items nearest this part. It holds more than this page lists.

199+items
❒
package · born here
❒asset
▲ 1↑ born
·
assettype · born here
❒asset
# 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**.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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 ```
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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).
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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`.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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()`.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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`).
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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`.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
❒
package · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
toolbox · born here
❒asset
# 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`.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
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 |
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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>`.
▲ 0↑ born
·
tool · born here
❒asset
# 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).
▲ 0↑ born
·
tool · born here
❒asset
# 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).
▲ 0↑ born
·
tool · born here
❒asset
# 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`.
▲ 0↑ born
·
tool · born here
❒asset
# 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>`.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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).
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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).
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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 ```
▲ 0↑ born
·
tool · born here
❒asset
# 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 ```
▲ 0↑ born
·
tool · born here
❒asset
# 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 ```
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# lighting.removeLight Removes a light entity by name — exact match first, then substring (ambiguity errors and lists the scene's lights). Despawns the entity.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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`.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
backing path · modules/tools.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

These counts cover only the parts this page shows. Problems on the parts it leaves out are not counted here.

1+problem

Inside

1 problem across 1 item
●
dep.unresolved · L937
could not resolve `@builtin`
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.