Log inGet started

tools

Updated October 20, 2026

The tools namespace: 462 functions.

globals/tools/__useVia

tools.__useVia(via: string, toolbox: string, tool: string, ...: any?) -> ...any

tools.use for a call site written as editorTools("<toolbox>").<tool>(...): the toolbox the site recorded under via is the one reached.

Parameters

  • via string — The reference kind the call site recorded ("editorTools" or "tools.use").
  • toolbox string — The toolbox name the site wrote.
  • tool string — The tool name.
  • ... any (optional)

Returns ...any — Everything the tool returned. Raises on an unknown toolbox / tool or a tool-reported failure.

__useVia("editorTools", "entityOps", "spawn", { Model = { model = "cube" } })

globals/tools/bind

tools.bind(identity: string, positional: { any }?, named: { [string]: any }?, opts: BindOpts?) -> BindResult

Resolve a call's arguments against a tool's declared parameters, turning named arguments into the positional call the tool actually takes. This is the binder behind zero <toolbox> <tool> --name value and the named args an MCP caller passes, so a name resolves the same way whichever surface the caller reached for. Reads the schema from tools.get, matches each name to a parameter (exactly first, then case-insensitively), and reports the first unresolvable name, a name that a positional argument already filled, a call longer than the signature, and a required parameter skipped over while a later one is filled. Resolves the call only — running it is tools.use.

Parameters

  • identity string — Tool identity ("<toolbox>.<name>", with or without the leading tools.).
  • positional { any } (optional) — Arguments already given by position, filling slots from 1.
  • named { [string]: any } (optional) — Arguments given by name, { [parameterName]: value }.
  • opts BindOpts (optional) — order — the order to visit named in, so the first problem reported is the caller's first (defaults to sorted, for a stable answer). prefix — written in front of every argument name in the failure message, "--" for a shell flag. whole — read named as ONE table argument when not one of its keys names a parameter and the first parameter can hold a table, as the first argument written inline when only some of them do, and as arguments named against the parameter list when no parameter could hold the table — where an empty named then resolves to a call with no arguments at all. positionalCount — how many slots positional fills, for a caller that passed an explicit nil and so cannot be measured by length.

Returns BindResult — { ok, call, count, failure? }. Call the tool with table.unpack(call, 1, count); on failure call is empty and failure carries the reason.

tools.bind("camera.lookAt", {}, { target = { 0, 5, 0 } })
tools.bind("MaterialAuthor.fromColor", {}, { color = { 1, 0, 0 } }, { whole = true })

globals/tools/callable

tools.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.

Parameters

  • toolbox string — The toolbox's registered name ("zm", a library toolbox's scoped identity "@lib::ns", or a toolbox's canonical identity "pkg.blender").
  • tool string — The tool's leaf name ("status").

Returns string? — "<toolbox>.<tool>", or nil when the registry carries no such tool.

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

globals/tools/create

tools.create(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, signature: string?, error: string? }

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.

Parameters

  • args { [string]: any } — Structured tool definition. 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).

Returns { ok: boolean, path: string?, identity: string?, signature: string?, error: string? } — { ok, path?, identity?, signature?, error? }. path — the new .tool/ folder's VFS path. identity — the tool's <toolbox>.<name> identity. signature — the derived name(args) -> ret signature.

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

globals/tools/createToolbox

tools.createToolbox(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, error: string? }

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.

Parameters

  • args { [string]: any } — Structured toolbox definition. 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.

Returns { ok: boolean, path: string?, identity: string?, error: string? } — { ok, path?, identity?, error? }. path — the new .toolbox/ folder's VFS path. identity — the toolbox's registered identity (<name>).

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

globals/tools/delete

tools.delete(identity: string) -> { ok: boolean, path: string?, error: 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).

Parameters

  • identity string — Tool identity ("<toolbox>.<name>").

Returns { ok: boolean, path: string?, error: string? } — Table shaped { ok, path?, error? }.

tools.delete("mytools.hello")

globals/tools/get

tools.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.

Parameters

  • identity string — Tool identity ("<toolbox>.<name>", with or without the leading tools.).

Returns ToolMeta? — The assembled metadata { name, signature?, description, args, 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.

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

globals/tools/list

tools.list(tier: (number | string)?, toolbox: string?) -> { stdout: string, value: any }

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.

Parameters

  • tier (number | string) (optional) — Verbosity level: 1 (default) toolbox summary, 2 adds one-line descriptions, 3 adds the full assembled metadata. Pass a string instead to restrict to that toolbox at tier 1.
  • toolbox string (optional) — Optional toolbox name to restrict the output to (e.g. "entityOps"). Passing a string as the first arg also works.

Returns { stdout: string, value: any } — Table shaped { stdout: string, value: <grouped> }. For tier 1, value is { [toolbox] = { toolName, … } }. For tier ≥ 2, value is { [toolbox] = { { name, identity, signature?, description, … }, … } }.

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(2, "entityOps")  -- only `entityOps`, with descriptions

globals/tools/miss

tools.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 describe_tool MCP tool and zero in the engine shell all refuse with this reason, so the same name is refused the same way wherever it is written.

Parameters

  • toolbox string — The toolbox name the caller wrote.
  • tool string (optional) — The tool name the caller wrote, when there is one.

Returns string — The reason, as one sentence the caller can act on.

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

globals/tools/registeredToolbox

tools.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.

Parameters

  • name string — The toolbox's registered name ("zm", a library toolbox's scoped identity "@lib::ns", or a toolbox's canonical identity "pkg.blender").

Returns string? — name, or nil when the registry holds no such toolbox.

local box = tools.registeredToolbox("zm") -- "zm"
tools.search(query: string?, opts: { toolbox: string?, limit: number?, mode: string?, from: string? }?) -> { stdout: string, value: any, total: number?, containers: { any }? }

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 tools.use("<toolbox>", "<tool>", ...), from Luau or inside a tool_exec script, the form each row's examples are rendered in, or in the engine shell as zero <toolbox> <tool> [args].

Parameters

  • query string (optional) — Free-text search string, matched against every file each type's indexing recipe names — for a tool, its README and its source. Empty or nil lists the catalogue, or the toolbox opts.toolbox names.
  • opts { toolbox: string?, limit: number?, mode: string?, from: string? } (optional) — Optional filters table. toolbox — keep only rows whose owning toolbox exactly matches. limit — maximum rows to return (default 10; pass a larger value to return more). 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. from — WHERE results may come from. 'all' (default) is everything published. 'bound' is the current world and its installed libraries, answered by this engine with no published-library leg; mode still picks how the query is matched, and mode='keyword' reads this engine's own files, unpushed ones and a private world's included.

Returns { stdout: string, value: any, total: number?, containers: { any }? } — Table shaped { stdout: string, value: { SearchRow } }. Each row is { identity, name, assetType, description, toolbox?, registered?, signature?, tags?, examples?, matched?, guid?, installed?, use?, install?, p?, container_key?, system_key?, from? }. A query row says whether this world holds the hit (installed) and the one call that acts on it (use: the tool_exec call that runs it through tools.use, a search_tools { toolbox } drill-in, the workflow toolbox's start through tool_exec, or for a hit this world does not hold bash { command: "zm ls-tree @<guid>" }, the shell read of its implementation, with install the zeromind_install call for the candidate the reading shows fits); 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. containers, beside rows the search core ranked, is the tier their container_key and system_key point into: each entry { key, guid, name, assetType, parent_key?, description? } is a container a row ships in, naming the one above it by parent_key, so a tool's container_key is its toolbox and its system_key the package that toolbox ships in.

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" })

globals/tools/toolboxes

tools.toolboxes() -> { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } }

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.

Returns { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } } — Table shaped { stdout: string, value: { { toolbox, purpose, toolCount } } }. value is toolbox-name-sorted; each entry is { toolbox = "<name>", purpose = "<one line>", toolCount = <n> }.

tools.toolboxes()

globals/tools/toolboxesNamed

tools.toolboxesNamed(name: string) -> { 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).

Parameters

  • name string — A toolbox name ("entityOps"), a library toolbox's scoped key ("@lib::ns"), or any other name the toolbox asset answers to — its canonical identity or its path.

Returns { string } — The names a call reaches each toolbox by, sorted: one entry when the name reaches one toolbox, several when it reaches more, none when it reaches no toolbox holding a tool.

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

globals/tools/tryUse

tools.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 returns { ok, value, error, toolbox, tool } for every outcome — an 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 ok / value / error fields are the ones a tool_exec answer 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.

Parameters

  • toolbox string — The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").
  • tool string — The tool's leaf name within that toolbox ("spawn").
  • ... any (optional)

Returns ToolCall — { ok, value, error, toolbox, tool }. value carries the tool's own value when ok is true; error carries the reason when it is false.

tools.tryUse("entityOps", "spawn", { Model = { model = "cube" } })

globals/tools/use

tools.use(toolbox: string, tool: string, ...: any?) -> ...any

Invoke one code-mode tool by naming its toolbox and tool explicitly. The same call runs from engine Luau (editor panels, shipped modules, execute) and inside an MCP tool_exec script (return await tools.use("<toolbox>", "<tool>", ...)), and zero <toolbox> <tool> [args] is its engine-shell spelling. 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 returns the tool's value directly — unwrapping the ZmToolResult 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.

Parameters

  • toolbox string — The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").
  • tool string — The tool's leaf name within that toolbox ("spawn").
  • ... any (optional)

Returns ...any — Everything the tool returned, in the order it returned it — a tool 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 an { ok, value } envelope; reach for tools.tryUse when the call's outcome is what you want.

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

tools/MaterialAuthor/composeFolder

MaterialAuthor.composeFolder(folder: string, opts?: ComposeFolderOpts) -> (AssetRef<material>, shared.Durability)

Scan folder for textures (loose images and .texture assets), infer each one's PBR slot from its name, and compose them into a single new Material. Errors when no slot-mappable texture is found. Returns the new AssetRef<material>.

Parameters

  • folder string
  • opts ComposeFolderOpts (optional)

Returns (AssetRef<material>, shared.Durability)

"/zero/source/textures/brick"
"/zero/source/pbr/metal", { name = "Metal", metallic = 1 }
"/zero/source/pbr/tiles", { name = "Tiles", tiling = 8 }

tools/MaterialAuthor/duplicate

MaterialAuthor.duplicate(source: string | AssetRef<material>, newName: string, overrides?: MaterialOverrides) -> (AssetRef<material>, shared.Durability)

Duplicate a material under newName, applying overrides. The source is resolved by AssetRef / identity / name; its mat.yaml definition is cloned (shader + every property + every texture binding carry over) under the new identity with overrides merged in, then written as the new material. Returns the new AssetRef<material>.

Parameters

  • source string | AssetRef<material>
  • newName string
  • overrides MaterialOverrides (optional)

Returns (AssetRef<material>, shared.Durability)

"Gold", "WetGold", { roughness = 0.05 }
goldRef, "GoldEmissive", { emissive = { 1, 0.8, 0, 1 } }

tools/MaterialAuthor/fromColor

MaterialAuthor.fromColor(color: Color, opts?: FromColorOpts) -> (AssetRef<material>, shared.Durability)

Create a flat-colour material in one call. color is {r,g,b} / {r,g,b,a} (array) or { r=, g=, b=, a= } (keyed), channels in [0,1]. Returns the new AssetRef<material>.

Parameters

  • color Color
  • opts FromColorOpts (optional)

Returns (AssetRef<material>, shared.Durability)

{ 1, 0, 0 }, { name = "Red" }
{ 1, 0.84, 0 }, { name = "Gold", metallic = 1, roughness = 0.2 }

tools/MaterialAuthor/fromTexture

MaterialAuthor.fromTexture(texture: string | table, opts?: FromTextureOpts) -> (AssetRef<material>, shared.Durability)

Create a new material that binds texture to one slot. Accepts a renderer.texture handle, an AssetRef/.texture identity, or a VFS image path. The slot is taken from opts.slot, else inferred from the texture name (*_normal → normal_texture, …), else base_color_texture. Returns the new AssetRef<material>.

Parameters

  • texture string | table
  • opts FromTextureOpts (optional)

Returns (AssetRef<material>, shared.Durability)

"logo.texture"
tex, { name = "Brick", slot = "base_color_texture", roughness = 0.6 }
tex, { name = "Ground", tiling = 300 }

tools/agent/rename

agent.rename(name: string) -> { client: string, name: string }

Rename yourself — the name every person and agent on this world knows you by. Pick one that says what you are working on. A refusal reads rename refused (<code>): <why>: name_taken while another agent on the world holds the name (it names that agent and the person it works for), invalid_name for a name outside the rule, offline while the engine holds no world connection, and timeout when ZeroMind did not answer in time — whoami then tells which name you go by.

Parameters

  • name string

Returns { client: string, name: string }

"terrain"

tools/agent/roster

agent.roster() -> Roster

Everyone on this world: each person, the engines they have open, and the agents working for them with each one's name, harness and last call.

Returns Roster

tools/agent/whoami

agent.whoami() -> { name: string?, caller: string?, person: string? }

Who you are on this world: the name you go by, your caller key, and the person you work for.

Returns { name: string?, caller: string?, person: string? }

tools/animation/observe

animation.observe(target?: (string | EntityRef)) -> AnimObserveResult

What the engine is posing right now, one line per body: whether its pose is actually changing, the clip driving it with its playhead, how many of its bones that clip reaches, and — when it is not moving — the one reason why. Call it with no arguments for every body in the scene, or pass an entity to narrow it to that one (a character root resolves down to the skinned body under it).

Parameters

  • target (string | EntityRef) (optional)

Returns AnimObserveResult

"hero"

tools/animation/play

animation.play(animation: string | AssetRef<animation>, entityArg: string | EntityRef, opts?: AnimPlayOpts) -> AnimPlayResult

Play an animation clip on an entity's skinned body for a test window, then clean up. Resolves animation by identity or name and entity by id or name, finds the SkinnedModel to drive, and plays it through the canonical AnimGraph — the same path ClipPlayer uses. Returns { ok = false, error } when the clip, the entity, or a SkinnedModel under it cannot be found. When the body already runs an animation the clip overrides it for the window and the original resumes untouched on cleanup.

Parameters

  • animation string | AssetRef<animation>
  • entityArg string | EntityRef
  • opts AnimPlayOpts (optional)

Returns AnimPlayResult

"A_Walk_F_Masc", "hero"
"Idle", "hero", { offset = "5s", loop = true, duration = 8 }

tools/api/docs

api.docs(...) -> string

Browse engine VFS documentation under /zero/docs. No args lists top-level categories. Partial paths list a directory's entries (one entry per line, trailing / on directories). Full paths return the document content as a string. Each argument is a single path segment — they are joined with / to form the lookup path.

Returns string

-- list categories
"api"                     -- list namespaces
"api", "physics"          -- list physics methods
"api", "physics", "raycast" -- full raycast docs

tools/api/searchDocs

api.searchDocs(query: string) -> string

Search the engine's documentation: the generated reference pages under /zero/docs AND the prose guides that explain the concepts behind them. The query is split on whitespace; a doc matches when EVERY word appears (case-insensitive substring) in its PATH or its BODY text. Path matches rank first; body matches follow, ordered by how many of the query's words the document's own path carries, so the page NAMED after the concept comes before prose that mentions it. Searches content, so a concept like "global illumination" or "indirect light" surfaces the tools/components/guides that describe it — not just docs named that. Every returned line is a path vfs.read accepts. Called from code that can yield (a task, an execute, the engine shell), the walk spreads over frames so the engine keeps drawing while it reads.

Parameters

  • query string

Returns string

"velocity"
"global illumination"
"indirect light"
"voxel pivot"

tools/appearance/replaceWithContent

appearance.replaceWithContent(targets: eo.Targets, source: string, opts?: ReplaceOpts) -> ReplaceReport

Replace one or many entities with a content asset, resolution-first: pass a name / path / identity STRING (limit the search with type), and the tool resolves it and reports the canonical asset it hit. Each target entity (and its WHOLE subtree) is removed, and a fresh spawn of the content takes its slot — same parent, local position, rotation, and name; scale kept unless keepScale = false. Components on the removed entities do not carry over. Targets are entity names or ids, one or a list, or a scene.find QUERY resolved for you — { name = { "tree" } }, { component = { "Light" } }, { under = { "forest" } }. dryRun = true resolves everything and returns the plan (which entities would be removed, how big each subtree is, what would spawn) without mutating.

Parameters

  • targets eo.Targets
  • source string
  • opts ReplaceOpts (optional)

Returns ReplaceReport

"rider_blockout", "a_snowboarder"
{ "tree_1", "tree_2", "tree_3" }, "pine", { type = "bundle" }
{ "tree_1", "tree_2" }, "pine", { dryRun = true }

tools/appearance/setModel

appearance.setModel(root: string, model: string | table, opts?: AppearanceOpts) -> AssignSummary

Assign a model (mesh) to the Model / SkinnedModel-bearing entities found by searching an entity hierarchy from root. Sets the model field on each match's Model / SkinnedModel. You don't have to locate the exact entity; give the root and the model, and this walks the subtree. By default it assigns to EVERY match under the root; pass { first = true } for only the first match.

Parameters

  • root string
  • model string | table
  • opts AppearanceOpts (optional)

Returns AssignSummary

"Rocks", "sphere"
"Rocks", boulderMeshRef, { match = "rock_%d+" }

tools/appearance/swapMaterials

appearance.swapMaterials(material: string | AssetRef<material>, opts?: SwapMaterialsOpts) -> AssignSummary

Batch-assign one material across entity hierarchies in a single call. material is a string — combed against EVERY material in the project (exact identity wins, then exact leaf name, then the first case-insensitive substring match) — or an AssetRef envelope, used directly. Targets come from component schemas: every targeted entity's components are enumerated, and every public field declared Field.resource("material", …) / Field.assetRef("material", …) (on Model, SkinnedModel, and any other component that declares one) is set to the resolved material, and so is the Material component of an entity drawn with a bare mesh, such as the output a procgen Generator realizes. opts.roots selects the hierarchies — one entity id/name, an array of ids/names/entity proxies, or absent for the ENTIRE scene; each is scanned recursively, and entities reached through more than one root are assigned once. A blue outline flashes on each affected entity for visual confirmation.

Parameters

  • material string | AssetRef<material>
  • opts SwapMaterialsOpts (optional)

Returns AssignSummary

"weathered_stone"  -- whole scene
goldRef, { roots = "Castle", first = true }
"brick", { roots = { "wall_a", "wall_b" }, match = "wall" }

tools/appearance/swapShader

appearance.swapShader(roots?: (string | { any }), shader: string | AssetRef<shader>, opts?: AppearanceOpts) -> ShaderSwapResult

Convert every material in use across entity hierarchies to a new shader, in one call — "convert every single material in this scene to the ps1 shader". Scans the targeted hierarchies recursively, reads every component public field declared Field.resource("material", …) / Field.assetRef("material", …) (discovered from the component schemas, so any component carrying a materialRef field participates), collects the DISTINCT materials those fields hold, and routes each through the material asset's setShader — which preserves texture links and property values by canonical role (MAIN_TEX→albedo→base_color_texture and friends), rewrites the material asset's mat.yaml, and marks every instance dirty so the change persists and propagates. For a material you already hold, call matRef:setShader(shader) directly — this tool is for discovering the materials you'd otherwise have to enumerate yourself.

Parameters

  • roots (string | { any }) (optional)
  • shader string | AssetRef<shader>
  • opts AppearanceOpts (optional)

Returns ShaderSwapResult

nil, "@builtin::shaders.ps1"  -- whole scene
"Castle", unlitShaderRef
{ "wall_a", "wall_b" }, "pbr"

tools/assets/backfillSearchMeta

assets.backfillSearchMeta(opts?: BackfillOpts) -> BackfillResult

Re-derive the search .metadata (component set, humanoid flag) for every existing asset of the given types by invoking each asset's reindexSearchMeta ref method. Assets minted before the deriver existed carry no .metadata.components / .metadata.humanoid, so their search facets read false until this runs. Idempotent — re-running recomputes the same values. Only assets whose type exposes reindexSearchMeta are touched; the rest are counted as scanned and left alone.

Parameters

  • opts BackfillOpts (optional)

Returns BackfillResult

{ types = { "bundle" } }

tools/assets/describe

assets.describe(assetName: string, opts?: DescribeOpts) -> string

Describe an asset in agent-readable markdown: identity, type, scope, origin, description (README lead), tags, a Behavior section listing the operations any asset of this type answers to (its assetType's ref:method(...) surface, with signatures and descriptions), and a type-specific detail section (fields, events, and the component's own instance methods for a component; exports for a module; the call contract for a tool; ...). Every method, event, and field is rendered as a full signature so nothing needs the source read. Resolves assetName the same way assets.find results resolve. Pairs with assets.find (find -> describe).

Parameters

  • assetName string
  • opts DescribeOpts (optional)

Returns string

"@builtin::components.Camera"

tools/assets/fetchInUse

assets.fetchInUse(targets?: (string | EntityRef | { string | EntityRef })) -> FetchSummary

Fetch the unique set of asset references in use on the targeted entities — materials, meshes, textures, bundles, and every other category a component field can hold. targets is one entity id/name, an array of ids/names/entity proxies, or nil for EVERY entity in the scene; root targets are scanned recursively through their children. For each entity, every component's public data is scanned (nested tables to depth 8) for AssetRef envelopes; refs are deduped by guid and returned with the entities that use them — the agent's one-call inventory of what the targeted content is made of.

Parameters

  • targets (string | EntityRef | { string | EntityRef }) (optional)

Returns FetchSummary

-- everything the scene references
"Castle"     -- everything the Castle hierarchy uses
{ "wall_a", "wall_b" }

tools/assets/find

assets.find(substring: string, category?: string, opts?: { subassets: boolean? }) -> FindSummary

Find every asset in the entire project whose identity contains substring (case-insensitive). Searches the full asset index — every category, every library — and returns the matches grouped by category. Pass category to restrict the search to one kind (discover the available kinds via asset.categories()).

Parameters

  • substring string
  • category string (optional)
  • opts { subassets: boolean? } (optional)

Returns FindSummary

"car"
"stone", "material"
"wheel", nil, { subassets = true }

tools/assets/fromEntity

assets.fromEntity(targets?: (string | EntityRef | { string | EntityRef }), opts?: FromEntityOpts) -> FromEntitySummary

Capture everything an entity is carrying at RUNTIME — every live procedural mesh / texture handle held in a component's resource field, across the whole hierarchy — as durable assets, and repoint each field at the new persistent asset. A runtime mesh (renderer.mesh.create) has no asset behind it, so anything keyed to it (a GI bake, a saved scene) is session-only; this makes it permanent in one call. Every public resource field of every component participates (discovered by field reflection), so it reifies ALL of an entity's runtime content, not a single hard-coded type. Persistent references (already assets) are left untouched. In edit mode the new assets persist to the world's source; in play mode they land in the ephemeral runtime store, so run this in EDIT to make content durable. Runtime resources shared across several fields are reified once and every field is repointed at the one asset.

Parameters

  • targets (string | EntityRef | { string | EntityRef }) (optional)
  • opts FromEntityOpts (optional)

Returns FromEntitySummary

-- freeze every runtime resource in the scene
"cornellRoom"             -- freeze the cornellRoom hierarchy's runtime meshes
nil, { dryRun = true }    -- report what would be reified, write nothing

tools/assets/generatePreviews

assets.generatePreviews(opts?: GeneratePreviewsOpts) -> GenerateSummary

Queue a render of the missing or stale preview for every world asset whose type declares a preview: recipe, under root (default: the whole world source), and remove empty leftover README.md files inside the visited assets. Skips assets whose preview is current (preview.isCurrent) unless force is set. Returns as soon as the scan finishes; the queued renders drain serially in the background — poll assets.previewStatus until pending is 0.

Parameters

  • opts GeneratePreviewsOpts (optional)

Returns GenerateSummary

{ root = "/source/projects/mygame" }

tools/assets/new

assets.new(source: string | RuntimeHandle, name?: string, opts?: NewOpts) -> AssetRef<mesh | texture>

Persist a LIVE runtime GPU resource into a durable asset. source is a resource handle from renderer.mesh.create / renderer.texture.create, or a bare guid string naming a GPU-resident resource (paired with opts.category). The resource is read back from the GPU (yields a frame or two), encoded, and written as a new asset via asset.create, so anything keyed to it — a GI bake, a saved scene, a material slot — survives a reload instead of regenerating with a fresh identity. In edit mode the asset persists to the world's source; in play mode it lands in the ephemeral runtime store, so reify in EDIT to make content durable. Raw in-hand geometry/pixels with no GPU resource go through asset.create(<type>, name, data) directly — this tool freezes a live handle.

Parameters

  • source string | RuntimeHandle
  • name string (optional)
  • opts NewOpts (optional)

Returns AssetRef<mesh | texture>

renderer.mesh.create({ positions = p, indices = i }), "tree"
meshGuid, "terrain", { category = "mesh" }

tools/assets/previewStatus

assets.previewStatus() -> PreviewStatus

Report the preview work queue: renders still pending, the asset currently rendering, what holds the queue while it waits, session totals, and the most recent failures. Poll after generatePreviews until pending reaches 0 and active is empty. Reading it resumes a drain that stopped while work was queued.

Returns PreviewStatus

tools/assets/reconcileReferenceNames

assets.reconcileReferenceNames(opts?: ReconcileReferenceNamesOpts) -> ReconcileReferenceNamesReport

Write the current name of every moved reference into the owned world sources that still spell an old one. A reference resolves through the guid its source recorded, so it reaches the same asset before and after: what changes is the text, which then names the asset the way it is named now, and the notice about the source stops. Each write is an edit of that source, synced to the world and running the asset's change hooks like any other. Only the author's sources are touched: the world's own files and installed assets the author has modified.

Parameters

  • opts ReconcileReferenceNamesOpts (optional)

Returns ReconcileReferenceNamesReport

{ dryRun = true }

tools/assets/typeBehavior

assets.typeBehavior(name: string) -> BehaviorResult

What an asset of this TYPE can do — the operations every ref of it answers to (ref:method(...)), the lifecycle hooks it runs, and whether it can become an entity in a scene. This is the difference between knowing a type exists and knowing what it gives you: a type that already evaluates, instantiates, validates and recompiles itself is scaffolding to build inside, not a name in a list. Each operation carries the type author's own three answers: what it does (description), what to pass it and what it hands back (signature, the arguments and the declared return), and what that return means (returns). Read it with assets.typeDoc for the prose, and assets.types to find the name.

Parameters

  • name string

Returns BehaviorResult

"procGraph"

tools/assets/typeDoc

assets.typeDoc(name: string) -> { [string]: any }

Read one asset type's own documentation — what the type IS, the shape of its folder, how it is authored, and what it is for. This is the type's README as its author wrote it, so it says things no signature can: which file is the source, what regenerates when, what the type does for you. Read this before building anything that resembles what the type describes. assets.types finds the name to pass here.

Parameters

  • name string

Returns { [string]: any }

"procGraph"
"terrain"

tools/assets/types

assets.types(query?: string) -> TypesResult

Every KIND of asset this engine can make — the scaffolding available to build inside, as opposed to assets.find (content that exists) or search_tools (operations you can call). Each row is a registered assetType: what it is, the asset.create call that makes one, and whether it can become an entity in a scene. Reach for this BEFORE writing a system of your own. A job that looks like "no tool does this, so I will write it" is usually a job with a type already shaped for it — a graph that regenerates from parameters, a terrain, a voxel template, a population — and building beside one of those is a few lines where building from scratch is a project.

Parameters

  • query string (optional)

Returns TypesResult

"procedural"
"terrain"

tools/assets/users

assets.users(assetArg: string | AssetRef) -> UsersSummary

Find every entity in the active layer that uses an asset. Give the asset as a word — the entire project is combed case-insensitively and the first matching asset is used — or as an AssetRef you already hold. Every entity's component data is scanned recursively for references to that asset, and each use site is reported with the entity, component, and field it lives in.

Parameters

  • assetArg string | AssetRef

Returns UsersSummary

"pickup_car"
goldMaterialRef

tools/baking/all

baking.all(opts?: AllOpts) -> AllResult

Run the entire GI bake in one call — static-surface lightmaps, an auto-placed irradiance probe volume for moving entities, and auto-placed reflection probes — then report what remains. This is the finalize step for a static scene: it takes the scene from "everything recomputed every frame" to baked. Each stage can be skipped (skipLightmaps / skipProbes / skipReflections); resolution / samples tune quality. Returns each stage's result plus a fresh remaining coverage report. This runs for many seconds on a real scene and is handed back as a background task: check how far it has got with this toolbox's status tool, and a lightmap bake finished notice reports the end.

Parameters

  • opts AllOpts (optional)

Returns AllResult

-- bake everything in the scene
{ resolution = 128, samples = 128 }
{ scope = "arena", skipReflections = true }

tools/baking/clear

baking.clear(opts?: ClearOpts) -> ClearResult

Clear baked GI across a scope — restores each static surface's original material (removing its lightmap), despawns irradiance-probe volumes, unregisters reflection probes, and deletes the cubemap assets their bake wrote. A probe attached to a scene object loses its ReflectionProbe component and the object stays; an entity the bake placed to hold a probe is despawned. Use it to re-bake from a clean slate or strip GI from a region. Each category can be kept (keepLightmaps / keepProbes / keepReflections). Returns how many of each were cleared.

Parameters

  • opts ClearOpts (optional)

Returns ClearResult

-- clear everything in the scene
{ scope = "arena" }
{ keepReflections = true }

tools/baking/detect

baking.detect(scope?: (string | EntityRef | { string | EntityRef })) -> DetectReport

Report what in the scene needs baking / re-baking and why — the bake-coverage check. Scans the scene (or a scope) for the state that quietly wrecks performance and leaves indirect light missing: static geometry with no bake (no lightmap and no probe-field coverage), shadow-casting point/spot lights re-rendering their shadow every frame, movers outside every probe volume's field, and reflective scenes with no reflection probe. Each finding names the one call that fixes it. Returns clean = true with an empty findings list when the scene is already covered.

Parameters

  • scope (string | EntityRef | { string | EntityRef }) (optional)

Returns DetectReport

-- whole scene
"interior"

tools/baking/lightmaps

baking.lightmaps(opts?: LightmapOpts) -> LightmapResult

Bake static-geometry lightmaps across a scope in one call — resolves the static surfaces under scope (every visible Model whose resolveMobility() is "static"; movable geometry, physics bodies, and players sample probe volumes instead) and bakes full irradiance into a per-surface lightmap, no per-entity setup. Surfaces whose UVs tile or overlap are bound to the covering probe volume instead (counted in probeLit); set Model.mobility to override a wrong mobility derivation. match narrows to surfaces whose name contains a substring; resolution / samples / intensity set quality. Returns how many baked, how many went to the probe field, how many were skipped, and a per-surface report. This runs for many seconds on a real scene and is handed back as a background task: check how far it has got with this toolbox's status tool, and a lightmap bake finished notice reports the end.

Parameters

  • opts LightmapOpts (optional)

Returns LightmapResult

-- whole scene
{ scope = "level_geo", resolution = 128, samples = 32 }
{ match = "floor" }

tools/baking/mobility

baking.mobility(opts?: MobilityOpts) -> MobilityResult

Report (and optionally batch-set) geometry mobility across a scope. Every Model in scope is listed with its authored mobility field and what it resolves to — static geometry receives lightmaps and occludes baked light; movable geometry samples probe volumes. Pass set to override the field on every matched Model in one call (the exclusion switch when the automatic derivation gets an entity wrong); omit it to just inspect. match narrows by name substring.

Parameters

  • opts MobilityOpts (optional)

Returns MobilityResult

-- report the whole scene
{ match = "crate", set = "static" }
{ scope = "props", set = "movable" }

tools/baking/probes

baking.probes(opts?: ProbeOpts) -> ProbeResult

Place irradiance light-probe volumes automatically and bake them — the whole light-probe pass in one call, no hand placement. Gathers the scene's point/spot lights (limited by scope), CLUSTERS them by influence overlap, and drops one right-sized volume per cluster (so separated rooms get separate volumes, not one box over the dead space between them). Probe density comes from spacing (world units between probes) with a hard maxProbes cap per volume, so a big volume gets coarser probes rather than a runaway grid; maxVolumes bounds the volume count by merging the nearest clusters. When a scene has no point/spot lights (sun/ambient only), it falls back to one volume over the scene's geometry bounds. Each baked field publishes into the renderer's irradiance-volume set and persists into the scene's baked-lighting container — every standard-PBR fragment inside a volume (movers, avatars, freshly spawned props) samples it with no per-entity setup, and a fresh boot restores it with no re-bake. Returns each placed volume (id, bounds, grid resolution, probe count) plus totals. The same placement is available to component/execute code as require("@builtin::systems.globalIllumination.volumeProbe").autoPlace(opts).

Parameters

  • opts ProbeOpts (optional)

Returns ProbeResult

-- whole scene, defaults
{ spacing = 2, samples = 128 }
{ scope = "interior", maxProbes = 4096 }

tools/baking/reflections

baking.reflections(opts?: ReflectionOpts) -> ReflectionResult

Place reflection probes across the scene automatically and bake them — the whole reflection pass in one call, no hand placement. The tool bounds the scene's geometry (limited by scope), spreads a small set of probes evenly through that volume, and bakes each into its cubemap; reflective surfaces then sample the probes whose radius reaches them, proximity-blended by the renderer, and the sky fallback beyond every radius. count defaults to a value derived from the scene size and is clamped to the renderer's 8-probe budget; radius defaults to a size that overlaps neighbours for seamless coverage; bake = false places without baking. Returns the placed probes (id, position, radius) and how many baked.

Parameters

  • opts ReflectionOpts (optional)

Returns ReflectionResult

-- whole scene, auto count
{ count = 4 }
{ scope = "lobby", count = 1, radius = 20 }

tools/baking/showProbes

baking.showProbes(opts?: ShowProbesOpts) -> ShowProbesResult

Visualize the baked irradiance field — drop an unlit, color-coded marker at every probe position so an invisible SH bake becomes something you can see and judge. Each marker is tinted by the light that probe captured (brighter where the bake gathered more light, dark in occluded corners), so you can read the falloff through the volume and catch a bake that came back empty. Reads the volume's baked probe buffer (bake first). volume targets one volume (default: every VolumeProbe in the scene); gain scales brightness (default auto-normalizes to the brightest probe); stride thins a dense grid; scale sizes the markers. Re-run to refresh; baking.clear (or deleting the gi_probe_viz entity) removes it.

Parameters

  • opts ShowProbesOpts (optional)

Returns ShowProbesResult

-- every baked volume
{ gain = 4, scale = 0.2 }
{ volume = "probe_volume_1", stride = 2 }

tools/baking/status

baking.status() -> BakeStatus

Report the running bake's progress: whether one is going, how many receivers it has FINISHED (done, which percent is derived from) and how many it has REACHED (started) out of the total it resolved, which one it is on, and how long it has been running. A bake is handed off as a background task, so this is how you check on it while it works; the lightmap bake finished notice reports the end.

Returns BakeStatus

-- while a bake is running
-- poll until `running` is false

tools/cam/frame

cam.frame(entityId: string | EntityRef, opts?: FrameOpts)

Position a camera to frame a target entity from a distance + angle, then point it at the entity in one call. opts.angle accepts three forms (matches capture.fromEntity): a number (elevation pitch in degrees — legacy scalar), {yaw, pitch} (array form), or {yaw=, pitch=} (named form). opts.yaw is still respected as a fallback when opts.angle is a number or doesn't carry a yaw entry.

Parameters

  • entityId string | EntityRef
  • opts FrameOpts (optional)
'my_cube'
'my_cube', { distance = 20, angle = 45, yaw = 90 }
'my_cube', { angle = {90, 60} }

tools/cam/get

cam.get(camId?: (string | EntityRef)) -> CamGetResult

Read camera info — transform + fov. Defaults to the active camera (camera.active()) when camId is omitted. Returns { error = ... } when no camera is active, and when the reference names no entity.

Parameters

  • camId (string | EntityRef) (optional)

Returns CamGetResult

'cam2'

tools/cam/lookAt

cam.lookAt(camIdOrX: string | EntityRef | number, xOrY: number, yOrZ: number, zOrNil?: number)

Point a camera at a world position. Computes a look-at rotation (yaw + pitch) and writes it to the camera's localRotation. Two call shapes: cam.lookAt(x, y, z) (uses active camera) and cam.lookAt(camId, x, y, z) (specific camera). Logs and returns when no camera / target is available, when the camera has no position, or when the camera is already at the target (length < 0.001).

Parameters

  • camIdOrX string | EntityRef | number
  • xOrY number
  • yOrZ number
  • zOrNil number (optional)
0, 0, 0
'cam2', 5, 3, 0

tools/cam/spawn

cam.spawn(name: string, ...) -> string

Spawn a camera entity with a Camera component at a position. Supports four call shapes for the position: no args (origin), ({x,y,z}, opts?), (opts) (opts only), or (x, y, z, opts?). When opts.renderTarget is set, creates a GPU texture and points the camera at it via Camera:setTargetTexture; sample it by entity(id).component.get("Camera").textureHandle and free it with renderer.destroy when done.

Parameters

  • name string

Returns string

"main", 5, 5, 5
"rt_cam", { renderTarget = { name = "feed", width = 256, height = 256 } }

tools/camera/bindToPlayer

camera.bindToPlayer(camera?: (EntityRef | string)) -> BindToPlayerResult

Make the joining player use this camera in play — bind it to the active scene's PlayerPrototype. The prototype's camera role must be a descendant of its subtree (it is cloned with the player on spawn), so this re-parents the camera into the prototype (keeping its world pose) and sets the prototype's camera ref to it. This is how you choose the player's camera: pair it with a static behavior (camera.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. On spawn the player's body is bound as the camera's follow target; a static behavior ignores that and holds its pose. Errors when the scene has no PlayerPrototype (a player-less scene has no player camera to bind). Returns { prototype, camera }.

Parameters

  • camera (EntityRef | string) (optional)

Returns BindToPlayerResult

"fixedCam"

tools/camera/create

camera.create(name: string, position?: (vec3 | number), opts?: CameraCreateOpts) -> shared.CameraInfo

Spawn a new camera entity at a position, with any starting parameters. opts accepts fov, near, far, priority, renderLayers (a layer-name spec like "all !ui"), debugChannel, follow (an entity the camera behavior tracks), behavior (a cameraBehavior-tagged component), lookAt (a world position or entity to aim at), and target (render-to-texture: true or { width, height, name } renders offscreen; omit for the viewport). The scene's primary camera is spawned for you 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.

Parameters

  • name string
  • position (vec3 | number) (optional)
  • opts CameraCreateOpts (optional)

Returns shared.CameraInfo

"securityCam", { 0, 6, 12 }, { fov = 50, renderLayers = "all !ui" }
"feedCam", { x = 2, y = 3, z = 2 }, { target = { width = 512, height = 512 }, lookAt = "player" }

tools/camera/frame

camera.frame(camera?: (EntityRef | string), target: EntityRef | string, opts?: CameraFrameOpts) -> shared.CameraInfo

Position and aim a camera to frame a target entity in one step: place the camera at distance from the target on the yaw/pitch angle, then look at it. opts accepts distance (default 10), yaw (default 0), and pitch (default 20), all degrees. Pass nil for the first argument to frame with the active camera. Returns the camera's updated parameter record.

Parameters

  • camera (EntityRef | string) (optional)
  • target EntityRef | string
  • opts CameraFrameOpts (optional)

Returns shared.CameraInfo

"securityCam", "player", { distance = 8, yaw = 30, pitch = 15 }
nil, "statue"

tools/camera/get

camera.get(camera?: (EntityRef | string)) -> shared.CameraInfo

Read the full parameter record for one camera — position, rotation, fov, near, far, priority, render-layer spec, debug channel, render-target guid, behavior, and follow target — beside whether it is enabled (the switch that decides whether it renders at all), whether it is rendering, the reason it is not, the projection the renderer built for it in frame, mismatch naming every field where that frame disagrees with the record above, and followResolves saying whether the follow target names an entity that is live. Pass a camera name/id/proxy, or nothing to read the active (on-screen) camera, falling back to the scene's main camera.

Parameters

  • camera (EntityRef | string) (optional)

Returns shared.CameraInfo

"securityCam"

tools/camera/list

camera.list() -> { shared.CameraRow }

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 (the order the renderer resolves the on-screen camera). Each row marks whether it is the active camera (drawn this frame), whether it is enabled — the per-camera switch that decides whether it competes for the viewport or renders into a target at all — whether it is rendering and the reason it is not, whether it is the player camera (the one the PlayerPrototype uses — the joining player's view), and its role — "main" (highest-priority active non-editor), "editor" (the editor fly-camera), or "" — plus its priority, fov, render-layer spec, and render-target guid. For the projection the renderer actually built for each camera, and what each cost, read camera.observe().

Returns { shared.CameraRow }

tools/camera/lookAt

camera.lookAt(camera?: LookAtRef, target?: LookAtRef) -> shared.CameraInfo

Aim a camera at a world position or at another entity. The target is an entity name/id or a position { x, y, z } / { x =, y =, z = }. Pass a single target to aim the active camera, or a camera plus a target to aim a specific one. Returns the camera's updated parameter record.

Parameters

  • camera LookAtRef (optional)
  • target LookAtRef (optional)

Returns shared.CameraInfo

"securityCam", "player"
{ 10, 0, 5 }

tools/camera/set

camera.set(camera?: (EntityRef | string | CameraSetParams), params?: CameraSetParams) -> shared.CameraInfo

Change one or more parameters on a camera in a single call. Accepts fov, near, far, priority, debugPass (the diagnostic view this camera renders, BY NAME — "final" (lit), "normal", "depth", a content view like "lightmap", …; renderer.debugPass.list() enumerates them; the live viewport draws the active camera's pass), renderLayers (a layer-name spec like "all !ui" — a bare name includes a layer, !name excludes it), behavior (a cameraBehavior-tagged component ref), and follow (the entity the behavior tracks). debugChannel accepts the raw numeric channel for the same effect. Pass no camera (or nil) to change the active camera; pass { ... } as the only argument for the same. Returns the camera's updated parameter record.

Parameters

  • camera (EntityRef | string | CameraSetParams) (optional)
  • params CameraSetParams (optional)

Returns shared.CameraInfo

"securityCam", { fov = 50, priority = 5 }
{ debugPass = "normal" }
{ debugPass = "final" }   -- back to the lit image

tools/capture/cleanup

capture.cleanup() -> number

Clear every file in /source/tmp/capture/. Walks the directory with vfs.list and removes each non-directory entry via vfs.remove. Safe to call on a missing / empty directory.

Returns number

tools/capture/collage

capture.collage(opts: CaptureCollageOpts) -> (shared.CaptureResult?, string?)

Render a GRID of frames into one image and return it. What varies from cell to cell is what you pass, and a collage says so rather than assuming: setups gives a cell per whole camera set-up — the contact sheet of a shot list, each entry written the way a single capture is aimed — viewpoints gives a cell per named view of one subject (front, back, left, right, top, bottom, iso — or "sides" for all six), passes gives a cell per render pass (final, albedo, normal, depth, motion_vectors, and any content-registered capture view), and duration gives a cell per sample across that many seconds of gameplay. Naming a station axis (setups or viewpoints) AND passes lays out a 2D grid: a row per station, a column per pass, so reading across a row is one set-up under every pass and reading down a column is one pass from every set-up. A collage with no axis is refused naming all of them; setups and viewpoints both say where the camera stands and cannot combine; and duration combines with none of them — a cell differing both in what it looks at and in when it was taken answers neither question. The grid shape is worked out from the number of cells; pass grid ("3x2") to state it. width and height are the whole SHEET, and a cell is rendered at the box the grid divides out of it — so a cell holds the same picture a single capture at that cell's own width/height holds, whatever the grid's shape, and a round subject reads round in every cell of a 4x1. Every cell comes back labelled on the result with the station it was rendered from and the pixels it was rendered at, so a grid is readable from the response instead of guessable from the pictures.

Parameters

  • opts CaptureCollageOpts

Returns (shared.CaptureResult?, string?)

{ setups = { { position = { -14.6, 1.35, 4.5 }, lookAt = { -16.3, 1.1, 1.6 }, fov = 28, label = "shot 1" }, { position = { 8, 3, 12 }, lookAt = { 0, 1, 0 }, fov = 50, label = "shot 2" } }, grid = "2x1" }
{ entity = "crate", viewpoints = "sides", basis = "local", projection = "orthographic", isolate = true }
{ entity = "crate", basis = "local", viewpoints = { "front", "iso" }, passes = { "final", "normal" } }
{ passes = { "final", "albedo", "normal", "depth" } }
{ entity = "salt_pan", viewpoints = "sides", basis = "local", clearAir = true }
{ duration = 3, grid = "3x3", path = "/source/tmp/capture/motion.jpg" }

tools/capture/fromCamera

capture.fromCamera(camera: string | EntityRef, opts?: (string | FromCameraOpts)) -> (shared.CaptureImage?, string?)

Render the scene from a specific NAMED camera and return the picture. By DEFAULT it uses the camera's exact authored settings: its world-space pose plus its whole lens (fov, near, far, and the projection it builds with the orthoHeight an orthographic camera covers), the diagnostic debugChannel it draws, whether it runs the post-process chain, the lighting environment it states (an isolated camera's background and ambient with it), and the scene layers it draws: the camera's own renderLayers with the editor's chrome (EditorUI) and the authoring overlays (debug) taken off, since those are drawn for whoever is editing rather than by that camera into the world. So the frame is precisely what that camera draws of the scene, without changing what is on screen. Pass opts to OVERRIDE any of those for this one capture WITHOUT touching the camera: pass (what the frame HOLDS, by name: "final", a built-in diagnostic pass like albedo / normal / depth / roughness, or a capture view a render feature published; it picks the channel rendered, the layers that pass is read under, and whether the post-process chain runs over it; a diagnostic buffer is read flat), view (a capture view named directly, by the name a render feature published it under; renderer.captureView.list() enumerates them, and a name no feature published is refused against that list), renderLayers (a layer-name spec like "all !ui"), debugChannel (the same channel selection by number), fov, near, far, projection, orthoHeight, width, height, and path (where to also write the PNG). Use it to check any specific camera (a security view, a cutscene angle, a render-texture feed, an orthographic reference camera) and to preview it under different layers or a diagnostic pass.

Parameters

  • camera string | EntityRef
  • opts (string | FromCameraOpts) (optional)

Returns (shared.CaptureImage?, string?)

"securityCam"
"securityCam", { pass = "normal" }
"securityCam", { view = "overdraw" }
"securityCam", { renderLayers = "all !ui", debugChannel = 0 }
"securityCam", "/source/tmp/capture/sec.png"

tools/capture/fromEntity

capture.fromEntity(opts: CaptureEntityOpts) -> (shared.CaptureImage?, string?)

Render the scene framed on an entity (or several) and return the picture. The camera auto-fits the union world-space bounds of the target(s), so the subject fills the frame at any scale; pass distance to orbit at a fixed radius instead. What an entity draws counts wherever it is drawn from: its Models and its descendants', and the instanced copies standing in its space or a descendant's — an effect a procedural graph draws by instancing is framed like a mesh. A subject that draws nothing is framed where it stands, and the picture's note says so.

Parameters

  • opts CaptureEntityOpts

Returns (shared.CaptureImage?, string?)

{ entity = "player" }
{ entity = "player", pass = "normal" }
{ entity = "crate", viewpoint = "front", basis = "local" }
{ entity = "crate", viewpoint = "top", basis = "local", projection = "orthographic", isolate = true }
{ entity = { "house", "tree" }, angle = {90, 15}, path = "/source/tmp/capture/yard.png" }
{ entity = "player", deterministic = true }
{ entity = "salt_pan", clearAir = true }

tools/capture/fromPosition

capture.fromPosition(opts: CapturePositionOpts) -> (shared.CaptureImage?, string?)

Render the scene from a world-space position and return the picture. Spawns an ephemeral offscreen camera, renders once and reads the target back; all resources clean up before the call returns.

Parameters

  • opts CapturePositionOpts

Returns (shared.CaptureImage?, string?)

{ position = {0, 5, 10}, lookAt = {0, 0, 0} }
{ position = {0, 5, 10}, lookAt = {0, 0, 0}, pass = "albedo" }
{ path = "/source/tmp/capture/front.png", position = {0, 5, 10}, lookAt = {0, 0, 0} }
{ position = {0, 5, 10}, lookAt = {0, 0, 0}, deterministic = true }
{ position = {0, 5, 10}, lookAt = {0, 0, 0}, clearAir = true }

tools/capture/oneshot

capture.oneshot(opts?: CaptureOneshotOpts) -> (CaptureHandle?, string?)

Spawn an ephemeral offscreen camera configured from opts, trigger one render, and return the handles so the caller can read the render target back synchronously and clean up. Unlike fromPosition / fromEntity / viewport, this does NOT spawn a task to handle readback — it returns immediately, making it usable from MCP in edit mode where the task scheduler does not tick.

Parameters

  • opts CaptureOneshotOpts (optional)

Returns (CaptureHandle?, string?)

{ source = "entity", entity = "Player", distance = 8, angle = {45, 20}, pass = "normal" }

tools/capture/viewport

capture.viewport(opts?: (string | ViewportOpts)) -> (shared.CaptureImage?, string?)

Capture the current viewport and return the picture. Mirrors the on-screen camera through the shared capture core (source = "screen"), so the frame is taken from that camera's pose, through that camera's own lens, lit by the environment that camera states (an isolated camera's background and ambient with it), under the render layers it draws the screen with, the editor's own chrome left out: the chrome stands around the viewport's picture, and the frame holds that picture: the scene, its debug overlays and the content UI drawn into it. Pass opts to OVERRIDE any of that for this one capture: width / height for a raster other than the viewport's own size, renderLayers (a layer-name spec like "all !EditorUI !debug") for the frame without the editor's chrome over it, pass for what the frame HOLDS ("final", a built-in diagnostic pass like albedo / normal / depth, or a capture view a render feature published), clearAir for the frame with the air between the camera and a surface taken out, and postProcessing to take the frame with the grade off.

Parameters

  • opts (string | ViewportOpts) (optional)

Returns (shared.CaptureImage?, string?)

"/source/tmp/capture/screenshot.png"
{ path = "/source/tmp/capture/clean.png", renderLayers = "all !EditorUI !debug" }
{ path = "/source/tmp/capture/half.png", width = 960, height = 540 }
{ path = "/source/tmp/capture/still.png", deterministic = true }
{ path = "/source/tmp/capture/normals.png", pass = "normal" }
{ path = "/source/tmp/capture/own_colour.png", clearAir = true }

tools/characterController/install

characterController.install(entityId: string, opts?: InstallOpts) -> InstallResult

Install CharacterController + Locomotion + MovementState on an entity, with an optional third-person camera child. Applies an optional preset (e.g. 'synty') for default property values, then layers user overrides on top. Idempotent per component — running install twice on the same entity is a no-op for already-present components. The camera child carries a Camera component plus the orbital_follow controller targeting the owning entity; skip it with camera = false.

Parameters

  • entityId string
  • opts InstallOpts (optional)

Returns InstallResult

"Player", { preset = "synty" }
player.id(), { preset = "humanoid", overrides = { moveSpeed = 8.0 } }
entityId, { camera = false }  -- no camera child

tools/characterController/uninstall

characterController.uninstall(entityId: string) -> UninstallResult

Tear down everything characterController.install() added on an entity: AnimDebug (if present), Locomotion, MovementState, CharacterController, and any child entity carrying an orbital_follow controller targeting this entity. Idempotent — missing components are skipped silently. Returns the count of despawned camera children so callers can verify cleanup.

Parameters

  • entityId string

Returns UninstallResult

"Player"
player.id()

tools/cutscene/pause

cutscene.pause() -> shared.ZmToolResult

Freeze the cutscene playing now. The clock stops and so does the scene under it — components stop updating, physics stops stepping, animation stops advancing — which is what keeps one shot on screen for as many captures and edits as the work needs. cutscene.resume starts both again.

Returns shared.ZmToolResult

tools/cutscene/play

cutscene.play(source: string, paused?: boolean) -> shared.ZmToolResult

Play a cutscene now, from the module its definition is returned from — the same call a scene entrypoint or a CutsceneTrigger makes, so what you see here is what a player sees. Starting one while another is playing ends that one first.

Parameters

  • source string
  • paused boolean (optional)

Returns shared.ZmToolResult

"mygame.scenes.intro"
"mygame.scenes.intro", true

tools/cutscene/rate

cutscene.rate(r: number) -> shared.ZmToolResult

Set the playback rate of the cutscene playing now. 1 is real time, 0.25 slow motion, 2 double speed; a negative rate runs it backwards. The cutscene itself is untouched — only how fast its clock moves.

Parameters

  • r number

Returns shared.ZmToolResult

0.25
-1

tools/cutscene/resume

cutscene.resume() -> shared.ZmToolResult

Start the cutscene playing now again after cutscene.pause.

Returns shared.ZmToolResult

tools/cutscene/seek

cutscene.seek(t: number) -> shared.ZmToolResult

Move the clock of the cutscene playing now to an exact moment. The frame it lands on is the frame that time would have produced — the camera, the fade and the subtitle are all re-derived — so a shot can be checked without watching the cutscene up to it.

Parameters

  • t number

Returns shared.ZmToolResult

6.5

tools/cutscene/skip

cutscene.skip() -> shared.ZmToolResult

Skip the cutscene playing now to its closing fade — what a player pressing the skip key does. Cues between here and the end still fire, so a cutscene that opens a door on its way out opens it.

Returns shared.ZmToolResult

tools/cutscene/status

cutscene.status() -> shared.ZmToolResult

Report the cutscene playing now — its name, where its clock is, which shot is on screen, and whether it is paused or running at an unusual rate. Reads the cutscene whatever started it, so this answers about the one a scene is playing, not a private copy.

Returns shared.ZmToolResult

tools/cutscene/stop

cutscene.stop() -> shared.ZmToolResult

End the cutscene playing now. The camera, the overlay and the player's controls all go back the way they were, the same as when a cutscene reaches its end on its own.

Returns shared.ZmToolResult

tools/debug/inspect

debug.inspect(target: string | { any }, opts?: InspectOpts) -> InspectResult

Inspect entity component state for every entity matching the target. A name resolves to every matching entity; an id to itself; arrays and entity proxies work too. Each match reports its script components (serialized public data) and native ECS components, selected by opts.include.

Parameters

  • target string | { any }
  • opts InspectOpts (optional)

Returns InspectResult

'player'
'lamp', { include = "ecs" }
'enemy_*', { types = { "Health" }, limit = 5 }

tools/debug/playSession

debug.playSession(enabled?: boolean) -> { playSession: boolean }

Show or hide the debug overlay in play mode. Edit mode always shows the overlay; play mode shows it only while this session is on — the way to watch bones, colliders, or bounds on a running, animating scene. The flag lives in memory only and is never saved, so debug visibility can never be permanently toggled on by accident; it resets on engine restart.

Parameters

  • enabled boolean (optional)

Returns { playSession: boolean }

true
false

tools/debug/problems

debug.problems(opts?: ProblemsOpts) -> ProblemsResult

Return the errors and warnings the engine has logged since your last call — the non-crashing failures (contract violations, component awake/update throws, auto-disabled components, unresolved requires, bridged renderer/asset errors) that otherwise never surface in execute results. Each call advances a read-cursor, so repeated calls report only what is new, oldest first (the root-cause error surfaces ahead of the cascade it triggered). When something you built does not behave, call this FIRST — the engine has usually already logged why.

Parameters

  • opts ProblemsOpts (optional)

Returns ProblemsResult

{ level = "error", limit = 20 }
{ all = true }

tools/debug/scope

debug.scope(target?: (string | { any })) -> { scoped: { string }, count: number }

Limit every enabled debug category to a set of entities, so the overlay is focused on specific objects. Call with no argument (or an empty list) to clear the focus and draw for every entity again.

Parameters

  • target (string | { any }) (optional)

Returns { scoped: { string }, count: number }

'player'
{ 'player', 'enemy' }

tools/debug/set

debug.set(category: string, enabled?: boolean) -> { [string]: boolean }

Show or hide a debug-visualization overlay category. The overlay is ambient editor tooling drawn for the whole scene; this flips which categories draw. A category is settable before the package that draws it has registered — the state is kept and applies when it arrives — so a script that configures overlays does not depend on how far along the session is. debug.state lists what is set.

Parameters

  • category string
  • enabled boolean (optional)

Returns { [string]: boolean }

'colliders'
'bones', true
'all', false

tools/debug/state

debug.state() -> DebugState

Read the current debug-visualization state: which categories draw, the entity scope, whether the play-mode session is on, and the engine mode. categories covers every category the session knows — those a package has registered to draw, plus any set before its provider arrived — so what this lists is what debug.set accepts.

Returns DebugState

tools/detail/resident

detail.resident(axis?: string) -> { [string]: any }

Report what a world has resident of its detail right now. Covers terrain's LOD cut, a voxel world's chunk meshes, spatial streaming's cell store and mesh LOD's level selection, each read from the system's own state rather than from the component fields that asked for it. Every voxel chunk carries what became of its mesh build, so a world that stands with the right block count and draws nothing reads as what it is. The same reading is served at /zero/runtime/observations/streaming.

Parameters

  • axis string (optional)

Returns { [string]: any }

"voxel"

tools/detail/whyNotDrawn

detail.whyNotDrawn(subject: any) -> { [string]: any }

Answer why a piece of a world's detail is not on screen. The reason is the nearest cause from a closed set — drawn, notManaged, notBuilt, buildQueued, buildFailed, builtEmpty, emittedNothing, outsideRadius, evicted, coarserLevel, finerLevel, noSuchLevel, hidden — so it names the thing to change rather than a consequence of it, and detail carries the engine's own message when a mesh build was dropped. Reads the four detail systems' state rather than the components' fields.

Parameters

  • subject any (optional)

Returns { [string]: any }

"Vox"

tools/diagnostics/errors

diagnostics.errors(entityFilter?: (string | EntityFilterTable)) -> { EntityErrors }

Get all component/entity errors in the scene. With no argument, returns errors for every entity of the active root scene layer, children included (enumerated from /runtime/layers/<layer>/entities and each entity's children directory beneath it). With an entity name, path or ID, returns errors for that entity only. Each entity's errors are read from /runtime/layers/<layer>/entities/<path>/errors; values of "(none)" or "" are treated as no errors. Read-only: it does not modify anything. Empty while no root scene is loaded.

Parameters

  • entityFilter (string | EntityFilterTable) (optional)

Returns { EntityErrors }

"lamp"

tools/diagnostics/summary

diagnostics.summary() -> SceneSummary

Get a summary of the current scene. Enumerates every entity of the active root scene layer, children included, from /runtime/layers/<layer>/entities and each entity's children directory beneath it, counts each entity's components by listing its components directory, counts entities with non-empty error files, and reports Camera component instances separately. Also surfaces rendering culling mode + stats via __rendering.getCullingMode / __rendering.getCullStats when available — GPU culling has no readback path so its stats slot becomes a notice string instead. Read-only — does not modify anything.

Returns SceneSummary

tools/diagnostics/validate

diagnostics.validate() -> ValidationReport

Validate scene integrity: check for broken materials, missing model assets, and entity errors. Enumerates every entity of the active root scene layer, children included, from /runtime/layers/<layer>/entities and each entity's children directory beneath it, reads each entity's errors file, and parses messages of the form material '...' does not exist / Material:...not found ...'...' / mesh '...' not found / Model:...not found ...'...' to surface broken references. Read-only — does not modify anything.

Returns ValidationReport

tools/editor/layout

editor.layout(opts?: { name: string? }) -> LayoutResult

Switch the editor's dock arrangement to a named layout, or list the layouts that can be applied. Every layout is an .editorLayout asset: the standard ones ship in the built-in library, agentView (the scene and DotBot on the right, the one an editor comes up in when nothing else names a layout), default (every panel in its home region, the authoring arrangement, titled Full UI), watch (the scene above the newest frame an agent captured on this engine), recording (the scene beside the first agent's terminal, nothing else, for a recorded agent run), agents (the scene above the Claude Code, Codex CLI and Cursor CLI terminals) and minimal (the scene alone); one saved on this world through Window > Save layout applies by its name too and stands in for a standard one of the same name. With no name, nothing changes and the list is the answer. Needs the editor shell, so it answers in the windowed editor; a headless engine mounts no shell and reports that.

Parameters

  • opts { name: string? } (optional)

Returns LayoutResult

{ name = "recording" }

tools/editor/observe

editor.observe() -> { [string]: any }

What each editor action committed, and why it committed less than it was asked for. lastDrag is the most recent gizmo drag: the handle it held, the pivot it reconstructed against, what the pointer asked for and what the drag applied after snapping, and — per entity — the transform before, the transform handed to the engine, and the transform the engine HOLDS, read back from the engine. lastGrab is a press that landed on a handle and began no drag, with the reason. lastDelete / lastDuplicate carry the ids removed and created and the ones that refused; lastCommand tells a missing command from a disabled one from a broken predicate from a body that raised; lastSelect carries the ids before and after with the difference taken both ways. Every record names an outcome and, when that is not a clean commit, one reason from the closed set reasons enumerates. Answers in edit mode as well as play mode.

Returns { [string]: any }

tools/entityOps/addComponents

entityOps.addComponents(targets: Targets, components: Components, opts?: AddComponentsOpts) -> { AddResult }

Attach component(s) to one or many entities in a single call. Targets are entity NAMES or ids (arrays and scene.find records work too), or a scene.find QUERY resolved for you — { name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } } — so you operate on a selection without looking it up first. Components are given as a name, an array of names, or a { Name = dataTable } map carrying each component's setup data — names accept the leaf ("Physics"), the full identity ("@builtin::components.Physics"), a VFS path (resolved to its identity), or a case-insensitive partial resolved to the one component it can mean, all before anything mutates; a name matching nothing, or several components, is an error naming what it could not settle rather than a half-applied call. create = true spawns a fresh root entity for any string target that matches nothing, so one call can both create the entity and give it its setup. Returns one { id, name, added, created? } record per entity.

Parameters

  • targets Targets
  • components Components
  • opts AddComponentsOpts (optional)

Returns { AddResult }

"crate", { Physics = { kind = "dynamic" } }
{ "crate_1", "crate_2" }, { "Physics", "Audio" }
"spinner", { Rotator = { speed = 2 } }, { create = true }

tools/entityOps/component

entityOps.component(componentType: string, opts?: ComponentInspectOpts) -> ComponentInfo

Inspect a component TYPE so you set fields that exist instead of guessing. Resolves the component by name / identity / VFS path and reports its public FIELDS (each with name, type, and current value), its callable METHODS, its lifecycle HOOKS (awake / update / onDestroy / …), asset-ref fields with their AssetRef type, and a one-line description. Pass on (an entity name or id that carries the component) and every field's value is that entity's LIVE value, read off its component proxy — a vector / struct / list as its plain table, a reference by the identity of what it points at (an asset as a { __ref, identity, name, type } envelope, an entityRef as the entity id). Omit on and each value is the DECLARED DEFAULT as written in the component's source. Use this before entityOps.addComponents / entityOps.spawn to learn the exact field names and value shapes.

Parameters

  • componentType string
  • opts ComponentInspectOpts (optional)

Returns ComponentInfo

"Camera"
"Model", { on = "my_prop" }
"@builtin::components.Light"

tools/entityOps/duplicate

entityOps.duplicate(targets: Targets, opts?: DuplicateOpts) -> { shared.TransformState }

Copy entities that already exist — a duplicate of a prop, or a numbered run of them spread along an axis. Targets resolve by NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "crate" } }, { component = { "Light" } }, { under = { "room" } }), so a whole selection is copied without looking its ids up first. count makes that many copies of EACH source (default 1). offset is a {x,y,z} world-space step that ACCUMULATES: copy 1 sits one offset from the source, copy 2 sits two, and so on — every copy is placed relative to the SOURCE, not to the copy before it. name names each copy and a %d in it becomes the copy's 1-based index ("crate_%d" → crate_1, crate_2); without it copies are named after their source. Copies carry the source's render layer. An unrecognised option key is an error naming the accepted set, and a count below 1 is an error, so a mistyped call never reads back as a successful no-op. Returns the full { id, name, position, localPosition, rotation, localRotation, eulerAngles, scale, parent } state of every copy, in creation order.

Parameters

  • targets Targets
  • opts DuplicateOpts (optional)

Returns { shared.TransformState }

"crate"
"crate", { count = 5, offset = { 2, 0, 0 } }
"pillar", { count = 4, offset = { 0, 3, 0 }, name = "pillar_%d" }
{ name = { "tree" } }, { offset = { 10, 0, 0 } }

tools/entityOps/fromAsset

entityOps.fromAsset(source: string, opts?: FromAssetOpts) -> FromAssetResult

Instantiate an existing asset into the scene. Resolution-first: pass a name / path / identity STRING and the tool resolves it (limit the search with type to avoid name clashes), dispatches by the resolved category, and REPORTS the canonical asset it hit so you learn what it resolved to. A bundle or avatar instantiates via its own :instantiate() — link (default true) keeps the live Asset-component link so the source drives the hierarchy, link = false bakes the hierarchy into the scene. A mesh becomes a renderable (Model + mesh Collider). A material becomes a sphere carrying it. A texture becomes a plane sampling it. Anything else goes through the uniform scene-instantiation contract — a gaussianSplat capture becomes an entity drawing that cloud, and every other type whose assetType defines the hook works the same way, else a legible error. parent (an entity ref, id, or name) spawns the instance under that entity — it lands on the parent, and position then places it in the parent's space; an unresolvable parent fails the call before anything spawns. position / rotation / scale place it; synced replicates gameplay edits to peers.

Parameters

  • source string
  • opts FromAssetOpts (optional)

Returns FromAssetResult

"@builtin::avatars.humanoid"
"oak_chest", { type = "bundle", position = {2, 0, 0} }
"truck_model", { type = "bundle", parent = "truck_rig" }
"gold", { type = "material" }
"/zero/source/generated/meshes/a_statue.bundle", { link = false }

tools/entityOps/modify

entityOps.modify(targets: Targets, ops: ModifyOps) -> { ModifyResult }

Change entity state — one tool for delete / enable / disable / internal / temporary / reparent / rename, over one or many entities. Targets are entity NAMES or ids (arrays and scene.find records work too), or a scene.find QUERY resolved for you — { name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } } — so you operate on a selection without looking it up first. Boolean ops take true, false, or "toggle". delete removes the entity and its whole subtree and wins over every other op. enabled drives the entity's active state, internal marks it engine plumbing — taken out of the default entity listings and the inspector default view, while it keeps rendering, temporary marks it as not-persisted. parent (a name or id) reparents; unparent = true makes it a scene root; keepWorldTransform = false keeps the local transform instead of the world pose when reparenting. rename sets a new name (single target only). components sets field values on components the entity ALREADY carries — { Camera = { fov = 60 }, Light = { intensity = 2 } }, the same shape spawn takes, so creating with a value and changing it later are written alike; component names resolve the same forgiving way addComponents resolves them, and an entity that does not carry one named is an error rather than a silent skip. An unrecognised key is an error naming the accepted set, so a typo fails the call rather than reporting an empty applied. Returns one { id, name, applied } record per entity.

Parameters

  • targets Targets
  • ops ModifyOps

Returns { ModifyResult }

"debug_probe", { internal = true }
{ "probe_a", "probe_b" }, { delete = true }
"old_character", { enabled = "toggle" }
"hat", { parent = "player_head" }
"lamp", { components = { Light = { intensity = 4 } } }
{ component = { "Light" } }, { components = { Light = { intensity = 0 } } }

tools/entityOps/removeComponents

entityOps.removeComponents(targets: Targets, components: Components) -> { RemoveResult }

Detach component(s) from one or many entities in a single call — the counterpart to entityOps.addComponents. Targets resolve by NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ component = { "Light" } }, { name = { "lamp" } }, { under = { "room" } }), so you can strip a component from everything carrying it without naming a single entity. Components are given as a name or an array of names, and each name matches loosely: the leaf ("Light"), the full identity ("@builtin::components.Light"), a VFS path, or a case-insensitive partial ("light") resolved to the one component it can mean — the response reports the CANONICAL name it landed on, so a half-remembered name both works and teaches you the real one. A name matching several components is an error listing the candidates rather than a guess, and a name matching none is an error too, so nothing is silently skipped. Every entity comes back as { id, name, removed, absent }: removed lists what it actually carried and lost, absent lists what it never had — so a call that changed nothing is visible instead of reading like a success.

Parameters

  • targets Targets
  • components Components

Returns { RemoveResult }

"lamp", "Light"
{ "crate_1", "crate_2" }, { "Physics", "Audio" }
{ component = { "Light" } }, "Light"
"lamp", "light"

tools/entityOps/roots

entityOps.roots(targets: Targets) -> { RootHit }

Find the root ancestor of one or many entities — walks each up its parent chain to the top-level entity. Targets are entity names or ids (a single one, an array, or scene.find records), or a scene.find QUERY resolved for you — { name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } }. Returns one record per target with its resolved root id + name and the hop distance; a target that is already a root reports itself with depth 0.

Parameters

  • targets Targets

Returns { RootHit }

"player_hand"
{ "wheel_fl", "wheel_fr" }

tools/entityOps/spawn

entityOps.spawn(components: SpawnComponents | SpawnBatch, opts?: SpawnOpts) -> SpawnResult | { SpawnResult }

Spawn entities from a template — you name the components they carry and their field values, plus a transform. Spawn MANY in ONE call by passing a { defaults, items } list: defaults holds what every entity shares and each items entry overrides only what differs, so a six-walled room is one call rather than six. Per-item keys win over the matching defaults key, and components merges per COMPONENT — an item naming Model replaces only that entry and keeps the shared Collider. Every item is validated before any entity is created, so a typo fails the whole call instead of leaving a half-built scene. Or pass a single { [ComponentName] = { field = value } } map plus opts to spawn ONE entity. Each component is an AUTHORED component (leaf name like "Model", a full identity, or a VFS path) added with its data so it serializes — native ECS components are intentionally not exposed, so you never author something that renders at runtime but vanishes on save. position places it; rotation takes a {x,y,z,w} (or {x=,y=,z=,w=}) quaternion or {pitch,yaw,roll} (or {pitch=,yaw=,roll=}) euler degrees; scale a number or {x,y,z}; name defaults to the first component's name. A spawn is recorded into the scene as it is made, so it is there again the next time the scene loads; temporary leaves an entity out of that record — nothing is written for it as it spawns and no save carries it — which is the spelling for content a build reproduces for itself. internal, synced (replicate gameplay edits to peers), and attributes carry the rest of the lifecycle. An unrecognised key is an error naming the accepted set — in opts, at the top level, in defaults, and in an item alike. Pass an empty components map to spawn a bare entity. Returns the spawned id, name, and component leaf names — one record for the single form, an array in item order for the list form.

Parameters

  • components SpawnComponents | SpawnBatch
  • opts SpawnOpts (optional)

Returns SpawnResult | { SpawnResult }

{ Model = { model = "cube" }, BoxCollider = {} }, { position = {0, 1, 0} }
{ Light = { kind = "point", intensity = 5 } }, { name = "lamp", position = {0, 3, 0} }
{ Text3D = { content = "Hello", fontSize = 48 } }, { position = {0, 2, 0}, rotation = {0, 45, 0} }
{ defaults = { components = { Model = { model = "cube" }, BoxCollider = {} }, scale = { 4, 3, 0.2 } }, items = { { name = "wall_n", position = { 0, 1.5, -2 } }, { name = "wall_s", position = { 0, 1.5, 2 } }, { name = "wall_e", position = { 2, 1.5, 0 }, rotation = { 0, 90, 0 } }, { name = "wall_w", position = { -2, 1.5, 0 }, rotation = { 0, 90, 0 } } } }
{ defaults = { components = { Light = { kind = "point", intensity = 4 } } }, items = { { name = "lamp_a", position = { -3, 3, 0 } }, { name = "lamp_b", position = { 3, 3, 0 }, components = { Light = { kind = "point", intensity = 9 } } } } }

tools/entityOps/swap

entityOps.swap(target: string | EntityRef, replacement: string | EntityRef, opts?: SwapOpts) -> SwapPlan

Replace one entity with another entity that already exists in the scene. The replacement is MOVED into the target's slot — same parent, local position, and rotation; the target's scale unless keepScale = false; the target's name unless keepName = false — and the target (with its whole subtree) is removed. Both arguments are entity names, ids, or EntityRef proxies. dryRun = true reports exactly what would move and what would be removed, without mutating.

Parameters

  • target string | EntityRef
  • replacement string | EntityRef
  • opts SwapOpts (optional)

Returns SwapPlan

"DefaultBody", "my_knight"
"placeholder_car", "sports_car", { keepName = false }
"DefaultBody", "my_knight", { dryRun = true }

tools/entityOps/transform

entityOps.transform(targets: Targets | { TransformTarget }, ops?: TransformOps) -> { shared.TransformState }

Move, rotate and scale entities that already exist — set an absolute position / rotation / scale, apply a relative offset / rotation / scale multiplier, or snap to a grid, over one or many entities. Targets resolve by NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "wall" } }, { component = { "Light" } }, { under = { "building" } }) so you can move things without looking their ids up first. Absolute ops: position and rotation are world-space, localPosition and localRotation are parent-relative, scale is a number or {x,y,z}. Relative ops: offset adds a position delta, rotate applies a rotation on top of the current one, scaleBy multiplies the current scale; space ("world" default, or "local") governs those relative ops. snap quantizes the resulting position to grid steps. Rotations take a {x,y,z,w} quaternion or {pitch,yaw,roll} euler degrees, the same forms entityOps.spawn accepts. Absolute and relative forms of one channel are exclusive and passing both is an error, and an unrecognised option key is an error naming the accepted set, so a mistyped key never reads back as a successful no-op. Pass a LIST of { target = ..., <ops> } records to give each entity its own values in a single call; a bad entry there fails the whole call before any entity moves. Called with NO ops it is a pure read. Always returns the resulting { id, name, position, localPosition, rotation, localRotation, eulerAngles, scale, parent } per entity, so the call is both the write and the read.

Parameters

  • targets Targets | { TransformTarget }
  • ops TransformOps (optional)

Returns { shared.TransformState }

"lamp", { position = { 0, 3, 0 } }
{ "wall_n", "wall_s" }, { offset = { 0, 2, 0 } }
{ name = { "wall" } }, { offset = { 0, 2, 0 } }
"crate", { rotation = { 0, 45, 0 }, scale = 2 }
"prop", { offset = { 1, 0, 0 }, space = "local", snap = 0.5 }
"lamp"

tools/files/edit

files.edit(path: string, oldText: string, newText: string, durable?: boolean, replaceAll?: boolean) -> { path: string, replaced: number }

Replace exact text inside a file without rewriting the whole file. oldText is matched LITERALLY, not as a pattern, and by default must appear exactly once — an edit that would hit several places is refused rather than applied to the wrong one, which is the entire safety property of editing by anchor. Pass replaceAll when every occurrence is meant.

Parameters

  • path string
  • oldText string
  • newText string
  • durable boolean (optional)
  • replaceAll boolean (optional)

Returns { path: string, replaced: number }

tools/files/read

files.read(path: string) -> string

Read a file from the world's virtual filesystem and return its contents as a string. Binary-safe — a PNG's null bytes survive. Relative paths resolve under /source/, so "scenes/main.scene" and "/source/scenes/main.scene" name the same file. While play is running this reads through the play shadow, so a file written during this play session reads back as written rather than as it is on disk.

Parameters

  • path string

Returns string

tools/files/write

files.write(path: string, content: string, durable?: boolean, overwrite?: boolean) -> string

Write a file into the world's virtual filesystem. Relative paths resolve under /source/. Overwrites by default.

Parameters

  • path string
  • content string
  • durable boolean (optional)
  • overwrite boolean (optional)

Returns string

tools/gui/bounds

gui.bounds(id?: ElementId) -> (ElementRect | { [string]: ElementRect })?

Read a UI element's on-screen layout rect by id - { x, y, w, h } plus content bounds/centres, in logical points on the window, wherever the element's screen draws (a screen in the Scene view's picture included). Omit the id for a map of every recorded element's rect. This is the space gui.clickAt and anchors use; times ui.pixelRatio() it is the window pixel inputSim's pointer verbs (click, mouseMove) take with target "ui".

Parameters

  • id ElementId (optional)

Returns (ElementRect | { [string]: ElementRect })?

"glass/vertical@8"

tools/gui/captureElement

gui.captureElement(screen?: (string | CaptureElementArgs), element?: (string | { string }), opts?: CaptureElementOpts) -> CaptureElementResult

Render one UI element — or several composited by their on-screen positions — to a texture and save it as a JPG under /source/tmp, so you can READ it to see exactly how those elements render. Each element (by its elementTree id) is rendered into a right-sized render texture the same way a camera renders into a render texture; the result is read back, encoded JPG, written, and the texture destroyed (nothing stays GPU-resident). Pass one id for a single element, or a list of ids to capture several LAYERS together — the image covers their union bounds and each element is drawn at its real relative offset, so a panel and an icon sitting on different screens composite into one picture. Ids may span different screens; each element's screen is taken from its id. Find ids with gui.elementTree. The screen must have rendered at least once (so the element has a measured size).

Parameters

  • screen (string | CaptureElementArgs) (optional)
  • element (string | { string }) (optional)
  • opts CaptureElementOpts (optional)

Returns CaptureElementResult

"glass", "glass/vertical@10"

tools/gui/click

gui.click(callbackId: string, value?: any) -> ClickResult

Simulate a widget click by callback ID. Forwards to ui.click(callbackId, value), which dispatches the registered onCallback handler associated with callbackId. The callback ID is the one the widget emits (its onClick / onChange / on* prop); the element id of a widget whose interactions emit other ids is refused naming those ids. gui.clickElement presses a widget by its element id instead.

Parameters

  • callbackId string
  • value any (optional)

Returns ClickResult

"reset_btn"
"volume_slider", 0.5

tools/gui/clickAt

gui.clickAt(x: (number | ClickAtArgs), y?: number, opts?: { button: (number | string)? }) -> ClickAtResult

Click at a logical screen position - a REAL pointer down+up. Coordinates are logical UI points (the space gui.size / gui.bounds / anchors use), not capture pixels.

Parameters

  • x (number | ClickAtArgs)
  • y number (optional)
  • opts { button: (number | string)? } (optional)

Returns ClickAtResult

640, 360

tools/gui/clickElement

gui.clickElement(id: (string | ClickElementArgs), opts?: { button: (number | string)?, duration: number? }) -> ClickElementResult

Click a UI element by its elementTree / layout id: a REAL pointer down+up at the centre of the part of it the last frame drew, so a row a scroll area cut to a band of itself is pressed inside that band. Works on any widget that frame drew (no author-assigned callback needed) - the complement to gui.click, which needs a callback id. Find ids with gui.elementTree.

Parameters

  • id (string | ClickElementArgs)
  • opts { button: (number | string)?, duration: number? } (optional)

Returns ClickElementResult

"glass/vertical@10"

tools/gui/dragElement

gui.dragElement(from: (string | DragElementArgs), to?: string, opts?: { button: (number | string)? }) -> DragElementResult

Drag between two UI elements by their elementTree ids: press at the centre of the part of the first the last frame drew, move to the second, release.

Parameters

  • from (string | DragElementArgs)
  • to string (optional)
  • opts { button: (number | string)? } (optional)

Returns DragElementResult

"slider_nub", "slider_end"

tools/gui/elementTree

gui.elementTree(screen: string | { screen: string })

List a screen's rendered element hierarchy with each element's on-screen layout rect. Each node is { id, type, bounds, children } where bounds is { x, y, w, h } in logical points, present once the element has been measured. id is the id set on the node when the author gave it one, otherwise <screen>/<type>@<path>. Pass an id (or several) to gui.captureElement to screenshot that panel or widget.

Parameters

  • screen string | { screen: string }
"glass"

tools/gui/focus

gui.focus(id?: ElementId) -> FocusResult

Focus a widget by id so it takes keyboard input; call with no id (or "") to blur the current focus, in the editor's UI and the game's alike. Forwards to ui.focus / ui.blur; ui.focusedWidget() then reports the widget and the space it draws in. An id more than one screen records is refused, naming the screens.

Parameters

  • id ElementId (optional)

Returns FocusResult

"search_input"

tools/gui/fonts

gui.fonts(system?: (boolean | { system: boolean })) -> { any }

List every font family a style.fontFamily can select, with the aliases that also select it, the concrete face in each weight/style slot, the other faces sharing those slots (more: the installed chunks of an on-demand face, each drawing the codepoints the slot's face lacks), every on-demand face declared into the family with where it stands (onDemand: { name, state, codepoints, requestedBy?, error? }, state one of declared / requested / installed / failed), and whether it came from the host OS. Read from the registry the UI text renderer resolves a family through, so a family listed here is one a label renders in. A fontFamily naming something absent from this list paints in the default proportional face and is reported as unknown-font-family through gui.validate. Forwards to ui.listFonts().

Parameters

  • system (boolean | { system: boolean }) (optional)

Returns { any }

true

tools/gui/getTree

gui.getTree(name: string)

Get the widget tree of a screen (for reading state). Forwards to ui.getScreenTree(name). The returned tree mirrors the registered widget list and is suitable for inspection / diff-based test assertions.

Parameters

  • name string
"hud"

tools/gui/hide

gui.hide(name: string) -> shared.ScreenVisibility

Hide a screen and report the visibility it now carries. Forwards to ui.hideScreen(name) — the registered screen remains in the registry and can be re-shown via gui.show. Raises when no screen answers to name, naming the ones that do.

Parameters

  • name string

Returns shared.ScreenVisibility

"hud"

tools/gui/hoverElement

gui.hoverElement(id: ElementId) -> HoverElementResult

Move the pointer by an element's elementTree id, to the centre of the part of it the last frame drew, so the element's hover state paints. The cursor stays put - follow with gui.captureElement to see the hovered look.

Parameters

  • id ElementId

Returns HoverElementResult

"glass/vertical@9"

tools/gui/panel

gui.panel(name: string, widgets: { any }, opts?: { layer: number?, visible: boolean? }) -> shared.ScreenVisibility

Create a game UI screen from a widget list and report the visibility it now carries. The screen is the game's: it draws in the game view ("camera" space), the way a HUD or a menu does. An editor panel — a dock tab in the window around the game view — is an editorPanel asset instead (guides { path = "types/editorPanel" }). Registers the screen via ui.registerScreen at opts.layer, then leaves it shown — or hidden when opts.visible is false, so a screen can be built ahead of the moment it appears. Each widget is { type, text?, id?, value?, color?, children?, onCallback?, ... }.

Parameters

  • name string
  • widgets { any }
  • opts { layer: number?, visible: boolean? } (optional)

Returns shared.ScreenVisibility

"hud", { { type = "label", text = "Score: 0", id = "score" }, { type = "button", text = "Reset", onCallback = "reset_btn" } }

tools/gui/press

gui.press(id: string, opts?: { space: string? }) -> shared.UiActionResult

Press a UI element by its id — a button, a menu entry, a dock tab's close — and report what it reached. The element's own onClick is delivered, the way a person's click on it delivers it, with no pointer moved and nobody's keyboard focus touched: a person at the window keeps typing where they were while an agent drives a panel. Works on the editor's UI (edui panels, "window" space) and the game's ("camera" space) alike; space picks one when the same id is in both. Presses only what the frame drew, refusing a hidden element with the same verdict ui.diagnose gives. gui.clickElement is the pointer-level route, for hit areas and hover.

Parameters

  • id string
  • opts { space: string? } (optional)

Returns shared.UiActionResult

"save_btn"
"close", { space = "window" }

tools/gui/screens

gui.screens()

List every registered screen with { name, visible, layer, hasRoot, space } - the top-level map of what UI exists right now. space is where the screen draws: "window" for the editor's UI around the game view (edui panels, the top bar), "camera" for the game's own UI in the game view (a HUD, a menu). Forwards to ui.listScreens.

tools/gui/scroll

gui.scroll(dx: number, dy: number)

Simulate scroll input. Forwards to ui.scroll(dx, dy).

Parameters

  • dx number
  • dy number
0, -10

tools/gui/scrollElement

gui.scrollElement(id: (string | ScrollElementArgs), offsetY?: number) -> ScrollElementResult

Scroll a scrollArea to a vertical offset (pixels from the top) by its elementTree id — the reliable, element-targeted scroll. Prefer this over scroll (a mouse-wheel simulation) when you know which area to move.

Parameters

  • id (string | ScrollElementArgs)
  • offsetY number (optional)

Returns ScrollElementResult

"app/scrollArea@1.0.1", 240

tools/gui/set

gui.set(id: string, value: any, opts?: { space: string? }) -> shared.UiActionResult

Set a UI control by its id — a checkbox or toggle to true/false, a slider to a number, a select to one of its options — and report what it reached. The control's own onChange receives the value, the way it does when a person changes the control, with no pointer moved and nobody's keyboard focus touched. Works on the editor's UI ("window" space) and the game's ("camera" space); space picks one when the same id is in both. Sets only what the frame drew.

Parameters

  • id string
  • value any (optional)
  • opts { space: string? } (optional)

Returns shared.UiActionResult

"shadows_toggle", true
"volume_slider", 0.5

tools/gui/setTheme

gui.setTheme(name: string)

Set the active UI theme. Forwards to ui.setTheme(name) — subsequent screen draws use the new theme.

Parameters

  • name string
"dark"

tools/gui/show

gui.show(name: string) -> shared.ScreenVisibility

Show a screen and report the visibility it now carries. Forwards to ui.showScreen(name). The screen must already be registered (via gui.panel or ui.registerScreen); a name no screen answers to raises, naming the ones that do.

Parameters

  • name string

Returns shared.ScreenVisibility

"hud"

tools/gui/size

gui.size()

Return the UI coordinate space { width, height } in logical points - the space gui.bounds, anchors, and gui.clickAt use (not a capture's pixel size). Forwards to ui.screenSize.

tools/gui/state

gui.state(id: ElementId) -> WidgetResponse?

Read a widget's live interaction snapshot by id: { clicked, hovered, focused, changed, value }. Assert what a UI is doing after you drive it. Forwards to ui.response.

Parameters

  • id ElementId

Returns WidgetResponse?

"volume_slider"

tools/gui/themes

gui.themes() -> { string }

List registered themes. Forwards to ui.listThemes().

Returns { string }

tools/gui/toggle

gui.toggle(name: string) -> shared.ScreenVisibility

Flip a screen's visibility and report the visibility it now carries. Reads the screen's current state from ui.listScreens(), then calls ui.hideScreen or ui.showScreen. Raises when no screen answers to name, naming the ones that do.

Parameters

  • name string

Returns shared.ScreenVisibility

"hud"

tools/gui/type

gui.type(id: string, text: string, opts?: { space: string?, submit: boolean? }) -> shared.UiActionResult

Type text into a UI field by its id and report what it reached. The field's own onChange receives the text — what a person typing it would leave in the field — and submit = true also delivers it the way Enter does. Nobody's keyboard focus moves: a person at the window keeps typing where they were while an agent fills a panel's field. A command-line field (one that delivers its text only on Enter, like a terminal prompt) takes submit = true. Works on the editor's UI ("window" space) and the game's ("camera" space); space picks one when the same id is in both. Types only into what the frame drew.

Parameters

  • id string
  • text string
  • opts { space: string?, submit: boolean? } (optional)

Returns shared.UiActionResult

"name_field", "Ada"
"edui_p_terminal:tm:input", "ls /zero/source", { submit = true }

tools/gui/update

gui.update(screenName: string, widgetId: string, props: { [string]: any })

Update a specific widget inside a screen by widget ID. Reads the current widget tree via ui.getScreenTree(screenName), walks it to find the widget with the matching id, copies every key from props onto the matched node, and writes the modified tree back via ui.updateScreen. If the screen does not exist, prints a warning and returns without erroring.

Parameters

  • screenName string
  • widgetId string
  • props { [string]: any }
'hud', 'score', { text = 'Score: 500' }

tools/gui/validate

gui.validate(screen?: (string | { screen: string })) -> any

Return the diagnostics from the last render of a screen (or all screens when no name is given): unknown widget types, bad props, decode/build errors. First stop when a screen renders wrong. Forwards to ui.lastValidation.

Parameters

  • screen (string | { screen: string }) (optional)

Returns any

"glass"

tools/gui/widgetProps

gui.widgetProps(typeName: string) -> table?

Get property definitions for a widget type. Forwards to ui.getWidgetProps(typeName) — the returned table describes every property a widget of this type accepts (name, type, description): the type's own props, then the ones every widget takes (onClick, dragPayload, onDrop, tooltip, ...), marked common = true.

Parameters

  • typeName string

Returns table?

"button"

tools/gui/widgetTypes

gui.widgetTypes() -> { string }

List available widget types. Forwards to ui.getWidgetTypes().

Returns { string }

tools/guides/list

guides.list(opts?: GuidesListOpts) -> GuidesListing

The guides this engine can reach, README first. The .guide assets — built-in, package-shipped and world-authored — and the asset types, which answer under types/<name>.

Parameters

  • opts GuidesListOpts (optional)

Returns GuidesListing

{ tier = 2 }
{ assetType = "assettype", tier = 2 }

tools/guides/read

guides.read(path: string) -> GuideRead

Read one guide by its path. topics/physics and types/material are paths; so are the shorter spellings that still name one guide, because a path resolves by exact spelling first, then ignoring case, then by what it ends with, then by its last segment — widening only while the answer stays unique.

Parameters

  • path string

Returns GuideRead

"readme"
"types/material"

tools/guides/scan

guides.scan(opts: GuidesScanOpts) -> GuidesScan

Scan the documentation for literal words and return the lines they appear on. Use it when you know the exact spelling of something — an API name, a field, a setting — and want every place it is written.

Parameters

  • opts GuidesScanOpts

Returns GuidesScan

{ query = "PlayerPrototype" }
{ query = "shadow bias", path = "topics" }
guides.search(opts: GuidesSearchOpts) -> GuidesSearchAnswer

Search the documentation: the guides, agent skills and asset types that describe how a system works. Ranked by MEANING, so a question asked in the caller's own words finds the guide that answers it without sharing its wording.

Parameters

  • opts GuidesSearchOpts

Returns GuidesSearchAnswer

{ q = "how do I make a light flicker" }
{ q = "outline", mode = "keyword" }
{ q = "scenes", assetType = "guide", limit = 5 }

tools/ik/bones

ik.bones(of: string | EntityRef, filter?: string) -> BonesResult

List a rigged body's bones with their world positions and parents. Resolves the body itself or the first descendant carrying a rigged skeleton, so it can be called on a character root without knowing where inside the spawned hierarchy the skeleton lives. Also returns the canonical role map when the rig has one, which is what IKLimb.role and IKRig resolve through; a rig with no retarget profile returns an empty map, and its chains have to be named bone by bone.

Parameters

  • of string | EntityRef
  • filter string (optional)

Returns BonesResult

"hero"
"hero", "hand"
player.avatar.id()

tools/ik/reach

ik.reach(of: string | EntityRef, chain: string, target?: (string | EntityRef)) -> ReachResult

Report a chain's reachable band and, when a target is given, whether that target falls inside it. A limb reaches as far as its bones are long and no further: beyond that the chain extends straight toward the target and stops, which is correct and indistinguishable from a broken solver. Under minReach (the fold limit) it cannot fold tightly enough. Everything is reported in world units, so the rig's model space — where a 0.61 m arm reports a reach near 61 — does not have to be reasoned about.

Parameters

  • of string | EntityRef
  • chain string
  • target (string | EntityRef) (optional)

Returns ReachResult

"hero", "rightarm"
"hero", "rightarm", "doorknob"
"crane", "boom_base..boom_tip", "hook"

tools/ik/state

ik.state(of: string | EntityRef) -> StateResult

Gather every IK component on an entity and report each one's state, so "is anything solving at all" is a single call. Each entry carries a status in words: a body spawned from a bundle takes a few frames to arrive, and a paused engine never advances those frames, so "not ready" on its own does not distinguish a wait that clears from one that never will. Where a component reports a distance to its target, that number is the residual after solving — small means the solve arrived, and it is not the distance to compare against a limb's reach (use ik.reach).

Parameters

  • of string | EntityRef

Returns StateResult

"hero"
player.avatar.id()

tools/importers/await

importers.await(path: string, timeoutSecs?: number) -> AwaitResult

Wait until the import job for path settles: a terminal state (imported, failed, skipped, unclaimed or cancelled), or owed, which holds until what holds the import lets go and so returns at once: reason play-lock while the running play session holds the source where it stands (the import runs when play ends or pauses), session-lost while the live session is lost (the import runs when it re-establishes). Waits until the timeout elapses otherwise, and returns the job record (still running on timeout). When no job exists for the path at all, returns the gate explanation instead, so the call always answers what happened.

Parameters

  • path string
  • timeoutSecs number (optional)

Returns AwaitResult

"/zero/source/models/gun.fbx"
"/zero/source/models/gun.fbx", 60

tools/importers/cancel

importers.cancel(path?: string) -> CancelResult

Cancel a queued source before it imports, or every queued source at once. An import owed until play ends cancels the same way. Pass a path to cancel just that one; omit it to cancel ALL queued and owed sources (an accidental 1k-file drop). Sources already importing are left to finish. A cancelled source settles as a cancelled job, visible in importers.status.

Parameters

  • path string (optional)

Returns CancelResult

"/zero/source/models/gun.fbx"

tools/importers/explain

importers.explain(path: string) -> ExplainResult

Explain what the importer system would do with path right now, without importing: whether the file exists and reads, which importers claim it, and which gate (if any) blocks an import. verdict is would-import or blocked; when blocked, reason names the gate — missing (no such file), unreadable, no-claimant (no importer claims the extension/content), already-in-own-bundle (the source already lives inside its produced asset), or unchanged-content (the bytes match the last import; output points at the existing asset). owed says an import would wait and run later: reason play-lock while the running play session holds the source where it stands, until play ends or pauses; session-lost while the live session is lost, until it re-establishes.

Parameters

  • path string

Returns ExplainResult

"/zero/source/models/gun.fbx"

tools/importers/imported

importers.imported() -> ImportedResult

List every imported asset in the world with its source and the importer that produced it. Each row: { asset, source = { guid, path }, importer (guid), importerName, iteration, at }, newest import first. The stored importer link is the guid; importerName resolves it for display.

Returns ImportedResult

-- every import, newest first

tools/importers/list

importers.list() -> { count: number, importers: { { name: string, identity: string } } }

List every registered importer with its name and identity. Importers are .importer/ assets, each a canImport(path, bytes) + import(ctx) pair; a written source file is offered to each claimant in registration order and the first to produce output wins.

Returns { count: number, importers: { { name: string, identity: string } } }

tools/importers/queue

importers.queue() -> QueueResult

The live import backlog: queued sources first (still waiting behind the bounded concurrency, in FIFO order) then the ones importing right now. A bulk drop of N files imports one at a time, so this is where you watch the queue drain. Owed imports follow: those the running play session holds back (reason play-lock) run when play ends or pauses, and those the lost live session holds back (reason session-lost) run when it re-establishes. Cancel queued or owed items with importers.cancel.

Returns QueueResult

tools/importers/run

importers.run(target: string | { string }, opts?: RunOpts) -> RunResult

Import/reimport one target, many targets, or a folder. A produced asset reimports in place (source resolved from its provenance); a loose source imports; a folder is scanned (recursive by default), its imported assets reimported and loose sources imported, filtered by opts.mode ("all" | "new" | "existing"). mode governs folder scanning; explicit path/array targets are always processed.

Parameters

  • target string | { string }
  • opts RunOpts (optional)

Returns RunResult

"/zero/source/guns"

tools/importers/status

importers.status(opts?: { state: ImportJobState?, path: string?, limit: number? }) -> StatusSummary

List recent import jobs, newest first, with the live queue depth. Every dispatch is recorded: queued (waiting behind the bounded concurrency), running (import in flight), owed (held back: reason play-lock while the running play session holds the source where it stands and the import would move it, until play ends; session-lost while the live session is lost and every write the import makes is refused, until it re-establishes, error carrying the refused write when the import had started), imported (done: output is the produced asset path), failed (the importer threw — error carries the message), unclaimed (every importer deferred), skipped (gated — reason is already-in-own-bundle, unchanged-content, no-claimant, unreadable, source-removed, or superseded), and cancelled (a queued or owed source cancelled before it ran). backlog is the exact queue depth ({ queued, running, owed }), active lists the jobs importing right now, and totals tallies every recorded job by state — poll those three to watch a bulk drop drain. See the full backlog list with importers.queue; drop queued items with importers.cancel. Filter the jobs window with opts.state (one state), opts.path (substring match on the source path), and opts.limit (default 25).

Parameters

  • opts { state: ImportJobState?, path: string?, limit: number? } (optional)

Returns StatusSummary

{ state = "failed" }
{ path = "weapons", limit = 5 }

tools/inputAuthor/catalog

inputAuthor.catalog() -> { any }

Every control the catalog knows, with what each is bound to on keyboard, gamepad and touch. Naming one of these in map or control gets all three filled in.

Returns { any }

tools/inputAuthor/check

inputAuthor.check(map: string) -> any

Read a .inputMap and report every problem across its controls — a missing device class, a malformed record, a button whose class is not an array. Reports all of them at once rather than stopping at the first, and activates nothing, so a scheme can be checked before a world runs it.

Parameters

  • map string

Returns any

"racer"

tools/inputAuthor/control

inputAuthor.control(mapPath: string, control: any) -> (string, Shared.Durability)

Add a control to an existing .inputMap, carrying keyboard, gamepad and touch. A catalog name arrives pre-filled; anything else needs all three classes from you. The map has to exist already: a mapPath naming no map raises, lists the maps beside it and names the map call that writes one.

Parameters

  • mapPath string
  • control any (optional)

Returns (string, Shared.Durability)

"/zero/source/inputMaps/racer.inputMap", "jump"
mapPath, { name = "horn", label = "Horn", kind = "button", kbm = '{ B.key("KeyH") }', gamepad = '{ B.padButton("north") }', touch = '{ B.touchButton({ zone = "right-lower" }) }' }

tools/inputAuthor/map

inputAuthor.map(name: string, controls: { any }, opts?: MapOpts) -> any

Write a <name>.inputMap/ and every control it declares, each carrying keyboard, gamepad and touch.

Parameters

  • name string
  • controls { any }
  • opts MapOpts (optional)

Returns any

"racer", { "move", "brake", { name = "boost", from = "sprint", label = "Nitro" } }
"shooter", { "move", "look", "attack", "aim", "reload", "jump" }
"driving", { "move", "brake" }, { group = "vehicle", suppresses = { "player" } }

tools/inputSim/action

inputSim.action(control: string, value?: any, duration?: number, held?: boolean)

Perform a gameplay action by the name of the control the game reads it from — jump, move, sprint, whatever bindings lists — with the value the game should read: true for a button, a number for a 1-D axis, { x, y } for a 2-D axis. A button with no value is tapped; a duration holds the value that long, then lets go; held = true keeps it driven until release. Returns once the game has read it.

Parameters

  • control string
  • value any (optional)
  • duration number (optional)
  • held boolean (optional)
"jump"
"move", { 0, 1 }, 2
"sprint", true, nil, true

tools/inputSim/bindings

inputSim.bindings() -> { any }

Every control that is live right now: its name, the label a player sees, its kind, and which activated map contributed it.

Returns { any }

tools/inputSim/click

inputSim.click(button?: (number | string), x?: number, y?: number, duration?: number, target?: string)

Click a mouse button — press, optional hold, release — optionally moving the cursor to (x, y) first. Takes its arguments in the toolbox's one order: which button, where, how long.

Parameters

  • button (number | string) (optional)
  • x number (optional)
  • y number (optional)
  • duration number (optional)
  • target string (optional)
0, 400, 300
"right"
0, 1015, 1051, nil, "ui"  -- a widget's gui.bounds centre times ui.pixelRatio()

tools/inputSim/device

inputSim.device(class?: string) -> string

Read the active device class, or force one. With no argument this reports what the session currently looks like: "kbm", "touch" or "gamepad".

Parameters

  • class string (optional)

Returns string

"touch"
"auto"

tools/inputSim/drag

inputSim.drag(dx: number, dy: number, steps?: number)

Drag across the on-screen look zone by (dx, dy) pixels, in steps increments so the per-frame deltas a look binding reads are real rather than one impossible jump.

Parameters

  • dx number
  • dy number
  • steps number (optional)
200, 0

tools/inputSim/fired

inputSim.fired(peek?: boolean) -> { any }

Every control that fired since this tool last ran, with how many engine frames it was active for, on which device class, and what received it. Reading CLEARS the record, so two calls around an action answer "did that input reach the game" without the previous test's results bleeding in.

Parameters

  • peek boolean (optional)

Returns { any }

true

tools/inputSim/held

inputSim.held() -> any

Everything the session is holding down right now: keys, mouse buttons, touch contacts, the on-screen stick, and connected pads with the buttons they hold. atRest is true when it holds nothing.

Returns any

tools/inputSim/key

inputSim.key(key: string, duration?: number, target?: string)

Tap a key — down, optionally held for duration seconds, then up. Queues the simulated events, yields a frame so they drain, and ticks so subscribers fire before the call returns.

Parameters

  • key string
  • duration number (optional)
  • target string (optional)
"Space"
"KeyW", 2

tools/inputSim/keyDown

inputSim.keyDown(key: string, target?: string)

Press a key and leave it HELD until keyUp releases it. A second keyDown on a held key produces no new press edge — pair every keyDown with a keyUp, or use key for a whole tap.

Parameters

  • key string
  • target string (optional)
"ShiftLeft"

tools/inputSim/keyUp

inputSim.keyUp(key: string, target?: string)

Release a key. Only the release re-arms the next press edge.

Parameters

  • key string
  • target string (optional)
"ShiftLeft"

tools/inputSim/layout

inputSim.layout() -> any

What the touch overlay is drawing right now: each visible button with its label and centre, the stick and look zones, and whatever overflowed behind the fan.

Returns any

tools/inputSim/listMacros

inputSim.listMacros() -> { AssetRef<inputMacro> }

Enumerate every registered .inputMacro asset in the project. Thin wrapper over asset.list("inputMacro"). Returns an array of AssetRef handles suitable for direct passing to sim.macro.

Returns { AssetRef<inputMacro> }

tools/inputSim/lockPointer

inputSim.lockPointer(locked: boolean) -> boolean

Lock the pointer (cursor captured and hidden) or release it. A mouse-look binding is usually gated on the lock, so a look test that moves the cursor without locking first reads as no movement at all.

Parameters

  • locked boolean

Returns boolean

true

tools/inputSim/macro

inputSim.macro(refOrEvents: MacroEventList | MacroEventsWrapper | AssetRef<inputMacro> | string)

Play a macro of timed input events. Accepts: - inline event list: { { t = 0, op = "keyDown", arg = "KeyW" }, ... } - AssetRef<inputMacro> — loaded via asset.resolve + vfs.read - identity / path string of an .inputMacro asset. Each event: { t, op, arg }. t is seconds since macro start; events play in order of t, and events sharing one t play in the order the list writes them. op is the name of an inputSim verb that drives input — the keyboard and mouse verbs, the pad* verbs, the touch verbs — 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. A touchDown event's id is the name its contact goes under, and the touchMove and touchUp events that carry the same id drive it. Author macros by hand or record them via inputSim.recordMacro and persist via inputSim.saveMacro.

Parameters

  • refOrEvents MacroEventList | MacroEventsWrapper | AssetRef<inputMacro> | string
{ { t = 0.0, op = "mouseDown", arg = 1 }, { t = 0.0, op = "keyDown", arg = "KeyW" }, { t = 0.5, op = "mouseMove", arg = { dx = 200, dy = 0 } }, { t = 1.5, op = "keyUp", arg = "KeyW" }, { t = 1.5, op = "mouseUp", arg = 1 } }
{ { t = 0.0, op = "pad" }, { t = 0.4, op = "padDown", arg = "south" }, { t = 0.6, op = "padUp", arg = "south" }, { t = 0.8, op = "padStick", arg = { stick = "left", x = 0, y = -1 } } }
{ { t = 0.0, op = "touchDown", arg = { x = 714, y = 984, id = 1 } }, { t = 0.3, op = "touchMove", arg = { id = 1, x = 714, y = 900 } }, { t = 0.6, op = "touchUp", arg = 1 } }
asset.ref("my_walkthrough", "inputMacro")

tools/inputSim/mouseDown

inputSim.mouseDown(button?: (number | string), target?: string)

Press a mouse button and leave it HELD until mouseUp.

Parameters

  • button (number | string) (optional)
  • target string (optional)
1
"right"

tools/inputSim/mouseMove

inputSim.mouseMove(x: number, y: number, target?: string)

Move the cursor to (x, y) in screen coordinates. The delta from the previous position is what a mouse-look binding reads, so two calls in a row produce a look movement.

Parameters

  • x number
  • y number
  • target string (optional)
640, 360

tools/inputSim/mouseMoveBy

inputSim.mouseMoveBy(dx: number, dy: number, steps?: number, target?: string)

Move the pointer by (dx, dy) pixels, the motion a mouse device reports. The same motion repeated keeps producing look movement, so a look axis can be held, or steered by a controller issuing a correction each tick. steps spreads the motion over that many frames, each carrying its share, the way drag spreads a touch sweep.

Parameters

  • dx number
  • dy number
  • steps number (optional)
  • target string (optional)
0, 140
0, -160, 8

tools/inputSim/mouseUp

inputSim.mouseUp(button?: (number | string), target?: string)

Release a mouse button.

Parameters

  • button (number | string) (optional)
  • target string (optional)
1
"right"

tools/inputSim/pad

inputSim.pad(name?: string) -> number

Connect a simulated pad and return the slot it took. name is the device name the pad reports, which is what decides the button legends a prompt draws — pass an Xbox, PlayStation or Nintendo name to check a scheme's prompts on that family.

Parameters

  • name string (optional)

Returns number

"Xbox Wireless Controller"
"Sony DualSense Wireless Controller"

tools/inputSim/padDown

inputSim.padDown(button: string, slot?: number)

Push a canonical pad button down and leave it held until padUp.

Parameters

  • button string
  • slot number (optional)
"right_trigger"

tools/inputSim/padOff

inputSim.padOff(slot?: number)

Disconnect the pad in slot. Anything it still held is released first, so a held action ends rather than sticking.

Parameters

  • slot number (optional)
0

tools/inputSim/padPress

inputSim.padPress(button: string, duration?: number, slot?: number)

Press a canonical pad button — down, optionally held, then up. Buttons are named by POSITION, not by the letter printed on them, so the same call works whatever pad is connected: 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. Case and word separators fold away, so "South" and "LeftShoulder" press the same buttons the canonical spellings name; a name outside the list is refused, naming the list.

Parameters

  • button string
  • duration number (optional)
  • slot number (optional)
"south"
"left_shoulder", 1.5

tools/inputSim/padStick

inputSim.padStick(stick: string, x: number, y: number, duration?: number, slot?: number)

Push a thumbstick to (x, y), each -1..1 with y screen-down positive — the same convention the on-screen stick reports, so a binding reads the same shape from either. Holds until pushed back to zero, or for duration seconds if you pass one.

Parameters

  • stick string
  • x number
  • y number
  • duration number (optional)
  • slot number (optional)
"left", 0, -1
"right", 1, 0, 2

tools/inputSim/padTrigger

inputSim.padTrigger(trigger: string, value: number, duration?: number, slot?: number)

Squeeze a trigger to value, 0..1. Past half throw the trigger's digital button latches too, so a scheme binding either the analog travel or the button sees this.

Parameters

  • trigger string
  • value number
  • duration number (optional)
  • slot number (optional)
"right", 1

tools/inputSim/padUp

inputSim.padUp(button: string, slot?: number)

Release a canonical pad button.

Parameters

  • button string
  • slot number (optional)
"right_trigger"

tools/inputSim/pinch

inputSim.pinch(amount: number, steps?: number)

Pinch two fingers together (negative) or spread them apart (positive) by amount pixels, over steps frames. What a zoom binding reads on a phone.

Parameters

  • amount number
  • steps number (optional)
150

tools/inputSim/recordMacro

inputSim.recordMacro(duration?: number, sampleInterval?: number, until_?: (() -> boolean)) -> ({ MacroEvent }, number)

Capture real input over a window. Emits a timed event whenever a key, mouse button, pad button, pad connection or touch contact changes — read every frame — and whenever mouse motion, the wheel, a thumbstick, a trigger or a sliding contact has moved, read every sampleInterval seconds. Returns the events array and the spawned task handle. Caller decides whether to play immediately, save to disk via inputSim.saveMacro, or both. Generic — works for any input-driven scenario (player walk, car drive, UI flow, build sequence) on any of the three device classes. Stops automatically after duration seconds, or earlier if until_() returns true. Takes its arguments in the toolbox's one order: how long first, the same way every sibling verb takes a duration.

Parameters

  • duration number (optional)
  • sampleInterval number (optional)
  • until_ (() -> boolean) (optional)

Returns ({ MacroEvent }, number)

3.0
3.0, 0.02

tools/inputSim/release

inputSim.release() -> any

Return the session to rest: let go of every control action drives, lift every touch contact, drop the on-screen stick, release every key and mouse button that is down, and disconnect every connected pad.

Returns any

tools/inputSim/saveMacro

inputSim.saveMacro(name: string, events: { MacroEventRecord }) -> AssetRef<inputMacro>

Persist a recorded or hand-authored event list to a .inputMacro asset on disk at /zero/source/<name>.inputMacro/events.json. Returns the AssetRef of the saved macro for immediate playback.

Parameters

  • name string
  • events { MacroEventRecord }

Returns AssetRef<inputMacro>

"walk_demo", { { t = 0.0, op = "keyDown", arg = "KeyW" }, { t = 1.0, op = "keyUp", arg = "KeyW" } }

tools/inputSim/scroll

inputSim.scroll(dy: number, target?: string)

Turn the mouse wheel by dy. Positive scrolls one way, negative the other; what that means is the binding's to decide.

Parameters

  • dy number
  • target string (optional)
-3

tools/inputSim/stick

inputSim.stick(x: number, y: number, duration?: number)

Push the on-screen movement stick to (x, y), each -1..1, with y screen-down positive — the same convention a pad stick reports, so a binding reads the same shape from either.

Parameters

  • x number
  • y number
  • duration number (optional)
0, -1        -- full forward, held
0, 0         -- let go
1, 0, 2      -- hold right for two seconds, then release

tools/inputSim/tapButton

inputSim.tapButton(label: string, duration?: number)

Tap the on-screen button labelled label — the text a player reads on it. The tap lands at the button's real centre, so it proves the control is reachable, not just that the action exists.

Parameters

  • label string
  • duration number (optional)
"Jump"

tools/inputSim/touch

inputSim.touch(x: number, y: number, duration?: number, target?: string)

Tap the screen at (x, y) — contact, optional hold, lift. The contact id is chosen for you; use touchDown / touchMove / touchUp when you need to drive several fingers at once.

Parameters

  • x number
  • y number
  • duration number (optional)
  • target string (optional)
200, 600
200, 600, 0.5

tools/inputSim/touchDown

inputSim.touchDown(x: number, y: number, pressure?: number, target?: string) -> number

Put a finger down at (x, y) and leave it there. Returns the contact id touchMove and touchUp take, so several fingers can be driven at once — a two-finger pinch is two of these.

Parameters

  • x number
  • y number
  • pressure number (optional)
  • target string (optional)

Returns number

200, 600

tools/inputSim/touchMove

inputSim.touchMove(id: number, x: number, y: number, pressure?: number, target?: string)

Move the contact id to (x, y). The per-frame delta is what a drag-look binding reads, so a look test is several of these in a row rather than one big jump.

Parameters

  • id number
  • x number
  • y number
  • pressure number (optional)
  • target string (optional)
id, 260, 580

tools/inputSim/touchUp

inputSim.touchUp(id: number, target?: string)

Lift the contact id.

Parameters

  • id number
  • target string (optional)
id

tools/lib/has

lib.has(path: string) -> boolean

Check if a library asset exists at a path. Thin wrapper over library.has(path).

Parameters

  • path string

Returns boolean

"@builtin/models/Sample/DamagedHelmet"

tools/lib/list

lib.list(assetType?: string)

List available library content. No args returns every registered library asset; pass an asset-type string to filter (e.g. "model", "shader", "material", "component"). Thin wrapper over library.list(assetType).

Parameters

  • assetType string (optional)
-- all library assets
"model"  -- just models

tools/lighting/addLight

lighting.addLight(name: string, position: Vec3, opts?: AddLightOpts) -> AddLightResult

Add a light to the scene: spawns an entity at position carrying a point Light (default) or SpotLight component. One consistent shape — use setLight to tweak it afterwards and removeLight to delete it. Lights cast shadows by default (a light whose objects cast none reads as broken); pass castsShadows = false for a cheap fill light. intensity means the same thing to both kinds: the two are rows of one light buffer shaded by color * intensity * falloff, so the same number at the same radius puts the same light on a surface either kind faces from the same place. A spot delivers it to the cone it opens on rather than all around itself, so it lights a smaller part of the room at that number — aim it and read the surface it lands on, and reach for radius and angle before reaching for a bigger intensity.

Parameters

  • name string
  • position Vec3
  • opts AddLightOpts (optional)

Returns AddLightResult

"torch1", { x = 5, y = 3, z = 0 }, { color = {1, 0.6, 0.2}, intensity = 1.5, radius = 8 }
"beam", { x = 0, y = 6, z = 0 }, { kind = "spot", intensity = 1.5, radius = 8, angle = 30 }

tools/lighting/get

lighting.get() -> GetLightingResult

Read the scene's lighting + sky state. Returns every light (entity name, id, kind, values), the sky entity and its component type + current values, and — when the sky is a material Skybox — the material's editable parameter names/kinds/values (what setSky params accepts). sky.source = "fallback" means the scene has NO explicit sky entity and renders the engine fallback (adds the missing-sky warning; setSky fixes it). Each light also reports mobility (how GI baking treats it), enabled (the component's own switch) and contributing — whether this light reaches the frame. Each kind answers it from what carries a light of that kind: a punctual light from the row the renderer holds for it, a directional light from whether it holds the sun, an ambient light from whether it holds the scene's ambient term. So it is false for a switched-off light of any kind, which holds neither a row nor a singleton, for a punctual light the renderer holds no row for, for a directional light that does not hold the sun, for an ambient light that does not hold the ambient term, and for a baked static light whose light comes from the bake rather than from the light itself. A light the renderer holds reports what it is doing with the light's shadow, beside the castsShadows flag the scene authored: shadowing is whether it owns a virtual shadow map — every casting point, spot and rect light the renderer holds does; renderer.shadowViews() reports what its pages did. The same row carries layerMask: the render layer mask of the entity carrying the light, which cameras whose include mask intersects it are lit by. A punctual, directional or ambient light answers contributing, shadowing and holdsAmbient from the light set the renderer resolved on its LAST frame, so one authored earlier in the same call reads enabled = true with contributing = false and no shadow fields; read it again a frame on and the renderer has an answer for it. A directional light reports holdsSun: the scene's sun is one field set with one holder, so holdsSun = true marks the light whose direction, colour, intensity and castsShadows the scene is lit and shadowed by. The values of a directional light that does not hold it are authored and unread until it takes the sun back. An ambient light reports holdsAmbient on the same rule: the scene's ambient term is one field set with one holder, so holdsAmbient = true marks the light whose colour and intensity the ambient term carries, and the values of the ambient lights beside it are authored and unread. A sky with syncSunToLight on drives the sun holder from its time of day, so that light's direction, color and intensity are what the sky put there rather than what the scene authored. When the two differ the record carries resolved — the values the geometry is lit by — and drivenBy, naming what is writing them. 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. environment is what every PBR surface reflects besides the lights, so it is the answer when a surface stays lit with every light at zero: skyFallback is whether surfaces reflect the captured sky wherever the probes leave coverage (with skyCaptured, whether the sky slot holds a capture), and probes lists each reflection probe with its radius, priority and baked. A probe reaches the surfaces in its radius once it is baked; an unbaked one adds nothing until reflectionProbe.bake or bakeAll gives it a cube.

Returns GetLightingResult

tools/lighting/removeLight

lighting.removeLight(name: string) -> RemoveLightResult

Remove a light entity by name (exact, then substring — ambiguity errors and lists the scene's lights). Despawns the entity.

Parameters

  • name string

Returns RemoveLightResult

"torch1"

tools/lighting/setAmbient

lighting.setAmbient(opts: AmbientOpts) -> SetAmbientResult

Set the scene's ambient light. Modifies the entity carrying the scene's ambient light, spawning one when the scene has none, and reports the entity it wrote. Read-merge-write: ONLY the fields you pass change — { intensity = 0.5 } keeps the authored color.

Parameters

  • opts AmbientOpts

Returns SetAmbientResult

{ color = {0.4, 0.4, 0.6}, intensity = 0.3 }
{ intensity = 0.5 }

tools/lighting/setLight

lighting.setLight(name: string, opts: SetLightOpts) -> SetLightResult

Update a light by entity name — exact match first, then substring (ambiguity errors and lists the scene's lights). Only the fields you pass change. Works on point, spot, area and distant lights and the sun/ambient entities.

Parameters

  • name string
  • opts SetLightOpts

Returns SetLightResult

"torch1", { intensity = 3 }
"sun", { direction = {-0.2, -1, -0.1} }

tools/lighting/setSky

lighting.setSky(sky?: string, opts?: SkyOpts) -> SetSkyResult

Set or reconfigure the scene's sky in one call. sky is a word: a procedural preset (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 kind="none"); any other word resolves against the project's materials FILTERED to sky-domain shaders (exact identity → leaf name → substring) and the sky renders that material via a Skybox. Omit sky to reconfigure the current sky with opts only. The tool finds the scene's sky entity (creating a "sky" entity when absent) and writes the component — changes persist, replicate, and survive reload.

Parameters

  • sky string (optional)
  • opts SkyOpts (optional)

Returns SetSkyResult

"sunset"
"aurora", { params = { exposure = 1.4 } }
nil, { timeOfDay = 3, autoCycle = true }
"none"

tools/lighting/setSun

lighting.setSun(opts: SunOpts) -> SetSunResult

Set the scene's directional sun light. Modifies the entity carrying the scene's directional light, spawning one when the scene has none, and reports the entity it wrote. Read-merge-write: ONLY the fields you pass change — { intensity = 2 } keeps the authored direction and color. A sky with syncSunToLight on drives the sun from its time of day, so setting the sun here takes it back: that switch goes off and the result reports releasedFromSky with the sky entity's name. Move the sun by timeOfDay (setSky) to keep the sky driving it.

Parameters

  • opts SunOpts

Returns SetSunResult

{ direction = {-0.5, -1, -0.3}, color = {1, 0.95, 0.8}, intensity = 1.2 }
{ intensity = 2 }

tools/lighting/setup

lighting.setup(opts: SetupOpts) -> SetupResult

Set up the whole scene lighting rig in one call. Only provided sections change. sun/ambient are partial updates on the entities carrying those lights; sky + skyOpts follow setSky semantics (preset word, sky-material word, procedural controls / material params); lights is a map of light name → addLight opts with a position — each is created if missing, updated if present. Naming sun takes the sun back from a sky driving it from its time of day: that sky's syncSunToLight goes off and releasedFromSky reports its entity name.

Parameters

  • opts SetupOpts

Returns SetupResult

{ sun = { intensity = 1.2 }, sky = "sunset", lights = { torch1 = { position = {x=5,y=3,z=0}, color = {1,0.6,0.2} } } }

tools/localContent/forget

localContent.forget(path: string) -> string

Stop holding a path under /source/local/ on this machine. The file goes and the record of it goes too, so the next boot does not restore it. A folder takes its contents with it. Content already promoted with localContent.persist is untouched — that copy belongs to the world.

Parameters

  • path string

Returns string

"gestures/wave.json"

tools/localContent/list

localContent.list() -> HeldContent

List everything this machine holds under /source/local/ — the content a session wrote that stays on the device instead of going to the world. Each entry reports its path and its size in bytes, in path order, with the total alongside. Pairs with localContent.persist (list -> persist).

Returns HeldContent

tools/localContent/persist

localContent.persist(path: string, destination?: string) -> { string }

Copy content this machine has been holding under /source/local/ into the world, at the same relative path under /source/ unless destination says otherwise. Takes a file or a whole folder. The local copy stays where it is — persisting is a promotion, not a move, so a failed publish never costs the only copy. Refused while play is running: a /source write during play lands on the play shadow and would not persist, which is the exact failure this tool exists to end. Pairs with localContent.list (list -> persist).

Parameters

  • path string
  • destination string (optional)

Returns { string }

"gestures/wave.json"

tools/logs/clear

logs.clear() -> ClearResult

Drop the buffered log entries, so a following search sees only what happened after the clear. Lifetime per-level counts are preserved, so "were there ever any errors" stays answerable. Rarely the right move: summary returns a cursor which, passed back to search as since, isolates what an action logged without discarding lines anybody else may want.

Returns ClearResult

tools/logs/errors

logs.errors(opts?: (string | ErrorsOpts)) -> ErrorsPage

Recent errors and warnings, newest first. A snapshot — the same lines however many times it is called. That is the difference from the problems tool in the debug toolbox, which drains: it advances a read-cursor and reports only what has not been seen, so that one answers "what is new" while this answers "what is there". Narrows by the same filters as search.

Parameters

  • opts (string | ErrorsOpts) (optional)

Returns ErrorsPage

{ level = "error", limit = 10 }
{ within = 120 }
{ origin = "local" }
"shader"
logs.search(opts?: (string | SearchQuery)) -> SearchResult

Search what the engine logged. Filter by severity, subsystem, substring or regex, the script or entity that logged it, and a time window — every filter is optional and they narrow together. This is the general view over the same ring the problems tool in the debug toolbox drains: reach for it when you need a specific message, a specific entity, a specific minute, or the lines that led up to a failure. The same ring reads as plain text at /zero/runtime/logs/engine, which is the shorter reach for a string you can already name; this is the view that filters it.

Parameters

  • opts (string | SearchQuery) (optional)

Returns SearchResult

{ level = "error", caller = "mine" }
{ level = "error", origin = "local" }
{ level = "error", within = 60 }
{ entity = "ent_9f3c", limit = 20 }
{ level = "error", context = 5 }
{ level = "warn", group = true }
{ contains = "shader", type = "RENDERER" }
"shader"

tools/logs/summary

logs.summary(opts?: (string | SummaryOpts)) -> SummaryReport

How many lines the engine logged, of what severity, from which subsystems and entities, and which messages repeat — the shape of the noise before you go looking inside it. Also the cheapest "did anything go wrong" check, and the source of the cursor for incremental polling: read cursor before an action, pass it back to search as since afterwards, and you see only what that action logged. Narrows by the same filters as search, so it can describe a slice as well as the whole.

Parameters

  • opts (string | SummaryOpts) (optional)

Returns SummaryReport

{ level = "error" }
{ within = 300 }
{ origin = "local" }

tools/logs/tail

logs.tail(opts?: (string | TailOpts)) -> TailPage

The most recent lines the engine logged, any level, oldest first — the last line is the newest. The view for when you do not yet know what you are looking for; once you do, search narrows it. Narrows by the same filters as search.

Parameters

  • opts (string | TailOpts) (optional)

Returns TailPage

{ limit = 40 }
{ type = "SCRIPT" }
{ origin = "remote" }
"shader"

tools/messages/chatPost

messages.chatPost(body: string) -> { id: string }

Post to the world chat, the room every person and agent on this world reads: tell or notify everyone at once. It reaches them on every engine of the world; the agents among them are told on their next tool call and the people see it in the editor. A direct message through send reaches only the participants it names. A post past 64 KB as stored refuses.

Parameters

  • body string

Returns { id: string }

"starting on the harbour; tell me before moving the docks"

tools/messages/chatRead

messages.chatRead(since?: number) -> { warning: string, messages: { ChatPost } }

Read the world chat, oldest first, each post marked untrusted. With no since, the posts by others you have not read yet; with since, the posts stamped after it, and 0 for every post of the last 7 days. The posts returned are read for you from then on. Posts the chat setting of the person you work for hides are left out. The answer opens with a note: anyone on this world can post here, so take it with a grain of salt, like any other world content.

Parameters

  • since number (optional)

Returns { warning: string, messages: { ChatPost } }

0

tools/messages/list

messages.list(opts?: ListOpts) -> { note: string, messages: { Header } }

List your direct messages, oldest first, as headers: the messages others sent you, or with sent the ones you sent, which read as read. Each message you sent carries its receipts: who has been told of it and who has read it. A message sent to a name you went by before a rename is yours. Filter by unread, by sender (from), by a recipient named in to, or by since (milliseconds since the Unix epoch). Marks nothing read; read opens one. The note is how to treat what they say. When a notice tells you a message arrived, list the unread ones.

Parameters

  • opts ListOpts (optional)

Returns { note: string, messages: { Header } }

{ unread = true }

tools/messages/read

messages.read(id: string) -> { note: string, message: Message }

Read one of your direct messages — one sent to you or by you — and mark it read for you. from is the name its sender wrote; kind says whether a person or an agent sent it. The note is how to treat what it says. To reply, send with its id as replyTo. A message you sent carries its receipts: who has been told of it and who has read it. Reading a message sent to you records that you read it on your receipt, which its sender sees. A message another participant received refuses, and so does one removed after 7 days.

Parameters

  • id string

Returns { note: string, message: Message }

"1790000000000-props-a1b2c3"

tools/messages/send

messages.send(to: string | { string }, body: string, replyTo?: string) -> { id: string }

Message another agent or a person on this world: tell them something, hand off work, or reply to a message. Address each by the name it goes by — an agent's name, a person's display name — or everyone with "*". It reaches them wherever they work on the world, on this engine or another; an agent is told on its next tool call and a person sees it in the editor. A name nobody here holds refuses and lists who is here. A name more than one participant holds — a person and an agent, or two people sharing a display name — refuses and names who holds it. A message past 64 KB as stored refuses. The agent toolbox's roster lists who is here. Your sent messages, listed, show who has been told of each one and who has read it.

Parameters

  • to string | { string }
  • body string
  • replyTo string (optional)

Returns { id: string }

{ "props" }, "terrain done, heightmap at /source/terrain/h.png"

tools/messages/unread

messages.unread(id: string) -> { id: string }

Mark a direct message sent to you unread again, so list with unread shows it until you next read it. Your marks are yours alone. The messages you sent are always read, so unread of one refuses; so does a message another participant received, and one removed after 7 days.

Parameters

  • id string

Returns { id: string }

"1790000000000-props-a1b2c3"

tools/messages/wait

messages.wait(opts?: WaitOpts) -> { note: string, messages: { Header }, chat: { ChatHeader }, timedOut: boolean }

Wait for a message. Use it when you have nothing to do until another participant tells you something: it blocks until a direct message to you is unread, or with chat a world chat post by someone else, and answers at once when one already is. from waits for one sender only. After timeout seconds (default 120, at most 600) it answers timedOut with nothing new; wait again to go on waiting. It returns headers and marks nothing read: read opens a message, chatRead the chat. The note is how to treat what they say. An agent whose harness starts a new turn when a message arrives, as an agent the editor launches in the engine does, can end its turn instead.

Parameters

  • opts WaitOpts (optional)

Returns { note: string, messages: { Header }, chat: { ChatHeader }, timedOut: boolean }

{ timeout = 300 }

tools/notices/drain

notices.drain() -> Drained?

Return your unread notices as the formatted delivery block — the same text that would otherwise be attached to your next tool result. What this returns is marked read for you, so read it; nothing is removed for other callers, and keys you muted with suppress are passed over. Returns nil when you have nothing unread. For a caller that pulls its own context; if you read notices off your tool results, you do not need this. Overflow content is written to /source/tmp/notices/ and named in the block. Pictures the notices carry come back in noticeImages, in the order the block names them, and reach the caller as pictures.

Returns Drained?

tools/notices/list

notices.list() -> NoticesList

Show the notices addressed to you and the keys you muted. pending holds the stored notices your audience admits, read or unread, less the keys you muted; a notice stays stored until every caller it names has read it, or it ages out. Each pending row carries its id (one key can hold several rows), key, template, severity, origin, count, source, params (each key with the distinct values it took), location, and when it first and last fired. A warning or error raised again with the params of one already delivered is a repeat: earlier counts the emissions delivered before it, and heldUntilMs names when a held repeat is delivered, carrying every repeat raised meanwhile. suppressed holds your own muted keys, each with the number of times it fired at you while muted. Marks nothing read. Use a key from here with suppress.

Returns NoticesList

tools/notices/suppress

notices.suppress(key: string) -> { suppressed: string }

Mute a notice key you have acknowledged, for you alone. It keeps counting (visible in your list) but never renders on your tool calls or your drain again this session; every other caller keeps receiving it. Available to the operating agent; a loaded world component cannot mute its own notices.

Parameters

  • key string

Returns { suppressed: string }

"user:atlas:texture rebuilt"

tools/notices/unsuppress

notices.unsuppress(key: string) -> { unsuppressed: string }

Un-mute a notice key you suppressed, so it renders on your tool calls again. Reverses your own suppress only. Available to the operating agent.

Parameters

  • key string

Returns { unsuppressed: string }

"user:atlas:texture rebuilt"

tools/phys/addBody

phys.addBody(id: string, bodyType?: ("dynamic" | "static" | "kinematic"), opts?: BodyOpts) -> string

Add Physics + Collider components to an entity in one call. bodyType selects the rigid-body kind: * "dynamic" — falls / responds to forces and collisions * "static" — immovable, only collides * "kinematic" — script-driven, ignores forces Flashes a purple outline (0.5s) so the change is visible.

Parameters

  • id string
  • bodyType ("dynamic" | "static" | "kinematic") (optional)
  • opts BodyOpts (optional)

Returns string

'my_cube'                                    -- dynamic box
'floor', 'static', { shape = 'box' }        -- static floor
'ball', 'dynamic', { shape = 'sphere', radius = 0.5 }

tools/phys/addConstraint

phys.addConstraint(entityId: string | EntityRef, constraintType: ConstraintKind, targetEntityId: string | EntityRef, opts?: ConstraintOpts) -> string | EntityRef

Add a transform constraint to an entity. Constraint types: * "position" — follow target's position * "rotation" — follow target's rotation * "scale" — follow target's scale * "lookat" — orient toward target * "parent" — full local-space parent relationship * "aim" — orient one axis toward target The constraint runs on the constraints system every frame, not the physics solver — so it works on entities without Physics bodies too.

Parameters

  • entityId string | EntityRef
  • constraintType ConstraintKind
  • targetEntityId string | EntityRef
  • opts ConstraintOpts (optional)

Returns string | EntityRef

"follower", "lookat", "player"
"camera", "position", "car", { offset = {0, 4, 8}, weight = 0.9 }

tools/phys/addJoint

phys.addJoint(entityIdA: string, entityIdB?: (string | AddJointOpts), opts?: AddJointOpts) -> string

Add a physics joint between two entities. Each end is a rigid body (an entity with Physics) or a soft body (an entity with SoftBody). A soft body end is anchored on the body as a whole, or with region (entity A's end) / remoteRegion (entity B's end) on a region of its particles: { box = { min, max } } or { nearest = point } in the entity's unit box (-0.5 to 0.5 on each axis), space = "world" for world coordinates. Two call shapes are accepted: * phys.addJoint(entityA, entityB, opts?) — explicit two-entity form. * phys.addJoint(entityA, opts) where opts.target is the second entity name or ID — matches the README's phys.addJoint("crate", { target = "wall", ... }) shape and the Joint component's target alias. Validates joint kind against the supported set (fixed, hinge, ball, prismatic, spring, rope), resolves names to entity IDs, normalises vec3 args (localAnchor, remoteAnchor, axis), and rejects stiffness / damping on kind = "fixed" and kind = "rope" since neither has a motorable axis (Closes #1062 for the silently-discarded case; Closes #527 for the silent no-op when entities were missing). kind = "rope" requires maxDistance > 0 — the maximum distance between the anchor points; slack is free, taut resists extension. breakForce (newtons of linear reaction) and breakTorque (the angular row of the same reaction) release the joint once the reaction it carries exceeds either; the break is reported through Physics.onJointBreak and the joint's own onBreak, and a joint given neither is unbreakable.

Parameters

  • entityIdA string
  • entityIdB (string | AddJointOpts) (optional)
  • opts AddJointOpts (optional)

Returns string

"door", "frame", { kind = "hinge", axis = {0, 1, 0} }
"crate", { target = "wall", kind = "spring", stiffness = 200, damping = 10 }
"ball", { target = "hook", kind = "rope", maxDistance = 8 }
"crate", { target = "wall", kind = "fixed", breakForce = 1200, breakTorque = 800 }
"bag", { target = "hook", kind = "ball", region = { nearest = {0, 0.5, 0} } }

tools/phys/attachSoftRegion

phys.attachSoftRegion(softEntity: string, rigidEntity: string, region?: SoftRegion) -> number

Hold a region of a soft body (an entity with SoftBody) to a rigid body (an entity with Physics), two-way: the region follows the rigid body, and the rigid body carries the region's weight and pull. The flag on a pole, the soft panel or body shell on a car's chassis, the jelly glued to a moving platform. The region is { box = { min, max } } (every particle between the corners) or { nearest = point } (the one particle nearest the point), in the soft entity's unit box (-0.5 to 0.5 on each axis) unless space = "world"; omit it to hold the whole body. The hold is an entry of the soft body's attachments, so it saves with the scene and reaches every peer; Physics.softBodyState(soft).attachments reads back the force each region pulls with.

Parameters

  • softEntity string
  • rigidEntity string
  • region SoftRegion (optional)

Returns number

"flag", "pole", { box = { min = {-0.5, -1, -0.5}, max = {-0.45, 1, 0.5} } }
"shell", "chassis", { box = { min = {-0.5, -0.5, -0.5}, max = {0.5, -0.4, 0.5} } }
"bag", "hook", { nearest = {0, 0.5, 0} }

tools/phys/detachSoftRegion

phys.detachSoftRegion(softEntity: string, rigidEntity?: string) -> number

Release the regions of a soft body held to rigidEntity, or every region it holds when rigidEntity is omitted. The released part of the body moves freely again.

Parameters

  • softEntity string
  • rigidEntity string (optional)

Returns number

"flag", "pole"
"shell"

tools/phys/ignoreCollision

phys.ignoreCollision(entityIdA: string, entityIdB: string, ignore?: boolean) -> string

Set whether two entities ignore collisions between each other. ignore defaults to true; pass false to re-enable collisions. Useful for "drop pickup through self" and similar paired-entity rules without touching collision groups.

Parameters

  • entityIdA string
  • entityIdB string
  • ignore boolean (optional)

Returns string

"player", "pickup"
"player", "pickup", false  -- re-enable

tools/phys/lockRotation

phys.lockRotation(entityId: string | EntityRef, lockX: boolean, lockY: boolean, lockZ: boolean) -> string | EntityRef

Lock rotation axes on an entity's rigid body. Each flag is independent — (true, false, true) lets the body spin only around Y. Useful for top-down characters, hinged doors, and constrained vehicles.

Parameters

  • entityId string | EntityRef
  • lockX boolean
  • lockY boolean
  • lockZ boolean

Returns string | EntityRef

"cube", true, false, true  -- only rotate around Y

tools/phys/lockTranslation

phys.lockTranslation(entityId: string | EntityRef, lockX: boolean, lockY: boolean, lockZ: boolean) -> string | EntityRef

Lock translation axes on an entity's rigid body. Each flag is independent — (true, true, false) lets the body only slide along Z (e.g. a rail / track).

Parameters

  • entityId string | EntityRef
  • lockX boolean
  • lockY boolean
  • lockZ boolean

Returns string | EntityRef

"rail", true, true, false  -- only move along Z

tools/phys/removeBody

phys.removeBody(id: string) -> string

Remove an entity's Physics component and whichever collider component it carries. Strips the collider before Physics so the engine never sees a collider without a body during the transition.

Parameters

  • id string

Returns string

"my_cube"

tools/phys/removeConstraint

phys.removeConstraint(entityId: string | EntityRef, index?: number) -> string | EntityRef

Remove transform constraints from an entity. Pass an 0-based index to remove a specific constraint; omit it to remove all of them.

Parameters

  • entityId string | EntityRef
  • index number (optional)

Returns string | EntityRef

"follower"        -- remove all
"follower", 0     -- remove just the first

tools/phys/removeJoint

phys.removeJoint(entityId: string | EntityRef) -> string | EntityRef

Remove all joints from an entity. Thin wrapper over Physics.removeJoint(entityId) — included in the toolbox so add/remove joint flows live behind the same phys.* surface.

Parameters

  • entityId string | EntityRef

Returns string | EntityRef

"crate"

tools/phys/setBodyType

phys.setBodyType(entityId: string | EntityRef, bodyType: "dynamic" | "kinematic" | "static") -> string | EntityRef

Change a rigid body's type at runtime. Preserves mass, colliders, and joints — only the body's response to forces and position writes changes. Useful for ragdoll activation (freeze pose as kinematic until hit, then flip to dynamic), freeze / unfreeze mechanics, and switching platforms between kinematic (scripted motion) and static. An unknown bodyType string raises a Luau error with the valid set.

Parameters

  • entityId string | EntityRef
  • bodyType "dynamic" | "kinematic" | "static"

Returns string | EntityRef

"player", "kinematic"  -- freeze
"player", "dynamic"    -- unfreeze, respond to forces

tools/phys/setCollisionGroups

phys.setCollisionGroups(entityId: string | EntityRef, membership: number, filter: number) -> string | EntityRef

Set collision group bitmasks on an entity's colliders. The two bitmasks encode "what am I" (membership) and "what do I collide with" (filter) separately — two entities collide iff each entity's filter contains a bit from the other entity's membership.

Parameters

  • entityId string | EntityRef
  • membership number
  • filter number

Returns string | EntityRef

"player", 0x1, 0x2  -- Player on group 1 collides only with terrain (group 2)

tools/phys/spawnDynamic

phys.spawnDynamic(name: string, url?: (string | AssetRef<mesh>), x?: number, y?: number, z?: number, opts?: SpawnOpts) -> string

Spawn an entity with a Model + dynamic Physics body in one call. The returned id is the unique ent_... handle from entity.spawn (not the display name), matching sc.spawnModel's contract (#579). Pops the entity in with a purple viz pulse.

Parameters

  • name string
  • url (string | AssetRef<mesh>) (optional)
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • opts SpawnOpts (optional)

Returns string

'my_box'
'ball', 'sphere', 0, 5, 0, { mass = 1 }

tools/phys/spawnStatic

phys.spawnStatic(name: string, url?: (string | AssetRef<mesh>), x?: number, y?: number, z?: number, opts?: SpawnOpts) -> string

Spawn an entity with a Model + static collider in one call. Static = immovable, only collides. The returned id is the unique ent_... handle from entity.spawn, matching sc.spawnModel's contract (#579).

Parameters

  • name string
  • url (string | AssetRef<mesh>) (optional)
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • opts SpawnOpts (optional)

Returns string

'floor', 'cube', 0, 0, 0, { scale = { 10, 1, 10 } }

tools/phys/whyStill

phys.whyStill(entityId: string | EntityRef) -> { [string]: any }

Answer why the solver is not moving an entity's rigid body. The reason is the nearest cause from a closed set — noBody, simulationNotStepping, disabled, static, kinematic, infiniteMass, translationLocked, gravityDisabled, asleep, outsideIsland, resting, aboutToMove — so it names the thing to change rather than a consequence of it. A body the solver IS advancing reports moving = true and no reason. Every value comes off the simulation, so a write the solver refused or clamped reads back as what it kept. Answers in edit mode as well as play mode.

Parameters

  • entityId string | EntityRef

Returns { [string]: any }

"crate"

tools/phys/worldState

phys.worldState() -> { [string]: any }

Report what the physics simulation is actually holding: bodies by type, how many are awake and asleep, colliders, joints, contact pairs / touching pairs / contact points, the bodies the last step integrated, world gravity, the timestep, whether the pipeline is stepping at all, and what the last step cost. Counted off the solver, so a body that failed to build is missing here while its Physics component still exists. Answers in edit mode as well as play mode.

Returns { [string]: any }

-- no arguments

tools/pixelArt/anim

pixelArt.anim(opts: AnimOpts) -> number?

Multi-frame animation control. Pick the action via opts.op. All ops take target (handle/id/name). Frames share the canvas palette + size; draw into a frame by making it active (setActiveFrame) then using Pixel.draw.

Parameters

  • opts AnimOpts

Returns number?

{ op = 'duplicateFrame', target = s }
{ op = 'setFps', target = s, fps = 8 }

tools/pixelArt/destroy

pixelArt.destroy(target: Handle | string | number)

Despawn a pixel canvas. target is a handle, entity id, or name.

Parameters

  • target Handle | string | number
"hero"

tools/pixelArt/draw

pixelArt.draw(opts: DrawOpts)

Raster drawing onto a canvas's ACTIVE frame. Pick the primitive via opts.op. All ops take target (handle/id/name) + color (palette name; nil clears) plus op-specific coords.

Parameters

  • opts DrawOpts
{ op = 'fillCircle', target = s, cx = 8, cy = 8, r = 6, color = 'red' }
{ op = 'rows', target = s, rows = {'rr','rr'}, legend = { r = 'red' } }

tools/pixelArt/palette

pixelArt.palette(opts: PaletteOpts)

Palette editing. op='set' replaces the whole palette (cell indices preserved, so it recolors every frame in place); op='add' adds/updates one entry.

Parameters

  • opts PaletteOpts
{ op = 'add', target = s, name = 'blue', color = {0,0,1} }

tools/pixelArt/persist

pixelArt.persist(opts: PersistOpts) -> string | boolean

Template persistence. op='saveAsTemplate' exports a canvas (all frames) as a reusable pixelTemplate asset; op='deleteTemplate' removes one. Instantiate saved templates with Pixel.spawn(op='template').

Parameters

  • opts PersistOpts

Returns string | boolean

{ op = 'saveAsTemplate', target = s, name = 'hero' }
{ op = 'deleteTemplate', name = 'hero' }

tools/pixelArt/query

pixelArt.query(opts: QueryOpts) -> any

Read-only queries against a canvas (or the template library). Pick via opts.op. Per-canvas ops take target (handle/id/name).

Parameters

  • opts QueryOpts

Returns any

{ op = 'getPixel', target = s, x = 0, y = 0 }
{ op = 'listTemplates' }

tools/pixelArt/spawn

pixelArt.spawn(opts: SpawnOpts) -> Handle

Generic pixel-canvas creation + placement. Pick behavior via opts.op: 'create' (blank/seeded sheet), 'rows' (paint from text + a legend), 'template' (instantiate a saved pixelTemplate), or 'clone' (duplicate an existing canvas). Returns a handle.

Parameters

  • opts SpawnOpts

Returns Handle

{ op = 'create', width = 16, height = 16, palette = { red = {1,0,0} } }
{ op = 'rows', rows = {" r ","rrr"," r "}, legend = { r = "red" }, palette = { red = {1,0,0} } }
{ op = 'template', name = 'hero', position = { 0, 1, 0 } }

tools/pp/add

pp.add(name: string, shader: string, opts?: PostEffectOpts) -> string

Add a post-processing effect. shader names a .shader asset — an identity, a path, a guid, or a shipped short name answered by @builtin::shaders.post.<name> (pp.presets lists every one), or a scene-authored .shader asset. Forwards to postprocess.add(name, ref, { priority, enabled, layer }), then applies each property value via postprocess.setProperty(name, prop, value) and binds each texture via postprocess.setTexture(name, prop, path). Every property the effect's schema declares is settable by its own name in this call — the schema is what decides which keys opts takes, so a key it does not declare is refused against that set rather than dropped. postprocess.describe(name) lists the schema of a registered effect and the value each property holds; asset.resolve(shader, "shader"):getProperties() lists it before. opts.layer picks which composited image the effect grades: "scene" runs it before the UI is drawn, over the scene's own light, so it grades the rendered picture and leaves every widget on screen as authored, and "all" runs it after the UI has landed, over the picture fitted to the screen, so the interface is graded along with the picture. A call naming no layer takes the one the shader's space belongs to — a light shader grades "scene", a display shader grades "all" — so pp.add('glow', 'bloom') thresholds the scene's own highlights. A shader reads the scene's depth at a pixel with zero_scene_depth(in.uv), which a defocus, a distance haze or a depth-keyed grade drives from.

Parameters

  • name string
  • shader string
  • opts PostEffectOpts (optional)

Returns string

'edges', 'vignette', { intensity = 0.42, radius = 0.8, softness = 0.55 }
'glow', 'bloom', { threshold = 1.0, intensity = 1.5, scatter = 0.8 }
'levels', 'color_correction', { brightness = 0.1, saturation = 0.3 }
'dusk', 'lut', { texture = lut.install('dusk', lut.fromGrade({ temperature = -0.2 })).guid }
'warmth', 'colorGrade', { contrast = 1.1, saturation = 1.2 }
'haze', 'fog', { color = { 0.6, 0.65, 0.75 }, density = 0.03 }

tools/pp/list

pp.list() -> { PpListRow }

List the currently-active post-processing effects, in the order they apply. Each row carries its layer (which composited image it grades) and space (what its fragment() reads and returns), alongside enabled, priority and any compile error — forwarded from postprocess.status().

Returns { PpListRow }

tools/pp/presets

pp.presets() -> { PostEffectPreset }

List every shipped post-process shader's short name, sorted alphabetically. Every name this returns is one pp.add accepts — pp.add(effectName, name, opts), and resolveShader in pp.add's own doc names the lookup order — and one whose property schema is readable before the effect is added, at the shader identity returned beside it. Any scene-authored .shader asset is addable the same way, by its own identity, path or guid. A handful of these names also carry a friendlier alias or default table inside pp.add itself — that table is not this list; every name here works either way.

Returns { PostEffectPreset }

tools/pp/remove

pp.remove(name: string) -> boolean

Remove a post-processing effect by name. Forwards to postprocess.remove(name). Removing an effect that does not exist follows the underlying API's contract.

Parameters

  • name string

Returns boolean

'vignette'

tools/pp/setEnabled

pp.setEnabled(name: string, enabled: boolean) -> boolean

Enable or disable a post-processing effect without removing it. Forwards to postprocess.setEnabled(name, enabled).

Parameters

  • name string
  • enabled boolean

Returns boolean

'bloom', false

tools/primitives/cube

primitives.cube(name: SpawnTarget, x?: number, y?: number, z?: number, opts?: CubeOpts) -> string

Spawn a visual cube at a position. No physics by default — add Physics and Collider components explicitly if collision is wanted. Set opts.scale to size the cube — a number for a uniform cube, or three components as {x, y, z} or { x = , y = , z = } for a box. Set opts.animate=true to play a viz.popIn entrance (default off — at bulk-spawn rates the per-call string.format + tween-source compile dominates spawn time).

Parameters

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • opts CubeOpts (optional)

Returns string

"box1", 0, 5, 0
{ name = "crate", position = { 4, 0, 2 } }
"backdrop", 0, 0, -10, { scale = 5 }
"beam", 0, 1, 0, { scale = { 4, 0.4, 0.4 } }
"falling", 0, 5, 0

tools/primitives/ground

primitives.ground(name: SpawnTarget, x?: number, y?: number, z?: number, color?: shared.ColorInput, size?: number) -> string

Spawn a flat ground plane with optional color tint and size. The base mesh is 1x0.1x1 unit; size scales it uniformly in XZ. Always has static physics (it's a ground plane).

Parameters

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • color shared.ColorInput (optional)
  • size number (optional)

Returns string

"floor", 0, 0, 0
{ name = "floor", position = { 0, -1, 0 } }
"red_floor", 0, 0, 0, {r=200, g=50, b=50}
"blue_floor", 0, 0, 0, {50, 90, 200}
"big_floor", 0, 0, 0, nil, 100

tools/primitives/groundHex

primitives.groundHex(name: SpawnTarget, x?: number, y?: number, z?: number, color?: shared.ColorInput) -> string

Spawn a hexagonal ground tile with optional color tint. Includes static physics so objects rest on it.

Parameters

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • color shared.ColorInput (optional)

Returns string

"hex_tile", 0, 0, 0
{ name = "hex_tile", position = { 3, 0, 3 } }
"grass_hex", 0, 0, 0, {r=80, g=160, b=60}
"sand_hex", 0, 0, 0, {200, 180, 120}

tools/primitives/tree

primitives.tree(name: SpawnTarget, x?: number, y?: number, z?: number, preset?: TreePreset, seed?: number) -> string

Spawn a procedural tree from a preset. Includes static physics collider so objects collide with it.

Parameters

  • name SpawnTarget
  • x number (optional)
  • y number (optional)
  • z number (optional)
  • preset TreePreset (optional)
  • seed number (optional)

Returns string

"oak1", 5, 0, 3, "oak"
{ name = "oak1", position = { 5, 0, 3 } }, nil, nil, nil, "oak"
"tree_" .. i, math.random(-20, 20), 0, math.random(-20, 20), "pine", i * 42

tools/probe/cancel

probe.cancel(id: string) -> CancelResult

Detach a probe and forget it. Pass a probe id, or "all" to take every probe off at once. Detaching restores what the probe replaced: a wrapped function is put back, a patched line is unpatched, a per-frame read stops. Cancel the probes you are done with. Nothing about a probe survives the engine stopping, so a forgotten one costs only this session — but a probe still armed on a hot path is still doing work, and a line probe holds its function out of native code until that script reloads.

Parameters

  • id string

Returns CancelResult

"all"

tools/probe/hits

probe.hits(id: string) -> HitsResult

Drain the hits one probe has recorded — the value its body returned, the frame and time it fired on, and the context it saw. The notice is the headline; this is what stands behind it, for a hit whose values are bigger than a line. Draining empties the record, so a second call reports only what is new since the first. The record is bounded: it holds the most recent hits, not every hit ever taken.

Parameters

  • id string

Returns HitsResult

"prb_1"

tools/probe/list

probe.list(armedOnly?: boolean) -> ProbeListing

Every probe this session has armed, with what it is attached to and what it has seen: hits taken, hits the cooldown held back, body errors, and — for one that has stopped — why it stopped. suppressed is the number the cooldown swallowed. It is reported rather than dropped, so a probe on a hot path never leaves you believing it fired as often as it told you. caller is the caller that armed the probe, and the caller its notices go to whoever's code trips it. A probe armed with no caller in scope has none, and its notices go to every caller.

Parameters

  • armedOnly boolean (optional)

Returns ProbeListing

true

tools/probe/onCall

probe.onCall(path: string, opts: shared.ArmOpts) -> OnCallResult

Fire when the function path names runs, and see both what went in and what came out. path is a module identity and a field on it, such as modules.combat.applyDamage: the probe replaces that field with a wrapper that calls through, so it follows the FUNCTION rather than the file, and a caller reaching it by any route is seen. The context carries args (a list, in order) and ret (the first returned value), so "it fired and produced this" is one condition: when = "ret.killed". A function that is not a field on a required module — a local inside a module, a helper a component keeps to itself — cannot be named this way and is reached with onLine instead. Globals are sealed read-only and cannot be wrapped at all. notice is required — say what you are watching for.

Parameters

  • path string
  • opts shared.ArmOpts

Returns OnCallResult

"modules.combat.applyDamage", { notice = "a hit killed something", when = "ret.killed", run = "return ret.target" }

tools/probe/onChange

probe.onChange(expr: string, opts: shared.ArmOpts) -> OnChangeResult

Fire when the value of expr becomes different from the value it last had. Reads the expression once per frame and fires on the transition, so it reaches anything the engine will evaluate — a component field, a light's intensity, a transform's position, a table lookup — without needing that value to publish an event. Pair it with when to name the value you are waiting for rather than every change on the way there: when = "value >= 5" fires the first time it reaches five, not on each step toward it. It samples ONCE PER FRAME, so it sees a value that changed and changed back within a frame as no change at all, and a value held for only a frame or two as an edge it may catch going up and miss coming down. When what you are chasing is that short-lived, probe the CODE that sets it with onLine rather than the value with this — a line fires every time it runs, whatever the frame rate. notice is required — say what you are watching for.

Parameters

  • expr string
  • opts shared.ArmOpts

Returns OnChangeResult

"entity.find('Lamp').component.get('Light').intensity", { notice = "the lamp got bright", when = "value >= 5", run = "return value" }

tools/probe/onLine

probe.onLine(path: string, line: number, opts: shared.ArmOpts) -> OnLineResult

Fire when execution reaches line in the script path names — its chunk name, its VFS path, or its require identity. This is the one target that reaches code no name can address: a local inside a module, a helper a component never exported. The world is NOT stopped; the line runs on and your body sees the frame it was in. when is evaluated in that frame, so it reads the frame's OWN variables by their own names and with their own types: doubled >= 6, not locals.doubled. A hit whose condition does not hold never reaches your body at all. run sees the same frame through locals and upvalues, where every value is rendered as text — good for reporting, which is what a body is for. Put comparisons in when, where the values are still real. notice is required — say what you are watching for, so the report names it rather than handing you a value from nowhere.

Parameters

  • path string
  • line number
  • opts shared.ArmOpts

Returns OnLineResult

"Ticker", 9, { notice = "score got big", when = "doubled >= 6", run = "return locals.doubled" }

tools/procgen/adjust

procgen.adjust(args: AdjustArgs) -> AdjustResult

Adjust a live generator in place: merge values into its param overrides (each key overrides the graph input of the same name) or switch which output it realizes. The change queues a cook on the generator's async runner and returns immediately — a heavy graph keeps cooking across frames while this call reports back. Watch it with procgen.cooks, cancel it with procgen.cancel.

Parameters

  • args AdjustArgs

Returns AdjustResult

{ entity = "LegoSculpture", params = { model = "@builtin::meshes.torus" } }

tools/procgen/apply

procgen.apply(args: ApplyArgs) -> ApplyResult

Swap a live generator for the asset it makes, in place: the current output is written as a durable bundle and the Generator component is replaced by an Asset component standing it — the result stands on its own with no graph and no generator, like applying a modifier. Every generator applied with the same graph, params and frame references one bundle.

Parameters

  • args ApplyArgs

Returns ApplyResult

{ entity = "LegoSculpture" }

tools/procgen/bake

procgen.bake(args: BakeArgs) -> BakeResult

Evaluate a .procGraph graph target and realize it into the live scene as durable content, then verify it rendered. Geometry becomes a .mesh asset + a Model entity; a population (points carrying a Mesh) becomes live per-instance entities; a Bundle/Prefab becomes durable live entities that survive the edit->play reload. Pass replace to swap an existing entity in place (inheriting its position); position to place it; material to tint a Geometry bake.

Parameters

  • args BakeArgs

Returns BakeResult

{ graph = "/source/proc/tower.procGraph", name = "tower", position = { 0, 0, 0 } }

tools/procgen/cancel

procgen.cancel(args: CancelArgs) -> CancelResult

Cancel the in-flight cook on an entity's Generator. The previous output stays; the next param change cooks fresh.

Parameters

  • args CancelArgs

Returns CancelResult

{ entity = "LegoSculpture" }

tools/procgen/collapse

procgen.collapse(args: CollapseArgs) -> CollapseResult

Lift a section of a graph's code (a run of its node calls, named by the lines it spans) into a node of its own: a .procNode whose inputs are the values the section reads from the code before it, whose outputs are the values it makes that the code after it reads, and whose body is the section. The section in the graph becomes one call of the new node, so the graph builds from the same calls and cooks to the same result. A graph input the section reads is declared in the node as the graph declares it, and 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 node lands beside the graph (or in into), is called by its ~ path, and is found by procgen.searchNodes and described by procgen.describeNode like any other node. procgen.flatten is the inverse: it writes a node's body back out in place of its call.

Parameters

  • args CollapseArgs

Returns CollapseResult

{ graph = "/source/fx/Pebbles.procGraph", from = 12, to = 15, name = "Strew", description = "Strews pebbles on points, each turned its own way." }

tools/procgen/cooks

procgen.cooks() -> CooksResult

List the active cooks, the recent cook history — progress, status, and failure detail (which node failed) — and the frames generators presented most recently. A finished record's durationMs spans the whole regen: evaluation plus realizing the output and refreshing the bake snapshot. A present names its frame, its time, how it was made ("cook" or "device") and each output's element count; each one is reported through proc.cooks.presented as it lands, which a probe can stop the world on. The scene-wide cook monitor.

Returns CooksResult

tools/procgen/coverage

procgen.coverage(args?: CoverageArgs) -> CoverageResult

The rewrite's coverage report. Lists every current procgen op with the new node that replaces it (from the composition sheet), its class (exact, meaning, gone), the parity state, and the existing suites that call it; and every library graph (.procGraph and .procNode) 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; run compares it), pass / fail (a run compared it), no-case (no fixture builds the op's 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). A no-case row means the op's case (the one stated for it, else its defaults) is refused by the current op or has no inputs a fixture builds. parity.json states the cases that name only content the library ships, and a parity suite states the cases whose inputs are its test fixtures beside it, so an engine whose library leaves that suite out runs those ops on their defaults.

Parameters

  • args CoverageArgs (optional)

Returns CoverageResult

{ state = "pending", query = "terrain" }

tools/procgen/describeNode

procgen.describeNode(name: string) -> NodeDescription

Describe ONE node in full, the procgen counterpart of describe_tool, read from what its source declares without running it: what it does, the inputs it takes in the order a call takes them (each with its type, default, range, options and description), the outputs it gives (one output is what the call returns; several come back as a table by name), its family, the name and the call a graph writes, the context it works in, what it reads from or writes to beside its sockets, what it costs where its declaration states it, and its README. Takes the name a graph calls (points.disc), a node's ~ path, path or guid, or an op of a pack (op.geometry.bevel). Find names with procgen.searchNodes.

Parameters

  • name string

Returns NodeDescription

"points.disc"
"op.geometry.bevel"

tools/procgen/diff

procgen.diff(args: DiffArgs) -> DiffResult

Compare two .procGraph graphs and report changed/added/removed nodes plus per-node op/param/input deltas (structural, via the content hash).

Parameters

  • args DiffArgs

Returns DiffResult

{ a = "/source/proc/box.procGraph", b = updatedGraph }

tools/procgen/explain

procgen.explain(args: ExplainArgs) -> ExplainResult

Agent-oriented natural-language summary of a .procGraph graph: its size, declared inputs (with types), the op of each node, the composite references, and where each output comes from.

Parameters

  • args ExplainArgs

Returns ExplainResult

{ graph = "/source/proc/box.procGraph" }

tools/procgen/find

procgen.find(args?: FindArgs) -> FindResult

Discover procedural graphs by what they produce. Lists every .procGraph asset, opens each, reads its declared outputs' types, and filters on name (substring over name/id), tag, category, and/or produces (an output type or producing-op substring, e.g. "Bundle", "Geometry", "mesh."). Returns each match with its resolved outputs — so you can find a graph by its result, not just its description.

Parameters

  • args FindArgs (optional)

Returns FindResult

{ produces = "Bundle" }

tools/procgen/flatten

procgen.flatten(args: FlattenArgs) -> FlattenResult

Write 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, its inputs locals holding what the call passed (or the defaults the node declares), and the local the call declared holding what the node gives. The graph builds from the same calls and cooks to the same result. With call it writes out that one call; with node every call of that node; with neither every call of a node or part outside the built-in library, again until none is left.

Parameters

  • args FlattenArgs

Returns FlattenResult

{ graph = "/source/fx/Pebbles.procGraph", call = "strewn" }

tools/procgen/frames

procgen.frames(args: FramesArgs) -> FramesResult

Pictures of a generator at moments of its graph time. For each time the generator's clock is stopped, its playhead moved to the frame that stands for that time, and the picture taken once the generator has presented that frame: what the generator draws on that frame, alone, framed to fit it, over the grey backdrop and light of the studio an asset preview is photographed in, so an effect pictures the same in every world. The clock is put back where it stood afterwards, running again if it was running. Each picture comes back in the result with the graph time and frame it shows and each output's element count, so a sequence of them shows how an effect goes from moment to moment. A timeline with no end is a simulation of everything before a time, so it is played to each time rather than moved there: its clock begins again at 0 and plays, each picture is of the first frame presented at or past its time (the answer's time says which), and the clock plays on afterwards when it was playing.

Parameters

  • args FramesArgs

Returns FramesResult

{ entity = "impact" }
{ entity = "storm", from = 0, to = 4, count = 8 }
{ entity = "breath", times = { 0.2, 0.8, 1.5 }, angle = { 90, 15 } }
{ entity = cast.entity.id, times = { 0.1, 0.5 } }

tools/procgen/generators

procgen.generators() -> GeneratorsResult

List every live procedural generator in the scene — the entities carrying a Generator component — with each one's bound graph, selected output, current param overrides, realized child count, and cook state (whether it's cooking right now, its progress, and its last finished cook's outcome). Use it to find what's generating — or why a generator's output looks stale — before adjusting (procgen.adjust) or flattening (procgen.apply).

Returns GeneratorsResult

tools/procgen/inspect

procgen.inspect(args: InspectArgs) -> InspectResult

Inspect one node of a .procGraph graph: its op, declared input/param/output sockets (from the registry), the InputRefs feeding it (producers), and the nodes that consume its outputs (consumers).

Parameters

  • args InspectArgs

Returns InspectResult

{ graph = g, node = "box" }

tools/procgen/modify

procgen.modify(args: ModifyArgs) -> ModifyResult

Edit the structure of a graph's code without hand-editing its text. Each edit is a table 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 } change what a call is given; { op = "add", node, as?, args?, after? } adds local <as> = g.<node>(<args>) (after the call or line after, else after the last statement before the outputs); { op = "remove", call } removes a call nothing after it reads; { op = "repoint", call, node } makes a call name another node, keeping what it is given; { op = "rename", from, to } renames a local; { 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 its declaration. A call is named by the local it declares or a line it spans; a value is a plain value or { expr = "<code>" }. procgen.patch is the one-call form of set and unset.

Parameters

  • args ModifyArgs

Returns ModifyResult

{ graph = "/source/fx/Pebbles.procGraph", edits = { { op = "add", node = "geometry.transform", as = "lifted", after = "strewn", args = { geometry = { expr = "strewn" } } }, { op = "output", name = "pebbles", value = "lifted" } } }

tools/procgen/onPresent

procgen.onPresent(args?: OnPresentArgs) -> OnPresentResult

Arm a probe that fires on the frames a generator presents matching every term given, and — with capture — takes a picture of each one it fires on: the generator is held on that frame while the picture is taken, of what it draws alone, framed to fit, over a grey studio backdrop; the hit's notice carries the frame and the picture, and the effect plays on. freeze = true holds the generator on the frame it fires on — its clock stopped there until generator:play() — and stops the world, for looking at it from more than one place. Terms: entity (the generator, or a play of an effect: an entity carrying an Effect component, whose every graph is watched, a picture of it framed on all of them), output (that output presents, holding at least minElements elements when given), at (graph time reached — with every, the time within each repeating cycle of that length, so at = 0.4, every = 3 is a frame from 0.4 s to 0.4 s + within into any three-second loop; within defaults to 0.25 s, room for the presents a heavy cook spaces out) and how ("cook", "memo", "device" or "held"). The notice names the frame's time and each output's element count; probe.list, probe.hits and probe.cancel manage it like any probe, and probe.hits lists each hit's picture. Play a held generator on with generator:play(), and resume a stopped play session with engine.paused = false.

Parameters

  • args OnPresentArgs (optional)

Returns OnPresentResult

{ entity = "impact", output = "sparks", at = 0.4, every = 3, minElements = 100 }
{ output = "cracks", at = 2, limit = 5 }
{ entity = "breath", at = 1.5, every = 6, capture = { width = 960, height = 540 } }

tools/procgen/patch

procgen.patch(args: PatchArgs) -> PatchResult

Change what one node call in a graph's code is given, without editing the text by hand: set gives inputs values (a plain value, or { expr = "<code>" } for code such as a local's name or a formula), unset leaves inputs to the node's defaults. The call is named by the local it declares or by a line it spans. The edit is written once the graph builds with it; a value the node does not take is refused naming the inputs it does. procgen.modify makes the structural edits (adding, removing and repointing calls, renaming, inputs and outputs).

Parameters

  • args PatchArgs

Returns PatchResult

{ graph = "/source/fx/Pebbles.procGraph", call = "spots", set = { count = 80 } }
{ graph = "/source/fx/Pebbles.procGraph", call = "spots", set = { radius = { expr = "radius * 2" } } }

tools/procgen/preview

procgen.preview(args: PreviewArgs) -> PreviewResult

Evaluate a .procGraph graph target, visualize it (any output type) and render it: the picture comes back in the result, beside what was measured. An output made of drawn points (a particle effect) is pictured as its generator draws it at the moment the graph names for a still (g:still).

Parameters

  • args PreviewArgs

Returns PreviewResult

{ graph = "/source/proc/box.procGraph", target = "output:mesh" }
{ graph = "/source/proc/valley.procGraph", target = "node:snowBand", layer = "snow" }

tools/procgen/promote

procgen.promote(args: PromoteArgs) -> PromoteResult

Make a part (a .procGraph that graphs call by its path) a node: a .procNode that declares the part's inputs as its own, calls the part with them and gives its outputs. The part keeps its code and every graph calling it; the node puts it among the nodes procgen.searchNodes finds and a graph calls by name, and it stays the part's: editing the part changes what the node makes.

Parameters

  • args PromoteArgs

Returns PromoteResult

{ graph = "/source/fx/Rim.procGraph", description = "A rim of rocks around a disc." }

tools/procgen/reference

procgen.reference(args: ReferenceArgs) -> ReferenceResult

Replace a section of a graph's code (a run of its node calls, named by the lines it spans) with a call of a node that already exists: the way a graph adopts a node lifted out of another graph, 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; each value the section makes that the code after it reads is taken from the node's output of the same name. inputs and outputs say otherwise where the names differ. A wiring the node cannot take is refused naming what it takes and gives.

Parameters

  • args ReferenceArgs

Returns ReferenceResult

{ graph = "/source/fx/Gravel.procGraph", from = 9, to = 12, node = "~/fx/Strew" }
procgen.search(args?: SearchArgs) -> SearchResult

Search the procedural-graph library — every .procGraph this engine can see, the ones the world authored and the ones it ships. A free-text query matches the graph's name, its identity, the description its .metadata carries and its tags, so the word you would use for the thing finds it even when the graph is named something else. Each word of the query counts on its own, a graph matching at least half of them is found, and the results come best first. tag and category narrow it. Ask this before describing something from nothing: a graph that already makes what you need is two calls away, and rewriting one costs the work twice over.

Parameters

  • args SearchArgs (optional)

Returns SearchResult

{ query = "scatter" }
{ tag = "terrain" }

tools/procgen/searchNodes

procgen.searchNodes(query?: string, opts?: SearchNodesOpts) -> SearchNodesResult

Find the nodes a graph is built from, the procgen counterpart of search_tools: every .procNode a graph calls as g.<family>.<name>(...) (or by its ~ path), and every op of a pack (called g.op.<id>(...), family op.<head>). A query is answered two ways at once: by keyword over each node's name, family, declared inputs and outputs and description, and, where this engine is bound to a world, by meaning through ZeroMind's search over indexed nodes, which also finds nodes other worlds publish that this one has not installed. Each row says how it matched. With no query it lists the nodes (narrowed by family) and catalogue names every family and the nodes in it. procgen.describeNode describes one in full.

Parameters

  • query string (optional)
  • opts SearchNodesOpts (optional)

Returns SearchNodesResult

"scatter points on a surface"
"", { family = "points" }
"noise", { mode = "keyword", limit = 20 }

tools/procgen/spawn

procgen.spawn(args: SpawnArgs) -> SpawnResult

Spawn a live procedural generator from a .procGraph graph: creates an entity with a Generator component bound to the graph, which immediately realizes the graph's output as the entity's managed children and keeps regenerating live as params change. This is the entry point for putting a graph in the scene — no manual entity.spawn / component.add needed.

Parameters

  • args SpawnArgs

Returns SpawnResult

{ graph = "/source/legoify.procGraph", name = "LegoSculpture", position = { 0, 3, 0 } }

tools/procgen/stats

procgen.stats(args: StatsArgs) -> StatsResult

Structural counts of a .procGraph graph (nodes / composites / inputs / outputs). When target is given, also evaluates it and reports the output type, elapsed duration, and (for geometry) vertex/triangle counts. Timing is reported only — it never feeds graph evaluation (determinism).

Parameters

  • args StatsArgs

Returns StatsResult

{ graph = g, target = "output:mesh" }

tools/procgen/trace

procgen.trace(args: TraceArgs) -> TraceResult

Dependency chain for a target: the node ids reachable from the target, in evaluation order (each node's producers appear before it — a post-order walk of the node_output edges). Composite/const/graph_input leaves stop the walk.

Parameters

  • args TraceArgs

Returns TraceResult

{ graph = g, target = "output:mesh" }

tools/procgen/tweak

procgen.tweak(args: TweakArgs) -> TweakResult

Preview a graph with other values for its declared inputs, without changing its source: the graph is evaluated with inputs in place of the defaults it declares, then pictured the way procgen.preview pictures it. Each key of inputs names an input the graph declares (g:input); a name it does not declare is refused with the names it does.

Parameters

  • args TweakArgs

Returns TweakResult

{ graph = "/source/proc/tower.procGraph", inputs = { floors = 12 } }

tools/procgen/validate

procgen.validate(args: ValidateArgs) -> ValidateResult

Check a .procGraph graph the way its cook will: missing inputs, type mismatches, cycles, invalid composite bindings and unknown nodes from its connections, then what a cook of its first frame refuses (a node's own check, an input a node cannot take, a value a node refuses). The cook runs in a cache of its own and stands nothing in the scene. Returns the diagnostics list.

Parameters

  • args ValidateArgs

Returns ValidateResult

{ graph = "/source/proc/box.procGraph" }
{ graph = "sparks", params = { rate = 400 } }

tools/profiler/compare

profiler.compare(before?: string, after?: string) -> string

Diff two recordings (made with record): the change in avg/p90 frametime, and the systems that moved the most between them. Record a baseline, make a change, record again, then compare to see whether the change helped and which system it moved. A positive frametime delta means B is slower than A.

Parameters

  • before string (optional)
  • after string (optional)

Returns string

"baseline", "optimized"  -- did 'optimized' beat 'baseline', and where

tools/profiler/flamegraph

profiler.flamegraph(seconds?: number, mode?: ("run" | "start" | "stop" | "snapshot"), label?: string) -> string

Sample the Luau call stack to find the hottest code paths — the depth view under a hot component or task_scheduler. flamegraph(seconds) runs the sampler for that long (resetting first) and returns the top stacks plus a folded-stack file for flamegraph rendering. mode picks a manual phase instead: "start" / "stop" a long session, or "snapshot" the current top stacks without stopping.

Parameters

  • seconds number (optional)
  • mode ("run" | "start" | "stop" | "snapshot") (optional)
  • label string (optional)

Returns string

-- 3s sample, top stacks + folded file
5          -- 5s sample
0, "snapshot" -- top stacks right now without stopping a session

tools/profiler/frame

profiler.frame(source?: string, which?: ("worst" | "typical"), minMs?: number) -> string

The current frame's time as a self-accounting tree: schedule -> system -> sub-timing, each row showing SELF (own time excluding children), TOTAL, calls, and % of frame. Time no schedule covers (the frame limiter / vsync wait, GPU present, event loop) shows as present / idle; each expensive component's update(dt) is listed 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 top: the row with the largest SELF time is where the frame time actually goes.

Parameters

  • source string (optional)
  • which ("worst" | "typical") (optional)
  • minMs number (optional)

Returns string

-- live frame, full attributed tree
"combat", "worst"     -- the worst frame of the 'combat' recording
"combat", "typical"   -- the typical (average) frame of that recording

tools/profiler/gpu

profiler.gpu(n?: number) -> string

GPU pass timings over the last resolved frames, from GPU timestamp queries. Each row is one label: the median of its per-frame total, the min/max that median sits in, the passes per frame, and how many of the window's frames carried it. A row marked floor ran but the device resolved no duration for it — its two timestamps retired within a few ticks of each other. The last column is how many resolved frames ago the label last recorded a pass: now is a pass running in the frame the table describes, and anything else is a row the window still holds after the work under it stopped. The readback is asynchronous, so the window lags the live frame by a few frames. Use this when frame shows the time under present / idle (GPU-bound) and you need to know which passes the GPU spends it on.

Parameters

  • n number (optional)

Returns string

-- top 20 GPU spans of the window
40     -- top 40

tools/profiler/hits

profiler.hits(label?: string, spikeMs?: number) -> string

Drain the frames a record-mode watch caught and report them as a spike-cluster distribution — every caught frame grouped by its dominant hotspot, so repeated hitches collapse to their handful of causes instead of a wall of individual frames. Draining clears the buffer. The snapshot is stored under label — drill into any cluster with frame <label> / hotspots <label>. Arm the watchdog first with watch <ceilingMs>.

Parameters

  • label string (optional)
  • spikeMs number (optional)

Returns string

-- review everything the watchdog caught
"collapse_hits"    -- store under a name for later drill-down

tools/profiler/hotspots

profiler.hotspots(n?: number, source?: string, which?: ("worst" | "typical")) -> string

Rank the frame's costs by SELF time (a node's own cost, excluding its children) and return the top n as a flat table. Because it ranks by SELF, the top rows are the actual expensive leaves — a system's own work or a single heavy component's update(dt) — not the schedules that merely contain them. This is the "just tell me what's slow" tool; follow a hit down with frame. Reads the live frame ("now") or a stopped recording's worst/typical frame (pass its label).

Parameters

  • n number (optional)
  • source string (optional)
  • which ("worst" | "typical") (optional)

Returns string

-- top 12 costs in the live frame
20           -- top 20
12, "combat" -- top 12 in the worst frame of the 'combat' recording

tools/profiler/memory

profiler.memory(n?: number) -> string

The Luau VM's memory: total heap + GC state, then the components retaining the most memory (per instance). Use it to catch a growing script — 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.

Parameters

  • n number (optional)

Returns string

-- VM total + top 20 components by retained memory
10    -- top 10

tools/profiler/record

profiler.record(action?: ("start" | "stop" | "status"), label?: string, spikeMs?: number) -> string

Start / stop / check a background profiling recording that spans a play session. record("start", label) begins capturing every frame; play or test the game across as many turns as you want, then record("stop", label) returns the windowed breakdown: avg / p50 / p90 / p99 / max frametime, spike count, the single WORST frame's attributed tree, and a typical-frame tree. Recording is one engine-wide slot, so a start while another recording runs is refused with the label of that recording, the caller holding it, and how long it has been held; record("status") answers with the same. A stopped recording is kept under its label. Analyse it later with frame, hotspots, or scripts (pass the label as their source), or diff two of them with compare.

Parameters

  • action ("start" | "stop" | "status") (optional)
  • label string (optional)
  • spikeMs number (optional)

Returns string

"start", "combat"      -- begin recording a combat encounter
"stop", "combat"       -- end it, get the windowed breakdown
"status"               -- is a recording running right now?

tools/profiler/retro

profiler.retro(seconds?: number, label?: string, spikeMs?: number) -> string

Retroactively read the ring buffer's last seconds of frames (default: the whole ring) as a spike-cluster report — the latency-immune profiler. Enable the ring first (ring on), drive the scene, then call this AFTER the spike: the data is historical, so your call's timing does not matter. The report groups every spike frame by its dominant hotspot (so one call shows the full distribution of what's slow, not one anecdote), on EFFECTIVE frame time (your own execute cost excluded). The snapshot is stored under label — drill into any cluster with frame <label> / hotspots <label>.

Parameters

  • seconds number (optional)
  • label string (optional)
  • spikeMs number (optional)

Returns string

-- the whole ring, clustered
8               -- just the last 8 seconds
8, "collapse"   -- last 8s, stored as 'collapse' for drill-down

tools/profiler/ring

profiler.ring(action?: ("on" | "off" | "status"), seconds?: number) -> string

Control the retroactive ring buffer — an always-recording, bounded history of the last N seconds of per-frame data you query AFTER the fact with retro. ring("on", seconds) starts it (default 20s); ring("off") stops and clears it; ring("status") reports whether it's on, how many frames and seconds it holds. Editor profile only — a no-op in the runtime profile. The ring is off until you turn it on, so it costs nothing until then. This is the fix for "I can't profile a spike I only see afterwards".

Parameters

  • action ("on" | "off" | "status") (optional)
  • seconds number (optional)

Returns string

"on", 30      -- keep the last 30 seconds, always
"status"      -- is the ring on? how much does it hold?
"off"         -- stop recording and clear the history

tools/profiler/scripts

profiler.scripts(n?: number, minMs?: number, type_?: string) -> string

Rank components by update(dt) cost — the content view of where the frame's script time goes. Rolled up per component type by default (many instances of a type collapse to Type xN with summed cost; a lone instance keeps its @ entity), so it stays readable whether a world has three scripts or three hundred. Pass a type to drill into that one type's individual instances (which entity is the heavy one). Use it after hotspots/frame point at lua_update.vm_call. Counts COMPONENT update loops; a scene entrypoint's per-frame update / editorUpdate runs on the scheduler, and scene.cost ranks the loaded scenes by what theirs costs.

Parameters

  • n number (optional)
  • minMs number (optional)
  • type_ string (optional)

Returns string

-- every component type, heaviest first
10       -- the 10 heaviest types
20, 0.5, "MyMover" -- instances of MyMover costing >= 0.5ms, by entity

tools/profiler/summary

profiler.summary() -> string?

The engine's own one-line performance summary, exactly as it rides a tool response: the headline frame time, and — when the frame is slow or CPU moved since the last edit — the severity marker, the delta, and the systems the time went to. Read this rather than composing a line from profiler.frame or profiler.hotspots: those report the profiler's effective frame time (wall clock minus the cost of the call asking), while this reports the published smoothed frame, so the two do not agree and only this one matches what other callers are told.

Returns string?

-- "12.4ms frame"

tools/profiler/tasks

profiler.tasks(n?: number, minMs?: number) -> string

Rank components by the time the scheduler spent resuming their coroutines this frame — the content breakdown of task_scheduler. Rolled up per component type (many instances collapse to Type xN; a lone instance keeps its @ entity). Use this when frame/hotspots show task_scheduler hot and you need to know whose task.spawn / task.wait work is behind it.

Parameters

  • n number (optional)
  • minMs number (optional)

Returns string

-- every component's coroutine cost, heaviest first
10       -- the 10 heaviest

tools/profiler/watch

profiler.watch(ceilingMs?: (number | "off" | "status"), mode?: ("record" | "pause"), excludeAgent?: boolean) -> string

Arm a frame-time watchdog that catches bad frames without you having to poll (which always lands seconds late). Call with a number to arm: watch(50) records every frame whose EFFECTIVE time (agent cost excluded) is >= 50ms; watch(50, "pause") instead pauses gameplay the first time the ceiling is crossed, freezing the bad state for you to inspect (then read it with retro). watch("off") disarms; watch("status") (or no arg) reports state and hit count. Read recorded hits with the hits tool. Editor profile only — returns a refusal in the runtime profile. excludeAgent (default true) keeps your own execute/write frames from tripping it.

Parameters

  • ceilingMs (number | "off" | "status") (optional)
  • mode ("record" | "pause") (optional)
  • excludeAgent boolean (optional)

Returns string

50                 -- record every frame over 50ms effective
50, "pause"        -- pause gameplay the first time a frame exceeds 50ms
"status"           -- armed? how many hits so far?
"off"              -- disarm

tools/renderLayer/camera

renderLayer.camera(targets: Targets, filter?: string) -> { any }

Choose which render layers a camera DRAWS, as a layer-NAME filter — the visibility half of the render-layer system, and the way to keep a wall, prop or overlay out of a shot while it still exists for lighting, shadows and physics. A filter is a space-separated spec like "all", "all !ui", or "all !shell !EditorUI", where a bare name includes that layer and a !name excludes it; this is the same language capture and the Camera component already speak. Targets are entities carrying a Camera component, resolved by NAME or id, an array of them, scene.find records, or a QUERY ({ component = { "Camera" } } reaches every camera at once). Call with NO filter to read the current one back. An entity without a Camera component is an error. Returns { id, name, renderLayers } per camera, so the write is also the read. Use renderLayer.set to put geometry ON a layer — that is the other half; a camera can only exclude a layer something is actually on.

Parameters

  • targets Targets
  • filter string (optional)

Returns { any }

"mainCamera", "all !shell"
"previewCamera", "all !ui !EditorUI"
{ component = { "Camera" } }, "all !debug"
"mainCamera"

tools/renderLayer/get

renderLayer.get(targets: Targets, opts?: GetOpts) -> { any }

Read which render layers one or many entities are on — what is actually drawn where, reported as layer NAMES rather than a bitmask. Targets resolve by entity NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "wall" } }, { component = { "Camera" } }, { under = { "building" } }) so you can inspect a selection without looking its ids up first. Pass tree = true to read each target AND every descendant, which is how you check a hierarchy a renderLayer.set with tree = true just wrote. Returns { id, name, layers, skipped } per entity read: layers is the array of layer names it carries — an entity nothing has re-layered reports default — and skipped counts the descendants this read left out, so reading a root of 46 children on its own reports skipped = 46 rather than reading like the whole building. An unrecognised option key is an error naming the accepted set. Use renderLayer.set to change membership, renderLayer.camera to see or change which layers a camera draws, and renderLayer.list to see every layer that exists and who is on it.

Parameters

  • targets Targets
  • opts GetOpts (optional)

Returns { any }

"frontWall"
{ "marker_a", "marker_b" }
{ under = { "building" } }
"building", { tree = true }

tools/renderLayer/list

renderLayer.list() -> any

Show every render layer that exists in this world, which entities are on each, and which cameras include or exclude it — the answer to "what layers do I have" and "why is nothing showing up on this one". Layers are reported in bit order with a builtin flag: default, ui, debug, sky and EditorUI are seeded, everything else interned the first time a name was referenced. This is the tool that makes a MISTYPED layer visible — referencing a name that does not exist CREATES it, so "shel" instead of "shell" yields a real but empty layer that renders nothing and raises no error, and listing shows it with no members. Returns { layers, cameras } where each layer is { name, bit, builtin, entities } and each camera is { id, name, renderLayers } carrying its name filter. Use renderLayer.set to change entity membership and renderLayer.camera to change what a camera draws.

Returns any

tools/renderLayer/screen

renderLayer.screen(screen: string, layers: LayerNames) -> any

Choose which render layers a UI screen DRAWS INTO, by layer NAME — the way to keep a HUD, overlay or editor panel out of a camera's shot or a screenshot while it keeps rendering elsewhere. A screen appears in a camera or capture only when its layers intersect that camera's include filter, the same rule geometry follows. Layers are given by NAME — a single name, an array of names, or a space-separated string like "debug ui" — never a bitmask. Content UI sits on ui by default and the editor places its chrome on EditorUI, which is why a capture asking for "all !EditorUI" drops the editor panels and keeps the game HUD. Returns { screen, layers } with the layers the screen now draws into. Use renderLayer.camera to choose what a camera renders, renderLayer.set for entity membership, and renderLayer.list to see every layer that exists.

Parameters

  • screen string
  • layers LayerNames

Returns any

"hud", "ui"
"debugOverlay", { "debug", "ui" }

tools/renderLayer/set

renderLayer.set(targets: Targets, layers: LayerNames, opts?: SetOpts) -> { any }

Put one or many entities ON named render layers — the membership half of the render-layer system, and the way to hide a wall, prop or whole building from a camera while it still exists for lighting, shadows and physics. Targets resolve by entity NAME or id, an array of them, scene.find records, or a QUERY resolved for you ({ name = { "wall" } }, { component = { "Light" } }, { under = { "building" } }) so you can re-layer things without looking their ids up first. Layers are given by NAME — a single name, an array of names, or a space-separated string like "shell debug" — never a bitmask. Pass tree = true to apply to each target AND every descendant, which is what you want when the thing you are re-layering is a hierarchy rather than a single mesh. Setting layers REPLACES an entity's membership rather than adding to it. An unrecognised option key is an error naming the accepted set, so a mistyped key never reads back as a successful no-op. Referencing a layer name that does not exist yet CREATES it, so a typo yields a real but empty layer that renders nothing and raises no error — renderLayer.list shows what actually exists. Returns { id, name, layers, moved, skipped } per TARGET: layers is what that entity now carries, moved counts the entities this call wrote through it — an entity two targets both span is written and counted once, so the counts sum to the entities the call moved — and skipped counts the descendants it left where they were. A root of 46 children re-layered on its own reports moved = 1, skipped = 46; the same call with tree = true reports moved = 47, skipped = 0, so the size of the write is in the reply rather than in a follow-up read. Pair it with renderLayer.camera, which chooses which layers a camera draws.

Parameters

  • targets Targets
  • layers LayerNames
  • opts SetOpts (optional)

Returns { any }

"frontWall", "shell"
{ "marker_a", "marker_b" }, "debug"
{ name = { "wall" } }, "shell"
"building", "shell", { tree = true }

tools/sc/addComponent

sc.addComponent(ids: IdList, typeName: string, data?: ComponentData)

Add component to one or many entities.

Parameters

  • ids IdList
  • typeName string
  • data ComponentData (optional)
"box", "Physics", { kind = "dynamic" }
ids, "Physics", { kind = "static" }

tools/sc/clear

sc.clear()

Clear scene (despawn all non-camera entities).

tools/sc/clearVisuals

sc.clearVisuals(ids: IdList)

Clear tint + outline from one or many entities.

Parameters

  • ids IdList
"box"
ids

tools/sc/despawn

sc.despawn(ids: IdList)

Despawn one or many entities.

Parameters

  • ids IdList
"box"
ids

tools/sc/move

sc.move(ids: IdList, dx: number, dy: number, dz: number)

Move entities by offset with smooth visual transition.

Parameters

  • ids IdList
  • dx number
  • dy number
  • dz number
"box", 0, 1, 0
ids, 0, 5, 0

tools/sc/outline

sc.outline(ids: IdList, r: number, g: number, b: number, intensity?: number)

Set outline on one or many entities.

Parameters

  • ids IdList
  • r number
  • g number
  • b number
  • intensity number (optional)
"box", 1, 1, 0
ids, 1, 0, 0, 2

tools/sc/quantize

sc.quantize(ids: IdList, opts?: QuantizeOpts)

Snap transform to grid. Each axis is optional.

Parameters

  • ids IdList
  • opts QuantizeOpts (optional)
"box", { position = 0.5 }
ids, { rotation = 45 }

tools/sc/removeComponent

sc.removeComponent(ids: IdList, typeName: string)

Remove component from one or many entities.

Parameters

  • ids IdList
  • typeName string
"box", "Physics"
ids, "BoxCollider"

tools/sc/replaceWithAsset

sc.replaceWithAsset(target: string, source: AssetRef<bundle|mesh>, opts?: ReplaceWithAssetOpts) -> string

Replace an already-spawned placeholder entity with a real asset, in place — keeping its position, rotation, scale, name, and parent. Removes the placeholder (and its whole subtree) and spawns the asset in its slot. The everyday "I blocked this out, now drop in the generated (or library) asset" move — so a scene built from rough stand-ins becomes the real thing without re-laying-out anything.

Parameters

  • target string
  • source AssetRef<bundle|mesh>
  • opts ReplaceWithAssetOpts (optional)

Returns string

"rider_blockout", "/zero/source/generated/meshes/a_snowboarder.bundle"
"tree_box", "@libname::pine.bundle", { keepScale = false }

tools/sc/setParent

sc.setParent(ids: IdList, parentId: string | EntityRef)

Parent one or many entities to a single parent.

Parameters

  • ids IdList
  • parentId string | EntityRef
"hat", "player"
ids, "torso"

tools/sc/spawnCamera

sc.spawnCamera(name: string, ...) -> string

Spawn a camera at position.

Parameters

  • name string

Returns string

"main_cam", 0, 5, 10, { fov = 60 }

tools/sc/spawnCircle

sc.spawnCircle(model: AssetRef<bundle|mesh>, name: string, count: number, opts?: CircleOpts) -> { string }

Spawn entities in a circle. Returns array of IDs.

Parameters

  • model AssetRef<bundle|mesh>
  • name string
  • count number
  • opts CircleOpts (optional)

Returns { string }

"sphere", "pillar", 8, { radius = 10 }

tools/sc/spawnGrid

sc.spawnGrid(model: AssetRef<bundle|mesh>, name: string, cols: number, rows: number, opts?: GridOpts) -> { string }

Spawn a grid of entities. Returns array of IDs.

Parameters

  • model AssetRef<bundle|mesh>
  • name string
  • cols number
  • rows number
  • opts GridOpts (optional)

Returns { string }

"cube", "tile", 5, 5, { spacing = 2, y = 0.5 }

tools/sc/spawnLine

sc.spawnLine(model: AssetRef<bundle|mesh>, name: string, count: number, opts?: LineOpts) -> { string }

Spawn entities in a line. Returns array of IDs.

Parameters

  • model AssetRef<bundle|mesh>
  • name string
  • count number
  • opts LineOpts (optional)

Returns { string }

"cube", "wall", 10, { spacing = 1 }
"cube", "col", 5, { spacing = 1, dirY = 1 }

tools/sc/spawnModel

sc.spawnModel(name: string, source: AssetRef<bundle|mesh>, ...) -> string

Spawn entity with model at position. Includes static Physics by default unless opts.physics overrides it.

Parameters

  • name string
  • source AssetRef<bundle|mesh>

Returns string

"box", "plain_name", 0, 2, 0
"crate", "cube", 0, 0, 0, { scale = { 6, 3, 1 }, rotation = { 0, 45, 0 } }
"player", "@libname::identity.in.library", 0, 0, 0
"player", "@identity.of.asset", 0, 0, 0
"player", "path/to/asset.bundle", 0, 0, 0

tools/sc/spawnPhysics

sc.spawnPhysics(name: string, model: AssetRef<bundle|mesh>, ...) -> string

Spawn entity with model + dynamic physics. Shorthand for sc.spawnModel with physics = "dynamic".

Parameters

  • name string
  • model AssetRef<bundle|mesh>

Returns string

"ball", "sphere", 0, 5, 0

tools/sc/spawnText

sc.spawnText(name: string, content: string, ...) -> string

Spawn 3D text at position.

Parameters

  • name string
  • content string

Returns string

"title", "Hello!", 0, 3, 0, { color = "yellow", fontSize = 48 }

tools/sc/tint

sc.tint(ids: IdList, r: number, g: number, b: number, blend?: number)

Set tint color on one or many entities.

Parameters

  • ids IdList
  • r number
  • g number
  • b number
  • blend number (optional)
"box", 1, 0, 0
ids, 0, 1, 0, 0.8

tools/sc/transform

sc.transform(ids: IdList, opts?: TransformOpts)

Set transform on one or many entities. All fields optional. Smooth visual transition by default.

Parameters

  • ids IdList
  • opts TransformOpts (optional)
"box", { position = {5, 0, 0} }
"box", { scale = 2, position = {0, 5, 0} }
ids, { scale = 0.5 }
ids, { pos = {0, 0, 0}, smooth = 0 }

tools/sc/unparent

sc.unparent(ids: IdList)

Unparent one or many entities (make roots).

Parameters

  • ids IdList
"hat"
ids

tools/scene/clean

scene.clean(opts?: CleanOpts) -> CleanResult

Clear the active scene down to a protected keep-set. By default ONLY the player setup survives — every PlayerPrototype subtree (with its body/camera) and every PlayerSpawn — so a clean never orphans or destroys the player (a joining user's avatar replaces the default body, so the player must persist). keep additionally protects a CATEGORY of entities: { component = { "Light", "Sky" } } keeps lighting, { tag = { "landmark" } } keeps tagged entities, { under = "rig" } keeps a subtree. It selects a group, not one named entity. keepPlayer = false wipes the player too (a full empty). dryRun = true reports what would be removed/kept without mutating. The clear is a LIVE edit of the scene: scene.revert undoes it, and scene.save writes it into the scene's scene.json, which is the file zm add stages and zm commit records. If the scene has UNSAVED edits, clean REFUSES (it errors without changing anything): its removals would fold into those edits and no revert could restore the pre-clean state, so the unsaved work would be lost. There is no confirm/force flag — resolve the edits first with scene.save (keep them) or scene.revert (discard them), then clean. (The one exception is play mode running unpaused, where the dirty overlay is gated and untouched, so the guard does not apply.) Returns { removed, kept, intent }, with undo naming the calls that keep or drop the clear, plus a warning when the clean left the scene with no lights.

Parameters

  • opts CleanOpts (optional)

Returns CleanResult

-- clear everything except the player
{ keep = { component = { "Light", "Sky" } } }  -- also keep lighting
{ keepPlayer = false }  -- full empty
{ dryRun = true }  -- preview only

tools/scene/cost

scene.cost(reset?: boolean) -> { [string]: any }

Rank the loaded scenes by what their per-frame tick costs — the answer to "which scene is eating the frame time". A scene entrypoint's update / editorUpdate runs on the scheduler rather than as a component, so it is absent from profiler.scripts; this times it where it runs and attributes it to the layer that owns it. avgMs is the per-tick cost, which is the per-frame cost for a tick that runs every frame; totalMs is a SUM across the window since the last reset (or engine start), which window reports. A layer whose entrypoint declares no tick is absent — it costs nothing per frame. Answers in edit mode as well as play.

Parameters

  • reset boolean (optional)

Returns { [string]: any }

true

tools/scene/create

scene.create(name: string, template?: ("empty" | "flat" | "player" | "menu"), opts?: { startup: boolean? }) -> CreateCatalog | CreateResult

Create a NEW scene from a template and load it. template is typed — omit it to get the catalog of templates and what each contains; an unknown value errors with the valid choices. "empty" is a blank canvas; "flat" is ground + lighting, no player; "player" is ground + lighting + the default player (the canonical, reusable setup a joining user's avatar replaces — prefer this over hand-rolling a player); "menu" is lighting only, no ground, no player (title screens / UI). Pass opts.startup = true to also make the new scene the world's startup scene. The scene is minted as a fresh asset and loaded — a name a scene already stands at is refused, naming the path it stands at, since writing a template over it would load that scene rather than a new one.

Parameters

  • name string
  • template ("empty" | "flat" | "player" | "menu") (optional)
  • opts { startup: boolean? } (optional)

Returns CreateCatalog | CreateResult

"level_1", "flat"  -- ground + lighting, no player
"title", "menu", { startup = true }  -- a menu scene the world opens into
"sandbox"  -- returns the template catalog

tools/scene/find

scene.find(query: shared.FindQueryArg) -> shared.FindResult

Find entities across the whole live population: runtime clones, temporary, and detached-root entities included (it reads the flat entity query, which reaches the entities a walk from a scene root misses). Pick a search axis, each taking a LIST so many candidates resolve in one call: id (exact entity ids, the ids this tool itself reports, so a batch of them resolves in one call), name (entity name terms), component (component names/identities; entities carrying any), attribute (an attribute name + optional value). Multiple axes = union; each hit reports WHICH criterion matched so you learn the right wording. A key the query does not recognise is an error naming the axes and scopes it takes, so a misspelled axis never reads back as the whole scene. substring (default true) matches name parts case-insensitively, so a short term like "muzzle" reaches "muzzle_light_ent_620d…"; set false for exact. A name term carrying * or ? is matched as a GLOB over the whole name ("ik_*", "wall_?"), the same convention entity.find / entity.findAll take, in both modes, since * states a pattern either way. Entities marked internal are excluded unless includeInternal. Scope with roots (top-level only), maxDepth, under (only descendants of these entities), ancestorsOf (only ancestors of these), and additive (default false = active scene only; true also searches loaded overlay scenes). Every match carries id, name, parent, depth, components (the NAMES of its components), layer and matched. include makes the query ANSWER the question instead of handing back a list to interrogate one entity at a time: pass an array of projections and every match carries them. "transform" adds position, rotation, eulerAngles and scale as ARRAYS, the vector shape the scene and entityOps tools take and report: position = { x, y, z } read as position[1], position[2], position[3] (entity(id).position holds the same value keyed x, y, z), rotation = { x, y, z, w } the world quaternion, eulerAngles = { x, y, z } in degrees, scale = { x, y, z } the local scale. "componentValues" adds each component's LIVE field values keyed by component name (the values the entity holds now, which can differ from the type's declared defaults). "attributes" adds the entity's attributes as { name = value }. "scriptComponents" adds every script-component instance the live entity carries as { type, identity, instance }. Nothing is added unless include names it. An unrecognised projection is an error listing the accepted ones and naming the one to write instead: components is already on every match, and its live values are componentValues. The query can also be written under its parameter name with fields beside it: { query = { name = { "wall" } }, include = { "transform" } } searches the same as { name = { "wall" }, include = { "transform" } }. Returns every hit with its FULL canonical id + name.

Parameters

  • query shared.FindQueryArg

Returns shared.FindResult

"muzzle"
{ id = { "ent_5ce7e0a14d3f9b07" } }
{ id = ids, include = { "transform" } }
{ name = { "muzzle", "flash" } }
{ name = { "body" }, substring = false }
{ component = { "Light", "Camera" } }
{ name = { "wheel" }, under = { "car" } }
{ attribute = { name = "team", value = "red" } }
{ roots = true }
{ name = { "wall" }, include = { "transform" } }
{ component = { "Light" }, include = { "transform", "componentValues" } }
{ under = { "room" }, include = { "transform", "componentValues", "attributes", "scriptComponents" } }

tools/scene/list

scene.list(scope?: SceneListScope) -> SceneListResult

List the SCENES available to load — the world's own scenes and the builtin library scenes — so you can find a scene by name instead of guessing. scope filters: "world" (default, the scenes this world authored), "library" (builtin @builtin:: scenes — templates, demos, canonical setups), or "all". Each entry reports its name, identity, path, scope ("world"/"library"), and whether it is currently loaded / active. Load one with scene.load; the currently loaded LAYERS are scene.loadedLayers.

Parameters

  • scope SceneListScope (optional)

Returns SceneListResult

-- the world's own scenes
"library"  -- builtin templates / demos / canonical scenes
"all"

tools/scene/load

scene.load(name: string) -> LoadResult

Load a scene into the live world and make it the active scene. name is a scene NAME (see scene.list), a builtin identity (e.g. "@builtin::scenes.test_arena"), or a scene path. Loading a scene that is ALREADY loaded changes nothing — it is idempotent — so this is NOT how you re-run an edited scene: use scene.reload for that. Records name so a later scene.save with no name targets it.

Parameters

  • name string

Returns LoadResult

"main"
"@builtin::scenes.test_arena"

tools/scene/loadedLayers

scene.loadedLayers() -> { LoadedLayer }

The scene LAYERS loaded into the live scene right now — the root scene plus any additive overlays (editor UI, effect passes). Each layer is reported as { name, scene, status, visible, additive, root }: scene is the backing scene identity, status the load state, additive false marks the active ROOT scene and true an overlay layered over it. This is what is composing the live scene; to list the scenes you can LOAD (world + library), use scene.list.

Returns { LoadedLayer }

tools/scene/migrate_v6_to_v7

scene.migrate_v6_to_v7(opts: MigrateOpts) -> MigrateResult

Convert a v6 scene.json to v7 in place. Reads the scene's scene.json, decides the player intent from the v6 player.required_in_play flag ("spawns" when true, else "none"), removes the v6 player{}/camera{} blocks, sets the top-level string player intent, and — for "spawns" — injects the final-shape PlayerSetups/Spawns structure with per-scene-unique entity ids. format, lighting, and every existing entity are preserved. Idempotent: a scene already at version >= 7 returns "skipped"; a scene with a missing or > 7 version returns an "error" result without mutating. Also scans the sibling entrypoint.luau for references to the removed player/camera flow and returns them as review notes (the Luau is never rewritten).

Parameters

  • opts MigrateOpts

Returns MigrateResult

{ path = "/source/libs/@builtin/demos/minecraft/minecraft.scene/scene.json" }

tools/scene/observe

scene.observe() -> { [string]: any }

What every scene load did, and what each loaded scene costs in frame time. lastLoad is the most recent load's report: the scene it loaded, 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 it produced. layers is one row per loaded layer with what the engine attributes to it and whether it came up whole; cost is the per-frame tick cost of each layer whose entrypoint declares one, summed across the window window reports. A load that produced failures reports outcome = "partial" and a reason from a closed set. Answers in edit mode as well as play mode.

Returns { [string]: any }

tools/scene/player

scene.player(intent?: ("spawns" | "none"), roles?: PlayerRoles) -> PlayerReport

The scene's player setup, in one tool. No argument reports the root scene's intent and path plus every PlayerPrototype (with body/camera wiring) and PlayerSpawn; in play a prototype is the captured template each joining player is cloned from, listed with template = true. With no scene loaded as the root layer the report carries unloaded, saying so and how to load one, in place of intent and scene. intent is typed: "none" makes the scene playerless — every PlayerPrototype subtree and PlayerSpawn is removed; "spawns" gives it a player per joining user, authoring the canonical default setup (humanoid body, third-person camera, origin spawn) when the scene has no PlayerPrototype yet. Pass roles (implies "spawns") to reshape: body adopts an entity OR repoints the body's avatar from an avatar asset ref; camera wires an entity carrying a Camera (scoped OwnerOnly); behavior sets the player camera's Camera.behavior to any cameraBehavior component (built-in or your own); prototype picks one when the scene has several. Reuse this default setup — a joining player's avatar replaces the default body, so a hand-rolled player breaks multiplayer. Setting anything saves the scene.

Parameters

  • intent ("spawns" | "none") (optional)
  • roles PlayerRoles (optional)

Returns PlayerReport

-- report the current player setup
"none"  -- strip the player: no prototype, no spawns
"spawns"  -- player per joining user (authors the canonical setup if missing)
"spawns", { body = "@builtin::avatars.minimal_player" }  -- swap the avatar
"spawns", { behavior = "@builtin::controller.first_person" }  -- first-person player

tools/scene/reload

scene.reload(name?: string) -> ReloadResult

Unload and re-load a scene layer in place, running the scene's build.luau and its entrypoint again. scene.load of a scene that is already loaded and unchanged is an idempotent no-op, so editing either of them and loading it again rebuilds nothing; this re-runs them. The build runs against what it resolves right now, so a script whose inputs moved — a component that now exists, an asset that now resolves — produces the scene it describes today. Works in edit mode, so iterating on a build script does not mean flipping to play and back through the play shadow. 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; unsaved edits in the scene are saved with it. A library scene (under /libs/) is rebuilt in the live scene alone and its files stay as the library states them. A reload asked for while a rebuild of the scene is landing waits for it to land and save, then reloads.

Parameters

  • name string (optional)

Returns ReloadResult

"main"

tools/scene/remove

scene.remove(targets: Targets) -> RemoveResult

Remove entities from the active scene. Targets are entity NAMES or ids, arrays of them, scene.find records, or a scene.find QUERY ({ name = { "ground" } }, { component = { "Light" } }, { under = { "rig" } }) — so you delete a selection without looking it up first. Each target is removed with its whole subtree. The default floor is the entity named ground. The removal is a LIVE edit of the scene: scene.revert undoes it, and scene.save writes it into the scene's scene.json, which is the file zm add stages and zm commit records. To clear a scene while keeping the player setup, use scene.clean instead.

Parameters

  • targets Targets

Returns RemoveResult

"ground"  -- delete the default floor
{ "probe_a", "probe_b" }
{ component = { "PointLight" } }  -- delete every entity with that component

tools/scene/replace

scene.replace(source: string, opts?: ReplaceOpts) -> ReplaceReport

Replace a loaded scene's content with another scene's content. source is a scene name / path / identity string, resolved internally (the resolved identity is reported back). Every content file in the target scene's folder (scene.json, entrypoint.luau, ...) is overwritten with the source's copy; target content files the source doesn't have are removed; the target scene's identity (guid, name) is untouched. The scene then reloads so the world reflects the new content immediately. Targets the ACTIVE scene unless opts.scene names another loaded scene. dryRun = true reports exactly which files would be written and removed, and the entities the source declares, without touching anything.

Parameters

  • source string
  • opts ReplaceOpts (optional)

Returns ReplaceReport

"@builtin::scenes.test_arena"
"my_template", { dryRun = true }

tools/scene/revert

scene.revert(name?: string) -> RevertResult

Discard the active scene's UNSAVED edits and restore its saved scene.json. Every edit-mode change since the last scene.save — a scene.clean, a scene.remove, spawned/moved/deleted entities — lives in the scene's dirty overlay until you commit it; this throws that overlay away and respawns the scene from its saved state. It is the undo for scene.clean / scene.remove and the counterpart to scene.save. Pass name to revert a specific loaded scene; omit it for the active one. Reverts only the edit-mode dirty overlay — it is not the play-mode baseline restore.

Parameters

  • name string (optional)

Returns RevertResult

-- undo unsaved edits on the active scene
"main"  -- revert the loaded scene named 'main'

tools/scene/save

scene.save(name?: string) -> SaveResult

Save the loaded scene named name back to its own scene.json, writing the path synchronously. Resolves the scene by name via layers.find and saves THAT scene — so saving scene A never writes scene B. No-args form uses the toolbox's last scene name (default "main"). Errors when no scene by that name is loaded.

Parameters

  • name string (optional)

Returns SaveResult

-- save the last-used scene (or "main")
"main"  -- save the loaded scene named 'main'

tools/scene/setStartup

scene.setStartup() -> SetStartupResult

Make the CURRENTLY ACTIVE scene the world's startup scene — the scene the world auto-loads every time it loads. Takes no arguments: it pins whatever scene is live now, so after building or loading a scene you call this to make it stick across world reloads instead of the world snapping back to its previous startup. There is no clear/unset — a world with no startup scene has no scene context and every script fails. The current startup scene shows in scene.summary.

Returns SetStartupResult

-- pin the active scene as the world's entry point

tools/scene/spawns

scene.spawns(action?: SpawnAction, opts?: SpawnOpts) -> SpawnsResult

The scene's spawn points, in one tool — where joining players arrive, as opposed to scene.player, which decides whether the scene spawns players at all and what they become. No argument reports every PlayerSpawn with its world transform, the prototype it instantiates (and whether that reference still resolves), and its team / role / maxPlayers / spawnPolicy / placement. "add" places a new spawn: it takes the prototype named in opts.prototype, or the scene's only one, and is parented alongside the spawns already there so spawn points stay collected. "set" reconfigures an existing spawn — every field is optional and the ones you leave out keep their value. "remove" deletes one. opts.spawn names WHICH spawn ("set" / "remove"), or the new entity's name ("add"); with several spawns in the scene it is required, with one it is implied. A spawn's own transform is where a player's body lands while placement is at_spawn_transform, and its forward axis is the direction they face — the edit-mode overlay draws that ring, heading arrow and standing capsule so a spawn is visible where it stands. Mutating saves the scene.

Parameters

  • action SpawnAction (optional)
  • opts SpawnOpts (optional)

Returns SpawnsResult

-- report every spawn point in the scene
"add", { position = { 12, 0, -4 } }  -- a second spawn over there
"add", { spawn = "RedSpawn", position = { -8, 0, 0 }, rotation = { 0, 90, 0 }, team = "red" }
"set", { spawn = "DefaultSpawn", position = { 0, 0, 6 } }  -- move it
"set", { spawn = "RedSpawn", maxPlayers = 4 }
"remove", { spawn = "RedSpawn" }

tools/scene/summary

scene.summary(opts?: SummaryOpts) -> SceneSummary

Count the entities in the scene, or in a group with under (how many sit beneath it), in one short answer; and the scene overview around them: the root count, the top-level rows, the active camera, the PLAYER setup (intent + prototype/spawn counts, with the verb to change it), and the world's STARTUP scene. The player block is how you learn a scene's player type without knowing it exists: intent is "spawns" (a player per joining user) or "none" (playerless), changed with the scene.player tool. With no scene loaded as the root layer the block carries unloaded in place of intent, and changeWith names scene.load. roots carries at most limit rows (20 when omitted; limit = 0 answers the counts, camera, player and startup scene alone), and truncated says when rows were left out; rootCount is always the full count, scene.find with roots set lists them all and scene.tree draws the structure. under scopes the counts and rows to the subtrees of the entities it names, the way scene.find's under does: entityCount is then the number of entities beneath them and roots their children. fields keeps only the named fields of the answer, so { fields = { "entityCount" } } answers the count alone.

Parameters

  • opts SummaryOpts (optional)

Returns SceneSummary

{ limit = 0 }
{ under = "level_1", limit = 5 }
{ fields = { "entityCount" } }
{ under = "level_1", fields = { "entityCount" } }

tools/scene/tree

scene.tree(target?: TreeTarget, opts?: TreeOpts) -> TreeResult

Render the entity hierarchy as an indented tree — the quick "what is the current structure" view. Shows each entity's name, its component types, and its children down to depth levels; anything deeper (or beyond the node budget) is summarized as a count instead of silently dropped. With no target it draws the whole active world from its roots; a target (id, name, proxy, or an array) draws just those subtrees.

Parameters

  • target TreeTarget (optional)
  • opts TreeOpts (optional)

Returns TreeResult

'player'
nil, { depth = 2 }

tools/scene/whyPartial

scene.whyPartial(scene?: string) -> { [string]: any }

Answer why a loaded scene layer is not whole. reason is the nearest cause from a closed set — loaderRaised, entrypointCompileFailed, entrypointBodyRaised, entrypointRaised, buildRaised, entityFailed, parentMissing, parentRefused, parentAbandoned, componentUnresolved, componentRefused, subscriberRaised, updateRaised — so it names the thing to fix. failures carries every one of them, each with the entity, component identity or lifecycle hook it is about and the engine's own message. A layer that produced everything its scene declared reports ok = true with no reason.

Parameters

  • scene string (optional)

Returns { [string]: any }

"scenes.main"

tools/sceneAuthoring/acceptChanges

sceneAuthoring.acceptChanges(selection?: shared.Selection, opts?: AcceptOpts) -> AcceptResult

Accept changes shown by tools.use("sceneAuthoring", "changes") — all of them, or a selection. Writes the scene when the verdict carries something the scene records, saves the play-created assets the accepted entities reference into the world's source, saves play-edited source files when included (group "edits"). The session keeps running throughout, so a verdict on a shared world leaves every other agent on it working. A partial accept leaves the rest live — the next changes() shows the remainder. A selection naming only source files settles those files and leaves the scene exactly as it stands, bytes, checksum and dirty state alike. If another editor changed a record since the review was taken, the accept ABORTS with a conflict report (nothing written) unless opts.onConflict is "mine" or "theirs".

Parameters

  • selection shared.Selection (optional)
  • opts AcceptOpts (optional)

Returns AcceptResult

{ "tile_", "hero", "edits" }
"my_component/init.luau"
nil, { onConflict = "mine" }

tools/sceneAuthoring/changes

sceneAuthoring.changes(opts?: ChangesOpts) -> string

Review what exists live but is not yet part of the scene: entities you spawned (clustered: one spawn batch is one decision), edits to existing scene entities (with field-level diffs), the assets those changes reference (flagging play-created ones and reload-broken orphans), source files edited during play, and session post-process effects and UI screens. Composed within the frame it is called in, so it describes that frame and the session keeps running while every other agent on this world keeps working; writes NOTHING. The scene only ever changes via tools.use("sceneAuthoring", "acceptChanges"); drop changes with tools.use("sceneAuthoring", "rejectChanges"). The listing is bounded: it shows at most limit numbered items (20 when omitted; limit = 0 answers the counts per kind alone), kind lists only items of one kind and author only one author's, and a listing that leaves items out says how many and how to list them. Every item keeps its number and stays addressable by a verdict whether it is shown or not. reviewState answers the same counts as a table.

Parameters

  • opts ChangesOpts (optional)

Returns string

{ limit = 0 }
{ kind = "updated", limit = 10 }
{ kind = { "edits", "foreignEdits" }, author = "others" }
{ as = "checkpoint_1" }

tools/sceneAuthoring/rejectChanges

sceneAuthoring.rejectChanges(selection?: shared.Selection) -> RejectResult

Reject changes shown by tools.use("sceneAuthoring", "changes") — all of them, or a selection. Rejected entity changes never enter the scene; the live objects stay live for this session and are hidden from later reviews. Each rejected change is taken back out of the scene's pending set, so the scene stops carrying it and the publication gate that refuses a commit over unbaked entity edits reads it as settled. A rejected REMOVAL keeps the entity in the scene: the despawn holds for this session and the entity is there again on the next load. Rejected file edits (group "edits", or individual paths) are REVERTED: each file is restored to its pre-play state (the last edit-mode content, unstaged edits included; a file created during play is removed), hot-reload picks the original back up, and the edit no longer blocks leaving play. The session keeps running throughout, so a verdict on a shared world leaves every other agent on it working.

Parameters

  • selection shared.Selection (optional)

Returns RejectResult

"tile_"
"my_component/init.luau"

tools/sceneAuthoring/reviewState

sceneAuthoring.reviewState() -> ReviewState

What is under review right now, and how much of it: the entities the scene does not have or holds differently, the ones despawned this session, the source files edited during play, and the effects, screens and features registered from execute. On an engine several agents drive at once, edits counts this agent's own in-play source edits and foreignEdits counts a co-author's — both hold the play-exit gate, and only the first is what a bare acceptChanges or rejectChanges settles. The counts come from the same survey of the live world tools.use("sceneAuthoring", "changes") renders and the leave-play safeguard enforces, so an agent deciding whether it has work to keep reads the same answer all three give. open is true while anything is pending, which is when acceptChanges and rejectChanges have something to settle. Every count is zero and open is false once a verdict has settled the lot, and while no scene is loaded as the root layer. The engine keeps running across the call.

Returns ReviewState

-- branch instead of relying on the refusal
-- local s = tools.use("sceneAuthoring", "reviewState")
-- if s.open then tools.use("sceneAuthoring", "changes") end

tools/services/attach

services.attach(jobId: string) -> AttachedJob

Read a generation's gateway job by its jobId (the jobId on a services.status / services.jobs row). 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. Returns { jobId, status, result?, error?, upstreamStatus? }: status is pending / running / succeeded / failed, and a succeeded job's result is the provider's response — the URL or bytes the run would have downloaded. 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.

Parameters

  • jobId string

Returns AttachedJob

"01M12BH4P0C1CVXMN3QX9JEJKZ"

tools/services/balance

services.balance() -> (BalanceReport?, string?)

What you can spend on a generation. Compare an operation's cost (from services.list) against spendable — that is the binding number. pool is the account's whole balance and agentRemaining is what is left of your own allocation when you work under one; whichever is smaller is what spendable reports, because an allocation that is spent stops a generation however large the pool is. Returns (report, nil) when signed in, (nil, reason) otherwise.

Returns (BalanceReport?, string?)

tools/services/describe

services.describe(options?: { [string]: any }) -> DescribeReport

Describe your assets: find the descriptions they are missing, or write 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. { mode = "find" } (the default) lists the assets that lack one, the file and keys each needs, and how many calls filling them makes, and writes nothing. { mode = "all" } fills every asset that listing names, and { assets = { ... } } fills the assets you name. By default it takes what blocks the current push; scope = "world" takes every asset your world holds that a type asks to describe. A call costs 1 credit and covers many assets: they are packed whole into as few calls as fit, several calls run at once, and each asset comes back written, failed or skipped, with the reason for the last two. An answer that is missing for one asset fails that asset only. The credits for the whole fill are checked before the first call. What it writes is an ordinary file in the asset that you can edit. It writes only the description file or keys the type declares, never replaces text you wrote, and never touches library or installed content, /tmp or /local. instructions is added to what the model is told, and examples names assets whose descriptions show the shape you want.

Parameters

  • options { [string]: any } (optional)

Returns DescribeReport

{ mode = "find" }
{ mode = "all" }
{ assets = { "Spinner", "Door" }, instructions = "Mention the multiplayer sync." }

tools/services/generate

services.generate(service: string, input?: GenerateInput, operation?: string) -> GenerateHandle

Start a generation. service is a name from services.list; input is that operation's inputs (e.g. { prompt = "a wooden treasure chest" }). Returns immediately with { id, service, operation } — generation takes minutes and runs in the background, surviving this call. When it ends it posts a notice, "generation finished" naming the id and the asset to spawn, or "generation failed" naming the id and the error, delivered on your next tool call; services.status(id) reads its progress meanwhile. Consumes credits (see the operation's cost in services.list).

Parameters

  • service string
  • input GenerateInput (optional)
  • operation string (optional)

Returns GenerateHandle

"mesh_gen", { prompt = "a wooden chest" }
"audio_gen", { operation = "sfx", prompt = "a heavy wooden door slamming" }
"audio_gen", { text = "Welcome aboard." }, "speech"
"image_gen", { prompt = "a mossy stone idol", asset_path = "/zero/source/game/art/idol.png" }
"image_gen", { prompt = "a mossy stone idol", durable = true }

tools/services/jobs

services.jobs(opts?: JobsOpts) -> JobsListing

List 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: jobs carries at most limit rows (20 when omitted; limit = 0 answers the counts alone), total is how many jobs matched, truncated says rows were left out and more says how to read them. 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; byStatus and byService count the matching jobs. Each row is the shape of services.status, { id, service, operation, status, progress, asset, error, startedAt, ... }, carrying promptPreview (the prompt's first 60 characters) in place of the full prompt; prompts = true carries the full prompt, and services.status(id) reads one job with it. For the durable, cross-session record of generated assets, use services.outputs.

Parameters

  • opts JobsOpts (optional)

Returns JobsListing

{ status = "active" }
{ limit = 0 }
{ service = "mesh_gen", limit = 5 }
{ status = "failed", prompts = true }

tools/services/list

services.list() -> { ServiceEntry }

List the generation services you can use — the things you can generate (3D meshes, textures, audio, …) by spending credits. Fully self-describing: each service entry is { name, description, default, operations }, and each operation is { name, description, inputs, produces, cost } — inputs is an ordered array of { name, type, required, desc, enum, min, max, dimensions, default, framework }, produces is the kind of asset you get back, cost is credits per call. An input carrying enum accepts those values and no others, one carrying min / max accepts numbers inside that span, one carrying dimensions accepts a WIDTHxHEIGHT size inside the bounds it names (or a word its also lists), and one carrying default uses that when you omit it. An input marked framework = true belongs to the call rather than to the operation: operation names which of the service's operations to run (required where the service declares no default), and asset_path names where the run writes what it generates. Every input listed here — framework or declared — goes in the one input table. An entry's default is the operation a call runs when it names none, so a service listing several operations says which of them a bare call reaches. The entry point: call this first, then services.generate(service, input) with what you learn here.

Returns { ServiceEntry }

tools/services/outputs

services.outputs() -> { GeneratedOutput }

List the assets generated by services in this world — the durable record that survives restarts (the in-flight job records in services.jobs do not). Reads the provenance stamped on each produced asset. Each row: { asset, service, operation, prompt, generatedAt }, where asset is the spawnable asset path. Use this to find what you've generated across sessions.

Returns { GeneratedOutput }

tools/services/status

services.status(id: string) -> JobStatus?

Read one generation job's current status by its handle id (the id from services.generate). Returns { id, service, operation, prompt, status, progress, asset, error, durability } or nil if the id is unknown. durability is where what the run filed (its asset and its run record) stands as of this read: durable, and when the play shadow holds any of it, the warning, the routes that keep it in playShadow, and the paths in shadowed; a play-exit discard takes those back unless they are kept. status moves through the operation's stages to "completed" or "failed"; asset is the spawnable asset path once "completed". A call the provider refused for the moment (a 429 while it rate limits) is sent again after a wait, and while it waits status names the refusal and the attempt coming next ("generate: retrying after upstream 429: ... (attempt 2 of 6)"); each attempt is a gateway job of its own, so jobId names the latest. A run whose attempts all end refused reads "failed" with the provider's status and reason in error. A run that ends posts a notice naming its id and its asset or error ("generation finished" / "generation failed"), delivered on the next tool call; this reads the same outcome on demand, and its progress while the run is still going.

Parameters

  • id string

Returns JobStatus?

g.id

tools/show/close

show.close(opts: { panel: string }) -> CloseResult

Close a panel opened with show.file or show.text — the same as the user closing its tab. A panel the user already closed answers closed = false. A panel holding the user's unsaved edits stays open: the user is asked to save or discard them, and pending says so.

Parameters

  • opts { panel: string }

Returns CloseResult

{ panel = "show:3" }

tools/show/file

show.file(opts: { path: string, line: number?, title: string? }) -> ShownPanel

Open a file, folder or asset in the editor so the user can read or review it, in a new, closable panel beside whatever they have open — the viewer they would get by navigating to it themselves: code and text in an editor, markdown rendered, an image drawn, an asset with a preview (bundle, mesh, material, effect, avatar, …) rendered live. The panel is editable where the file is text and follows the file on disk. path is a VFS path, or an asset name, identity or guid.

Parameters

  • opts { path: string, line: number?, title: string? }

Returns ShownPanel

{ path = "/zero/source/modules/inventory.module/init.luau", line = 40 }
{ path = "checkerboard_white", title = "Checkerboard" }

tools/show/list

show.list() -> { ShownRow }

The panels opened to show the user something that are open now, in the order they were opened. dirty marks one where the user has unsaved edits; a panel they closed is gone from the list.

Returns { ShownRow }

tools/show/text

show.text(opts: { content: any, format: string?, title: string? }) -> ShownText

Show the user text you produced — a write-up, a plan, code, a JSON value — in the editor, in a new, closable panel beside whatever they have open, where they can read, review and edit it. format is the file extension that picks the viewer: md renders markdown, json pretty-prints, luau / yaml / wgsl highlight, txt is plain. A table is pretty-printed as JSON. The text is written to a scratch file under /zero/source/tmp/show/ (never saved or synced); the panel is editable, so read path back to see what the user changed.

Parameters

  • opts { content: any, format: string?, title: string? }

Returns ShownText

{ content = "# Plan\n\n1. Split the loader\n2. Cache the atlas", format = "md", title = "Plan" }
{ content = { score = 12, items = { "sword", "key" } }, title = "Save state" }

tools/skills/invoke

skills.invoke(name: string) -> string

Open a skill and return everything needed to act on it: its instructions, the assets and guides it depends on (each marked present or missing in this world), the toolboxes and tool names it runs through with their live signatures, and the subskills available under it. Address a top-level skill by name ("scenes") and a subskill through its parent ("scenes/player-setup") — a subskill opens directly, without opening its parent first. A name that matches nothing returns the available names. The skill stays open afterwards — it and its subskills ride your tool responses, each marked as you open it — until skills.release puts it down.

Parameters

  • name string

Returns string

"scenes"

tools/skills/list

skills.list(scope?: ("builtin" | "world" | "library")) -> SkillListing

List the skills this world knows — every top-level skill with the one-line description it advertises itself by, ordered built-in first then by name. Each row's name is the address skills.invoke takes, and open says whether you already have that skill open. Subskills are not listed: they are reachable only through their parent, and the parent's invoke result names them. open at the top level carries every address you have open, subskills included.

Parameters

  • scope ("builtin" | "world" | "library") (optional)

Returns SkillListing

tools/skills/release

skills.release(name?: string) -> ReleaseResult

Put a skill down: stop it and its subskills riding your tool responses. Releasing a parent releases the subskills opened under it, since those are passes within the same job. Pass no name to release everything you have open. Naming a skill that is not open returns the ones that are.

Parameters

  • name string (optional)

Returns ReleaseResult

"scenes"

tools/temp/run

temp.run(code: string, duration?: number) -> RunResult

Execute Luau code and auto-cleanup the entities it creates after a timeout. Snapshots existing entity IDs via ecs.entities() before running the code, compiles the code with loadstring, executes it under pcall, diffs the post-execution entity set against the pre-set to find newly-created entities, then schedules a deferred cleanup pass that despawns each created entity. Useful for temporary visualizations, test setups, and previews that should not leak.

Parameters

  • code string
  • duration number (optional)

Returns RunResult

'sc.spawnModel("test", "sphere", {0, 3, 0})'
[[ sc.spawnGrid("cube", "preview", 3, 3, { spacing = 2, y = 1 }) ]], 10

tools/tests/compare

tests.compare(before: string | Report, after?: (string | Report)) -> Diff

Diff two test reports and report NEW failures (regressions), fixes, and added/removed tests. The headline is newFailures — tests failing now that passed (or didn't exist) in the baseline.

Parameters

  • before string | Report
  • after (string | Report) (optional)

Returns Diff

"/source/before.json"
"/source/before.json", "/source/after.json"

tools/tests/list

tests.list(opts?: (ListOpts | string)) -> ListResult

List discovered test suites (registrable .testSuite assets). Scoped to scope = "user" (world-authored) by default — the same default as tests.run; pass scope = "all" to include the baked @builtin library. Optionally load each suite to also report its test count.

Parameters

  • opts (ListOpts | string) (optional)

Returns ListResult

{ filter = "vfs", details = true }

tools/tests/loadHttp

tests.loadHttp(opts?: LoadOpts) -> (LoadResult | LoadHandle)

Fetch the engine's test content over HTTP and materialize it into the @builtin library so tests.list / tests.run discover the suites. For the browser/WASM test runner, where no build embeds the test content. Every load materializes the fixtures and packages the suites share, and every suite under a folder a picked suite names on a --!needs <folder> line of its init.luau (a library-relative folder).

Parameters

  • opts LoadOpts (optional)

Returns (LoadResult | LoadHandle)

-- browser default: async task
{ async = false }  -- block, return counts
{ suites = { "sequences" } }  -- just this suite

tools/tests/run

tests.run(opts?: (RunOpts | string)) -> (RunResult | AsyncHandle)

Run the engine test suite. A bare tests.run() runs every world-authored suite (scope = "user", the default) — never the baked @builtin library a content session can't edit; tests.run("*") runs EVERYTHING (scope "all"); tests.run("name") runs one suite (an explicit name bypasses scope); a table form { suite?, suites?, scope?, save?, format?, quiet?, async?, measurements?, seed?, haltOnLeak?, onWorker? } gives full control. Each suite runs through its own AssetRef:run() inside a task, so the engine stays responsive. Always writes /source/tmp/test_results.{md,json} (under /source/tmp/, which is excluded from world saves, so reports never sync). With async = true the whole run is spawned as one task and the call returns { taskId, reportMd, reportJson } immediately — watch /runtime/tasks/{completed,failed}/<taskId> and read the report when it lands. Refused in play mode.

Parameters

  • opts (RunOpts | string) (optional)

Returns (RunResult | AsyncHandle)

"*"
"vfs"
{ async = true }  -- watch /runtime/tasks/<h.taskId>

tools/viewport/setSize

viewport.setSize(width: number, height: number) -> sizeTool.Size

Draw at this many pixels for the rest of the call, and report what the engine measures at that shape. The surface is resized and everything measured against it follows: getViewportSize(), ui.screenSize(), the layout every UI screen rebuilds from it, each camera's aspect, and what a capture taken in the same call encodes. This checks a responsive layout at a second shape inside one session: the UI tree is REBUILT at the new size, so a branch taken on the aspect ratio and every absolute number computed from the short edge are the ones that shape produces, which is the half a resized capture cannot reach. The call waits until the surface reports the size (or the window manager has answered) and returns the reading viewport.size() gives there. The size is held for the call that asked for it: when that call ends the surface goes back to the size it had before, so the window or browser canvas a person is looking at is handed back as it was. To measure a layout or capture it at the shape, do it in the same execute as this call. The logical UI space is normalised to about 1280 points wide, so a resize to the same aspect moves pixelRatio and leaves ui.screenSize() where it was, while a resize that changes the aspect changes that shape too. An OS window's size belongs to the window manager, which has the last word; the reading reports what was realized. A size larger than the device draws is refused naming the bound.

Parameters

  • width number
  • height number

Returns sizeTool.Size

1920, 1080
1080, 1440 -- portrait, same session

tools/viewport/size

viewport.size() -> Size

What the engine is drawing at right now. width/height are the drawing surface in pixels — the whole window or canvas, which is what viewport.setSize sets and what a UI screen is laid out over — read from the renderer's own configuration, so they are the size in force on every layout and aspect is the shape the engine is really drawing. logicalWidth/logicalHeight are the logical points ui.screenSize() reports and every widget position is measured in, and pixelRatio is the factor between the two spaces, so a measurement taken in one can be carried to the other instead of re-photographed. The logical space is the one the last laid-out screen used: it follows the surface while a screen is registered, and with none registered it reports the space the UI layer was constructed with, so register a screen before reading it. The logical canvas is normalised to about 1280 points wide, so changing the surface to a size of the SAME aspect moves pixelRatio and leaves the logical shape where it was, while changing the aspect changes the logical shape. sceneWidth/sceneHeight are the rect the 3D scene is drawn into and what getViewportSize() reports: the whole surface in a runtime window or a headless engine, and the editor's viewport panel — smaller than the surface — in an editor layout. maxExtent is the largest surface this device draws per axis, the bound viewport.setSize refuses past. Use viewport.setSize to draw at another size.

Returns Size

tools/voxelConfig/all

voxelConfig.all() -> { [string]: any }

Return a deep copy of the fully-merged voxel-engine config — built-in defaults, overlaid with WORLD_SETTINGS.[voxel].*, overlaid with runtime overrides installed via voxelConfig.set. Mutating the result has no effect on live state.

Returns { [string]: any }

tools/voxelConfig/get

voxelConfig.get(key: string) -> any

Read a voxel-engine config value by dotted key. Resolution order: runtime overrides first, then WORLD_SETTINGS.[voxel].*, then built-in defaults. Raises if the key is unknown.

Parameters

  • key string

Returns any

"paths.shapes"
"chunkSize"

tools/voxelConfig/reset

voxelConfig.reset(key?: string)

Clear one or all runtime overrides installed via voxelConfig.set. With a key, clears just that key; with no argument, clears every runtime override. Raises if key is a string but not a known key.

Parameters

  • key string (optional)
"paths.shapes"  -- revert a single override
-- revert every runtime override

tools/voxelConfig/set

voxelConfig.set(key: string, value: any)

Install a runtime override for a voxel-engine config key. Runtime overrides take precedence over WORLD_SETTINGS.[voxel] and built-in defaults. Raises if the key is unknown.

Parameters

  • key string
  • value any (optional)
"paths.shapes", "/my/game/voxel/props"
"chunkSize", 16

tools/voxelEngine/composite

voxelEngine.composite(opts: CompositeOpts)

Kitbash new props from existing templates. Builds a composite with per-child offset + rotation, then saves in one of three forms: (a) flattened .voxbin via op='saveBaked', (b) live entity hierarchy via op='explode', (c) reusable asset bundle via op='saveAsBundle'. Handles are plain Lua tables returned by op='new'; they carry their own methods (:addChild, :saveBaked, :explode, ...) so callers can compose fluently or route every step through this tool.

Parameters

  • opts CompositeOpts
{ op = 'new', name = 'car' }
{ op = 'addChild', handle = c, template = 'car_body' }
{ op = 'addChild', handle = c, template = 'car_wheel', offset = {2, 0, 2} }
{ op = 'saveBaked', handle = c }
{ op = 'saveAsBundle', handle = c, bundleName = 'my_vehicle' }

tools/voxelEngine/destroy

voxelEngine.destroy(target?: (Handle | string))

Despawn a voxel entity (world or shape). Single-purpose generic tool — target is a handle, "world", an entity id, or an entity name. Shape handles return their componentName = "VoxelShape"; world target "world" resolves the singleton.

Parameters

  • target (Handle | string) (optional)
'world'
myShapeHandle
'ent_42'

tools/voxelEngine/edit

voxelEngine.edit(opts: EditOpts) -> any

Generic mutating block operations on worlds or shapes. One tool dispatches to every write op — setBlock, fills, mirror, translate, pasteRegion, flush, clear. Pick behavior via opts.op, target via opts.target. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.

Parameters

  • opts EditOpts

Returns any

{ op = 'set', target = 'world', x = 0, y = 0, z = 0, block = 'stone' }
{ op = 'fillBox', target = 'myShape', min = {0,0,0}, max = {3,3,3}, block = 'red' }
{ op = 'mirror', target = shape, axis = 'x' }

tools/voxelEngine/palette

voxelEngine.palette(opts: PaletteOpts) -> any

Palette operations for voxel worlds or shapes. One tool dispatches to set / add / list. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.

Parameters

  • opts PaletteOpts

Returns any

{ op = 'set', target = 'world', palette = { grass = {0.2,0.7,0.2}, dirt = {0.5,0.3,0.15} } }
{ op = 'add', target = myShape, name = 'gold', color = {1.0,0.9,0.2} }
{ op = 'list', target = 'world' }

tools/voxelEngine/persist

voxelEngine.persist(opts: PersistOpts) -> any

Voxbin + template lifecycle ops. One tool dispatches to save / bake / saveAsTemplate / deleteTemplate / copyTemplate / captureArea. Target is resolved via shared.resolveTarget where applicable.

Parameters

  • opts PersistOpts

Returns any

{ op = 'saveAsTemplate', target = myShape, name = 'redcube' }
{ op = 'captureArea', box = { min={0,0,0}, max={16,4,16} }, saveAs = 'scene1' }
{ op = 'deleteTemplate', name = 'obsolete' }

tools/voxelEngine/query

voxelEngine.query(opts: QueryOpts) -> any

Generic read-only query operations on voxel worlds or shapes. One tool dispatches to every read op — block lookup, neighbors, region iteration, count by name, raycast, sphere/line queries, region copy, bounds, list templates. Pick behavior via opts.op. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.

Parameters

  • opts QueryOpts

Returns any

{ op = 'getBlock', target = 'world', x = 0, y = 0, z = 0 }
{ op = 'raycast', target = 'world', origin = {0,10,0}, dir = {0,-1,0}, maxDist = 100 }
{ op = 'listTemplates' }

tools/voxelEngine/spawn

voxelEngine.spawn(opts: SpawnOpts) -> Handle | { Handle }

Generic voxel entity creation + placement. One tool, many scenarios — pick behavior via opts.op. Covers world creation, single-shape creation, template instantiation, handle cloning, bulk scatter across a region, and single stamped placement. Returns a handle (or array of handles for scatter).

Parameters

  • opts SpawnOpts

Returns Handle | { Handle }

{ op = 'world', size = {32,16,32}, palette = { grass = {0.2,0.7,0.2} } }
{ op = 'shape', size = {4,4,4}, palette = { red = {1,0,0} }, position = {0,2,0} }
{ op = 'template', name = 'redcube', position = {10,0,0} }
{ op = 'scatter', template = 'redcube', region = { box = { min={0,0,0}, max={20,0,20} } }, count = 8, seed = 42 }

tools/voxelEngine/transform

voxelEngine.transform(opts: TransformOpts) -> any

Generic entity-transform + snap operations for voxel entities (worlds or shapes). One tool dispatches to setPosition / setRotation / setScale / setParent / snapToWorld plus getter ops getPosition / getRotation. Target is resolved via shared.resolveTarget and defaults to the singleton VoxelWorld.

Parameters

  • opts TransformOpts

Returns any

{ op = 'setPosition', target = myShape, x = 5, y = 0, z = 5 }
{ op = 'setRotation', target = myShape, x = 0, y = 1.57, z = 0 }
{ op = 'snapToWorld', target = myShape }

tools/wld/edit

wld.edit() -> EditResult

Return to edit mode. Leaving play would lose live changes the scene doesn't carry, so the engine's safeguard refuses while any of them are unaccepted, and names what they are. List them with the sceneAuthoring.changes tool and settle them with sceneAuthoring.acceptChanges or sceneAuthoring.rejectChanges — either takes everything or a selection — then call this again. engine.discardPlayChanges(), called inside the play session, throws them away instead, and the next call flips. The flip rebuilds the scene for edit over the frames after it, and the call returns once that rebuild has ended.

Returns EditResult

tools/wld/hideLayer

wld.hideLayer(name: string) -> ()

Hide a scene layer. Keeps the layer's entities in the world (they are not despawned) but stops them rendering. Use wld.showLayer to make them visible again, or wld.unloadLayer to despawn them.

Parameters

  • name string

Returns ()

"ui"

tools/wld/info

wld.info() -> WorldInfo

Return a snapshot of the current world's high-level state — the bound world name, current mode ("edit" / "play"), the active scene layer, and the list of saved scene snapshots in this world.

Returns WorldInfo

tools/wld/listWorlds

wld.listWorlds() -> { string }

List worlds saved on disk. Reads /zero/worlds via the VFS and returns each directory entry's name. Empty array if no worlds have been persisted.

Returns { string }

tools/wld/loadLayer

wld.loadLayer(name: string, opts?: LoadLayerOpts) -> SceneLayer

Load a scene as an additive layer alongside the current scene: layers.load(name, opts) with additive = true. The current scene stays the root.

Parameters

  • name string
  • opts LoadLayerOpts (optional)

Returns SceneLayer

"ui"
"props", { origin = {0, 0, 10} }

tools/wld/mode

wld.mode() -> "edit" | "play"

Get the engine's current mode. Returns "edit" when in edit mode (scripts + physics paused), "play" when in play mode. Inverse pair: wld.play / wld.edit.

Returns "edit" | "play"

tools/wld/play

wld.play() -> PlayResult

Enter play mode and leave the gameplay simulation running. Flips engine.mode = "play" when the engine is elsewhere, and resumes the standing gameplay pause when the engine is already in play. A pause another caller holds is theirs to settle: this refuses and names them rather than lifting it. The calling agent takes part in the play session from then on, so its /source writes land on the play shadow with the session's. The flip rebuilds the scene for play over the frames after it, and the call returns once that rebuild has ended.

Returns PlayResult

tools/wld/promoteAndSwitch

wld.promoteAndSwitch(mode: "edit" | "play") -> PromoteAndSwitchResult

Promote pending play-mode source-file edits (everything accepting the "edits" group would promote), then switch the engine to mode — one call replacing the changes() / acceptChanges("edits") / wld.edit()-or-wld.play() sequence. With zero pending edits the promote is a no-op and the mode switch still happens. Reaching the mode settles the gameplay run state the way that mode's own tool settles it: "play" leaves the simulation running, "edit" leaves it at rest. A pause another caller holds is theirs to settle, so a switch to play over one refuses and names them, exactly as wld.play() does. Entity changes are untouched: a flip to edit with unaccepted entity changes stops at the leave-play safeguard, exactly as wld.edit() does. The flip rebuilds the scene for the new mode over the frames after it, and the call returns once that rebuild has ended, as wld.play() and wld.edit() do.

Parameters

  • mode "edit" | "play"

Returns PromoteAndSwitchResult

"edit"
"play"

tools/wld/showLayer

wld.showLayer(name: string) -> ()

Show a previously-hidden scene layer. Restores rendering of the layer's entities. Inverse of wld.hideLayer.

Parameters

  • name string

Returns ()

"ui"

tools/wld/unloadLayer

wld.unloadLayer(name: string)

Unload an additive scene layer. Despawns the layer's entities entirely — to keep the entities but stop rendering them, use wld.hideLayer instead.

Parameters

  • name string
"props"

tools/workflow/answer

workflow.answer(request: string, value: any) -> boolean

Answer a request a workflow is waiting on. The run resumes from where it parked and carries on with whatever it decides comes next. Pass the value the request asked for. Where the request carries a schema (workflow.next returns it), that is the shape the answer must have: a value of any other shape is refused by this call, naming the field and the shape expected there, and the request stays open to be answered again.

Parameters

  • request string
  • value any (optional)

Returns boolean

{ request = "call-f6641ff8", value = { title = "Tornado Run", ready = true } }

tools/workflow/correct

workflow.correct(request: string, value: any) -> any

Replace the answer to a workflow request with a corrected value, while the run has not read the answer yet. The run then reads the corrected value as though it had been the first. The value is held to the request's schema exactly as workflow.answer holds the first one: a value of another shape is refused, and the answer on record stays as it was. A run reads an answer within a moment of it being given, and from then on it carries on from that answer: this call then refuses, naming the request and saying the answer was already consumed.

Parameters

  • request string
  • value any (optional)

Returns any

{ request = "call-f6641ff8", value = { title = "Tornado Run", ready = true } }

tools/workflow/fail

workflow.fail(request: string, reason: string) -> boolean

Tell a workflow that a request cannot be answered, and why. The run decides what that means — a stage may stop, or carry on without it. Use this rather than leaving a claim unanswered: an abandoned claim leaves the run parked until the claim expires.

Parameters

  • request string
  • reason string

Returns boolean

{ request = "call-f6641ff8", reason = "the world it names does not exist" }

tools/workflow/list

workflow.list() -> { { [string]: any } }

List the workflows in this world: what each one is for, when to reach for it, the phases it moves through, and what it expects in args. Start one by name with workflow.start. A job with a workflow is a job whose shape somebody already worked out — look here before deciding to do a multi-step job by hand.

Returns { { [string]: any } }

tools/workflow/next

workflow.next(runner: string, run?: string) -> { [string]: any }?

Take the next request a workflow run is waiting on, claimed for you in the same call. Returns nothing when no workflow is waiting. Asked for a run that has ended, it raises with how the run ended and why: a run that has failed or finished waits on nothing. A request's schema is the shape its answer must have: hand it on with the prompt.

Parameters

  • runner string
  • run string (optional)

Returns { [string]: any }?

"me"

tools/workflow/renew

workflow.renew(request: string, seconds?: number) -> { [string]: any }

Keep your claim on a workflow request through a stretch with no engine calls. A claim holds for a lease (workflow.next states lease_seconds and expires_in with the request) counted from the last tool call your session makes on this engine, so working keeps it; renewing starts it over, for seconds or the length it was granted. You are told once when a claim is about to run out and once if it does; a claim that ran out is taken back by renewing it, as long as nobody else has taken it since. Every workflow.next you make renews the claims you already hold.

Parameters

  • request string
  • seconds number (optional)

Returns { [string]: any }

"call-f6641ff8"
{ request = "call-f6641ff8", seconds = 7200 }

tools/workflow/start

workflow.start(name: string, args?: { [string]: any }) -> { [string]: any }

Start a workflow by name. The workflow decides what happens and in what order; your job is to answer the questions it stops to ask, with workflow.next and workflow.answer. Returns the run id — the run is already going, and its first request arrives on your next tool response.

Parameters

  • name string
  • args { [string]: any } (optional)

Returns { [string]: any }

"lighthouse"
{ name = "lighthouse", args = { instance = "wfrt" } }

tools/workflow/status

workflow.status(run?: string) -> any

Report what workflow runs are doing: which phase each is in, what it is waiting on, who claimed it and how long the claim has left (expires_in, seconds), how many questions it has asked and had answered, and what it returned once it is done. A run that has ended waits on nothing: cancelled counts the requests it ended unanswered, and error says why a failed run failed. A run outlives the engine: restored counts the times an engine read it back from its record. Give a run id for one run, or nothing for all of them.

Parameters

  • run string (optional)

Returns any

"run-098c453d"

tools/worldValidation/check

worldValidation.check(opts?: CheckOpts) -> ZmToolResult

Validate YOUR world's authored content — every script and asset under /source/ EXCEPT /source/libs/. The cargo-check equivalent for a Zero world. Imported libraries and engine builtins are NOT scanned by default (they're not yours to validate, and walking the whole builtin tree is slow); pass opts.scope to widen — "libraries", "library:<name>", or "all" (world + libraries). Read-only. The report is on .data; a one-line health summary is on .stdout.

Parameters

  • opts CheckOpts (optional)

Returns ZmToolResult

-- your world only
{ severity = "error", includePlaceholders = false }
{ scope = "all" }                    -- world + imported libraries

tools/worldValidation/checkLibraries

worldValidation.checkLibraries(opts?: FilterOpts) -> ZmToolResult

Validate every imported library under /source/libs/. World content is skipped — the report's world bucket is nil. The libraries map carries one entry per library directory.

Parameters

  • opts FilterOpts (optional)

Returns ZmToolResult

{ severity = "warning" }

tools/worldValidation/checkLibrary

worldValidation.checkLibrary(name: string, opts?: FilterOpts) -> ZmToolResult

Validate one named library under /source/libs/<name>/. If the library does not exist the report carries a single library.missing error. Use when you want to isolate the health of one dependency.

Parameters

  • name string
  • opts FilterOpts (optional)

Returns ZmToolResult

"@builtin"
"@mylib", { severity = "error" }

tools/worldValidation/checkWorld

worldValidation.checkWorld(opts?: FilterOpts) -> ZmToolResult

Validate ONLY the world's authored content (everything under /source/ except /source/libs/). Use to verify your own code without library noise. The report's world bucket is populated; the libraries map is empty.

Parameters

  • opts FilterOpts (optional)

Returns ZmToolResult

{ severity = "error" }

tools/worldValidation/report

worldValidation.report(opts?: ReportOpts) -> ZmToolResult

Generate a validation report with structured filtering + formatting options. opts.scope selects which bucket to scan (default "world" — YOUR content, never the imported libraries / engine builtins); opts.format picks the .stdout rendering; opts.savePath writes the rendered report to a VFS path (format inferred from extension when not set).

Parameters

  • opts ReportOpts (optional)

Returns ZmToolResult

{ scope = "world", format = "markdown" }
{ savePath = "/source/.validation/last-run.md" }
{ severity = "error", includePlaceholders = false, format = "human" }

tools/worldValidation/summary

worldValidation.summary(opts?: CheckOpts) -> ZmToolResult

Validate your world and return ONLY the one-line health summary — no problem list. Cheap to call when all you need is a yes/no health gate. Same scope rules as worldValidation.check: YOUR content under /source/ (excluding /source/libs/) by default; pass opts.scope to widen. Most useful filter here: { includePlaceholders = false }.

Parameters

  • opts CheckOpts (optional)

Returns ZmToolResult

{ includePlaceholders = false }

tools/zeromind/assetInfo

zeromind.assetInfo(guid: string) -> { [string]: any }

Read one published asset's record from ZeroMind. Lighter than zeromind.inspect, which also fetches an asset's children and reverse-dependencies and its thumbnail; before installing something, also read its files with zm ls-tree @<guid>, zm grep <pattern> @<guid> and zm show @<guid>:<path> in the shell. This is the plain record when you already know what the asset is and just need its current fields.

Parameters

  • guid string

Returns { [string]: any }

tools/zeromind/assetTypes

zeromind.assetTypes() -> { [string]: any }

List the asset types published content carries, with a count for each. This is what the LIBRARY holds, not what this engine's registry declares — the difference matters when narrowing a zeromind.search by assetType, because a type with no published content will return nothing however well-formed the query is.

Returns { [string]: any }

tools/zeromind/authStatus

zeromind.authStatus() -> ZeromindAuthStatus

Whether this engine is signed in to ZeroMind, as whom, against which ZeroMind, and when its session token expires. While a link started by zeromind.login is pending, link carries the page and code to approve it with.

Returns ZeromindAuthStatus

tools/zeromind/bookmark

zeromind.bookmark(target: string, guid: string, on?: boolean) -> { [string]: any }

Save a published world or asset for later, or unsave it. A bookmark is private to the account this engine is linked to and does not affect anything's ranking — it is a note to yourself, not a vote.

Parameters

  • target string
  • guid string
  • on boolean (optional)

Returns { [string]: any }

tools/zeromind/comment

zeromind.comment(target: string, guid: string, body: string, parent?: string) -> { [string]: any }

Post a comment on a published world or asset. Pass parent to reply to an existing comment rather than start a new thread. Comments are public and attributed to the account this engine is linked to.

Parameters

  • target string
  • guid string
  • body string
  • parent string (optional)

Returns { [string]: any }

tools/zeromind/convertInstalls

zeromind.convertInstalls(opts?: ZmConvertInstallsOpts) -> { { root_path: string, record: string, rows_removed: number, overrides_kept: number, files_added: number, completion: string? } }

Brings installs an earlier engine landed as one row per file into one record each: the form zeromind.installAsset writes now, which every engine materializes and Source Control lists as one entry. The record states the whole published version of the install, so files the old rows never held are added. A file changed since the install stays a row of the world. Nothing is committed: rows that were committed leave as deletions in the next commit. zeromind.installs lists the installs and their form.

Parameters

  • opts ZmConvertInstallsOpts (optional)

Returns { { root_path: string, record: string, rows_removed: number, overrides_kept: number, files_added: number, completion: string? } }

{ dryRun = true }
{ only = "/zero/source/legacy/skate_physics.package" }

tools/zeromind/create

zeromind.create(title: string, opts?: ZmCreateOpts) -> ZmCreateResult

Creates a new world owned by the caller.

Parameters

  • title string
  • opts ZmCreateOpts (optional)

Returns ZmCreateResult

"Test Library A"
"Combat", { visibility = "private" }

tools/zeromind/deleteComment

zeromind.deleteComment(id: string) -> { [string]: any }

Delete one of your own comments. The comment is tombstoned rather than erased, so replies to it keep their place in the thread. You can only delete comments you posted.

Parameters

  • id string

Returns { [string]: any }

tools/zeromind/follow

zeromind.follow(target: string, guid: string, on?: boolean) -> { [string]: any }

Follow a person or a world so their new work reaches you, or stop following. Unlike a bookmark, which names one thing, a follow is a standing subscription to whatever comes next.

Parameters

  • target string
  • guid string
  • on boolean (optional)

Returns { [string]: any }

tools/zeromind/inspect

zeromind.inspect(guid: string | { string }, target?: string, include?: { string }) -> ZmInspection | ZmInspections

Read the full published record for one asset or world by guid — what zeromind.search returns a summary of. For an asset: its assetType, description, capabilities, schema, README excerpt, engagement counters and a ready-to-paste import hint, and its thumbnail as a picture when it has one, with preview_description saying what the picture shows. For a world: the world record with its addresses. It shortlists hits rather than trusting their names: look at the picture of a mesh, a material or a scene. What decides between candidates is their implementation, read in the shell before any install (zm ls-tree @<guid>, zm grep <pattern> @<guid>, zm show @<guid>:<path>).

Parameters

  • guid string | { string }
  • target string (optional)
  • include { string } (optional)

Returns ZmInspection | ZmInspections

tools/zeromind/installAsset

zeromind.installAsset(guid: string, opts?: ZmInstallAssetOpts) -> worldVcs.InstallAssetResult

Installs the specified asset into the world.

Parameters

  • guid string
  • opts ZmInstallAssetOpts (optional)

Returns worldVcs.InstallAssetResult

asset_guid
asset_guid, { path = "/source/imported/foo" }

tools/zeromind/installLib

zeromind.installLib(guid: string, opts?: ZmInstallLibOpts) -> worldVcs.InstallLibraryResult

Installs the specified world as a library. After install, it is reachable as @<name>::<path> from this world's scripts (e.g. require("@combat::weapons.sword")).

Parameters

  • guid string
  • opts ZmInstallLibOpts (optional)

Returns worldVcs.InstallLibraryResult

world_guid
world_guid, { as = "combat" }
world_guid, { as = "@combat", version = commit_id }

tools/zeromind/installs

zeromind.installs(opts?: ZmInstallsOpts) -> { worldVcs.InstallInfo }

Lists the installs this world holds, whichever form each is stored in. record is one row of the world that every engine materializes the files from; rows is an install an earlier engine landed as one row per file, which zeromind.convertInstalls brings into one record. An asset installed at more than one place names the other places in also_at.

Parameters

  • opts ZmInstallsOpts (optional)

Returns { worldVcs.InstallInfo }

{ rows = false }

tools/zeromind/issue

zeromind.issue(body: string, kind?: string, title?: string, worldGuid?: string) -> { [string]: any }

File a bug report or feedback about the Zero platform itself — the engine, the editor, ZeroMind. Not for content: something wrong with a published asset or world is zeromind.report (rule breaking) or zeromind.rate / zeromind.review (it did not work). Use this when the engine did the wrong thing.

Parameters

  • body string
  • kind string (optional)
  • title string (optional)
  • worldGuid string (optional)

Returns { [string]: any }

tools/zeromind/list

zeromind.list() -> { ZmWorldEntry }

List every world the authenticated user has access to (owner / maintainer / contributor / viewer). Calls world.list() which wraps the spacetime list_my_worlds procedure (forwards ZeroMind's GET /v1/me/worlds). Returns the array of ZmWorldEntry records, sorted by title. A runtime error raises with a clean zeromind.list:-prefixed message.

Returns { ZmWorldEntry }

tools/zeromind/listComments

zeromind.listComments(target: string, guid: string) -> { [string]: any }

Read the comment thread on a published world or asset. Worth reading before installing something: the thread is where people say what broke for them, which the asset's own description will not.

Parameters

  • target string
  • guid string

Returns { [string]: any }

tools/zeromind/login

zeromind.login(opts?: ZeromindLoginOpts) -> ZeromindLoginResult

Sign this engine in to ZeroMind — the device-code link. Answers the page to open and the code to enter there (awaiting_approval), then returns; approving in a browser finishes the link, and zeromind.authStatus reports it. An engine already linked answers already_linked with its account: zeromind.logout first to switch accounts. opts.as suggests a handle on the approval page. The bearer stays in the trusted VM. A link that fails raises with the reason.

Parameters

  • opts ZeromindLoginOpts (optional)

Returns ZeromindLoginResult

{ as = "builder" }

tools/zeromind/logout

zeromind.logout() -> boolean

Sign this engine out of ZeroMind: revoke the device-code link and clear the cached credential. A live session already connected keeps running until the engine restarts. A refused logout raises with the reason.

Returns boolean

tools/zeromind/preview

zeromind.preview(guid: string, opts?: ZmPreviewOpts) -> worldVcs.PreviewResult

Preview what installing an asset WOULD write, without writing anything. Returns the resolved closure tree: every file and dependency with its computed dest_path, size, content hash, and dependency reason, plus rollup totals and a truncated flag. Use it to vet a package before zeromind.installAsset.

Parameters

  • guid string
  • opts ZmPreviewOpts (optional)

Returns worldVcs.PreviewResult

asset_guid
asset_guid, { path = "/source/imported/foo" }

tools/zeromind/rate

zeromind.rate(target: string, guid: string, bucket: string, body?: string) -> { [string]: any }

Rate a published world or asset. The two take different buckets because they ask different questions: a world asks whether it was enjoyable, an asset whether it actually worked when you used it. An optional note says why, which is what makes the rating useful to the next person rather than just a number.

Parameters

  • target string
  • guid string
  • bucket string
  • body string (optional)

Returns { [string]: any }

tools/zeromind/report

zeromind.report(target: string, guid: string, reason: string, note?: string) -> { [string]: any }

Flag a published world or asset for moderation. This is for content that breaks the rules — not for something that merely does not work, which is what zeromind.rate and zeromind.review are for. Reports are reviewed by a person.

Parameters

  • target string
  • guid string
  • reason string
  • note string (optional)

Returns { [string]: any }

tools/zeromind/review

zeromind.review(guid: string, compatTier: string, usability: number, codeQuality: number, performance: number, verdict?: string, shimAssetGuid?: string) -> { [string]: any }

File a structured review of an asset: how well it fits the current engine, three scored metrics, and a verdict. This is the review the library ranks on, and ZeroMind reserves it for agent callers — which is to say, for a caller that has actually installed the asset and run it, rather than read its page. Review what you used; a score from a reader is noise.

Parameters

  • guid string
  • compatTier string
  • usability number
  • codeQuality number
  • performance number
  • verdict string (optional)
  • shimAssetGuid string (optional)

Returns { [string]: any }

tools/zeromind/reviewUpdate

zeromind.reviewUpdate(unit: string, opts?: ZmReviewUpdateOpts) -> worldVcs.UpdateReview

What updating one unit changes, before you apply it. Writes nothing. The answer is the update, not the asset: the versions it moves from (what this world holds) and to, the commits of the origin world in between with their messages, authors and dates (newest first), then the files. For an installed asset files lists each file the update adds, changes or removes with its text diff, and local_edit says whether this world edited that file and whether the edit merges cleanly or conflicts. For a library the pin move and its commits lead, with counts of the library files that change and of this world's files whose references move (summary.moving); pass { files = true } for the per-file lists. unit is what zeromind.updates lists: a library name ("@builtin", "@combat", the @ optional) or an installed asset's root path. Apply it with zeromind.update(unit).

Parameters

  • unit string
  • opts ZmReviewUpdateOpts (optional)

Returns worldVcs.UpdateReview

"@builtin"
"@builtin", { files = true }
"/zero/source/Fire.module"
zeromind.search(q?: string, assetType?: string, target?: string, limit?: number, from?: string, mode?: string) -> ZmSearchResults

Search the ZeroMind library for published assets and worlds that other people have shared. Reusing a published module, shader, material or world beats writing one from scratch, so this is normally the first call when a world needs something it does not already have. Shortlist two or three hits, read each one's implementation in the shell (zm ls-tree @<guid>, zm grep <pattern> @<guid>, zm show @<guid>:<path>, zm log @<guid>), then pass the guid of the one that fits to zeromind.installAsset and verify it. Requires the engine to be linked to ZeroMind (zeromind.login).

Parameters

  • q string (optional)
  • assetType string (optional)
  • target string (optional)
  • limit number (optional)
  • from string (optional)
  • mode string (optional)

Returns ZmSearchResults

tools/zeromind/setCover

zeromind.setCover(guid: string, vfsPath?: string, blobSha256?: string, contentType?: string) -> ZmWorldRecord

Set a published world's cover image on ZeroMind — the picture people see in listings before they open it. Point it at an image in this world's VFS and the bytes are uploaded and stamped in one call; a viewport capture (capture.viewport) written to a path is the usual source, and an authored image works the same way. Pass blobSha256 instead when the image is already uploaded. Needs Publish scope to upload and Maintainer access to stamp.

Parameters

  • guid string
  • vfsPath string (optional)
  • blobSha256 string (optional)
  • contentType string (optional)

Returns ZmWorldRecord

tools/zeromind/setMetadata

zeromind.setMetadata(guid: string, title?: string, description?: string, body?: string, tags?: { string }, topics?: { string }, category?: string, visibility?: string) -> ZmWorldRecord

Edit a published world's metadata on ZeroMind — title, description, long body, tags, topics, category and visibility. This is how a world is presented in the library, and it is a different thing from the world's runtime state (wld.*): these are the fields someone browsing ZeroMind reads before they ever open it. Pass only the fields you are changing; the rest are left as they are. Needs Maintainer access to the world.

Parameters

  • guid string
  • title string (optional)
  • description string (optional)
  • body string (optional)
  • tags { string } (optional)
  • topics { string } (optional)
  • category string (optional)
  • visibility string (optional)

Returns ZmWorldRecord

tools/zeromind/setTarget

zeromind.setTarget(issuer: string) -> ZeromindTargetResult

Point this engine at a different ZeroMind backend (e.g. http://localhost:3001). The target is saved with the cached credential and takes effect on the next zeromind.login; switching to a different backend clears the credential held for the old one, and an engine already connected reaches the new one once it restarts.

Parameters

  • issuer string

Returns ZeromindTargetResult

"http://localhost:3001"

tools/zeromind/swap

zeromind.swap(guid: string, opts?: ZeromindSwapOpts) -> string

Switches the engine over to a different world. The world that was running is left first: its world entrypoint's onWorldUnload runs, its scene and the entities in it are unloaded, and the entities it made with no scene loaded are despawned. After this returns, every vfs.* / zm.* call targets the new world, whose content and startup scene then load (world.syncStatus().binding reports them). A world that does not exist, that you may not open, or that is asked for while another world load is running raises with the reason, and the engine stays on the world it was on. So does a local change that has not reached the world being left: one still on its way is waited for with world.awaitOutgoingSync(), and one the server refused (the world was deleted, or write access to it was lost) is settled with world.resolveConflict(path, "discard"); the error names which.

Parameters

  • guid string
  • opts ZeromindSwapOpts (optional)

Returns string

"11111111-2222-3333-4444-555555555555"
guid, { version = "passthrough:01HXAMPLECOMMITID" }
guid, { mode = "play" }

tools/zeromind/uninstallLib

zeromind.uninstallLib(name: string) -> string

Removes a previously installed library from this world.

Parameters

  • name string

Returns string

"combat"
"@combat"

tools/zeromind/update

zeromind.update(units?: (string | { string }), opts?: ZmUpdateOpts) -> worldVcs.UpdateResult

Bring this world's libraries and installed assets up to date. With no argument every unit zeromind.updates lists is updated; pass one unit or a list (library names such as "@builtin" and installed assets' root paths, mixed) to update those. A library moves its pin, an installed asset takes its origin's new version three-way merged with this world's edits. Units are updated one at a time: one that fails is reported in its entry with the reason and the rest still run, and an install whose edits conflict lists the conflicted files (resolve them with zm.checkout("", { paths = { path }, theirs = true }), or ours). Nothing is committed: to_commit lists every path the updates changed, ready for zm.add + zm.commit. A file you changed and have not committed that an update merged into or conflicted is named in warnings. zeromind.reviewUpdate(unit) shows what an update changes first, and its modified lists those files ahead.

Parameters

  • units (string | { string }) (optional)
  • opts ZmUpdateOpts (optional)

Returns worldVcs.UpdateResult

"@builtin"
{ "@combat", "/zero/source/Fire.module" }

tools/zeromind/updates

zeromind.updates() -> { [string]: any }

List the libraries and installed assets that have an update available. A library is one unit: its marker's requested ref now resolves to a newer commit than the one the world pins. An installed asset is checked by its root only: the origin holds a newer version of the asset you installed, and a dependency of an install is never listed on its own. Empty result = everything is up to date. Each entry's apply names the call that applies it, zeromind.update("<unit>"), and the one that shows what it changes first, zeromind.reviewUpdate("<unit>"); zeromind.update() applies every entry. A unit whose origin could not be reached is listed in skipped and never fails the call.

Returns { [string]: any }

-- what needs updating?

tools/zeromind/vote

zeromind.vote(target: string, guid: string, value: number) -> { [string]: any }

Vote on a published world, asset or comment. Voting is how the library ranks things, so it is the cheapest way to make a good asset easier for the next person to find. Voting again with a different value replaces your vote; 0 clears it.

Parameters

  • target string
  • guid string
  • value number

Returns { [string]: any }

tools/zeromind/worldInfo

zeromind.worldInfo(guid: string) -> { [string]: any }

Read a published world's record from ZeroMind — its title, description, body, tags, visibility, score and your own viewer state. The read side of zeromind.setMetadata: call this first to see what a field currently holds, since setMetadata replaces the fields it is given rather than merging into them.

Parameters

  • guid string

Returns { [string]: any }

tools/zm/add

zm.add(paths: string | { string }, opts?: { stage: string?, renormalize: boolean? }) -> boolean

Stage one or more paths' manifest rows for the next commit via world.add(paths). Pass a single VFS path string for the single-arg form, or an array of path strings to stage in one call: every row the paths select is staged once, so several files of one asset stage that asset's family once. The "stage everything" sentinels . / -A / --all (matching the shell zm add) route to world.add_all(). Empty array / empty string raise an argument error. Naming a path takes it from whichever other staging area of the account holds it, which is how a claim is handed over, and a path an area of another account holds is refused. A world.add runtime error raises with a clean zm.add:-prefixed message. opts.stage names one of the caller's own staging areas to stage into, so a commit naming that area carries these paths and leaves every other caller's staged. Omitted, the call stages into the default area every unnamed call shares.

Parameters

  • paths string | { string }
  • opts { stage: string?, renormalize: boolean? } (optional)

Returns boolean

"/source/foo.luau"
{ "/source/a.luau", "/source/b.luau" }
"."  -- stage every dirty path

tools/zm/branch

zm.branch(name?: string, opts?: ZmBranchOpts) -> any

Branches — git branch. With no name (or opts.list) answers the branches, the session's marked current; opts.remotes / opts.all answer origin's (origin/<name>, as the last fetch recorded them). With a name it creates that branch at opts.start (default HEAD); with opts.delete it deletes it (opts.force deletes one whose commits the session's branch does not hold, or whose working tree holds uncommitted changes); with opts.rename it renames it. Creating leaves the session where it is; zm.checkout moves it. A name that is not a branch name — one starting with - included — raises.

Parameters

  • name string (optional)
  • opts ZmBranchOpts (optional)

Returns any

nil
"feature"
"feature", { delete = true }

tools/zm/checkout

zm.checkout(target: string, opts?: ZmCheckoutOpts) -> any

git checkout. With a branch: switch this session to it (opts.create makes it first, at opts.start). Your uncommitted changes and staged entries come with you; a switch that would replace a path you changed with a different version refuses, naming the paths, and nothing moves. Returns once the branch's content has landed, so a commit made straight afterwards targets it. With opts.paths and opts.ours / opts.theirs: take that side of each unmerged path whole (then zm.add records the resolution). With opts.paths and opts.rev: write the paths as the revision holds them, and stage them.

Parameters

  • target string
  • opts ZmCheckoutOpts (optional)

Returns any

"feature"
"feature", { create = true }
"", { paths = { "/source/foo.luau" }, theirs = true }
"", { paths = { "/source/foo.luau" }, rev = "HEAD~1" }

tools/zm/commit

zm.commit(message: string, opts?: { stage: string? }) -> string

Materialize the staged tree as a new commit via world.commit(message). Git semantics — commits ONLY what's already staged. The reducer auto-deletes the stage row on success so a subsequent zm.commit opens a fresh stage. A failure (empty stage, dangling dep, …) raises with a clean zm.commit:-prefixed message. opts.stage materialises one of the caller's own staging areas, so the commit carries the paths staged under that name and leaves every other caller's staged.

Parameters

  • message string
  • opts { stage: string? } (optional)

Returns string

"add character controller component"

tools/zm/derived

zm.derived(opts: { action: string, path: string?, commit: string?, stage: string? }) -> any

List the derived outputs of the staging area and of the sealed commits (list), compute one again (retry), take the file you wrote at its path as the output (provide), leave a best-effort one out of the commit (drop), or return a sealed commit to the staging area with its message kept (unseal). A commit made while an output it needs is still computing is sealed and lands when the output is done; retry, provide and unseal are the ways out of an output that failed. A bad action, a missing path or a refused call raises with a clean zm.derived:-prefixed message.

Parameters

  • opts { action: string, path: string?, commit: string?, stage: string? }

Returns any

{ action = "list" }
{ action = "retry", path = "/zero/source/gold.material/preview.png" }

tools/zm/fetch

zm.fetch(branch?: string) -> worldVcs.FetchResult

Update the origin/<branch> remote-tracking ref — git fetch. Mirrors the world's ZeroMind branch head into local commit history (no working-tree change) and reports how the session branch relates to it: behind origin commits to pull, ahead local commits to push, diverged when both. A stale pin or an out-of-band ZeroMind change shows up as behind — reconcile with zm.pull().

Parameters

  • branch string (optional)

Returns worldVcs.FetchResult

tools/zm/log

zm.log(opts?: ZmLogOpts) -> { worldVcs.CommitRow }

The history of a revision, newest first — git log: every commit reachable from opts.rev (default HEAD) through both parents, so a merged branch's commits and pulled commits appear with the ones recorded on the branch. Each entry carries commit_id, parent_commit_id, merge_parent_id and message. opts.limit caps the count; opts.path keeps the commits that touched one file. A runtime error raises with a clean zm.log:-prefixed message.

Parameters

  • opts ZmLogOpts (optional)

Returns { worldVcs.CommitRow }

{ rev = "feature", limit = 10 }

tools/zm/merge

zm.merge(source: string, opts?: ZmMergeOpts) -> worldVcs.MergeResult?

Merge a branch into the session's branch — git merge <source>. The merge runs locally in the world; nothing reaches ZeroMind until the result is pushed. When the session's head is in the source's history it fast-forwards onto it (unless opts.noFf); otherwise a two-parent merge commit lands (opts.ffOnly refuses that). A local change on a path the merge rewrites refuses it with git's message naming the paths; every other local change stays in the tree. On conflicts, git-style markers are written into each conflicting text file and the cleanly-merged remainder is applied; zm.status lists the unmerged paths. Resolve each (edit out the markers, or zm.checkout with ours / theirs), then zm.add + zm.commit — that commit records the merge. opts.abort aborts the merge in progress instead.

Parameters

  • source string
  • opts ZmMergeOpts (optional)

Returns worldVcs.MergeResult?

"feature"
"feature", { noFf = true }
"", { abort = true }

tools/zm/prConflicts

zm.prConflicts(worldGuid?: string, number: number) -> any

Read what stands between a pull request and a merge. Returns the mergeability verdict, the merge base, both heads, and one entry per conflicting path. A conflicting TEXT path carries marked_text: the same <<<<<<< / ======= / >>>>>>> rendering a merge leaves in a working tree, source and target laid against their common ancestor. Resolve a path by writing the settled bytes back to it and committing on the source branch — the request re-analyses on the next read. A binary path carries the two sides' hashes and no text, so pick a side. A mergeable request returns an empty conflict list. zm.prView says HOW MANY conflicts there are; this says WHAT they are.

Parameters

  • worldGuid string (optional)
  • number number

Returns any

nil, 1
"11111111-2222-3333-4444-555555555555", 3

tools/zm/prCreate

zm.prCreate(roots?: { string }, opts?: ZmPrCreateOpts) -> { worldVcs.ContributeOutcome }

Send your changes to installed content upstream — zm pr create. For each origin world, the files you changed are three-way merged against the origin's current content (a region the origin changed too becomes a local conflict to resolve first), pushed as a branch in the origin world and opened there as a pull request, merged straight away when you may merge there (opts.merge = false leaves it open). roots names the install roots to send; omitted, every installed asset ahead of its upstream (as zm.status lists them) goes. A runtime error raises with a clean zm.prCreate:-prefixed message.

Parameters

  • roots { string } (optional)
  • opts ZmPrCreateOpts (optional)

Returns { worldVcs.ContributeOutcome }

{ "/source/Doom.component" }
nil, { dryRun = true }

tools/zm/prList

zm.prList(worldGuid?: string, number?: number) -> any

List pull requests in a world. To read ONE request — its diff, mergeability and conflict count — use zm.prView. A fork's outgoing pull requests live in the world they target, so read them there.

Parameters

  • worldGuid string (optional)
  • number number (optional)

Returns any

"11111111-2222-3333-4444-555555555555", 3

tools/zm/prMerge

zm.prMerge(worldGuid: string, number: number, strategy?: ("merge" | "squash" | "fast_forward")) -> any

Merge a pull request — the agent-side merge button. Read the request with zm.prView first: it reports what the request changes and whether it merges cleanly.

Parameters

  • worldGuid string
  • number number
  • strategy ("merge" | "squash" | "fast_forward") (optional)

Returns any

"11111111-2222-3333-4444-555555555555", 3
"11111111-2222-3333-4444-555555555555", 3, "squash"

tools/zm/prOpen

zm.prOpen(opts: any) -> any

Open a pull request — propose the work on one (world, branch) pair to another. From a fork the target defaults to the world it was forked from, so opening one with just a title proposes your work upstream; in an ordinary world the target is the same world, giving a branch → main request. The pull request lives in — and is numbered by — the world it targets, and that is the world zm.prList reads.

Parameters

  • opts any (optional)

Returns any

{ title = "fix the door hinge" }
{ title = "ship the HUD", sourceBranch = "hud", targetBranch = "main" }

tools/zm/prView

zm.prView(worldGuid?: string, number: number) -> any

Read one pull request in full — gh pr view. Returns the record plus a LIVE re-analysis against the current branch heads: mergeability (clean / conflicts / fast_forwardable / up_to_date / unrelated), conflict_count, and diff — every path the request adds, modifies or deletes, with its checksums. Read this before merging: it is what tells you WHAT the request changes.

Parameters

  • worldGuid string (optional)
  • number number

Returns any

nil, 1
"11111111-2222-3333-4444-555555555555", 3

tools/zm/pull

zm.pull(branch?: string, opts?: ZmPullOpts) -> worldVcs.PullResult

git pull: fetch origin/<branch> (default the session branch) and merge it into the session branch. Strictly behind fast-forwards; diverged makes a two-parent merge commit with the same conflict/marker flow as zm.merge (resolve, then zm.add + zm.commit; abortable with zm.merge("", { abort = true })). opts.ffOnly refuses anything but a fast-forward; opts.noFf always makes a merge commit. Updating an installed asset or a library is the zeromind toolbox's update, not a pull.

Parameters

  • branch string (optional)
  • opts ZmPullOpts (optional)

Returns worldVcs.PullResult

"feature"
"main", { ffOnly = true }

tools/zm/push

zm.push(opts?: { stage: string?, force: boolean? }) -> ZmPushResult

Publish unpushed commits to ZeroMind via world.push(). Mirrors git push semantics — no-args form walks the parent chain from HEAD back to the most-recently-pushed ancestor and pushes the entire unpushed stack oldest-first. The engine reads the caller's JWT from UserCredential / ZERO_USER_TOKEN; Luau scripts never see the token. A refused publish (parent-chain gap, ACL denial, network error) raises with a clean zm.push:-prefixed message. opts.stage names one of the caller's own staging areas, the way zm.add / zm.commit / zm.status take it: the pre-push content gate then refuses over the assets that area holds or committed, and publishes an asset another area holds with the shortfall recorded on it, named back with the area that holds it. Omitted, the call acts on the shared default area, which holds nothing — the gate refuses over the assets no area holds. A push only moves the remote branch forward: a remote head the branch does not hold is git's ! [rejected] ... (fetch first), one the branch is behind is (non-fast-forward). opts.force is git push --force (a Maintainer's): a published head moves the remote branch regardless; a branch with unpublished commits is refused, pull first.

Parameters

  • opts { stage: string?, force: boolean? } (optional)

Returns ZmPushResult

tools/zm/reset

zm.reset(target: string, opts?: { mode: string? }) -> worldVcs.ResetResult

Move the session branch's head to target — git reset [--soft|--mixed|--hard] <commit>. target is any revision zm.revParse reads (a commit id, HEAD~1, a branch, ...), any commit of the world. opts.mode is soft (keep the index and the tree; the paths the move changed read as staged), mixed (the default: unstage, keep the tree) or hard (unstage and take the tree to target: every change of your account goes, untracked files and uncommitted installs included, each to trash first, and writes the world never took are given up and a scene's autosave overlay goes; another account's local changes stay and are listed in kept). zm.reset("HEAD") unstages everything; zm.reset("HEAD", { mode = "hard" }) discards your local changes. The commits the head leaves behind become trash entries; zm.trash restores them in chain order, oldest first.

Parameters

  • target string
  • opts { mode: string? } (optional)

Returns worldVcs.ResetResult

"HEAD~1"
"HEAD", { mode = "hard" }

tools/zm/restore

zm.restore(paths: string | { string }, opts?: ZmRestoreOpts) -> ZmRestoreResult

git restore. Without opts.staged: take the working edits on each path back to what it was staged or committed as — the stage is the baseline where the path is staged, the last commit where it is not, and where it is neither there is nothing to come back to, so the path goes away. A folder takes every file beneath it back with it, and a deleted path comes back as its committed version. Edited bytes going back to a committed version are snapshotted to trash first and recoverable via zm.trash. opts.source takes them to a revision's version instead: each path, a folder with every file the revision holds beneath it, comes back as the asset the revision records, its guid included. opts.staged unstages the paths and leaves the working tree as it is; with opts.source it makes each path's staging-area entry that revision's version instead (git reset <rev> -- <path>); with opts.worktree too it does both. Pass a single VFS path, an array of paths, or . / -A / --all for every path. . takes the whole working tree back in one operation (world.discardAll): every file edited since its commit, every deletion, and every file no commit holds, an uncommitted install included, each to trash first. A path held in a staging area stays, and a change another account is making is left and reported under skipped. opts.untracked does the same for each folder named, where without it a folder leaves the files no commit holds in place, as git restore <dir> does. A path with nothing to restore is reported under skipped. A file listed only because its references moved holds no edit of its own: named alone it raises, naming what moved its references and the undo; reached through a folder it is reported under skipped, and . leaves it out. A runtime error raises with a clean zm.restore:-prefixed message.

Parameters

  • paths string | { string }
  • opts ZmRestoreOpts (optional)

Returns ZmRestoreResult

"/source/foo.luau"
"/source/foo.luau", { staged = true }
"/source/foo.luau", { source = "HEAD~1" }
"."

tools/zm/revParse

zm.revParse(rev: string) -> string

The commit a revision names — git rev-parse <rev>. A revision is HEAD, a branch, origin/<branch> (as the last fetch recorded it), a commit id or a unique prefix of one, each optionally followed by ~N (N first parents back), ^ (the first parent) or ^2 (a merge commit's second parent). A revision that names nothing raises git's message, HEAD on a branch with no commit included.

Parameters

  • rev string

Returns string

"HEAD"
"HEAD~2"
"origin/main"

tools/zm/stash

zm.stash(action?: string, ref?: any) -> any

git stash. zm.stash() / "push" saves your changed and staged paths into a stash and takes them back to their committed version (opts.message names it); content a stash records by checksum alone, and files no commit holds, stay in the tree and are listed under kept. "list" answers your stashes, newest first (stash@{0}). "pop" applies a stash and drops it, "apply" applies it and keeps it, "drop" drops it to trash; each takes stash@{n} or a row id and defaults to stash@{0}. Applying refuses, writing nothing, when a path the stash restores holds a different local change. A runtime error raises with a clean zm.stash:-prefixed message.

Parameters

  • action string (optional)
  • ref any (optional)

Returns any

"push", { message = "half-done menu" }
"pop"
"drop", "stash@{1}"

tools/zm/status

zm.status(opts?: { stage: string? }) -> ZmStatusResult

Show staged + dirty paths in the active world. Returns the raw { dirty, staged, untracked } table from world.vcsStatus, with branch and head folded in. blockers is world.publishBlockers(opts): every reason a push by this caller would refuse, one entry per class (script errors, unmet content requirements, computed references on the gameplay path, entrypoints reaching a dynamic resolve, references to assets the world no longer holds, references of the world's own files recorded before the world pinned the library they name, recorded error-severity problems), each naming its offending subjects and the one remedy for them. A blocker committed earlier is named here too, and an asset another staging area holds is that area's to complete; zm push publishes once the list is empty. One class blocks the next commit rather than the publish and carries blocks = "commit": the files whose references the staged set does not resolve (staleReferences); zm commit refuses until it is empty, and zm push never reads it. A runtime error from the underlying world.* reads raises with a clean zm.status:-prefixed message. upstream is the installed content ahead of its upstream — the files you changed in an installed asset, grouped by the world it came from, each entry naming its install root; zm diff <root> upstream shows the changes and zm.prCreate sends them. opts.stage reports one of the caller's own staging areas in place of the default one every unnamed call shares, and decides which assets the content class carries. The dirty and untracked sets are the world's working tree and read the same whichever area is named.

Parameters

  • opts { stage: string? } (optional)

Returns ZmStatusResult

tools/zm/trash

zm.trash(action?: string, handle?: (number | string)) -> any

The world's shared 7-day trash — what destructive operations threw away: commits a zm.reset left behind, working edits zm.restore replaced, dropped stashes, and removed files. zm.trash() (or "list") answers the entries, each row_id its handle; zm.trash("restore", handle) recovers one (commits a reset left behind re-attach in chain order, oldest first); zm.trash("empty") deletes every entry for good and answers how many. A runtime error raises with a clean zm.trash:-prefixed message.

Parameters

  • action string (optional)
  • handle (number | string) (optional)

Returns any

"restore", 42
"empty"
  • api
  • reference