# tool (asset type)
A tool is one named operation: a single luau `typed function` inside a toolboxe's `<name>.tool` asset,
callable as `<toolbox>.<tool>`. The function's signature is the tool's interface,
so anyone can call it correctly without reading its code.
A tool is also a shell command, `zero <toolbox> <tool> <args>`; `guides core/tools`,
"A tool's command line", covers its flags, paths, `render.luau` and exit status.
Each tool has a declared input shape and return shape; the engine wraps each call's
result in an envelope.
## Why use a tool?
Tools are typed operations with a known serialized output format, made to enable efficient operation in the engine and provide an easily callable interface for operations around a topic.
They are also useful in that they can be called from a multitude of surfaces, from `execute`, to `bash` to `tool_exec`
to users Tools portion in the edui.
Compared to say a `module.assetType`, that might contain multiple functions with varying shapes, experct references or specific formats of tables as input and could return potentially anyting including flat tables.
that has multiple callable functionm in that they might return anything a tool is meant to be called with a serialized value, and return a serialized value.
A tool is meant to perform an self contained operation and return results/info/relevant data in a readable format, including eg. image data.
Whenever you identify an operation that requires specific orchestration and or is something that could be a repeated operation and makes it easier to manage the project/task or when you want to expose logic written in `luau`
to the other call surfaces like `bash` under its `zero` command, it propably should be a `<name>.tool` asset.
## How tools are written
In its simplicity, a tool is a single Typed luau function that returns a value.
written plainly, in its basic form it could look like this.
```luau
-- probe.toolbox/onCall.tool
--
-- Fire when a named function runs, seeing its arguments and what it returned.
--- A toolbox offered shared module, that in this case,contains a type shared by the tools in their toolbox.
local shared = require(".shared")
local M = {}
export type OnCallResult = {
ok: boolean,
id: string?,
result: string?,
error: string?,
}
--!desc example tool
--!arg path target path for the operation
--!arg opts `{ notice, when?, run?, capture?, label?, limit?, cooldown?, freeze?, silent? }`.
-- `capture` is Luau, compiled against the hit's context the way `run`
-- is, that returns a picture for the hit — a capture tool you aim:
-- `return tools.use("capture", "fromEntity", { entity = entity })`. It
-- runs once the hit is recorded, and the hit's notice then carries the
-- picture under `shot`, delivered as a picture.
--!return `{ ok, id, result }`.
--!example "arg1", arg2
typed function M.gatherResults(path: string, opts: shared.ArmOpts): OnCallResult
local modulePath, key = splitPath(path)
--- the tool code implementation
local results = myCodeResults
--- the tool call returns a table with keys, that is then wrapped in a return envelope.
return { ok = true, id = results.id, result = results.gathered }
end
return M
```
Now when called, the tool runs and returns the envelope with the inspectable results.
## Why return plainly, and fail by raising
Every caller gets a tool's outcome in the same form, success or failure, so
return the value itself and raise an error when the tool can't do its job. A
tool that returns its own `{ ok = false }` reads as a success that happened to
return that table. Make the error say what to do instead: the caller reads it
and acts on it.
## What the shell prints
`zero <toolbox> <tool>` prints the text the tool's `render.luau` states: a
module in the tool's folder returning one function `(result) -> string |
{ out, err?, exit? }`. A returned string is the output, and `{ out, err?, exit? }`
sets the output, the standard error text and the exit status. A call that raises
exits 1, and a command line the shell cannot bind to the tool's parameters exits 2.
## When to answer later instead of blocking
A caller waits for the tool to return, and might be calling multiple tools at the same time.
Should your call run past the waiting threshold in order to return, even if it yields
it comes back as a handle to a background task that will then complete on its own,
but require the caller to separately read the task results.
For a call that is small, answering inline with the result is better. When a
tool can tell that this particular call will be long (a big bake, a large
batch, a slow fetch), it hands itself to the background with
`task.background()` early, so the caller gets the handle at once and carries on
with other work.
## When to post a notice
The return value is the answer to what was asked. A notice is for what the
caller needs to know besides that: a side effect it didn't ask for, a return to one that has no caller,
notices should be treated as a way to make sure your tool can return quickly, when you arent sure how
long it takes for the results to be ready. It allows the agent to continue working, and then react when
the notice fires. `notices.post` delivers it to the caller,
and an agent that is idle is woken by it, so the caller learns it without
polling. Name in it what to do about it. A notice for something the return
value already says is noise.
## Returning more than text
Tools also support returning images, that the agent can then directly read from the tool response, regardles
of what surface they used to call the tool.
Images also work on notices, so you can eg attach visual results to the eventual outcomes of
your tool results and the agent will see them the second the notice fires.
## What a tool may do that other code may not
A tool, and code only tools call, may act on whatever an input names, resolving
an asset by a name that only arrives with the call. That is what makes a tool
the place for operations that depend on what they are given. Tools are for
working on a world rather than running in the game: a component may not call
one unless it declares `declare { componentSafe = true }`, and a tool doesn't
call another tool, so logic several tools need goes in the toolbox's shared
module or exist elsewhere.
For information how to group tools and organize them, and how they are called read `toolbox.assetType` readme.
## In the Inspector
Tool: how the tool is called (its signature), what it does, each argument, what it returns, and its examples, with **Open** for its source.
# procgen
The procedural-generation tool family: find the nodes a graph is built from,
edit a graph's code, preview it, try values for its inputs, inspect it, spawn a
live generator to iterate in the scene, and watch cooks as they run. A graph is
the code of its `init.luau` (`proc.define(function(g) ... end)`); the tools here
read that code, edit it, picture it and run it.
Each tool is called as `tools.use("procgen", "<tool>", ...)` from Luau, as
`zero procgen <tool>` in the engine shell (`zero procgen <tool> --help` gives its
arguments), and through `use_tool` over MCP. A tool's README and
`describe_tool` give its arguments and what it answers.
## Discovery
- `searchNodes`: find nodes, the counterpart of `search_tools`: every node a
graph calls (`g.<family>.<name>`, and the ops of packs as `g.op.<id>`), by
keyword over names, families, inputs, outputs and descriptions and, in a
session bound to a world, by meaning through ZeroMind. With no query it
names every family and the nodes in it.
- `describeNode`: one node in full, the counterpart of `describe_tool`: what
it does, its inputs in call order with their types, defaults, ranges and
options, its outputs, its family, the call a graph writes and, for an op of a
pack, what it costs.
- `search` — free-text search over the `.procGraph` graph library (name /
id / description / tag / category).
- `find` — discover graphs by what they *produce* (an output type or
producing-op substring, e.g. `{ produces = "Bundle" }`).
- `inspect` — one node's op, sockets, producers, and consumers.
- `coverage` — every current op with the new node that replaces it and its
parity state, and every library graph with the worst state among the ops it
names.
- `stats` — structural counts (nodes / composites / inputs / outputs),
plus evaluated-output stats when a `target` is given.
- `trace` — the dependency chain for a target, in evaluation order.
- `explain` — an agent-oriented natural-language summary of a graph.
- `diff` — a structural diff between two graphs (changed/added/removed
nodes, per-node deltas).
- `validate` — check a graph the way its cook will: that its source builds,
its connections (type mismatches, missing inputs, unknown nodes and params),
then what a cook of its first frame refuses (bounded by `timeoutMs`), and the
nodes that run on the CPU every frame.
- `generators` — survey every live procedural generator in the scene:
bound graph, params, realized child count, and cook state.
## Edit the code
These edit a graph's (or node's) `init.luau` as code and write it only once it
builds, so the graph stays valid. A section is a run of statements named by the
lines it spans; a call is named by the local it declares or a line it spans.
- `collapse`: lift a section (a few node calls doing one job) into a node of
its own: its inputs are the values the section reads, its outputs the values
the code after it reads, and the section becomes one call of the new node.
The graph cooks to the same result, and `searchNodes` finds the node.
- `flatten`: the inverse: write a node's body out in place of a call to it
(one call, every call of a node, or every call of a node outside the
built-in library).
- `reference`: put a call of an existing node in place of a section.
- `promote`: make a part (a `.procGraph` called by its path) a node found by
name, which stays the part's.
- `patch`: set and unset what one call is given.
- `modify`: structural edits as one change: add, remove and repoint calls,
rename locals, declare inputs and outputs.
## Try it
A tool that takes a graph to preview or run takes it by its VFS path or its
name, or as a graph object (what `proc.define`, `proc.graph` and `proc.load`
return); `spawn` and `play` take a saved graph, since a generator plays an asset.
- `tweak` — preview a graph with other values for its declared inputs, in a
single step, leaving its source as it is: the knob-tuning loop.
## See it / make it real
- `preview` — evaluate one target (an output or a node) of a graph and render
it for any output type (Geometry, points, Field heatmap, Material ball,
Texture, Bundle); the picture comes back in the result beside its stats.
- `spawn` — spawn a live procedural generator from a saved graph: an entity
with a Generator component bound to it, which cooks the graph on its own
runner and cooks again as its params change.
- `adjust` — adjust a live generator in place: merge param overrides or
select another output.
- `apply` — swap a live generator in place for the asset it makes: its
current output becomes a durable bundle, and an Asset component standing it
replaces the Generator, like applying a modifier.
- `bake` — evaluate one target of a graph and realize it straight into the
scene as durable content, without a live generator.
## Play it
- `play` — play a graph on the preview stage with nothing added to the scene: it
plays from its first frame with the inputs given (each a value or a path over
the play's time), for `seconds`, and answers the profile of that play and a
picture of what it draws at each time of `at`.
## Cook observability
- `cooks` — the scene-wide cook monitor: every in-flight cook, a bounded
history of recently finished ones, with progress (the nodes cooked so far and
the node running now), status, duration, and — on failure — the error and the node
that broke, and the frames generators presented most recently.
- `cancel` — cancel an entity's in-flight cook; it stops at its next
checkpoint and the previous output stays realized.
- `onPresent` — be told the moment a generator presents the frame you
describe (an output, a graph time or a time within a repeating cycle, an
element count), with the probe's own options beside the terms: `capture` a
picture of that frame (what the generator draws alone, framed to fit, over
the grey studio backdrop), `freeze` the generator and the world on it (the
generator plays on with `generator:play()`), a `when` and `run` of your
own. It arms a probe, so `probe.list`, `probe.hits` and `probe.cancel`
manage it.
- `frames` — see how an effect goes: a picture of the frame a generator
presents at each graph time you name (or `count` of them from `from` to
`to`), each of what it draws alone, framed to fit, over the grey studio
backdrop an asset preview is photographed in, with the frame, how it was
presented and each output's element count beside it. A time past the end
of a timeline shows its last frame, with a `note` saying so; a picture of
a frame that draws nothing, or one that holds the backdrop alone, says so in
its own `note`. A generator whose cook failed raises, naming the node and
the reason. The clock goes back where it stood after.
- `profile` — what evaluating graphs has cost, read from what was recorded as
the work ran: every graph's run and wait time, its nodes costliest first,
the verdicts on what was paid again, saved runs to compare against, and one
graph invoked once with what that invoke cost.
# assetType (asset type)
An asset type says what a kind of asset is: the files an asset of the kind is
made of, the code that gives it behaviour, the interface a person uses to work
with it, and how it is found, pictured and checked. A `<typename>.assetType/`
folder declares a type called `<typename>`, and every `<name>.<typename>/`
folder is an asset of it. `assetType` is itself a type, so a type is an asset
like any other: it is created, versioned, installed and contributed the same way.
## Making a type
`asset.create("assetType", "<typename>")` scaffolds a type. The type registers
the moment its folder is written, anywhere under `/zero/source/`, and from then
on `<name>.<typename>/` folders are its assets. An asset of a type that is not
registered yet is not an asset, so make the type first.
A type folder is a root: `~.x` in any file inside it names the type's own `x`,
wherever the type is installed. Keep the type's whole implementation inside its
folder and reach it that way.
What a type folder must hold is set by this type's own `type.yaml`, the same way
every type's `type.yaml` sets what its assets hold. `asset.validate` on a type
reports anything missing, and the scaffold produces a folder that passes.
## What sets your type's schema (`type.yaml`)
`type.yaml` is the structural contract every asset of the type is checked
against. The keys below are the ones the engine reads; a key not listed here is
ignored.
| Key | What it controls |
|---|---|
| `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. |
| `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. |
| `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. |
| `is_container` | Whether the suffix stays in the identities of what the asset holds. |
| `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. |
| `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. |
| `is_text_default` | Whether the asset's files are text unless stated otherwise. |
| `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. |
| `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. |
| `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. |
| `files: { required, optional }` | The files an asset holds (below). |
| `allow_unlisted` | `false` reports every file the type does not declare. |
| `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. |
| `settings:` | Typed per-asset settings (below). |
| `inspector:` | The interface people use for the type and its assets (below). |
| `preview:` | How an asset is pictured (below). |
| `references:` | Names of other assets the type's files carry (below). |
| `indexing:` | What search reads for each asset (below). |
| `describe:` | How a missing description is generated (below). |
### The files an asset holds
Each entry under `files.required` or `files.optional` is
`{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches
within one segment. Entries that share a `one_of_group` are satisfied by any one
of them. `content` lists checks on the file's bytes:
| Check | Passes when |
|---|---|
| `non_empty` | the file has content |
| `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape |
| `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML |
| `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") |
Each check takes a `message`, which is what the author is told when it fails.
### Settings
`settings: { values, schema: { type | instance, export? } }` gives every asset
typed settings. `values` names the file an asset's values live in; `schema.type`
declares one schema for the whole type, and `schema.instance` names a file each
asset declares its own in. `asset.create` writes the resolved values, and a write
to the values file reaches `onChange` with what changed.
## The human side (`inspector.luau`)
A type is used by people through its Inspector, and without one the type is
usable only from code. The Inspector is the type's interface for a person: for a
procedural graph it is the graph editor, for a kit it is the tracking view of
what the kit holds and what is still missing. Writing it is part of writing the
type.
`inspector: { type, instance? }` in `type.yaml` names the files. `type` is the
interface for the type itself and for its assets; `instance` names a file an
asset may ship to add its own. Each returns:
```luau
return {
sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } },
replace = { contents = { ... } }, -- replace a default section by id
order = { "graph", "contents" }, -- section order
}
```
`build(ctx)` receives the asset and builds the section. A section's `title`, and
the text its rows carry, is a string or an `i18n.text` value read from a catalog
(`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType",
...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a
type that names no inspector, or names one that does not load, reports it.
## Say what your type is for (`about.yaml`)
Every type carries an `about.yaml` that says what the type is for, in three
plain-language texts:
```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, in the words a game maker uses. Up to 800 bytes.
- **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes.
- **`usage`** is what a person or an agent does with one. Up to 1200 bytes.
Write about the type as a whole, not one asset of it, and say what it does now.
Agents read these texts to learn when to reach for the type and how; people
searching find the type by them. The scaffold starts each text with
`[[describe]]` and a note on what to write; a text that still starts that way
counts as not written. `services describe` can write them.
They show up in `asset.inspect` as `detail.about`, in
`assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide,
and at the top of the type's Inspector.
## Ask your assets to say what they are
A type can require each asset to carry a description: a file, written by the
author or generated, saying what the asset is and what it is for. Declare it with
a `described` check:
```yaml
files:
required:
- path: "README.md"
kind: "file"
content:
- { check: "described", message: "Say what this widget is for and where it goes." }
```
A file that is missing, empty or still has a line starting with `[[describe]]`
fails the check, and so does a YAML file whose named `keys` are missing or over
their `max_bytes`, or a Markdown file missing a named heading. `asset.validate`
names the asset, and a push that carries it is refused with `message`.
`services describe` finds such assets and writes their descriptions.
`describe:` steers what is generated:
```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>"
```
An example is an asset of your type whose description is a true, written one,
never a placeholder and never the asset being described; with none, leave
`examples` empty. A type whose assets the model cannot perceive (a sound, a
motion) says so in `guidance`, and the description is drawn only from what the
asset's own files state.
Start the file unwritten in your `template/`: a heading and one line starting
with `[[describe]]`, never example content, which every new asset would carry
verbatim. A type whose assets say what they are through their own content (a
mesh, a texture) declares no check.
## The type's code (`behavior.luau`)
`behavior.luau` returns one table. These are the keys the engine reads from it;
anything else on the table is the type's own.
### What the type's assets can do
| Key | Shape | What the engine does with it |
|---|---|---|
| `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. |
| `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. |
| `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. |
| `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). |
| `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. |
| `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. |
Some `ref` names mean the same for every type, and the engine calls them:
- **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below).
- **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below).
- **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe.
- **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints.
- **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode.
Some names are answered for every asset before the type is asked, so a `ref`
entry of the same name is never reached: `getSource`, `getBytes`, `getText`,
`exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours
when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the
residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`,
`gpu_resident`.
### When the engine calls the type
| Hook | Signature | When it runs |
|---|---|---|
| `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. |
| `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. |
| `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. |
| `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. |
| `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. |
| `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. |
| `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. |
### Settings hooks
| Hook | When it runs |
|---|---|
| `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. |
| `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. |
| `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. |
| `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. |
| `settingsHealed(values, stored)` | On a read, to repair stored values. |
| `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. |
`creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options
that are inputs to the type rather than settings.
### Reloading
An edit to `behavior.luau` reaches existing assets with no restart.
## How an asset is pictured (`preview:`)
| `kind` | The picture is | The type provides |
|---|---|---|
| `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` |
| `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order |
| `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` |
| `custom` | whatever `ref.preview` returns | `ref.preview` |
```yaml
preview:
kind: surface
mesh: sphere
```
`angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop
and the framing, so an asset pictures the same in every world. A type with no
`preview:` that defines `instantiate` still answers `ref:preview()`.
A picture is rendered when something asks to view it (the editor showing the
asset, `ref:preview()`), when previews are generated on request
(`assets.generatePreviews`), and when the asset is staged. An edit alone
renders nothing. The picture records what it was made
from, so a stale one is told from a current one.
## The names your type's assets hold (`references:`)
Where your type's own format or API carries the name of another asset, declare
it here:
```yaml
references:
keys: # a top-level key of the type's files that holds a name, or a table of them
- { key: "shader", type: "shader" }
args: # an argument of a call, counting from 1
- { call: "g:node", position: 2, type: "procNode" }
fields: # a field of the options table a call takes
- { call: "fx.beam", field: "material", type: "material" }
```
`type` is the category the name resolves in. `asset.declareReferenceKey`,
`asset.declareReferenceArg` and `asset.declareReferenceField` declare the same
from a script.
## What search reads (`indexing:`)
`indexing:` is how assets are found. Search reaches an asset only through what
its type's `indexing:` names, and adds nothing of its own. That includes you: an
asset indexed badly is one you cannot find later, when you are making something
else and need it. A type whose assets are never looked for says `indexing: none`.
```yaml
indexing:
data: # text, read from the asset's files
- role: identity
extractor: verbatim
source: { file: "README.md" }
- role: interface
extractor: symbols
source: { files: ["init.luau"] }
- role: implementation
extractor: verbatim
source: { files: ["init.luau"] }
content: # the picture
- role: identity
modality: image
source: { file: "preview.png" }
facets: # what a query filters and re-ranks by
file: ".metadata"
rigged: { type: bool, from: { components: { contains: SkinnedModel } } }
roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } }
```
**Roles.** Each `data` and `content` entry has a `role`, and a query matches in
two slots:
- **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is.
- **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both.
Ask what a person would recognise an asset of the type by, and what it does, and
point each role at the file that says it. For text, that is the asset's
description: the file your template starts with a `[[describe]]` block, which the
author writes or `services describe` fills in (see "Ask your assets to say what
they are").
**`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds
the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`.
Never index a file name (`source: { field: name }`): one
or two words carry nothing to match.
**`content`.** `modality: image` embeds the picture itself, so a text query
matches what the asset looks like. A type whose assets are looked at makes the
picture its identity.
**`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded
whole, so keys nobody declared in it are still found. Each further key is
computed for every asset and states a value for every one, including `false`: a
file can imply "not rigged" but never say it, and a query separates rigged from
unrigged only because the unrigged ones say `rigged: false`. A key is
`{ type: bool | number | enum, filter?: range, from }`, where `from` is
`{ file, field }` (a field of one of the asset's files, dotted for nesting),
`{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the
asset's entities carry).
## What blocks a push
A push checks every asset it carries against its type. These refuse it:
- a required file that is missing, a `content` check that fails, a `described` file not written yet;
- an error from the type's `validate`.
Unlisted files and warnings do not. A `described` file blocks only a push that
changes the asset. When a push is refused, the message names each asset and what
it lacks. `world.publishBlockers()` and `zm status` read the same list without
pushing.
## Typing a result from the asset (`refShapes`)
A `ref` method whose result depends on the asset (one entry per child folder, per
row of a config, per control a map declares) can only be annotated as
`{ [string]: Thing }`, which accepts any key. `refShapes` states the result for
each asset instead: a function returning a Luau type expression for that asset,
and the module whose types the expression uses. Read the asset; never run it.
```lua
-- inputMap.assetType/behavior.luau
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)` is checked and `map.noexisting` is reported.
A per-asset type applies where the call site names which asset: a component
field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`.
`asset.resolve(name, category)` carries only the category, and a name computed
at runtime carries nothing, so calls on those keep the declared return. The type
belongs to the call's result: index it there, or bind what you need from it
there, and the names stay checked.
`require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows
what was published: `categories` (each type's method surface) and `returns`
(per-asset results by identity, then method). An identity missing from
`returns` was never published. Results are recomputed when an asset is written;
at most 64 assets of one type are read.
## Putting an asset in a scene (`ref.instantiate`)
A caller writes one piece of code against every instantiable type, so what
`instantiate` returns is fixed:
```lua
local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")
M.ref = {
instantiate = function(self, target, opts)
local root = Instantiable.root(self, target, opts)
-- compose the asset under `root`, now
return Instantiable.result(self, root, idMap)
end,
}
```
**In.** `target` is the entity the asset lands under, or onto for a hierarchy
type; with none, the type spawns a new root. `position`, `rotation` (three
numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the
same for every type: `Instantiable.root` applies them to a root you make,
`Instantiable.place` to one you adopt. Document any options of your own in the
hook's `--!arg` docs.
**Out.** `(root, idMap)`. `root` is the composed root, live when you return:
build synchronously, never defer to a component's `awake`. `idMap` maps each
original id to the spawned one, so a component that composes the asset again on
every load can pass it back in and keep references into the composition. Every
call goes through `Instantiable.result`, which refuses a root that is not live
and turns a missing `idMap` into `{}`.
## Inspecting an asset (`ref.inspect`)
`asset.inspect(ref)` builds a record every asset shares (`identity`, `name`,
`guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`,
`description`, `tags`), then calls `ref.inspect(self)` for the type's own
`detail`:
```lua
M.ref = {
inspect = function(self)
-- read the asset's own files, never runtime state: inspect works on an asset
-- that has not been compiled, uploaded or registered
return { --[[ what makes this asset what it is ]] }
end,
}
```
Without it, the record is the shared part alone, and an agent inspecting the
asset learns nothing of what it is. A hook that raises leaves `detail` empty and
sets `record.warning`.
## Discovery
- `asset.list("assetType")`: every registered type.
- `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts.
- `asset.resolve("<typename>", "assetType")`: the type itself.
# procgen.bake
Evaluate a `.procGraph` graph target and realize it into the live scene as durable
content, then verify it landed — the bake-to-scene step that turns a graph into
real, persistent world content.
By output type:
- **Geometry** → a `.mesh` asset + a Model entity referencing it (`material`
optionally tints it).
- **A population** (points carrying a `Mesh`) → live per-instance entities (via `instances.spawn`).
- **Bundle / Template** → a persisted `.bundle` instantiated at its root.
Pass `replace` to swap an existing entity in place (the bake inherits its
position); `position` to place the baked root; `name` to name the asset/entity
(defaults to the graph id). Verification reads the spawned entities' world bounds
back, so a bake that produced nothing renderable reports `verified = false`.
Part of `procgen.toolbox`; surfaces as `procgen.bake`. Returns `{ output_type,
entities, verified, bounds }`.
# procgen.collapse
Lifts a section of a graph's code into a node of its own. A graph is the code
of its `init.luau` (`proc.define(function(g) ... end)`), and a section is a run
of whole statements of that function, named by the lines it spans
(`from`, `to`): typically a few node calls that together do one job.
- The node's **inputs** are the locals declared before the section that its
code reads. A graph input it reads is declared in the node as the graph
declares it (`local seed = g:input("seed", 3)`); any other value becomes an
input typed by what the section wires it into.
- The node's **outputs** are the locals the section declares that the code
after it reads.
- A local function the section calls goes into the node with it: moved when
nothing left in the graph calls it, copied when something does.
- The section is replaced by **one call** of the node, so the graph builds from
the same calls and cooks to the same result:
`local strewn = g["~/fx/Strew"]({ seed = seed, spots = spots })`. A node
giving several outputs is called once and its outputs are read off the table
it returns.
The node lands beside the graph (or in `into`) as `<name>.procNode`, with the
`description` as its `g:describe`, `.metadata` and README, and is found by
`procgen.searchNodes` and described by `procgen.describeNode` at once. The
graph's new source is written only once it builds.
A section holding the graph's own inputs or outputs (`g:input`, `g:output`,
`return { ... }`), or assigning to a value declared before it, is refused with
the line and why.
`procgen.flatten` is the inverse: it writes a node's body back out in place of
its call. `procgen.reference` puts an existing node in place of a section.
Part of `procgen.toolbox`; surfaces as `procgen.collapse`. Returns
`{ node, call, inputs, outputs, copied, moved, lines, source, nodeSource,
diagnostics }`.
# procgen.coverage
The procgen rewrite's coverage report. Every current op is listed with the new node that replaces it (from the composition sheet), its class (`exact`, `meaning`, `gone`), its parity state and the existing suites that call it. Every library graph (`.procGraph` and `.procNode`) is listed with the current ops it names and the worst parity state among them.
States: `pending` (the new node does not exist yet), `ready` (it exists), `pass` / `fail` (a run compared it), `no-case` (the case `parity.json` states for the op is refused by the current op, or no fixture builds its inputs), `unmapped` (the sheet names no counterpart), `decided` (a named decision: the op is gone, for example `bundle.join` because the bundle surface is gone).
`{ run = true }` runs every ready entry's comparison and gives each compared op its cost as separate numbers: the current op's CPU time, the plan time, the cook's CPU time (read-back waits left out: the number the cost ratio judges), the read-back waits, the device dispatches, the read-backs and the frames waited. `state`, `kind` and `query` filter the lines. The answer carries the states tallied (`summary`), the same tally by class (`byClass`), and how many new nodes and kernel families the package holds. The data is `proc.module/parity.module/parity.json`; the comparison is `proc.parity`.
# procgen.describeNode
Describes one node in full, the way a graph calls it: the procgen counterpart
of `describe_tool`. It reads what the node's source declares, without running
it.
- `description`: what it does.
- `inputs`: in the order a call takes them, each with its type, default, range
(`min`, `max`), `options` and description, and whether it is optional.
- `outputs`: each with its type, where the op or node that makes it states one. A node with one output gives that value; a
node with several gives a table of them by name.
- `name`, `call` and `family`: what a graph writes (`g.points.disc`), and the
family it is listed under.
- `context`, `pulls`, `pushes`: the space its work happens in, and what it reads
from or writes to beside its sockets.
- `cost` and `cache`: what its declaration states about what it costs and how
its result is kept.
- `readme`: the node's README.
It takes the name a graph calls (`points.disc`), a node's `~` path, path or
guid, or an op of a pack (`op.geometry.bevel`). `procgen.searchNodes` finds the
names.
Part of `procgen.toolbox`; surfaces as `procgen.describeNode`.
# procgen.diff
A readable diff between two `.procGraph` graphs: which nodes were added, removed, or
changed, plus per-node op/param/input deltas (compared structurally via the
`proc.hash.valueHash` content digest). Part of `procgen.toolbox`; surfaces as
`procgen.diff`. Each argument is a VFS `.procGraph` path or an in-memory graph object.
# procgen.explain
An agent-oriented natural-language summary of a `.procGraph` graph — its size, the
declared inputs (with types), each node's op, the composite references, and
where each output comes from. Part of `procgen.toolbox`; surfaces as `procgen.explain`.
The summary text is in `value.text`. Accepts a VFS `.procGraph`
path or an in-memory graph object.
# procgen.find
Discover procedural graphs by **what they produce**. Where `procgen.search`
matches free-text over a graph's metadata, `find` opens each `.procGraph`, reads the
type of every declared output, and filters on that — so you can ask "which graphs
output a `Bundle`?" or "a `Geometry` named tower?".
Filters: `name` (substring over name/id), `tag`, `category`, and `produces` (an
output-type or producing-op substring, e.g. `"Bundle"`, `"Geometry"`, `"mesh."`).
Each result carries its resolved `outputs` (`{ name, type, op }`), so you find a
graph by its result, not just its description.
Part of `procgen.toolbox`; surfaces as `procgen.find`. Returns `{ results, count }`.
# procgen.flatten
Writes a node's code out in place of a call to it: the inverse of
`procgen.collapse`. The call becomes a `do ... end` block holding the node's
body, so the node's own locals stay its own:
- each input the node declares becomes a local holding what the call passed,
or the default the node declares when the call passed nothing;
- the local the call declared is declared just above the block and given what
the node gives: the node's value itself for a node with one output, a table
of its outputs by name for a node with several;
- `g:describe` and the node's other declarations about itself drop out, and a
file-level local the node's body uses (`local rand = ...`) is carried into the
graph's file.
The graph builds from the same calls and cooks to the same result.
- `call` writes out one call, named by the local it declares or a line it
spans.
- `node` writes out every call of that node.
- With neither, every call of a node or part outside the built-in library is
written out, and again for the calls those bring in, until none is left.
The graph's new source is written only once it builds. Part of
`procgen.toolbox`; surfaces as `procgen.flatten`. Returns `{ inlined, source,
diagnostics }`.
# procgen.inspect
Node-level introspection for a `.procGraph` graph: the node's op, its declared
input/param/output sockets (from the op registry), the `InputRef`s feeding it
(producers), and the nodes that consume its outputs (consumers). Part of
`procgen.toolbox`; surfaces as the global `procgen.inspect`. Accepts a VFS path or an
in-memory graph object.
# procgen.modify
Edits the structure of a graph's code without hand-editing its text. `edits`
is a list of tables, each with an `op`; they apply in order and are written
together, once the graph builds with all of them.
- `{ op = "set", call, input, value }` / `{ op = "unset", call, input }`: what a
call is given (as `procgen.patch`).
- `{ op = "add", node, as?, args?, after? }`: a new call
`local <as> = g.<node>(<args>)`, after the call or line `after`, else after
the last statement before the outputs.
- `{ op = "remove", call }`: a call nothing after it reads.
- `{ op = "repoint", call, node }`: the call names another node and keeps what
it is given.
- `{ op = "rename", from, to }`: a local, everywhere the body names it.
- `{ op = "output", name, value }`: declares or rewires an output
(`remove = true` drops it).
- `{ op = "input", name, default?, type?, min?, max?, options?, description?,
optional? }`: declares an input, or changes the declaration of one.
A call is named by the local it declares or by a line it spans. A value is a
plain value, or `{ expr = "<code>" }` for code; an output's `value` may also be
a local's name as a string. An edit that cannot apply is refused naming which
edit and why, and nothing is written.
Part of `procgen.toolbox`; surfaces as `procgen.modify`. Returns `{ applied,
source, diagnostics }`.
# procgen.patch
Changes what one node call in a graph's code is given, without editing its
text by hand. The call is named by the local it declares (`call = "spots"` for
`local spots = g.points.disc({ ... })`) or by a line it spans.
- `set = { count = 80 }` gives an input a plain value: a number, string,
boolean, vector or a table of them.
- `set = { radius = { expr = "radius * 2" } }` gives it code: a local's name, a
formula, another call.
- `unset = { "seed" }` leaves inputs to the node's defaults.
A call written with values by position is rewritten naming its inputs. The
edit is written only once the graph builds with it, so a value the node does
not take is refused naming the inputs it does.
`procgen.modify` makes the structural edits around a call (adding, removing,
repointing, renaming, inputs and outputs). Part of `procgen.toolbox`; surfaces
as `procgen.patch`. Returns `{ line, callee, source, diagnostics }`.
# procgen.preview
Evaluate a `.procGraph` graph target and render it for any output type (Geometry,
points, Field heatmap, Material ball, Texture, Bundle). The picture comes back in
the result as `image`, beside the visualizer's stats as `metrics`; `path` also writes
the PNG there.
An output made of drawn points (a particle effect, a part of one) is drawn by the
generator that presents it, so its preview is the picture of that generator at the
moment the graph names for a still (`g:still`; else half way through its timeline),
framed to fit over the studio backdrop. `metrics` then states `output_type =
"Generator"` with the graph time and frame pictured and the elements each output
drew. Such an output is previewed as `target = "output:<name>"`; a node of the graph
is not an output a generator presents.
# procgen.promote
Makes a part a node. A part is a `.procGraph` that graphs call by its path
(`g["~/fx/Rim"]({ ... })`); a node is a `.procNode`, found by
`procgen.searchNodes` and called by name.
The node promote writes declares the part's inputs as its own (with their
types, defaults, ranges, options and descriptions), calls the part with them
and gives the part's outputs. The part keeps its code and every graph calling
it, and the node stays the part's: editing the part changes what the node
makes.
- `name` is the node's name (default: the part's), `into` the folder it goes in
(default: the one holding the part).
- `description` and `tags` are what search finds it by (default: the part's own
`.metadata`; a part whose `.metadata` carries no description needs one
given).
`procRef:promote(opts)` on a `.procGraph` ref is the same call. Part of
`procgen.toolbox`; surfaces as `procgen.promote`. Returns `{ node, call, part,
inputs, outputs }`.
# procgen.reference
Replaces a section of a graph's code (a run of its statements, named by the
lines it spans) with a call of a node that already exists: how a graph adopts a
node lifted out of another graph by `procgen.collapse`, or swaps hand-written
calls for a library node doing the same work.
- Each input of the node takes the value the section reads under the same name
(a graph input under the name it is declared with). `inputs =
{ <node input> = "<code>" }` wires one from any local or expression.
- Each value the section makes that the code after it reads is taken from the
node's output of the same name. `outputs = { <local> = "<node output>" }`
says which output when the names differ.
- An input the node needs that nothing wires, an input it does not take, and a
value the code after the section reads that the node does not give are
refused, naming what the node takes and gives.
The node is named the way a graph calls it: `points.disc`, its `~` path
(`~/fx/Strew`), its path or its guid. The graph's new source is written only
once it builds.
Part of `procgen.toolbox`; surfaces as `procgen.reference`. Returns
`{ node, call, inputs, outputs, source, diagnostics }`.
# procgen.search
Searches the procedural-graph library — every `.procGraph` asset surfaced by
`asset.list("procGraph")` — by a free-text `query` (matched case-insensitively
against name/id/description), a required `tag`, and/or a required `category`.
Returns the matching asset summaries `{ results, count }`.
Part of `procgen.toolbox`; surfaces as `procgen.search`. All filters are optional —
calling `procgen.search()` with no args returns every `.procGraph` asset. If no `.procGraph`
assets are discoverable it returns an empty result set rather than failing.
# procgen.searchNodes
Finds the nodes a graph is built from: the procgen counterpart of
`search_tools`. It covers every `.procNode` a graph calls as
`g.<family>.<name>(...)` (or by its `~` path), and every op, called
`g.op.<id>(...)` and listed in the family `op.<head>`: the ops of a pack, and a
`.procNode` not written in the call form, listed as the op it registers, which
only a `proc.graph` graph calls (through `g:node`). A node is read from what its source declares (its `g:describe`
text, its `g:input` declarations and the outputs it returns), without running
it, so a search loads no node.
Each row's `name` is the name a graph calls it by: `points.grid` is
`g.points.grid(...)`, `op.transfer.upload` is `g.op.transfer.upload(...)`. No two rows
carry one name. A row of a node built with `g:node` carries `callable = false`:
a graph built with `proc.define` does not call it, and `g.op.<name>` refuses it
at the call. When a node called by name answers to the same name, the row's
`supersededBy` names it: the call to write.
A query is answered two ways at once:
- **keyword**, over each node's name, family, declared inputs and outputs and
description; a word found in the name counts most, then the family, the
inputs and outputs, and the description;
- **semantic**, where the engine is bound to a world: ZeroMind's search by
meaning over indexed nodes, the same search `search_tools` answers by meaning
with. It also finds nodes other worlds publish that this one has not
installed (`installed = false`, with `use`, the `zm ls-tree @<guid>` read of
its files in the shell, and `install`, the `zeromind_install` call for the
one reading showed fits).
Each row says how it matched (`matched.words`, `matched.keyword`,
`matched.semantic`), and rows both ways found come first. A row carries the
first sentence of the node's description as `summary`; `detail = true` adds
the whole `description`.
A row of a `.procNode` built with `g:node` (the `procNodes/` of the procgen
package) carries `callable = false`: a graph built with `proc.define` does not
call it, and `g.op.<name>` of it is refused where it is written. Only a
`proc.graph` graph calls it, through `g:node`. `supersededBy` names the node
written in the call form that a graph calls in its place (`points.grid` for
the `procNodes/pointsGrid.procNode` row).
Each row's `context`, `pulls` and `pushes` are what the node states: a node
written as a table states them in its source, and a node written with
`proc.define` in its `.metadata` or, for every node it holds, its nodebox's
`.metadata`.
With no query it lists the nodes and `catalogue` names every family with the
nodes in it, small enough to read whole.
Options: `family`, `limit` (default 10), `mode` (`"keyword"` or `"semantic"` to
ask one way only), `library = true` (search the whole published library by
meaning), `context` (`sim`, `field`, ...), `pulls` / `pushes` (`"scene"` or
`"asset"`), `pack` and `detail`.
`procgen.describeNode` describes one node in full, by the name a row carries.
Part of `procgen.toolbox`; surfaces as `procgen.searchNodes`. Returns
`{ results, count, catalogue?, keyword, semantic? }`.
# procgen.toolbox shared
Cross-tool helpers for the procgen toolbox. Tools return their raw values
(the `tool.assetType` wrapper packages them into the system
`ZmToolResult` envelope) and raise via `error(message)` on failure.
# procgen.stats
Structural counts of a `.procGraph` graph — nodes, composites, inputs, outputs.
When a `target` is supplied it also evaluates it and reports the output type,
the wall-clock evaluation duration, and (for geometry outputs) vertex and
triangle counts. Timing is wall-clock only and never feeds graph evaluation, so
determinism is unaffected. Part of `procgen.toolbox`; surfaces as `procgen.stats`.
# procgen.trace
The dependency chain for a target — the node ids reachable from it in
evaluation order (each node's producers appear before it, a post-order walk of
the `node_output` edges). `const` / `graph_input` / composite leaves stop the
walk. Part of `procgen.toolbox`; surfaces as `procgen.trace`. Accepts a VFS `.procGraph`
path or an in-memory graph object.
# procgen.tweak
Preview a graph with other values for its declared inputs, in one step: try a
knob, see what it makes, repeat. The graph's source is left as it is.
`inputs` is `{ <input name> = <value> }`, each name an input the graph declares
with `g:input`; a name it does not declare is refused with the names it does.
`target`, `size` and `path` behave as in `procgen.preview`, and an output drawn by
a generator (a particle effect) is pictured by a generator given those inputs.
Part of `procgen.toolbox`; surfaces as `procgen.tweak`. Returns `{ inputs, image,
metrics }`: the values tried, the rendered picture and the visualizer's stats.
# procgen.validate
Type-checks a `.procGraph` graph and returns diagnostics —
`{ level, code, node, input, expected, actual, suggestions }` — empty when the
graph is valid. Part of `procgen.toolbox`; surfaces as the global `procgen.validate`.
Wraps the `modules.proc` `validate(graph)` model function. Accepts a VFS path
to a `.procGraph` folder or an in-memory graph object.
# toolbox (asset type)
A toolbox is an asset grouping set of `<tool>.tool` assets around one area of work. Each tool in
it becomes callable under the toolbox `<toolbox>.<tool>`, and is a shell command,
`zero <toolbox> <tool> <args>`; `guides core/tools`, "A tool's command line", covers its
flags, paths, `render.luau` and exit status.
## When to group tools together
When creating tools for the engine, a common theme should be identified and
and a toolbox created to house them, this allows each individual operation
to be easily accessible instead of being scattered in a million places.
## Sharing functionality between tools
A toolbox features ability to define functions that code inside each individual tool
inside it can access, this is usefull when establishing helpers etc so you dont have
to duplicate code for each new tool you write.
A `shared.module` inside the toolbox root is where this code lives,
each tool inside a toolbox can access its functions by requiring it with `require(".shared")`.
```luau
-- <toolbox>.toolbox/shared.module/init.luau
local M = {}
-- One step a tool reports in its result.
export type Step = { name: string, ok: boolean, detail: string? }
--!desc Check that a numeric argument lies within bounds.
--!arg arg The argument's name, for the error.
--!arg value The value passed.
--!arg min The lowest value allowed.
--!arg max The highest value allowed.
--!return The value; raises naming the argument and the range when it lies outside.
--!example "count", 12, 1, 100
typed function M.range(arg: string, value: number, min: number, max: number): number
if value < min or value > max then
error(string.format("%s must be between %s and %s, got %s", arg, tostring(min), tostring(max), tostring(value)))
end
return value
end
--!desc A byte count as a readable size.
--!arg bytes The count.
--!return The size in B, KB, MB or GB.
--!example 2681107
typed function M.size(bytes: number): string
local units = { "B", "KB", "MB", "GB" }
local i = 1
while bytes >= 1024 and i < #units do
bytes /= 1024
i += 1
end
return if i == 1 then string.format("%d %s", bytes, units[i]) else string.format("%.1f %s", bytes, units[i])
end
--!desc A step for a tool's result.
--!arg name What the step did.
--!arg ok Whether it succeeded.
--!arg detail What happened, when there is more to say.
--!return The step.
--!example "copy textures", true
typed function M.step(name: string, ok: boolean, detail: string?): Step
return { name = name, ok = ok, detail = detail }
end
--!desc The asset of `kind` that a caller named, looked up by that name.
--!arg kind The asset type to look in.
--!arg name The name the caller passed.
--!return The asset; raises naming the kind and name when nothing matches.
--!example "material", "gold"
typed function M.named(kind: string, name: string): AssetRef
local ref = asset.tryResolve(name, kind)
if ref == nil then
error(string.format("no %s named '%s'", kind, name))
end
return ref
end
return M
```
This shared code should be limited to being used by the tools inside it,
dont put general functionality you might need otuside the toolbox into its shared module.
Instead reference those in the shared module should you wish to utilize something.
A benefit the shared module shares with the tools own code is that it allows
the author to write code that has computed references, meaning eg resolving
an asset from a computed variable `asset.resolve(<type>,name)` that is normally
not allowed in other parts of the engine surface, due to said references not being
recorded.
This is usefully for utilizing tools to handle operations that are called by users/agents
and are dynamic in nature, such as finding/inspecting/testing assets, or content based on an input etc.
When writing a shared module, you should follow standard module practices, please see `module.assetType` readme.md
for more indepth information.
## Describing the toolbox
When writing the readme for your toolbox, ensure you focus on the overall
area what the toolbox focuses on, rather than listing individual tools inside it,
discovery surfcaces already group tools and toolboxes together so a toolbox can be discoverd
by the tools it hosts and doesnt require separately maintaining a list of the tools in it's readme.
## What are tools and when do i create ones?
Tools are typed operations with a known serialized output format, made to enable efficient operation in the engine and provide an easily callable interface for operations around a topic.
They are also useful in that they can be called from a multitude of surfaces, from `execute`, to `bash` to `tool_exec`
to users Tools portion in the edui.
A toolbox and its tools differ from eg a module that has multiple callable functionm in that they might return anything,
and come with no guarantee that the output is meant to be inspected directly, where as tools are meant to perform an self contained operation and return results/info/relevant data in a readable format, including eg. image data.
Whenever you identify an operation that requires specific orchestration and or is something that could be a repeated operation and makes it easier to manage the project/task or when you want to expose logic written in `luau`
to the other call surfaces like `bash` under its `zero` command,you should reach for creating a toolbox.
Toolboxes also come in handy for limiting control/inputs, exposing a limited set of functionality in a clean manner or ensuring the caller doesnt have to understand the entire code.
For comparison, you could think of toolboxes as programs, and the tools inside them as commands.
A package might contain tens of tousands of lines of code, dozens of modules, components etc,
a toolbox turns that surface into a system that any agent/user can then utilize without having
to read or understand the code inside.
Its the UI equilevant to operations.
For the indepth explanation of what `tool.assetType` is and does, read it's readme.
# luau_introspect (module)
Parses Luau/asset source TEXT into structured declarations. Pure
string/pattern parsing (`string.find` / `string.match` / `string.gmatch`)
over the literal source — no compiling, no VM spawn, no runtime state, no
engine FFI. Given source text, it extracts what is written in the file.
This is the shared parser every per-assetType `M.ref.inspect` hook uses to
turn a component/module/tool's own source into the structured record
`asset.inspect` returns.
## Exports
- `docstrings(src) -> DocMap` — scans `--!desc` / `--!arg` / `--!return` /
`--!example` comment runs and binds each run to the name of the
declaration on the next non-comment line (the identifier after
`function`, `public:`, `local function`, or `M.`).
- `publicFields(src) -> { FieldEntry }` — parses `public = { name =
Field.<kind>(default, mode), ... }` table-literal entries and
module-scope `public.<name> = Field.<kind>(...)` assignments.
- `events(src) -> { EventEntry }` — parses `events = { name =
Event(payloadSchema?, syncMode?), ... }` table-literal entries.
- `methods(src) -> { MethodEntry }` — parses `function public:<name>(...)`
/ `typed function public:<name>(...)` declarations (params + optional
return type).
- `lifecycleHooks(src, catalog) -> { string }` — top-level `function
<name>(` declarations whose name is in the caller-supplied `catalog`
array.
- `moduleExports(src) -> { ExportEntry }` — parses `M.<name> = function`
assignments, `function M.<name>(...)` declarations, and a trailing
`return { foo = foo, ... }` export table.
- `forSource(src, computeFn) -> any` — memoises `computeFn(src)` keyed on
`src` itself in a module-local table. The same source text returns the
cached result without re-running `computeFn`; different source text
recomputes. Keyed on the source content (not an asset checksum) because
composite/container assets (component folders, scene folders, etc.) all
hash to the empty-string checksum — a checksum-keyed cache collapses
every such asset's detail onto whichever one was inspected first.
## Usage
```luau
local Introspect = require("modules.luau_introspect")
local src = self:getInitScript()
local detail = Introspect.forSource(src, function(s)
return {
fields = Introspect.publicFields(s),
events = Introspect.events(s),
methods = Introspect.methods(s),
hooks = Introspect.lifecycleHooks(s, lifecycleCatalog),
}
end)
```
## Notes
- Every function is a pure string→table transform: same source text in,
same structured table out, every time. No reads of live component
state, no entity/world queries.
- Structural scanning runs over a comment- and string-aware **mask** of
the source (a same-length copy where comment bodies and string-literal
contents are blanked to spaces, delimiters and newlines preserved).
Pattern matches and brace/paren balancing run on the mask — so a brace,
`Field.`, `Event(`, or a whole entry that appears inside a comment or a
string literal never registers — while captured values are sliced from
the original source at the same (aligned) indices. This makes the
parser robust to apostrophes in comments, commented-out entries, string
defaults containing `}` / `Field.` / `Event(`, and `[[ ]]` long
strings.
- `default` / `category` are raw trimmed source-text slices (with any
trailing comment stripped), not evaluated values.
- For a **ref constructor** (`Field.assetRef` / `dataRef` / `resource`
/ `componentRef`) the FIRST positional argument is the category /
contract / componentType selector, captured under `category`; the
SECOND argument is captured under `default`.
- For every **other** Field kind the first argument is the `default`
and `category` is nil.
- A trailing `Sync` / `NoSync` identifier is captured as `sync`,
tolerating a trailing comma or a trailing comment after the last
argument.
- Field/Event entries whose key is a computed or indirect expression
(`[expr] = Field.number(...)`) are omitted — only literal identifier
keys are recognised.
- `methods` captures a `...` variadic parameter (as `name = "..."`), with
its `: <type>` annotation when present.
- A `--!desc` written above a field/event declaration (`<name> =
Field.<kind>(...)` / `<name> = Event(...)`) or a `public.<name> =`
assignment binds to that name in `docstrings`, and is folded into the
corresponding `publicFields` / `events` entry's `desc`.
- `lifecycleHooks` never hardcodes the lifecycle-callback catalog; the
caller passes the list of names to match against.
- `forSource`'s memo is bounded (oldest-key eviction) so a long session
touching many distinct asset versions can't grow it without limit.
# assetType.shared.ref
Builds the metatable every `AssetRef` (the `{ __ref, type, name, guid, identity, path }` envelope returned by `asset.resolve` / `asset.ref`) carries. Replaces the per-call C metatable that used to live in `crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable`.
## Public API
```lua
local assetRef = require("@builtin::assetTypes.assetType.shared.ref")
-- Called by the Rust factory (push_asset_ref_handle) via
-- _G.__build_asset_ref_proxy after the envelope fields are set.
assetRef.build(envelope) -- attaches AssetRefMT, returns the table
```
The module exposes no invalidation surface — discovery is registry-driven and hot-reload is transparent (see "Discovery and hot-reload" below).
## Method dispatch order
`ref.foo` is resolved in this order:
1. **Default methods** — `getSource` / `getBytes` / `getText` / `exists` / `inspect` and the lazy `meta` property. Per-type definitions cannot override these (engine contract from gh#1889).
2. **Per-type `ref` table** — discovered through `asset.resolve(<typename>, "assetType")` + `require(<identity>.behavior)`. The lookup resolves to whichever `<typename>.assetType/` folder is currently registered for the typename (built-in, user, or library-vendored) and pulls its `ref` methods.
3. **Nil** — unknown key, same shape as the original C metatable.
## Adding methods for a new asset type
Drop a `behavior.luau` into the type folder:
```
src/lua/lib/assetTypes/<typename>.assetType/
├── type.yaml -- structural spec (existing)
├── behavior.luau -- ref + global behaviour (new)
└── template/ -- placeholder body (existing)
```
`behavior.luau` returns:
```lua
return {
ref = {
--!desc Per-instance method on every `<typename>` AssetRef.
myMethod = function(self, ...) ... end,
},
global = {
-- Reserved for the asset.<typename>.* surface — not consumed
-- yet; included so the contract from gh#1889 lands cleanly.
},
}
```
Within `ref` methods, `self` is the AssetRef envelope: `self.path`, `self.identity`, `self.guid`, `self.type`, `self.name` are available. Engine-required fields (`guid`, `path`, `identity`, `type`) must not be redefined.
## Discovery and hot-reload
Type discovery uses the asset registry, so it doesn't matter where a `<typename>.assetType/` folder lives — engine built-ins under `@builtin::assetTypes.<X>`, user types dropped under `/zero/source/<X>.assetType/`, or types vendored inside an imported library all resolve identically. The dispatcher walks `asset.resolve(<typename>, "assetType")` → canonical identity → `require <identity>.behavior` on every miss; the asset registry is the cache for the resolve half and Luau's `_LOADED` is the cache for the require half.
Hot-reload is transparent. When a `<typename>.assetType/behavior.luau` is rewritten through VFS, the engine's `invalidate_require_cache_for_state` re-runs the module and mutates the cached `_LOADED[<identity>.behavior]` table in place. Dispatch reads `mod.ref` on every access (rather than caching a sub-pointer), so existing AssetRefs see the new methods on the very next access — no Rust-side hook into this module is needed, and no invalidation API is exposed for callers to call.
The same registry-driven path also handles type **registration** correctness: dropping a new `<typename>.assetType/behavior.luau` updates the asset registry, the next `asset.resolve(<typename>, "assetType")` returns the new ref, and AssetRefs of that type immediately gain the new methods.
## Why Luau, not Rust
The asset surface needed per-category behaviour (`material:getProperties()`, `bundle:instantiate(entityId)`, `tool:run(args)`) and the old shape couldn't grow without an FFI change per method. Owning the proxy in Luau means:
- Type-specific methods ship as content (the `<typename>.assetType/` folder), not engine code.
- Adding a new asset type drops in a new folder; no Rust rebuild.
- Methods can call any other Luau global (`vfs`, `asset`, `Material`, `entity`, etc.) directly, instead of going through narrow FFI primitives.
## Consumers
- `crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable` — the Rust factory that pushes envelopes invokes `_G.__build_asset_ref_proxy` once per push.
- Every Luau call site of `asset.resolve` / `asset.ref` / `entity(id).component.X.material` / any binding declared `AssetRef<...>` — they all receive envelopes built through this module.
# ik.reach
How far a limb can extend, and how far away a point is, in the same units —
plus whether that point falls inside the reachable band. Part of `ik.toolbox`;
surfaces as `ik.reach`.
This is the first thing to check when an IK solve looks inert. A limb reaches as
far as its bones are long and no further: a target beyond that has no solution,
so the chain extends straight toward it and stops. That is correct behaviour and
it is indistinguishable from a broken solver by eye. There is a lower bound too
— a two-bone chain cannot fold its tip closer to the root than the difference
between its segment lengths.
Everything is reported in world units, so the rig's model space never has to be
reasoned about. That matters because a limb's `reach` as the component reports it
is in model space, where a 0.61 m arm reads as about 61, and the scale factor
usually lives on a node inside the imported body rather than on the entity you
set `localScale` on.
Takes either a canonical limb role (`leftarm`, `rightarm`, `leftleg`,
`rightleg`) or a `"rootBone..tipBone"` pair, so it works on rigs that carry no
retarget profile at all.
# assetType runtime
The machinery that reads a `<typename>.assetType/behavior.luau` and turns its
declarations into live behaviour.
A `behavior.luau` is a table of declarations — `ref` methods, `modules` shared
code, an `events` schema, a `namePattern`, `refShapes`, and the `onCreate` /
`onRegister` / `onChange` / `onDelete` / `validate` / `settingsDefaults` /
`settingsCorrections` hooks. None of it runs itself. This is the code that
runs it, and it lives here because reading a type's declarations is the
**assetType type's own behaviour** — the same way `material.assetType` owns
what a `.material` does.
## Sub-modules
| Module | Reads | Responsibility |
|---|---|---|
| `ref.module` | `ref`, `modules` | Builds every `AssetRef` envelope, attaches the shared metatable, and dispatches per-type methods and shared modules onto it. Also owns edit-mode write-behind persistence. |
| `create.module` | `onCreate`, `namePattern`, `validate` | Backs `asset.create`: resolves the type, checks the name against its pattern, runs `onCreate` (or clones `template/`), and writes the instance. `validate.module` beside it checks a creation `opts` table against the type's `createSchema`. Before running `onCreate`, resolves the type's settings chain (below) and hands the result to the hook as `opts.settings`. |
| `settings.module` | `settings`, `settingsDefaults`, `settingsCorrections` | A type's declared settings schema and where its values live (`type.yaml`'s `settings:` block). `Settings.resolveForCreate` layers the schema's own defaults, the type's own `settingsDefaults(name, opts, asked)` hook (a DEFAULT the schema can't express — a name-derived convention, a fact read off a payload the caller handed it, a value following a field `asked` shows a higher layer naming; any later layer overrides it), the Preset Manager, an importer's settings, the caller's `settings` table, then the type's own `settingsCorrections(values, name, opts)` hook (a CROSS-FIELD CONSTRAINT the schema can't express, applied last over the fully merged values). A correction over a default/preset/importer value lands silently; a correction that would override what the caller's own `settings` table explicitly asked for refuses the creation instead, naming the field and why. Neither hook is ever handed a table to mutate; each returns what it wants changed (`settingsCorrections` may also return a `{ [field]: reason }` table for that refusal message). `Settings.cloneValues` deep-copies a values table — `create.module` hands `onCreate` a copy, never the table it stamps. `Settings.valuesOf` / `writeValues` read and write an asset's own values file. |
| `typeYaml.module` | `type.yaml` | The folder a type lives in and its parsed `type.yaml`, decoded once per content version of the file. `settings.module` and `inspector.module` read their blocks through it. `about` / `aboutAt` read a type's `about.yaml` (`intent`, `function`, `usage`), leaving out any key that is empty or still starts with `[[describe]]`; `aboutRows` and `aboutMarkdown` lay the texts out for the Inspector and the `types/<name>` guide. |
| `inspector.module` | `type.yaml`'s `inspector:` block | Which inspector a type declares for its assets (`inspector: { type: ... }` or `{ instance: ... }`), and the declaration that file returns — the sections it adds, the generic ones it replaces, and their order — or the problem that kept it from loading. |
| `onRegister.module` | `onRegister` | Fires an instance's once-only registration hook — on arrival, on world load, and for `@builtin` library instances. |
| `changeDispatch.module` | `onChange`, `onDelete`, `refShapes` | Routes a VFS source write or a folder delete to the enclosing typed asset's type hooks. |
| `refShapes.module` | `refShapes`, `ref` | Publishes what every `AssetRef<category>` answers to, so a member read on an asset-typed value is checked against that category's real surface. |
| `events.module` | `events` | The per-asset event runtime behind `ref.events`. |
| `instantiable.module` | `ref.instantiate` | The instantiation contract an asset type opts into: `root` stands the root entity, `place` applies the base placement opts to one the type adopted, and `result` shapes the `(root, idMap)` every `instantiate` returns. Also registers the `sceneInstantiable` field-constraint validator. |
| `declaration.module` | `type.yaml` | The type's decoded declaration, read once and held until a write inside an `.assetType` folder; what every question about a type as a whole reads. |
| `previewRecipe.module` | `type.yaml` `preview:` | How an instance's `preview.png` is produced: which of the four recipes (`instantiate`, `image`, `surface`, `custom`) the type declares, and what that recipe needs. |
## Reaching it
```luau
local rt = require("@builtin::assetTypes.assetType.shared")
rt.ref.loadTypeBehavior("material")
```
Fields resolve lazily. The prelude requires the sub-modules directly, in
dependency order (`ref`, then `changeDispatch`, `onRegister.install()`,
`refShapes.install()`) — a table that required all six on load would take that
ordering away from it.