Log inGet started

modules

Updated October 28, 2026

The modules namespace: 3711 functions.

modules/AgentSkillAssetTypeRef/README

require("@builtin::assetTypes.agentSkill.behavior") -- AgentSkillAssetTypeRef

Per-instance methods exposed on every AssetRef<agentSkill>. Loaded lazily by assetType.shared.ref.

Usage: local AgentSkillAssetTypeRef = require("@builtin::assetTypes.agentSkill.behavior")

modules/AgentSkillAssetTypeRef/getInstructions

getInstructions(self): string?

Read the skill's instructions.md body — the prose an agent receives when it invokes the skill.

Parameters

  • self any (optional)
local body = skillRef:getInstructions()

modules/AgentSkillAssetTypeRef/getManifest

getManifest(self): { [string]: any }

Read and parse the skill's skill.yaml manifest — its description, declared dependencies, and subskill ordering.

Parameters

  • self any (optional)
local m = skillRef:getManifest()

modules/AgentSkillAssetTypeRef/getReadme

getReadme(self): string?

Read the skill's README.md body.

Parameters

  • self any (optional)
print(skillRef:getReadme())

modules/AgentSkillAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail — the skill's whole structured surface: { description, kind, when, checks, stages, verdicts, instructions, dependencies, subskills, manifestError }. checks carries each acceptance criterion as { check, observe } — what must hold, and where it is seen. stages carries the ordered passes as { name, detail, checks }, each holding the criteria that only mean anything once that pass has run. verdicts carries the outcomes a reader reports as { verdict, means } — the word, and what reporting it asserts. dependencies carries each declared identity with what it resolved to and whether it resolves here; a toolbox additionally carries the tools it holds, a module the exports it carries, and a tool its own signature, all read live rather than restated. subskills carries each nested skill's parent/sub invoke address. This is the dump skills.invoke renders — a skill with no readable instructions.md still describes, reporting the gap rather than erroring.

Parameters

  • self any (optional)
local subskills = asset.inspect(skillRef).detail.subskills

modules/AgentSkillAssetTypeRef/onChange

onChange(self, change)

Lifecycle hook: re-publish this skill when anything inside its folder is written, so a skill just authored is listed and an edited description is the one agents see. Reads the manifest and writes nothing back.

Parameters

  • self any (optional) — The changed skill's AssetRef.
  • change any (optional) — The write record the dispatcher passes through.

modules/AgentSkillAssetTypeRef/onDelete

onDelete(self)

Lifecycle hook: take this skill's row out of the roster when the skill leaves the runtime — deleted, or moved to a new identity, which registers it again there. The roster lists each skill that exists once.

Parameters

  • self any (optional) — The departing skill's AssetRef.

modules/AgentSkillAssetTypeRef/onRegister

onRegister(self)

Lifecycle hook: publish this skill to the roster agents read when the skill first registers. The scope it is listed under is derived from the asset's own identity.

Parameters

  • self any (optional) — The registering skill's AssetRef.

modules/AnimationAssetTypeBehavior/README

require("@builtin::assetTypes.animation.behavior") -- AnimationAssetTypeBehavior

Behaviour for the animation asset type — a clip's disk shape. The payload is data.zanim (the engine-native binary AnimAsset, parallel to .mesh's data.zmsh). onCreate writes that payload, and — when the clip's source rig guid is supplied — records it as source_rig.json (a .rig asset reference) so the clip knows the skeleton it was authored on and retargets onto any humanoid. Converters call asset.create("animation", name, { bytes, rig }) to mint a clip.

Usage: local AnimationAssetTypeBehavior = require("@builtin::assetTypes.animation.behavior")

modules/AnimationAssetTypeBehavior/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .animation/ instance: when the clip's source rig link (source_rig.json) changes, record the rig's humanoid classification into .metadata for search — the clip inherits it from the rig it targets. Originator-only; .metadata syncs as ordinary content.

Parameters

  • ref any (optional) — The AssetRef for the changed clip.
  • change { [string]: any } — { path, asset, type, kind, origin }.

modules/AnimationAssetTypeBehavior/reindexSearchMeta

reindexSearchMeta(self): ()

Re-derive this clip's search .metadata (its humanoid classification, resolved from the source rig). onChange calls it when source_rig.json changes; the search-metadata backfill calls it per clip to populate clips minted before the deriver existed.

Parameters

  • self any (optional)
animationRef:reindexSearchMeta()

modules/AssetChangeDispatch/README

require("@builtin::assetTypes.assetType.shared.changeDispatch") -- AssetChangeDispatch

Routes a VFS source write to the enclosing typed-asset's onChange(ref, change) hook. Installs _G.__zero_dispatch_asset_change, which the engine calls (via ffi_callbacks::fire_asset_change_dispatch) once per source write.

This is the type-level analogue of the component-centric onAssetReload(field) fan-out. Components react to assets they reference; an asset type reacts to writes INSIDE its own instances. When any file under a <name>.<type>/ folder changes on the VFS, the engine hands the written path here; this module finds the enclosing typed-asset folder, loads its <type>.assetType/behavior.luau, and — if that module exports an onChange function — invokes onChange(ref, change). The engine half is deliberately thin (a generic "this path was written" signal); all the resolution + behaviour lives here in Luau, mirroring the __build_asset_ref_proxy split.

Usage: local AssetChangeDispatch = require("@builtin::assetTypes.assetType.shared.changeDispatch")

modules/AssetChangeDispatch/dispatch

dispatch(path: string, kind: string?, origin: string?)

Dispatch the asset-type change hook for a VFS path. Called by the engine for two orthogonal axes:

  • kind: a per-file EDIT ("edited", path is the written file) vs an asset COMPLETION ("seeded", path is the typed-asset folder, now fully present from a world seed). The type inspects kind to decide whether to filter by which file changed or act on the whole asset. "dependency" says a module the asset's code at path required has re-run, or that a reference path names has bound to a target that did not exist before, and goes to the type's onDependencyChange(ref, change) alone.
  • origin: "local" for a write made on this client, "remote" for a peer-synced write — forwarded so hooks can gate on it (importers run on the originator only). Resolves the typed asset, loads its behavior.luau, and invokes onChange(ref, change) (change = { path, asset, type, kind, origin }, plus settings and settingsChanged for a write to the settings values file). The written file's content version moves first; the enclosing asset folder's moves last, after the type has reacted. An edit schedules no preview. When the path has no enclosing typed asset, forwards to the loose-file seam (dispatch_loose) so the importer system can claim orphan writes. A hook may wait on what it reads: vfs.read of bytes only the blob store holds fetches them. The writes routed to one asset react in the order they were routed, each once the hook before it has finished.

Parameters

  • path string — The VFS path: the written file (edited) or the asset folder (seeded).
  • kind string? (optional) — "edited" (default) or "seeded".
  • origin string? (optional) — "local" (default) or "remote".
__zero_dispatch_asset_change("/zero/source/Goblin.dynamicAsset/prompt.json", "edited", "local")

modules/AssetChangeDispatch/dispatchDelete

dispatchDelete(path: string, guid: string?)

Dispatch the asset-type DELETE hook for a removed typed-asset FOLDER — the teardown counterpart of dispatch. path is the folder that was removed; this resolves its <name>.<type> identity, loads the type's behavior.luau, and invokes onDelete(ref) if defined (a .component unregisters its type). No-op when the path isn't a registered typed asset or the type defines no onDelete. The engine calls this via ffi_callbacks::fire_asset_delete_dispatch once per typed-asset folder removal. A removal routed while the asset's onChange is waiting tears the asset down once that hook, and every write queued behind it, has finished.

Parameters

  • path string — The removed asset folder's VFS path.
  • guid string? (optional) — The removed asset's guid, as its manifest row carried it, when the removal knew it.
__zero_dispatch_asset_delete("/zero/source/Spinner.component", guid)

modules/AssetRef/README

require("@builtin::assetTypes.assetType.shared.ref") -- AssetRef

AssetRef proxy builder. Attaches the default method metatable to every { __ref, type, name, guid, identity, path } envelope produced by asset.resolve / asset.ref, and dispatches type-specific methods from <typename>.assetType/behavior.luau so a .material ref carries material-only methods (and so on).

The Rust side (crates/zero_scripting/src/ffi/bindings/asset.rs) used to ship a C metatable with a fixed __index that knew about getSource / getBytes / getText / exists / inspect / meta. That shape couldn't grow without an FFI change, which meant the per-asset-type behaviour required to make refs useful (read a material's properties, instantiate a bundle, run a tool) had no hook. This module owns that responsibility now. It exposes one entry point — M.build(envelope) — that the Rust factory invokes via the _G.__build_asset_ref_proxy global the prelude installs. The build call attaches a shared metatable whose __index first serves the default method table, then falls through to the type's own ref table (loaded lazily from @builtin::assetTypes.<type>.behavior). The default method surface is fixed and engine-required — per-type ref tables CANNOT shadow getSource / getBytes / getText / exists / deps / canInstantiate / canPreview / preview (which calls the type's own ref.preview when it has one), or the lazy meta, runtime, events, typeRef, modules and residency properties. inspect is NOT a fixed default — it falls through to the type's own ref.inspect(self) (returning the type-specific detail for asset.inspect's dispatch), and is nil when the type defines none.

Usage: local AssetRef = require("@builtin::assetTypes.assetType.shared.ref")

modules/AssetRef/build

build(envelope: any): any

Attach the AssetRef method metatable to an envelope table. Invoked by the Rust factory (push_asset_ref_handle → _G.__build_asset_ref_proxy) immediately after the six envelope fields (__ref, type, name, guid, identity, path) have been set, so the metatable's __index only ever fires for method / property lookups, never for the literal envelope fields.

Parameters

  • envelope any (optional) — The freshly-built envelope table.
local r = require("@builtin::assetTypes.assetType.shared.ref").build({ type = "material", path = "/zero/source/Gold.material", ... })

modules/AssetRef/canInstantiate

canInstantiate(self): boolean

Whether this asset can be instantiated into a scene — true iff its asset type defines an instantiate method. Generic capability query (no type allowlist); consumers gate on it before offering a scene path (an Asset.source field, a viewport drop, a tool argument).

Parameters

  • self any (optional)
if asset.resolve("Golem","dynamicAsset"):canInstantiate() then ... end

modules/AssetRef/canPreview

canPreview(self): boolean

Whether this asset can render a preview — true when its type declares a preview: recipe in its type.yaml, defines its own preview, or defines instantiate (a scene-instantiable asset previews as itself).

Parameters

  • self any (optional)
if ref:canPreview() then local p = ref:preview() end

modules/AssetRef/deps

deps(self): { deps: { any }, unresolved_deps: { any }, problems: { any } }

Return this asset's outbound reference table, aggregated across every file inside it (for composite asset folders) — same data asset.deps(ref) returns. deps holds the references that resolved ({ asset_guid, origin, literal, via, line?, checksum?, ... }), unresolved_deps the literals nothing answered ({ literal, via, reason, line? }), and problems the findings attached to the asset. An asset that references nothing returns all three empty.

Parameters

  • self any (optional)

modules/AssetRef/flushPendingPersists

flushPendingPersists()

Write out every asset whose edit-mode persistence is still coalesced, spending no allowance and waiting on no refill. The runtime-state wipe on a mode flip calls this first, so a change made in the last window before the flip reaches the asset instead of being cleared with the overlay it lives in. Call it before reading an asset's file for a value a runtime write may have just changed.

require("@builtin::assetTypes.assetType.shared.ref").flushPendingPersists()

modules/AssetRef/forgetRuntime

forgetRuntime(guid: string): boolean

Forget everything a type derived from ONE asset's content — the values it cached in ref.runtime off the bytes that asset used to hold. Called when an asset's content is REPLACED under a guid live consumers already hold: a type memoizes its parse, its GPU handle, its settings against the content it read, and each of those describes the previous bytes the moment the new ones land. Emptying the table in place rather than replacing it is what makes the clear reach every holder — the runtime table is shared by every resolver of the copy the world holds under the guid, and a type may be holding it directly. A recorded version's copy, held in that version's folder, keeps a runtime table of its own and is left as it is.

Parameters

  • guid string — The asset's stable guid; the copy the world holds under it is the one forgotten.
require("@builtin::assetTypes.assetType.shared.ref").forgetRuntime(ref.guid)

modules/AssetRef/loadTypeBehavior

loadTypeBehavior(asset_type: string): ({ [string]: any }?, string?)

Load an asset type's behavior.luau module table, reporting a behavior that raised while loading. The first return is the module (nil when the type ships no behavior.luau); the second is set when the type HAS a behavior.luau that raised, and carries the require key plus the error it raised. A caller that runs the type's hooks — asset.create runs onCreate — reads the second return to tell "this type declares no behavior" from "this type's behavior is broken", which are opposite situations for the asset it is about to write.

Parameters

  • asset_type string — The type name (e.g. "dynamicAsset", "material").
local mod, err = require("@builtin::assetTypes.assetType.shared.ref").loadTypeBehavior("dialogue")

modules/AssetRef/loadTypeBehaviorOf

loadTypeBehaviorOf(type_ref: any): ({ [string]: any }?, string?)

Load the behavior.luau module table of the asset type type_ref names, from the folder the type asset lies in, the way loadTypeBehavior loads it once the name has resolved. For a caller that already holds the type asset, such as a row of asset.list("assetType").

Parameters

  • type_ref any (optional) — The type asset: a ref or a listing row holding its identity and path.
for _, t in ipairs(asset.list("assetType")) do local mod = require("@builtin::assetTypes.assetType.shared.ref").loadTypeBehaviorOf(t) end

modules/AssetRef/loadTypeModule

loadTypeModule(asset_type: string): { [string]: any }?

Load the full behavior.luau module table for an asset type (every key the type declares), or nil when the type ships no behavior.luau. Registry-driven resolution — same path the per-type ref dispatch uses. Exposed so the asset-change dispatcher (@builtin::assetTypes.assetType.shared.changeDispatch) can reach a type's onChange hook without duplicating the resolution logic.

Parameters

  • asset_type string — The type name (e.g. "dynamicAsset", "material").
local m = require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("dynamicAsset")

modules/AssetRef/persistInEditMode

persistInEditMode(self: any)

Generic edit-mode persistence hook an assetType calls when a change of its own is meant to reach the file. In EDIT mode, flush a ref's transient runtime overlay (ref.runtime) to its backing asset file by invoking the type's own saveDefinition(self), so the change syncs to peers and is saved. Works for any assetType that defines a saveDefinition; whether a given type's runtime writes route through here is that type's own contract. The write-through is rate-limited per asset: an asset carries an allowance of 8 writes that refills at one per 250ms. Changes made in one frame are coalesced onto a single re-emit, and a caller that changes a value and moves on has it on disk a frame or two later. A caller that keeps changing the same asset runs the allowance down to its refill rate, so over any span the asset costs at most that allowance plus one write per 250ms, whatever cadence the changes arrive at. In PLAY mode this is a deliberate no-op: runtime overlays stay transient (frame-fast) and are persisted back to the source asset on demand. An assetType opts in simply by exposing ref.saveDefinition; no per-type branching lives here.

Parameters

  • self any (optional) — Any AssetRef.
require("@builtin::assetTypes.assetType.shared.ref").persistInEditMode(matRef)

modules/AssetRef/preview

preview(self, opts: { [string]: any }?): { [string]: any }

Render this asset's preview in the preview studio, by the recipe its type declares under preview: in its type.yaml — or through the type's own preview when it defines one (kind: custom). A scene-instantiable type that declares nothing previews by instantiating the asset.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — { size? = { width, height }, angle? = { yaw, pitch }, live? }.
local p = asset.resolve("gold", "material"):preview({ size = { width = 256, height = 256 } })

modules/AssetRef/requireKeyOf

requireKeyOf(ref: any): string?

The key an asset's own code is required by from a type's behavior: its path, which the module loader answers with the module the asset's identity names, for a world asset and a library one alike. A ref that holds no path is required by the path its guid resolves to, and only a ref with neither by its identity.

Parameters

  • ref any (optional) — The asset ref.
local ok, mod = pcall(require, AssetRef.requireKeyOf(moduleRef))

modules/AssetRef/requireTypeModule

requireTypeModule(identity: string?, path: string?, stem: string?, accept: ((any) -> boolean)?): (any?, string?)

Require a module inside an asset folder — a type's <stem>.luau ("behavior", a settings schema file's own stem, or any other module a type.yaml names), or, with no stem, the asset's own entry module (its init.luau, as an importer or a component ships) — from wherever the asset lives (the builtin library, a world-authored folder, a vendored library), through the ONE resolution that works for all of them: see the comment above for the routes it tries in order.

Parameters

  • identity string? (optional) — The asset's own resolved identity (ref.identity), or nil.
  • path string? (optional) — The asset's own resolved folder path (ref.path), or nil.
  • stem string? (optional) — The module's file stem (its filename with .luau stripped), or nil for the asset's entry module.
  • accept ((any) -> boolean)? (optional) — Optional acceptance check a loaded module must satisfy to count as a hit — distinguishing "declares none" from "failed to load" for a module whose shape this caller knows. Omit to accept any table.
local schema, err = require("@builtin::assetTypes.assetType.shared.ref").requireTypeModule(identity, path, "settings")

modules/AssetRef/typeModuleKeys

typeModuleKeys(identity: string?, path: string?, stem: string?): { string }

The require keys requireTypeModule tries, in order, for a module inside an asset folder: the asset's identity with the stem appended, then the folder path under @local/. A key reads true from library.has once the module source is registered under it.

Parameters

  • identity string? (optional) — The asset's own resolved identity (ref.identity), or nil.
  • path string? (optional) — The asset's own resolved folder path (ref.path), or nil.
  • stem string? (optional) — The module's file stem, or nil for the asset's entry module.
for _, key in ipairs(AssetRef.typeModuleKeys(ref.identity, ref.path, "schema")) do print(library.has(key)) end

modules/AssetRefShapes/README

require("@builtin::assetTypes.assetType.shared.refShapes") -- AssetRefShapes

Publishes what every AssetRef<category> answers to, so a member read on an asset-typed value is checked against the category's own surface.

An asset category's per-instance surface is authored: a <name>.assetType/behavior.luau declares M.ref = { ... }, and each category declares its own. So the members an AssetRef<inputMap> carries are knowable only to inputMap itself — no fixed set of types covers the ones a world defines, and the checker has no way to guess them. This module reads each registered category's M.ref table from its source (never executing it) and renders it as a Luau table type, then hands the whole set to the engine. From there a ref:method(...) on an asset-typed value resolves against the category's real surface: a name it does not carry is reported with the list of the ones it does. The sweep is complete and replaces what was published before, so a category whose type is removed stops being published. It runs at world load, and again whenever a behavior.luau is written — the assetType type's own onChange hook re-publishes, which is what makes an edited surface take effect without a restart.

Usage: local AssetRefShapes = require("@builtin::assetTypes.assetType.shared.refShapes")

modules/AssetRefShapes/ensure

ensure()

Publish the surfaces if they are not known to be current, and do nothing when they are. This is what a checker calls before it reads them: it makes the published set complete at the moment of use rather than at some earlier moment that may not have arrived yet. A set a sweep left stale while it waited on bytes is swept again once the engine is past the frame that sweep ran in, so a run of checks in one frame pays for one sweep.

AssetRefShapes.ensure()

modules/AssetRefShapes/install

install()

Arm the world-load sweep. Idempotent.

AssetRefShapes.install()

modules/AssetRefShapes/instanceReturnsOf

instanceReturnsOf(typeRef: any, out: { { category: string, identity: string, method: string, definition: string, source: string } })

The instance-derived returns one category contributes: for each of its instances, the type each refShapes entry states for THAT asset. A category whose behavior declares no refShapes contributes none.

This is how a method whose result is shaped by the asset gets typed at all. inputMapRef:activate() answers one handle per binding the map declares — a set that is authored, differs per map, and changes when a control is added — so no fixed signature can state it and only the type itself can compute it.

Each entry is function(self) -> (typeExpression, source?): the type that call answers for THIS asset, and the module whose type vocabulary the expression is written in — the handles an inputMap:activate() answers are Handle, a name its own module declares. Omit the source when the expression names only types visible from anywhere.

Parameters

  • typeRef any (optional) — The AssetRef<assetType> for the category.
  • out { { category: string, identity: string, method: string, definition: string, source: string } } — Array the { category, identity, method, definition, source } records append to.
AssetRefShapes.instanceReturnsOf(asset.resolve("inputMap", "assetType"), {})

modules/AssetRefShapes/invalidate

invalidate(path: string?)

Mark the published set stale, so the next ensure publishes it again, in the same frame as well. With a path, a write landed at that path: the answers of the instances holding it are computed again and every other one is read from what was kept. Without one, every answer is computed again — what a changed type definition or a world load asks for.

Parameters

  • path string? (optional) — The written path, or nil to recompute everything.
AssetRefShapes.invalidate("/zero/source/controls.inputMap/bindings.yaml")

modules/AssetRefShapes/publish

publish(): number

Sweep every registered asset type and publish its ref: surface, plus every instance-derived return its types compute. Complete each time: a category the sweep does not reach stops being published, so a removed type's surface never lingers. The sweep never yields: a source whose bytes are not resident yet is fetched, left unstated, and stated by an ensure in a later frame, which sweeps again while one is outstanding.

AssetRefShapes.publish()

modules/AssetRefShapes/published

published(): { categories: { [string]: string }, returns: { [string]: { [string]: string } } }

What the checker currently believes, as { categories = { [category] = definition }, returns = { [identity] = { [method] = definition } } }. Publishes first, so it answers about the set a check would use rather than a stale one.

This is the answer to "is my type not published, or published and correct?" — from a call site the two look the same, because both are simply no diagnostic. Read it when a member access you expected to be reported was not.

AssetRefShapes.published().returns["@builtin::inputMaps.default"]

modules/AssetRefShapes/shapeOf

shapeOf(typeRef: any): string?

The Luau table type describing one category's ref: surface — the methods its behavior.luau declares, plus everything every AssetRef carries regardless of category. Returns nil when the type ships no behavior or declares no M.ref methods: a category that states nothing of its own is left opaque rather than described by the common members alone, so an access on it stays unchecked instead of being judged against a surface its author never wrote.

Parameters

  • typeRef any (optional) — The AssetRef<assetType> for the category.
local t = AssetRefShapes.shapeOf(asset.resolve("inputMap", "assetType"))

modules/AssetTypeAssetTypeRef/README

require("@builtin::assetTypes.assetType.behavior") -- AssetTypeAssetTypeRef

Per-instance methods exposed on every AssetRef<assetType>. Loaded lazily by assetType.shared.ref.

An AssetRef<assetType> points at a <typename>.assetType/ definition folder. SHARED CODE that a type exposes to every instance is declared in that type's own behavior.luau as a modules map of tracked requires, e.g.: -- inside <typename>.assetType/behavior.luau M.modules = { shared = require(".shared") } and reached from any instance through the pinned instance->type link: -- inside any foo.<thatType>/init.luau local api = asset.containing(FILE).modules.shared Resolution follows the instance's pinned typeRef guid (its .refs via = "asset_type" dep), so identically-named modules in different types never collide and an imported instance reaches the exact type version it was authored against. This is the mechanism that makes a type fully self-contained: the behaviour every instance relies on lives in the type, not duplicated as a require("modules.x") in 100 instance files. See @builtin::assetTypes.assetType.shared.ref.module for the dispatch.

Usage: local AssetTypeAssetTypeRef = require("@builtin::assetTypes.assetType.behavior")

modules/AssetTypeAssetTypeRef/getAbout

getAbout(self): { [string]: string }?

Read the three texts the type states about itself in its about.yaml: intent (what the type is for in someone's game), function (how it works) and usage (what a person or agent does with one). Only keys that hold written text are returned.

Parameters

  • self any (optional)
local about = assetTypeRef:getAbout(); if about then print(about.intent) end

modules/AssetTypeAssetTypeRef/getReadme

getReadme(self): string?

Read the asset type's own README.md body (type-level docs — what THE TYPE is, surfaced by asset.inspect).

Parameters

  • self any (optional)
print(assetTypeRef:getReadme())

modules/AssetTypeAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail for a type definition: { about }, the intent / function / usage texts of its about.yaml, when the type has written them.

Parameters

  • self any (optional)
local about = asset.inspect("font", "assetType").detail.about

modules/AssetTypeAssetTypeRef/onChange

onChange(ref, change)

Re-publish every asset category's ref: surface after a write inside a type definition.

Parameters

  • ref any (optional) — The AssetRef<assetType> for the edited type.
  • change any (optional) — { path, asset, type, kind, origin } for the write.
-- the engine calls this; a behavior.luau edit is picked up live

modules/AssetTypeAssetTypeRef/onCreate

onCreate(name: string): { [string]: string }

Generic-creation hook for asset.create("assetType", name). Scaffolds a new asset type by cloning this type definition's own template/ skeleton with the [name] placeholder rewritten to your type's name, so the new <name>.assetType/ is ready to edit (type.yaml, behavior.luau, and its own template/ instance body). Pure: returns the substituted file map.

Parameters

  • name string — The new type's name. Becomes its .<name> instance suffix.
asset.create("assetType", "Waypoint")

modules/AssetTypeAssetTypeRef/validate

validate(ref): { any }

asset.validate's semantic check of a type definition, read from the folder's own type.yaml. Every type names the file its Inspector sections come from with inspector: { type }; a type that names none is assetType.inspector_required, an error for a @builtin type the engine's library authors and a warning for a world's own or for one the library carries by reference (vfs.installedFrom), which the world that published it answers for (a push refuses only errors, so a world holding one still publishes, and its assets show the generic sections). A block that is not a map naming .luau files is assetType.inspector_block, a named file missing from the folder assetType.inspector_file_missing, and one that does not load or returns the wrong shape assetType.inspector_invalid, an error.

Parameters

  • ref any (optional) — The AssetRef<assetType> being validated.
local v = asset.validate("@builtin::assetTypes.material")

modules/AssetTypeDeclaration/README

require("@builtin::assetTypes.assetType.shared.declaration") -- AssetTypeDeclaration

A type's type.yaml, decoded: the declaration every question about the type as a whole reads from — which files it holds, whether it broadcasts its subassets, how its instances are pictured. Read once per type and held; the change dispatcher calls forget for every write inside an .assetType folder and for every removed type, so an edited type.yaml is read again.

Usage: local AssetTypeDeclaration = require("@builtin::assetTypes.assetType.shared.declaration")

modules/AssetTypeDeclaration/forget

forget()

Drop every held declaration, so each is read again the next time it is asked for.

declaration.forget()

modules/AssetTypeDeclaration/of

of(typeName: string): { [string]: any }?

The decoded type.yaml of typeName, or nil when no type answers to the name, it has no type.yaml, or the file does not decode — the last with a warning naming the type. The table is shared: read it, do not write to it.

Parameters

  • typeName string — The asset type's name ("bundle").
local broadcasts = declaration.of("package").broadcasts_subassets == true

modules/AssetTypePreviewRecipe/README

require("@builtin::assetTypes.assetType.shared.previewRecipe") -- AssetTypePreviewRecipe

How a type makes its instances' pictures, as its type.yaml declares it under preview:. The declaration names one of the four recipes the preview runtime implements, and whatever that recipe needs: | kind | The picture is | Needs | |---|---|---| | instantiate | the asset stood in the studio through its own instantiate | — | | image | a file the instance already holds, fitted to the frame | source.files | | surface | the asset painted on a builtin mesh | mesh | | custom | whatever the type's behavior.luau preview returns | — | It says how an instance's preview.png is PRODUCED. What is embedded for search is a separate declaration, indexing.content, and neither implies the other.

Usage: local AssetTypePreviewRecipe = require("@builtin::assetTypes.assetType.shared.previewRecipe")

modules/AssetTypePreviewRecipe/forget

forget()

Drop every held recipe, so each is read again from its type.yaml the next time it is asked for. The change dispatcher calls this for every write inside an .assetType folder and for every type removed.

previewRecipe.forget()

modules/AssetTypePreviewRecipe/of

of(typeName: string): PreviewRecipe?

The preview recipe typeName declares, or nil when it declares none — in which case its instances persist no picture. Read from the type's type.yaml once, and again after any of the type's files change.

Parameters

  • typeName string — The asset type's name ("material").
local r = previewRecipe.of("texture")   -- { kind = "image", files = { ... } }

modules/AssetTypeRuntime/README

require("@builtin::assetTypes.assetType.shared") -- AssetTypeRuntime

The assetType runtime — the machinery that reads a type's behavior.luau and turns its declarations into live behaviour.

A <typename>.assetType/behavior.luau is a table of DECLARATIONS: ref methods, modules shared code, events schema, namePattern, refShapes, and the onCreate / onRegister / onChange / onDelete / validate hooks. Nothing in that table runs itself. This module is what runs it, and it lives inside assetType.assetType/ because reading a type's declarations IS the assetType type's own behaviour — the same way material.assetType owns what a .material does. The sub-modules, one per stage of a type's life:

FieldReads from behavior.luauWhat it does
refref, modulesBuilds every AssetRef and dispatches per-type methods onto it.
createonCreate, onReplace, namePattern, creationInputsBacks asset.create — scaffolds a new instance of a type.
onRegisteronRegisterFires an instance's once-only registration hook.
changeDispatchonChange, onDelete, refShapesRoutes a VFS write or delete to the enclosing asset's type.
refShapesrefShapes, refPublishes each category's ref: surface to the checker.
eventseventsThe per-asset event runtime ref.events reads.
instantiableref.instantiateThe instantiation contract a type opts into — stands the root, applies the base placement opts, and shapes the return.
declarationtype.yamlThe type's decoded declaration, read once and held.
previewRecipetype.yaml preview:How an instance's preview.png is produced.
settingstype.yaml's settings: blockResolves an asset's schema, values file and defaults.
typeYamltype.yamlThe type's folder and its parsed type.yaml, read once per content version.
inspectortype.yaml's inspector: blockThe inspector declaration a type names for its assets.
Fields resolve LAZILY. The prelude requires the sub-modules in
dependency order (ref, then changeDispatch, onRegister.install(),
refShapes.install()); a table that required all of them on load would
take that ordering away from it.

Usage: local AssetTypeRuntime = require("@builtin::assetTypes.assetType.shared")

modules/AssetTypeSharedInspector/README

require("@builtin::assetTypes.assetType.shared.inspector") -- AssetTypeSharedInspector

Which inspectors a type declares, and the declaration each file returns. A type names them in type.yaml under inspector:, with either key or both: type: "inspector.luau" is the Inspector for every asset of the type (a file in the type folder); instance: "inspector.luau" is a file each asset may ship in its own folder, whose sections apply to what that asset declares — a component's instances on entities — and never to the asset's own view.

Usage: local AssetTypeSharedInspector = require("@builtin::assetTypes.assetType.shared.inspector")

modules/AssetTypeSharedInspector/blockFor

blockFor(typeName: string): any?

The inspector: value a type declares, or nil. Well-formed, it is a map naming type and/or instance; blockProblem says what is wrong with any other value.

Parameters

  • typeName string — The asset type's name.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local block = InspectorDecl.blockFor("material")

modules/AssetTypeSharedInspector/blockProblem

blockProblem(block: any): string?

Why an inspector: value is malformed, or nil. A block is a map naming type, instance or both, each a .luau file in the folder it applies to.

Parameters

  • block any (optional) — The decoded inspector: value.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local problem = InspectorDecl.blockProblem({ type = "inspector.luau" })

modules/AssetTypeSharedInspector/declarationFor

declarationFor(ref: any): (any?, string?, string?)

The declaration for ref in the asset view: its type's type: inspector.

Parameters

  • ref any (optional) — The asset ref.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local decl, problem, file = InspectorDecl.declarationFor(matRef)

modules/AssetTypeSharedInspector/declarationStamp

declarationStamp(typeName: string): string

Text that moves when what typeName's inspector declaration is made from changes: its type folder and inspector file, and each module the file requires, a module it required before that module was known included. Read after typeDeclaration, which reloads the declaration on such a change; empty for a type whose declaration was never read.

Parameters

  • typeName string — The asset type's name.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local s = InspectorDecl.declarationStamp("material")

modules/AssetTypeSharedInspector/declaresTypeInspector

declaresTypeInspector(typeName: string): boolean

Whether typeName's assets show a declared Inspector in their own view: an inspector: block naming type, or one malformed enough that its problem is what that view shows.

Parameters

  • typeName string — The asset type's name.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
if InspectorDecl.declaresTypeInspector("material") then print("declared") end

modules/AssetTypeSharedInspector/instanceDeclarationFor

instanceDeclarationFor(declarerRef: any): (any?, string?, string?)

The declaration declarerRef ships for the things it declares — a component's inspector.luau, applied to that component on an entity. (nil, nil, file) when the type names an instance: file this asset does not ship; (nil, nil, nil) when the type names none.

Parameters

  • declarerRef any (optional) — The declaring asset (a component).
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local decl, problem, file = InspectorDecl.instanceDeclarationFor(compRef)

modules/AssetTypeSharedInspector/loadIn

loadIn(typeRef: any, file: string): (any?, string?, string)

Load the inspector file file of the type definition typeRef — the type folder that ref names, whatever other type shares its name — and check its shape.

Parameters

  • typeRef any (optional) — The AssetRef<assetType> whose folder holds the file.
  • file string — The inspector file its type.yaml names.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local _, problem = InspectorDecl.loadIn(typeRef, "inspector.luau")

modules/AssetTypeSharedInspector/requiredSeverity

requiredSeverity(typeRef: any): string

How severe a missing inspector: block is on the type definition typeRef: "error" for a @builtin type the engine's library authors, "warning" for a world's own type and for one the library carries by reference (vfs.installedFrom), which the world that published it answers for. A push refuses only errors.

Parameters

  • typeRef any (optional) — The AssetRef<assetType>, or any table carrying its identity and path.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local severity = InspectorDecl.requiredSeverity(typeRef)

modules/AssetTypeSharedInspector/shapeProblem

shapeProblem(decl: any): string?

Check a declaration's shape: sections an array of sections, replace a map of id to section, order an array of ids; any other member a function.

Parameters

  • decl any (optional) — What the inspector file returned.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local problem = InspectorDecl.shapeProblem(decl)

modules/AssetTypeSharedInspector/typeDeclaration

typeDeclaration(typeName: string): (any?, string?, string?)

The declaration a type's own inspector file returns — the sections every asset of typeName shows. (nil, nil, nil) when the type names no type: inspector; (nil, problem, file?) when the block is malformed or the file is missing, fails to load or returns the wrong shape. The answer, a failure included, is held until a file of the type folder or a module the inspector file requires changes.

Parameters

  • typeName string — The asset type's name.
local InspectorDecl = require("@builtin::assetTypes.assetType.shared.inspector")
local decl, problem = InspectorDecl.typeDeclaration("material")

modules/AssetTypeSharedSettings/README

require("@builtin::assetTypes.assetType.shared.settings") -- AssetTypeSharedSettings

Typed per-asset settings: which schema governs an asset, what its values are, and where both are declared. A type's type.yaml names its schema file and its values file; nothing here is found by convention.

Usage: local AssetTypeSharedSettings = require("@builtin::assetTypes.assetType.shared.settings")

modules/AssetTypeSharedSettings/acceptedFieldNames

acceptedFieldNames(typeName: string): { string }

Every field name a write to typeName may carry: the fields its schema declares plus the older names its normalizeSettings maps (behavior.luau's normalizedSettingNames), sorted.

Parameters

  • typeName string — The asset type's name.
local names = Settings.acceptedFieldNames("texture")

modules/AssetTypeSharedSettings/applyCorrections

applyCorrections(

Apply typeName's cross-field corrections to a complete values table — the one path every settings write takes: resolveForCreate for a new asset, M.writeValues for every later write (asset.setSettings, a type's first-encode seed, the legacy heal). The type's settingsCorrections hook (M.settingsCorrectionsHookOf) is asked once, over a copy of values, for the fields a constraint the schema cannot express forces to another value.

explicit names the fields the writer asked for by name — the settings table passed to asset.create, the patch passed to asset.setSettings. Corrections merge into the values the way every settings layer does (M.mergeValues): a correction to one field of a group leaves the group's other fields. A correction over a field the writer did NOT name applies silently: nobody asked for that value, so overriding it states a fact. A correction that changes a leaf field the writer DID name refuses the whole write, naming the field's path (group.field inside a group), the value asked for, the value it is corrected to, and why it cannot hold (the hook's second return, keyed by the path or the top-level field, or a generic reason) — an explicit request is never silently rewritten into something else.

local fixed = Settings.applyCorrections("texture", values, patch, { label = "asset.setSettings" })

modules/AssetTypeSharedSettings/applyDerivedDefaults

applyDerivedDefaults(

Apply typeName's derived defaults to a complete values table: the type's settingsDerivedDefaults hook (M.derivedHookOf) is asked once, over a copy of values, and every field it answers that explicit does not name takes the answered value. A create passes every schema field as changed; a write passes the fields whose value it moved.

local values = Settings.applyDerivedDefaults("texture", values, { textureType = true }, { textureType = true })

modules/AssetTypeSharedSettings/blockFor

blockFor(typeName: string): any?

The settings: block a type declares, or nil when it declares none.

Parameters

  • typeName string — The asset type's name, e.g. "texture".
local block = Settings.blockFor("texture")

modules/AssetTypeSharedSettings/cloneValues

cloneValues(values: { [string]: any }): { [string]: any }

A deep copy of a settings values table — every table-valued field, and every table nested inside one (a Field.settingsOf group, a list of structs), copied at whatever depth it goes. Settings values are YAML-shaped data (tables, strings, numbers, booleans, never a cycle), so a plain recursive table copy is the whole guarantee this makes: nothing a hook or onCreate does to what it's handed, at ANY depth, can reach the table asset.create goes on to stamp into the values file.

Parameters

  • values { [string]: any } — The values table to copy.
local copy = Settings.cloneValues(resolved)

modules/AssetTypeSharedSettings/creationInputsOf

creationInputsOf(typeName: string): { [string]: string | boolean }

The creation inputs typeName declares, and the shape each one belongs to — its behavior.luau's exported creationInputs map, when it ships one, or an empty map. A key is a top-level asset.create opt the type's onCreate reads as what the asset is MADE FROM; its value is the name of the opt that selects the shape the input belongs to, or true for an input that belongs to every shape. soundClip declares { sampleRate = "pcm", channels = "pcm", ext = "bytes", writeSource = "bytes" }: sampleRate and channels describe raw PCM as it arrives and mean something only alongside pcm; ext names the container encoded source bytes are in, and writeSource keeps a copy of them, and both mean something only alongside bytes.

asset.create reads it before anything else touches the caller's opts. A declared input given alongside the opt that selects its shape is a creation input and reaches onCreate unchanged. A declared input given on any other shape has no meaning there and never reaches onCreate; one that names no declared setting refuses the create, naming the input and the opt it belongs with.

Parameters

  • typeName string — The asset type's name.
Settings.creationInputsOf("soundClip").sampleRate --> "pcm"

modules/AssetTypeSharedSettings/defaultsOf

defaultsOf(schema: any): { [string]: any }

Every declared default of schema, one value per field. A field declared without a default is absent rather than nil-valued.

Parameters

  • schema any (optional) — The Field schema table.
local values = Settings.defaultsOf(schema)

modules/AssetTypeSharedSettings/derivedHookOf

derivedHookOf(typeName: string): (({ [string]: any }, { [string]: true }, { [string]: true }) -> { [string]: any }?)?

typeName's own derived-defaults hook — its behavior.luau's exported settingsDerivedDefaults(values, explicit, changed), when it ships one — or nil. It answers the values of fields whose default follows another field (a texture's colour space follows its texture type), given the complete values, the fields the writer named (explicit) and the fields this write changed (changed). The framework applies a returned field only when the writer did not name it (M.applyDerivedDefaults).

Parameters

  • typeName string — The asset type's name.

Returns { [string]: any }?)? — The hook function, or nil when the type declares none.

local hook = Settings.derivedHookOf("texture")

modules/AssetTypeSharedSettings/diffFor

diffFor(ref: any, writtenPath: string): ({ [string]: any }?, { [string]: any }?)

The settings a write to writtenPath produced, and which fields it changed, when that path IS the asset's declared values file. A write to anything else answers (nil, nil). A values file that fails its schema answers (nil, nil) too, after reporting the violations — the asset keeps the settings it last had rather than taking an invalid set. changed is computed against an in-process baseline (last_values, forgotten on delete via forgetChangeBaseline) — a fresh process, or an asset diffed for the first time this session, reports every field as old = nil.

Parameters

  • ref any (optional) — The asset ref.
  • writtenPath string — The path that was written.
local values, changed = Settings.diffFor(ref, change.path)

modules/AssetTypeSharedSettings/fieldNames

fieldNames(typeName: string): { string }

Every field name typeName's settings schema declares, sorted.

Parameters

  • typeName string — The asset type's name.
local names = Settings.fieldNames("texture")

modules/AssetTypeSharedSettings/forgetChangeBaseline

forgetChangeBaseline(assetPath: string)

Forget the diff baseline diffFor holds for assetPath — the settings it last saw for that asset's own folder path. Called when the asset is deleted, so a later create that reuses the same path (a delete-then-recreate) diffs its first write against nothing rather than against what the deleted asset last held, and the baseline map does not grow for assets no longer alive.

Parameters

  • assetPath string — The asset's own folder path, as diffFor keys its baseline.
Settings.forgetChangeBaseline("/zero/source/Rock.texture")

modules/AssetTypeSharedSettings/healLegacyIfNeeded

healLegacyIfNeeded(ref: any, origin: string?)

Migrate ref's settings out of legacy storage, when a LOCAL edit just landed on an asset that still carries them there: settings in the legacy .metadata block move into its values file, and a values file holding an older spelling (one the type's normalizeSettings rewrites) is rewritten in the declared spelling — every existing asset self-heals this way as it is touched, rather than only on an explicit settings write. Called once per dispatched edit, generically, for every registered type (a type that declares no settings block, an asset whose values file its type ships no normalizeSettings for or holds only the declared spelling, or one with no values file whose .metadata carries no settings key, returns without writing — this is a no-op the overwhelming majority of the time, by construction, and convergent: a second call after healing finds the values file in the declared spelling and does nothing).

Never runs for a REMOTE-origin dispatch — the peer whose own edit this was heals it there, and the migration reaches every other session as an ordinary synced write once it does. Never runs for installed / foreign-origin content (isForeignOrigin, the same check M.writeValues already applies to the .metadata removal) — a library's own content is not this world's to rewrite.

The healed values are what M.valuesOf already reports for this asset — the stored settings read through the type's normalizeSettings (M.normalizeFor), filtered to declared fields, schema-checked — with the fields the type's settingsHealed hook changes for settings stored before its schema decided them (a texture whose settings predate compression heals to the compression it should have). The diff baseline is seeded with the values read before the heal, so that write's own follow-up dispatch reports only what the hook changed: nothing, when healing only relocates storage — the edit that triggered the heal already got its own reaction from the dispatch that called this — and the changed fields otherwise, which the type reacts to as to any settings change.

Parameters

  • ref any (optional) — The asset ref the just-applied edit landed on.
  • origin string? (optional) — The dispatch's origin ("local"/nil, or "remote") — see AssetChangeDispatch.dispatch.
Settings.healLegacyIfNeeded(ref, change.origin)

modules/AssetTypeSharedSettings/isForeignOrigin

isForeignOrigin(ref: any): boolean

True when ref's origin is outside this world — installed from a library, or carried by the engine's library by reference, rather than authored here or shipped with the engine. Read through the same envelope asset.inspect reports, origin.kind: "installed" for either, and "authored" for a world-authored or builtin asset. An origin asset.inspect cannot answer (it raises, or reports a shape these reads don't recognise) resolves foreign: what this guards is that a foreign asset's legacy settings are never dropped or rewritten, so an undetermined origin takes the side that leaves the asset's content as it is.

Parameters

  • ref any (optional) — The asset ref.
if not Settings.isForeignOrigin(ref) then Settings.healLegacyIfNeeded(ref, "local") end

modules/AssetTypeSharedSettings/mergeValues

mergeValues(schema: any, base: { [string]: any }?, overlay: { [string]: any }?): { [string]: any }

Lay overlay over base under schema, the one way settings layers combine: a field overlay names replaces the field in base, except a struct field (a Field.struct, or a Field.settingsOf group) that both hold as tables, which merges field by field at whatever depth it nests — so a layer naming one field of a group leaves the group's other fields where the layers below put them. Neither input is modified; the result is a deep copy.

Parameters

  • schema any (optional) — The Field schema the values follow (the fields of a struct at depth), or nil to replace every named field.
  • base { [string]: any }? (optional) — The values below.
  • overlay { [string]: any }? (optional) — The values laid over them.
Settings.mergeValues(schema, { group = { a = 1, b = 2 } }, { group = { b = 3 } }) --> { group = { a = 1, b = 3 } }

modules/AssetTypeSharedSettings/normalizeFor

normalizeFor(typeName: string?, values: { [string]: any }, stored: { [string]: any }?): { [string]: any }

values in the spelling typeName's schema declares: the type's normalizeSettings applied, or values itself when it ships none. Every settings source passes through here — a values file, a legacy .metadata block, a create's preset / importer / caller layers, a asset.setSettings patch — so an older spelling reads, creates and writes as the field it names today. The hook is handed copies; neither table passed here is modified.

Parameters

  • typeName string? (optional) — The asset type's name (nil answers values unchanged).
  • values { [string]: any } — A settings table in any spelling the type accepts.
  • stored { [string]: any }? (optional) — The asset's current values, when values is a patch over them.
local values = Settings.normalizeFor("texture", { format = "bc7" })

modules/AssetTypeSharedSettings/normalizeHookOf

normalizeHookOf(typeName: string): (({ [string]: any }, { [string]: any }?) -> { [string]: any })?

typeName's own normalisation hook — its behavior.luau's exported normalizeSettings(values, stored?), when it ships one — or nil. The hook maps every older spelling a stored file or a write may carry (a renamed field, an older enum member) to the field and member the schema declares now, and returns a new table. stored, when given, is the asset's current values, which an older spelling in a patch may need to keep the meaning it had against them.

Parameters

  • typeName string — The asset type's name.

Returns { [string]: any })? — The hook function, or nil when the type declares none.

local hook = Settings.normalizeHookOf("texture")

modules/AssetTypeSharedSettings/orderedNames

orderedNames(schema: any): { string }

The field names of schema in the order they are declared: by each descriptor's declaredOrder, a name breaking a tie and a descriptor without one sorting after those that carry one.

Parameters

  • schema any (optional) — The Field schema table.
for _, name in ipairs(Settings.orderedNames(schema)) do print(name) end

modules/AssetTypeSharedSettings/presetLayerFor

presetLayerFor(typeName: string, destPath: string): ({ [string]: any }?, any?)

The Preset Manager's layer for a new asset of typeName at destPath: the valid values of the world's first [[presets]] row naming the type's assetType whose filter matches the new asset's path (under /source for a durable asset, under /runtime/assets for a runtime one), and the match. Nil for any other destination, a type with no type-level schema, or no matching row.

Parameters

  • typeName string — The asset type being created.
  • destPath string — Where the new asset lands.
local layer = Settings.presetLayerFor("texture", "/zero/source/sprites/hero.texture")

modules/AssetTypeSharedSettings/recordedWrite

recordedWrite(path: string, text: string)

Write text to path, recording the write into the open undo operation when one is open so undoing it restores the file's previous contents. Raises when the write fails.

Parameters

  • path string — VFS path.
  • text string — The whole new contents.
local Yaml = require("@builtin::modules.yaml")
Settings.recordedWrite(ref.path .. "/values.yaml", Yaml.encode(doc))

modules/AssetTypeSharedSettings/requiredModulesCurrent

requiredModulesCurrent(record: any, ref: any): boolean

Whether a value read alongside record still stands: every module it listed holds the version it was listed at, and — when some required module was not yet known — none of those has become known since. That last question is asked again at most every quarter second, so a read held while a module was unknown is not read again on every call.

Parameters

  • record any (optional) — What requiredModulesRecord answered.
  • ref any (optional) — The asset the record was taken for.
if held and Settings.requiredModulesCurrent(held.modules, ref) then return held.value end

modules/AssetTypeSharedSettings/requiredModulesRecord

requiredModulesRecord(ref: any): { deps: { { path: string, version: number } }, depsPending: boolean, depsCheckedAt: number }

What a read derived from ref's code depends on beyond ref's own files: the entry file of each module its sources require (as its reference table has bound them), with the content versions they have now, and whether some module it requires is not yet known. Hold it beside the value read, and ask requiredModulesCurrent before serving the value again.

Parameters

  • ref any (optional) — The asset.
held.modules = Settings.requiredModulesRecord(ref)

modules/AssetTypeSharedSettings/requiredModulesStamp

requiredModulesStamp(record: any, ref: any): string

Text that moves when a module a record listed changes, or when a module it required before that module was known becomes known (asked at most every quarter second, as requiredModulesCurrent asks it): the content versions of its listed modules now, and whether the read beside it has gone stale. Fold it into a build signature to rebuild what was read from code that requires them.

Parameters

  • record any (optional) — What requiredModulesRecord answered.
  • ref any (optional) — The asset the record was taken for.
parts[#parts + 1] = Settings.requiredModulesStamp(held.modules, ref)

modules/AssetTypeSharedSettings/resolveForCreate

resolveForCreate(

Resolve the settings a created asset is stamped with. For a new asset the layers, lowest priority first, are: the schema's declared defaults, the type's own settingsDefaults hook, the Preset Manager's first matching row, the creating importer's settings for this type, and the settings table the caller passed. When destPath already holds an asset of typeName that records settings of its own, it is being re-authored — overwrite = true, or an importer re-creating what it made inside its container — and those settings take the place of every layer below the caller's, so what it was stamped with and edited to stays. An asset that records none (a hand-written container being seeded) takes the layers a new asset takes. Each layer overrides the ones before it, field by field, and a struct field merges field by field inside (M.mergeValues). Each layer is read through the type's normalizeSettings (M.normalizeFor), so a layer naming a field in an older spelling sets the field it names today. Once every layer has merged, the type's derived defaults (M.applyDerivedDefaults) set each field whose default follows another and that no layer above the schema named; for a re-authored asset its own settings name nothing, and a derived field follows only a field the caller's settings changed. Then M.applyCorrections applies the type's cross-field corrections with the caller's settings table as the explicit fields: a correction over a field that came from any layer below the caller applies silently, and one that contradicts a field the caller's own settings table set refuses the creation, naming the field, the value asked for, and why it cannot hold.

The (possibly corrected) result is checked against the schema, so a bad value nothing corrects refuses the creation rather than landing in a new asset.

local values = Settings.resolveForCreate("texture", name, dest, opts, opts.settings, nil)

modules/AssetTypeSharedSettings/resolvePatch

resolvePatch(ref: any, patch: { [string]: any }): ({ [string]: any }, { [string]: any })

The complete values a partial settings patch writes — the values asset.setSettings hands M.writeValues. The patch is read through the type's normalizeSettings against the asset's current values, so an older field name lands as the field it names today with the meaning it had for this asset, and is merged over those values, a struct group field by field (M.mergeValues); the type's derived defaults (M.applyDerivedDefaults) then move each field that follows a field this patch changed, unless the patch names it too. M.writeValues applies the corrections with the normalised patch as the explicit fields.

Parameters

  • ref any (optional) — The asset ref.
  • patch { [string]: any } — Any subset of the schema's fields, in the current or an older spelling.
local values, named = Settings.resolvePatch(texRef, { compression = "uastc" })

modules/AssetTypeSharedSettings/schemaFor

schemaFor(ref: any): any?

The schema governing ref's own settings values — the type-level schema when its type declares one, else the schema the asset declares for itself. ref naming an assetType asset itself (rather than an instance of one) reports that TYPE's own declared schema — the same schema every instance of it reports.

Parameters

  • ref any (optional) — The asset ref.
local schema = Settings.schemaFor(texRef)

modules/AssetTypeSharedSettings/schemaForType

schemaForType(typeName: string): any?

The schema governing the settings of assets of typeName, when the type declares one at type level (schema: { type = ... }).

Parameters

  • typeName string — The asset type's name.
local schema = Settings.schemaForType("texture")

modules/AssetTypeSharedSettings/schemaOfDeclarer

schemaOfDeclarer(identity: string): any?

The schema a declarer declares, named by its identity. This is what Field.settingsOf(identity) resolves to: an assetType's instance-settings schema, or an importer's / component's own declared schema.

Parameters

  • identity string — The declarer's asset identity.
local schema = Settings.schemaOfDeclarer("@builtin::assetTypes.texture")

modules/AssetTypeSharedSettings/schemaOfInstance

schemaOfInstance(ref: any): any?

The schema an individual asset declares for itself, when its type says the schema is per instance (schema: { instance = ... }). Whether an instance may lack the file is what the type's files: block says of it: a schema file listed under files.optional may be absent, and an instance without one declares no settings; one listed under files.required, or not listed at all, must load, and a missing one is the instance's declaration broken, and is logged.

Parameters

  • ref any (optional) — The asset ref.
local schema = Settings.schemaOfInstance(importerRef)

modules/AssetTypeSharedSettings/schemaProblemOf

schemaProblemOf(ref: any): string?

Why ref's settings schema resolved to nothing: the declaring file could not be read, compiled, run, or did not declare its table; or the type refused what it declared — the sentence its behavior.luau's exported settingsSchemaProblem(schema, ref) returned. Nil when the schema resolved or ref's type declares none.

Parameters

  • ref any (optional) — The asset ref.
local why = Settings.schemaProblemOf(compRef)

modules/AssetTypeSharedSettings/seedChangeBaseline

seedChangeBaseline(assetPath: string, values: { [string]: any })

Record values as the settings diffFor last saw for assetPath, so the dispatch of a values file written with exactly these values reports nothing changed. Every writer that has ALREADY done what those values ask for calls it with what it writes: asset.create after stamping a new asset (its onCreate encoded against them), a type's first-encode seed of a hand-written container, and the legacy heal (which moves values without changing one). A later write that changes a field still diffs against these and reports that field.

Parameters

  • assetPath string — The asset's own folder path, as diffFor keys its baseline.
  • values { [string]: any } — The complete values table the values file holds.
Settings.seedChangeBaseline(rawget(texRef, "path"), values)

modules/AssetTypeSharedSettings/settingsCorrectionsHookOf

settingsCorrectionsHookOf(typeName: string): (({ [string]: any }, string, { [string]: any }?) -> ({ [string]: any }?, { [string]: string }?))?

typeName's own settings-corrections hook — its behavior.luau's exported settingsCorrections(values, name, opts), when it ships one — or nil. Where settingsDefaults supplies an opinion a layer below the caller can still override, this hook enforces a cross-field constraint the schema itself has no way to express (a field whose valid range depends on another field's resolved value). M.applyCorrections calls it on every settings write — once a create's layers have merged, and on every later write of the values file. Called with the complete values; it READS them and RETURNS a table of the corrections to apply (or nil for none), rather than mutating what it was given. A SECOND return, { [field]: reason }, names why a corrected field cannot hold the value it is being corrected away from — used only when the writer named that field explicitly, to explain the refusal; omitted or missing a field falls back to a generic reason.

Parameters

  • typeName string — The asset type's name.

Returns ({ [string]: any }?, { [string]: string }?))? — The hook function, or nil when the type declares none.

local hook = Settings.settingsCorrectionsHookOf("texture")

modules/AssetTypeSharedSettings/settingsDefaultsHookOf

settingsDefaultsHookOf(typeName: string): ((string, { [string]: any }?, { [string]: any }) -> { [string]: any }?)?

typeName's own create-time settings-defaults hook — its behavior.luau's exported settingsDefaults(name, opts, asked), when it ships one — or nil. A type contributes a layer here for whatever it can only compute at creation (a name-derived convention, a fact read off a pre-encoded payload the caller handed it, a default that follows a field another layer sets); resolveForCreate calls it once per create, above the schema's own defaults and below every other layer, with asked holding what the layers above it (the Preset Manager, an importer, the caller) name, field by field, the higher layer's value where two name one. The hook returns a fresh table of its own opinions; it is never handed anything to mutate, and a field it returns that a layer above it names takes that layer's value.

Parameters

  • typeName string — The asset type's name.

Returns { [string]: any }?)? — The hook function, or nil when the type declares none.

local hook = Settings.settingsDefaultsHookOf("texture")

modules/AssetTypeSharedSettings/settingsHealedHookOf

settingsHealedHookOf(typeName: string): (({ [string]: any }, { [string]: any }) -> { [string]: any }?)?

typeName's own settings-heal hook — its behavior.luau's exported settingsHealed(values, stored), when it ships one — or nil. Healing moves an asset's settings out of older storage on a local edit; this hook answers the fields whose value that move changes, for content whose stored settings predate what the type's schema now decides. values is what a read of the stored settings reports, stored the settings exactly as stored (before an older spelling is read as today's). It returns the fields to change, or nil for none.

Parameters

  • typeName string — The asset type's name.

Returns { [string]: any }?)? — The hook function, or nil when the type declares none.

local hook = Settings.settingsHealedHookOf("texture")

modules/AssetTypeSharedSettings/valuesEqual

valuesEqual(a: any, b: any): boolean

Structural equality for a settings value at any depth: two tables are equal when they hold the same keys and every value is itself equal (recursively), and two scalars (string, number, boolean, nil) are equal by ==. Table VALUES in a settings table are always freshly built — defaultsOf clones a default, read_values_file decodes a fresh table from YAML — so comparing them by reference would report a table-valued field as changed on every read even when its contents are identical. The one comparison every settings-value equality check in this module shares — diffFor's own diff and resolveForCreate's caller-conflict check both call this rather than each carrying its own. Takes acyclic settings values — YAML-shaped data, the same invariant clone_value relies on — so the recursive walk always terminates.

Parameters

  • a any (optional) — A settings value (a scalar or a table, at any nesting depth).
  • b any (optional) — A settings value to compare it against.
Settings.valuesEqual({ a = 1, b = { 2, 3 } }, { a = 1, b = { 2, 3 } }) --> true

modules/AssetTypeSharedSettings/valuesOf

valuesOf(ref: any): { [string]: any }?

This asset's settings values: every declared default, overlaid with what its values file holds — or, for content that predates the values file, with its legacy .metadata settings. A field neither sets reads as its declared default, so content written before a field existed still answers for it. A stored field the schema no longer declares is filtered out, so a schema revision that drops a field leaves old content reading as clean rather than carrying a value nothing recognises. A stored value that fails its own field's constraint is dropped the same way — the field falls back to its declared default and a warning names the path, the field and the violation — so a read never answers with a value the schema (and therefore the type's own encode) refused. The values are resolved in memory from what is already on disk. An asset whose type declares no values file holds no settings values of its own — a component's live on each entity it is on — and reads as nil, while its schema still answers through schemaFor.

Parameters

  • ref any (optional) — The asset ref.
local s = Settings.valuesOf(texRef)

modules/AssetTypeSharedSettings/valuesPathOf

valuesPathOf(ref: any): string?

The path of ref's settings values file, per its type's declared settings.values, or nil when its type declares no values file. A ref that carries its guid or identity without its path reaches the same file through what that name resolves to.

Parameters

  • ref any (optional) — The asset ref.
local p = Settings.valuesPathOf(texRef)

modules/AssetTypeSharedSettings/whyNoSettings

whyNoSettings(ref: any): string

Why ref carries no settings, as a sentence naming where they would be declared: the type declares no settings block; or the type declares a schema per instance and this instance ships none, naming the file it would be; or the type's declared schema did not load.

Parameters

  • ref any (optional) — The asset ref, one for which M.schemaFor answered nil.
error("asset.setSettings: " .. Settings.whyNoSettings(ref))

modules/AssetTypeSharedSettings/writeActedValues

writeActedValues(ref: any, values: { [string]: any })

Write ref's whole values file (M.writeValues) with values, the settings the caller has already acted on, and seed the change baseline with what was written (M.seedChangeBaseline) before that write's dispatch runs, so the dispatch reports nothing changed and no reaction runs a second time.

Parameters

  • ref any (optional) — The asset ref.
  • values { [string]: any } — The complete values table.
Settings.writeActedValues(texRef, values)

modules/AssetTypeSharedSettings/writeValues

writeValues(ref: any, values: { [string]: any }, explicit: { [string]: any }?): { [string]: any }

Write this asset's whole settings values file. The values (and explicit) are read through the type's normalizeSettings first, so an older spelling is stored as the field it names today. They then take the type's cross-field corrections (M.applyCorrections, with explicit as the fields the writer named — a correction contradicting one of them refuses the write), then are checked against the schema. Every violation is reported in one error naming the field, what it takes, and where the schema is declared. When the asset also carries legacy .metadata settings and its origin is authored — this world's own content, or the engine's builtin library — the legacy key is dropped as part of this same write, so the values file above is what a read prefers from here on. A library-installed asset keeps its .metadata settings key alongside the values file this call writes. Inside an undo operation, the write is recorded so undoing the operation restores the previous file contents; authored legacy content is first healed into its values file, so undo returns it to the healed file, which reads the same values as the legacy one. When that heal does not complete, the legacy settings stay in .metadata and the step records the values file alone, so undo falls back to the legacy values.

Parameters

  • ref any (optional) — The asset ref.
  • values { [string]: any } — The complete values table.
  • explicit { [string]: any }? (optional) — The fields the writer asked for by name (a patch), or nil.
Settings.writeValues(texRef, { filter = "nearest", colorSpace = "srgb" })

modules/AssetTypeSharedTypeYaml/README

require("@builtin::assetTypes.assetType.shared.typeYaml") -- AssetTypeSharedTypeYaml

A type's type.yaml, parsed: the folder a type lives in and the document its type.yaml holds, read once per content version of that file. Every block a type declares — settings:, inspector: — is read from here, and so are the texts of its about.yaml (about, aboutAt).

Usage: local AssetTypeSharedTypeYaml = require("@builtin::assetTypes.assetType.shared.typeYaml")

modules/AssetTypeSharedTypeYaml/about

about(typeName: string): { [string]: string }?

The written about.yaml texts of the type typeName; see aboutAt.

Parameters

  • typeName string — The asset type's name.
local about = TypeYaml.about("font")

modules/AssetTypeSharedTypeYaml/aboutAt

aboutAt(folder: string): { [string]: string }?

The texts the type folder at folder states in its about.yaml: intent (what the type is for in someone's game), function (how it works) and usage (what a person or agent does with one). Only the keys that hold written text are returned; a key that is missing, empty or still starts with the [[describe]] marker is left out.

Parameters

  • folder string — The type's folder, e.g. /zero/source/libs/@builtin/assetTypes/font.assetType.
local about = TypeYaml.aboutAt("/zero/source/libs/@builtin/assetTypes/font.assetType")

modules/AssetTypeSharedTypeYaml/aboutMarkdown

aboutMarkdown(about: { [string]: string }?): string

The about.yaml texts as a Markdown section titled "About this type", each text under its label. Empty text when no key holds written text.

Parameters

  • about { [string]: string }? (optional) — What about / aboutAt returned, or nil.
print(TypeYaml.aboutMarkdown(TypeYaml.about("font")))

modules/AssetTypeSharedTypeYaml/aboutRows

aboutRows(about: { [string]: string }?): { { key: string, label: string, text: string } }

The labelled about.yaml texts as { { key, label, text } } in reading order: Intent, Function, Usage. Empty when no key holds written text.

Parameters

  • about { [string]: string }? (optional) — What about / aboutAt returned, or nil.
for _, row in ipairs(TypeYaml.aboutRows(TypeYaml.about("font"))) do print(row.label, row.text) end

modules/AssetTypeSharedTypeYaml/describeBlock

describeBlock(typeName: string): { guidance: string?, examples: { string } }

What services describe hands the model when it writes a description for an asset of this type: the type's own guidance on what to cover, and the identities of the examples whose descriptions show the shape.

Parameters

  • typeName string — The asset type's name.
local block = TypeYaml.describeBlock("module"); print(block.guidance)

modules/AssetTypeSharedTypeYaml/described

described(typeName: string): { DescribedFile }

The files a type's assets must describe: every file its type.yaml declares with a described content check, with the keys and headings the check names and the limit on each key. Empty when the type asks its assets for no description.

Parameters

  • typeName string — The asset type's name.
for _, f in ipairs(TypeYaml.described("module")) do print(f.file, f.required) end

modules/AssetTypeSharedTypeYaml/folder

folder(typeName: string): (string?, string?)

The folder the assetType typeName lives in, and its identity.

Parameters

  • typeName string — The asset type's name, e.g. "texture".
local folder, identity = TypeYaml.folder("material")

modules/AssetTypeSharedTypeYaml/of

of(typeName: string): { [string]: any }?

The document typeName's type.yaml holds, decoded. Read again when the file's content changes.

Parameters

  • typeName string — The asset type's name.
local doc = TypeYaml.of("material"); print(doc.suffix)

modules/AssetValidator/README

require("@builtin::systems.worldValidation.assetValidator") -- AssetValidator

Structure validator for asset folders. Delegates each asset to asset.validate — the engine primitive that checks the folder against its type's type.yaml (required files, one_of_groups, unexpected entries, content constraints), waives the describe-this-asset requirements for private subassets (assets nested inside a non-broadcasting container like a .bundle), and runs the type's own semantic validate hook — and maps the result into the report's Problem records.

asset.validate is the same primitive the world.push gate runs, so a world that validates clean here also passes the structural half of the publish gate. Each problem is attributed to the asset path (missing files anchor at the asset root; existing files — unexpected entries, content violations — anchor at the file). An asset whose type has no registered type.yaml gets a schema.unknown_type warning; an asset the registry cannot resolve (e.g. written this frame, registration still draining) gets a schema.unresolved_asset warning.

Usage: local AssetValidator = require("@builtin::systems.worldValidation.assetValidator")

modules/AssetValidator/dependencyProblemsFrom

dependencyProblemsFrom(path: string, deps: any, guidCache: { [string]: boolean })

Map an asset's reference table into publish-blocking Problem records. Split from the lookup so the mapping can be exercised on a reference table directly. A guid already present in guidCache is not looked up again.

Parameters

  • path string — Asset path the problems are attributed to.
  • deps any (optional) — Reference table in the shape asset.deps returns.
  • guidCache { [string]: boolean } — Memo of guid → whether the live asset index carries it.
local problems = AssetValidator.dependencyProblemsFrom(path, asset.deps(path), {})

modules/AssetValidator/validate

validate(entry, guidCache: { [string]: boolean }?)

Validate one asset folder via asset.validate and map the result into Problem records.

Parameters

  • entry any (optional) — Asset entry from vfsScanner — { path, name, type }.
  • guidCache { [string]: boolean }? (optional)
local problems = AssetValidator.validate({ path = "/source/Foo.component", name = "Foo.component", type = "component" })

modules/AssetValidator/validateBatch

validateBatch(assets)

Validate a batch of asset entries and flatten the per-asset problem lists into one array. Convenience wrapper over M.validate.

Parameters

  • assets any (optional) — Array of asset entries from the scanner.
local all = AssetValidator.validateBatch(bucket.assets)

modules/AvatarAssetTypeBehavior/README

require("@builtin::assetTypes.avatar.behavior") -- AvatarAssetTypeBehavior

Behaviour for the avatar asset type — a playable character composed from three INDEPENDENT parts so each swaps on its own axis: - body — a .bundle: skinned mesh + bone entities (the look). - controller — the movement system (default: CharacterController + MovementState + Humanoid, the standard kinematic humanoid mover). - animation — the system that animates the body (default: the shared Locomotion blend space, which reads the controller's velocity and plays retargeted clips; or ClipPlayer; or your own component). Movement and animation are NOT tied together: attaching the controller never pulls in Locomotion, and choosing your own animation keeps the standard controller — so authoring your own locomotion is a one-field swap. The clip set retargets onto any humanoid rig, so the default locomotion drives any humanoid body with no per-body setup. :instantiate() composes the parts; :setCharacter(root, body) swaps the body. Point a scene's PlayerPrototype.body at a body carrying this avatar to make it the body every connecting player spawns with.

Usage: local AvatarAssetTypeBehavior = require("@builtin::assetTypes.avatar.behavior")

modules/AvatarAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: any): (EntityRef, { [string]: string })

Instantiate this avatar: spawn an avatar root, spawn the body bundle under it, then attach the movement controller and the animation system — independently. With the defaults the root gets the standard humanoid controller and the shared Locomotion that drives the body with retargeted clips. Pass a target entity REF to make that entity the avatar root (the player-avatar path passes the freshly-spawned avatar entity here).

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional avatar root as an EntityRef. Omit to spawn one.
  • opts any (optional) — Optional { position, rotation, scale, name, temporary, idMap, diff, sourceTag } — the base placement opts land on the root; idMap pins the composed body root, label, and every body-bundle child to stable ids across re-instantiations; diff replays the user's sparse edits to the composed body. sourceTag marks the call as owned — the Asset component sets it when it drives the composition itself.
local av = avatarRef:instantiate()

modules/AvatarAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Generic-creation hook for asset.create("avatar", name, opts). Composes the avatar from a body plus an independent movement controller and animation system, written as avatar.json. The body is a .bundle (skinned mesh + bones), a plain .mesh (a simple visual), or omitted (a body-less avatar — a controller / first-person camera with no mesh). Defaults: the standard humanoid controller and, for a humanoid body, the shared Locomotion (clips from the locomotion preset, default "synty"). Override animation to author your own locomotion without touching movement; pass controller = false for a body the engine doesn't move; pass clip for a single-clip ClipPlayer. Humanoid-ness drives the default animation only and is auto-derived from a rigged body; pass humanoid to set it explicitly (e.g. a body-less first-person avatar that still carries Humanoid).

Parameters

  • name string
  • opts CreateOpts? (optional)
asset.create("avatar", "my_hero", { body = heroBundle })  -- standard controller + locomotion
asset.create("avatar", "fp_player", { humanoid = true })  -- body-less first-person player

modules/AvatarAssetTypeBehavior/setCharacter

setCharacter(self, root: any, body: any): EntityRef

Swap the body on a live avatar. Despawns the current body and spawns body (a .bundle ref) in its place. The controller + animation systems live on the avatar root and re-resolve the new body's rig, so the look swaps while movement and animation are kept. A single-clip ClipPlayer rides the skinned body, so it is re-attached to the new body. Every item seated with Humanoid:attach is kept and re-seated on the new body's socket for the same point, with the same transform, as soon as the new body's skeleton stands (within the call when the body wakes inside it); an item whose point the new body does not have stays on the avatar root. The new body takes the old body's id and is composed with the options the avatar was instantiated with, and the avatar's Asset id map is updated to name it.

Parameters

  • self any (optional)
  • root any (optional) — The live avatar root (an EntityRef from :instantiate()).
  • body any (optional) — The new body — a .bundle ref (guid / identity / AssetRef).
avatarRef:setCharacter(avatarRoot, asset.resolve("knight", "bundle"))

modules/AvatarTypeAssetTypeBehavior/README

require("@builtin::assetTypes.avatarType.behavior") -- AvatarTypeAssetTypeBehavior

Behaviour for the avatarType asset type. An avatar type describes a kind of body (a humanoid, a quadruped, a vehicle): the roles and chains a rig of that kind maps onto, and how to find them in a skeleton. An avatar points at its type by guid, and a rig stores its author's overrides per type. The type's init.luau is the module; ref:module() loads it.

Usage: local AvatarTypeAssetTypeBehavior = require("@builtin::assetTypes.avatarType.behavior")

modules/AvatarTypeAssetTypeBehavior/onChange

onChange(self, _change)

Drop the cached module and advance the generation when the type's content changes, so the next :module() loads the edited init.luau and every rig qualified under the old one is qualified again.

Parameters

  • self any (optional) — The avatarType AssetRef that changed.
  • _change any (optional) — What happened to it; the cache is dropped whatever it was.

modules/BlankAppLayout/README

require("@builtin::_templates.blank_app.blank_app_layout") -- BlankAppLayout

Minimum-viable app — one screen built from the raw CSS-parity widget tree. Clone-and-edit starting point; replace build with your own tree.

Usage: local BlankAppLayout = require("@builtin::_templates.blank_app.blank_app_layout")

modules/BundleAssetTypeRef/README

require("@builtin::assetTypes.bundle.behavior") -- BundleAssetTypeRef

Per-instance methods exposed on every AssetRef<bundle>. Loaded lazily by assetType.shared.ref.

Usage: local BundleAssetTypeRef = require("@builtin::assetTypes.bundle.behavior")

modules/BundleAssetTypeRef/getTemplate

getTemplate(self): any

Parse the bundle's entity_template into a Lua value. The engine format is a flat array of entity records ({ name, original_id, parent_id, components, ... }), so the result is an array-table; the return type is any because the payload is decoded JSON. Returns nil when parsing fails or the bundle has no template. Parsed results are cached per-guid — repeat :instantiate on the same bundle reuses the parsed table instead of re-decoding the JSON. Cleared when the template is written — by :setTemplate or straight to the bundle's entity_template — so authoring-time edits land on the next instantiate.

Parameters

  • self any (optional)
local t = bundleRef:getTemplate()

modules/BundleAssetTypeRef/getTemplateRaw

getTemplateRaw(self): string?

Read the raw entity_template file (JSON) as a string. Use :getTemplate() for a parsed table.

Parameters

  • self any (optional)
local raw = bundleRef:getTemplateRaw()

modules/BundleAssetTypeRef/inspectDetail

inspectDetail(self): any

asset.inspect type-specific detail: { nodeCount, attachedComponents, rootName }, parsed from the bundle's own entity_template JSON — never instantiates the hierarchy. A bundle with no readable template returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local attached = asset.inspect(bundleRef).detail.attachedComponents

modules/BundleAssetTypeRef/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this bundle. The DEFAULT — bundle:instantiate() with no target entity — spawns ONE root entity carrying an Asset component that references this bundle: the component spawns the bundle's hierarchy as TEMPORARY runtime children (never written to the saved scene) and owns save / reload / diff, so the scene stays clean (one entity per instance, not the whole exploded permanent tree). This is the one-call path to put a bundle in the world.

Passing a target entityId (instantiate ONTO that entity, which becomes the bundle root) OR opts.raw = true (build a permanent fresh hierarchy) selects the RAW explode: it spawns every child in the entity_template, remaps cross-entity references, applies any saved sparse diff, and writes the bundleProvenance live-link attribute, returning the root entity id and the originalId → runtimeId map. The Asset component, Player avatars, and template tooling use this raw path.

Spawn mode is selected by opts.deferred. Default (deferred = false) uses the legacy batch() flush — every component is visible on the SAME tick so spawn-then-query patterns (tests, scene-load critical path, spawn-then-read flow) keep working. Pass opts.deferred = true for hundreds-to-thousands-of-entities bundles (large levels, density spawners, gibbed-prop debris bundles): the spawn+component work wraps in queue() and the engine drainer spreads it across multiple frames at the QUEUE_DRAIN_BUDGET_PER_FRAME cap. The root entity id is minted same-tick either way (entity.spawn always returns immediately), but in deferred mode children + components arrive in the ECS over the next few frames — don't read them back synchronously.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional target entity — an entity REF (proxy) to explode ONTO (that entity becomes the bundle root; this selects the RAW path). Omit for the default (fresh root + Asset component).
  • opts { [string]: any }? (optional) — Optional { position, rotation, scale, name, temporary, raw, idMap, diff, sourceTag, deferred }. The base placement opts land on the returned root; raw = true forces the raw explode into a fresh permanent hierarchy (no Asset component); idMap reuses saved child ids (stable ids across reload); diff reapplies sparse child overrides keyed by template original_id; sourceTag labels the provenance record (defaults to "bundle"); deferred=true switches the spawn pump from same-tick batch() to cross-frame queue() — required for thousand-entity bundles to avoid one huge frame stall; temporary=true spawns every child born temporary (the Asset component's re-created scaffolding — kept out of the saved scene so a reload doesn't respawn them alongside a re-instantiate). idMap / diff / deferred / temporary apply to the raw path only.
local root = bundleRef:instantiate()                          -- scene-clean instance (Asset component); root is an EntityRef
local root, map = bundleRef:instantiate(entity.spawn("mount"))     -- raw, ONTO an entity ref
local root, map = bundleRef:instantiate(nil, { raw = true })  -- raw permanent fresh hierarchy

modules/BundleAssetTypeRef/listChildren

listChildren(self, rootsOnly: boolean?): { string }

List the names of every entity captured in THIS bundle instance's entity_template. Pass rootsOnly = true to restrict to entities with no parent_id. Best-effort: empty list when the template can't parse.

Parameters

  • self any (optional)
  • rootsOnly boolean? (optional) — When true, only entities with no parent_id are returned.
for _, name in ipairs(bundleRef:listChildren()) do print(name) end

modules/BundleAssetTypeRef/listContents

listContents(self): { { name: string, isDirectory: boolean } }

List the entries directly under the bundle root folder (meshes, textures, materials, sub-bundles) for tooling (inspectors, dependency graphs).

Parameters

  • self any (optional)
for _, e in ipairs(bundleRef:listContents()) do print(e.name) end

modules/BundleAssetTypeRef/meshBindings

meshBindings(self): { any }

List each renderable node's mesh→material binding from THIS bundle's entity_template — the static data a spawned Model / SkinnedModel resolves — WITHOUT instantiating anything. Answers "which material does this mesh bind" as a direct read, with no spawn / settle / tree-walk / despawn cycle. Each entry is { node, nodeId, component, mesh, material }, where mesh and material are ref descriptors { guid, identity, name, path } (nil when the component omits that field). One entry per renderable component, in template order. Best-effort: empty list when the template can't parse.

Parameters

  • self any (optional)
for _, b in ipairs(bundleRef:meshBindings()) do print(b.node, b.material and b.material.name) end

modules/BundleAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .bundle/ instance: an entity_template change means the assembled model changed — regenerate the bundle's preview.png, its persisted visual description (syncs, publishes, feeds search and browser thumbnails), and record the component set + humanoid classification into .metadata for search facets. Originator-only; both artifacts sync to peers as ordinary content.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any } — { path, asset, type, kind, origin }.

modules/BundleAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Generic-creation hook for asset.create("bundle", name, opts). With opts.entity, composes that LIVE entity's hierarchy into the new bundle's entity_template in the same call — one step, capturing live component state (serialized component snapshots) + transforms of the root and every non-temporary descendant. With no opts, the bundle starts from the template skeleton's entity_template.

Parameters

  • name string
  • opts CreateOpts? (optional)
asset.create("bundle", "tree_prefab", { entity = rootRef })

modules/BundleAssetTypeRef/reindexSearchMeta

reindexSearchMeta(self): ()

Re-derive this bundle's search .metadata (component set + humanoid classification) from its current entity_template, without touching the preview. onChange calls it on every template edit; the search-metadata backfill calls it per bundle to populate assets minted before the deriver existed. Reads the template fresh — the parsed cache is only invalidated by setTemplate.

Parameters

  • self any (optional)
bundleRef:reindexSearchMeta()

modules/BundleAssetTypeRef/setTemplate

setTemplate(self, template: { any }): boolean

Overwrite the bundle's entity_template with a Lua table, JSON-encoded. The VFS write triggers the engine's asset hot-reload pipeline, so every component subscribed to this bundle's guid (via a declared asset field) reconciles automatically. Returns true on success.

Parameters

  • self any (optional)
  • template { any } — The full entity-template table (array of records) to write.
bundleRef:setTemplate(bundleRef:getTemplate())

modules/BundleAssetTypeRef/update

update(self, entityId: string): boolean

Re-compose THIS bundle from entityId's current hierarchy and write the new template back to the bundle's on-disk path. Walks the whole hierarchy under entityId (recursive), capturing live component state + transforms of every non-temporary descendant. The VFS write fires the engine's asset hot-reload pipeline, so every component subscribed to this bundle's guid reconciles automatically.

Parameters

  • self any (optional)
  • entityId string — The entity whose live hierarchy is captured into the bundle.
bundleRef:update(playerId)   -- "save" the live edits back into the bundle

modules/BundleUpdate/README

require("@builtin::modules.bundle_update") -- BundleUpdate (also available as global 'bundle')

Adds bundle.update to the existing bundle namespace. Pure Luau — thin ergonomic wrapper around bundle.compose. The prelude grafts this onto the engine's bundle table at boot.

bundle.update(entityId, bundleRef?) re-composes the bundle from the entity's current hierarchy state, writing the new template into the bundle's existing on-disk path. The reconcile is fully engine-driven: the VFS write fires the generic asset hot-reload pipeline, which dispatches onAssetReload(field) on every component instance whose declared asset field references the bundle's guid. Components decide what reload means for them (Asset.component → despawn + re-instantiate template; user components → whatever they implement in onAssetReload). With one arg, the bundle ref is inferred from entity(entityId).component.get("Asset").source. With two args the explicit ref wins. There is no opt-in subscription surface — declaring an asset field IS the subscription.

Usage: local BundleUpdate = require("@builtin::modules.bundle_update") Also available as global: bundle

modules/BundleUpdate/installInto

installInto(bundle: BundleNamespace)

Install update onto the supplied bundle-shaped namespace. The prelude calls this once at boot with the engine's bundle global; users reach the result as bundle.update.

Parameters

  • bundle BundleNamespace — The target namespace table. No-op when given a non-table value.
require("@builtin::modules.bundle_update").installInto(bundle)

modules/BundleUpdate/update

update(entityId: string, bundleRef: BundleRef?): any

Re-compose a bundle from an entity's current hierarchy and write it back to the bundle's on-disk path. The VFS write triggers the engine's generic asset hot-reload pipeline, which fires onAssetReload(field) on every component subscribed to this bundle's guid via a declared asset field — those components reconcile per their own policy.

Parameters

  • entityId string — The entity whose hierarchy is captured into the bundle.
  • bundleRef BundleRef? (optional) — Optional. When omitted, the ref is inferred from the entity's Asset.source field. When given, the explicit ref wins.
bundle.update(entityId)                     -- infer from Asset
bundle.update(entityId, { guid = "..." })   -- explicit ref

modules/CanvasAppLayout/README

require("@builtin::_templates.canvas_app.canvas_app_layout") -- CanvasAppLayout

Node-graph canvas built from the raw CSS-parity widget tree — absolutely positioned node cards over an SVG bezier connector; click a node to select it.

Usage: local CanvasAppLayout = require("@builtin::_templates.canvas_app.canvas_app_layout")

modules/CarriedReferences/README

require("@builtin::modules.api.engine.carried.references") -- CarriedReferences

Usage: local CarriedReferences = require("@builtin::modules.api.engine.carried.references")

modules/CarriedReferences/close

close(value: any, site: string, prefix: string): any

value (decoded component data) with every asset-reference envelope in it, at any depth, naming what the closure at site means by it: the guid the reference resolves to there (the session guid of the carried copy, or the engine library asset), or, while the mount answers nothing for it, the reference spelled in the mount (@carried.<key>::<reference>). A field holding that spelling waits on the mount and applies once the mount holds the asset. A component reference (componentType beside __ref) stays as written.

Parameters

  • value any (optional) — The data to read.
  • site string — The site M.site answers for the asset holding value.
  • prefix string — The prefix M.site answers for that asset.
local data = references.close(comp.data, site, prefix)

modules/CarriedReferences/records

records(template: { any }, path: string?, identity: string?): { any }

The records of an entity template as the asset holding them composes them. For an asset of a carried mount, a copy whose records' component data names every asset reference within the closure (close); for an asset of the open world, template itself.

Parameters

  • template { any } — The template's records ({ { components = { { type, data } } } }).
  • path string? (optional) — The VFS path of the asset holding the template.
  • identity string? (optional) — Optional identity of the same asset.
local records = references.records(bundleRef:getTemplate(), bundleRef.path, bundleRef.identity)

modules/CarriedReferences/site

site(path: string?, identity: string?): (string?, string?)

Where an asset of a carried mount stands: the site a reference it holds is read at, and the prefix its mount spells content with.

Parameters

  • path string? (optional) — The asset's VFS path.
  • identity string? (optional) — Optional identity of the same asset, read when path names no mount (@carried.<key>::<name>).
local site, prefix = references.site(ref.path, ref.identity)

modules/ComponentAssetTypeRef/README

require("@builtin::assetTypes.component.behavior") -- ComponentAssetTypeRef

Per-instance methods exposed on every AssetRef<component>. Loaded lazily by assetType.shared.ref.

Usage: local ComponentAssetTypeRef = require("@builtin::assetTypes.component.behavior")

modules/ComponentAssetTypeRef/attachTo

attachTo(self, entityId: string, fields: { [string]: any }?): any

Attach this component to an entity, equivalent to entity(entityId).component.add(compRef, fields). The ref names the component by its identity, so a component whose leaf name another package's component shares attaches as itself.

Parameters

  • self any (optional)
  • entityId string — Target entity ID.
  • fields { [string]: any }? (optional) — Optional public field overrides.
compRef:attachTo(playerId, { speed = 5 })

modules/ComponentAssetTypeRef/getAssetFields

getAssetFields(self): any

The component type's asset-field declarations — a map of field name to asset category (e.g. { material = "material" }), or nil when the component declares no Field.assetRef fields.

Parameters

  • self any (optional)
for field, cat in pairs(compRef:getAssetFields() or {}) do end

modules/ComponentAssetTypeRef/getInfo

getInfo(self): any

Metadata for the component type: { name, builtin, executionOrder, hooks }, where hooks is a { <hookName> = true } map scanned from the source.

Parameters

  • self any (optional)
local info = compRef:getInfo() print(info.executionOrder)

modules/ComponentAssetTypeRef/getInitScript

getInitScript(self): string?

Read the component's entry script (init.luau / init.lua) as raw text.

Parameters

  • self any (optional)
local src = compRef:getInitScript()

modules/ComponentAssetTypeRef/getReadme

getReadme(self): string?

Read the component's README.md body.

Parameters

  • self any (optional)
print(compRef:getReadme())

modules/ComponentAssetTypeRef/getSource

getSource(self): string?

The component type's registered source text — the live definition in the runtime registry. getInitScript reads the on-disk entry file; this reads what the engine actually registered (and resolves builtin components by identity).

Parameters

  • self any (optional)
local src = compRef:getSource()

modules/ComponentAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { fields, privateFields, methods, events, hooks, assetFields, executionOrder }, parsed from the component's own entry script (getInitScript) via luau_introspect. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A component with no readable entry script returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local events = asset.inspect(compRef).detail.events

modules/ComponentAssetTypeRef/isRegistered

isRegistered(self): boolean

Whether the component type is registered with the ECS, so that component.add(name) takes it. The source is stored the moment the asset is written; the type registers when that registration drains, on a later frame, and this reads the registration rather than the source.

Parameters

  • self any (optional)
while not compRef:isRegistered() do task.wait() end

modules/ComponentAssetTypeRef/listInstances

listInstances(self): { string }

List every entity in the live scene that currently has a component of this type. Useful for inspector tooling.

Parameters

  • self any (optional)
for _, id in ipairs(compRef:listInstances()) do print(id) end

modules/ComponentAssetTypeRef/listPublicFields

listPublicFields(self): { string }

Best-effort list of the component's public field names by parsing the entry script. Recognises public.<name> = ... assignments at module scope; doesn't expand metatable declarations. Comments and string literals are not read. Use as a discovery hint, not a strict schema.

Parameters

  • self any (optional)
for _, f in ipairs(compRef:listPublicFields()) do print(f) end

modules/ComponentAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)register + hot-reload this component type whenever its .component is seeded (a world seed) or its entry script (init.luau / init.lua) is edited. This is what registers and live-reloads USER components — library components register through the VFS write hook (author-immutable content does not dispatch onChange). Mirrors the .material / .shader assetTypes owning their own registration. Convergent

  • idempotent: registration never writes back into the asset folder, and the underlying registry upsert is a no-op when the source is unchanged.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ComponentAssetTypeRef/onDelete

onDelete(ref)

Asset-type delete callback: unregister this component type when its .component folder is removed. The teardown counterpart of onChange's registration — the type owns both halves of its runtime lifecycle, the way the .material / .shader types do. __components.unregister drops every type the folder registered, under each name it answers to (a nested component's full identity and its bare leaf), from the runtime registry, the FFI type-info / schema mirrors, and the /registered/components/ projection; instances already attached to live entities keep running (their closures are already bound).

Parameters

  • ref any (optional)

modules/ComponentAssetTypeRef/onRegister

onRegister(self)

Initial-registration callback: register this component's type from its entry script the moment the instance first registers. Fired by the world-ready onRegister sweep BEFORE the world entrypoint (and its scene load) runs, so component.add-by-name resolves for entities the same load materialized. Idempotent: the registry upsert is a no-op when the source is unchanged, and a later onChange re-registration converges on the same definition.

Parameters

  • self any (optional)

modules/ComponentAssetTypeRef/settingsSchemaProblem

settingsSchemaProblem(schema: { [string]: any }, ref: any): string?

The component type's verdict on a component's settings schema: a field named for one of the names the engine reserves on every component (__components.reservedFields) is refused, because the engine refuses to add a component whose public table declares one to an entity. Called by the settings module on every schema it reads for a component.

Parameters

  • schema { [string]: any } — The Field schema the component's public table declares.
  • ref any (optional) — The component's asset ref.

modules/ComputeShaderAssetTypeRef/README

require("@builtin::assetTypes.computeShader.behavior") -- ComputeShaderAssetTypeRef

Per-instance methods exposed on every AssetRef<computeShader>. Loaded lazily by assetType.shared.ref.

Usage: local ComputeShaderAssetTypeRef = require("@builtin::assetTypes.computeShader.behavior")

modules/ComputeShaderAssetTypeRef/buffer

buffer(self, name: string): any?

The buffer this shader last made under name. Two systems driving one shader over the same data reach for this rather than each creating their own; a system that wants its own calls createBuffer again.

Parameters

  • self any (optional)
  • name string — The name the buffer was created under.
local params = shaderRef:buffer("params") or shaderRef:createBuffer("params", { type = "vec4", len = 3 })

modules/ComputeShaderAssetTypeRef/compile

compile(self)

Compile this shader now, rather than on its first dispatch. Idempotent. NORMALLY UNNECESSARY — a dispatch compiles on first use. Reach for this only to avoid the one-frame first-dispatch warm-up in a latency-critical spot.

Parameters

  • self any (optional)
shaderRef:compile()

modules/ComputeShaderAssetTypeRef/compileByName

compileByName(ref: string): boolean

Compile a compute shader by reference from its .computeShader VFS source — the lazy compile-on-first-use entry. A system that must guarantee the shader is registered before its first dispatch calls this (via compute.compileByName) to bring it online through the same generic compileCompute path an edit runs. Resolution goes through the universal asset system — a reference that doesn't resolve is a bad reference in the content that owns it, not something to special-case here.

Parameters

  • ref string — A compute-shader asset reference (identity / guid) resolvable by asset.resolve.
require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("computeShader").compileByName("@builtin::shaders.compute_double")

modules/ComputeShaderAssetTypeRef/copyBufferToTexture

copyBufferToTexture(self, source: any, name: string, width: number, height: number, format: string?): TextureHandle

Copy one of this shader's buffers into a cached GPU texture, staying on the GPU — the path for an image a compute pass produced.

Parameters

  • self any (optional)
  • source any (optional) — The buffer holding tightly-packed rows in the format's texel layout.
  • name string — Texture name, unique within this shader.
  • width number — Texture width in texels.
  • height number — Texture height in texels.
  • format string? (optional) — Texel format: "rgba16f" (default), "rgba32f", "rgba8".
local tex = shaderRef:copyBufferToTexture(packed, "foam", 256, 256, "rgba16f")

modules/ComputeShaderAssetTypeRef/createBuffer

createBuffer(self, name: string, opts: { [string]: any }): any

Create a GPU buffer this compute shader owns. It is a substrate buffer — the same one every other part of the engine deals in — filed under this shader's guid, name, and a serial, so two callers asking this shader for a params each get their own. Pass the handle to any shader's dispatch, including another shader's, to bind it there. The shader remembers what it made: shaderRef:buffer(name) returns the last buffer created under that name, which is how two callers share one instead.

Parameters

  • self any (optional)
  • name string — What this buffer is for, e.g. "params" or "verts".
  • opts { [string]: any } — { type, len, usage? } — type is the element ("f32", "vec3", "vec4", "quat", "mat4"), len is how many of them, and usage adds what the buffer is used for beyond the storage it always has: "readback" to read it on the CPU, "vertex" to draw it as geometry, "index" to draw it as an index run, "indirect" for a draw to read its arguments out of. A buffer can carry several.

There is no integer element: a buffer is a block of 32-bit words, so a binding the shader declares as u32 or atomic<u32> in bindings.yaml is created as "f32" here and written with buf:writeU32. The element type sets the STRIDE; what the words mean is the shader's to say.

local verts = shaderRef:createBuffer("verts", { type = "vec3", len = 1024, usage = { "readback" } })
verts:write(packed)  -- an array of numbers, or a `buffer` already holding the words
local values = verts:read():result()
local counts = shaderRef:createBuffer("counts", { type = "f32", len = 64 })  -- bindings.yaml: element: u32
counts:writeU32({ 0, 0, 0, 0 })
local geo = shaderRef:createBuffer("geo", { type = "vec3", len = 4096, usage = { "vertex", "readback" } })
local args = shaderRef:createBuffer("args", { type = "f32", len = 5, usage = { "indirect" } })  -- DrawIndexedIndirectArgs

modules/ComputeShaderAssetTypeRef/createSampler

createSampler(self, name: string, opts: { [string]: any }?): TextureHandle

Create a sampler owned by this compute shader, for its texture bindings.

Parameters

  • self any (optional)
  • name string — Sampler name, unique within this shader.
  • opts { [string]: any }? (optional) — Sampler options — filtering and addressing.
local smp = shaderRef:createSampler("linear", { filter = true, clamp = true })

modules/ComputeShaderAssetTypeRef/createStorageTexture2D

createStorageTexture2D(self, name: string, opts: { [string]: any }): TextureHandle

Create a write-only 2D storage texture owned by this compute shader — the target a raymarch or image pass writes.

Parameters

  • self any (optional)
  • name string — Texture name, unique within this shader.
  • opts { [string]: any } — { width, height, format? }.
local out = shaderRef:createStorageTexture2D("out", { width = 1920, height = 1080, format = "rgba16f" })

modules/ComputeShaderAssetTypeRef/createTexture3D

createTexture3D(self, name: string, opts: { [string]: any }): TextureHandle

Create a 3D texture owned by this compute shader — a density volume, an occupancy grid, a signed-distance field. Keyed by the shader's guid plus name, so it cannot collide with another asset's.

Parameters

  • self any (optional)
  • name string — Texture name, unique within this shader.
  • opts { [string]: any } — { width, height, depth, format?, storage? }.
local density = shaderRef:createTexture3D("density", { width = 64, height = 64, depth = 64, format = "r16f", storage = true })

modules/ComputeShaderAssetTypeRef/createTextureHistory

createTextureHistory(self, name: string, opts: { [string]: any }): TextureHandle

Create a temporal history pair owned by this compute shader — the previous frame's result to read while writing this frame's.

Parameters

  • self any (optional)
  • name string — History name, unique within this shader.
  • opts { [string]: any } — { width, height, format? }.
local history = shaderRef:createTextureHistory("taa", { width = 1920, height = 1080, format = "rgba16f" })

modules/ComputeShaderAssetTypeRef/destroy

destroy(self): boolean

Destroy this texture or sampler and free its GPU memory.

Parameters

  • self any (optional)

modules/ComputeShaderAssetTypeRef/dispatch

dispatch(self, opts: { [string]: any }): boolean

Dispatch this compute shader with named buffers bound to its declared storage bindings, in order. Compiles on first use. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how much data there is passes through math.max(1, ...) first.

Parameters

  • self any (optional)
  • opts { [string]: any } — { buffers, workgroups } — buffers names one compute buffer per declared storage binding; workgroups is { x, y, z }, { x }, or x.
shaderRef:dispatch({ buffers = { "positions" }, workgroups = { 64 } })

modules/ComputeShaderAssetTypeRef/dispatchEx

dispatchEx(self, opts: { [string]: any }): boolean

Dispatch this compute shader with explicit texture / storage-texture / sampler resources, one per declared binding in order. A params: block's uniform is engine-owned and takes no entry here. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how much data there is passes through math.max(1, ...) first.

A resource's kind says what it is, in the words bindings.yaml uses: buffer (name is a buffer handle), texture3d / storage3d (a 3D texture handle), storage2d (a 2D storage texture handle), texture2d (a copyBufferToTexture handle or a texture asset's guid), sampler (a sampler handle), history_prev / history_curr (one side of a history pair) and scene_depth (the frame's scene depth, no name). A slot takes the kind it is declared with; a texture2d slot also reads a storage2d, a history side or scene_depth, and a storage2d slot also writes history_curr. The engine spellings texture_3d, texture_2d and storage_2d name the same resources. Any other kind is refused, naming the accepted ones.

Parameters

  • self any (optional)
  • opts { [string]: any } — { resources, workgroups }: each resource is { kind, name }.
shaderRef:dispatchEx({ resources = { { kind = "storage2d", name = target } }, workgroups = { 8, 8 } })
shaderRef:dispatchEx({ resources = { { kind = "storage3d", name = density }, { kind = "buffer", name = params } }, workgroups = { 8, 8, 8 } })

modules/ComputeShaderAssetTypeRef/dispatchOnVertices

dispatchOnVertices(self, opts: { [string]: any }): boolean

Dispatch this compute shader with a model's vertex buffer bound at the first storage binding, and opts.buffers filling the rest. Use to mutate vertex positions directly. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how many vertices there are passes through math.max(1, ...) first.

Parameters

  • self any (optional)
  • opts { [string]: any } — { model, buffers?, workgroups } — model is the mesh guid whose vertices the shader writes.
shaderRef:dispatchOnVertices({ model = meshHandle.guid, workgroups = { 64 } })

modules/ComputeShaderAssetTypeRef/getBindings

getBindings(self): { [string]: any }

List this compute shader's declared bindings + params (parsed from bindings.yaml). Returns { bindings = { {name, kind, access?, element?, format?}, ... }, params = { {name, type, default}, ... } }. This is the editor-discovery surface, and the SAME parse the compile uses. Raises, naming the file, when bindings.yaml is text the YAML decoder refuses (with the line) or bindings: / params: holds anything but a list of mappings (with the key).

Parameters

  • self any (optional)
for _, b in ipairs(computeRef:getBindings().bindings) do print(b.name, b.kind) end

modules/ComputeShaderAssetTypeRef/getSource

getSource(self): string?

Read the compute WGSL body (shader.wgsl) as raw text.

Parameters

  • self any (optional)
local src = computeRef:getSource()

modules/ComputeShaderAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)compile the compute shader whenever its WGSL body or bindings.yaml is written. This is the ONLY thing that compiles a .computeShader — so it fires on the initial create (the template write) AND on every later edit, with no world reload. Convergent: see compileCompute.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ComputeShaderAssetTypeRef/onDelete

onDelete(ref)

Asset-type delete callback: release the program a removed .computeShader folder registered. The teardown counterpart of onChange's compile, so the type owns both halves of its runtime lifecycle the way .material does: the program, the params uniform it carried and the dispatch history under its key go with the asset, and a shader authored afresh at the same path registers a new program rather than inheriting the one the removed asset left.

Parameters

  • ref any (optional)

modules/ComputeShaderAssetTypeRef/program

program(self): any

This shader's kernel ABI as a program: program:variant(spec) lowers and compiles one specialization, and variant:run(env) allocates the outputs bindings.yaml declares (count, capacity, append with a live counter), binds and dispatches. See modules.compute_abi.

Parameters

  • self any (optional)
local variant = shaderRef:program():variant({ specialize = { T = "vec3<f32>" } })
local run = variant:run({ n = 64, inputs = { src = srcBuffer } })

modules/ComputeShaderAssetTypeRef/read

read(self): Readback

Start a GPU→CPU read-back of this 3D texture's voxels. The read takes frames to arrive — ask the returned Readback whether it is :ready(), then drain it.

Parameters

  • self any (optional)
local pending = density:read()
if pending:ready() then local voxels = pending:result() end

modules/ComputeShaderAssetTypeRef/setParam

setParam(self, prop: string, value: number): boolean

Set one scalar parameter declared in this shader's bindings.yaml params: block. The next dispatch sees the new value; a value set before the shader's first compile is the value it starts with.

Parameters

  • self any (optional)
  • prop string — Parameter name as declared in bindings.yaml.
  • value number — New scalar value.
shaderRef:setParam("scale", 4.0)

modules/ComputeShaderAssetTypeRef/setSource

setSource(self, src: string): boolean

Overwrite the compute WGSL body on disk. Hot-reload recompiles the shader on the next frame. Returns true on success.

Parameters

  • self any (optional)
  • src string — New WGSL source (only @compute fn main + helpers).
computeRef:setSource(myWgsl)

modules/ComputeShaderAssetTypeRef/status

status(self): { { [string]: any } }

What the engine did with this shader's dispatches, one record per target. A dispatch is recorded into a command encoder frames after the call that asked for it returned, so this is where its outcome lands: ok is the most recent outcome, dispatches counts what reached the encoder, failures how many of those could not be recorded, and lastError says why the last failure failed (kept after a recovery). target is the mesh guid for a dispatchOnVertices, empty for a dispatch that writes only its bound buffers. This answers "is this pass running?" — an empty result means nothing has dispatched this shader.

Parameters

  • self any (optional)
for _, d in ipairs(shaderRef:status()) do print(d.target, d.ok, d.lastError) end

modules/ComputeShaderAssetTypeRef/write

write(self, data: buffer | string | { number }): boolean

Upload voxels into this 3D texture: a buffer or a binary string carrying the texture's byte layout verbatim, or one number per channel in the texture's format.

Parameters

  • self any (optional)
  • data buffer | string | { number } — Voxel bytes, or voxel values in texel order.

modules/ComputeShaderAssetTypeRef/writeFloats

writeFloats(self, floats: { number }, formatOrOpts: (string | { [string]: any })?): boolean

Upload float voxels into this 3D texture, converting to the texture's format.

Parameters

  • self any (optional)
  • floats { number } — Voxel values in texel order.
  • formatOrOpts (string | { [string]: any })? (optional) — Source format name, or an options table.

modules/ContentVersion/README

require("@builtin::modules.content_version") -- ContentVersion

Per-path content-version counters — a cheap, synchronous "has this file changed?" token that lets an assetType behavior memoize a parse in its ref's runtime and serve repeated reads as a pure table lookup instead of re-reading + re-parsing the file every call.

A source write bumps the written path's counter. A memoized reader keyed by the value get(path) returned when it parsed can then check, on every later call, whether the counter still matches — an integer compare, no vfs.read, no parse. It rebuilds only when the counter moved. Two write surfaces feed it, so the token reflects a change no matter where it came from:

  • vfs.write / vfs.move bump SYNCHRONOUSLY, in the same call, so a script that writes a file and reads it back in the SAME tick sees the new content immediately (the asset-change onChange dispatch fires a frame LATER, too late for a same-tick read).
  • the generic asset-change dispatcher bumps on every source write it routes, including peer-synced and engine-originated writes that never pass through the Luau vfs.* surface — with the engine's normalized path, which is the canonical form a reader keys on. A typed asset's FOLDER path carries a counter too: the dispatcher bumps it after it has routed a write inside the asset and the asset's type has reacted to it, so a reader of "anything in this asset" keys on one number. Counters are per PATH (not one global epoch): the per-frame dirty-entity writer churns scene-dirty paths every frame during play, and a global epoch would let that churn invalidate every unrelated cache. Per-path isolation means only a change to THE file a reader depends on rebuilds it. The counters are held once for the process (__contentVersion): a write made in a worker VM, the engine thread's dispatch of it, and a write made on the engine thread advance the same number, and every VM reads it. A counter lives as long as its path, or as long as a reader has read it. Removing a path (vfs.remove, a move off it, a folder removal reaching it, a peer's removal, the shell's rm) moves the counter of a path some reader has read, so that reader rebuilds: inside the removing call for vfs.remove and vfs.move, and on the frame the engine delivers the removal for a peer's or the shell's. The counter of a path no reader read goes with the path, and a write back starts it from 0. The store therefore holds one small integer per path that exists or that a reader read, which keeps it the size of the content rather than of every path a session ever wrote.

Usage: local ContentVersion = require("@builtin::modules.content_version")

modules/ContentVersion/bump

bump(path: string)

Bump path's version counter, invalidating every reader memoized against its previous value. Called by the vfs.* write surface and the asset-change dispatcher; content code rarely calls it directly.

Parameters

  • path string — VFS path whose content changed.
require("@builtin::modules.content_version").bump(p)

modules/ContentVersion/get

get(path: string): number

The current version counter for path (0 if no VM ever wrote it). A memoized reader stores the value it saw when it parsed, and treats a later call as a cache hit exactly while get(path) still returns it.

Parameters

  • path string — Normalized VFS path.
local v = require("@builtin::modules.content_version").get(p)

modules/DataAssetTypeRef/README

require("@builtin::assetTypes.data.behavior") -- DataAssetTypeRef

Per-instance methods on every AssetRef<data> — a configured value instance of a dataType contract. Instances carry values only; structure and behavior live on the contract.

Usage: local DataAssetTypeRef = require("@builtin::assetTypes.data.behavior")

modules/DataAssetTypeRef/contract

contract(self)

The dataType contract this instance is bound to.

Parameters

  • self any (optional)
local weaponType = smg:contract()

modules/DataAssetTypeRef/data

data(self, wanted: string?)

Resolve this instance into a read-only typed value object: this instance's values.yaml with the contract's schema defaults applied, ref fields materialized (assetRef -> AssetRef, dataRef -> nested value object), and the bound contract chain's behavior methods reachable on it — the bound contract's own methods win over an ancestor's when both define the same name, and any explicit instance value wins over both. Errors loudly when the instance violates its contract, when wanted is given and the bound contract's chain doesn't include it, and when dataRef fields form a circular chain.

Parameters

  • self any (optional)
  • wanted string? (optional) — Optional contract the caller requires — an assertion, not a filter: the returned object still carries the FULL bound schema and method chain.
local w = smg:data("tdWeapon"); print(w.damage, w:effectiveRating())

modules/DataAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { contract, valueKeys }, parsed from this instance's own values.yaml — the bound contract identity plus the sorted top-level keys of its values map. Never resolves the contract or materializes ref fields (that's :data()).

Parameters

  • self any (optional)
local valueKeys = asset.inspect(dataRef).detail.valueKeys

modules/DataAssetTypeRef/onCreate

onCreate(name: string, opts: { contract: string })

Create a data instance bound to a contract. Seeds values.yaml with the contract's declared defaults so a fresh instance is immediately valid wherever every required field has a default.

Parameters

  • name string
  • opts { contract: string }

modules/DataAssetTypeRef/satisfies

satisfies(self, wanted: string): (boolean, string?)

Whether this instance's contract chain includes wanted — true for the bound contract itself and for any contract it extends.

Parameters

  • self any (optional)
  • wanted string — Contract identity (bare leaf or full identity).
if smg:satisfies("weapon") then ... end

modules/DataAssetTypeRef/satisfiesDetail

satisfiesDetail(self, wanted: string): { ok: boolean, code: string, reason: string? }

Chain-membership check that also reports WHY, as a stable code a caller can branch on without matching reason text.

Parameters

  • self any (optional)
  • wanted string — Contract identity (bare leaf or full identity).
local d = smg:satisfiesDetail("weapon")
if not d.ok and d.code ~= "contract-mismatch" then warn(d.reason) end

modules/DataAssetTypeRef/setValues

setValues(self, patch: { [string]: any })

Change this instance's values: patch replaces the named top-level fields. The result is checked against the bound contract, and it is refused — one error naming each violation, nothing written — when it introduces a violation, or leaves one standing at or under a value the change set. A list item is known by what it holds rather than where it sits, so removing or reordering items sets none of the rest. A violation the file already held anywhere else stays listed without stopping the change, so an instance that does not yet satisfy its contract is filled in one field at a time and a value beside an undeclared key stays editable. A written result goes to values.yaml (re-encoded, so comments in the file are not kept), recorded into the open undo operation when there is one.

Parameters

  • self any (optional)
  • patch { [string]: any } — Top-level field -> new value, in the forms values.yaml holds.
ref:setValues({ clipmap = { baseTile = 100, levels = 5 } })

modules/DataAssetTypeRef/validateInstance

validateInstance(self): (boolean, { DS.Violation })

Validate this instance's values against its contract's merged schema (missing required fields, constraint breaches, unknown fields, ref-field existence + contract compatibility).

Parameters

  • self any (optional)
local ok, v = smg:validate()

modules/DataSchema/README

require("@builtin::modules.data_schema") -- DataSchema

Schema engine for the typed-data system. A contract's schema.yaml declares fields + constraints; this module parses the decoded declaration, merges extends chains, applies defaults, validates value tables, and maps a contract onto Field descriptors so a data instance's values edit through typed controls: values convert between the file's forms and the controls', and an edit lands on the stored values leaf by leaf. No VFS reads — callers decode the YAML themselves and inject asset/contract resolvers; converting a reference resolves it.

Usage: local DataSchema = require("@builtin::modules.data_schema")

modules/DataSchema/applyDefaults

applyDefaults(merged: { [string]: FieldSpec }, values: { [string]: any }): { [string]: any }

Produce a NEW value table with every schema default filled in where values has no explicit entry — recursively: struct values gain their subfield defaults and array elements gain their item defaults, at every depth. Neither input is mutated.

Parameters

  • merged { [string]: FieldSpec } — Merged field map from mergeChain.
  • values { [string]: any } — The instance's raw value table.
local filled = DataSchema.applyDefaults(merged, rawValues)

modules/DataSchema/editedValues

editedValues(merged: { [string]: FieldSpec }, stored: { [string]: any }, edited: { [string]: any }, base: string?): { [string]: any }

The top-level values a control edit writes: for each field edited names, the value stored holds with only the leaves the edit changed replaced, each in the form the file held it in. Every value the edit leaves alone keeps its stored value and form — a colour held as numbers (above 1 included), a map-form vector, and keys the contract does not declare.

Parameters

  • merged { [string]: FieldSpec } — The merged field map.
  • stored { [string]: any } — The values as the data file holds them.
  • edited { [string]: any } — Control-form values for the fields the edit names.
  • base string? (optional) — The data file's VFS path, which a ~ reference expands against.
local patch = DataSchema.editedValues(schema, stored, controlPatch, ref.path)

modules/DataSchema/fromFieldValues

fromFieldValues(merged: { [string]: FieldSpec }, values: { [string]: any }): { [string]: any }

Values from the Field controls in the forms the data file holds: the inverse of toFieldValues.

Parameters

  • merged { [string]: FieldSpec } — The merged field map.
  • values { [string]: any } — The control-form values (any subset of fields).
local stored = DataSchema.fromFieldValues(schema, patch)

modules/DataSchema/mergeChain

mergeChain(chain: { Schema? }): ({ [string]: FieldSpec }?, { string })

Merge an extends chain of parsed schemas into one field map. The chain is ordered ROOT PARENT FIRST, derived contract LAST. A child redeclaring a parent field is a problem — shared shape comes from the parent, per-child shape from new fields.

Parameters

  • chain { Schema? } — Array of Schema, root parent first. A nil hole (e.g. a failed parseSchema result passed straight in) is a problem entry.
local merged, problems = DataSchema.mergeChain({ itemSchema, weaponSchema })

modules/DataSchema/parseSchema

parseSchema(raw: any): (Schema?, { string })

Parse a decoded schema.yaml table into a Schema. Returns (schema, problems) — schema is nil when any problem was found. fields may be written as a map (name -> spec) or as a sequence of specs each carrying its own name:; both key the resulting fields by name.

Parameters

  • raw any (optional) — The decoded document ({ extends?, fields }).
local Yaml = require("@builtin::modules.yaml")
local schema, problems = DataSchema.parseSchema(Yaml.decode(bytes))

modules/DataSchema/toFieldValues

toFieldValues(merged: { [string]: FieldSpec }, values: { [string]: any }, base: string?): { [string]: any }

A data instance's values in the forms the Field controls read: colours as { r, g, b, a }, vectors as { x, y, z }, references as { __ref = guid }, number enum members as strings.

Parameters

  • merged { [string]: FieldSpec } — The merged field map.
  • values { [string]: any } — The values as the data file holds them.
  • base string? (optional) — The data file's VFS path, which a ~ reference expands against.
local shown = DataSchema.toFieldValues(schema, values, ref.path)

modules/DataSchema/toFields

toFields(merged: { [string]: FieldSpec }): { [string]: any }

One Field descriptor per contract field, so a data instance's values edit through the same typed controls as asset settings: a number with both bounds is a range, a string or number with enum an enum (a number enum's members shown as strings), and every other type its Field kind.

Parameters

  • merged { [string]: FieldSpec } — The merged field map (dataTypeRef:schema()).
local fields = DataSchema.toFields(contract:schema())

modules/DataSchema/validateValues

validateValues(

Validate a raw value table against a merged field map. Checks missing required fields (a field with a default is never missing), per-field constraints, unknown top-level fields, and ref fields via the injected resolvers.

local violations = DataSchema.validateValues(merged, rawValues, resolvers)

modules/DataTypeAssetTypeRef/README

require("@builtin::assetTypes.dataType.behavior") -- DataTypeAssetTypeRef

Per-instance methods exposed on every AssetRef<dataType> — the contract surface of the typed-data system: schema (extends chain merged), validation, defaults, chain navigation, instance listing.

Usage: local DataTypeAssetTypeRef = require("@builtin::assetTypes.dataType.behavior")

modules/DataTypeAssetTypeRef/chain

chain(self)

The full extends chain as refs, root parent first, this contract last.

Parameters

  • self any (optional)
for _, link in ipairs(t:chain()) do print(link.identity) end

modules/DataTypeAssetTypeRef/defaults

defaults(self)

The default value table the merged schema declares.

Parameters

  • self any (optional)
local d = weaponType:defaults()

modules/DataTypeAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { fields, extends }, the contract's OWN declared field schema (not the merged extends chain) — fields is { {name, type, required}, ... } sorted by name, parsed from this contract's own schema.yaml. extends is the parent contract identity, or nil for a root contract.

Parameters

  • self any (optional)
local fields = asset.inspect(dataTypeRef).detail.fields

modules/DataTypeAssetTypeRef/instances

instances(self)

Every data instance bound to this contract (or to a contract that extends it). Scans the whole data-asset registry — a discovery surface for authoring and tooling, not a per-frame call. A broken instance (unreadable values.yaml, dangling contract binding) warns and is skipped, so one bad instance never aborts enumeration of the rest.

Parameters

  • self any (optional)
for _, ref in ipairs(weaponType:instances()) do print(ref.identity) end

modules/DataTypeAssetTypeRef/parent

parent(self)

The parent contract ref (extends), or nil for a root contract.

Parameters

  • self any (optional)
local base = rangedType:parent()

modules/DataTypeAssetTypeRef/schema

schema(self)

The contract's merged field schema — its own fields plus every field inherited through the extends chain.

Parameters

  • self any (optional)
local schema = weaponType:schema()

modules/DataTypeAssetTypeRef/validateValues

validateValues(self, values: { [string]: any })

Validate a value table against this contract's merged schema.

Parameters

  • self any (optional)
  • values { [string]: any }
local ok, v = weaponType:validate({ damage = 12 })

modules/Debris/README

require("@builtin::modules.debris") -- Debris

Timed entity despawn — Roblox Debris:AddItem(instance, lifetime) analog. Wraps the internal __debris.* FFI with a typed Luau surface. Installed as the global debris via the prelude.

Collapses the common task.delay + entity.despawn pattern into one call. Use for transient entities — bullet tracers, hit FX, dropped pickups, tween-completion cleanups, ragdoll cleanup. Semantics (match Roblox):

  • debris.add(id, lifetime) queues a despawn after lifetime seconds. Default lifetime is 10 seconds.
  • Calling add twice on the same entity REPLACES the prior deadline.
  • If the entity is despawned through any other path the pending record is dropped silently — no error.
  • Pending entries survive world.save / world.load (saved lifetime is seconds-remaining, so timers resume from where they paused).

Usage: local Debris = require("@builtin::modules.debris")

modules/Debris/add

add(id: any, lifetime: number?): DebrisHandle

Schedule the entity for despawn after lifetime seconds (default 10). Calling again on the same entity replaces the prior deadline. Negative or zero lifetime despawns immediately. Returns a handle for cancel(), or 0 if the entity id couldn't be resolved.

Parameters

  • id any (optional) — Entity id, name, or proxy table.
  • lifetime number? (optional) — Seconds before despawn — defaults to 10 when nil.
local bullet = entity.spawn("Bullet"); debris.add(bullet.id, 2.0)
local h = debris.add(target.id, 5); debris.cancel(h)

modules/Debris/cancel

cancel(handleOrId: any): boolean

Cancel a pending despawn. Accepts either a handle from debris.add or an entity id / proxy. Returns true if a pending record was actually removed.

Parameters

  • handleOrId any (optional) — Cancel handle, or entity id / name / proxy.
debris.cancel(handle); debris.cancel(target.id)

modules/Debris/clear

clear(): boolean

Drop every pending entry. Used by the test suite to isolate cases — not part of the user-facing surface.

modules/Debris/count

count(): number

Number of currently pending debris entries — handy for diagnostics overlays.

print(debris.count(), "pending despawns")

modules/Debris/list

list(): { DebrisEntry }

Snapshot every pending entry as a flat array of {id, remainingSecs, handle} records. Order is not stable — don't rely on it.

for _, e in debris.list() do print(e.id, e.remainingSecs) end

modules/Debris/pending

pending(id: any): number?

Return the number of seconds remaining before the entity is despawned, or nil if it isn't scheduled.

Parameters

  • id any (optional) — Entity id, name, or proxy.
local s = debris.pending(bullet.id); if s then print("dies in", s) end

modules/DepthProbe/README

require("@builtin::tests.fixtures.depth_probe") -- DepthProbe

The reader half of the depth probe — the two depth grids depth_probe_feature.renderFeature writes each frame, one at afterDepthPrepass and one at afterOpaque. A suite requires this module and enables the feature; the feature records the targets it writes here, so both halves address the same targets.

Usage: local DepthProbe = require("@builtin::tests.fixtures.depth_probe")

modules/DepthProbe/read

read(): ({ number }?, { number }?)

Read both grids: the nearest depth of each tile at afterDepthPrepass and at afterOpaque, row by row.

local seam, final = probe.read()

modules/DisplayCurve/README

require("@builtin::tests.fixtures.display_curve") -- DisplayCurve

The chain's display transform in Luau, for the suites that predict what a light value arrives at the frame as: the Khronos PBR Neutral tone curve __display__ applies at its default (curve = 3), the sRGB encode over it, and the two composed. The port is of zero_display_neutral in post_process_renderer.rs — the same formula read on a light triple rather than on a fragment, since the curve mixes channels by their shared peak and a single channel cannot be converted on its own.

Usage: local DisplayCurve = require("@builtin::tests.fixtures.display_curve")

modules/DisplayCurve/neutralCurve

neutralCurve(rgb: { number }): { number }

The Khronos PBR Neutral tone curve on a light triple, as __display__ applies it at curve = 3.

Parameters

  • rgb { number } — Linear light, one entry per channel.
local curved = DisplayCurve.neutralCurve({ 1.0, 0.5, 0.25 })

modules/DisplayCurve/shownRGB

shownRGB(rgb: { number }): { number }

The code values the delivered frame shows for a light triple: the Neutral curve, then the sRGB encode, on each channel. The curve mixes channels by their shared peak, so one channel's code value depends on the other two — which is why this takes the whole triple. The engine applies the display exposure before the curve, so this answers for exposure = 1; a case running at another exposure scales the triple by it first.

Parameters

  • rgb { number } — Linear light, one entry per channel. A negative channel reads 0.
local shown = DisplayCurve.shownRGB({ 0.8, 0.8, 0.8 })

modules/DisplayCurve/srgbDecode

srgbDecode(code: number): number

The inverse of srgbEncode: a 0-255 code value back to the channel's share of peak light before the transfer function encoded it. Exact for a frame held at curve = 0, where __display__ only clamps to [0, 1] and encodes — a case reading such a frame at an exposure other than 1 divides that back out afterward to recover the light the scene carried.

Parameters

  • code number — The code value, on a 0-255 scale.
local peak = DisplayCurve.srgbDecode(128)

modules/DisplayCurve/srgbEncode

srgbEncode(v: number): number

One channel of a display's peak light as the 0-255 code value that shows it, IEC 61966-2-1.

Parameters

  • v number — The channel's share of peak light; clamped to [0, 1].
local code = DisplayCurve.srgbEncode(0.5)

modules/DockedAppLayout/README

require("@builtin::_templates.docked_app.docked_app_layout") -- DockedAppLayout

Docked-shell UI template — a top toolbar, a scrolling body, and a bottom status bar built as a raw CSS-parity widget tree. Clone-and-edit starting point for editor-style tool apps.

Usage: local DockedAppLayout = require("@builtin::_templates.docked_app.docked_app_layout")

modules/DotBot/README

require("@builtin::plugins.dotbot.surface") -- DotBot

DotBot's Luau surface: the agents a person has connected, the conversations they hold, and the turns they run.

Usage: local DotBot = require("@builtin::plugins.dotbot.surface")

modules/DotBotRun/README

require("@builtin::modules.api.editor.dotbotRun") -- DotBotRun

A DotBot run driven from outside the panel: a boot that names a prompt (--agent dotbot --agent-prompt <text>, or start_instance { prompt }) hands it to DotBot as the first message of a fresh conversation, or a boot that names --agent-resume sends its message into the conversation this world's run already holds. The run is recorded the way an agent CLI records its own: the conversation in this machine's agent-session store under (world, dock slot), and every prompt and turn event in its transcript (agentSessions.record), which agent_run_report reads.

Usage: local DotBotRun = require("@builtin::modules.api.editor.dotbotRun")

modules/DotBotRun/start

start(opts: { [string]: any }): { [string]: any }

Start a DotBot run over this engine's DotBot plugin, in the background: the prompt as the first message of a fresh conversation, or a resume message into the conversation this machine recorded for the world and slot. The outcome and every turn are recorded in the run's transcript and reported as the agent's state in the engine status. One run per engine; a second call starts nothing and answers whether the first was started.

Parameters

  • opts { [string]: any } — { agent, worldGuid, slot?, prompt?, resume?, model?, effort?, account?, seeded?, asPanel? }: agent is dotbot (DotBot on its Claude account) or dotbot-<provider>; account names the connected account of that provider the run is put on (its agent such as dotbot-claude~2, its account key, its panel name or its email; an account the host provides also by the host's id for it); seeded waits for the Claude accounts the host hands this engine to reach DotBot before the account is chosen; asPanel, in place of agent and account, runs on the agent a person's own message in the DotBot panel starts a conversation on.

modules/DotBotRun/startArmed

startArmed(): boolean

Start the DotBot run this engine's boot armed, from the boot's own settings (--agent, --agent-prompt or --agent-resume, --agent-model, --agent-effort, --agent-account, the bound world). Does nothing on a boot that armed no DotBot run, or once this engine has started one.

modules/DotBotRun/watchHanded

watchHanded(): boolean

Serve the prompts the page hosting this engine hands the editor's agent (origozero.ai's /create idea, posted as agent_prompt on the worker channel): each is sent as the first message of the world's DotBot conversation, once the DotBot plugin has loaded, and the page is told whether it was sent or why not. Started once per engine; a later call starts nothing.

modules/DotBotTranscript/README

require("@builtin::plugins.dotbot.transcript") -- DotBotTranscript

The DotBot panel's transcript: a conversation's rows as the widget tree one tab draws, rebuilt row by row. A row whose content has not changed since the last draw is the same subtree it was, its callbacks carried over, so a delta streaming into the last answer rebuilds that answer and nothing above it. The newest rows are drawn and the rest wait behind a "Show earlier messages" row, so what a redraw costs is set by how much is on screen rather than by how long the conversation is.

Usage: local DotBotTranscript = require("@builtin::plugins.dotbot.transcript")

modules/DotBotTranscript/folded

folded(key: string, runs: { any }, conversation: string?): boolean

Whether key's section is folded to its heading for runs, the agents of conversation: as the person set it for that conversation since its agents last started or all ended, else open while an agent works and folded once every agent has ended.

Parameters

  • key string
  • runs { any }
  • conversation string? (optional)

modules/DotBotTranscript/new

new(deps: { [string]: any })

Build a transcript.

Parameters

  • deps { [string]: any } — { source, icons, expanded, onToggle, pageSize? }: source reads a tool answer (traceOf, toolsRunIn); icons holds the down, right, alert and bot paths; expanded is the open/closed table, keyed by row key, that the panel keeps; onToggle() runs after a row is opened or closed; pageSize overrides M.PAGE.

modules/DotBotTranscript/newAgents

newAgents(deps: { [string]: any })

Build the agent cards one tab draws above its composer.

Parameters

  • deps { [string]: any } — The same { source, icons, expanded, onToggle } a transcript takes (icons.term marks a tool call, icons.stop the Stop button), plus onOpen(run, key) (a card's chevron or name) and onStop(run, key) (its Stop), each handed the run and the key of the section it was pressed in; pageSize overrides M.AGENT_PAGE.

modules/DotBotTranscript/render

render(tabId: string, messages: { any }, opts: { [string]: any }?): any

messages as the scroll area tabId draws.

Parameters

  • tabId string
  • messages { any }
  • opts { [string]: any }? (optional) — { conversation?, now? }: the conversation the rows belong to (a new one starts again at one page), and the clock a thinking row's seconds are read against (os.clock() when absent).

modules/DotBotTranscript/setFolded

setFolded(key: string, runs: { any }, conversation: string?, folded: boolean)

Fold (true) or open key's section for conversation's agents runs, as a press on its heading does: it holds until the conversation's agents next start or all end.

Parameters

  • key string
  • runs { any }
  • conversation string? (optional)
  • folded boolean

modules/DotBotTranscript/showEarlier

showEarlier(tabId: string)

Draw one page more of the earlier rows on tabId's next render.

Parameters

  • tabId string

modules/DotBotTranscript/stats

stats(tabId: string): any

What tabId's last render did: { rows, drawn, built, kept, hidden }. rows is every row the conversation has, drawn the rows on screen, built the rows whose nodes were made by that render, kept the rows that carried the nodes they already had, and hidden the rows behind "Show earlier messages".

Parameters

  • tabId string

modules/DotBotWorkflows/README

require("@builtin::plugins.dotbot.workflows") -- DotBotWorkflows

The DotBot panel's Workflows view: every workflow run in the engine as a card (its name, state, who started it, how long it has run, its phase and how many of its requests are answered), and one run opened: its phases with each request (its state, the agent that holds it and for how long, the claims that ran out, its error, and its answer once opened), its log, and Cancel while it runs. A card or a request row whose content has not changed since the last draw is the same subtree it was, its callbacks carried over, so a change to one request builds that row and nothing else.

Usage: local DotBotWorkflows = require("@builtin::plugins.dotbot.workflows")

modules/DotBotWorkflows/detail

detail(key: string, run: any, opts: { [string]: any }?): { any }

One run opened: its header (name, state, who started it, how long it has run, its progress, why it ended, and Cancel while it runs), each phase with its requests, and its log. opts.nowSec is the wall clock times are read against.

Parameters

  • key string
  • run any (optional)
  • opts { [string]: any }? (optional)

modules/DotBotWorkflows/list

list(key: string, opts: { [string]: any }?): any

Every workflow run in the engine, newest first, one card each; a press on a card opens the run. opts.nowSec is the wall clock times are read against.

Parameters

  • key string
  • opts { [string]: any }? (optional)

modules/DotBotWorkflows/new

new(deps: { [string]: any })

Build a Workflows view.

Parameters

  • deps { [string]: any } — { source, icons, onToggle?, onOpenAgent?, onConversation? }: source is the panel's source (workflows, workflow, workflowPhases, workflowRevision, workflowAgent, workflowConversation, cancelWorkflow, conversations, subagents, splitAddress); icons holds the right, down, stop and term paths; onToggle() runs after something in the view opened, closed or was asked of a run; onOpenAgent(agentRun, key) opens an agent a turn started; and onConversation(address, key) opens a conversation, each from view key.

modules/DotBotWorkflows/open

open(key: string): string?

The run opened in view key, by id, or nil while the list shows.

Parameters

  • key string

modules/DotBotWorkflows/setOpen

setOpen(key: string, runId: string?)

Open run runId in view key, or go back to the list with nil.

Parameters

  • key string
  • runId string? (optional)

modules/DotBotWorkflows/stats

stats(key: string): any

What the last draw of key did: { runs | requests, built, kept }.

Parameters

  • key string

modules/DynamicAssetTypeRef/README

require("@builtin::assetTypes.dynamicAsset.behavior") -- DynamicAssetTypeRef

Behaviour for the dynamicAsset asset type — a prompt-driven, self-regenerating 3D asset. Reference example for the asset-type onChange change-callback. Loaded lazily by @builtin::assetTypes.assetType.shared.ref.

Usage: local DynamicAssetTypeRef = require("@builtin::assetTypes.dynamicAsset.behavior")

modules/DynamicAssetTypeRef/onChange

onChange(ref, change)

Regenerate this dynamic asset when its own prompt.json is written with a prompt other than the one already generated. A write anywhere else in the instance is ignored, and a prompt arriving while a generation is in flight is held for the poll loop to pick up when that one settles.

Parameters

  • ref any (optional) — The changed .dynamicAsset's reference.
  • change any (optional) — The change record the asset dispatcher raised for the write.

modules/EcsAudioListenerSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsAudioListener") -- EcsAudioListenerSpec

Usage: local EcsAudioListenerSpec = require("@builtin::modules.api.engine.ecs.components.EcsAudioListener")

modules/EcsAudioSourceSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsAudioSource") -- EcsAudioSourceSpec

Usage: local EcsAudioSourceSpec = require("@builtin::modules.api.engine.ecs.components.EcsAudioSource")

modules/EcsCameraSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsCamera") -- EcsCameraSpec

Usage: local EcsCameraSpec = require("@builtin::modules.api.engine.ecs.components.EcsCamera")

modules/EcsColliderSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsCollider") -- EcsColliderSpec

Usage: local EcsColliderSpec = require("@builtin::modules.api.engine.ecs.components.EcsCollider")

modules/EcsCollisionGroupsSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsCollisionGroups") -- EcsCollisionGroupsSpec

Usage: local EcsCollisionGroupsSpec = require("@builtin::modules.api.engine.ecs.components.EcsCollisionGroups")

modules/EcsComponentSpecs/README

require("@builtin::modules.api.engine.ecs.components") -- EcsComponentSpecs

Usage: local EcsComponentSpecs = require("@builtin::modules.api.engine.ecs.components")

modules/EcsFluidForcesSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsFluidForces") -- EcsFluidForcesSpec

Usage: local EcsFluidForcesSpec = require("@builtin::modules.api.engine.ecs.components.EcsFluidForces")

modules/EcsForceSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsForce") -- EcsForceSpec

Usage: local EcsForceSpec = require("@builtin::modules.api.engine.ecs.components.EcsForce")

modules/EcsGravityFieldSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsGravityField") -- EcsGravityFieldSpec

Usage: local EcsGravityFieldSpec = require("@builtin::modules.api.engine.ecs.components.EcsGravityField")

modules/EcsHandle/README

require("@builtin::modules.api.engine.ecs.handle") -- EcsHandle

Usage: local EcsHandle = require("@builtin::modules.api.engine.ecs.handle")

modules/EcsHandles/README

require("@builtin::modules.api.engine.ecs.handles") -- EcsHandles

Usage: local EcsHandles = require("@builtin::modules.api.engine.ecs.handles")

modules/EcsHandles/handleFields

handleFields(typeName: string): { { rust: string, category: string } }?

The fields of a native component that hold a GPU handle.

Parameters

  • typeName string — The component's public name under ecs.
local fields = handles.handleFields("Mesh")

modules/EcsHandles/insertAll

insertAll(specs: { { id: string, type: string, data: any } }, site: string?): { { type: string, ids: { string }, message: string } }

Insert saved native components, one insertMany per distinct entry.

Parameters

  • specs { { id: string, type: string, data: any } } — A list of { id, type, data }: the entity, the public component name and the saved row. Entities stating the same type and data share one insert.
  • site string? (optional) — Optional path or identity of the asset the rows were read from. A site inside a carried mount resolves each guid within that mount's closure.
local failed = handles.insertAll({ { id = id, type = "Mesh", data = { mesh = guid } } })

modules/EcsHandles/value

value(entry: any, site: string?): any

The component value a saved ecs entry states: its handle fields resolved from asset guids to the handles the component holds.

Parameters

  • entry any (optional) — { type, data }, type the public name under ecs and data the saved row.
  • site string? (optional) — Optional path or identity of the asset the entry was read from. A site inside a carried mount resolves each guid within that mount's closure (asset.tryResolve(guid, category, site)).
ecs.insert(id, handles.value({ type = "Mesh", data = { mesh = guid } }))

modules/EcsLiftSurfaceSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsLiftSurface") -- EcsLiftSurfaceSpec

Usage: local EcsLiftSurfaceSpec = require("@builtin::modules.api.engine.ecs.components.EcsLiftSurface")

modules/EcsLightSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsLight") -- EcsLightSpec

Usage: local EcsLightSpec = require("@builtin::modules.api.engine.ecs.components.EcsLight")

modules/EcsMaterialSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsMaterial") -- EcsMaterialSpec

Usage: local EcsMaterialSpec = require("@builtin::modules.api.engine.ecs.components.EcsMaterial")

modules/EcsMeshSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsMesh") -- EcsMeshSpec

Usage: local EcsMeshSpec = require("@builtin::modules.api.engine.ecs.components.EcsMesh")

modules/EcsMorphWeightsSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsMorphWeights") -- EcsMorphWeightsSpec

Usage: local EcsMorphWeightsSpec = require("@builtin::modules.api.engine.ecs.components.EcsMorphWeights")

modules/EcsPhysicsJointSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsPhysicsJoint") -- EcsPhysicsJointSpec

Usage: local EcsPhysicsJointSpec = require("@builtin::modules.api.engine.ecs.components.EcsPhysicsJoint")

modules/EcsPhysicsSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsPhysics") -- EcsPhysicsSpec

Usage: local EcsPhysicsSpec = require("@builtin::modules.api.engine.ecs.components.EcsPhysics")

modules/EcsPlan/README

require("@builtin::modules.api.engine.ecs.plan") -- EcsPlan

Usage: local EcsPlan = require("@builtin::modules.api.engine.ecs.plan")

modules/EcsPlaneHoldSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsPlaneHold") -- EcsPlaneHoldSpec

Usage: local EcsPlaneHoldSpec = require("@builtin::modules.api.engine.ecs.components.EcsPlaneHold")

modules/EcsPlayerOwnedSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsPlayerOwned") -- EcsPlayerOwnedSpec

Usage: local EcsPlayerOwnedSpec = require("@builtin::modules.api.engine.ecs.components.EcsPlayerOwned")

modules/EcsRetargetProfileSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsRetargetProfile") -- EcsRetargetProfileSpec

Usage: local EcsRetargetProfileSpec = require("@builtin::modules.api.engine.ecs.components.EcsRetargetProfile")

modules/EcsShaderDataSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsShaderData") -- EcsShaderDataSpec

Usage: local EcsShaderDataSpec = require("@builtin::modules.api.engine.ecs.components.EcsShaderData")

modules/EcsShaftNetworkSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsShaftNetwork") -- EcsShaftNetworkSpec

Usage: local EcsShaftNetworkSpec = require("@builtin::modules.api.engine.ecs.components.EcsShaftNetwork")

modules/EcsSkeletonSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsSkeleton") -- EcsSkeletonSpec

Usage: local EcsSkeletonSpec = require("@builtin::modules.api.engine.ecs.components.EcsSkeleton")

modules/EcsSkySpec/README

require("@builtin::modules.api.engine.ecs.components.EcsSky") -- EcsSkySpec

Usage: local EcsSkySpec = require("@builtin::modules.api.engine.ecs.components.EcsSky")

modules/EcsSoftBodySpec/README

require("@builtin::modules.api.engine.ecs.components.EcsSoftBody") -- EcsSoftBodySpec

Usage: local EcsSoftBodySpec = require("@builtin::modules.api.engine.ecs.components.EcsSoftBody")

modules/EcsTessellationSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsTessellation") -- EcsTessellationSpec

Usage: local EcsTessellationSpec = require("@builtin::modules.api.engine.ecs.components.EcsTessellation")

modules/EcsTransformConstraintsSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsTransformConstraints") -- EcsTransformConstraintsSpec

Usage: local EcsTransformConstraintsSpec = require("@builtin::modules.api.engine.ecs.components.EcsTransformConstraints")

modules/EcsTransformSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsTransform") -- EcsTransformSpec

Usage: local EcsTransformSpec = require("@builtin::modules.api.engine.ecs.components.EcsTransform")

modules/EcsVisibilityRangeSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsVisibilityRange") -- EcsVisibilityRangeSpec

Usage: local EcsVisibilityRangeSpec = require("@builtin::modules.api.engine.ecs.components.EcsVisibilityRange")

modules/EcsWatch/README

require("@builtin::modules.api.engine.ecs.watch") -- EcsWatch

Usage: local EcsWatch = require("@builtin::modules.api.engine.ecs.watch")

modules/EcsWatch/entities

entities(ids: { string }, ...: any): Watch

A watch over the named entities, whatever they hold, and over the entities holding any of the given component terms. changed() also reports a named entity that moved.

Parameters

  • ids { string } — The ids of the entities watched for movement.
  • args any (optional)

modules/EcsWatch/new

new(...: any): Watch

A watch over the entities holding any of the given component terms.

Parameters

  • args any (optional)

modules/EcsWheelColliderSpec/README

require("@builtin::modules.api.engine.ecs.components.EcsWheelCollider") -- EcsWheelColliderSpec

Usage: local EcsWheelColliderSpec = require("@builtin::modules.api.engine.ecs.components.EcsWheelCollider")

modules/EditorLayoutAssetTypeRef/README

require("@builtin::editor.edui.editorLayout.behavior") -- EditorLayoutAssetTypeRef

Per-instance methods + the onRegister / onChange lifecycle hooks for every AssetRef<editorLayout>. Loaded lazily by asset_ref.module.

Usage: local EditorLayoutAssetTypeRef = require("@builtin::editor.edui.editorLayout.behavior")

modules/EditorLayoutAssetTypeRef/loadRecordMethod

loadRecordMethod(self): any?

Read this instance's layout record: its name, title, description, slot, mode, panels with regions, fractions, origin, and the captured arrangement when it holds one.

Parameters

  • self any (optional)
local rec = layoutRef:loadRecord()

modules/EditorPanelAssetTypeRef/README

require("@builtin::editor.edui.editorPanel.behavior") -- EditorPanelAssetTypeRef

Per-instance methods + the onRegister lifecycle hooks for every AssetRef<editorPanel>. Loaded lazily by asset_ref.module.

Usage: local EditorPanelAssetTypeRef = require("@builtin::editor.edui.editorPanel.behavior")

modules/EditorPanelAssetTypeRef/getInitScript

getInitScript(self): string?

Read the instance's init.luau source as raw text (inspectors).

Parameters

  • self any (optional)

modules/EditorPanelAssetTypeRef/loadSpecMethod

loadSpecMethod(self): any

Read this instance's panel spec (the table its init.luau returns).

Parameters

  • self any (optional)
local spec = panelRef:loadSpec()

modules/EditorPanelAssetTypeRef/onChange

onChange(self, change)

Parameters

  • self any (optional)
  • change any (optional)

modules/EditorPanelAssetTypeRef/onDelete

onDelete(self)

Parameters

  • self any (optional)

modules/EditorPanelAssetTypeRef/onRegister

onRegister(self)

Parameters

  • self any (optional)

modules/EditorPanelAssetTypeRef/panelIds

panelIds(self): { string }

The panel ids this file registers, one per spec it returns, in order: a spec's own id, else the instance name, suffixed by position for the later specs of a multi-tab file. Answered from the instance's last registration, which every write to it repeats, so reading it runs nothing; an instance never registered (a runtime profile) loads its spec to answer. Raises the load error when init.luau does not return a spec.

Parameters

  • self any (optional)
for _, id in ipairs(panelRef:panelIds()) do print(id) end

modules/EffectAssetTypeBehavior/README

require("@builtin::systems.effects.effect.behavior") -- EffectAssetTypeBehavior

Per-instance methods and lifecycle hooks of the effect asset type. An effect's effect.yaml names what it plays: each entry under plays is one graph or a stack of graphs played together, called by its name. play plays an entry where the caller asks, with its inputs, and answers a handle that drives it; entries lists what can be played and the inputs each takes.

Usage: local EffectAssetTypeBehavior = require("@builtin::systems.effects.effect.behavior")

modules/EffectAssetTypeBehavior/entries

entries(self): (any, string?)

What this effect plays: each entry's name, the graphs it plays together and the inputs it takes, and the entry played when none is named.

Parameters

  • self any (optional)
for name, e in effectRef:entries().entries do print(name, #e.graphs) end

modules/EffectAssetTypeBehavior/entry

entry(self, name: string?, cooperative: boolean?): (any, string?)

One of this effect's entries: the graphs it plays together, each with the inputs it declares.

Parameters

  • self any (optional)
  • name string? (optional) — The entry's name, as effect.yaml lists it under plays; nil for the default.
  • cooperative boolean? (optional) — True from a task: describing the effect hands the frame back between its graphs.
local burst = effectRef:entry("burst")

modules/EffectAssetTypeBehavior/instantiate

instantiate(self, target: any?, opts: { [string]: any }?): (any, { [string]: string })

Place this effect in the scene: an entity with an Effect component that plays one of its entries there, the way a drop into the viewport or an Asset field places it.

Parameters

  • self any (optional)
  • target any? (optional) — Optional owning entity ref; the effect's entity spawns under it.
  • opts { [string]: any }? (optional) — The base placement opts (position, rotation, scale, name, temporary), plus entry (the entry it plays; the effect's default when omitted) and inputs (values for the entry's inputs by name).
local root = effectRef:instantiate(nil, { position = { 0, 1, 0 }, entry = "flare" })

modules/EffectAssetTypeBehavior/isDescribed

isDescribed(self): boolean

Whether the effect's entries are described already, so that entry answers without reading its graphs.

Parameters

  • self any (optional)
if effectRef:isDescribed() then bind() end

modules/EffectAssetTypeBehavior/newGraph

newGraph(self, name: string, opts: { part: boolean? }?): any

Make a new graph inside this effect: under graphs/ for one it plays, under parts/ for one its graphs use as a node.

Parameters

  • self any (optional)
  • name string — The graph's name.
  • opts { part: boolean? }? (optional) — { part? }: true makes it under parts/.
local core = beacons:newGraph("flareCore")
local crack = beacons:newGraph("crack", { part = true })

modules/EffectAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Scaffold a new effect playing one entry: effect.yaml with that entry as its default, the graph it plays under graphs/ by the same name, and a README to describe each. The entry is opts.entry, or the effect's own name when none is given.

Parameters

  • name string — The effect's name.
  • opts CreateOpts? (optional) — { entry? }: the name of the entry it starts with and of its graph.
asset.create("effect", "beacons", { entry = "flare" })

modules/EffectAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback: describe the effect in a task of its own, a slice of each frame at a time, so the Effect that plays it finds its entries described when it binds them.

Parameters

  • self any (optional)

modules/EffectAssetTypeBehavior/play

play(self, entryName: string?, opts: PlayOpts?): any

Play one of this effect's entries. Every graph the entry holds plays together from the same moment, each given the inputs it declares, under the entity root names, so the effect moves with it (a sword, a torch, a character's hand); with no root it stands in the world. A finished play is kept and played again by the next call for the same entry.

Parameters

  • self any (optional)
  • entryName string? (optional)
  • opts PlayOpts? (optional) — { root?, offset?, rotation?, inputs?, ending?, speed?, keep? }. root: the entity it plays on (proxy or id). offset: where it stands, from root or, with no root, in the world. rotation: its rotation there (pitch/yaw/roll degrees, or a quaternion). inputs: values for the entry's inputs by name; each goes to every graph of the entry that declares it. ending: "graph" (what the timeline declares), "loop" or "hold". speed: 1 its own rate. keep: stay once finished instead of going.
local beacons = asset.resolve("beacons", "effect")
local shown = beacons:play("flare", { root = mast, offset = { 0, 0.9, 0 }, inputs = { size = 0.4 } })
shown:onFinished(function() print("done") end)

modules/EffectAssetTypeBehavior/playing:playback

playing:playback(): any

Where the play stands and what it has cost: the Effect's playback(), its combined cost and one entry per graph. Once the play has finished and gone back to the kept plays, the answer is what it stood at when it went.

modules/EffectAssetTypeBehavior/playing:set

playing:set(input: string, value: any)

Set one of the entry's inputs while it plays, on every graph that declares it.

Parameters

  • input string
  • value any (optional)

modules/EffectAssetTypeBehavior/preview

preview(self, opts: { [string]: any }?): { [string]: any }

Render this effect to a still: its default entry played on the preview stage (effects.stage) from its first frame, over the dark backdrop and framed on the effect's body, and the picture taken at the effect's still moment: the latest of the moments its graphs name with g:still (halfway through its range, or two seconds into a timeline with no end, for a graph that names none). A timeline with an end is moved to that moment; one with no end is played to it.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — { size? = { width, height }, angle? = { yaw, pitch }, backdrop?, fit? }, as preview.fromEntities takes them.
local shot = effectRef:preview({ size = { width = 256, height = 256 } })

modules/EffectAssetTypeBehavior/removeEntry

removeEntry(self, name: string)

Remove an entry and the comment lines directly above it. The default moves to the first entry left; the last entry cannot be removed.

Parameters

  • self any (optional)
  • name string — The entry's name.
beacons:removeEntry("flare")

modules/EffectAssetTypeBehavior/renameEntry

renameEntry(self, old: string, new: string)

Rename an entry; the default follows it.

Parameters

  • self any (optional)
  • old string — The entry's name now.
  • new string — Its new name: letters, digits and _, starting with a letter.
beacons:renameEntry("glow", "flare")

modules/EffectAssetTypeBehavior/setDefault

setDefault(self, name: string)

Make an entry the one played when none is named.

Parameters

  • self any (optional)
  • name string — The entry's name.
beacons:setDefault("flare")

modules/EffectAssetTypeBehavior/setEntry

setEntry(self, name: string, graphs: { any })

Set what one entry plays: replaces the entry's graphs, or adds the entry after the others when the effect has none by that name. Other lines of effect.yaml, comments included, are kept.

Parameters

  • self any (optional)
  • name string — The entry's name: letters, digits and _, starting with a letter.
  • graphs { any } — The graphs it plays together: procGraph refs, or names as effect.yaml states them (~.graphs.flash).
beacons:setEntry("flare", { asset.resolve("~.graphs.flareCore", "procGraph"), "~.graphs.flareTrail" })

modules/EffectAssetTypeBehavior/validate

validate(ref): { { code: string, message: string, severity: string? } }

The effect's own rules: effect.yaml plays graphs that resolve, its default is one of its entries, every graph under graphs/ is played by some entry, and each folder holds only the assets it is for.

Parameters

  • ref any (optional)

modules/EffectAssetTypeBehavior/warm

warm(self, entryName: string?, count: number?, inputs: { [string]: any }?)

Keep plays of this effect's entries ready, built ahead of time, so a play takes one and shows its first frame at once. Plays are made one after another on the frames after the call, and one counts as ready once every graph of it is planned, cooked and has its first frame presented (entries() reports ready). A play made for inputs is ready for a play given the same inputs; one given others cooks for them as it starts. A finished play goes back on its first frame, prepared again for the inputs it last played with, while its entry has fewer than this count. An entry nothing sized keeps the effect's pool setting ready, made the first time that entry plays; call this at a level's load, or before a burst, with the number of plays of an entry that run at once.

Parameters

  • self any (optional)
  • entryName string? (optional)
  • count number? (optional) — How many to keep ready; nil keeps the effect's pool setting. 0 keeps none.
  • inputs { [string]: any }? (optional) — The values of the entry's inputs the plays are made for, as play takes them; nil makes them with the defaults.
beacons:warm()
beacons:warm("flare", 6)
beacons:warm("flare", 2, { size = 0.4 })

modules/EffectShared/README

require("@builtin::systems.effects.effect.shared") -- EffectShared

What the effect type and its Effect component share: which of an entry's graphs takes each input, and the plays the type handed out and keeps to play again. An entry is what effectRef:entry(name) answers.

Usage: local EffectShared = require("@builtin::systems.effects.effect.shared")

modules/EffectShared/add

add(path: string, entry: string, e: any, stillPreparing: boolean?)

Put a play made ahead of time among the kept plays of its entry: it waits disabled and out of the scene's hierarchy. A play still preparing counts as ready once prepared is called for it.

Parameters

  • path string — The effect's VFS path.
  • entry string — The entry's name.
  • e any (optional) — The play's entity, carrying a bound Effect component.
  • stillPreparing boolean? (optional) — Whether its graphs are still doing the work its first play needs.

modules/EffectShared/current

current(record: any): boolean

Whether record is still the play its entity is playing.

Parameters

  • record any (optional)

modules/EffectShared/defaultSize

defaultSize(path: string): number

The pool size every entry of the effect at path keeps until warm() sizes it.

Parameters

  • path string

modules/EffectShared/dropKept

dropKept(path: string)

Let go of every kept play of the effect at path: what it plays changed.

Parameters

  • path string — The effect's VFS path.

modules/EffectShared/finished

finished(entityId: string)

Called by an Effect component when every graph of its entry has finished. A play the effect type handed out tells its listeners, and goes back to the kept plays unless it stays.

Parameters

  • entityId string — The Effect component's entity.

modules/EffectShared/handOut

handOut(e: any, path: string, entry: string, stays: boolean): any

Hand a play out: who to tell when it finishes, and whether it stays where it is once finished instead of going back to the kept plays.

Parameters

  • e any (optional) — The play's entity.
  • path string — The effect's VFS path.
  • entry string — The entry's name.
  • stays boolean — Whether the play stays once finished.

modules/EffectShared/inputs

inputs(entry: any): { [string]: any }

The inputs an entry takes: every input any of its graphs declares.

Parameters

  • entry any (optional) — An entry, as effectRef:entry(name) answers it.

modules/EffectShared/isPreparing

isPreparing(entityId: string): boolean

Whether the kept play entityId is still doing the work its first play needs.

Parameters

  • entityId string — The play's entity.

modules/EffectShared/keep

keep(record: any)

Take a handed-out play back: pause it, take it out of the scene and keep it ready for the next play of its entry, or let it go when the entry already has as many plays, ready and playing, as its pool size.

Parameters

  • record any (optional) — The play's record.

modules/EffectShared/onKept

onKept(fn: (any) -> ())

Name what is done with each play that goes back among the kept plays: it counts as preparing until prepared is called for it.

Parameters

  • fn (any) -> () — Called with the play's entity.

modules/EffectShared/prepared

prepared(entityId: string)

A kept play's graphs are done with the work its first play needs.

Parameters

  • entityId string — The play's entity.

modules/EffectShared/ready

ready(path: string, entry: string): number

How many kept plays of entry are ready now: built, and their graphs done with the work their first play needs.

Parameters

  • path string
  • entry string

modules/EffectShared/route

route(entry: any, inputs: { [string]: any }?): ({ { [string]: any } }?, string?)

Each graph's share of inputs: the values of the inputs it declares.

Parameters

  • entry any (optional) — An entry, as effectRef:entry(name) answers it.
  • inputs { [string]: any }? (optional) — Values by input name.

modules/EffectShared/setDefaultSize

setDefaultSize(path: string, count: number)

Set the pool size every entry of the effect at path keeps until warm() sizes it.

Parameters

  • path string
  • count number

modules/EffectShared/setSize

setSize(path: string, entry: string, count: number)

Set how many plays of entry of the effect at path are kept ready; ready plays past the count are let go.

Parameters

  • path string — The effect's VFS path.
  • entry string — The entry's name.
  • count number — Whole number >= 0.

modules/EffectShared/size

size(path: string, entry: string): number

How many plays of entry are kept ready.

Parameters

  • path string
  • entry string

modules/EffectShared/sortedKeys

sortedKeys(t: { [string]: any }): { string }

The keys of t, sorted.

Parameters

  • t { [string]: any }

modules/EffectShared/stopped

stopped(entityId: string): boolean

Called by an Effect component told to stop. A play the effect type handed out goes back to the kept plays.

Parameters

  • entityId string — The Effect component's entity.

modules/EffectShared/take

take(path: string, entry: string): any?

A kept play of entry of the effect at path, or nil when none is kept.

Parameters

  • path string — The effect's VFS path.
  • entry string — The entry's name.

modules/EffectShared/total

total(path: string, entry: string): number

How many plays of entry there are: kept ones, ready or preparing, and ones playing now. The pool size counts both, so plays are built while fewer exist and a finished one always goes back while the pool is not over its size.

Parameters

  • path string
  • entry string

modules/Engine/README

require("@builtin::modules.api.engine.engine") -- Engine (also available as global 'engine')

Process-level engine state surface. Read-only boot profile and read-write engine mode. Both exposed as computed properties via metatable __index / __newindex — never call them as methods. Usage: if engine.profile == "editor" then ... end engine.mode = "play" engine.setMode("play", { strict = false }) Implemented as a thin Luau wrapper over internal FFI: engine.profile → __engineSettings.get("profile") engine.mode → __mode.get() / __mode.set(value) engine.setMode → __mode.set(value, strict) engine.paused → __pause.get() / __pause.set(value) engine.timeScale → __timescale.get() / __timescale.set(value) engine.timeScaleOwner → who the rate answers to (hold, or last writer) engine.gameplayReady → __gameplay.ready() engine.mode = X is load-bearing — the setter owns the mode-flip side effects so any path that changes mode behaves identically: edit → play: drain pending edit-mode marks into the dirty overlay (so wld.edit() can reassemble the user's authored state) BEFORE flipping __mode. Never writes canonical scene.json. play → edit: flip __mode first, then reload the active non-additive layer so the dirty overlay reapplies + play-mode runtime mutations are discarded. Toolbox wrappers (wld.play() / wld.edit()) are thin pass-throughs — they MUST NOT carry side-effect logic. If a future caller writes engine.mode = "play" directly (or via a different toolbox tool), the same side effects fire. Putting drain/reload on the toolbox alone would let a direct assignment leave the scene in a half-applied state. See man engine for the full surface and side effects.

Usage: local Engine = require("@builtin::modules.api.engine.engine") Also available as global: engine

modules/Engine/_fireWorldLoaded

_fireWorldLoaded(detail: string?)

INTERNAL. Mark the world fully loaded and fan out to every onWorldLoaded subscriber. Called by the builtin world-entrypoint loader once onWorldLoad has returned. Idempotent per load — re-fires on a genuine reload (mirrors a scene's onReady), so the flag is set true and subscribers run on each call.

Parameters

  • detail string? (optional) — Optional JSON text the boot timeline's world_loaded mark carries as its detail: the loader's account of the frames the load ran across.

modules/Engine/_resetWorldLoaded

_resetWorldLoaded()

INTERNAL. Clear the world-loaded latch on unbind/unload so a subsequent bind re-fires onWorldLoaded for the new world.

modules/Engine/editWaiters

editWaiters(): { EditWaiter }

Everyone waiting for this engine to leave play so their edit-mode work can run, oldest first. Each entry is { key, id, name, reason, heldSeconds }: id is the waiting caller, the same value vfs.currentAuthor().id reads, and heldSeconds is how long it has been waiting. A session holding play reads this to see whose work it is holding. Empty in edit mode and whenever nobody has said they are waiting.

for _, who in ipairs(engine.editWaiters()) do
log.warn(("%s has waited %.0fs for edit mode"):format(who.name or who.key, who.heldSeconds))
end

modules/Engine/engineRecord

engineRecord(record: { [any]: any })

Declare record an engine record: a table a subsystem keeps as its own account of what the engine holds — the renderer's registry of live features, its capture views — and puts back through its own reset. engine.snapshotModuleState neither copies nor walks it, and engine.restoreModuleState leaves it as that reset left it, so a module restore never seats a record back around what the reset did: a feature a suite destroyed stays destroyed, and one it made again under its identity stays the one the renderer ticks.

Parameters

  • record { [any]: any } — The table.
engine.engineRecord(activeFeatures)

modules/Engine/holdTimeScale

holdTimeScale(scale: number, options: TimeScaleHoldOptions?): () -> ()

Run the simulation at scale for as long as the hold stands, and return the release. engine.timeScale is one number for the whole process, so a rate one agent sets is the rate every other agent's entities and measurements run at; a hold records who set it and refuses a plain engine.timeScale write from anywhere else while it stands, so the rate cannot move under a measurement without the measurement hearing about it. Holds nest: the innermost names the rate, and the rate that stood before the outermost is back once the last release is called. Each release takes its own hold off the stack whatever order the releases come in. exclusive takes the rate for the owner key the call states: while that hold stands, a hold is admitted only when it states the same key, and every other one is refused with an error naming the key and the rate. That is what lets one caller pin the rate its run is measured at while another agent drives the same engine.

Parameters

  • scale number — The rate to run at: 1 is real time, 0.5 half speed, 0 frozen, 2 double. Finite and >= 0.
  • options TimeScaleHoldOptions? (optional) — owner is the key this hold is taken under, a non-empty string, and an exclusive hold states one. A hold that states no key takes the key of the agent the call is attributed to, or a minted hold#n when the call carries no agent identity, so every hold answers to a key engine.releaseTimeScale accepts and engine.timeScaleOwner reports as id. exclusive takes the rate for the stated key until the hold is released.

Returns () — A function that releases this hold. Calling it twice releases once.

local release = engine.holdTimeScale(0.08) ; task.wait(0.12) ; release()
local release = engine.holdTimeScale(1, { exclusive = true, owner = "duel-measurement" })

modules/Engine/markBaseline

markBaseline(): number

Read this module's own memory — the time-scale holds standing, who wrote the rate last, the rate that stood before any hold, the onTimeScaleChange subscribers and the highest watcher id each registry has handed out — and answer with a generation engine.resetToBaseline puts exactly that back from. Take it once, before the work whose changes are to be undone.

local mark = engine.markBaseline() -- then run, then engine.resetToBaseline(mark)

modules/Engine/markScriptingBaseline

markScriptingBaseline(): number

Record the scripting registries — world-event subscriptions, the four lifecycle-watcher lists, and the require cache — as they stand right now, as a mark engine.resetScriptingState() restores to. Marks stack: a reset names the generation it restores to, and the marks taken after that one go with it. Returns the new mark's generation, counting from 1. Mark once the engine is serving rather than while it boots: the registries keep growing as the prelude subscribes, the world entrypoint runs and the startup scene loads, so a mark taken partway through sits below the rest of that work and the first reset would remove it.

engine.markScriptingBaseline()
world.on("player_join", function() end)
engine.resetScriptingState() -- the subscription above is gone

modules/Engine/offDeviceRebuilt

offDeviceRebuilt(id: number): boolean

Remove an engine.onDeviceRebuilt subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none — already removed, or never registered.

Parameters

  • id number — Watcher id returned by engine.onDeviceRebuilt.
local id = engine.onDeviceRebuilt(function() end)
engine.offDeviceRebuilt(id)

modules/Engine/offMemoryPressure

offMemoryPressure(id: number): boolean

Remove an engine.onMemoryPressure subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none: already removed, or never registered.

Parameters

  • id number — Watcher id returned by engine.onMemoryPressure.
local id = engine.onMemoryPressure(function() end)
engine.offMemoryPressure(id)

modules/Engine/offModeChange

offModeChange(id: number): boolean

Remove an engine.onModeChange subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none — already removed, or never registered.

Parameters

  • id number — Watcher id returned by engine.onModeChange.
local id = engine.onModeChange(function() end)
engine.offModeChange(id)

modules/Engine/offPauseChange

offPauseChange(id: number): boolean

Remove an engine.onPauseChange subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onPauseChange.

modules/Engine/offTimeScaleChange

offTimeScaleChange(id: number): boolean

Remove an engine.onTimeScaleChange subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onTimeScaleChange.
local id = engine.onTimeScaleChange(function() end)
engine.offTimeScaleChange(id)

modules/Engine/offWorldLoaded

offWorldLoaded(id: number): boolean

Remove an onWorldLoaded subscriber by its watcher id.

Parameters

  • id number

modules/Engine/offWorldReady

offWorldReady(id: number): boolean

Remove an engine.onWorldReady subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onWorldReady.

modules/Engine/offWorldUnloading

offWorldUnloading(id: number): boolean

Remove an engine.onWorldUnloading subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onWorldUnloading.

modules/Engine/onDeviceRebuilt

onDeviceRebuilt(callback: (number) -> ()): number

Register a callback that fires after the engine has answered a lost render device by building another one. The callback receives the new device generation — a number that counts the devices this session has run on, and moves exactly once per rebuild. Returns a watcher id.

A device is lost when the driver resets, when the GPU is taken away, or when a browser reclaims a WebGPU context. Everything the engine can re-derive by itself it does: meshes, materials, shaders, render passes and the UI are all back on the new device before this fires. What it cannot re-derive is what YOUR content made and only the GPU held — a texture uploaded from pixels a script computed, a compute buffer it filled, a render target it created. Make those again here.

Content that owns no GPU resource of its own needs no subscriber: asset handles re-materialise on their next use.

Parameters

  • callback (number) -> () — Function invoked as (generation: number).
engine.onDeviceRebuilt(function(generation)
-- the noise field lived only on the GPU, so it is computed again
print("upload the noise field again for device generation", generation)
end)

modules/Engine/onMemoryPressure

onMemoryPressure(callback: (string, string) -> ()): number

Register a callback that fires when the system says it is short of memory, after the engine has released what that level of pressure releases. The callback receives the level ("normal", "moderate", "low" or "critical") and who reported it: onTrimMemory(<level>) for Android's report, script for renderer.signalMemoryPressure. Returns a watcher id.

The engine frees what it can rebuild by itself: mip levels the frame does not sample, render-target storage nothing is drawing into, and from "critical" half the texture budget. What it cannot free is what YOUR content keeps: a cache of generated meshes, a table of decoded audio, pixels kept for a later upload. Drop those here and build them again when they are next needed. A level stands for a minute after the last report, and renderer.memoryPressure() reads the level in force and what the engine released.

Parameters

  • callback (string, string) -> () — Function invoked as (level: string, source: string).
engine.onMemoryPressure(function(level, source)
if level ~= "normal" then
decodedClips = {} -- decoded again on next play
end
end)

modules/Engine/onModeChange

onModeChange(callback: (string, string) -> ()): number

Parameters

  • callback (string, string) -> ()

modules/Engine/onPauseChange

onPauseChange(callback: (boolean, boolean) -> ()): number

Register a callback that fires synchronously whenever the gameplay pause flag flips via an explicit engine.paused write. Callback receives (newPaused, oldPaused) as booleans. Returns a watcher id. Pause is independent of engine.mode: pausing play mode returns the editor authoring surface (free camera + EditorOnly entities) over the frozen play world, and resuming hides it again. Mode-driven pause resets (the edit=paused / play=running defaults applied on a mode flip) are delivered through onModeChange, not this hook.

Parameters

  • callback (boolean, boolean) -> () — Function invoked as (newPaused: boolean, oldPaused: boolean).
local id = engine.onPauseChange(function(paused)
print(paused and "frozen" or "running")
end)

modules/Engine/onTimeScaleChange

onTimeScaleChange(

Register a callback that fires on every move of the simulation rate, whoever moved it: an engine.timeScale write, a hold taken at a rate other than the one in force, a release that hands a different rate back. The callback receives (newScale, oldScale, owner), where owner is the record engine.timeScaleOwner answers with. It counts transitions, so a write or a hold landing on the rate already in force moves nothing and announces nothing; engine.timeScaleOwner is what answers who holds a rate that has stayed where it is. Several agents drive one engine at once, so this is how a run whose numbers depend on the rate learns that somebody else moved it instead of reading the number and finding out afterwards. Returns a watcher id.

local id = engine.onTimeScaleChange(function(new, old, who)
log.warn(("rate %g -> %g by %s"):format(old, new, who and who.id or "nobody"))
end)

modules/Engine/onWorldLoaded

onWorldLoaded(callback: () -> ()): number

Register a callback fired (no args) when the world is fully LOADED — its .world_entrypoint.luau ran AND its onWorldLoad returned (the startup scene loaded, defaults seeded, editor UI mounted). This is strictly AFTER onWorldReady (content synced): ready = "bytes are in the VFS"; loaded = "the entrypoint has run". LATCHED — a callback registered after the world is already loaded fires immediately, so a late consumer never misses it and never has to poll. Read the same state synchronously via engine.worldLoaded.

Parameters

  • callback () -> () — Function invoked with no arguments.

modules/Engine/onWorldReady

onWorldReady(callback: () -> ()): number

Register a callback fired (no args) when the bound world's content has been synced into the VFS and the world is ready to load. This is the race-free, user-space hook that drives the whole world-VM lifecycle: the builtin world-entrypoint loader subscribes to it and, when it fires, loadstring(vfs.read(...))s /source/.world_entrypoint.luau and runs its onWorldLoad — exactly the way a scene entrypoint loads. The trusted VM fires this (via world.markReady()) ONLY once the bytes are in the VFS, so a subscriber never sees a half-synced world. Returns a watcher id.

Parameters

  • callback () -> () — Function invoked with no arguments.

modules/Engine/onWorldUnloading

onWorldUnloading(callback: () -> ()): number

Symmetric teardown of engine.onWorldReady: register a callback fired (no args) when the bound world is unbinding/swapping out. The builtin loader runs the world entrypoint's onWorldUnload here, so the world entrypoint has the same load/unload parity a scene entrypoint has. Returns a watcher id.

Parameters

  • callback () -> () — Function invoked with no arguments.

modules/Engine/releaseBaseline

releaseBaseline(generation: number): boolean

Drop the mark generation names, and every mark taken after it, once the caller that took it is done resetting to it. It touches the marks alone.

Parameters

  • generation number — The mark to drop, as engine.markBaseline returned it.
engine.releaseBaseline(mark) -- the run is over

modules/Engine/releaseScriptingBaseline

releaseScriptingBaseline(generation: number): boolean

Drop the scripting mark generation names, and every mark taken after it, once the caller that took it is done resetting to it. The registries stay as they stand. A mark holds the require cache's keys as they stood, so a caller that marks once per run drops each mark when its run is over.

Parameters

  • generation number — The mark to drop, as engine.markScriptingBaseline returned it.
engine.releaseScriptingBaseline(mark) -- the run is over

modules/Engine/releaseTimeScale

releaseTimeScale(owner: string): number

Parameters

  • owner string

modules/Engine/resetScriptingState

resetScriptingState(generation: number?): { [string]: number }

Drop every world-event subscription, lifecycle watcher and cached module registered since the mark generation names — the newest engine.markScriptingBaseline() when omitted — leaving everything registered before it in place, including the builtin world-entrypoint loader, which subscribes at VM boot and so always sits below any mark. The marks taken after that one go with the reset. A module with a frame on the stack keeps its cache entry and its registrations. Raises when no such mark is in force. Returns per-registry counts of what was removed: worldEvents, modeWatchers, worldReadyWatchers, worldUnloadingWatchers, pauseWatchers, deviceRebuiltWatchers, memoryPressureWatchers, modules, and total.

Parameters

  • generation number? (optional) — The generation to restore to, as markScriptingBaseline returned it; the newest mark when omitted.

modules/Engine/resetToBaseline

resetToBaseline(generation: number?): number

Put this module's own memory back as engine.markBaseline read it: every time-scale hold taken since comes off, every onTimeScaleChange subscriber added since is removed, the last writer and the base rate read as they did, the rate in force follows, and the record of who registered each watcher forgets the ids handed out since. The marks taken after generation go with the reset.

Parameters

  • generation number? (optional) — The mark to restore to; the newest when omitted.
engine.resetToBaseline(mark)

modules/Engine/restoreModuleState

restoreModuleState(snapshot: { [string]: any }, options: { remint: { [string]: string }?, gone: ((any) -> boolean)?, successor: ((any) -> any?)?, only: { [string]: boolean }? }?): { [string]: any }

Put every module's state back to what engine.snapshotModuleState read. A table is refilled in place, so a closure holding it sees the restored contents, and the upvalue is pointed back at it, so a module that assigned its local a new table is reading the restored one again.

The same snapshot restores to the same state however many times it is used, so one reading taken before a run puts every later point in that run back to it.

A value whose reading names an object the engine no longer holds — a buffer whose storage was destroyed since, answering alive false — is left as it stands: what it remembered cannot be given back to it, and what it holds now is what it went on to make. The module holding it is left whole, because what it remembers beside that object — the count it sized the object for, the flag that says it made one — is the state of the object it now holds instead; it is counted under stale. A data table naming a gone object in one of its fields is a registry, and is refilled around that field. Called from a coroutine that can yield, the restore gives the frame back every few milliseconds. the engine re-minted since replaced by the id it now uses.

Parameters

  • snapshot { [string]: any }
  • options { remint: { [string]: string }?, gone: ((any) -> boolean)? — remint maps an id the reading holds to the id the engine now uses for the same thing — a render feature's guid after the feature was made again under its identity. gone answers whether a value the reading holds names something the engine no longer holds — a record carrying a render feature's guid when no feature carries it now; a handle answering alive false is known without it. successor answers, for an object that has gone, the live object standing in for it — the handle under a buffer's name after the buffer was made again under it — and the reading is put back holding that one, its own fields left as they are. A handle that already answered alive false when the reading was taken is the module's own record of something gone before it, and is put back as it is. same answers whether a value a module holds now already stands in for the gone object a successor was found for — the module's own live handle to the buffer made again under the name — and such a value is kept, the successor released. only names the modules to put back, by module name, and every other module's reading is left untouched — a second pass over the modules an earlier one left as they stood, once what stands in for the objects they named exists.
  • successor ((any) -> any?)? (optional)
  • only { [string]: boolean }? }? (optional)
engine.restoreModuleState(snap)
engine.restoreModuleState(snap, { remint = { [oldGuid] = newGuid } })

modules/Engine/scriptingRegistryCounts

scriptingRegistryCounts(): { [string]: number }

How many subscriptions each scripting registry holds right now, plus the size of the require cache and the generation of the mark in force. Keys: worldEvents, modeWatchers, worldReadyWatchers, worldUnloadingWatchers, pauseWatchers, deviceRebuiltWatchers, memoryPressureWatchers, modules, and baselineGeneration (nil when no mark has been taken).

modules/Engine/setMode

setMode(mode: string, options: { strict: boolean?, endSession: boolean? }?): { mode: string, bypassed: { any }, leftPlaySession: boolean }

Change the engine mode with per-call control over the play gate, and read back what the change went past. engine.mode = value is the same flip with the defaults.

options.strict = false lets THIS call enter play while your own content carries error-severity diagnostics. It settles with the call: the world's lsp.strict_mode is untouched, so no other session and no later session of the world sees a different gate. The returned bypassed array holds the diagnostics the call went past — each { path, line, col, code, message, severity } — and the engine log carries the same list. An error in content another session wrote never gates the flip, so it never appears here; a push still refuses to publish while any of them stands.

Parameters

  • mode string — "edit" or "play".
  • options { strict: boolean?, endSession: boolean? }? (optional) — { strict: boolean?, endSession: boolean? }. strict = false waives the play gate for this call; true or omitted honours the world's lsp.strict_mode. endSession = true with "edit" ends the running play session for every caller in it, whether or not you take part, and answers for everything the session holds (engine.discardPlayChanges() arms its discard); without it, an "edit" from one of several callers in play takes only that caller out, and one from a caller outside the session is refused.
local report = engine.setMode("play", { strict = false })
for _, d in ipairs(report.bypassed) do
print(("entered play past %s:%d — %s"):format(d.path, d.line, d.message))
end

modules/Engine/snapshotModuleState

snapshotModuleState(): { [string]: any }

Read the state every cached module holds — the upvalues of the closures it hands back — and answer with it, so engine.restoreModuleState can put exactly that back later.

Take it once, before the work whose changes are to be undone. Data is copied, so a table a module goes on mutating does not change what was read; an object — a table carrying a metatable or methods of its own — is kept by reference and its fields copied, so restoring returns the object every other holder is still attached to rather than a lookalike that answers none of its methods.

A module with a frame on the stack while the reading is taken — the caller, and whatever called it — is mid-operation, and what it holds is left out: neither copied here nor written back by the restore, however another module reaches it. Called from a coroutine that can yield, the walk gives the frame back every few milliseconds, so the engine keeps stepping through a reading that takes seconds.

local snap = engine.snapshotModuleState()

modules/Engine/timeScaleHolds

timeScaleHolds(): { string }

modules/Engine/waitForEdit

waitForEdit(reason: string?): () -> ()

Say that this caller's edit-mode work is waiting on the play session, and get back the call that takes the wait off.

One process runs one simulation, so a play session holds the engine in play for every caller outside it, and an agent whose edit-mode work cannot proceed has no way to say so. While this wait stands it rides the run-state block of every response any session gets from this engine, so the callers in the play session learn somebody is waiting for it. Read engine.playSession for who they are, and engine.modeOwner for who entered play and how long ago.

A wait is keyed by the caller the call was attributed to, so a named caller that registers twice moves its own entry forward rather than queueing behind itself: one caller waits once. A call carrying no agent identity is named by nothing to key on, so each such ask stands as its own waiter and the release it handed back takes that one off. Reaching edit mode takes every wait off, so a caller whose release is lost with the task that held it leaves nothing standing.

Parameters

  • reason string? (optional) — What this caller is waiting to do, shown beside its name.

Returns () — A function that takes this caller's wait off.

local done = engine.waitForEdit("promote my scene edits")
-- ... once the engine reaches edit and the work is taken ...
done()

modules/Engine/watcherSources

watcherSources(): { [string]: { [number]: string } }

Who registered each watcher still standing, by registry: modeWatchers, pauseWatchers, deviceRebuiltWatchers, memoryPressureWatchers, worldReadyWatchers and worldUnloadingWatchers, each a map from watcher id to the source and line that registered it. A watcher registered before this module loaded is absent. What a reading of the registry counts names when it finds a watcher more than it began with.

for id, at in pairs(engine.watcherSources().modeWatchers) do print(id, at) end

modules/EntityRecords/README

require("@builtin::modules.entity_records") -- EntityRecords

Walks a live entity hierarchy into the flat record array the engine's entity templates use. Shared by bundle (its entity_template) and by scene builds (their baked output), so both speak one record shape.

Usage: local EntityRecords = require("@builtin::modules.entity_records")

modules/EntityRecords/addData

addData(data: any, instanceName: string?): any

The data table component.add puts a record's instance back with: data itself for the anonymous instance, and a copy carrying name for a named one, which is the key component.add reads the instance name from.

Parameters

  • data any (optional) — The entry's field values.
  • instanceName string? (optional) — The entry's instance name, or nil for the anonymous instance.
entity(id).component.add("Pool", EntityRecords.addData({ amount = 120 }, "mana"))

modules/EntityRecords/authoredComponentData

authoredComponentData(

A component instance's snapshot with the fields the instance wrote about its own runtime removed, leaving what states how it was CONFIGURED. A capture reads it to build the record, and whoever diffs a live entity against that record reads it too, so both sides speak the same fields.

local d = EntityRecords.authoredComponentData(id, ty, nil, snapshot)

modules/EntityRecords/captureRecord

captureRecord(rid: string, parentOriginalId: string?): any

Capture ONE entity into a template record. Reads LIVE component public state (serialized component snapshots), not init data, so the record matches what is on screen, and states the fields each instance's configuration states (configuredFields): a field left to its declared default stays out, so a later change to that default reaches whatever the record rebuilds. Each component INSTANCE gets its own entry, carrying instance_name when the instance has one, so a type the entity carries several of comes back as the same several. The record also carries the entity's own active flag, every attribute it holds, and its lifecycle mode and replication scope when either is other than the default. The entity's runtime id IS its record original_id, so cross-entity component references — which already point at runtime ids — round-trip and get remapped on the next instantiate. Each component entry names where it holds such a reference in entity_fields, taken from the component's declared field kinds: a top-level field by its name, and a reference held as a list element or struct subfield by its path from the field (an array such as { "attachments", 1, "target" }), so a rebuild resolves exactly those.

Parameters

  • rid string — Runtime entity id to capture.
  • parentOriginalId string? (optional) — Parent's original_id, or nil for a root record.
local rec = EntityRecords.captureRecord(id, nil)

modules/EntityRecords/carriesInstance

carriesInstance(rid: string, typeName: string, instanceName: string?): boolean

Whether the entity carries the component instance a record entry names: the instance of typeName under instanceName, or the type's anonymous instance when instanceName is nil. A loader asks this before it puts an instance back, so a named instance is never read as already present because the anonymous one is, and the reverse.

Parameters

  • rid string — Runtime entity id.
  • typeName string — Component type the entry names.
  • instanceName string? (optional) — The entry's instance name, or nil for the anonymous instance.
if not EntityRecords.carriesInstance(id, "Pool", "mana") then entity(id).component.add("Pool", { name = "mana" }) end

modules/EntityRecords/componentIsCodeAttached

componentIsCodeAttached(rid: string, componentType: string): boolean

Whether another component's lifecycle attached this component instance, rather than an author putting it there. A composed asset brings its own machinery with it — a humanoid avatar attaches a character controller to the body it expands into — and that machinery comes back on its own wherever the composition does. A record that named it would put a second one beside the one the expansion just produced, and a rebuild that removed every component its records leave unnamed would tear the expansion off the entity it belongs to. Both sides of a rebuild ask this.

Reached through the _G singleton the origin module publishes, which is the same answer the scene serializer takes for the same question; a load order that has not published it yet reads every component as authored.

Parameters

  • rid string — Runtime entity id carrying the instance.
  • componentType string — Component type name as the entity reports it.
if EntityRecords.componentIsCodeAttached(id, "Humanoid") then continue end

modules/EntityRecords/compose

compose(

Build the flat record array by walking rootId's hierarchy. Skips temporary entities and their descendants — scaffolding and editor-only tooling stay out of a baked result. The explicit root is always captured: the caller named THAT entity as the thing to serialize, so temporary pruning applies to descendants.

local records = EntityRecords.compose(rootId)

modules/EntityRecords/composeMany

composeMany(

Compose several roots into one flat record array. A build captures a SET of roots (a builder may spawn several unparented entities), not the single root a bundle composes from. The roots are ordered the same way siblings are — by rank, then name, then id.

local records = EntityRecords.composeMany({ idA, idB })

modules/EntityRecords/configuredComponentData

configuredComponentData(ref: any, data: any): any

A live component instance's snapshot limited to the fields its configuration states (configuredFields). A field the instance leaves to its declared default is left out, so whatever is rebuilt from the result takes the component's default as it stands then, rather than the value it had when this was read.

Parameters

  • ref any (optional) — The live component instance (a component proxy).
  • data any (optional) — Snapshot of the instance's data.
local ComponentSnapshot = require("@builtin::modules.component_snapshot")
local d = EntityRecords.configuredComponentData(ref, ComponentSnapshot.snapshot(ref))

modules/EntityRecords/configuredFields

configuredFields(ref: any): { [string]: boolean }?

The fields one live component instance's configuration states: the keys of the table it was attached with, and every field written since (through its proxies, or by a replicated or undo write). That is the set a hot reload carries across a source edit; every other field holds the component's declared default, so a record that leaves it out lets a changed default reach the instance. Nil when the engine does not answer, which a caller reads as "every field".

Parameters

  • ref any (optional) — The live component instance (a component proxy).
local set = EntityRecords.configuredFields(entity(id).component("Light"))

modules/EntityRecords/instanceNameOf

instanceNameOf(entry: any): string?

The instance name a component entry of a record states, or nil for the type's anonymous instance. Every record shape names a component instance the same way: a scene entity record, a captured entity record and a build record each carry instance_name beside type on the entry of a named instance, so an entity carrying several instances of one type comes back carrying the same several.

Parameters

  • entry any (optional) — One entry of a record's components list.
local name = EntityRecords.instanceNameOf({ type = "Pool", instance_name = "mana" }) -- "mana"

modules/EntityRecords/instancesOf

instancesOf(rid: string, typeName: string?): { ComponentInstance }

The { type, instance?, data } records component.getAll answers for the entity, normalised: instance is nil for the anonymous instance and the instance's name otherwise. Narrowed to one type when typeName is given.

Parameters

  • rid string — Runtime entity id.
  • typeName string? (optional) — Component type to narrow to, or nil for every instance.
for _, inst in ipairs(EntityRecords.instancesOf(id, "Pool")) do print(inst.type, inst.instance) end

modules/FontAssetTypeRef/README

require("@builtin::assetTypes.font.behavior") -- FontAssetTypeRef

Hooks for .font assets. onCreate is the type's contribution to the generic asset.create("font", name, opts) flow (mirroring texture.assetType): it parses the font file ONCE into the baked, vectorized glyph format (data.zfnt) — the asset payload. onRegister loads that baked format via the engine font.register(name, zfnt) primitive — a font is a general CPU resource, so one registration makes fontFamily = "<name>" resolve on the egui UI text surface AND on text.* 2D/3D rendering, and feeds font.glyph / font.textMesh for true 3D text.

Usage: local FontAssetTypeRef = require("@builtin::assetTypes.font.behavior")

modules/FontAssetTypeRef/onChange

onChange(self, change)

Install the instance again when a file its registration reads is written: face.json, data.zfnt, a raw font file, or the font or coverage file an on-demand entry names. The font is then selected by the names the descriptor now gives it and no longer by a name it dropped, on every text surface, and text drawn in it is drawn again. A folder that arrives whole installs once, unless onRegister already installed it.

Parameters

  • self any (optional) — The .font instance's AssetRef.
  • change any (optional) — { path, asset, kind, ... }: the written file and how it arrived.

modules/FontAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

The .font type's contribution to asset.create("font", name, opts). Parses the font file ONCE into the baked vectorized glyph format and stores it as data.zfnt. The original font bytes are embedded inside the baked payload, so no separate font file is kept. The heavy parse happens here, at author time — onRegister then loads the result cheaply.

Parameters

  • name string — The font asset name (also the registered fontFamily).
  • opts CreateOpts
asset.create("font", "Inter", { bytes = fontBytes, ext = "ttf" })

modules/FontAssetTypeRef/onRegister

onRegister(self)

Install this .font instance into the engine's text systems. Reads the instance's baked data.zfnt and calls font.register(name, zfnt), which loads the vectorized glyph data into the runtime store (for font.glyph / font.textMesh) and feeds the embedded font bytes to the 2D text and egui UI systems. Falls back to a raw font file for instances authored before the baked layout. An instance whose face.json lists onDemand entries is declared by their coverage instead (font.declare), and each entry's file is read the first time text needs a character it covers. Fired once per instance by the asset system (live on asset.create, and in the world-load sweep). Guarded so a single bad font never errors the sweep.

Parameters

  • self any (optional)

modules/FontAssetTypeRef/payloadReady

payloadReady(path: string): boolean

Whether a .font instance's payload is present to register from: its baked data.zfnt, or, for an on-demand face, the face.json that lists its chunks. The builtin boot registration asks this before firing onRegister; on WASM a builtin leaf arrives asynchronously, so a read that returns nil answers false and has started the fetch a later call finds landed.

Parameters

  • path string — VFS path of the .font folder.

modules/FrameBudget/README

require("@builtin::modules.frame_budget") -- FrameBudget

The share of a frame that cooperative engine work spends before it hands the frame back. Work that runs as a sequence of units on a task (a scene load applying its records, the world-load onRegister sweep) asks due() before each unit and yields once the frame's slice is spent, so a long piece of work runs across frames at the engine's frame rate rather than holding one frame for its whole length. One slice per frame is shared by every caller in the VM: what each caller runs between its readings of the slice in a frame (task.runTime, so none of what the rest of the frame runs) is charged to it. A unit is never split, so the first unit of a frame always runs, and a frame holds the slice plus the one unit that crossed it.

Usage: local FrameBudget = require("@builtin::modules.frame_budget")

modules/FrameBudget/due

due(ms: number?): boolean

Whether the caller hands the frame back before its next unit: it runs on a coroutine that can yield, and this frame's slice has spent ms. Code that cannot yield is answered false and runs its units in the frame it was called in.

Parameters

  • ms number? (optional) — Milliseconds the slice may spend (default FrameBudget.DEFAULT_MS).
if FrameBudget.due() then FrameBudget.wait() end

modules/FrameBudget/mark

mark(into: { [string]: any }?): { [string]: any }

A reading of how long the calling coroutine has run (task.runTime), for ranMs to measure the work it runs from here: a budget of a caller's own, counted apart from the frame's shared slice. Written into into when given, a table several pieces of work share by reference. The reading names its coroutine by a token of its own.

Parameters

  • into { [string]: any }? (optional) — The table to write the reading into, or nil for a new one.
local mark = FrameBudget.mark() ... if FrameBudget.ranMs(mark) > 5 then FrameBudget.wait() FrameBudget.mark(mark) end

modules/FrameBudget/ranMs

ranMs(mark: { [string]: any }): number

Milliseconds the calling coroutine has run since mark was read on it: none of the time it spent suspended (waiting on a frame, a read-back, or another coroutine it resumed). 0 when the mark was read on another coroutine.

Parameters

  • mark { [string]: any } — A reading from mark.
if FrameBudget.ranMs(mark) > 5 then FrameBudget.wait() FrameBudget.mark(mark) end

modules/FrameBudget/remainingMs

remainingMs(ms: number?): number?

Milliseconds this frame's slice has left before due(ms) answers true, never below zero, for work that bounds a unit by time itself (a native call told how long it may run). Nil when the caller runs on a coroutine that cannot yield: it runs its work in the frame it was called in, with no slice to keep to.

Parameters

  • ms number? (optional) — Milliseconds the slice may spend (default FrameBudget.DEFAULT_MS).
local left = FrameBudget.remainingMs()

modules/FrameBudget/spent

spent(): number

Milliseconds the budgeted work of the current frame has spent: the time each caller ran between its readings of the slice in this frame, summed over every caller. A caller's first reading in a frame charges nothing.

local ms = FrameBudget.spent()

modules/FrameBudget/sync

sync(fn: () -> ...any): ...any

Run fn without handing the frame back: due() answers false to every caller until it returns, so the work its callees do between their checkpoints completes inside the call. For a caller whose result must stand when the call returns. Answers what fn answers.

Parameters

  • fn () -> ...any — The function to run.
local entry = FrameBudget.sync(function() return effect:entry("burst") end)

modules/FrameBudget/wait

wait()

Hand the frame back: yield to the next frame and open that frame's slice as the caller resumes, so the unit it runs next is charged to the slice. A caller that does work of its own around the yield asks due() and calls this; yield() is the two together.

if FrameBudget.due() then FrameBudget.wait() end

modules/FrameBudget/yield

yield(ms: number?): boolean

Yield to the next frame when due(ms) answers true. Called before each unit of a long piece of work, so the work spends at most a slice of each frame it runs across.

Parameters

  • ms number? (optional) — Milliseconds the slice may spend (default FrameBudget.DEFAULT_MS).
for _, path in ipairs(paths) do FrameBudget.yield() print(vfs.read(path)) end

modules/FrameDriver/README

require("@builtin::modules.frameDriver") -- FrameDriver

A loop a library module runs on its own behalf once per frame, for as long as it has work: the driver behind a system that advances itself without an owner calling update(dt), such as an autoUpdate particle emitter or an effects.play.

The loop is a system task (task.spawnSystem), so it belongs to the module rather than to whoever happened to be running when the first piece of work arrived. A one-shot execute, a task scope, a component hook and a scene entrypoint can each start it, and none of them ending stops it. It runs while there is work and ends once there is none; the next ensure starts it again. A loop that ended any other way (cancelled by handle, halted by the watchdog, or torn down with its VM), whether at its frame wait or in the middle of a step, is recognised by its task status, and ensure starts a fresh one in its place. With gameplayClock = true the loop steps on the gameplay clock: in play, a frame that arrives while engine.paused is set calls nothing, and the next frame after the pause lifts carries on from there. In edit the loop keeps stepping, because there it is the editor's authoring preview, the same standing editorUpdate(dt) has. engine.timeScale reaches the step through the scheduler, which scales the delta task.wait() reports.

Usage: local FrameDriver = require("@builtin::modules.frameDriver")

modules/FrameDriver/gameplayClockRunning

gameplayClockRunning(): boolean

Whether the gameplay clock is running: false only in a play session that is paused. Edit mode reads as paused and still answers true, since a self-driven system there is the editor's preview.

if FrameDriver.gameplayClockRunning() then sys:update(dt) end

modules/FrameDriver/new

new(opts: Options): Driver

Make a driver. It starts on the first ensure and runs while hasWork answers true.

Parameters

  • opts Options — { name, hasWork, step, gameplayClock?, maxStep? }, see Options.
local driver = FrameDriver.new({ name = "ripples", hasWork = function() return #live > 0 end, step = advanceAll, gameplayClock = true })

modules/FrameShape/README

require("@builtin::tests.fixtures.frame_shape") -- FrameShape

The shape of the frame the viewport draws, for a suite whose fixture or whose reading depends on it: the width over the height of the scene rect, the extent the frame covers at a distance, whether the frame was drawn at the scene rect's shape yet, and a scene rect drawn at a chosen shape for as long as a case needs it. The frame is the rect the engine draws the scene into: the headless framebuffer a native engine was started at, the browser window on the web, and whatever the editor's panels leave of either. A case written for one of those shapes and read on another reads a different part of the scene.

Usage: local FrameShape = require("@builtin::tests.fixtures.frame_shape")

modules/FrameShape/aspectOf

aspectOf(w: number, h: number): number

The width over the height of a rect of pixels.

Parameters

  • w number — The rect's width.
  • h number — The rect's height.
local aspect = FrameShape.aspectOf(1920, 1080)

modules/FrameShape/frameHalfExtent

frameHalfExtent(distance: number): (number, number)

The half width and half height of the rectangle the viewport's frame covers distance in front of the camera drawing it. Read off the frame as it was drawn, so a fixture sized from it fills the frame whatever shape an editor layout gives the scene rect.

Parameters

  • distance number — How far in front of the camera, in metres.
local halfW, halfH = FrameShape.frameHalfExtent(5)

modules/FrameShape/frameMatchesSceneRect

frameMatchesSceneRect(cameraId: string?): boolean

Whether the viewport's frame was drawn at the shape of the scene rect the engine reports now. A resize reaches the camera's frame a frame after the surface, and a fixture sized or a tile worked out off the frame before it would be for a shape that is gone.

Parameters

  • cameraId string? (optional) — The camera whose frame to read; the viewport camera when nil.
while not FrameShape.frameMatchesSceneRect() do task.wait() end

modules/FrameShape/widenTheSceneRect

widenTheSceneRect(aspect: number?): () -> ()

Draw the scene rect aspect times wider than it is tall, by changing the surface's height and keeping its width. The panels an editor layout puts under or over the scene take a share of that height, so the height that gives the shape is found by measuring: each step resizes the surface, reads the scene rect the layout drew at that size, and aims the next step along the line through the two latest readings. The shape is asserted, to within a tenth of aspect either way, rather than assumed, so a host that keeps the size it has, or cannot draw the scene rect at that shape, fails the case instead of running it at another shape. Call it from inside a test: it registers a cleanup that puts the surface back, without waiting, for a case that ends early.

Parameters

  • aspect number? (optional) — The width over the height to reach; WIDE_ASPECT when nil.

Returns () — What puts the surface back and waits for the host to realize it, for the case to call at its end so the frames that do it are drawn under that case rather than under whatever runs next.

local putBack = FrameShape.widenTheSceneRect(); --[[ … ]] putBack()

modules/FxFrameProbe/README

require("@builtin::tests.fixtures.fx_probe") -- FxFrameProbe

The reader half of the frame probe — the grid fx_frame_probe.renderFeature writes each frame, and the measurements taken off it. A suite requires this module and enables the feature; the feature records the target it writes here, so both halves address one target. They are separate assets because renderer.feature.create compiles a feature's init.luau as its own chunk rather than requiring it, so a feature cannot publish state to a caller through its own module table. What it is for: a returned screenshot is not evidence. A fully transparent emitter reports live particles, reports that it drew, and changes the image hash. A reader needs a magnitude, taken from the frame a viewer actually sees, that a known control is shown to move.

Usage: local FxFrameProbe = require("@builtin::tests.fixtures.fx_probe")

modules/FxFrameProbe/channelSums

channelSums(grid: { [string]: any }?): { [string]: number }

Sum each colour channel over the grid — how much red, green and blue the frame carries. What a colour parameter has to move.

Parameters

  • grid { [string]: any }? (optional) — A grid from read().
local c = probe.channelSums(probe.read())

modules/FxFrameProbe/faceTiles

faceTiles(

The tiles the middle fifth of a flat face is drawn in: a six by six grid of points over that fifth, each turned into the tile it lands in. The middle rather than the whole, because a tile is a downsample of many pixels and one straddling the face's silhouette carries the background with it.

local tiles, off = probe.faceTiles(view, { -4, 40.25, -4 }, { 8, 0, 0 }, { 0, 0, 8 })

modules/FxFrameProbe/litRegion

litRegion(grid: { [string]: any }?, baseline: { [string]: any }?, threshold: number): { [string]: number }

The size of the lit region: how many tiles rise threshold luminance above baseline's same tile, and the radius of a disc with that area, in tiles. This is the spatial reading — how WIDE something drew, which no whole-frame number can answer.

Parameters

  • grid { [string]: any }? (optional) — The grid to measure.
  • baseline { [string]: any }? (optional) — The grid of the same view with the subject absent.
  • threshold number — How far above the baseline tile a tile must rise to count.
local r = probe.litRegion(after, before, 6).radius

modules/FxFrameProbe/movedTiles

movedTiles(grid: { [string]: any }?, other: { [string]: any }?, threshold: number): number

How many tiles differ between two grids by at least threshold luminance, in either direction. Zero says the two readings are the same picture — which is what a reading that has not caught up with a change yet also answers, so this is how a caller tells a still frame from one that has not arrived.

Parameters

  • grid { [string]: any }? (optional) — The grid to measure.
  • other { [string]: any }? (optional) — The grid to measure it against.
  • threshold number — How far a tile must differ to count.
if probe.movedTiles(now, before, 4) > 0 then --[[ the change landed ]] end

modules/FxFrameProbe/read

read(): { [string]: any }?

Read the last grid the probe wrote. Returns { width, height, tiles }, where tiles[y * width + x + 1] is that tile's mean colour as { r, g, b } in 0..255, plus lum — the tile's luminance on the same scale.

local grid = probe.read(); print(grid.tiles[1].lum)

modules/FxFrameProbe/rowOf

rowOf(tile: number): number

The row of the grid a tile from tileAt sits in, counted from 0 at the top of the frame.

Parameters

  • tile number — A 1-based tile index.
local row = probe.rowOf(tile)

modules/FxFrameProbe/skyTiles

skyTiles(view: { [string]: any }, toward: { number }): ({ number }, number)

The tiles of the rows above the horizon, which is where sky is drawn whatever shape the frame is. The horizon is the row a point level with the camera is drawn in, as far along the ground direction toward toward as the lens resolves; the row is read off the view's projection whether or not that point falls inside the frame's columns. The row just above it and the frame's top row are left out, so no tile read reaches the ground or the frame's edge.

Parameters

  • view { [string]: any } — A view from camera.viewData(id): its position is the eye.
  • toward { number } — A world point { x, y, z } the camera looks toward; only its x and z are read.
local sky, horizonRow = probe.skyTiles(view, { 0, 40, 0 })

modules/FxFrameProbe/sphereTiles

sphereTiles(

The tiles the inner part of a sphere is drawn in: every tile whose whole rect lies inside the sphere's outline drawn inner of the way out from its middle. The outline is the circle along which a line from the eye grazes the sphere, projected through the view the frame was drawn under, so the tiles follow the sphere at whatever shape the frame is. Whole tiles, and inside a margin, because a tile is a downsample of many pixels and one straddling the silhouette carries the background with it.

local tiles, off = probe.sphereTiles(camera.viewData(cam.id), { -3.5, 0, 0 }, 1.5)

modules/FxFrameProbe/tileAt

tileAt(view: { [string]: any }, x: number, y: number, z: number): number?

The tile a world point is drawn in, worked out from the view the frame was drawn under. The grid covers the whole frame whatever shape the frame is, so a tile written down as a constant lands on a different part of the scene at every other shape; a tile worked out from the view lands on the point at every shape.

Parameters

  • view { [string]: any } — A view from camera.viewData(id): its viewProj is the matrix the frame was drawn with.
  • x number — World x of the point.
  • y number — World y of the point.
  • z number — World z of the point.
local tile = probe.tileAt(camera.viewData(cam.id), 0, 1, 0)

modules/FxFrameProbe/tilesColour

tilesColour(grid: { [string]: any }, tiles: { number }): { r: number, g: number, b: number, lum: number }

The mean colour of a set of tiles in a grid.

Parameters

  • grid { [string]: any } — A grid from read().
  • tiles { number } — 1-based tile indices, as tileAt, faceTiles, sphereTiles and skyTiles hand them back.
local c = probe.tilesColour(probe.read(), tiles)

modules/FxFrameProbe/tilesLuminance

tilesLuminance(grid: { [string]: any }, tiles: { number }): number

The mean luminance of a set of tiles in a grid.

Parameters

  • grid { [string]: any } — A grid from read().
  • tiles { number } — 1-based tile indices, as tileAt, faceTiles, sphereTiles and skyTiles hand them back.
local lum = probe.tilesLuminance(probe.read(), tiles)

modules/FxFrameProbe/totalLuminance

totalLuminance(grid: { [string]: any }?): number

Sum the grid's luminance — the whole frame's brightness as one number.

Parameters

  • grid { [string]: any }? (optional) — A grid from read().
local before = probe.totalLuminance(probe.read())

modules/GI.Scenarios/README

require("@builtin::systems.globalIllumination.scenarios") -- GI.Scenarios

Several baked lighting states per probe volume, blended at runtime.

A volume holds one bake, so a scene lit for noon cannot become a scene lit for dusk without baking again — which takes far longer than a transition is allowed to. Capturing each bake under a name and blending the captures turns that into an interpolation the frame can afford. Irradiance adds, so blending the spherical-harmonic coefficients is the same as blending the light that produced them. Each coefficient's fourth lane is not light, though: it carries the probe's per-axis visibility reach and the markers the sampler reads. Those describe the geometry the volume sits in, which every scenario shares, so they are carried from the heaviest-weighted scenario rather than averaged — the mean of two distances describes no wall that exists. Carrying them from the heaviest contributor also makes the ends of a transition exact: a blend that names one scenario at full weight reproduces that scenario's field lane for lane.

Usage: local GI.Scenarios = require("@builtin::systems.globalIllumination.scenarios")

modules/GI.Scenarios/blend

blend(volumeEntityId: string, weights: { [string]: number }) -> boolean

Publish the weighted mix of named scenarios to the renderer.

Parameters

  • volumeEntityId string
  • weights { [string]: number }

Returns boolean

modules/GI.Scenarios/capture

capture(volumeEntityId: string, name: string) -> number

Store the volume's currently published field under a name. Returns how many probes it holds.

Parameters

  • volumeEntityId string
  • name string

Returns number

modules/GI.Scenarios/clear

clear(volumeEntityId: string)

Parameters

  • volumeEntityId string

modules/GI.Scenarios/field

field(volumeEntityId: string, name: string) -> { number }?

Parameters

  • volumeEntityId string
  • name string

Returns { number }?

modules/GI.Scenarios/forget

forget(volumeEntityId: string, name: string)

Parameters

  • volumeEntityId string
  • name string

modules/GI.Scenarios/list

list(volumeEntityId: string) -> { string }

Parameters

  • volumeEntityId string

Returns { string }

modules/GI.Scenarios/mix

mix(fields: { { number } }, weights: { number }) -> { number }

Blend baked SH fields by weight. Pure — no volume, no GPU.

Parameters

  • fields { { number } }
  • weights { number }

Returns { number }

modules/GI.Scenarios/stats

stats() -> table

Returns table

modules/GI.SkyResponse/README

require("@builtin::systems.globalIllumination.skyResponse") -- GI.SkyResponse

A probe volume's sky term, evaluated at publish time instead of baked in.

A probe bake follows each path until it escapes, and what escapes carries the sky's colour. Baking that in means the sky is fixed the moment the bake finishes: a scene cannot dim its sky for dusk, or clear an overcast, without paying for the bake again. The sky enters the path integral as throughput * sky at the point a ray escapes, and throughput — the product of the albedos the path already passed through — does not depend on the sky at all. So the baked field splits exactly into a term that has nothing to do with the sky and a term that is linear in it, per channel: field(sky) = lights + sky * aperture lights is a bake under no sky. aperture is what a unit sky adds: how much of it each probe can see, tinted by whatever the light passed through on the way, so sky arriving through a red wall stays red. Both come from real bakes at the same settings, and the bake seeds its paths from the probe and sample index alone — so the two runs trace the same paths and the subtraction that isolates the aperture carries no sampling residual.

Usage: local GI.SkyResponse = require("@builtin::systems.globalIllumination.skyResponse")

modules/GI.SkyResponse/apply

apply(volumeEntityId: string, sky: { number }) -> boolean

Publish the volume's field for a sky colour. No bake.

Parameters

  • volumeEntityId string
  • sky { number }

Returns boolean

modules/GI.SkyResponse/calibrate

calibrate(volumeEntityId: string) -> table

Bake the volume twice — once with no sky, once with a unit sky — and keep the two terms the split needs.

Parameters

  • volumeEntityId string

Returns table

modules/GI.SkyResponse/calibrated

calibrated(volumeEntityId: string) -> boolean

Parameters

  • volumeEntityId string

Returns boolean

modules/GI.SkyResponse/clear

clear(volumeEntityId: string)

Parameters

  • volumeEntityId string

modules/GI.SkyResponse/combine

combine(lights: { number }, aperture: { number }, sky: { number }) -> { number }

The field for a sky colour, from the two calibrated terms. Pure.

Parameters

  • lights { number }
  • aperture { number }
  • sky { number }

Returns { number }

modules/GI.SkyResponse/terms

terms(volumeEntityId: string) -> table?

Parameters

  • volumeEntityId string

Returns table?

modules/GaussianSplatAssetTypeBehavior/README

require("@builtin::assetTypes.gaussianSplat.behavior") -- GaussianSplatAssetTypeBehavior

Behaviour for the gaussianSplat asset type — how a capture becomes something standing in a scene. A cloud draws through a GaussianSplat component pointed at the container, so :instantiate() — the uniform contract every consumer reaches — spawns exactly that, in the axis convention the container recorded.

Usage: local GaussianSplatAssetTypeBehavior = require("@builtin::assetTypes.gaussianSplat.behavior")

modules/GaussianSplatAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this capture — spawn an entity drawing the cloud, in the axis convention the container recorded. The same name a bundle and an avatar answer to, so an asset goes into a scene the same way whatever kind it is.

Passing a target entityRef spawns the cloud as that entity's CHILD, so an owner that tears down its children takes the cloud with it. With no target the cloud is a fresh root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning EntityRef — the cloud spawns as its child.
  • opts { [string]: any }? (optional) — { position?, rotation?, scale?, name?, temporary? } — the base placement opts; temporary keeps the spawn out of the saved scene.
local root = asset.resolve("room", "gaussianSplat"):instantiate()
local root = captureRef:instantiate(entity.spawn("mount"))

modules/GeneratorInspector/README

require("@builtin::systems.procgen.generator_inspector") -- GeneratorInspector

The Generator component's card on an entity. It replaces the card's generic settings section with one built from the bound graph: the graph it runs, as an asset picker, and which of its outputs it realizes; then groups — INPUTS, one row per input the graph declares in the control its declared type takes, each able to take an expression over the graph's own time in its place; TIMELINE, the transport, a scrubber across the range, the range itself, the frame rate, looping and wall time; COOK, what is cooking, what cooking has cost and the frame budget — and the Flatten action.

Usage: local GeneratorInspector = require("@builtin::systems.procgen.generator_inspector")

modules/GeneratorInspector/buildFieldSpecs

buildFieldSpecs(graph: any, params: { [string]: any }?): { FieldSpec }

Build the editable field descriptors for a graph: one spec per declared graph input, sorted by name, carrying the input's kind, its current value (the param override when set, else the declared default), the declared min/max range, the asset kind a reference input picks from, and whether the value changes over the graph's time. Pure — no UI, no entity access — so the field mapping is testable headless.

Parameters

  • graph any (optional) — A loaded proc graph (proc.load).
  • params { [string]: any }? (optional) — The component's current param-override table (may be nil).

modules/GuideAssetTypeRef/README

require("@builtin::assetTypes.guide.behavior") -- GuideAssetTypeRef

Per-instance methods exposed on every AssetRef<guide>. Loaded lazily by assetType.shared.ref.

Usage: local GuideAssetTypeRef = require("@builtin::assetTypes.guide.behavior")

modules/GuideAssetTypeRef/getGuide

getGuide(self): string?

Read the guide's guide.md body.

Parameters

  • self any (optional)
local md = guideRef:getGuide()

modules/GuideAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { headings, wordCount }, parsed from this guide's own guide.md. A guide with no readable guide.md returns an empty headings list and zero wordCount rather than erroring.

Parameters

  • self any (optional)
local headings = asset.inspect(guideRef).detail.headings

modules/ImporterAssetTypeRef/README

require("@builtin::assetTypes.importer.behavior") -- ImporterAssetTypeRef

Per-instance methods exposed on every AssetRef<importer>. Loaded lazily by assetType.shared.ref.

Usage: local ImporterAssetTypeRef = require("@builtin::assetTypes.importer.behavior")

modules/ImporterAssetTypeRef/getInitScript

getInitScript(self): string?

Read the importer's entry script as raw text.

Parameters

  • self any (optional)
local src = importerRef:getInitScript()

modules/ImporterAssetTypeRef/getReadme

getReadme(self): string?

Read the importer's README.

Parameters

  • self any (optional)
print(importerRef:getReadme())

modules/ImporterAssetTypeRef/run

run(self, payload: any): any

Invoke the importer with a payload (e.g. a path to the file to import). Loads the importer module on demand and calls its primary exported function. Raises a Luau error tagged with the importer's identity on load failure / missing entry.

Parameters

  • self any (optional)
  • payload any (optional) — Argument forwarded verbatim to the importer entry.
importerRef:run("/zero/source/foo.glb")

modules/ImporterShared/README

require("@builtin::assetTypes.importer.shared") -- ImporterShared

The importer SYSTEM — shared by every .importer/ instance. Owns dispatch, origin-gating, content-gating, the asset.containing bundle skip, re-entrancy, output marking, and bundle relocation. The engine emits one generic "a source path was written" signal per write (onLooseWrite); this module decides whether any registered importer should claim it and runs the winner. Individual importers implement ONLY canImport(path, bytes) + import(ctx) — never trigger logic.

Usage: local ImporterShared = require("@builtin::assetTypes.importer.shared")

modules/ImporterShared/awaitJob

awaitJob(path: string, timeoutSecs: number?): ImportJob?

Wait until the latest job for path settles: a terminal state (imported / failed / unclaimed / skipped / cancelled), or "owed", which holds until play ends or the live session re-establishes (reason says which) and so is returned at once rather than waited out. The in-flight "queued" and "running" are waited through, until the timeout elapses. Returns the job (nil when no job exists for the path at all).

Parameters

  • path string — The source VFS path.
  • timeoutSecs number? (optional) — Max seconds to wait (default 30).

modules/ImporterShared/cancelAllQueued

cancelAllQueued(): number

Cancel EVERY queued source that hasn't started importing yet, and every owed import, held until play ends or until the live session re-establishes (in-flight imports are left to finish). Use it to abandon a large accidental drop.

modules/ImporterShared/cancelQueued

cancelQueued(path: string): boolean

Cancel a source before it imports: a QUEUED one is dropped from the pump's queue, an OWED one (waiting for play to end or for the live session to re-establish) is dropped from the owed set, and its job settles as "cancelled". A source already importing is mid-parse and can't be unwound cleanly, so it is left to finish: returns false for a path that is already running, terminal, or was never queued or owed.

Parameters

  • path string — The queued or owed source VFS path.

modules/ImporterShared/explain

explain(path: string): { [string]: any }

Dry-run the dispatch gates for a path WITHOUT importing: does the file exist, which importers claim it, and which gate (if any) would stop an import right now. The answer to "I wrote this file and nothing happened".

Parameters

  • path string — The source VFS path.

modules/ImporterShared/extensionsOf

extensionsOf(ref: any): { string }?

The file extensions ref's importer declares it claims (M.extensions in its init.luau), lower-case without the dot; nil when it declares none.

Parameters

  • ref any (optional) — The importer asset.
local exts = ImporterShared.extensionsOf(asset.resolve("@builtin::importers.texture"))

modules/ImporterShared/importedAssets

importedAssets(): { { [string]: any } }

Every asset in the world carrying import provenance, newest at first. Each row: { asset (path), importer (guid), source ({guid,path}), iteration, at }. Backed by the asset.list presence filter — one cross-type query.

modules/ImporterShared/job

job(path: string): ImportJob?

The latest import job recorded for an exact source path, or nil when no dispatch has reached a claimant for it.

Parameters

  • path string — The source VFS path.

modules/ImporterShared/jobs

jobs(filter: { state: string?, path: string?, limit: number? }?): { ImportJob }

Recent import jobs, newest first. Pass a filter to narrow: state keeps one state, path substring-matches the source path, limit caps the count (default 25).

Parameters

  • filter { state: string?, path: string?, limit: number? }? (optional) — Optional { state?: string, path?: string, limit?: number }.

modules/ImporterShared/jobsVersion

jobsVersion(): number

A number that moves each time an import job is recorded or its state changes: fold it into a signature to rebuild what lists the jobs.

parts[#parts + 1] = tostring(ImporterShared.jobsVersion())

modules/ImporterShared/listImporters

listImporters(): { { name: string, identity: string } }

Every registered importer: { name, identity } per .importer asset.

modules/ImporterShared/onAssetWrite

onAssetWrite(assetPath: string, path: string, origin: string?): string?

Engine-driven (through the asset-type change dispatcher): a file inside a typed asset was written. When that asset's import record names the file as the source the asset was imported from, the importers run again on it, regenerating the asset in place. The record is the whole link: it follows the source through a rename (by guid), and nothing else is watched. A write of the same bytes under the same importer settings is skipped, and a write made on a peer is left to that peer.

Parameters

  • assetPath string — The typed asset the written file is inside.
  • path string — The written file.
  • origin string? (optional) — "local" for a write made on this client, "remote" for a peer's.

modules/ImporterShared/onLooseWrite

onLooseWrite(path: string, origin: string?)

Engine-driven: a source path with no enclosing typed-asset folder was written. Gate on origin + re-entrancy, then ENQUEUE it on the bounded import queue (default concurrency 1) — the pump reads the bytes, decides whether a registered importer claims it, applies the content + containment gates, and runs the winner, one import at a time.

Parameters

  • path string — The written VFS path.
  • origin string? (optional) — "local" for a write made on this client, "remote" for a peer-synced write. Only "local" writes trigger — peers receive the originator's derived bundle as ordinary synced content.

modules/ImporterShared/queue

queue(): { ImportJob }

The live import backlog: queued sources (still waiting behind the concurrency cap, in FIFO order), then the ones importing right now, then the owed ones, held until play ends or the live session re-establishes (sorted by path). The focused "what is the importer doing this instant" view, distinct from jobs (the full recent-dispatch log including terminal states).

modules/ImporterShared/reimportAsset

reimportAsset(assetPath: string): string?

Reimport a produced asset in place: resolve its source and re-run the importers on it (the containment gate regenerates the container's derived assets from the retained source). Returns the produced path, or nil when no source could be resolved.

Parameters

  • assetPath string — The produced asset's path.

modules/ImporterShared/resolveImportSource

resolveImportSource(assetPath: string): string?

Resolve the source file a produced asset was imported from. Prefers the provenance source guid (survives rename/move), falls back to the recorded path, then to scanning the container for an importer-claimed file.

Parameters

  • assetPath string — The produced asset's path (e.g. a .bundle).

modules/ImporterShared/runImporter

runImporter(importer: any, path: string): string?

Run ONE importer on path now and return the produced asset path, or nil when that importer does not claim the file. runImporters hands a source to the first claimant that takes it; this names the importer, for a source two importers both claim or an importer being tried out. The import runs exactly as a dispatched one does — synchronously, in the calling task, with the importer's own settings as ctx.settings — and is recorded as a job.

Parameters

  • importer any (optional) — The importer: an AssetRef, or its name or identity.
  • path string — The raw source VFS path to import.
local out = Importers.runImporter("texture", "/zero/source/hero.png")

modules/ImporterShared/runImporters

runImporters(

Run the importers on path NOW and return the produced asset path (a .bundle / .texture / .audio / …), or nil if no importer claims it. The manual, deterministic counterpart to the engine's loose-write dispatch: forced (no origin, content, or containment gate) and synchronous — it runs in the calling task and returns only when the import is complete, so a caller can write a raw source quietly (vfs.write(path, bytes, { quiet = true })) and then import it deterministically instead of racing the watcher. A source that already lives inside its own output container re-imports in place, regenerating the container's derived assets from the retained source. While play runs and holds a source that predates it where it stands (vfs.playHoldsInPlace), a fresh import of it is owed: this returns nil, the job (M.job(path)) reads "owed" with reason "play-lock", and the import runs when play ends or pauses. While the live session is lost (vfs.sessionLost), and when a drop refuses a write the import makes, the import is owed with reason "session-lost" the same way, and runs when the session re-establishes.

modules/ImporterShared/runTargets

runTargets(target: any, opts: { recursive: boolean?, mode: string? }?): { [string]: any }

Import/reimport one target, an array of targets, or a folder. A produced asset (with provenance) reimports in place; a loose source imports; a folder is scanned (recursive by default) and its imported assets reimported + loose sources imported, filtered by opts.mode ("all" | "new" | "existing").

Parameters

  • target any (optional) — A VFS path string or an array of path strings.
  • opts { recursive: boolean?, mode: string? }? (optional) — { recursive?: boolean (default true), mode?: "all"|"new"|"existing" }.

modules/ImporterShared/stats

stats(): { queued: number, running: number, owed: number, states: { [string]: number } }

Live queue counters: queued (sources waiting on the pump — the authoritative backlog depth), running (imports in flight), owed (imports held back until play ends, because the running play session holds their source where it stands, or until the lost live session re-establishes), and states (every recorded job tallied by state). queued, running and owed are exact; states tallies the job registry, which keeps every in-flight job plus the most recent settled records.

modules/ImporterShared/writeOutcome

writeOutcome(path: string, sinceFrame: number?, timeoutSecs: number?): WriteOutcome?

Where the bytes a write put at path end up. A write outside every typed asset folder is offered to the importers, and one that claims the bytes may move them into the asset it makes. This waits, up to timeoutSecs, for the dispatch that write set off to settle, and answers with its outcome: nil when no importer claims the bytes (they stay at path), else the import's state, the importer, the asset it made (output), the file the bytes are in now (source, nil when the import kept no copy) and a note stating all of it in a sentence. An import still running when the wait runs out answers queued or running; an import held back answers owed at once, the bytes staying at path until it runs: reason "play-lock" when the running play session holds them, until play ends or pauses, and "session-lost" while the live session is lost, until it re-establishes; bytes an importer claims that no dispatch reached (a quiet write fires none) answer not-started, the bytes staying at path.

Parameters

  • path string — The written VFS path, as the engine spells it (/zero/source/...).
  • sinceFrame number? (optional) — The engine frame (time.frameCount()) read before the write: a settled job recorded for path before it answers for an earlier write.
  • timeoutSecs number? (optional) — Max seconds to wait for the import to settle (default 10).
local outcome = ImporterShared.writeOutcome("/zero/source/art/hero.png", time.frameCount() - 1)

modules/ImporterShared/writeOutcomes

writeOutcomes(paths: { string }, sinceFrame: number?, timeoutSecs: number?): { [string]: WriteOutcome }

writeOutcome for several paths written together, such as the files of one folder transfer, asked once every one of them has landed. One wait bounds them all: each path is answered as writeOutcome answers it, with whatever share of timeoutSecs is left when its turn comes, so a path whose import is still running when the wait runs out answers queued or running.

Parameters

  • paths { string } — The written VFS paths, as the engine spells them (/zero/source/...).
  • sinceFrame number? (optional) — The engine frame (time.frameCount()) read before the first of the writes.
  • timeoutSecs number? (optional) — Max seconds to wait for all of the imports to settle (default 10).
local outcomes = ImporterShared.writeOutcomes({ "/zero/source/art/a.png", "/zero/source/art/b.png" }, since)

modules/InputBindingAssetTypeRef/README

require("@builtin::assetTypes.inputBinding.behavior") -- InputBindingAssetTypeRef

Usage: local InputBindingAssetTypeRef = require("@builtin::assetTypes.inputBinding.behavior")

modules/InputBindingAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("inputBinding", name, opts). Writes a record carrying every device class, so a binding is complete the moment it exists.

Parameters

  • name string
  • opts CreateOpts
asset.create("inputBinding", "boost", { label = "Boost", kind = "button", kbm = '{ B.key("ShiftLeft") }', gamepad = '{ B.padButton("left_shoulder") }', touch = '{ B.touchButton({ zone = "right-lower" }) }' })

modules/InputBindingAssetTypeRef/problemsWith

problemsWith(record: any, name: string): { string }

Check a binding record for the things that make it unusable, and return the problems rather than raising, so a tool can report every fault in a map at once instead of stopping at the first.

Parameters

  • record any (optional) — The binding record to check.
  • name string — The binding's name, for the messages.

modules/InputMacroAssetTypeRef/README

require("@builtin::assetTypes.inputMacro.behavior") -- InputMacroAssetTypeRef

Per-instance methods exposed on every AssetRef<inputMacro>. Loaded lazily by assetType.shared.ref.

Usage: local InputMacroAssetTypeRef = require("@builtin::assetTypes.inputMacro.behavior")

modules/InputMacroAssetTypeRef/getEvents

getEvents(self): { any }?

Parse events.json into a Lua table.

Parameters

  • self any (optional)
local events = macroRef:getEvents()

modules/InputMacroAssetTypeRef/getEventsRaw

getEventsRaw(self): string?

Read the macro's events.json body as raw JSON text.

Parameters

  • self any (optional)
local raw = macroRef:getEventsRaw()

modules/InputMacroAssetTypeRef/length

length(self): number

Number of recorded events.

Parameters

  • self any (optional)
print(macroRef:length())

modules/InputMacroAssetTypeRef/replay

replay(self): number

Replay the macro by handing its recorded events to the sim toolbox's macro tool, which dispatches each timed input event through the engine input surface. Returns the number of events queued for replay.

Parameters

  • self any (optional)
local n = macroRef:replay()

modules/InputMapAssetTypeRef/README

require("@builtin::assetTypes.inputMap.behavior") -- InputMapAssetTypeRef

Usage: local InputMapAssetTypeRef = require("@builtin::assetTypes.inputMap.behavior")

modules/InputMapAssetTypeRef/activateShape

activateShape(self): (string, string)

The type activate() answers for one map — a field per control it declares, each a Handle.

Parameters

  • self any (optional) — The map.
local text = M.refShapes.activate(mapRef)

modules/InputMapAssetTypeRef/onChange

onChange(self, change)

Re-activate this map after a write inside the instance, so an edit to its bindings takes hold in the running session. Only the map that is currently active is re-activated; a removal is ignored.

Parameters

  • self any (optional) — The changed .inputMap asset's reference.
  • change any (optional) — The change record the asset dispatcher raised for the write.

modules/InputMapAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("inputMap", name, opts). With opts.record, bakes it into both shapes a map is read in — the "play it, bake it, tweak it" on-ramp Zin.map.bake drives.

init.luau returns the record's flat actions / axes tables, which is what Zin.map.materialize reads. Beside it, one <name>.inputBinding/ child per entry, which is what a controller subscribes to through activate(). Each child carries all three device classes, a class the baked map had nothing for written as false with the reason beside it.

A binding that fails to serialize (an unknown kind, or an axis/vector composite whose nested arm fails — the whole composite drops) is dropped from init.luau with an -- UNSERIALIZED kind: <kind> (<name>) comment line; a function-valued axis gate / curve is dropped with a -- <name>.gate omitted / -- <name>.curve omitted comment line. An entry whose name no asset folder can take, or that lost every device class, stays in init.luau alone and is named in the header comment as a control the bake could not make.

With no opts.record, contributes nothing on top of the type's template/ skeleton.

Parameters

  • name string
  • opts CreateOpts
Zin.map.bake("my_scheme")

modules/InspectorAppLayout/README

require("@builtin::_templates.inspector_app.inspector_app_layout") -- InspectorAppLayout

Property-inspector shell on the raw widget tree — a section header and labelled field rows (text input, slider, checkbox) whose edits update the displayed state.

Usage: local InspectorAppLayout = require("@builtin::_templates.inspector_app.inspector_app_layout")

modules/InspectorProbe/README

require("@builtin::tests.fixtures.inspector_probe") -- InspectorProbe

Helpers the Inspector suites share: walk a published edui tree for its callback ids and label texts, mount an app over a render function, render an asset's Inspector, and build one section of an asset by id.

Usage: local InspectorProbe = require("@builtin::tests.fixtures.inspector_probe")

modules/InspectorProbe/callbackIds

callbackIds(node: any): { string }

Every string-valued callback prop in a published tree, depth first.

Parameters

  • node any (optional) — The tree.
local app, tree = Probe.mount(Test, function() return node end)
local ids = Probe.callbackIds(tree())

modules/InspectorProbe/dropUndoHistoryAtCleanup

dropUndoHistoryAtCleanup(Test: any)

Drop the undo history at the test's cleanup, after every cleanup registered later has run. An edit dispatched through an Inspector app is an undo step on the scratch asset the test removes, so the test ends with nothing to undo or redo. Raises naming the wait when the history is still held once the frame budget runs out.

Parameters

  • Test any (optional) — The Test API.
Probe.dropUndoHistoryAtCleanup(Test)

modules/InspectorProbe/fact

fact(rows: { any }, label: string): string?

The value a fact row shows beside label: the last text of the first row whose first text is exactly label.

Parameters

  • rows { any } — A section's rows.
  • label string — The fact's label.
assert(Probe.fact(rows, "Splats") == "3")

modules/InspectorProbe/hasText

hasText(tree: any, needle: string): boolean

Whether any label text in tree contains needle.

Parameters

  • tree any (optional) — The tree.
  • needle string — Plain text to find.
local app, tree = Probe.mount(Test, function() return node end)
assert(Probe.hasText(tree(), "4 × 4"))

modules/InspectorProbe/idEndingWith

idEndingWith(tree: any, suffix: string): string

The callback id in tree ending with suffix; raises naming the ids the tree holds.

Parameters

  • tree any (optional) — The tree.
  • suffix string — The id's tail.
app:dispatch(Probe.idEndingWith(tree(), ":act:play"), nil)

modules/InspectorProbe/ids

ids(sections: { any }): string

The ids of sections, space-joined.

Parameters

  • sections { any } — Sections.
assert(string.find(Probe.ids(sections), "texture"))

modules/InspectorProbe/mount

mount(Test: any, render: () -> any): (any, () -> any)

Mount an app whose tree is render(). At the test's cleanup the app is unmounted and then the undo history is dropped (dropUndoHistoryAtCleanup).

Parameters

  • Test any (optional) — The Test API.
  • render () -> any — Returns the node to publish.

Returns any) — (app, lastTree).

local app, tree = Probe.mount(Test, function() return node end)

modules/InspectorProbe/pngOf

pngOf(size: number, hex: string): string

A solid PNG of size×size pixels.

Parameters

  • size number — Edge length in pixels.
  • hex string — Fill colour, e.g. "#336699".
local png = Probe.pngOf(4, "#ff00ff")

modules/InspectorProbe/renderAsset

renderAsset(Test: any, ref: any): (any, () -> any)

Mount the full Inspector for ref (generic plus declared sections).

Parameters

  • Test any (optional) — The Test API.
  • ref any (optional) — The asset.

Returns any) — (app, lastTree).

local app, tree = Probe.renderAsset(Test, texRef)

modules/InspectorProbe/rowsHave

rowsHave(rows: { any }, needle: string): boolean

Whether any row in a section's rows contains needle in a label.

Parameters

  • rows { any } — A section's rows.
  • needle string — Plain text to find.
assert(Probe.rowsHave(rows, "vertices"))

modules/InspectorProbe/rowsHaveExactly

rowsHaveExactly(rows: { any }, text: string): boolean

Whether any label text in a section's rows is exactly text.

Parameters

  • rows { any } — A section's rows.
  • text string — The whole text.
assert(Probe.rowsHaveExactly(rows, "3"))

modules/InspectorProbe/section

section(ref: any, id: string): ({ any }, any)

Build the section id of ref's Inspector. Raises naming the ids present when id is absent, and the build problem when it fails.

Parameters

  • ref any (optional) — The asset.
  • id string — The section id.
local rows = Probe.section(texRef, "texture")

modules/InspectorProbe/texts

texts(node: any): { string }

Every label text in a tree, depth first.

Parameters

  • node any (optional) — The tree.
local app, tree = Probe.mount(Test, function() return node end)
local all = Probe.texts(tree())

modules/InspectorProbe/texture

texture(Test: any, stem: string, size: number?, hex: string?, settings: { [string]: any }?): any

A scratch texture of size (default 4) pixels, removed at cleanup.

Parameters

  • Test any (optional) — The Test API.
  • stem string — Name stem.
  • size number? (optional) — Edge length (default 4).
  • hex string? (optional) — Fill (default "#336699").
  • settings { [string]: any }? (optional) — The texture's settings, where a case needs ones other than the defaults.
local tex = Probe.texture(Test, "sectTex", nil, nil, { compression = "rgba8" })

modules/LightmapDataAssetTypeRef/README

require("@builtin::assetTypes.lightmapData.behavior") -- LightmapDataAssetTypeRef

Per-instance methods on every AssetRef<lightmapData> — the baked-lighting container the bake flow writes and component awakes read. Entries are keyed (lightmaps by entity id, probe fields by field id); each entry pairs a manifest record with a raw f32 payload file inside the container.

Usage: local LightmapDataAssetTypeRef = require("@builtin::assetTypes.lightmapData.behavior")

modules/LightmapDataAssetTypeRef/beginBatch

beginBatch(self)

Hold the manifest open across a run of writes. Each setLightmap / setProbeField still writes its payload file as it goes, but the manifest is read once here and written once by commitBatch, instead of being decoded and re-encoded per entry. A bake storing many entries into one container is the case this exists for. Re-entrant calls are ignored, and a batch left open by an error is closed by the next commitBatch.

Parameters

  • self any (optional)
container:beginBatch()
for _, s in surfaces do container:setLightmap(s.id, s.meta, s.texels) end
container:commitBatch()

modules/LightmapDataAssetTypeRef/clearEntries

clearEntries(self)

Remove every entry and payload, leaving an empty manifest — the container-wide teardown baking.clear uses for a full-scene clear.

Parameters

  • self any (optional)
container:clearEntries()

modules/LightmapDataAssetTypeRef/commitBatch

commitBatch(self)

Write the manifest a beginBatch has been holding and close the batch. No-op when no batch is open.

Parameters

  • self any (optional)
container:commitBatch()

modules/LightmapDataAssetTypeRef/lightmap

lightmap(self, key: string): (LightmapEntry?, { number }?)

Read one entity's baked lightmap.

Parameters

  • self any (optional)
  • key string — The receiver entity id.
local entry, texels = container:lightmap(entityId)

modules/LightmapDataAssetTypeRef/manifest

manifest(self)

The decoded manifest: { version, lightmaps = { [entityId] = entry }, probeFields = { [fieldId] = entry } }.

Parameters

  • self any (optional)
local m = container:manifest()

modules/LightmapDataAssetTypeRef/onCreate

onCreate(name: string, opts: { scene: string? }?)

Create an empty baked-lighting container. The bake flow (baking.all / Lightmap.bake / VolumeProbe.bake) fills it.

Parameters

  • name string
  • opts { scene: string? }? (optional)

modules/LightmapDataAssetTypeRef/probeField

probeField(self, key: string): (ProbeFieldEntry?, { number }?)

Read one probe volume's baked field.

Parameters

  • self any (optional)
  • key string — The volume's stable field id.
local entry, sh = container:probeField(fieldId)

modules/LightmapDataAssetTypeRef/removeLightmap

removeLightmap(self, key: string)

Remove one entity's lightmap entry and its payload file. No-op when the entry is absent.

Parameters

  • self any (optional)
  • key string — The receiver entity id.
container:removeLightmap(entityId)

modules/LightmapDataAssetTypeRef/removeProbeField

removeProbeField(self, key: string)

Remove one probe volume's field entry and its payload file. No-op when the entry is absent.

Parameters

  • self any (optional)
  • key string — The volume's stable field id.
container:removeProbeField(fieldId)

modules/LightmapDataAssetTypeRef/setLightmap

setLightmap(self, key: string, meta: { [string]: any }, texels: { number })

Store one entity's baked lightmap: writes the texel payload file and its manifest entry, replacing any prior entry under the same key.

Parameters

  • self any (optional)
  • key string — The receiver entity id.
  • meta { [string]: any } — { resolution, intensity } — the parameters the runtime needs to rebuild the lightmap and place it in the atlas.
  • texels { number } — Flat f32 RGBA texel array (resolution² × 4 floats, dilated, alpha = coverage).
container:setLightmap(entityId, { resolution = 256, intensity = 1 }, data)

modules/LightmapDataAssetTypeRef/setProbeField

setProbeField(self, key: string, meta: { [string]: any }, sh: { number })

Store one probe volume's baked field: writes the SH payload file and its manifest entry, replacing any prior entry under the same key.

Parameters

  • self any (optional)
  • key string — The volume's stable field id.
  • meta { [string]: any } — { boundsMin = {x,y,z}, boundsMax = {x,y,z}, res = {x,y,z}, count }.
  • sh { number } — Flat f32 SH L2 array — 36 floats per probe, X-fastest.
container:setProbeField(fieldId, { boundsMin = mn, boundsMax = mx, res = r, count = n }, sh)

modules/LoadingScreenAssetTypeRef/README

require("@builtin::assetTypes.loadingScreen.behavior") -- LoadingScreenAssetTypeRef

Per-instance methods for every AssetRef<loadingScreen>. Loaded lazily by asset_ref.module.

Usage: local LoadingScreenAssetTypeRef = require("@builtin::assetTypes.loadingScreen.behavior")

modules/LoadingScreenAssetTypeRef/isShown

isShown(self): boolean

Whether the world shows this screen while it loads: its settings name this asset under [world] loading_screen.

Parameters

  • self any (optional)
if not screenRef:isShown() then world.loading_screen = screenRef end

modules/LoadingScreenAssetTypeRef/preview

preview(self, seconds: number?): any

Show this loading screen over the whole window exactly as a boot of the world draws it, for seconds (default 15): its pictures, colours, title, text and tips, with the progress filling and the tips changing on the screen's own clock. Capture it while it is up with capture { source = "window" }. Pass 0 to take a preview down.

Parameters

  • self any (optional)
  • seconds number? (optional) — How long the preview stays up (default 15; 0 takes it down).
local p = screenRef:preview(20) -- then capture { source = "window" }

modules/LoadingScreenAssetTypeRef/spec

spec(self): any

The screen this asset states, every key it leaves out at the engine's own look, and the problems that kept a key out. Pictures are paths relative to /zero/source.

Parameters

  • self any (optional)
local read = screenRef:spec(); print(read.spec.title, #read.problems)

modules/LoadingScreenShared/README

require("@builtin::assetTypes.loadingScreen.shared") -- LoadingScreenShared

The rules of a loadingScreen asset: the keys screen.yaml takes, the look a screen states nothing for, how the world's settings name its loading screen, and where each picture a screen names lives in the world. The engine's boot draws from what read answers here, and the type's Inspector and ref:spec() answer from the same rules.

Usage: local LoadingScreenShared = require("@builtin::assetTypes.loadingScreen.shared")

modules/LoadingScreenShared/folderFor

folderFor(identity: any): string?

The source-relative folder of the loadingScreen asset identity names: a world asset's dotted identity (branding.title is branding/title.loadingScreen) or a built-in one (@builtin::x.y).

Parameters

  • identity any (optional) — The identity [world] loading_screen holds.

modules/LoadingScreenShared/identityIn

identityIn(settingsText: any): (string?, string?)

The loading screen a .world_settings text names under [world] loading_screen.

Parameters

  • settingsText any (optional) — The text of .world_settings.

modules/LoadingScreenShared/parse

parse(text: any, folder: string): (Spec, { string })

Read a screen.yaml text into the screen it states, with every key it leaves out at the default, and the problems that left a key out.

Parameters

  • text any (optional) — The text of screen.yaml.
  • folder string — The screen's folder relative to /zero/source, which a picture named inside it is relative to.

modules/LoadingScreenShared/read

read(read: (string) -> string?): any

The loading screen a world states, read through read: the settings name a screen, and its screen.yaml states it.

Parameters

  • read (string) -> string? — A function taking a path relative to /zero/source and answering the file's text, or nil when the world holds none there.

modules/LoadingScreenShared/tipAt

tipAt(spec: Spec, elapsed: number): string?

The text one tip slot shows elapsed seconds into the load.

Parameters

  • spec Spec — A screen from parse.
  • elapsed number — Seconds since the screen went up.

modules/MaterialAssetTypeRef/README

require("@builtin::assetTypes.material.behavior") -- MaterialAssetTypeRef

Per-instance methods exposed on every AssetRef<material>. Loaded lazily by assetType.shared.ref via require("@builtin::assetTypes.material.behavior") the first time a material ref is touched in a VM.

Usage: local MaterialAssetTypeRef = require("@builtin::assetTypes.material.behavior")

modules/MaterialAssetTypeRef/applyToEntity

applyToEntity(self, entityId: string): boolean

Apply this material to an entity by setting the material field on its Model / SkinnedModel component (where the renderer reads it). A material only renders where there is a mesh.

Parameters

  • self any (optional)
  • entityId string — Target entity ID.
matRef:applyToEntity(playerId)

modules/MaterialAssetTypeRef/getDefinition

getDefinition(self): string?

Read the on-disk mat.yaml body as raw text. Use vfs.write(self.path .. "/mat.yaml", ...) to write the file directly, :setProperty / :setTexture followed by :saveDefinition to write the current values into it, or :setShader to rewrite it for a new shader.

Parameters

  • self any (optional)
local yaml = matRef:getDefinition()

modules/MaterialAssetTypeRef/getShader

getShader(self): string?

Read the shader identity currently bound to this material — parses mat.yaml and returns the shader: field as a string. The result is whatever the YAML names (a built-in like "pbr", a guid, or a full identity like "@builtin::shaders.pbr"); pass it to asset.resolve(..., "shader") to get a ref.

Parameters

  • self any (optional)
local s = matRef:getShader()

modules/MaterialAssetTypeRef/getShaderRef

getShaderRef(self): any?

The shader this material draws with, resolved from its shader: field read at the material's own path, so a ~ names the nearest root enclosing the material.

Parameters

  • self any (optional)
local props = matRef:getShaderRef():getProperties()

modules/MaterialAssetTypeRef/getTextures

getTextures(self): { [string]: { ref: string, name: string } }

The textures bound to this material's slots: slot name -> { ref, name }, the bound texture's guid and the id the material names it by.

Parameters

  • self any (optional)
for slot, t in pairs(matRef:getTextures()) do print(slot, t.name) end

modules/MaterialAssetTypeRef/isRegistered

isRegistered(self): boolean

Whether this material exists as an asset — its mat.yaml is present. Writing the asset is what registers the material, so file presence IS the registration check.

Parameters

  • self any (optional)
if matRef:isRegistered() then ... end

modules/MaterialAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)register this material into the renderer's MaterialRegistry whenever its .material is seeded (a baked builtin at boot, or a world seed) or its mat.yaml is edited. This is the ONLY thing that populates the registry for a material — the legacy boot-time library loader (which registered every material by name) is gone; materials register through their own assetType exactly as shaders do (__shader.compile). Keyed by guid@checksum, the key a MaterialRef.id wears; an edit files under the new version's key and moves every wearer onto it. Convergent + idempotent: see registerToGpu.

Parameters

  • ref any (optional)
  • change any (optional)

modules/MaterialAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("material", name, opts). Pure: returns the mat.yaml content for the caller to persist to the authored destination; the write registers the material definition (CPU-side) — GPU resources are built later, only on actual use. Properties resolve from the shader's properties.yaml overlaid by these overrides; no shader compile happens at create. Flat non-reserved top-level keys on opts are shader-property overrides (e.g. base_color = {1,0,0,1}, roughness = 0.3), admitted by the open schema and resolved against the shader's properties.yaml inside the hook.

Parameters

  • name string — Material identity (the instance name).
  • opts CreateOpts
asset.create("material", "Gold", { shader = "pbr", base_color = {1, 0.84, 0, 1}, metallic = 1, roughness = 0.2 })
asset.create("material", "Glass", { shader = "pbr", base_color = {0.2, 0.9, 0.4, 0.45}, render = { blend = "alphaBlend", depth_write = false } })

modules/MaterialAssetTypeRef/onDelete

onDelete(ref)

Asset-type delete callback: release the renderer record a removed .material folder filed. The teardown counterpart of onChange's registration, so the type owns both halves of its runtime lifecycle the way .component does. renderer.material.destroy drops the registry entry and the bind group the renderer prepared under that key; the module's own caches for the folder go with it. A material authored afresh at the same path therefore files a new record instead of inheriting the one the removed material left, and a surface still wearing the key draws the renderer's fallback until it does.

Parameters

  • ref any (optional)

modules/MaterialAssetTypeRef/previewMaterial

previewMaterial(self): { [string]: any }?

The material a surface preview paints this asset with: the material itself. ref:preview() paints it on the mesh type.yaml names under preview:.

Parameters

  • self any (optional)
local surface = matRef:previewMaterial()

modules/MaterialAssetTypeRef/saveDefinition

saveDefinition(self): boolean

Persist the material's live runtime overrides into its mat.yaml (the lazy/explicit flush). Property/texture sets are frame-fast and transient by default (they live on the ref's runtime, wiped on mode change); call this to bake the current values into the asset so they survive a restart. Routes through the mat.yaml write → RegisterMaterial path. The changed values are written where they already stand in the authored file, so its comments, the order of its keys and blocks, and the spelling of the literals it was written with all survive the save.

Parameters

  • self any (optional)
matRef:saveDefinition()

modules/MaterialAssetTypeRef/setProperties

setProperties(self, patch: { [string]: any }): number

Set many properties at once, resolving each key against the shader's vocabulary the way setProperty does. Entries the material's shader doesn't expose are skipped (a generic patch table won't error on shader differences). Each entry is judged and recorded the way setProperty judges and records it, so a refused entry raises and leaves its property as it was.

Parameters

  • self any (optional)
  • patch { [string]: any } — Table of { [propertyName] = value } pairs.
matRef:setProperties({ roughness = 0.2, metallic = 0.9 })

modules/MaterialAssetTypeRef/setShader

setShader(self, shaderRef: any): (boolean, any)

Switch this material to a different shader by rewriting the material asset itself (mat.yaml), then letting the asset-reload pipeline mark every instance dirty so the renderer repacks. The full mat.yaml is regenerated for the new shader's property vocabulary: current property values and texture links are carried across by canonical ROLE (via modules.material_remap), so MAIN_TEX→albedo→base_color_texture and friends survive the swap to our best ability. Properties the new shader does not expose are dropped; the name, builtin, parent, description, and render: block are preserved verbatim. This modifies the on-disk asset (or, in play mode, its runtime copy) — not a throwaway registry entry — so the change persists and propagates to all instances.

Parameters

  • self any (optional)
  • shaderRef any (optional) — An AssetRef<shader> (preferred) or a shader-identity string ("pbr", "@builtin::shaders.unlit").
matRef:setShader(asset.resolve("@builtin::shaders.unlit", "shader"))
matRef:setShader("pbr")

modules/MaterialAssetTypeRef/writeProperty

writeProperty(self, propertyName: string, value: any): (boolean, string?)

Write one property into the material's mat.yaml — the authored value, where setProperty is a runtime one. The session's own value for the property is dropped, so the material reads what the file now holds. Inside an undo operation the file write is recorded, so undoing the operation puts the previous file back. A texture-typed name routes to writeTexture, the way setProperty routes to setTexture.

Parameters

  • self any (optional)
  • propertyName string — Property name from the material's shader.
  • value any (optional) — New value (number, bool, or a numeric array for a color/vector).
local ok, why = matRef:writeProperty("roughness", 0.2)

modules/MaterialRemap/README

require("@builtin::modules.material_remap") -- MaterialRemap

Canonical property/texture-role dictionary for cross-shader value preservation. When a material swaps shaders the new shader exposes a DIFFERENT vocabulary (one shader's MAIN_TEX is another's albedo is a third's base_color_texture). This module maps any known alias onto a canonical role so values carry across the swap "to our best ability".

Usage: local MaterialRemap = require("@builtin::modules.material_remap")

modules/MaterialRemap/canonicalizeTextures

canonicalizeTextures(

Canonicalize texture-slot names so links survive a shader swap. Each known slot is rewritten to its canonical on-disk slot name; unknown slots pass through unchanged. (The engine does not yet expose a target shader's reflected texture-slot list, so canonicalizing to the builtin convention is the best-effort path — see material_remap module header.)

modules/MaterialRemap/propertyRole

propertyRole(name: string): string?

Canonical role for a scalar/color property name, or nil when unknown.

Parameters

  • name string — Property name as it appears in a shader / mat.yaml.
material_remap.propertyRole("MAIN_COLOR") -- "base_color"

modules/MaterialRemap/remapProperties

remapProperties(

Remap a table of old property values onto a target shader's accepted property names. Direct name matches win; otherwise the old key's canonical role is matched against the role of each target name. Values whose role the target shader does not expose are dropped (best-effort preservation).

modules/MaterialRemap/roleShapePair

roleShapePair(role: string): ({ colour: string, scalar: string })?

The colour+scalar role pair a role belongs to, as { colour, scalar }, or nil when the role stands alone. Both halves answer with the same pair, so a caller holding either one can ask which value shape belongs where.

Parameters

  • role string — A canonical role from propertyRole.
material_remap.roleShapePair("emissive") -- { colour = "emissive", scalar = "emissive_intensity" }

modules/MaterialRemap/textureRole

textureRole(slot: string): string?

Canonical texture-slot role (and on-disk slot name) for a texture slot name, or nil when unknown.

Parameters

  • slot string — Texture slot name as it appears in a shader / mat.yaml.
material_remap.textureRole("MAIN_TEX") -- "base_color_texture"

modules/MaterialSchema/README

require("@builtin::modules.material_schema") -- MaterialSchema

A shader's declared property vocabulary, and the routing of an authored key onto it. Every path that accepts material properties — the .material assetType and renderer.material.create — resolves the backing shader's properties.yaml through here, so the two agree on which keys are real, which are texture slots, and what to say about a key that is neither.

Usage: local MaterialSchema = require("@builtin::modules.material_schema")

modules/MaterialSchema/acceptsSlot

acceptsSlot(schema: Schema, slot: string): boolean

Whether a texture bound under slot reaches the bind group a schema describes: true for a slot the shader declares, and true for every slot while the schema holds no names at all (a shader whose vocabulary has not been read yet, which every write stays permissive about).

Parameters

  • schema Schema — A Schema from forShader.
  • slot string — The texture slot name.
if not MaterialSchema.acceptsSlot(schema, "base_color") then print("not a slot") end

modules/MaterialSchema/declaredNames

declaredNames(schema: Schema): { string }

Every name a schema declares, sorted — properties and texture slots.

Parameters

  • schema Schema — A Schema from forShader.
local names = MaterialSchema.declaredNames(schema)

modules/MaterialSchema/declaredNamesList

declaredNamesList(schema: Schema): string

Comma-joined declared names, for the "not declared" diagnostic so the author sees exactly which keys the shader accepts.

Parameters

  • schema Schema — A Schema from forShader.
warn("declared: " .. MaterialSchema.declaredNamesList(schema))

modules/MaterialSchema/declaredSlotsList

declaredSlotsList(schema: Schema): string

Comma-joined texture slot names a schema declares, sorted, for the diagnostic on a binding to a slot the shader does not declare.

Parameters

  • schema Schema — A Schema from forShader.
warn("texture slots: " .. MaterialSchema.declaredSlotsList(schema))

modules/MaterialSchema/displacedMessage

displacedMessage(name: string, key: string, declared: string): string

The diagnostic for an authored key whose value another spelling of the same declared property took precedence over.

Parameters

  • name string — The material's name or registry key.
  • key string — The authored key whose value did not apply.
  • declared string — The declared property that took its value from another spelling.
warn(MaterialSchema.displacedMessage(name, "emissive_color", "emissive"))

modules/MaterialSchema/forShader

forShader(shader: string): Schema?

The declared vocabulary of the shader backing a material, split into uniform properties and texture slots. Read from the shader assetType's getProperties() (its properties.yaml).

Parameters

  • shader string — Shader identity, path, or short name ("pbr").
local s = MaterialSchema.forShader("pbr")

modules/MaterialSchema/isTextureSlotType

isTextureSlotType(kind: any): boolean

Whether a declared property type is a texture slot: texture (a 2D texture) and texture_array (a 2D texture array, one binding holding every layer) both bind through the material's textures.

Parameters

  • kind any (optional) — The type a properties.yaml entry declares.
MaterialSchema.isTextureSlotType("texture_array") -- true

modules/MaterialSchema/isTextureValue

isTextureValue(value: any): boolean

Whether a value is a texture binding rather than a scalar/vector. A renderer.texture handle (kind == "TextureHandle") or any AssetRef<texture> envelope. Scalar/vector values are bare numbers or numeric arrays, so this never misfires on a colour like {1, 0, 0, 1}.

Parameters

  • value any (optional) — The authored value.
MaterialSchema.isTextureValue(tex) -- true

modules/MaterialSchema/roleIndex

roleIndex(props: { [string]: boolean }): { [string]: string }

The role index for a set of declared property names: canonical role -> the single declared name claiming it. A role two declared names both claim is left out, so an authored spelling never resolves to an arbitrary one of them. This is what turns emissive_color into the emissive a shader declares, and ao_strength into its occlusion_strength.

Parameters

  • props { [string]: boolean } — Declared property names, as a { [name]: true } set.
local byRole = MaterialSchema.roleIndex({ emissive = true })

modules/MaterialSchema/route

route(schema: Schema, key: string, value: any): (Route, string)

Resolve an authored key against a schema: which surface it belongs to and under what name. Routes by the schema's texture slots and by the value's own shape (a texture value is a texture binding whatever the key is called), then by canonical role — so a spelling from another engine's convention reaches the property this shader declares for that role, under the name the shader declares it by.

Parameters

  • schema Schema — A Schema from forShader.
  • key string — The authored key.
  • value any (optional) — The authored value.
local route, name = MaterialSchema.route(schema, "color", {1,0,0,1})

modules/MaterialSchema/routeAll

routeAll(schema: Schema, authored: { [string]: any }): (

Route a whole authored property table at once, so two spellings that resolve to one declared property are settled the same way everywhere.

Parameters

  • schema Schema — A Schema from forShader.
  • authored { [string]: any } — { [key] = value } as written by the author.
local props, tex, unknown = MaterialSchema.routeAll(schema, parsed)

modules/MaterialSchema/settle

settle(schema: Schema, claimed: { Claim }): (

Settle the authored values that resolved onto declared properties, so every path that accepts material properties answers the same way. One declared name takes one value — the spelling the shader declares verbatim, else the first in key order, so the outcome is the authored table's rather than the order pairs walked it in. Two spellings meeting on one half of a colour+scalar pair the shader declares both halves of are not competing: a number is the scalar and a vector is the colour, the reading the uniform buffer already makes of them, so a material naming its glow colour under one convention and its brightness under another keeps both values.

Parameters

  • schema Schema — A Schema from forShader.
  • claimed { Claim } — Every Claim that routed to a property.
local props, displaced = MaterialSchema.settle(schema, claims)

modules/MaterialSchema/undeclaredMessage

undeclaredMessage(name: string, key: string, shader: string, schema: Schema): string

The diagnostic for a key the shader declares under neither surface, naming the material, the key, the shader, and the accepted vocabulary. One wording for every path that accepts material properties.

Parameters

  • name string — The material's name or registry key.
  • key string — The key that is not declared.
  • shader string — The backing shader's name.
  • schema Schema — A Schema from forShader.
warn(MaterialSchema.undeclaredMessage(name, key, shader, schema))

modules/MaterialSchema/undeclaredSlotMessage

undeclaredSlotMessage(name: string, slot: string, shader: string, schema: Schema): string

The diagnostic for a texture bound to a slot the shader does not declare, naming the material, the slot, the shader and the texture slots it declares; a slot name the shader declares as a property says so. One wording for every path that binds a material texture.

Parameters

  • name string — The material's name or registry key.
  • slot string — The slot that is not declared.
  • shader string — The backing shader's name.
  • schema Schema — A Schema from forShader.
warn(MaterialSchema.undeclaredSlotMessage(name, slot, shader, schema))

modules/MaterialSchema/valueShape

valueShape(value: any): "scalar" | "vector" | "either"

What shape an authored value has, for the one question a colour+scalar pair asks of it. A number is the scalar; a numeric array or an {r, g, b} colour is the vector. Anything else answers "either" and stays with the half its key claimed.

Parameters

  • value any (optional) — The authored value.
MaterialSchema.valueShape(5) -- "scalar"

modules/MeshAssetTypeBehavior/README

require("@builtin::assetTypes.mesh.behavior") -- MeshAssetTypeBehavior

Behaviour for the mesh asset type — the disk + CPU side of the disk-asset ↔ GPU-mesh split. onCreate writes the engine-native ZMSH geometry payload as the container's data.zmsh primary; :load() decodes it into the CPU store and returns a CPU handle. Both go through the public renderer.mesh.* API — this behaviour calls no __ FFI directly.

Usage: local MeshAssetTypeBehavior = require("@builtin::assetTypes.mesh.behavior")

modules/MeshAssetTypeBehavior/getVertices

getVertices(self)

Read this mesh's vertices — one entry per vertex, { pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }. Requires the CPU copy to be resident: turn on keepCpu (meshRef:setSettings({ keepCpu = true })) for an in-use mesh. Errors loudly — naming the exact reason — rather than silently loading a transient copy.

Parameters

  • self any (optional)
local verts = meshRef:getVertices()

modules/MeshAssetTypeBehavior/handle

handle(self)

Materialize this mesh asset's live GPU resource (Disk→CPU→GPU) and return its MeshHandle. The handle is cached on the interned ref's shared runtime table — its presence IS "loaded to the GPU", so once materialised every later call (and every consumer of the same asset) gets the SAME handle back directly → ONE GPU entry, no re-work. (The mode-flip runtime wipe clears the cache so a mode change re-materialises.) A component that renders a mesh holds the mesh resource and calls this internally — you rarely call it by hand.

CPU lifecycle: the keepCpu setting (:settings/:setSettings) governs whether the CPU copy survives the upload. DEFAULT (keepCpu = false): the CPU copy is dropped right after the GPU upload (the GPU handle holds no data → no double memory). keepCpu = true retains the CPU store for geometry reads/edits.

Clusters: the mesh draws through clusters from this call, on its lod0 set until its bake lands. The clusterLod setting governs how deep the bake builds. DEFAULT (clusterLod = true): the LOD hierarchy. clusterLod = false: one level of clusters, drawn at full detail.

Parameters

  • self any (optional)
local h = meshRef:handle()

modules/MeshAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this mesh — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to. A mesh becomes an entity carrying a Model that renders it. With target the entity spawns as a child of that owner (so an owning Asset component tears it down with its other children); with no target it is a fresh root. The base opts — position, rotation, scale, name, temporary — place the root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional) — { position?, rotation?, scale?, name?, temporary? }.
meshRef:instantiate(owner)

modules/MeshAssetTypeBehavior/load

load(self)

Load this .mesh asset's geometry into the guid-keyed CPU store and return a CPU handle (the Disk→CPU step). The handle carries the guid, vertex / index counts, and per-handle geometry ops (getTriangles / getVertices / getBounds / encode / unload) that read the Rust-side store — it holds no geometry itself. Upload to the GPU with renderer.mesh.create(handle); the DEFAULT is to handle:unload() right after. Delegates to renderer.mesh.loadCpu (the sole __ caller).

Parameters

  • self any (optional)
local cpu = meshRef:load(); local gpu = renderer.mesh.create(cpu); cpu:unload()

modules/MeshAssetTypeBehavior/morphTargets

morphTargets(self): { string }

The names of the shapes this mesh blends towards, in the order an entity's ecs.MorphWeights addresses them — weight i drives the target named at i. An imported model keeps the names its source file gave its blend shapes, so a face is driven by the shape it means rather than by the ordinal that shape imported at. A target the source never named reads as an empty string, and a mesh with no morph targets returns an empty array. Requires the mesh to be materialised (meshRef:handle(), or anything rendering it) — it errors loudly naming that, rather than answering as though the mesh carried no shapes.

Parameters

  • self any (optional)
for i, name in meshRef:morphTargets() do print(i, name) end

modules/MeshAssetTypeBehavior/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside this mesh asset. A write to the stored geometry re-uploads it into the GPU mesh registered under this asset's guid, so everything already rendering it draws the new shape. A settings write — to the declared settings.yaml values file, or a legacy .metadata block for content published before it existed — acts on whichever fields it named: keepCpu and clusterLod are lifecycle settings governing PER-ENGINE state (a CPU copy in this process's store, a cluster set on this process's GPU), so they act on an already-materialised mesh right away for EVERY origin — a synced peer's engine holds its own CPU/GPU state that the write says just as much about as the one that made it; lightmapUvs reconciles the second (lightmap) UV set — "generate" (re)creates a non-overlapping unwrap into the second UV set, "none" strips it, "keep" leaves the stored geometry untouched. The re-cook round-trips the geometry through the codec, so tangents, skinning, and the skeleton are preserved; it is the one part of this hook that WRITES persisted content, so it alone stays local-origin-only (a peer's own re-cook of the same settings would just be a second, redundant write of the same bytes the sync already delivered) and play-mode-gated (the play-lock): a setting flip never rewrites the persisted geometry while the world is running; a resident mesh re-materialises on its next fetch.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any } — { path, asset, kind, origin, settings, settingsChanged } — path the written file, asset the container folder, kind "edited"/"seeded", origin "local"/"remote", settings the validated values and settingsChanged what changed when path is the settings values file.

modules/MeshAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("mesh", name, opts). Pure: returns the content-file map; asset.create writes it to the authored destination, registering the .mesh asset under its minted guid. Disk-only — nothing is uploaded to the GPU here (the GPU mesh is a separate, explicit renderer.mesh.create step keyed by this asset's guid).

opts is raw geometry { positions, indices, normals?, uvs?, colors? } (flat float / u32 arrays, encoded to data.zmsh via renderer.mesh.encode), or a pre-encoded { bytes } payload (stored verbatim).

Parameters

  • name string — Mesh identity (the instance name).
  • opts CreateOpts
asset.create("mesh", "tree", { positions = {...}, indices = {...} })

modules/MeshAssetTypeBehavior/setSettings

setSettings(self, patch: { [string]: any })

Write a partial settings patch. The patch is checked against this type's declared schema; a violation refuses the write naming every problem. A valid write reaches onChange: keepCpu loads or drops the CPU copy, clusterLod rebakes a mesh in use at the cluster depth it now asks for — neither touches the stored geometry — and lightmapUvs re-cooks the stored geometry's second UV set (edit mode only).

Parameters

  • self any (optional)
  • patch { [string]: any } — Any subset of the schema's fields.
meshRef:setSettings({ keepCpu = true })

modules/MeshAssetTypeBehavior/setVertices

setVertices(self, positions)

Replace this mesh's vertex positions IN PLACE — indices, normals/uvs, and skinning are preserved, the AABB recomputes, and the edit shows on screen (the GPU re-fetches the changed CPU copy). Requires the CPU copy resident: turn on keepCpu (meshRef:setSettings({ keepCpu = true })) for an in-use mesh. Errors loudly — naming the reason — when the CPU copy isn't resident or the vertex count doesn't match.

Parameters

  • self any (optional)
  • positions any (optional) — One position per vertex: an array of {x,y,z} (or [x,y,z]), or a flat {x,y,z, ...} array. The count must match the mesh's vertex count.
local v = meshRef:getVertices()
local p = {}; for i, vert in ipairs(v) do p[i] = { vert.pos.x*0.01, vert.pos.y*0.01, vert.pos.z*0.01 } end
meshRef:setVertices(p)   -- scale the mesh to 1/100

modules/MeshAssetTypeBehavior/settings

settings(self): { [string]: any }

Read this mesh asset's settings, with every declared default filled in.

Parameters

  • self any (optional)
if meshRef:settings().keepCpu then ... end

modules/MeshAssetTypeBehavior/settingsPending

settingsPending(self): boolean

Whether a settings change to this mesh is still reading its stored geometry. A clusterLod or keepCpu change on a mesh in use reads the geometry before it rebakes the mesh or loads its CPU copy, and where the geometry is fetched on first read that read spans frames. While it is in flight this answers true; once it answers false every settings change made so far has reached the mesh, a rebake it asked for is in renderer.mesh.clusterBakes().inFlight or has landed, and the CPU copy is as the latest keepCpu says.

Parameters

  • self any (optional)
meshRef:setSettings({ clusterLod = false })
while meshRef:settingsPending() do task.wait() end

modules/ModuleAssetTypeRef/README

require("@builtin::assetTypes.module.behavior") -- ModuleAssetTypeRef

Per-instance methods exposed on every AssetRef<module>. Loaded lazily by assetType.shared.ref.

Usage: local ModuleAssetTypeRef = require("@builtin::assetTypes.module.behavior")

modules/ModuleAssetTypeRef/getExports

getExports(self): { { name: string, type: string } }?

The module's exported names and their value types — requires the module and reflects over the table it returns. Available for every module (world-authored or library), since a module is just an asset.

Parameters

  • self any (optional)
for _, e in ipairs(modRef:getExports() or {}) do print(e.name, e.type) end

modules/ModuleAssetTypeRef/getInitScript

getInitScript(self): string?

Read the module's entry script as raw text.

Parameters

  • self any (optional)
local src = modRef:getInitScript()

modules/ModuleAssetTypeRef/getReadme

getReadme(self): string?

Read the module's README body.

Parameters

  • self any (optional)
print(modRef:getReadme())

modules/ModuleAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { exports }, where exports is parsed from the module's own entry script (getInitScript) via luau_introspect.moduleExports — name/kind/signature/desc of every top-level export, read from the source text rather than a require() reflection. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A module with no readable entry script returns an empty exports list rather than erroring.

Parameters

  • self any (optional)
local exports = asset.inspect(modRef).detail.exports

modules/ModuleAssetTypeRef/loadModule

loadModule(self): any

Require the module: the same module require(self.identity) answers from the world's own code, for a world module and a library one alike. A load failure raises a Luau error tagged with the module identity rather than propagating the raw error.

Parameters

  • self any (optional)
local mod = modRef:loadModule()

modules/ModuleAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: hot-reload this module whenever a .luau / .lua file inside it is edited (or the module is seeded). A user module reloads here; a library module has already reloaded through the VFS write hook by the time this runs, and the reload here finds its source unchanged. Mirrors the .material / .shader assetTypes owning their own reload. Convergent: the reload only invalidates the require() cache + fires watchers and never writes back into the asset folder.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ModuleAssetTypeRef/onRegister

onRegister(self)

Initial-registration callback: register every .luau / .lua file this module owns — its entry AND its plain-file submodules — into the require() layer, so require("<mod>.<sub>") resolves the instant it first registers (fired before the world entrypoint runs). A .luau inside a NESTED typed-asset folder (a nested .module / .component / …) belongs to that asset and registers through its own onRegister, so it is skipped here.

Parameters

  • self any (optional) — The per-instance AssetRef<module>.
-- driven by the assetType lifecycle; not called directly

modules/NodeboxAssetTypeRef/README

require("@builtin::systems.procgen.nodebox.behavior") -- NodeboxAssetTypeRef

Per-instance methods exposed on every AssetRef<nodebox>.

Usage: local NodeboxAssetTypeRef = require("@builtin::systems.procgen.nodebox.behavior")

modules/NodeboxAssetTypeRef/getReadme

getReadme(self): string?

Read the nodebox's README.md body.

Parameters

  • self any (optional)
print(boxRef:getReadme())

modules/NodeboxAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { nodes }, each { name, call, desc } where call is the name a graph calls the node by (<box>.<node>).

Parameters

  • self any (optional)
local nodes = asset.inspect(boxRef).detail.nodes

modules/NodeboxAssetTypeRef/listNodes

listNodes(self): { any }

List the node refs the box holds, each an AssetRef<procNode>.

Parameters

  • self any (optional)
for _, n in ipairs(boxRef:listNodes()) do print(n.identity) end

modules/OpenVDB/README

require("@builtin::plugins.openvdb.surface") -- OpenVDB

OpenVDB, as a dense voxel grid: the .vdb importer's parser.

Usage: local OpenVDB = require("@builtin::plugins.openvdb.surface")

modules/OpenVDB/parse

parse(vdb: string | buffer)

Parse a .vdb file's bytes into a dense RGBA8 voxel grid.

Parameters

  • vdb string | buffer — The file's bytes, as a string or a buffer.

modules/PackageAssetTypeRef/README

require("@builtin::assetTypes.package.behavior") -- PackageAssetTypeRef

Per-instance methods exposed on every AssetRef<package>. Loaded lazily by assetType.shared.ref.

Usage: local PackageAssetTypeRef = require("@builtin::assetTypes.package.behavior")

modules/PackageAssetTypeRef/getDefinition

getDefinition(self): string?

Read the package's package.yaml body as raw text.

Parameters

  • self any (optional)
local raw = packageRef:getDefinition()

modules/PackageAssetTypeRef/getReadme

getReadme(self): string?

Read the package's README body.

Parameters

  • self any (optional)
print(packageRef:getReadme())

modules/PackageAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { contents }, where contents is { {name, type}, ... } sorted by name — the package's own direct children (components, modules, tools, docs), enumerated via vfs.list against the package's own path. Never reads/executes any child's content.

Parameters

  • self any (optional)
local contents = asset.inspect(packageRef).detail.contents

modules/PackageAssetTypeRef/listContents

listContents(self): { { name: string, isDirectory: boolean } }

List the package's child entries — every VFS entry one level below the package root. Use this to enumerate components, modules, tools, etc. shipped by the package.

Parameters

  • self any (optional)
for _, e in ipairs(packageRef:listContents()) do print(e.name) end

modules/Particles.Curves/ColorSequence.new

ColorSequence.new(a, b?) -> ColorSequence

Three shapes: constant ({r,g,b}), two-point lerp (startColor, endColor), or keypoint table ({ {time=, value={r,g,b,a?}, envelope={r,g,b}?}, ... }).

Parameters

  • a any (optional)
  • b any (optional)

Returns ColorSequence

modules/Particles.Curves/ColorSequence:evaluate

ColorSequence:evaluate(t: number) -> (r, g, b, a)

Deterministic per-channel sample.

Parameters

  • t number

Returns (r, g, b, a)

modules/Particles.Curves/ColorSequence:getKeypoints

ColorSequence:getKeypoints() -> {table}

Returns a fresh array of { time, value = {r,g,b,a}, envelope = {r,g,b} }.

Returns {table}

modules/Particles.Curves/ColorSequence:pack

ColorSequence:pack(buffer: {number}, offset: number) -> number

Pack the color curve into a flat float array; returns the next-write offset.

Parameters

  • buffer {number}
  • offset number

Returns number

modules/Particles.Curves/ColorSequence:sample

ColorSequence:sample(t: number, rng?: number) -> (r, g, b, a)

Sample with per-channel RGB envelope randomness (alpha is deterministic).

Parameters

  • t number
  • rng number (optional)

Returns (r, g, b, a)

modules/Particles.Curves/NumberSequence.new

NumberSequence.new(a, b?) -> NumberSequence

Three shapes: constant (value), two-point lerp (start, end), or keypoint list ({ {time=, value=, envelope=}, ... }), whose entries also read positionally as ({ {time, value[, envelope]}, ... }). A field that is not a number is refused at the call.

Parameters

  • a any (optional)
  • b any (optional)

Returns NumberSequence

modules/Particles.Curves/NumberSequence:evaluate

NumberSequence:evaluate(t: number) -> number

Deterministic interpolation at life-fraction t in [0, 1].

Parameters

  • t number

Returns number

modules/Particles.Curves/NumberSequence:getKeypoints

NumberSequence:getKeypoints() -> {table}

Returns a fresh array of { time, value, envelope } entries.

Returns {table}

modules/Particles.Curves/NumberSequence:pack

NumberSequence:pack(buffer: {number}, offset: number) -> number

Pack the curve into a flat float array; returns the next-write offset.

Parameters

  • buffer {number}
  • offset number

Returns number

modules/Particles.Curves/NumberSequence:sample

NumberSequence:sample(t: number, rng?: number) -> number

Envelope-randomized sample; rng (in [0, 1]) seeds the jitter.

Parameters

  • t number
  • rng number (optional)

Returns number

modules/Particles.Curves/README

require("@builtin::deprecated.deprecatedParticles.curves") -- Particles.Curves

NumberSequence and ColorSequence — keyframe curves for VFX properties.

Animatable keyframe curves used by the particle system. Modelled on Roblox's NumberSequence / ColorSequence so the muscle memory carries over, with two differences:

  1. Up to 16 keypoints (Roblox caps at 20; we cap a bit lower so the GPU pack fits in a fixed-size param block).
  2. ColorSequence supports per-channel envelope (RGB) — Roblox does not. Curves are immutable plain tables with a metatable. Build once, sample many times. The shader-side pack format is a flat float array — see NumberSequence.pack / ColorSequence.pack for the contract. Usage: local size = NumberSequence.new({ { time = 0.0, value = 0.1 }, { time = 0.3, value = 1.0, envelope = 0.2 }, { time = 1.0, value = 0.0 }, }) print(size:evaluate(0.5)) -- deterministic interpolation print(size:sample(0.5)) -- envelope randomness applied local color = ColorSequence.new( { 1, 0.8, 0.2 }, -- yellow { 1, 0.1, 0.0 } -- red )

Usage: local Particles.Curves = require("@builtin::deprecated.deprecatedParticles.curves")

modules/Particles.Curves/isColorSequence

isColorSequence(x: any) -> boolean

True if x is a ColorSequence answering the calls one takes. A sequence that crossed a boundary preserving only its fields has its methods put back, so the value a caller tests is one it can go on to sample.

Parameters

  • x any (optional)

Returns boolean

modules/Particles.Curves/isNumberSequence

isNumberSequence(x: any) -> boolean

True if x is a NumberSequence answering the calls one takes. A sequence that crossed a boundary preserving only its fields has its methods put back, so the value a caller tests is one it can go on to sample.

Parameters

  • x any (optional)

Returns boolean

modules/Particles.Curves/new

new(a: any, b: any?): any

Parameters

  • a any (optional)
  • b any? (optional)
NumberSequence.new(1.0)
NumberSequence.new(0.0, 1.0)
NumberSequence.new({ {time=0,value=0}, {time=1,value=1,envelope=0.1} })
NumberSequence.new({ {0, 0}, {1, 1, 0.1} })

modules/Particles.Meshes/README

require("@builtin::deprecated.deprecatedParticles.meshes") -- Particles.Meshes

Built-in source-mesh templates for mesh particles.

Source-mesh geometries for the mesh-particle path of the particle system. Each built-in returns a table with positions (flat xyz array), normals (flat xyz array), uvs (flat uv array), and indices (flat u32 array) — the shape particles.create expects under its mesh field. Built-in kinds: "cube" — 24 verts × 36 indices (face-normalled box) "octahedron" — 24 verts × 24 indices (face-normalled octahedron) "tetrahedron" — 12 verts × 12 indices (face-normalled tetrahedron) "plane" — 4 verts × 6 indices (axis-aligned XY quad) Custom geometry: Pass mesh = { positions = {...}, normals = {...}, uvs = {...}, indices = {...} } directly to particles.create. The module also accepts an asset ref (assetRef("identity", "mesh")) but resolution happens through the engine's asset system — the fast path is built-in kinds.

Usage: local Particles.Meshes = require("@builtin::deprecated.deprecatedParticles.meshes")

modules/Particles.Meshes/list

list() -> {string}

Returns the built-in mesh kind names.

Returns {string}

modules/Particles.Meshes/resolve

resolve(spec: string | table) -> table

Resolve a mesh spec to { positions, normals, uvs, indices }. Spec is one of "cube" | "octahedron" | "tetrahedron" | "plane", or a custom { positions, normals, uvs?, indices } table.

Parameters

  • spec string | table

Returns table

modules/Particles.Shapes/README

require("@builtin::deprecated.deprecatedParticles.shapes") -- Particles.Shapes

Emission-shape samplers — CPU-side, called at spawn time.

Spawn positions and initial velocity directions for the standard emission shapes. CPU sampling is fine here because it runs at most N times per frame where N = emission rate, not N = active particle count — and the math is trivial. Shapes (matching Roblox naming): point — emit from origin box — Volume / Surface sphere — Volume / Surface, partial = hemisphere cap cylinder — Volume / Surface, axis = Y disc — Surface (XZ plane), partial = annulus inner radius cone — Surface, partial = half-angle Styles: "volume" — uniform inside the shape "surface" — uniform on the shape's boundary inOut (determines initial velocity direction): "outward" — surface-normal outward "inward" — surface-normal inward "inandout" — random sign per particle

Usage: local Particles.Shapes = require("@builtin::deprecated.deprecatedParticles.shapes")

modules/Particles.Shapes/frame

frame(ax: number, ay: number, az: number): (number, number, number, number, number, number, number, number, number)

Build an orthonormal frame whose middle axis is the given direction. Shape samplers emit around a local +Y axis; this frame maps those local samples into world space so emission can be aimed along any vector. Returns 9 numbers — right, axis, forward — for allocation-free use in per-particle loops: world = local.x * right + local.y * axis + local.z * forward. The frame is the shortest rotation that turns +Y onto the direction, so the local X and Z axes keep as close to world X and Z as the turn allows: a direction along +X or -X turns about Z and keeps Z, one along +Z or -Z turns about X and keeps X, and -Y is the half turn about X. A zero-length or +Y direction returns the exact identity frame.

Parameters

  • ax number — Direction X (any length; normalized internally).
  • ay number — Direction Y.
  • az number — Direction Z.
local rx, ry, rz, ax2, ay2, az2, fx, fy, fz = Shapes.frame(0, 0, -1)

modules/Particles.Shapes/inOuts

inOuts() -> {string}

Returns the directions a sample aims in — outward, inward, inandout.

Returns {string}

modules/Particles.Shapes/sample

sample(shape: string, opts: table) -> (px, py, pz, dx, dy, dz)

Sample one spawn position + unit direction from the named shape. opts: { size = {x,y,z}, style = "volume"|"surface", inOut = "outward"|"inward"|"inandout", partial = number, spreadAngle = degrees }.

Parameters

  • shape string
  • opts table

Returns (px, py, pz, dx, dy, dz)

modules/Particles.Shapes/shapes

shapes() -> {string}

Returns the supported shape names — point, box, sphere, cylinder, disc, cone.

Returns {string}

modules/Particles.Shapes/styles

styles() -> {string}

Returns the fill styles a shape is sampled with — volume, surface.

Returns {string}

modules/Particles.spawnBatches/README

require("@builtin::deprecated.deprecatedParticles.engine.spawnBatches") -- Particles.spawnBatches

The spawn batches an emitter keeps, as a min-heap on the clock each batch leaves the live bound at, and the count of particles they can still hold alive.

Usage: local Particles.spawnBatches = require("@builtin::deprecated.deprecatedParticles.engine.spawnBatches")

modules/Particles.spec/README

require("@builtin::deprecated.deprecatedParticles.engine.spec") -- Particles.spec

How the particle system reads an emitter spec: each field's value checked against the shapes it takes (a range, a size, a curve, a colour sequence, bursts, a direction, a number, a boolean, an enum, a vector, a table), with the refusal reported at the call that wrote it.

Usage: local Particles.spec = require("@builtin::deprecated.deprecatedParticles.engine.spec")

modules/Particles/M.list

M.list() -> {ParticleSystem}

Every live particle system this VM has created, newest last.

Returns {ParticleSystem}

modules/Particles/M.observe

M.observe(system: ParticleSystem?) -> table

One emitter's document, or — with no argument — the whole world's: { emitters, count, alive, capacity, spawned, silent, bytes, frame }.

Parameters

  • system ParticleSystem? (optional)

Returns table

modules/Particles/M.silenceReasons

M.silenceReasons() -> table

The closed set of reasons an emitter can be producing nothing, in the order they are resolved, each with what it means.

Returns table

modules/Particles/M.whySilent

M.whySilent(system: ParticleSystem) -> (string?, string?)

The reason one emitter is producing nothing and the detail line naming what it is about, or nil when it is producing.

Parameters

  • system ParticleSystem

Returns (string?, string?)

modules/Particles/ParticleSystem:billboard

ParticleSystem:billboard(): { [string]: any }

The camera frame this emitter's geometry was last built against: which camera supplied it, the world point it was read at, the right/up pair, and the direction that pair faces.

A sprite is a flat card placed in the compute pass against ONE camera frame, so a view looking along normal sees it face-on and a view looking across normal sees its edge. A mesh emitter spins each copy about the axis running from it to position. When a frame drawn from somewhere else shows an emitter that every count reports as producing, this is the reading that says where its geometry is turned, and means names which part of the frame this emitter's kind builds from.

local b = sys:billboard(); print(b.source, b.normal[1], b.normal[2], b.normal[3])

modules/Particles/ParticleSystem:clear

ParticleSystem:clear()

Kill every live particle by re-zeroing the state buffer, and take them out of the frame on the same call.

modules/Particles/ParticleSystem:destroy

ParticleSystem:destroy()

Release every GPU buffer and the render entity. Idempotent.

modules/Particles/ParticleSystem:emit

ParticleSystem:emit(count: number)

One-shot burst of N particles, independent of rate / enabled.

Parameters

  • count number

modules/Particles/ParticleSystem:getActiveCount

ParticleSystem:getActiveCount() -> number

How many particles are alive right now: the slots whose lifetime has not elapsed under the simulated time this emitter's dispatches have advanced. Falls to 0 when the last particle ages out.

Returns number

modules/Particles/ParticleSystem:getCastsShadows

ParticleSystem:getCastsShadows(): boolean

Whether this emitter's particles block light.

print(debris:getCastsShadows())

modules/Particles/ParticleSystem:getColliders

ParticleSystem:getColliders(): { any }

The colliders currently in force, in the shape setColliders takes — so what comes out of one goes back into the other.

local n = #sys:getColliders()

modules/Particles/ParticleSystem:getCreator

ParticleSystem:getCreator(): { [string]: any }

Whose this emitter is. owner and name are the keys its creator stated on the spec, and source and line are the code that made the call, read off the stack — so an emitter nobody tagged still names the module it came from. actor is the caller the creating call was attributed to and actorName the ZeroMind account it authenticated as, and createdAt is when the emitter was made, which orders two emitters one creator built across separate loads.

local who = sys:getCreator(); print(who.owner, who.source, who.line)

modules/Particles/ParticleSystem:getDepthFade

ParticleSystem:getDepthFade() -> number

That distance.

Returns number

modules/Particles/ParticleSystem:getGpuBytes

ParticleSystem:getGpuBytes() -> table

Every GPU buffer this emitter holds, by name, plus their total.

Returns table

modules/Particles/ParticleSystem:getLightEmission

ParticleSystem:getLightEmission() -> number

That share.

Returns number

modules/Particles/ParticleSystem:getLightInfluence

ParticleSystem:getLightInfluence() -> number

That share.

Returns number

modules/Particles/ParticleSystem:getMaxCount

ParticleSystem:getMaxCount() -> number

Buffer cap.

Returns number

modules/Particles/ParticleSystem:getRenderEntity

ParticleSystem:getRenderEntity() -> string?

Entity id of the renderable a sprite emitter spawns at world origin; nil for a mesh emitter, which draws as a GPU population instead.

Returns string?

modules/Particles/ParticleSystem:getRenderLayer

ParticleSystem:getRenderLayer(): string

The render layers this emitter's particles draw on, as the space-separated name string every render-layer surface speaks.

print(smoke:getRenderLayer())

modules/Particles/ParticleSystem:getSimulatedTime

ParticleSystem:getSimulatedTime() -> number

Seconds of simulation this emitter's dispatches have advanced. Every particle's age is measured against this clock.

Returns number

modules/Particles/ParticleSystem:getSpawnedCount

ParticleSystem:getSpawnedCount() -> number

How many slots the emitter has filled since the last clear(), saturating at maxCount. The emission schedule's own total, and the span the draw covers.

Returns number

modules/Particles/ParticleSystem:isPlaying

ParticleSystem:isPlaying() -> boolean

True while the timeline emits: enabled, past delay, and inside duration (or looping / untimed).

Returns boolean

modules/Particles/ParticleSystem:isSorted

ParticleSystem:isSorted() -> boolean

Whether this emitter reorders its own particles back to front every frame.

Returns boolean

modules/Particles/ParticleSystem:isVisible

ParticleSystem:isVisible() -> boolean

Whether the emitter's population reaches the frame.

Returns boolean

modules/Particles/ParticleSystem:observe

ParticleSystem:observe() -> table

Everything the engine holds for this emitter in one document: population, capacity, timeline, GPU bytes, render-side liveness, the GPU's own confirmation of the population, and — when it is producing nothing — the reason from a closed set.

Returns table

modules/Particles/ParticleSystem:play

ParticleSystem:play(restart?: boolean)

Start (or re-arm) the emission timeline: resets the clock to -delay, re-arms scheduled bursts, and enables emission. restart = true also clears live particles.

Parameters

  • restart boolean (optional)

modules/Particles/ParticleSystem:requestCensus

ParticleSystem:requestCensus()

Ask the GPU to count its own live slots on the next dispatch. The answer arrives on a later frame and reads back through observe().confirmation.

modules/Particles/ParticleSystem:setAcceleration

ParticleSystem:setAcceleration(x, y, z: number)

Set the constant acceleration vector (combined with gravity + wind).

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setBlendMode

ParticleSystem:setBlendMode(mode: string)

"alpha" (default) or "additive".

Parameters

  • mode string

modules/Particles/ParticleSystem:setBursts

ParticleSystem:setBursts(bursts: table)

Replace the scheduled-burst list ({ {time, count}, ... }); nil/empty removes the schedule.

Parameters

  • bursts table

modules/Particles/ParticleSystem:setCastsShadows

ParticleSystem:setCastsShadows(on: boolean?)

State whether this emitter's particles block light. A caster is drawn into the shadow map as the geometry it is — a sprite emitter's quads as quads, a mesh emitter's copies as copies — and the surfaces behind it are shaded in shadow. The declaration reaches both emitter kinds, so it means the same thing whichever one carries it.

Parameters

  • on boolean? (optional) — true to block light, false to let it through. Omitted, the emitter goes back to the default, which is letting it through.
debris:setCastsShadows(true)

modules/Particles/ParticleSystem:setColliders

ParticleSystem:setColliders(colliders: { any }?): number

Replace the shapes this emitter's particles collide against. Each entry is { kind = "plane" | "sphere" | "box" | "world", position = {x,y,z}, ... } — a plane also takes normal, a sphere radius, a box halfExtents. A world entry is the scene's own geometry, read from the distance field sceneProxy builds: it takes only radius, how far from a surface a particle counts as touching it. At most 8 are carried; the rest are dropped and reported.

Parameters

  • colliders { any }? (optional) — Array of collider descriptions.
sys:setColliders({ { kind = "world" } })

modules/Particles/ParticleSystem:setCollision

ParticleSystem:setCollision(mode: string?, opts: { [string]: any }?)

Choose what a particle does when it meets a collider.

Parameters

  • mode string? (optional) — "off", "bounce", "stop" or "kill".
  • opts { [string]: any }? (optional) — Optional { restitution, friction }. restitution is how much normal speed survives a bounce, friction how much tangential speed is lost on contact — both in [0, 1].
sys:setCollision("bounce", { restitution = 0.5, friction = 0.3 })

modules/Particles/ParticleSystem:setColor

ParticleSystem:setColor(value: ColorSequence | {number})

Per-particle color curve. A {number} array {r, g, b} is read as a constant color, and a list of such arrays as a ramp through them.

Parameters

  • value ColorSequence | {number}

modules/Particles/ParticleSystem:setDepthFade

ParticleSystem:setDepthFade(distance: number)

Set the distance, in world units, over which this emitter's particles dissolve into the surface drawn behind them. 0 turns the fade off.

Parameters

  • distance number

modules/Particles/ParticleSystem:setDirection

ParticleSystem:setDirection(x, y, z: number)

Aim emission: the shape's local +Y axis maps onto this world-space vector (positions and initial velocities both rotate). Pass nil to reset to the native +Y frame.

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setDrag

ParticleSystem:setDrag(d: number)

Set the velocity damping factor (per-second exponential decay).

Parameters

  • d number

modules/Particles/ParticleSystem:setEnabled

ParticleSystem:setEnabled(b: boolean)

Pause / resume continuous emission. Does not kill live particles.

Parameters

  • b boolean

modules/Particles/ParticleSystem:setGravity

ParticleSystem:setGravity(x, y, z: number)

Set the gravity vector (m/s^2). Default {0, -9.81, 0}.

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setLifetime

ParticleSystem:setLifetime(min: number, max?: number)

Per-particle lifetime range in seconds.

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setLightEmission

ParticleSystem:setLightEmission(share: number)

How much of itself each particle emits whatever the scene's light is, from 0 to 1. At 1 a particle is self-illuminated.

Parameters

  • share number

modules/Particles/ParticleSystem:setLightInfluence

ParticleSystem:setLightInfluence(share: number)

How much of the scene's light these particles take, from 0 to 1.

Parameters

  • share number

modules/Particles/ParticleSystem:setLook

ParticleSystem:setLook(look: string)

Parameters

  • look string — One of the eight look names.
sys:setLook("glow")

modules/Particles/ParticleSystem:setMaterial

ParticleSystem:setMaterial(materialName: string)

Replace the material applied to the render entity entirely.

Parameters

  • materialName string

modules/Particles/ParticleSystem:setOrientation

ParticleSystem:setOrientation(mode: string)

"FacingCamera" | "FacingCameraWorldUp" | "VelocityParallel" | "VelocityPerpendicular".

Parameters

  • mode string

modules/Particles/ParticleSystem:setOrigin

ParticleSystem:setOrigin(x, y, z: number)

Move the emitter to a new world-space origin.

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setParam

ParticleSystem:setParam(name: string, value: number)

Generic setter — matches PARAM_LAYOUT keys or "user" for the user block.

Parameters

  • name string
  • value number

modules/Particles/ParticleSystem:setRate

ParticleSystem:setRate(r: number)

Set continuous emission rate (particles/sec).

Parameters

  • r number

modules/Particles/ParticleSystem:setRenderLayer

ParticleSystem:setRenderLayer(layers: any)

Put this emitter's particles on named render layers. A camera or a capture including the layer draws them and one excluding it does not, so a reflection probe, a portal camera, a minimap or a clean screenshot can take the scene with the emitter's effect in it or without. The layers reach both emitter kinds.

Parameters

  • layers any (optional) — A layer name, an array of names, or a space-separated string. Omitted, the emitter goes back to the default layer.
smoke:setRenderLayer("vfx")

modules/Particles/ParticleSystem:setRotSpeed

ParticleSystem:setRotSpeed(min: number, max?: number)

Per-particle rotation-speed range (degrees/sec).

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setRotSpeedCurve

ParticleSystem:setRotSpeedCurve(value: NumberSequence | number | {start, finish} | {table})

Rotation-speed curve — multiplies each particle's own rotSpeed across its life. Written the same four ways as the transparency curve.

Parameters

  • value NumberSequence | number | {start, finish} | {table}

modules/Particles/ParticleSystem:setRotation

ParticleSystem:setRotation(min: number, max?: number)

Per-particle initial rotation range (degrees).

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setShape

ParticleSystem:setShape(opts: table)

Replace the emission shape — { kind, size, style, inOut, partial, spreadAngle } (any subset). Existing values are kept for omitted fields.

Parameters

  • opts table

modules/Particles/ParticleSystem:setSize

ParticleSystem:setSize(value: NumberSequence | number | {min, max} | {table})

Size, read the way create reads its size: a curve — a NumberSequence or the keypoint list {{time, value, envelope?}, ...} one is built from — is sampled per particle every frame on the GPU over base 1, and a number or {min, max} pair is the base each spawn samples under a constant curve of 1. Either way a particle draws at the size that was written.

Parameters

  • value NumberSequence | number | {min, max} | {table}

modules/Particles/ParticleSystem:setSorted

ParticleSystem:setSorted(on: boolean?)

State whether this emitter reorders its own particles back to front every frame. nil hands the choice back to the blend mode.

Parameters

  • on boolean? (optional)

modules/Particles/ParticleSystem:setSpeed

ParticleSystem:setSpeed(min: number, max?: number)

Per-particle initial-speed range.

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setSquash

ParticleSystem:setSquash(value: NumberSequence | number | {start, finish} | {table})

X-dimension scale curve (>1 stretches, <1 squashes). Written the same four ways as the transparency curve.

Parameters

  • value NumberSequence | number | {start, finish} | {table}

modules/Particles/ParticleSystem:setTexture

ParticleSystem:setTexture(texture: string)

Set the sprite texture (asset id or VFS path).

Parameters

  • texture string

modules/Particles/ParticleSystem:setTransparency

ParticleSystem:setTransparency(value: NumberSequence | number | {start, finish} | {table})

Transparency curve (0 = opaque, 1 = invisible). A {start, finish} pair of numbers is read as a two-stop ramp — a table holding one number as that value across the whole life — and a keypoint list as the curve through its stops.

Parameters

  • value NumberSequence | number | {start, finish} | {table}

modules/Particles/ParticleSystem:setUserParam

ParticleSystem:setUserParam(index: number, value: number)

Write one slot (1..32) of the 32-float user param block. Custom WGSL update shaders read these at params[31 + index].

Parameters

  • index number
  • value number

modules/Particles/ParticleSystem:setVisible

ParticleSystem:setVisible(on: boolean)

Whether the population the emitter holds reaches the frame. Emission is setEnabled; the draw is this, and a hidden emitter keeps its particles.

Parameters

  • on boolean

modules/Particles/ParticleSystem:setWind

ParticleSystem:setWind(x, y, z: number)

Set the wind vector (m/s).

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:simulate

ParticleSystem:simulate(t: number, stepDt?: number)

Deterministically advance the system by t seconds in fixed steps (default 1/60). Drives the full update — timeline, bursts, compute passes — so a system can be prewarmed or scrubbed to a known state.

Parameters

  • t number
  • stepDt number (optional)

modules/Particles/ParticleSystem:stop

ParticleSystem:stop(clearParticles?: boolean)

Stop emitting. Live particles finish their lifetime unless clearParticles = true.

Parameters

  • clearParticles boolean (optional)

modules/Particles/ParticleSystem:update

ParticleSystem:update(dt: number)

Step the simulation. Drives continuous emission, dispatches the compute update pass, and re-packs per-frame uniforms. Call once per frame.

Parameters

  • dt number

modules/Particles/README

require("@builtin::deprecated.deprecatedParticles.engine") -- Particles

Generic GPU-driven particle system — compute-shader simulation, zero-copy rendering.

A generic GPU particle substrate for VFX. Every active particle lives in a GPU storage buffer; the simulation runs entirely as compute dispatches; the per-frame vertex output is consumed directly by the renderer via renderer.mesh.create (no readback, no CPU re-upload). Spawning happens on the CPU (the bookkeeping cost is bounded by emission rate, not particle count) and writes new particle slots straight into the GPU state buffer. The module is one-stop: the high-level ParticleEmitter component declared elsewhere in src/lua/lib/components/ is a thin wrapper around this API. Usage: local particles = require("@builtin::deprecated.deprecatedParticles.engine") local fire = particles.create({ maxCount = 2000, rate = 100, -- particles/sec lifetime = { 0.8, 1.6 }, speed = { 2.5, 4.0 }, shape = { kind = "cone", size = {1, 1, 1}, partial = 0.3 }, gravity = { 0, 1.5, 0 }, -- buoyant drag = 0.6, size = NumberSequence.new({ { time = 0, value = 0.4 }, { time = 0.3, value = 1.0, envelope = 0.2 }, { time = 1, value = 0.0 }, }), color = ColorSequence.new({ 1, 0.9, 0.3 }, { 0.8, 0.1, 0 }), transparency = NumberSequence.new(0, 1), texture = "@builtin::textures.fire_sprite", blendMode = "additive", }) -- Each frame: fire:setOrigin(torch.position.x, torch.position.y, torch.position.z) fire:update(dt) See references/luau-cookbook.md#particles (zero-engine skill) for more recipes — explosion bursts, magic swirls, custom WGSL update injection.

Usage: local Particles = require("@builtin::deprecated.deprecatedParticles.engine")

modules/Particles/create

create(spec: table) -> ParticleSystem

Allocate a new particle system: GPU buffers, registered compute shader, renderer.mesh.create-wrapped renderable. See the about block for the full spec field list. Returns a handle whose methods are summarised below.

Parameters

  • spec table

Returns ParticleSystem

modules/Particles/list

list(filter: any?): { any }

Every particle system this VM has created and not destroyed, in creation order — or, given a filter, the ones whose creator matches it. An emitter is something this module made, so the list is answered from its own registry rather than by walking entities.

Called with nothing it answers with every emitter in the VM, which is what makes it the way to reach one whose creator has lost its handle, and :getCreator() on an entry says whose that one is. A filter narrows it to one creator's own, so a module sweeps what it left behind on a previous load without touching anybody else's.

Parameters

  • filter any? (optional) — Optional. A string matches the owner key a creator stated; a table matches every one of owner, name and source that it names.
for _, sys in ipairs(Particles.list()) do print(sys:getActiveCount()) end
for _, sys in ipairs(Particles.list("starfield")) do sys:destroy() end

modules/Particles/observe

observe(system: any?): { [string]: any }

One emitter's reading, or — called with no argument — the whole world's: every live emitter's document plus the totals they sum to. An engine with no emitters answers count = 0 with an empty list, which reads differently from an engine whose emitters are all silent.

Parameters

  • system any? (optional) — Optional ParticleSystem to read; omitted, every live one.
local world = Particles.observe(); print(world.alive, world.silent)

modules/Particles/silenceReasons

silenceReasons(): { { reason: string, means: string } }

The closed set of reasons an emitter can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every observe().reason is one of these.

for _, r in ipairs(Particles.silenceReasons()) do print(r.reason, r.means) end

modules/Particles/whySilent

whySilent(system: any): (string?, string?)

Why one emitter is producing nothing, from the closed set silenceReasons() enumerates — or nil when it is producing. The second return is the detail line naming what the reason is about.

Parameters

  • system any (optional) — The particle system to ask about.
local why, detail = Particles.whySilent(fire)

modules/PluginAssetTypeRef/README

require("@builtin::assetTypes.plugin.behavior") -- PluginAssetTypeRef

The lifecycle of every AssetRef<plugin>: onCreate makes one, registering loads it, and a change to its component or manifest loads it again in place.

Usage: local PluginAssetTypeRef = require("@builtin::assetTypes.plugin.behavior")

modules/PluginAssetTypeRef/doSomething

doSomething(input: string): any

What doSomething does.

Parameters

  • input string — What the caller passes.
local answer = await({name}.doSomething("x"))

modules/PluginAssetTypeRef/onChange

onChange(self, change)

Load the plugin again when its component or its manifest changes.

Parameters

  • self any (optional) — The AssetRef<plugin> whose files changed.
  • change any (optional) — { path, kind, origin } describing the write.

modules/PluginAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Make a new plugin asset: its manifest, README, surface module and search metadata, and its component when one is given.

Given bytes, the asset is made WITH its component in one write, so it registers as a plugin that can load rather than as an empty folder that is filled in afterwards. Without them it is a scaffold, and it loads when a plugin.wasm is written into it.

Parameters

  • name string — The plugin's name, which is also its identity in this world.
  • opts CreateOpts
asset.create("plugin", "motion", { bytes = componentBytes })

modules/PluginAssetTypeRef/onDelete

onDelete(self)

Stop the plugin this asset loaded, because the asset is gone.

Parameters

  • self any (optional) — The AssetRef<plugin> that was removed.

modules/PluginAssetTypeRef/onRegister

onRegister(self)

Load the plugin this asset carries.

Parameters

  • self any (optional) — The AssetRef<plugin> registering.

modules/PluginShared/README

require("@builtin::assetTypes.plugin.shared") -- PluginShared

What every plugin's Luau surface is built on: calling the plugin it belongs to, listening to it, and asking the person to open one of its pages. Shipped by the TYPE, so no plugin implements any of it.

Usage: local PluginShared = require("@builtin::assetTypes.plugin.shared")

modules/PluginShared/awaitingPerson

awaitingPerson(): boolean

Whether a call to this plugin is waiting on a person.

modules/PluginShared/call

call(method: string, ...): any

Call one of the methods this plugin's plugin.yaml exposes.

Parameters

  • method string

modules/PluginShared/identity

identity(): string

This plugin's engine identity, for a caller that needs to name it.

modules/PluginShared/on

on(listener: (string, any) -> ())

Listen to this plugin's events, waiting for it to load first.

Parameters

  • listener (string, any) -> () — function(eventType, payload).

modules/PluginShared/openUrl

openUrl(url: string): any

Ask the person to open one of this plugin's own declared pages.

Parameters

  • url string

modules/PopulationAssetTypeBehavior/README

require("@builtin::assetTypes.population.behavior") -- PopulationAssetTypeBehavior

Behaviour for the population asset type — a hardware-instanced population at rest. onCreate writes the recipe (population.json) plus the instance matrices as bytes (transforms.bin); :draw() turns that recipe back into live GPU buffers and draw registrations, and instantiate puts one entity in the scene that owns them for as long as it lives.

Usage: local PopulationAssetTypeBehavior = require("@builtin::assetTypes.population.behavior")

modules/PopulationAssetTypeBehavior/bounds

bounds(self): any

The box the drawn instances occupy — each variant's mesh AABB carried through every one of its matrices, in the space the matrices are taken in: the parent's own frame for a population drawn with a parent, world space otherwise.

Parameters

  • self any (optional)
local box = drawn:bounds()

modules/PopulationAssetTypeBehavior/count

count(self): number

Instances the engine reports drawing across every registration this holds. Read back from the renderer rather than from the recipe, so a registration that went away, or that the renderer turned away, counts as gone.

Parameters

  • self any (optional)
print(drawn:count())

modules/PopulationAssetTypeBehavior/destroy

destroy(self)

Release every registration and its transform buffer. The draw is dropped BEFORE its buffer is destroyed: a registration reserves slots against the buffer it was given, so a buffer that goes away takes its registration with it. The meshes belong to their .mesh assets and stay. A second call finds an empty list and returns.

Parameters

  • self any (optional)
drawn:destroy()

modules/PopulationAssetTypeBehavior/draw

draw(self, opts: { [string]: any }?): any

Rebuild this population's live draws: allocate a GPU transform buffer per variant, upload that variant's run of transforms.bin into it, resolve the mesh and material, and register one instanced draw. The returned value OWNS those resources — hold it and call :destroy() to release them; a registration nobody holds can be neither enumerated nor dropped afterwards. With a parent the matrices are taken in that entity's space and the draws move with it; without one they stand where the recipe placed them in the world.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — { castsShadows? = true, renderLayer?, parent? } — parent is the entity (or its id) the copies stand in.
local drawn = populationRef:draw(); … ; drawn:destroy()

modules/PopulationAssetTypeBehavior/drawCalls

drawCalls(self): number

Draw calls this population costs — one per variant the renderer is drawing, at any instance count. A variant the renderer turned away costs nothing and is counted nowhere; :errors() says why.

Parameters

  • self any (optional)
print(drawn:drawCalls())

modules/PopulationAssetTypeBehavior/errors

errors(self): { any }

Why this population is drawing less than its recipe asks for: one entry per registration the renderer turned away, carrying the variant it belongs to, the mesh it names and the renderer's own reason. A population drawing everything it holds answers with an empty list, so this and :drawCalls() agree with the frame.

Parameters

  • self any (optional)
for _, e in ipairs(drawn:errors()) do warn(e.variant, e.error) end

modules/PopulationAssetTypeBehavior/inspectDetail

inspectDetail(self): any

asset.inspect type-specific detail: { total, variantCount, variants }, read straight from population.json — never allocates a buffer or registers a draw. A population whose recipe can't be read returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local n = asset.inspect(populationRef).detail.total

modules/PopulationAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this population — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to. A population becomes ONE entity carrying a Population component that references this asset: the component rebuilds the live draws on awake and releases them on destroy, so the scene records one entity for a population of any size and a reload replaces the draws rather than stacking a second set. With target the entity spawns as a child of that owner (so an owning Asset component tears it down with its other children); with no target it is a fresh root. The base opts — position, rotation, scale, name, temporary — place the root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional) — { position?, rotation?, scale?, name?, temporary? }.
populationRef:instantiate(owner)

modules/PopulationAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Generic-creation hook for asset.create("population", name, opts). Pure: returns the content-file map; asset.create writes it to the mode-aware destination. Each variant contributes its mesh + material to population.json and its matrices to transforms.bin, in variant order.

Parameters

  • name string — Population identity (the instance name).
  • opts CreateOpts? (optional)
asset.create("population", "forest", { variants = { { mesh = meshGuid, material = matGuid, transforms = flat } } })

modules/PopulationAssetTypeBehavior/setRenderLayer

setRenderLayer(self, renderLayer: number)

Put every registration this holds on the render layers renderLayer names. Constant time per registration — the transforms are not re-uploaded and nothing is re-registered — so a population can follow a membership that moves, and the next frame drawn tests its copies against the new one.

Parameters

  • self any (optional)
  • renderLayer number — The membership bitmask, the same value :draw({ renderLayer }) takes. At least one bit must be set.
drawn:setRenderLayer(mask)

modules/PopulationAssetTypeBehavior/settled

settled(self): boolean

Whether the renderer has answered for every registration this holds. A registration is made a stage before the renderer sees it, so the frame it is made in is one where nothing yet says whether the copies are drawn; :errors() is complete from the frame this turns true.

Parameters

  • self any (optional)
if drawn:settled() then print(#drawn:errors(), "variants failed") end

modules/PopulationAssetTypeBehavior/spec

spec(self): any

The recipe this population draws from: { version, total, variants }, where each variant carries { mesh, material?, materialKey?, count, offset } — the .mesh it draws, the material it draws with, how many instances it holds, and where its matrices start in transforms.bin (counted in instances).

Parameters

  • self any (optional)
local total = populationRef:spec().total

modules/PopulationAssetTypeBehavior/transforms

transforms(self): string

The instance matrices as raw bytes — total * 64, little-endian f32, column-major mat4 per instance, in variant order.

Parameters

  • self any (optional)
local blob = populationRef:transforms()

modules/PostMetrics/README

require("@builtin::modules.postMetrics") -- PostMetrics

Instruments for judging a picture by numbers: FLIP against a golden, pixel metrics (banding, halo overshoot, temporal variance, ring energy, hue drift), CPU twins of the display curves and of the synthetic HDR chart, and scope pictures (histogram, waveform, vectorscope, false colour, banding zebra, tone curve). Every function takes a picture in any form one arrives in: a TextureCpuHandle, a frame renderer.presentedFrames returns, a capture result carrying base64 data, or PNG / ZTEX bytes. Pixel values are code values on the 0-255 scale the picture is delivered at unless a function says it reads light.

Usage: local PostMetrics = require("@builtin::modules.postMetrics")

modules/PostMetrics/banding

banding(frame: any, rect: Rect?, opts: { channel: string?, tolerance: number? }?): Banding

Banding along a ramp. The rectangle's columns are averaged over its rows (so dither, which varies from row to row, averages into a smooth profile and a quantised ramp does not), and the profile is read for how long it stays flat. longestRun is the most consecutive columns holding one value, distinctCodes how many distinct whole code values the profile passes through, and steps how many times it changes value. A ramp that climbs fewer code values than it has columns, undithered, holds each one for several columns: a long longestRun is a visible band. Give the rectangle several rows; one row of a dithered ramp has runs of its own.

Parameters

  • frame any (optional) — The picture.
  • rect Rect? (optional) — The ramp's rectangle, its gradient running along x; the whole picture when nil.
  • opts { channel: string?, tolerance: number? }? (optional) — { channel?, tolerance? }: tolerance is how far apart two values may be and count as one (default 0.001).
local b = postMetrics.banding(frame, { x = 0, y = 8, w = 256, h = 16 }); print(b.longestRun)

modules/PostMetrics/canvas

canvas(width: number, height: number, fill: { number }?): Canvas

A blank RGBA8 picture to draw into on the CPU, filled with one colour (code values, default black, opaque).

Parameters

  • width number — Pixels across.
  • height number — Pixels down.
  • fill { number }? (optional) — { r, g, b } in code values.
local c = postMetrics.canvas(64, 64)

modules/PostMetrics/chart.hide

chart.hide(name: string?)

Take the chart down again.

Parameters

  • name string? (optional) — The name chart.show returned (default "postMetrics_chart").

modules/PostMetrics/chart.light

chart.light(props: any, u: number, v: number): (number, number, number)

The light the chart holds at a point of the picture (u, v from the top-left, 0..1), the CPU twin of the chart shader: what a reference picture for the chart is built from. It returns the light before dither.

Parameters

  • props any (optional) — The chart's properties (pattern by number or name).
  • u number — Across, 0 at the left edge.
  • v number — Down, 0 at the top edge.
local r, g, b = postMetrics.chart.light({ pattern = "greyWedge" }, 0.5, 0.5)

modules/PostMetrics/chart.patch

chart.patch(props: any, width: number, height: number, stop: number, half: string?, inset: number?): Rect

The pixel rectangle one column of a wedge pattern covers in a width x height picture, inset by inset pixels on each side so a reading stays clear of the edges between columns. half picks the top ("top") or bottom ("bottom") half of a colour wedge; nil covers the whole height.

Parameters

  • props any (optional) — The chart's properties.
  • width number — The picture's width.
  • height number — The picture's height.
  • stop number — The column's exposure in whole stops.
  • half string? (optional) — "top", "bottom" or nil.
  • inset number? (optional) — Pixels kept clear of each edge (default 1).
local rect = postMetrics.chart.patch({ pattern = "colorWedge" }, 168, 64, 4, "top")

modules/PostMetrics/chart.reference

chart.reference(props: any, width: number, height: number, curve: any, exposure: number?): any

The picture the display should present for the chart: every pixel's chart light taken through curves.display with curve and exposure and rounded to a code value. Held against the presented chart with flip, it measures the display path against its reference; built with a different curve, it measures how far the display path stands from that.

Parameters

  • props any (optional) — The chart's properties.
  • width number — Pixels across, the presented chart's.
  • height number — Pixels down.
  • curve any (optional) — The curve, as curves.display takes it.
  • exposure number? (optional) — The exposure (default 1).
local ref = postMetrics.chart.reference({ pattern = "colorWedge" }, 128, 64, postMetrics.curves.acesHuePreserving)

modules/PostMetrics/chart.show

chart.show(props: any?, name: string?): string

Draw the chart in place of the scene: register @builtin::shaders.post.hdrChart first in the chain (on the scene layer, in light) with props. Everything after it in the chain (the effects registered after, and the display transform) draws over it.

Parameters

  • props any? (optional) — The chart's properties (pattern by number or name).
  • name string? (optional) — The effect's name (default "postMetrics_chart").
local name = postMetrics.chart.show({ pattern = "ramp", low = 0, high = 0.1 })

modules/PostMetrics/curves.aces

curves.aces(x: number): number

The ACES filmic fit of Narkowicz 2015 (__display__ curve 2), applied to one channel.

Parameters

  • x number

modules/PostMetrics/curves.acesHuePreserving

curves.acesHuePreserving(r: number, g: number, b: number): (number, number, number)

ACES (Narkowicz) applied to a colour's largest channel, the colour scaled with it: the channels keep their ratios, so the hue a per-channel curve skews is kept. On grey it is the per-channel curve exactly.

Parameters

  • r number
  • g number
  • b number

modules/PostMetrics/curves.clamp

curves.clamp(x: number): number

The clamp curve (__display__ curve 0): light clipped to 0..1.

Parameters

  • x number

modules/PostMetrics/curves.display

curves.display(r: number, g: number, b: number, curve: any, exposure: number?): (number, number, number)

What the display transform __display__ presents for a colour in light: exposure, then curve (0 clamp, 1 Reinhard, 2 ACES, 3 PBR Neutral, or a function (r, g, b) -> (r, g, b)), then the sRGB encode, in code values (0..255, unrounded).

Parameters

  • r number — Red, in light.
  • g number — Green, in light.
  • b number — Blue, in light.
  • curve any (optional) — The curve's number, or a function.
  • exposure number? (optional) — The multiplier on the light before the curve (default 1).
local r, g, b = postMetrics.curves.display(0.18, 0.18, 0.18, 3)

modules/PostMetrics/curves.pbrNeutral

curves.pbrNeutral(r: number, g: number, b: number): (number, number, number)

Khronos PBR Neutral (__display__ curve 3), over a colour in light. The reference is the Khronos sample implementation (https://github.com/KhronosGroup/ToneMapping).

Parameters

  • r number
  • g number
  • b number

modules/PostMetrics/curves.reinhard

curves.reinhard(x: number): number

Reinhard (__display__ curve 1): x / (1 + x).

Parameters

  • x number

modules/PostMetrics/expectGolden

expectGolden(frame: any, path: string, opts: any?): Golden

golden, raising its reason when the picture does not pass.

Parameters

  • frame any (optional) — The picture.
  • path string — The golden's VFS path.
  • opts any? (optional) — The options golden takes.
postMetrics.expectGolden(frame, here .. "/goldens/halo.png", { maxMean = 0.02 })

modules/PostMetrics/flip

flip(test: any, reference: any, opts: any?): any

NVIDIA FLIP of test against reference: how different the two look to a viewer flipping between them, per pixel in 0..1, summarised. mean is the number the reference evaluator reports for a pair; around 0.01 is invisible flipping between them, around 0.1 a difference anybody sees.

Parameters

  • test any (optional) — The picture being judged.
  • reference any (optional) — The picture it is held against, of the same size.
  • opts any? (optional) — { rect?, pixelsPerDegree?, errorMap? }: rect summarises one rectangle of the error map; errorMap = true hands back the per-pixel error as a grey picture.
local f = postMetrics.flip(frame, golden); print(f.mean)

modules/PostMetrics/golden

golden(frame: any, path: string, opts: any?): Golden

Compare a picture with the golden stored at path (a PNG), by FLIP. It passes when the FLIP mean is at most maxMean (default GOLDEN_MAX_MEAN) and, when maxP99 is given, its 99th percentile at most that: tolerant of the noise one GPU, driver or platform puts on a picture another drew, and failing at a change a viewer sees. status is "matched", "differs", "size" (the two are not one size), "missing" (no golden at path) or "recorded". record = true writes the picture as the golden instead of comparing, which is how a golden is made or remade after a change that is meant to change the picture.

Parameters

  • frame any (optional) — The picture.
  • path string — The golden's VFS path, a .png.
  • opts any? (optional) — { maxMean?, maxP99?, rect?, pixelsPerDegree?, record?, errorMap? }
local g = postMetrics.golden(frame, asset.containing(__FILE__).path .. "/goldens/halo.png")

modules/PostMetrics/halfWidth

halfWidth(profile: { number }): number

The distance, in profile entries, at which a profile has fallen to half of its first entry, interpolated between the two entries either side.

Parameters

  • profile { number } — A profile, from its source outward.
postMetrics.halfWidth(postMetrics.profile(frame, row))

modules/PostMetrics/haloOvershoot

haloOvershoot(frame: any, rect: Rect?, opts: { plateau: number?, channel: string?, linear: boolean? }?): Overshoot

Halo overshoot at an edge: how far a profile across a step between two flat regions rises above the brighter plateau and falls below the darker one. A sharpening or a ringing filter overshoots; a clean edge stays between its plateaus. The plateaus are the means of the profile's first and last plateau entries.

Parameters

  • frame any (optional) — The picture.
  • rect Rect? (optional) — A rectangle across the edge, its gradient running along x.
  • opts { plateau: number?, channel: string?, linear: boolean? }? (optional) — { plateau?, channel?, linear? }: plateau defaults to 4.
local e = postMetrics.haloOvershoot(frame, { x = 100, y = 60, w = 40, h = 8 })

modules/PostMetrics/hueDrift

hueDrift(colours: { any }): HueDrift

Hue drift of one colour across a series of readings (a saturated patch over exposure, say): each reading's CIELAB hue angle, drift the largest angle any reading stands from the first, and span the angle between the two readings furthest apart. A tone curve that keeps a colour's hue as it brightens drifts by nothing. Readings are colours in light ({ r, g, b }) or results of meanColour.

Parameters

  • colours { any } — The readings, in order.
local d = postMetrics.hueDrift({ postMetrics.meanColour(a, rect), postMetrics.meanColour(b, rect) }).drift

modules/PostMetrics/image

image(source: any): (any, boolean)

Load a picture (PNG / ZTEX bytes, a capture result, a presented frame) into a TextureCpuHandle. A handle passes through unchanged.

Parameters

  • source any (optional) — The picture.
local cpu, loaded = postMetrics.image(vfs.read(path)); ...; if loaded then cpu:unload() end

modules/PostMetrics/lab

lab(r: number, g: number, b: number): { L: number, a: number, b_: number, chroma: number, hue: number }

CIELAB of a colour in linear light (sRGB primaries, D65 white): lightness L, the opponent axes a and b_, chroma and hue (the hue angle in degrees, 0..360).

Parameters

  • r number — Red, in light.
  • g number — Green, in light.
  • b number — Blue, in light.
postMetrics.lab(0.05, 0.1, 1.0).hue

modules/PostMetrics/meanColour

meanColour(frame: any, rect: Rect?): Colour

The mean colour of a rectangle: its code values, the light they decode to (averaged as light), and that light's CIELAB lightness, chroma and hue.

Parameters

  • frame any (optional) — The picture.
  • rect Rect? (optional) — { x, y, w, h }; the whole picture when nil.
local patch = postMetrics.meanColour(frame, { x = 10, y = 10, w = 8, h = 8 })

modules/PostMetrics/picture

picture(source: any): any

A picture as an image value: returned from execute, a tool or a probe, it reaches the caller as a picture (mcp.image).

Parameters

  • source any (optional) — The picture.
return postMetrics.picture(postMetrics.scopes.waveform(frame))

modules/PostMetrics/profile

profile(frame: any, rect: Rect?, opts: { axis: string?, channel: string?, linear: boolean? }?): { number }

The values along a rectangle of a picture: one per column (axis = "x", the default), each the mean over the rectangle's rows, or one per row (axis = "y"). channel is "luma" (Rec. 709, the default), "r", "g" or "b"; linear = true reads light (0..1) in place of code values.

Parameters

  • frame any (optional) — The picture.
  • rect Rect? (optional) — { x, y, w, h }; the whole picture when nil.
  • opts { axis: string?, channel: string?, linear: boolean? }? (optional) — { axis?, channel?, linear? }
local row = postMetrics.profile(frame, { x = 0, y = 40, w = 256, h = 1 })

modules/PostMetrics/ringEnergy

ringEnergy(frame: any, cx: number, cy: number, inner: number, outer: number): RingEnergy

The light in a ring around a point: the summed Rec. 709 luminance, in light (0..1 per pixel, decoded from sRGB), of every pixel whose centre lies at least inner and less than outer pixels from (cx, cy). The energy a bloom spreads out of a source is the ring's energy with the bloom less the same ring's without it.

Parameters

  • frame any (optional) — The picture.
  • cx number — The ring's centre, in pixels from the left (a pixel's centre is +0.5).
  • cy number — The ring's centre, in pixels from the top.
  • inner number — The inner radius in pixels.
  • outer number — The outer radius in pixels.
local e = postMetrics.ringEnergy(frame, 64, 64, 4, 24).energy

modules/PostMetrics/scopes.falseColor

scopes.falseColor(frame: any, opts: { middleGrey: number? }?): any

False-colour exposure: each pixel painted by the band of stops its light stands at relative to middleGrey (light, default 0.18): deep purple below -6, blue to -4, cyan-blue to -2, grey to -0.5, green within half a stop of middle grey, light grey to +2, yellow to +2.4, orange above; pixels at code 255 in any channel are red (clipped) and at 0 in every channel dark purple-black (crushed).

Parameters

  • frame any (optional) — The picture.
  • opts { middleGrey: number? }? (optional) — { middleGrey? }
return postMetrics.picture(postMetrics.scopes.falseColor(frame))

modules/PostMetrics/scopes.histogram

scopes.histogram(frame: any, opts: { rect: Rect?, height: number? }?): any

A histogram picture: luma (grey) and the red, green and blue channels (each drawn as its own line) over the 256 code values, left to right, each column's height its share of the tallest bucket. rect is the metering window: only the pixels inside it are counted.

Parameters

  • frame any (optional) — The picture.
  • opts { rect: Rect?, height: number? }? (optional) — { rect?, height? }: height defaults to 128.
return postMetrics.picture(postMetrics.scopes.histogram(frame, { rect = { x = 0, y = 0, w = 64, h = 64 } }))

modules/PostMetrics/scopes.toneCurve

scopes.toneCurve(curves: { any }, opts: { evMin: number?, evMax: number?, size: number? }?): any

A tone curve plot: each curve's output code value (up) against its input light from evMin to evMax stops around middle grey (across, on a log scale), with middle grey and the output's 0 and 255 marked. A curve is a __display__ curve number or a function (r, g, b) -> (r, g, b) over light.

Parameters

  • curves { any } — The curves, each drawn in its own colour (white, orange, cyan, magenta, then repeating).
  • opts { evMin: number?, evMax: number?, size: number? }? (optional) — { evMin?, evMax?, size? }: defaults -10, 10, 256.
return postMetrics.picture(postMetrics.scopes.toneCurve({ 2, 3 }))

modules/PostMetrics/scopes.vectorscope

scopes.vectorscope(frame: any, opts: { size: number? }?): any

A vectorscope: every pixel's chroma (Rec. 709 Cb across, Cr up) on a disc, brighter where more pixels share it. Grey sits at the centre; a saturated colour sits out toward the rim along its hue. Graticule: the rim and the directions of the six primaries and secondaries.

Parameters

  • frame any (optional) — The picture.
  • opts { size: number? }? (optional) — { size? }: the scope's side in pixels (default 256).
return postMetrics.picture(postMetrics.scopes.vectorscope(frame))

modules/PostMetrics/scopes.waveform

scopes.waveform(frame: any, opts: { width: number? }?): any

A luma waveform: one column per column of the picture (resampled to at most width), its pixels plotted by code value from the bottom (0) to the top (255), brighter where more pixels share a value. A flat region is a thin line, a gradient a slope, a clipped highlight a line along the top.

Parameters

  • frame any (optional) — The picture.
  • opts { width: number? }? (optional) — { width? }: the most columns the scope has (default 512).
return postMetrics.picture(postMetrics.scopes.waveform(frame))

modules/PostMetrics/scopes.zebra

scopes.zebra(frame: any, opts: { minRun: number?, maxRun: number? }?): (any, number)

A banding zebra: the picture in grey, with diagonal magenta stripes over every run of minRun or more pixels along a row that holds one code value and ends in a step of one or two code values to its neighbour: a contour a smooth gradient was quantised into. Flat regions with no step at their end, and runs longer than maxRun, are left unmarked.

Parameters

  • frame any (optional) — The picture.
  • opts { minRun: number?, maxRun: number? }? (optional) — { minRun?, maxRun? }: defaults 4 and 128.
local z, marked = postMetrics.scopes.zebra(frame)

modules/PostMetrics/temporalVariance

temporalVariance(frames: { any }, rect: Rect?, opts: { channel: string?, linear: boolean? }?): TemporalVariance

Temporal variance: how much each pixel changes across a run of frames of the same view. Each pixel's variance over the frames is taken, in code values squared (or light squared, linear = true), and the rectangle summarised. A converged temporal resolve on a still view holds every pixel; flicker, shimmer and crawl are variance.

Parameters

  • frames { any } — Two or more pictures of one size, in order.
  • rect Rect? (optional) — { x, y, w, h }; the whole picture when nil.
  • opts { channel: string?, linear: boolean? }? (optional) — { channel?, linear? }
local f = renderer.presentedFrames({ frames = 16 }); local v = postMetrics.temporalVariance(f)

modules/PostMetrics/toImage

toImage(c: Canvas): any

A canvas as a TextureCpuHandle, for the metrics, flip and picture. The caller unloads it.

Parameters

  • c Canvas — The canvas.
local cpu = postMetrics.toImage(c)

modules/PostMetrics/upturns

upturns(profile: { number }, tol: number): number

How many times a profile turns back UP by more than tol: a local minimum followed by a rise of at least tol. A halo falls away from its source and turns back up nowhere; a kernel taking taps on a grid re-brightens at every tap.

Parameters

  • profile { number } — A profile, from its source outward.
  • tol number — The rise, in the profile's units, that counts as a turn.
postMetrics.upturns(postMetrics.profile(frame, row), 2)

modules/Prelude/README

require("@builtin::modules.prelude") -- Prelude

Auto-require globals available in every script without require(). Injects Entity, Physics, Transform, and Material as global Luau wrappers.

This script runs once after FFI registration. It requires builtin library modules and exposes them as globals. Users never need to write: local Entity = require(".entity_reflect") Instead they just use Entity directly. Only high-frequency, universally-useful APIs belong here. Specialized modules (e.g. agent tools, editor UI) still use require(). Convention: __name = raw FFI binding (Rust native call, never used directly by scripts) Name = Luau wrapper (user-facing, auto-injected via this prelude)

Usage: local Prelude = require("@builtin::modules.prelude")

modules/PresetAssetTypeRef/README

require("@builtin::assetTypes.preset.behavior") -- PresetAssetTypeRef

Per-instance methods on every AssetRef<preset>, the onCreate hook that writes a checked preset.yaml, and the validation asset.validate and every edit run.

Usage: local PresetAssetTypeRef = require("@builtin::assetTypes.preset.behavior")

modules/PresetAssetTypeRef/applyTo

applyTo(self, entityId: string, componentType: string, overrides: { [string]: any }?): boolean

Write this preset's values, overrides laid on top, onto the componentType component of entityId, field by field. Forwards to preset.load, which warns once that preset.values replaces it.

Parameters

  • self any (optional)
  • entityId string — Target entity ID.
  • componentType string — Component type name (e.g. "Locomotion").
  • overrides { [string]: any }? (optional) — Optional shallow overrides applied on top of the preset.

modules/PresetAssetTypeRef/getDefinition

getDefinition(self): string?

Read the preset's preset.yaml body as raw text.

Parameters

  • self any (optional)
local raw = presetRef:getDefinition()

modules/PresetAssetTypeRef/inspect

inspect(self): any

asset.inspect detail: the target, the format, the checked values, every rejected field, and whether the target changed since it was pinned. componentType names the component a component preset configures (nil for any other target), and fieldValues is the same table as values.

Parameters

  • self any (optional)
local detail = asset.inspect(presetRef).detail
local componentType = asset.inspect(presetRef).detail.componentType

modules/PresetAssetTypeRef/load_preset

load_preset(self, overrides: { [string]: any }?): { [string]: any }?

This preset as a plain table, overrides laid on top. Forwards to preset.load, which warns once that preset.values replaces it.

Parameters

  • self any (optional)
  • overrides { [string]: any }? (optional) — Optional table merged shallowly over the values.

modules/PresetAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Write a preset for opts.target's schema holding opts.values, checked against that schema before anything is written.

Parameters

  • name string — The preset's name.
  • opts CreateOpts

modules/PresetDefaults/README

require("@builtin::modules.preset.defaults") -- PresetDefaults

The Preset Manager: the world's ordered [[presets]] rows in /zero/source/.world_settings. A row names one declarer (target), one preset for that declarer (preset) and a path glob (filter). When something a declarer governs is created — an asset of an assetType, an import an importer runs, a component added to an entity — the first row naming that declarer whose filter matches the new thing's path gives a layer of its settings. Rows are read only at creation.

Usage: local PresetDefaults = require("@builtin::modules.preset.defaults")

modules/PresetDefaults/batchLayers

batchLayers(targets: { any }, typeName: string, data: { [string]: any }?): { { targets: { any }, data: { [string]: any }? } }

The targets of a batch component add split into runs by the data each takes: consecutive targets the same row reaches (or no row reaches) share one run, each with the data forComponent gives its members. Adding the runs in order adds the targets in the order given. A world whose rows name nothing for typeName gives one run of every target.

Parameters

  • targets { any } — Entity ids or proxies.
  • typeName string — The component type.
  • data { [string]: any }? (optional) — The values the caller passed, or nil.
for _, g in ipairs(Defaults.batchLayers(ids, "Model", {})) do print(#g.targets) end

modules/PresetDefaults/check

check(value: any, _constraint: any): (boolean, string?)

The presetDefaultRow constraint's validator: a row passes when rowProblems finds nothing.

Parameters

  • value any (optional) — The row.
  • _constraint any (optional) — The constraint descriptor.
local ok, why = Defaults.check(row, { kind = "presetDefaultRow" })

modules/PresetDefaults/consultedAt

consultedAt(declarer: any): string?

Where a declarer's settings are consulted when something it governs is created: "import" for an importer, "component.add" for a component, "asset.create" for an assetType that declares one schema for every instance. Nil for any other asset.

Parameters

  • declarer any (optional) — The declarer's AssetRef.
Defaults.consultedAt(asset.resolve("@builtin::importers.texture")) --> "import"

modules/PresetDefaults/entityPath

entityPath(id: string): string

An entity's hierarchy path: its ancestors' names from the root down and its own, joined by /. A nameless entity contributes its id.

Parameters

  • id string — The entity id.
Defaults.entityPath(id) --> "level/props/crate"

modules/PresetDefaults/forComponent

forComponent(entityId: string, typeName: string, data: { [string]: any }?): { [string]: any }?

The data a component add of typeName to entityId takes: data as given, or a new table holding the first matching row's values with data's own values laid over them, field by field. The row is read when the component is added; a row edited later changes no component already added. A row applies to what a scene build's capture scope adds (entity.capturing()), in any mode and whoever runs the build, and outside a build in edit mode. A component put back from existing content takes none: one added inside materializing, and one a component's own code adds outside a build, on any coroutine it runs. The component's own public defaults fill whatever neither states.

Parameters

  • entityId string — The entity the component is added to.
  • typeName string — The component type.
  • data { [string]: any }? (optional) — The values the caller passed, or nil.
local data = Defaults.forComponent(id, "Model", { model = "cube" })

modules/PresetDefaults/importerSettings

importerSettings(importerRef: any, sourcePath: string): ({ [string]: any }, Match?)

An importer's settings for importing sourcePath: its own values with its first matching [[presets]] row (the filter read against the source path under /source) laid over them, a struct group field by field.

Parameters

  • importerRef any (optional) — The importer.
  • sourcePath string — The source file being imported.
local s = Defaults.importerSettings(asset.resolve("@builtin::importers.texture"), "/source/sprites/hero.png")

modules/PresetDefaults/layerFor

layerFor(declarerGuid: string, relPath: string): ({ [string]: any }?, Match?)

The layer the Preset Manager gives a new thing declarerGuid governs at relPath, and the row that gives it. A matching row whose preset does not resolve, or is for another declarer, gives no layer and says why.

Parameters

  • declarerGuid string — The declarer's guid.
  • relPath string — The new thing's path, as relativePathOf gives it.
local values, match = Defaults.layerFor(asset.guid(importer), "sprites/hero.png")

modules/PresetDefaults/match

match(declarerGuid: string, relPath: string): (Row?, number?)

The first row naming declarerGuid whose filter matches relPath.

Parameters

  • declarerGuid string — The declarer's guid.
  • relPath string — The new thing's path, as relativePathOf gives it.
local row, index = Defaults.match(asset.guid(importer), "sprites/hero.png")

modules/PresetDefaults/materializing

materializing(fn: (...any) -> ...any, ...: any): ...any

Run fn with no Preset Manager row reaching a component add on this thread — for code that puts back a component that already existed (a peer's record, a saved scene's record, a template's clone).

Parameters

  • fn (...any) -> ...any — The function to run.
  • args any (optional)
Defaults.materializing(function() entity(id).component.add("Model", saved) end)

modules/PresetDefaults/normalize

normalize(input: RowInput): StoredRow

A row input as the row a world stores: its target and preset as guid envelopes, its filter as a string. An absent target is the preset's own. A target that does not resolve is kept as written so rowProblems names it; a preset that does not resolve raises.

Parameters

  • input RowInput — { preset, target?, filter? }; preset and target take an AssetRef, envelope, guid or identity.
local row = Defaults.normalize({ preset = "pixel_art", filter = "sprites/**" })

modules/PresetDefaults/recordingStamps

recordingStamps(fn: (...any) -> ...any, ...: any): ...any

Run fn and return, before its own results, what the Preset Manager rows gave the components added on this thread while it ran inside a capture scope: per stampKey, the fields the row gave that the add's caller did not state, sorted — a field of a group the caller stated only partly as group.field.

Parameters

  • fn (...any) -> ...any — The function to run.
  • args any (optional)
local stamps, ids = Defaults.recordingStamps(entity.capture, builder)

modules/PresetDefaults/relativePathOf

relativePathOf(path: string): string?

The path a row's filter reads for path, without a leading slash: the path under /source for a durable path, the path under /zero/runtime/assets/ for a runtime asset. Nil for any other path.

Parameters

  • path string — A VFS path.
Defaults.relativePathOf("/zero/source/sprites/hero.png") --> "sprites/hero.png"

modules/PresetDefaults/removeAt

removeAt(index: number)

Remove the row at index and write every other row back exactly as the file holds it — neither re-normalised nor checked, so a row can be removed while another is broken.

Parameters

  • index number — The row's index, as rows() reports it.
Defaults.removeAt(2)

modules/PresetDefaults/resolveValue

resolveValue(v: any): any?

A row value as the asset it names: an envelope by its guid, a string by the guid .world_settings's .refs pinned for it, else by the string itself as an identity or guid.

Parameters

  • v any (optional) — An envelope, a guid or identity string, or an AssetRef-shaped { path, type }.
local ref = Defaults.resolveValue(row.target)

modules/PresetDefaults/rowProblems

rowProblems(row: any): ({ string }, any?, any?)

Every problem that keeps a row from applying, and the declarer and preset it resolves to.

Parameters

  • row any (optional) — A stored row or a row input.
local problems = Defaults.rowProblems({ target = "@builtin::importers.texture", preset = "pixel_art" })

modules/PresetDefaults/rows

rows(): { Row }

The world's rows, in order, each resolved and with every problem that keeps it from applying.

for _, row in ipairs(Defaults.rows()) do print(row.index, row.targetName) end

modules/PresetDefaults/stampKey

stampKey(entityId: string, componentGuid: string, instanceName: string?): string

The key recordingStamps files a component instance under: its entity, its component's guid and its instance name (empty for an anonymous one).

Parameters

  • entityId string — The entity.
  • componentGuid string — The component's guid.
  • instanceName string? (optional) — The instance name, or nil.
local key = Defaults.stampKey(id, asset.guid(asset.resolve("Model", "component")), nil)

modules/PresetDefaults/version

version(): number

The content version of the rows file: the engine's own count for .world_settings, which moves the moment any change of the file lands, whatever made it, and at no other time.

local v = Defaults.version()

modules/PresetDefaults/write

write(inputs: { RowInput })

Replace the world's rows. Every input is normalised and the whole set checked first; any problem refuses the write, naming every one. The [[presets]] block is replaced and the rest of the file kept.

Parameters

  • inputs { RowInput } — The complete ordered rows.
Defaults.write({ { preset = "pixel_art", filter = "sprites/**" } })

modules/PresetDefaultsSection/README

require("@builtin::editor.edui.inspector.presetDefaultsSection") -- PresetDefaultsSection

The Preset Manager as an inspector section: the world's ordered [[presets]] rows, each a declarer chip, a preset chip that offers only presets for that declarer, and a filter glob, edited as typed fields through the Settings section, with move up / down, remove and add. An edit that leaves every row whole is one checked, undoable write of .world_settings. A row being filled in — just added, or given its declarer before its preset — stays in the section until it is whole, and shows what it still needs. Removing a row always writes, whatever the other rows hold. A row that cannot apply shows why.

Usage: local PresetDefaultsSection = require("@builtin::editor.edui.inspector.presetDefaultsSection")

modules/PresetDefaultsSection/context

context(): any

The section's context: the rows as values of the block's schema, a writer that checks and writes them, the chip predicate, and each row's problems as its rejection.

local ctx = PresetDefaultsSection.context()

modules/PresetDefaultsSection/render

render(): { any }

The section rendered with a fresh context.

local nodes = PresetDefaultsSection.render()

modules/PresetDefaultsSection/signature

signature(): string

A value that moves whenever what the section shows moves — the rows file's content version and the draft — for a panel's build signature.

sig ..= PresetDefaultsSection.signature()

modules/PresetDocument/README

require("@builtin::modules.preset.document") -- PresetDocument

A preset's preset.yaml. The typed form names the declarer the values are for (target) and the values themselves (values). target is written as a guid envelope, { __ref: <guid>, type: <type>, name: <name> }, which .refs pins by that guid; an identity string is accepted on read. The component-only form names a component by its registered name (component) and lists properties.

Usage: local PresetDocument = require("@builtin::modules.preset.document")

modules/PresetDocument/decode

decode(body: string): Doc

Decode a preset.yaml body. A body with a target is typed; a body with component or properties is component-only. A component-only body Yaml.decode refuses is read by the line parser presets have always been read with. A typed body that does not decode, or whose target is neither an envelope nor an identity string, raises.

Parameters

  • body string — The file's text.
local doc = Document.decode(vfs.read(path))

modules/PresetDocument/encode

encode(envelope: Envelope, values: { [string]: any }): string

The typed preset.yaml body for a target envelope and values.

Parameters

  • envelope Envelope — The target, { __ref = <guid>, type = <type>, name = <name> }.
  • values { [string]: any } — The values.
local Target = require("@builtin::modules.preset.target")
local body = Document.encode(Target.envelopeFor(texType), { filter = "nearest" })

modules/PresetDocument/pathOf

pathOf(ref: any): string

The preset.yaml path of a preset asset.

Parameters

  • ref any (optional) — The preset.
local path = Document.pathOf(presetRef)

modules/PresetDocument/read

read(ref: any): Doc

Read and decode a preset's preset.yaml.

Parameters

  • ref any (optional) — The preset.
local doc = Document.read(presetRef)

modules/PresetDocument/write

write(ref: any, envelope: Envelope, values: { [string]: any })

Write the typed preset.yaml for a target envelope and values, raising when the write does not land. Inside an open undo operation the write is recorded into it, so undoing the operation puts back what the file held.

Parameters

  • ref any (optional) — The preset.
  • envelope Envelope — The target envelope.
  • values { [string]: any } — The values.
Document.write(presetRef, envelope, { filter = "nearest" })

modules/PresetGlob/README

require("@builtin::modules.preset.glob") -- PresetGlob

Path globs for Preset Manager rows. * matches any run of characters inside one path segment, ** any run across segments, **/ zero or more whole segments, ? one character other than /; every other character matches itself. A glob matches a whole path. The empty glob matches every path.

Usage: local PresetGlob = require("@builtin::modules.preset.glob")

modules/PresetGlob/match

match(glob: string, path: string): boolean

Whether path matches glob as a whole.

Parameters

  • glob string — The glob; "" matches everything.
  • path string — A /-separated path.
Glob.match("sprites/**", "sprites/ui/button.png") --> true

modules/PresetHeal/README

require("@builtin::modules.preset.heal") -- PresetHeal

Rewrite a preset's preset.yaml into the form presets are written in, on a local edit of this world's own content. A typed document whose target is an identity string that resolves is rewritten with that target as a guid envelope; every value is kept as decoded, including any its target rejects. A component-only document whose component accepts every property it holds is rewritten in the typed form, its target the component, its values exactly as authored, and its legacy name moved to the .metadata title.

Usage: local PresetHeal = require("@builtin::modules.preset.heal")

modules/PresetHeal/forget

forget(ref: any)

Drop what this module remembers about ref, when the preset is removed.

Parameters

  • ref any (optional) — The preset.
Heal.forget(presetRef)

modules/PresetHeal/ifNeeded

ifNeeded(ref: any, origin: string?): boolean

Heal ref on a local edit of this world's own preset: a typed document naming its target by an identity string that resolves gets the guid envelope; a component-only document whose component accepts every property gets the typed form. A component-only document that cannot heal stays as it is, and the reason is warned once. Convergent: a healed document is left as it is.

Parameters

  • ref any (optional) — The preset.
  • origin string? (optional) — The edit's origin: "local" (or nil) heals; anything else does not.
Heal.ifNeeded(presetRef, change.origin)

modules/PresetHeal/writeTyped

writeTyped(ref: any, doc: any, declarer: any, values: { [string]: any })

Write values into ref's preset.yaml in the typed form, its target declarer as a guid envelope. A component-only document's legacy name moves to the .metadata title first, so every conversion from that form keeps the name the same way. Inside an open undo operation both writes are recorded into it.

Parameters

  • ref any (optional) — The preset.
  • doc any (optional) — The document ref holds now, as Document.read decoded it.
  • declarer any (optional) — The declarer the values are for.
  • values { [string]: any } — The values to write.
local Document = require("@builtin::modules.preset.document")
Heal.writeTyped(presetRef, Document.read(presetRef), comp, values)

modules/PresetTarget/README

require("@builtin::modules.preset.target") -- PresetTarget

The declarer a preset's values are for: found by the guid its target envelope names (pinned in the preset's .refs) or by the identity string a hand-written target names, checked against that declarer's current schema, and compared with the checksum the pin recorded.

Usage: local PresetTarget = require("@builtin::modules.preset.target")

modules/PresetTarget/check

check(schema: any, values: { [string]: any }): ({ [string]: any }, { Violation })

Check values against schema field by field: the fields that pass, and every violation by path, sorted.

Parameters

  • schema any (optional) — The declarer's Field schema.
  • values { [string]: any } — The values.
local valid, rejected = Target.check(schema, { filter = "nearest" })

modules/PresetTarget/declarerOfAsset

declarerOfAsset(ref: any): (any, any)

The declarer governing an asset's settings and its schema: the asset's assetType when the type declares one schema for every instance, else the asset itself when it declares its own.

Parameters

  • ref any (optional) — The asset.
local declarer, schema = Target.declarerOfAsset(texRef)

modules/PresetTarget/declarerOfComponent

declarerOfComponent(t: ComponentTarget): (any, any, any)

The component asset governing a component on an entity, its schema, and the live component proxy.

Parameters

  • t ComponentTarget — { entity = <id>, component = <name> }.
local comp, schema, proxy = Target.declarerOfComponent({ entity = id, component = "Model" })

modules/PresetTarget/describe

describe(ref: any): string

ref as a person reads it: its identity, else its name.

Parameters

  • ref any (optional) — The declarer.
local name = Target.describe(texTypeRef) --> "@builtin::assetTypes.texture"

modules/PresetTarget/envelopeFor

envelopeFor(ref: any): Envelope

The envelope a preset writes as its target for ref: the guid it resolves by, and the type and name a person reads it by. name is the asset's plain name, never an identity, so the envelope's guid is the one dependency .refs records for it.

Parameters

  • ref any (optional) — The declarer.
local envelope = Target.envelopeFor(texTypeRef) --> { __ref = "8cc2…", type = "assetType", name = "texture" }

modules/PresetTarget/isComponentTarget

isComponentTarget(v: any): boolean

True when v names a component on an entity: { entity = <id>, component = <name> }.

Parameters

  • v any (optional) — The value.
if Target.isComponentTarget(into) then ... end

modules/PresetTarget/literalFor

literalFor(ref: any): string

The literal a preset's .refs pins for ref: its guid.

Parameters

  • ref any (optional) — The declarer.
local guid = Target.literalFor(texTypeRef)

modules/PresetTarget/merge

merge(schema: any, current: { [string]: any }?, patch: { [string]: any }): { [string]: any }

patch laid over current, by the settings module's one merge rule (Settings.mergeValues): a struct-kind field (including a resolved Field.settingsOf group) merges field by field, anything else replaces.

Parameters

  • schema any (optional) — The declarer's Field schema.
  • current { [string]: any }? (optional) — The values the asset has now.
  • patch { [string]: any } — The values laid over them.
local merged = Target.merge(schema, asset.settings(tex), values)

modules/PresetTarget/refuse

refuse(prefix: string, violations: { Violation })

Raise one error listing every violation under prefix.

Parameters

  • prefix string — The error's first line.
  • violations { Violation } — The violations.
Target.refuse("preset.setValues refused:", violations)

modules/PresetTarget/resolve

resolve(presetRef: any, doc: any): Resolved

Resolve a typed document's target. An envelope resolves by its guid; pinned is true once the preset's .refs pins that guid, and changed is true while the pinned checksum differs from the target's current one. An identity string resolves by identity and reads as unpinned.

Parameters

  • presetRef any (optional) — The preset whose .refs holds the pin, or nil for a document read on its own.
  • doc any (optional) — The decoded typed document.
local Document = require("@builtin::modules.preset.document")
local target = Target.resolve(presetRef, Document.read(presetRef))

modules/PresetTarget/resolveLiteral

resolveLiteral(literal: string): Resolved

Resolve an identity string or a guid, unpinned.

Parameters

  • literal string — The declarer's identity or guid.
local target = Target.resolveLiteral("@builtin::assetTypes.texture")

modules/PresetTarget/valuesOf

valuesOf(presetRef: any, doc: any): ({ [string]: any }, Report, Resolved?)

A preset document's values and the report preset.values answers: a typed document's values checked against its target's current schema (the valid ones returned, every refused one listed by path), or a component-only document's values as authored with what its component would refuse. Raises naming the target when a typed document's target does not resolve or declares no schema.

Parameters

  • presetRef any (optional) — The preset the document is read from.
  • doc any (optional) — The decoded document (Document.read).
local Document = require("@builtin::modules.preset.document")
local values, report, target = Target.valuesOf(p, Document.read(p))

modules/Preview/README

require("@builtin::modules.preview") -- Preview

Render an asset's preview in the preview studio. ref:preview() renders any asset by the recipe its type declares under preview: in its type.yaml, through these primitives: they spawn temporary entities, frame an offscreen camera over them, render one frame, read it back as a PNG, and tear down. Scheduled previews drain through one serial work queue whose drainer renders in shared batches — subjects settle together and capture concurrently — so a world-scale backfill completes in seconds per hundred assets while the engine stays responsive.

Usage: local Preview = require("@builtin::modules.preview")

modules/Preview/__deviceState

__deviceState(): string

The render device state the preview work queue renders by, read each time the queue decides whether a queued render can draw: renderer.deviceState(). A test that has to meet the queue on a device in another state assigns a function returning that state here and puts this one back when it ends.

modules/Preview/backdrop

backdrop(): { number }

The studio backdrop's colour in a finished preview, as 0-255 { r, g, b } — what every pixel of a preview reads where no subject stands. Measured from a frame of the empty studio the first time it is asked for.

local bg = preview.backdrop()

modules/Preview/fitToSilhouette

fitToSilhouette(cam: { [string]: any }, shoot: (number, number) -> string?, passes: number?,

Move a camera until what it photographs of its subject stands centred in the frame and spans TARGET_SPAN (82%) of it, measured from the pixels. The box's centre is not the silhouette's — a posed model leans, a lamp's head sits above its post, an effect's faint outer sparks reach past its bright core — so each pass takes a small shot, finds the subject as every pixel standing off the backdrop (the shot's own corner), re-aims the camera at the silhouette's centre and moves it in or out by how far the silhouette reaches. A subject's share of the frame does not scale evenly with distance, so each correction is measured again at the framing it produced. It is how an asset preview is framed; a shot over any flat backdrop frames the same way.

local settled = preview.fitToSilhouette(cam, shoot)

modules/Preview/fromEntities

fromEntities(makeFn: () -> any, opts: { [string]: any }?): PreviewResult

Instantiate entities via makeFn, auto-frame an offscreen camera to their bounds, render one frame, and return the still PNG (base64) + stats. Spawned entities are marked temporary and despawned after the render. Renders are serialised on the shared preview rig; renders requested while the preview queue is draining a batch join its shared pass, so each still shows only its own subject at single-render wall-clock.

Parameters

  • makeFn () -> any — Function returning an entity id/proxy or array of them to preview.
  • opts { [string]: any }? (optional) — { size? = { width, height }, angle? = { yaw, pitch }, entityBudget?, backdrop?, fit? } — entityBudget is the most entities the subject may compose, roots and everything under them (default 2000); past it the result is unavailable and says how many it composed. backdrop names one of preview.backdrops (default "studio"). fit is { minShare?, keep? }, how the camera is fitted to what it sees (preview.fitToSilhouette): a subject that is mostly empty space, such as a spray of sparks, frames on its body with a small minShare and a keep below 1.
preview.fromEntities(function() local e = entity.spawn("x"); e.component.add("Model", { model = meshRef:handle() }); return e end)

modules/Preview/fromRecipe

fromRecipe(ref: any, recipe: { [string]: any }, opts: { [string]: any }?): PreviewResult

Render ref by the preview recipe its type declares (see @builtin::assetTypes.assetType.shared.previewRecipe). instantiate stands the asset in the studio through its own instantiate. surface paints the material the type's ref.previewMaterial returns on recipe.mesh. image fits the first of recipe.files the asset holds to the frame, on the studio backdrop, rendering nothing — its result carries rendered = false. ref:preview() is what calls this; a caller holding a ref calls that.

Parameters

  • ref any (optional) — The AssetRef to render.
  • recipe { [string]: any } — The type's recipe, { kind, mesh?, files?, angle? }.
  • opts { [string]: any }? (optional) — { size? = { width, height }, angle? = { yaw, pitch }, live? }.
preview.fromRecipe(ref, { kind = "surface", mesh = "sphere" })

modules/Preview/inputsFingerprint

inputsFingerprint(ref: any): string?

A fingerprint of the content an asset's preview pictures: every file of the asset that can change the picture, and the version of every asset those files reference. Its README.md and .metadata, its preview.png and where the asset sits are not part of it, so describing, renaming or moving an asset leaves it unchanged. A render records it in the preview it writes; isCurrent compares the two.

Parameters

  • ref any (optional) — The asset's AssetRef.
print(preview.inputsFingerprint(matRef))

modules/Preview/isCurrent

isCurrent(ref: any): boolean

Whether an asset's preview.png pictures the asset as it is now: it exists and the fingerprint it recorded matches inputsFingerprint. A preview rendered before the asset's content last changed, or one that records no fingerprint, is not current.

Parameters

  • ref any (optional) — The asset's AssetRef.
if not preview.isCurrent(matRef) then preview.schedulePreview(matRef) end

modules/Preview/liveFromEntities

liveFromEntities(makeFn: () -> any, opts: { [string]: any }?): PreviewResult

Instantiate entities via makeFn and hold them under a live orbiting camera instead of capturing a still: the returned session's rtHandle names a render target that camera writes EVERY frame (a UI image node with src = rtHandle shows it live), setOrbit(yaw, pitch, dist?) moves the camera around the subject's measured centre, and dispose() despawns the subject, light and camera and destroys the target. The session holds a key light over the subject the way a still render does. fromEntities routes here when opts.live is true, so every type's ref:preview({ live = true }) returns one of these with no wiring.

Parameters

  • makeFn () -> any — Function returning an entity id/proxy or array of them to preview.
  • opts { [string]: any }? (optional) — { live = true, size? = { width, height }, angle? = { yaw, pitch }, entityBudget?, deferFraming? } — entityBudget bounds the subject exactly as fromEntities does. deferFraming returns the session as soon as the subject stands, without waiting for it to report bounds: the camera and light stand for a unit box until the subject measures, and are placed again whenever its bounds change by more than a quarter of its extent (getBounds answers the bounds the session stands for).
local p = matRef:preview({ live = true })

modules/Preview/onWritten

onWritten(fn: (path: string) -> ()): () -> ()

Call fn(assetPath) whenever an asset's preview file is written with new bytes — a consumer showing the picture, which asked for it with schedulePreview(ref, { requested = true }), redraws when it lands.

Parameters

  • fn (path: string) -> () — Called with the asset's folder path.

Returns () — A function that stops fn being called.

local stop = preview.onWritten(function(path) print("new picture for", path) end)

modules/Preview/queueStateOf

queueStateOf(path: string): { [string]: any }?

Where the preview work queue stands on one asset.

Parameters

  • path string — The asset's VFS path, with or without the /zero prefix.
local s = preview.queueStateOf("/source/gold.material")

modules/Preview/queueStatus

queueStatus(): { [string]: any }

Report the preview work queue: how many renders are pending, the batch currently rendering, what holds the queue when it is waiting, session totals, and the most recent failures. Reading it starts a drain when work is queued and none is running, so a queue whose drain stopped resumes on the next read.

local s = preview.queueStatus(); print(s.pending, s.active, s.held)

modules/Preview/recordedFingerprint

recordedFingerprint(ref: any): string?

The fingerprint an asset's preview.png recorded when it was rendered, or nil when there is no preview or it records none.

Parameters

  • ref any (optional) — The asset's AssetRef.
print(preview.recordedFingerprint(matRef))

modules/Preview/registerDerivedHooks

registerDerivedHooks(): { string }

Register the <type>.preview derive hook of every asset type that declares a preview recipe, so staging such an asset reserves its preview.png. The hook is required when the type's type.yaml lists preview.png under files.required. A type already registered is left as it is.

modules/Preview/renderForReservation

renderForReservation(snapshot: any, reservation: any): (boolean, string?, string?)

Render the preview a derive reservation asks for: the staged content snapshot holds, pictured through its type's ref:preview() at 256x256 and returned as PNG bytes recording the fingerprint of that content. The render waits while the engine is in play mode. When the live asset is still the staged content it renders from the live asset; when it was edited or removed after it was staged, the staged files are mounted under /zero/source/tmp/derive-snapshots/<fingerprint>/ and rendered from that copy, which is removed again however the render ends. The copy lands whole, every staged file at once, and a reference it makes to the asset or to an asset within it (a material's own texture) names that asset's copy, so the picture is of the staged files alone. An asset the stage holds no file of is refused with the reason. A subject that draws nothing has no picture: the answer is true, nil, "empty", which the derive runner settles as an empty reservation.

Parameters

  • snapshot any (optional) — The derive snapshot of the asset: path, files, deps, read.
  • reservation any (optional) — The reservation row; its expected_fingerprint, when set, is the fingerprint the picture records.
local ok, bytes, kind = preview.renderForReservation(snapshot, row)

modules/Preview/renderStaleFor

renderStaleFor(paths: { string }): { string }

Queue a render for every asset holding one of paths whose type declares a preview recipe and whose preview is missing or not current (isCurrent), given the paths of staged files or asset folders: each asset on the way down to a path is asked, since a bundle's picture holds its interior material's. Staging does not call it. The renders run on the preview queue, so the caller waits for none of them, and each new picture is a change to the asset's preview.png. The queue's own rules still apply: library, dependency and scratch content keep the picture they carry, and a private subasset with no preview renders only when something asks for its picture.

Parameters

  • paths { string } — VFS paths of staged files or asset folders.
preview.renderStaleFor({ "/zero/source/gold.material/mat.yaml" })

modules/Preview/rendering

rendering(): boolean

Whether a preview render is under way: a batch of the work queue, or a render holding the preview rig. A render under way stands the scratch it draws through (its subjects, its capture targets, the clock its deterministic captures pin), and all of it goes when the render ends. A queued entry is under way once the batch that takes it begins.

Each is known by the engine frame it began on and bounded by the frames it can last: a batch by preview.RENDER_FRAMES, a hold of the rig by the frames past which the next waiter takes the rig. One older than its bound was left by a pass that stopped existing, and reads as ended. The answer reads the same from any caller.

while preview.rendering() do task.wait() end

modules/Preview/schedulePreview

schedulePreview(ref: any, opts: { [string]: any }?)

Queue a writePreview for ref on the preview work queue. Something that needs the picture calls this: a view that shows the asset (requested), a backfill over a world, renderStaleFor. An edit of the asset queues nothing; its preview goes stale until one of those asks. Schedules that land while the asset is queued render once, reading the asset as it is when its turn comes. Renders drain in shared batches in schedule order; poll queueStatus for progress. Only world source content under /zero/source/ is queued: scratch (/source/tmp/) carries no preview, and library content (/source/libs/) and pulled dependencies (/source/deps/) keep the picture they were committed or published with, which a direct writePreview call redraws. The queue renders in edit mode: what is scheduled while the engine plays stays pending and renders once the engine is back in edit.

Parameters

  • ref any (optional) — The asset's AssetRef.
  • opts { [string]: any }? (optional) — { debounce?: number, requested?: boolean } — debounce is the seconds the entry waits before its render becomes eligible (default 1; a backfill pass over settled content passes 0). requested says something wants this asset's picture: a private subasset (asset.isPrivateSubasset) is previewed only when requested or when it already holds a preview.
preview.schedulePreview(matRef)

modules/Preview/staleAssets

staleAssets(root: string?): { string }

The world's own assets whose preview.png pictures content they no longer hold: every instance of a type that declares a preview recipe, under root when one is given, that has a preview which is not current (isCurrent). Library, dependency and scratch content keep the picture they carry and are not listed. Reads only; staging the asset renders its current picture.

Parameters

  • root string? (optional) — A path prefix to narrow the walk to; the whole world when absent.
for _, p in ipairs(preview.staleAssets()) do print(p) end

modules/Preview/writePreview

writePreview(ref: any, opts: { [string]: any }?): (boolean, string?)

Render an asset's preview via its type's ref:preview() and persist it as preview.png inside the asset folder. The preview is the asset's visual description — it syncs and publishes like any other file, feeds image-based search, and gives browsers a thumbnail. Skips scratch content (/source/tmp/). Writes only when the rendered bytes differ from the existing file, so re-rendering an unchanged asset produces no sync traffic. This call is how a library or dependency asset's picture is redrawn: the preview queue leaves that content with the picture it carries, and in a --dev engine the file written here for a @builtin asset lands in the source checkout, to be committed with the asset.

Parameters

  • ref any (optional) — The asset's AssetRef — its type must expose preview (material, texture, bundle).
  • opts { [string]: any }? (optional) — Forwarded to ref:preview(); defaults to a 256×256 render.
preview.writePreview(matRef)

modules/ProcGraphAssetTypeBehavior/README

require("@builtin::systems.procgen.procGraph.behavior") -- ProcGraphAssetTypeBehavior

Per-instance methods exposed on every AssetRef<procGraph> — a .procGraph asset is a serialized procedural graph (proc.graph.v1). The methods below let callers use the graph THROUGH its ref (ref:eval("output:mesh"), ref:inputs(), ref:asOp()) instead of round-tripping through the proc module + a path string. asOp is also how the op registry resolves a .procGraph referenced as a graph node: the type owns "how a graph becomes an op", the evaluator's registry delegates to it. Loaded lazily by assetType.shared.ref via require("@builtin::systems.procgen.procGraph.behavior") the first time a procGraph ref is touched in a VM.

Usage: local ProcGraphAssetTypeBehavior = require("@builtin::systems.procgen.procGraph.behavior")

modules/ProcGraphAssetTypeBehavior/asOp

asOp(self): any

Wrap this graph as an op def so it can be used as a NODE in another graph: its params mirror this graph's declared inputs, its outputs mirror this graph's declared outputs, and its version folds this graph's structure with every content op it references (so an edit anywhere beneath re-keys the wrapping node). The op registry resolves a graph node whose op is a .procGraph ref through this method — the type owns the graph-as-op mapping; the evaluator's wrapGraphAsOp is the library that does the heavy folding.

Parameters

  • self any (optional)
local opDef = procRef:asOp()

modules/ProcGraphAssetTypeBehavior/compile

compile(self): { [string]: any }

Compile this asset's init.luau into its graph.json — the serialized form the evaluator loads when another graph composites this one, and the file whose write makes every live Generator bound to this asset re-cook. This is the SOURCE step, not the evaluation step: it produces the graph, it does not run it (that is a cook, which the Generator drives). The type compiles on its own whenever the source is written, so calling this is only for forcing it — a rebuild after restoring a file, or a script that wants the node count back.

Parameters

  • self any (optional)
local r = procRef:compile()

modules/ProcGraphAssetTypeBehavior/eval

eval(self, target: string, opts: any?): any

Evaluate one output of this graph and return the produced value (Geometry / Population / Texture / Material / Bundle / …). target is an output name ("mesh"), or an explicit "output:<name>" / "node:<id>[:<out>]" selector. opts forwards to proc.eval ({ inputs = { … }, seed = N }) so the graph's exposed inputs can be overridden per call.

Parameters

  • self any (optional)
  • target string — Output name or selector.
  • opts any? (optional) — Optional { inputs, seed, … } (see proc.eval).
local mesh = procRef:eval("mesh", { inputs = { radius = 3 } })

modules/ProcGraphAssetTypeBehavior/graph

graph(self): any

This asset's procedural graph, built from its init.luau source. The source is the graph's definition, so this answers with what the asset currently describes rather than with whatever was last compiled.

Parameters

  • self any (optional)
local g = procRef:graph()

modules/ProcGraphAssetTypeBehavior/inputs

inputs(self): { [string]: any }

The graph's declared inputs — the parameters that drive it, each with its procgen type and default. This is the vocabulary a Generator exposes as editable overrides.

Parameters

  • self any (optional)
for name, decl in pairs(procRef:inputs()) do print(name, decl.type) end

modules/ProcGraphAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Put this graph in the scene — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to. An entity carrying a Generator bound to this asset: the Generator cooks the graph and keeps the result as that entity's managed children, re-cooking whenever the graph or its params change — so the scene holds a live instance of the description, not a frozen copy of one evaluation. With target the generator spawns as a child of that owner (so an owning Asset component tears it down with its other children); with no target it is a fresh root. The base opts — position, rotation, scale, name, temporary — place the root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional) — { position?, rotation?, scale?, name?, temporary?, output?, params? } — output names which declared output to realize, params seeds the graph's exposed inputs.
local gen = procRef:instantiate()
local gen = procRef:instantiate(nil, { params = { rockCount = 800 } })

modules/ProcGraphAssetTypeBehavior/onChange

onChange(ref, change)

Change callback: recompile whenever the entry source is written. The graph.json this produces lands inside the same folder, which is what a live Generator bound to this asset watches — so editing the source is the whole update path, with no build step between writing and seeing it. Writes to any other file in the folder (that graph.json, the README, the sidecars) are not the definition and do not recompile — by the path filter here, and by the compiled-source guard for a change that names no path.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ProcGraphAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback: compile the graph the moment the instance registers, so an asset restored without a graph.json — or one whose source changed while the engine was down — has its compiled form before anything composites or cooks it.

Parameters

  • self any (optional)

modules/ProcGraphAssetTypeBehavior/outputs

outputs(self): { string }

The graph's declared output names (sorted). Each is a value the graph produces and a valid eval target.

Parameters

  • self any (optional)
local outs = procRef:outputs()

modules/ProcGraphAssetTypeBehavior/preview

preview(self, opts: { [string]: any }?): { [string]: any }

Render this graph to a still through the shared preview rig — the image preview.writePreview persists as the asset's preview.png, the picture search shows the graph by. The graph stands in the preview studio on a Generator of its own; an effect is brought to a representative moment (the one it names with g:still, else halfway through the range it plays, or two seconds into a timeline with no end) before the frame is taken, so the picture is the effect in motion rather than its first frame. A timeline with an end is moved to that moment; one with no end is played to it, since what it shows then is everything its solvers simulated before.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — { size? = { width, height }, angle? = { yaw, pitch } }.
local shot = graphRef:preview({ size = { width = 256, height = 256 } })

modules/ProcGraphAssetTypeBehavior/promote

promote(self, opts: { [string]: any }?): { [string]: any }

Make this graph a node: a .procNode that declares this graph's inputs as its own, calls this graph with them and gives its outputs. This graph keeps its code and every graph calling it by its path; the node puts it among the nodes procgen.searchNodes finds and a graph calls by name, and it stays this graph's: editing this graph changes what the node makes. procgen.promote is the tool form.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — Optional { name, into, description, tags }: the node's name (default: this graph's), the folder it goes in (default: the one holding this graph), what it does and the tags search finds it by (default: this graph's own .metadata).
local made = procRef:promote({ description = "A rim of rocks around a disc." })

modules/ProcGraphAssetTypeBehavior/stillTime

stillTime(self): number

The moment a still of this graph shows: the one it names with g:still, else halfway through the range it plays, or two seconds into a timeline with no end.

Parameters

  • self any (optional)
gen:seek(graphRef:stillTime())

modules/ProcGraphAssetTypeBehavior/validate

validate(self): { any }

Type-check this graph's connections. Returns proc.validate's diagnostic list (empty when the graph is well-typed) — each entry is { level, code, node, input, expected, actual, suggestions }.

Parameters

  • self any (optional)
for _, d in ipairs(procRef:validate()) do warn(d.code, d.node, d.input) end

modules/ProcNodeAssetTypeBehavior/README

require("@builtin::systems.procgen.procNode.behavior") -- ProcNodeAssetTypeBehavior

Registration lifecycle for .procNode assets — custom procedural operations authored as content. An instance's init.luau returns either a GRAPH (return proc.define(...) — the node carrying its own pipeline, the self-contained form) or a def table (op-style { inputs, params, outputs, eval } for a hand-written eval). What a node takes and gives is read from that source without running it (asset.interface), the interface: block of the type's type.yaml saying how; this behaviour loads the node's op into the proc op registry, keyed by the asset's stable GUID, when a build first runs it, and again on every edit after that.

Usage: local ProcNodeAssetTypeBehavior = require("@builtin::systems.procgen.procNode.behavior")

modules/ProcNodeAssetTypeBehavior/load

load(ref)

Load this node's op from its entry source into the proc op registry: the registry calls it the first time a build runs the node.

Parameters

  • ref any (optional)

modules/ProcNodeAssetTypeBehavior/onChange

onChange(ref, _change)

Change callback: a node a build has loaded loads again from its current entry source whenever its .procNode is written, and the version bump propagates the edit to every graph that reaches the node. A node no build has run has nothing loaded: its interface is read from the source as it now stands.

Parameters

  • ref any (optional)
  • _change any (optional)

modules/ProcNodeAssetTypeBehavior/onDelete

onDelete(ref)

Delete callback: unregister this node's op when its .procNode folder is removed. The delete ref carries no guid (the folder is already gone), so the guid is resolved from the removed folder path via the registry's path->guid index. A graph referencing it afterward errors loudly at eval — resolution is push-model, never a silent fallback. A node moved to another folder keeps its guid and stays registered at the folder it moved to.

Parameters

  • ref any (optional)

modules/ProcNodeAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback. The node's interface is read from its source wherever the asset index lists it, so nothing runs here unless a build already loaded the node, which then loads again.

Parameters

  • self any (optional)

modules/ProcNodeAssetTypeBehavior/validate

validate(ref): { { code: string, message: string, severity: string? } }

The node's own rules: its source declares its interface as literals, so what it takes and gives reads without running it; its entry source loads and returns a def or a graph; and it registers as an op.

Parameters

  • ref any (optional)

modules/ProcPackAssetTypeBehavior/README

require("@builtin::systems.procgen.procPack.behavior") -- ProcPackAssetTypeBehavior

Registration lifecycle for .procPack assets — a FAMILY of procedural operations authored as content. The instance's init.luau returns a pack table whose ops map op id to def, and this behaviour registers the whole map through registry.registerPack, so every op is validated exactly as a builtin is and tagged with the pack it came from. Ops registered this way carry the STABLE STRING IDS the pack names them with, which is what separates a pack from a .procNode — a single op keyed by its asset guid.

Usage: local ProcPackAssetTypeBehavior = require("@builtin::systems.procgen.procPack.behavior")

modules/ProcPackAssetTypeBehavior/onChange

onChange(ref, _change)

Change callback: re-register the family whenever the .procPack is seeded or its entry script is edited. Every op is re-registered from the current source and the version bump propagates the edit to every graph that reaches any of them; an op the edit removed is unregistered.

Parameters

  • ref any (optional)
  • _change any (optional)

modules/ProcPackAssetTypeBehavior/onDelete

onDelete(ref)

Delete callback: unregister every op this pack registered, and the pack itself, when its .procPack folder is removed. A graph naming one of its ops afterwards errors loudly at eval rather than cooking against a definition that no longer exists, and registry.pack answers nil.

Parameters

  • ref any (optional)

modules/ProcPackAssetTypeBehavior/onDependencyChange

onDependencyChange(ref, _change)

Dependency callback: a module the pack's entry requires re-ran, so the op definitions the pack built from it are built again from the module as it now stands, and every graph reaching one of them recooks.

Parameters

  • ref any (optional)
  • _change any (optional)

modules/ProcPackAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback: register this pack's whole family the moment the instance first registers, so a graph loaded in the same world-ready sweep can resolve every op in it. Idempotent with onChange.

Parameters

  • self any (optional)

modules/ProcPackAssetTypeBehavior/ops

ops(self): { string }

The op ids this pack registers, sorted: the ids its ops table declares, as its last completed registration read them. The former ids its aliases maps onto them answer only as aliases. A pack whose last registration did not complete, or that was never registered here, reads its entry script to answer.

Parameters

  • self any (optional)
for _, id in ipairs(packRef:ops()) do print(id) end

modules/ProcPackAssetTypeBehavior/registered

registered(path: string): boolean

Whether the pack at path has a registration that completed in this VM: every op its last registration declared stands in the registry. A registration still under way, or one that did not complete, answers false.

Parameters

  • path string — The pack's folder path.
if not behavior.registered(path) then behavior.onRegister(ref) end

modules/ProcRecordAssetTypeRef/README

require("@builtin::systems.procgen.procRecord.behavior") -- ProcRecordAssetTypeRef

Per-instance methods exposed on every AssetRef<procRecord>.

Usage: local ProcRecordAssetTypeRef = require("@builtin::systems.procgen.procRecord.behavior")

modules/ProcRecordAssetTypeRef/definition

definition(self): any

The record kind this asset defines: what its init.luau returns through proc.record.

Parameters

  • self any (optional)
local def = asset.resolve("@builtin::systems.procgen.records.material", "procRecord"):definition()

modules/PunctualLight/README

require("@builtin::tests.fixtures.punctual_light") -- PunctualLight

The engine's punctual-light response in Luau, for the suites that predict what light a point light puts on a surface: the windowed inverse-square falloff a light's radius defines, the radiance that falloff delivers, and the shading model's own answer at the one geometry that has a closed form — the surface normal, the direction to the camera and the direction to the light all the same vector. The falloff is a port of zero_punctual_falloff in engine_bindings.shaderModule, which is the one curve every reader of a light's reach answers from. The response is a port of zpbr_direct_full in pbr_shading.shaderModule, evaluated at that geometry, where every angular term is 1 and the microfacet lobes collapse to constants.

Usage: local PunctualLight = require("@builtin::tests.fixtures.punctual_light")

modules/PunctualLight/dielectricF0

dielectricF0(ior: number, reflectance: number): number

A dielectric's reflectance at normal incidence, from its index of refraction and the artist reflectance knob — zpbr_dielectric_f0. The PBR shader's defaults (ior 1.5, reflectance 0.5) give 0.04.

Parameters

  • ior number — The surface's index of refraction.
  • reflectance number — The [0, 1] reflectance knob, neutral at 0.5.
local f0 = PunctualLight.dielectricF0(1.5, 0.5)

modules/PunctualLight/falloff

falloff(dist: number, radius: number): number

How much of a punctual light's emitted power arrives at dist: the inverse-square law, windowed to zero at the light's radius. The engine's own curve, zero_punctual_falloff.

Parameters

  • dist number — Distance from the light to the shaded point, in world units.
  • radius number — The light's reach — a Light component's range.
local a = PunctualLight.falloff(2.0, 8.0)

modules/PunctualLight/normalIncidenceResponse

normalIncidenceResponse(

The light a surface answers for one radiance arriving along its own normal, while the camera also looks along that normal.

zpbr_direct_full evaluated where the normal, the view direction and the light direction coincide: the half-vector is the normal, so every one of n·h, n·v, v·h and n·l is 1, the Fresnel term is F0 itself, the GGX distribution is α²/(π·α⁴) and the Smith visibility is 1 at any roughness. What is left is a diffuse lobe and a specular lobe that are both constants of the surface, which is what lets a case state the light it expects as a number.

This models the diffuse and specular lobes alone — the ones a bare metal or dielectric surface shades with. Clearcoat, sheen, fibre, anisotropy and iridescence are lobes the PBR shader adds on top where a material's own properties ask for them.

local L = PunctualLight.normalIncidenceResponse(

modules/PunctualLight/radiance

radiance(color: { number }, intensity: number, dist: number, radius: number): { number }

The radiance one punctual light delivers at dist: its colour times its intensity times the falloff at that distance. The quantity the shading loop hands the BRDF, for a light with no cone, no cookie and no shadow.

Parameters

  • color { number } — The light's colour, one entry per channel.
  • intensity number — The light's intensity.
  • dist number — Distance from the light to the shaded point, in world units.
  • radius number — The light's reach — a Light component's range.
local r = PunctualLight.radiance({ 1, 0.6, 0.3 }, 4, 2.0, 8.0)

modules/Queue/README

require("@builtin::modules.api.engine.ecs.queue") -- Queue (also available as global 'queue')

Defer FFI mutations across frames — queue(fn) -> (ok, err, drainPromise).

Inside the body, write FFI calls (entity.spawn, component.add, position.set, etc.) enqueue rather than execute synchronously. The engine's per-frame drainer applies them in FIFO order with a per-frame budget. Returns (true, nil, drainPromise) on success, (false, err, nil) if the body throws — in which case the mutations the body queued are dropped so they do not later apply; what other code queued stays. drainPromise is a promise handle that resolves once every mutation queued inside fn has been applied by the engine drainer. await(drainPromise) blocks the calling coroutine until full drain — use this to gate post-queue work (scene/layer load fires onLoad after the drain promise resolves). If nothing was queued (e.g. an empty fn), the promise resolves before queue() returns. Nested queue() runs in the already-active queue mode: the depth counter is re-entrant, and an inner body that throws drops only what it queued. See docs/plans/2026-05-06-luau-queue-deferred-mutations.md for the full design (drain budget, error semantics, future per-key fences for reads).

Usage: local Queue = require("@builtin::modules.api.engine.ecs.queue") Also available as global: queue

modules/Random/README

require("@builtin::modules.random") -- Random (also available as global 'Random')

Seeded random-number generators. Each generator holds its own stream, so any number of independent, reproducible sequences run side by side, and none of them draws from or reseeds math.random.

A generator made from a seed produces the same sequence on every platform and every build: the same seed gives the same numbers on a native engine and in the browser, run after run. local rng = Random.new(1234) -- the same seed, the same stream local roll = rng:nextInteger(1, 6) -- integer in [1, 6], both ends included local t = rng:nextNumber() -- float in [0, 1) local x = rng:nextNumber(-5, 5) -- float in [-5, 5) Members every generator answers: rng:nextNumber() float in [0, 1), 53 random bits rng:nextNumber(min, max) float in [min, max) rng:nextInteger(min, max) integer in [min, max], both ends included, every value equally likely rng:clone() an independent generator at the same position: it draws what this one would draw next rng:state() the position as a 32-character hex string a save, a record or a network message can hold Random.fromState(rng:state()) makes a generator that continues the sequence from where rng stood, so a saved level, a replayed AI decision or a regenerated design picks up exactly where it left off. Random.new() with no seed takes its seed from the clock. math.random is a different thing: one stream per Luau VM, shared by every script in that VM and reseeded for all of them by math.randomseed. A generator here is a value of its own; drawing from one moves no other. The algorithm is xoshiro128** (Blackman and Vigna) over 32-bit words. A seed is read as the bits of its IEEE-754 double: the low word and the high word each go through the MurmurHash3 32-bit finalizer to give the first two state words, and the last two are mixed from those, so every distinct number is a distinct stream. nextNumber() joins the top 27 bits of one output and the top 26 of the next into a 53-bit fraction; nextInteger rejects the draws that would bias the range.

Usage: local Random = require("@builtin::modules.random") Also available as global: Random

modules/Random/fromState

fromState(state: string): RandomGenerator

Make a generator that continues a sequence from a position rng:state() recorded. It draws exactly what the recorded generator drew next, so a save holds the string and a load picks the stream up.

Parameters

  • state string — The 32-character hex string rng:state() returned.
local resumed = Random.fromState(Random.new(7):state())

modules/Random/new

new(seed: number?): RandomGenerator

Make a generator with its own stream. The same seed gives the same sequence on every platform and build; any number of generators run side by side without moving one another or math.random. With no seed it takes one from the clock, and rng:state() records where it stands.

Parameters

  • seed number? (optional) — Any finite number; every distinct number is a distinct stream. Omitted, the clock seeds it.
local roll = Random.new(42):nextInteger(1, 6)
local jitter = Random.new(42):nextNumber(-0.5, 0.5)
local clockSeeded = Random.new()

modules/RenderError/README

require("@builtin::modules.renderError") -- RenderError

Shared "this render is broken" marker. visibleError(entityId) swaps an entity to the builtin ERROR text model + error material, so a failed render reads as an unmistakable 3D "ERROR" sign on screen instead of empty pixels — empty silently reads as "fine" and a broken object gets mistaken for working. Every render component (built-in Model / SkinnedModel, or a user-authored one) calls this, so the error model is defined in ONE place: change it here and every render component updates.

Usage: local RenderError = require("@builtin::modules.renderError")

modules/RenderError/visibleError

visibleError(entityId: string, reason: string?): boolean

Swap entityId to the builtin ERROR marker so a broken render is unmistakable on screen. Un-skins the entity first (a bad skeleton can't collapse the marker), binds the ERROR mesh + error material via the public ecs.* API, and logs the reason at error level. Returns true if the marker was applied.

Parameters

  • entityId string — The entity whose renderable becomes the ERROR marker.
  • reason string? (optional) — Short human string describing what failed (logged).
renderError.visibleError(self.entityId, "mesh never became GPU-resident")

modules/RenderFeatureAssetTypeRef/README

require("@builtin::assetTypes.renderFeature.behavior") -- RenderFeatureAssetTypeRef

Per-instance methods exposed on every AssetRef<renderFeature>. Loaded lazily by assetType.shared.ref the first time a renderFeature ref is touched in a VM. A render feature is an init.luau exporting { setup?(ctx), render(ctx), teardown?(ctx) }; :enable() instantiates it (the engine begins calling its render(ctx) hook every frame) and returns a live handle, also visible under /zero/runtime/renderFeatures/.

Usage: local RenderFeatureAssetTypeRef = require("@builtin::assetTypes.renderFeature.behavior")

modules/RenderFeatureShared/README

require("@builtin::assetTypes.renderFeature.shared") -- RenderFeatureShared

Shared helpers + the authoring contract for renderFeature modules.

Usage: local RenderFeatureShared = require("@builtin::assetTypes.renderFeature.shared")

modules/ReportFormatter/README

require("@builtin::systems.worldValidation.reportFormatter") -- ReportFormatter

Filtering + formatting helpers for the world validation report. Pure functions: every call returns a new value, never mutates the input report.

Three exports: filter (re-filter an existing Report without re-scanning the VFS), format (render to a string in one of four shapes), and summary (one-line health line). All three are pure — they never mutate the input report.

Usage: local ReportFormatter = require("@builtin::systems.worldValidation.reportFormatter")

modules/ReportFormatter/filter

filter(report, opts)

Return a new Report containing only problems that match the filter options. Counts are recomputed from the filtered set so the caller can trust counts against the visible problems list. Pure — the input report is not mutated.

Parameters

  • report any (optional) — Report produced by the main validator.
  • opts any (optional) — Filter options — { severity, category, source, code, path (Lua pattern), includePlaceholders (default true), limit }.
local errs = ReportFormatter.filter(r, { severity = "error" })
local libsOnly = ReportFormatter.filter(r, { source = "library:@builtin" })

modules/ReportFormatter/format

format(report, format)

Render a Report into a string in the chosen format.

Parameters

  • report any (optional) — Report produced by the main validator.
  • format any (optional) — One of "human" (default), "markdown", "json", "summary". Unknown values fall back to "human".
print(ReportFormatter.format(r, "human"))
local md = ReportFormatter.format(r, "markdown")

modules/ReportFormatter/summary

summary(report)

Compact one-line health summary string — world: NE/NW libraries: NE/NW total: OK|FAIL.

Parameters

  • report any (optional) — Report produced by the main validator.
print(ReportFormatter.summary(r))

modules/ResourceHandle/README

require("@builtin::modules.resource_handle") -- ResourceHandle

Recognises a live GPU resource handle — the { kind = "<Category>Handle", category, guid, name } table renderer.mesh.create, renderer.material.create and renderer.texture.create return. Every boundary that carries a component field value out of the session that produced it consults this to tell a handle from an AssetRef.

Usage: local ResourceHandle = require("@builtin::modules.resource_handle")

modules/ResourceHandle/forget

forget(owner: string)

Let go of every handle parked for owner. A parked handle is put back by a load that builds the entity from its record inside this session, so once no record names the entity, nothing reads it again.

Parameters

  • owner string — The entity id the handles were parked under.
ResourceHandle.forget(id)

modules/ResourceHandle/isLive

isLive(v: any): boolean

Whether a value is a live GPU resource handle.

Parameters

  • v any (optional) — Any component field value.
ResourceHandle.isLive(renderer.mesh.create(geometry)) -- true

modules/ResourceHandle/label

label(v: any): string

A short phrase naming a handle, for a message about the value.

Parameters

  • v any (optional) — A handle table.
ResourceHandle.label(meshHandle) -- "mesh handle 'msh_grass_1'"

modules/ResourceHandle/park

park(owner: string, componentType: string, dropped: { [string]: any }?, instanceName: string?)

Hold the live handles one component instance's durable record left out, keyed by the entity and component instance they came from, until forget lets go of the entity. Each call replaces what that instance had parked before.

Parameters

  • owner string — The entity id the component sits on.
  • componentType string — The component's type name.
  • dropped { [string]: any }? (optional) — The { field = handle } map left out of the record, or nil when the component's record carries every field it holds.
  • instanceName string? (optional) — The instance's name, or nil for the type's anonymous instance.
ResourceHandle.park(id, "@builtin::components.Model", { model = mesh })

modules/ResourceHandle/withParked

withParked(data: any, owner: string, componentType: string, instanceName: string?): (any, number)

The component field map to apply, with every field this session parked for owner's instance of componentType and the record does not carry put back.

Parameters

  • data any (optional) — A component's { field = value } map from a record.
  • owner string — The entity id the component is being applied to.
  • componentType string — The component's type name.
  • instanceName string? (optional) — The instance's name, or nil for the type's anonymous instance.
ResourceHandle.withParked(record.data, id, "@builtin::components.Model")

modules/ResourceHandle/withoutLive

withoutLive(data: any): (any, { [string]: string }?)

The component field map to apply, with any live-handle value left out — the form a value read from durable content takes in a session other than the one that minted it. The field falls back to the component's declared default, and the caller reports what it stood for.

Parameters

  • data any (optional) — A component's { field = value } map.
ResourceHandle.withoutLive(record.data) -- data, nil

modules/RigAssetTypeBehavior/README

require("@builtin::assetTypes.rig.behavior") -- RigAssetTypeBehavior

Behaviour for the rig asset type — a skeleton's disk shape. The payload is rig.json: ordered bones with names, parent hierarchy, the node index each maps to, rest local transform and inverse-bind matrix, where its bind pose comes from (bind), plus the author's overrides — per avatar type, the roles an author set by hand. How the rig maps onto an avatar type (which bone is the hips, the left hand, ...) is derived from the bones whenever it is asked for, and the overrides are laid over it. A rig is its own primitive: a skinned mesh references a rig, an animation references its source rig, and retargeting maps one rig onto another. The format is text, so an agent can open it and read or correct it with an edit.

Usage: local RigAssetTypeBehavior = require("@builtin::assetTypes.rig.behavior")

modules/RigAssetTypeBehavior/onChange

onChange(self, _change)

Drop the cached decode and every qualification made from it when the rig's content changes — a hot-reload, a re-import or an override edit — so the next read re-derives from the new rig.json.

Parameters

  • self any (optional) — The rig AssetRef that changed.
  • _change any (optional) — What happened to it; the caches are dropped whatever it was.

modules/RigAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("rig", name, opts). Returns the content-file map; asset.create writes it to the authored destination, registering the .rig asset under its minted guid. A profile or humanoid section in the incoming document is dropped: how a rig maps onto an avatar type is derived from its bones.

Parameters

  • name string — Rig identity (the instance name).
  • opts CreateOpts
asset.create("rig", "PolygonSyntyCharacter", { json = rigJson })

modules/RigAssetTypeBehavior/onReplace

onReplace(dest: string, files: { [string]: string }): { [string]: string }

Replacement hook: when asset.create writes a rig over one already at dest (a re-import re-deriving the rig from its source file), the replaced rig's overrides are carried into the new document, so what an author set by hand survives. Overrides the new document states itself win for their type.

Parameters

  • dest string — The folder of the rig being replaced.
  • files { [string]: string } — The files onCreate produced for the new rig.

modules/SceneAssetTypeRef/README

require("@builtin::assetTypes.scene.behavior") -- SceneAssetTypeRef

Per-instance methods exposed on every AssetRef<scene>. Loaded lazily by assetType.shared.ref.

Usage: local SceneAssetTypeRef = require("@builtin::assetTypes.scene.behavior")

modules/SceneAssetTypeRef/build_scene

build_scene(self, opts: { save: boolean?, record: boolean?, layer: any?, trigger: string? }?): any

Run this scene's build.luau against what it resolves right now and land what it declares in the scene — for the case where something the builder reads changed and the file did not. Writing build.luau already runs the build, so a call after a write is unnecessary. content() writes the scene's entities and editorOnly() writes the ones that are present while authoring and absent in play; each is reconciled against the entities the last build placed, so an entity keeps its id across every rebuild and anything else in the scene is left alone. Runs in edit mode on a LOADED scene — the entities it lands on are the live ones. opts.save = false leaves the result unsaved; by default the scene is saved, which is what puts the build in scene.json. opts.record = false lands the result in the live scene alone: nothing it creates, changes or takes away reaches the scene's unsaved edits or scene.json. A call made while a build for this scene is running returns that build right away instead of starting a second one — a build the scheduler is still advancing. One whose task was cancelled, or that stopped for any other reason without returning, hands the scene back to this call, which runs the build. A build that a newer write to build.luau replaces stops where it stands and leaves the scene to the rebuild that write asked for.

Parameters

  • self any (optional)
  • opts { save: boolean?, record: boolean?, layer: any?, trigger: string? }? (optional) — Optional { save = false } or { record = false }.
layers.active.asset:build()

modules/SceneAssetTypeRef/discard_scene

discard_scene(self, opts: any): boolean

Discard this scene's UNSAVED (dirty) edits and restore its saved scene.json. When the scene is LOADED, the overlay is deleted and the live layer is respawned from canonical (with the scene-load gate held so the respawn is not re-marked dirty); when it is NOT loaded, the on-disk overlay is simply deleted. Edit-mode overlay only — not the play-mode baseline.

Parameters

  • self any (optional)
  • opts any (optional) — Optional { to = <name|path> }, or a bare name/path string, to target a scene other than this asset's own path.
sceneRef:discard()

modules/SceneAssetTypeRef/getBuildScript

getBuildScript(self): string?

Read the scene's build.luau body as raw text.

Parameters

  • self any (optional)
local src = sceneRef:getBuildScript()

modules/SceneAssetTypeRef/getEntrypoint

getEntrypoint(self): string?

Read the scene's entrypoint.luau body as raw text.

Parameters

  • self any (optional)
local code = sceneRef:getEntrypoint()

modules/SceneAssetTypeRef/getSceneJson

getSceneJson(self): { [string]: any }?

Parse the scene's scene.json into a Lua table.

Parameters

  • self any (optional)
local s = sceneRef:getSceneJson()

modules/SceneAssetTypeRef/getSceneJsonRaw

getSceneJsonRaw(self): string?

Read the scene's scene.json body as raw JSON text.

Parameters

  • self any (optional)
local raw = sceneRef:getSceneJsonRaw()

modules/SceneAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { entityCount, entityNames, player }, parsed from the scene's own scene.json — never executes entrypoint.luau. A scene whose scene.json can't be read/parsed returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local entityNames = asset.inspect(sceneRef).detail.entityNames

modules/SceneAssetTypeRef/listEntities

listEntities(self): { string }

List the entity names declared at the top level of the scene's JSON body. Best-effort; nested children are not flattened.

Parameters

  • self any (optional)
for _, n in ipairs(sceneRef:listEntities()) do print(n) end

modules/SceneAssetTypeRef/load_scene

load_scene(self, opts: { [string]: any }?): any

Load this scene into the root ("main") slot. Equivalent to layers.load(self) — the AssetRef envelope is passed straight through, so the loader threads scene identity by guid. Pass opts for additive overlays / persistence / origin offset (same shape as layers.load's second argument).

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — Optional load options forwarded to layers.load.
sceneRef:load()
sceneRef:load({ additive = true, name = "hud_overlay" })

modules/SceneAssetTypeRef/onChange

onChange(ref, change)

A write inside a .scene folder. A write to build.luau re-runs the build on the loaded scene, which is what makes an edit to the code show up in the scene without reloading it. The rebuild finishes after the write has returned, and posts a notice naming the scene and how many entities each surface placed — the write's own answer is that the file was written. A write that arrives while a rebuild is running gets a rebuild of its own once that one finishes, and several such writes get the single run that reads them all. A rebuild lands and saves over several frames; until it is through, layers.rebuildInFlight() reads true and a flip to play is refused. A write to entrypoint.luau reloads the layer holding the scene without rebuilding it: the previous load's onUnload runs and the tasks, callbacks and entities that load started end, then the new code runs and its onEditLoad fires. That reload also finishes after the write has returned and posts a notice saying whether the layer came back whole, and writes landing while it runs get one more reload once it finishes. A write holding the code the layer already runs (only comments and whitespace count as none) reloads nothing. Both apply in edit mode; an edit made in play runs at the scene's next load. A write to build.luau builds in every session sharing the world that has the scene loaded, whichever session made it. The session that made it records and saves what its build lands; every other one lands the build in its live scene alone, and the records and the save of the session that made the write land on the same entities, since each entity a build creates takes the id every session derives for it. When the session that made the write does not have the scene loaded, the scene's next save, or its next load, puts the build in scene.json.

Parameters

  • ref any (optional) — The scene whose folder was written.
  • change any (optional) — { path, asset, type, kind, origin }.
__zero_dispatch_asset_change("/zero/source/scenes/main.scene/build.luau")

modules/SceneAssetTypeRef/onCreate

onCreate(_name: string): { [string]: string }

Generic-creation hook for asset.create("scene", name). A new scene is the type's template shape verbatim.

Parameters

  • _name string
asset.create("scene", "my_level")

modules/SceneAssetTypeRef/save_scene

save_scene(self, opts: any): any

Publish this scene's current state to its canonical scene.json. When the scene is LOADED, its live scenegraph is captured into canonical; when it is NOT loaded, its pending dirty overlay is promoted into canonical. Either way the durable scene.json ends up reflecting the authored state. opts.to (a scene name or path) saves to a different scene ("save as"). opts.layer is the layer holding this scene, for a caller that already has it: a scene is findable through layers.list only once its load has registered it, so a save made DURING that load names its layer itself and captures the live scenegraph rather than reading it as unloaded.

Parameters

  • self any (optional)
  • opts any (optional) — Optional { to = <name|path>, layer = <SceneProxy> }, or a bare name/path string.
sceneRef:save()
layers.active.asset:save({ to = "level_2" })

modules/SceneAssetTypeRef/validate

validate(self): { { code: string, severity: string, message: string } }

Semantic content validation for a scene: the player-setup rule set run over this scene's scene.json, surfaced through asset.validate, plus a warning when the scene has no explicit sky entity.

Parameters

  • self any (optional)

modules/SceneBuild/README

require("@builtin::assetTypes.scene.shared.build") -- SceneBuild

Runs a builder function in an entity capture scope and composes what it created into records, then reconciles records into the scene with ids held stable across rebuilds. Also holds the build surface a build script reads, whose build.asset authors the assets a build produces and hands back the same one on every run the code has not changed.

Usage: local SceneBuild = require("@builtin::assetTypes.scene.shared.build")

modules/SceneBuild/attribute

attribute(refusals: { Refusal }): { Refusal }

Read a refusal's traceback for the one frame that belongs to the code being built. A build composed from several contributors reports this so an author reads which contributor was refused rather than which build ran.

Parameters

  • refusals { Refusal } — The refusal array entity.capture hands back.
local named = SceneBuild.attribute(select(4, entity.capture(fn)))

modules/SceneBuild/buildSurface

buildSurface(folder: string, sourceDigest: string): BuildSurface

The build surface a build script reads: the operations that belong to the build itself rather than to the scene it states. build.asset(kind, name, produce) is the asset a build makes — produce runs when the build script changed and its result is authored at <folder>/<name>.<kind>, and every other run hands back that same asset, guid and all, without running produce at all. The returned AssetRef is what a component field names, so the reference survives the save and the reload.

Parameters

  • folder string — The build's own folder, which the assets it produces are authored inside.
  • sourceDigest string — The digest of the build script running now, as M.digest reports it — what decides whether an asset it produced is still the asset the code states.
local surface = SceneBuild.buildSurface(dir, SceneBuild.digest(src))

modules/SceneBuild/digest

digest(source: string): string

A short, stable digest of a script's source. Two different scripts give different digests, and the same script gives the same one on every machine and every run — which is what makes it the answer to "did the code that produced this change?".

Parameters

  • source string — The script body to digest.
local key = SceneBuild.digest(vfs.read(path))

modules/SceneBuild/drift

drift(owner: string?): { Drift }

Where the live scene disagrees with the build that states it. Every entity a build placed records what that build last said about each of its properties, so anything an author has changed since reads back differently — and this is that list: the entity, the property, what the build said, and what the scene holds now.

These are the values a rebuild KEEPS. A build repeating itself leaves them alone, and only a build that states something DIFFERENT about that property takes it back. So this is what to read to know that a scene and its build.luau disagree, and where, before deciding which should win.

Property names are the ones the build records: n name, i internal, p position, r rotation, s scale, and a:<name> for an attribute.

Parameters

  • owner string? (optional) — Optional build name, as M.ownerOf reports it, to read just that build. Omitted, every build-owned entity in the scene is read.
for _, d in ipairs(SceneBuild.drift()) do print(d.name, d.property) end

modules/SceneBuild/isStatedNothingRefusal

isStatedNothingRefusal(err: any): boolean

Whether err is the refusal reconcile raises for a build that stated no records while its scene loads over entities an earlier build placed. The refusal is about the module's text, which a later request for the same text states again, so a caller keeps the text it built as the one that stands.

Parameters

  • err any (optional) — The value a build raised.
local refused = SceneBuild.isStatedNothingRefusal(err)

modules/SceneBuild/landed

landed(key: string, landing: Landing)

Record that the build landing stands for is through. An entry a later build of the same scene put in its place stands.

Parameters

  • key string — The key the scene is known by.
  • landing Landing — The entry noteLanding recorded for this build.
SceneBuild.landed(guid, entry)

modules/SceneBuild/landing

landing(): { { scene: string, startedBy: string } }

The builds landing in the live scene right now, one row per scene, ordered by scene. A build lands over many frames, and while one is landing the live scene holds part of what it produces and part of what the build before it produced. An entry whose build stopped without saying so is dropped here.

if #SceneBuild.landing() == 0 then engine.mode = "play" end

modules/SceneBuild/landingFor

landingFor(scene: { guid: string?, path: string? }): { scene: string, startedBy: string }?

The build landing in the live scene for one scene, named by its guid or by the folder it is stored in, or nil while none is. A build of that scene is carrying its records into the scene's layer and saving it, over many frames, while this answers a row; an entry whose build stopped without saying so is dropped here. A caller running on the build's own coroutine is that build, and is answered nil.

Parameters

  • scene { guid: string?, path: string? } — { guid, path } of the scene, either one or both.
while SceneBuild.landingFor({ guid = ref.guid, path = ref.path }) do task.wait() end

modules/SceneBuild/noteLanding

noteLanding(key: string, landing: Landing)

Record that a build of the scene known as key is landing, from the moment it is started until it is through. The entry replaces any standing one for the same scene.

Parameters

  • key string — The key the scene is known by: its guid, else its path.
  • landing Landing — { scene, startedBy, advancing }: the scene's name, what started the build, and a function answering whether the build is still being carried out.
SceneBuild.noteLanding(guid, { scene = "main", startedBy = "a write to build.luau", advancing = function() return true end })

modules/SceneBuild/notePreview

notePreview(entityId: string, values: { [string]: any }, componentType: string?): nil

Record the values an author left on entityId, an entity a build owns. The build states that entity from its own source, so the values hold until it runs again — and M.takePreview is what the next run reads to say which of them it replaced and with what. Each record REPLACES the one before it: what it states is everything the entity carries now, so a name dropped between two records is dropped here too.

Parameters

  • entityId string — Runtime entity id of the owned entity.
  • values { [string]: any } — The values the entity carries now, by name.
  • componentType string? (optional) — The component that states them, so a rebuild knows to state that type again instead of leaving it to the scene.
SceneBuild.notePreview(id, { count = 9 }, "SceneModule")

modules/SceneBuild/noteSourceWrite

noteSourceWrite(guid: string, origin: string?): nil

Record that a file of the asset guid, which a build is stated from (a .sceneModule a placement builds), was written, and by which session. Every session holding a build stated from it rebuilds; the session that made the write records what its rebuild lands, and every other session lands it in its live scene alone.

Parameters

  • guid string — The asset's guid.
  • origin string? (optional) — "local" for a write this session made (the default), "remote" for one another session sharing the world made.
SceneBuild.noteSourceWrite(ref.guid, change.origin)

modules/SceneBuild/ownerOf

ownerOf(entityId: string): string?

The build that placed entityId, or nil when no build placed it. A reconcile writes the name of the build onto every entity it places, as an attribute the scene records beside the entity's name and transform, so the answer holds across a reload — and an entity an author spawned carries no owner at all.

Parameters

  • entityId string — Runtime entity id.
if SceneBuild.ownerOf(id) ~= nil then print("a build states this") end

modules/SceneBuild/previewedComponentType

previewedComponentType(entityId: string): string?

The component type that recorded a preview for entityId, or nil when none is waiting. A component that records one is SAYING that a build states its fields and that it announces the replacement itself — so a rebuild states that type again rather than leaving it to the scene, which is what lets the announcement happen. Every other component is merged.

Parameters

  • entityId string — Runtime entity id of the owned entity.
if SceneBuild.previewedComponentType(id) == "SceneModule" then end

modules/SceneBuild/reconcile

reconcile(

Apply records to the scene under target, reusing the entities a previous reconcile left behind. A record that maps to a live entity updates THAT entity — same runtime id, so every reference to it survives the rebuild — and only a record with no live entity spawns one. The entity it spawns takes an id derived from owner, target and the record's identity, so every session sharing the world that runs the same build creates it under the same id, and the records one session's build writes land on the entity another session's build created. Entities the previous build held that this one no longer emits are despawned. A SceneModule placement the previous build stated on an entity and this one does not goes together with the entities it built beneath it. Name, transform, hidden, active, attributes, lifecycle mode, network scope, whether the entity's live state replicates, and components are all made to match the record, so a rebuild that drops a component or an attribute drops it from the scene. Each of them is a diff: what already matches the record is left exactly as it is, so a rebuild that changed nothing changes nothing — a running component keeps running and the scene stays clean. Only entities the build owns are touched: anything else under target is left exactly as it was. A component field holding an entity reference is resolved as the records are applied: a reference to an entity of the SAME build points at the entity this reconcile landed it on, and a reference to any other entity keeps pointing where it did. owner names the build. Every entity it places carries that name and the record's identity as attributes of its own, which is what lets a rebuild find the entities the last one placed without anything being remembered between them — the pair is in the scene, and a reload brings it back with the entity. Two builds sharing a target stay out of each other's way by using different owners. A record's identity is its place in the hierarchy — the chain of names from the build root down to it — so dropping, inserting or reordering a sibling leaves every other entity where it was. Several children of one parent sharing a name are told apart by their rank among those, counted in the order the builder created them.

local ids = SceneBuild.reconcile(records, root, "chairs")
local ids, created = SceneBuild.reconcile(records, layer, "build")
SceneBuild.reconcile(SceneBuild.run(build), layers.active, "build")

modules/SceneBuild/recordDrift

recordDrift(rec: any): { string }

Where an entity RECORD disagrees with what the build that placed it last stated: the question M.drift asks of the live scene, asked of a record the way scene.json and a scene's unsaved edits hold it. A record carries the build's statement in its own attributes, so a property that reads differently from that statement is one changed since the build ran, and an attribute the record holds that the build never stated is one added since. The record's name, transform and attributes are compared, numbers within the precision a saved record keeps.

Parameters

  • rec any (optional) — An entity record: name, transform = { position, rotation, scale } and attributes.
local drifted = SceneBuild.recordDrift(record)

modules/SceneBuild/run

run(builder: () -> ()): ({ any }, { Refusal })

Run builder inside an entity capture scope and return the records for every entity it created. The builder writes ordinary spawn code — real entity.spawn, real component.add, real loops — and the entities it creates are real for the duration of the call. They are composed into records and then despawned, so run leaves the scene untouched and hands back data. Reconciling that data into a scene is M.reconcile. The builder's entities are despawned even when it raises, so a failed build never leaks a half-built hierarchy into the scene. What a component the builder attached created while running its own lifecycle belongs to that component: the record names the COMPONENT, and the same lifecycle runs again wherever the record is put back, so the entities come from there rather than from records of their own. That covers a nested build — a placement the builder makes runs its own module and owns what it lands — and every other component that expands into entities. An operation the scope refused is refused BEFORE it lands, so the records describe the live world exactly as the builder left it, and a builder that ran to its end around a refusal somebody caught for it composes what it did make. Every such refusal comes back as the second return, naming the contributor it stopped, for the caller to report alongside what it baked.

Parameters

  • builder () -> () — Function taking no arguments; spawns whatever it wants.
local records, refused = SceneBuild.run(function() entity.spawn("chair") end)

modules/SceneBuild/sourceOf

sourceOf(owner: string): string?

The file that states the build named owner — what the reconcile running that build passed as opts.source. Nil for a build that has not run in this session and for one that named no source.

Parameters

  • owner string — Build name, as M.ownerOf reports it.
local file = SceneBuild.sourceOf(SceneBuild.ownerOf(id))

modules/SceneBuild/sourceWrittenHere

sourceWrittenHere(guid: string): boolean

Whether the latest write to the asset guid this session saw was its own, so a rebuild stated from it records what it lands. True for a write this session made and for an asset it has seen no write to; false when another session wrote it last, whose own rebuild records the change, and whose records reach this session.

Parameters

  • guid string — The asset's guid.
local record = SceneBuild.sourceWrittenHere(ref.guid)

modules/SceneBuild/states

states(owner: string, key: string): boolean?

Whether the build named owner placed an entity for the record identity key the last time it ran in this session. An entity carrying that pair that is gone from the scene was taken away by the build when the build no longer states it, and by an author when the build still does: the build's next run would put it back.

Parameters

  • owner string — Build name, as M.ownerOf reports it.
  • key string — The record identity, as the entity's buildKey attribute holds it.
local stillStated = SceneBuild.states(owner, key)

modules/SceneBuild/takePreview

takePreview(entityId: string): { [string]: any }?

Take the values M.notePreview recorded for entityId and clear them. Each set of values is read once — by whichever run of the build states that entity next.

Parameters

  • entityId string — Runtime entity id of the owned entity.
local set = SceneBuild.takePreview(id)

modules/SceneModuleAssetTypeRef/README

require("@builtin::assetTypes.sceneModule.behavior") -- SceneModuleAssetTypeRef

Per-instance methods exposed on every AssetRef<sceneModule>. Loaded lazily by assetType.shared.ref.

Usage: local SceneModuleAssetTypeRef = require("@builtin::assetTypes.sceneModule.behavior")

modules/SceneModuleAssetTypeRef/build

build(self, params: { [string]: any }?): ({ any }, { SceneBuild.Refusal })

Run this module's builder with params and return the entity records it built. The builder runs inside the entity capture scope, so it writes ordinary spawn code and what it created comes back as data — the scene is untouched by the call. SceneBuild.reconcile is what lands the records.

Parameters

  • self any (optional)
  • params { [string]: any }? (optional) — Per-placement input overrides, keyed by declared input name. An input the table omits resolves to its declared default. A builder that ran to its end around an operation the scope refused — one it caught itself, or one something between it and the refused call caught — hands back the records for what it did make, and every such refusal beside them: the operation was refused before it landed, so the records describe the scene as the builder left it.
local records, refused = moduleRef:build({ seed = 3 })

modules/SceneModuleAssetTypeRef/getInitScript

getInitScript(self): string?

Read the module's entry script (init.luau / init.lua) as raw text.

Parameters

  • self any (optional)
local src = moduleRef:getInitScript()

modules/SceneModuleAssetTypeRef/inputs

inputs(self): { InputSpec }

The inputs this module declares, sorted by name — each one's name, its Field kind, its declared default, and, for a closed-set input, the members it accepts. This is what an inspector renders as widgets and what a caller reads to learn which params a placement can set.

The script runs once per change to a file of the module or to a module it requires; later calls answer from that run. A script whose declarations depend on anything else it reads at its top level (world state, the clock, another asset's content) is answered from the run that last read it until one of those files changes. A script that fails to load runs again on the next call.

Parameters

  • self any (optional)
for _, i in ipairs(moduleRef:inputs()) do print(i.name, i.kind) end

modules/SceneModuleAssetTypeRef/inputsSignature

inputsSignature(ref: any): string

Text that moves when what inputs() answers for this module can have moved beyond the module's own files: the versions of the modules its script requires, as its last run found them. Empty before a run.

Parameters

  • ref any (optional) — The sceneModule.
parts[#parts + 1] = require(behavior).inputsSignature(moduleRef)

modules/SceneModuleAssetTypeRef/inspectDetail

inspectDetail(self): any

asset.inspect type-specific detail: { inputs } — the declared inputs, as :inputs() reports them. A module whose entry script cannot be loaded reports an empty input list rather than erroring.

Parameters

  • self any (optional)
local declared = asset.inspect(moduleRef).detail.inputs

modules/SceneModuleAssetTypeRef/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this module — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to, and what makes a sceneModule placeable wherever a scene-instantiable asset is accepted.

With target it builds with opts.params and reconciles the records under that entity, owned by that entity: the entities a previous build placed there name it as their owner and are UPDATED in place, so ids hold across every rebuild and across a reload.

With no target it places the module the way an author places any asset: one entity carrying a SceneModule component bound to this module. That component is what runs the build. The base opts — position, rotation, scale, name, temporary — place the root either way.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional) — { position?, rotation?, scale?, name?, temporary?, params?, handBack?, record? } — params seeds the module's declared inputs; handBack spreads the build's reconcile across frames, and record = false lands it in the live scene alone, as SceneBuild.reconcile takes them.
moduleRef:instantiate(owner, { params = { seed = 3 } })

modules/SceneModuleAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

A write inside a .sceneModule folder. Records which session wrote the module: the reload of every placement it reaches rebuilds in each session holding the placement, the session that made the write records what its rebuild lands, and every other one lands it in its live scene alone.

Parameters

  • ref any (optional) — The module whose folder was written.
  • change { [string]: any } — { path, asset, type, kind, origin }.
__zero_dispatch_asset_change("/zero/source/fort.sceneModule/init.luau")

modules/SceneModuleAssetTypeRef/onDelete

onDelete(ref: any)

Asset-type delete callback: the inputs a removed module declared are forgotten.

Parameters

  • ref any (optional) — The removed module.

modules/SceneModuleInspector/README

require("@builtin::modules.editor.scene_module_inspector") -- SceneModuleInspector

The SceneModule component's card on an entity: the bound module's identity, the file that states the placement, the module's DECLARED inputs as editable rows, and a Rebuild action. It replaces the card's generic settings section.

Usage: local SceneModuleInspector = require("@builtin::modules.editor.scene_module_inspector")

modules/SceneModuleInspector/buildFieldSpecs

buildFieldSpecs(declared: { any }, params: { [string]: any }?): { FieldSpec }

Build the editable field descriptors for a module's declared inputs: one spec per input, sorted by name, carrying the input's kind, its current value (the param override when the placement set one, else the declared default), and the members a closed-set input accepts. Pure — no UI, no entity access — so the field mapping is testable headless.

Parameters

  • declared { any } — The module's declared inputs, as moduleRef:inputs() reports.
  • params { [string]: any }? (optional) — The placement's current param-override table (may be nil).

modules/SceneModuleInspector/statedIn

statedIn(entityId: string): string?

The file the build that placed entityId is written in, for a placement a build made. That build states the placement's params every time it runs, so the rows below read as a preview of what it would state rather than as the placement's own settings. Nil for a placement an author made, whose params are the author's.

Parameters

  • entityId string — The inspected entity's id.
local file = SceneModuleInspector.statedIn(id)

modules/SceneRuntime/README

require("@builtin::assetTypes.scene.shared") -- SceneRuntime

The scene runtime — what a .scene folder does when it is loaded, saved, built, or swapped.

A <name>.scene/ folder carries scene.json (the entity arrangement), an optional build.luau (the code author's half — see scene.assetType's README), an optional entrypoint.luau, and a scene_dirty/ overlay of unsaved edits. Reading, composing, and writing all four is what this runtime does, and it lives here because that is the scene type's own behaviour.

FieldResponsibility
loaderReads scene.json v6/v7, composes canonical + dirty overlay, spawns the entities, runs the entrypoint.
saverPersists scene state as a delta overlay, promotes it to canonical, and answers what is still unsaved.
buildRuns a build.luau in an entity capture scope and composes what it spawned into records.
observeThe read side — per-scene counts and costs layers and loader write.
swapOrchestratorDrives multiplayer room transitions across a scene swap.
Fields resolve LAZILY. The prelude installs swapOrchestrator and starts
saver's dirty writer in a fixed order; a table that required all five on
load would take that ordering away from it.

Usage: local SceneRuntime = require("@builtin::assetTypes.scene.shared")

modules/ScriptValidator/README

require("@builtin::systems.worldValidation.scriptValidator") -- ScriptValidator

Per-file Luau / Lua script validator. Wraps lsp.check to return real parser + type-check diagnostics for a single script, on top of cheap textual sanity checks (read failure, empty file).

Each script gets the full LSP pass — lsp.check(path, opts) from the engine's embedded Luau LSP. Severity, line, column, code, and message are taken straight from the LSP and re-wrapped in the validator's Problem shape. The LSP diagnostics carry stable codes (unknown-global, type-error, parse-error, …) — we forward them verbatim so callers can severity = "error" or code = "parse-error" and get exactly the rows they expect. Two cheap textual checks run BEFORE the LSP is invoked:

  1. script.read_failed (error) — vfs.read returned nil (file deleted between scan and read, or unreadable).
  2. script.empty (warning) — zero non-whitespace content. These don't duplicate anything the LSP produces — the LSP is skipped when read fails (no source to feed it) and the empty check is informational about the file rather than the code.

Usage: local ScriptValidator = require("@builtin::systems.worldValidation.scriptValidator")

modules/ScriptValidator/validate

validate(script, opts)

Validate a single script. Combines two cheap textual checks (script.read_failed, script.empty) with the embedded Luau LSP's full diagnostic pass via lsp.check(path).

Parameters

  • script any (optional) — { path, name } script entry from vfsScanner.
  • opts any (optional) — Reserved for future use; currently ignored.
local problems = ScriptValidator.validate({ path = "/source/foo.luau", name = "foo.luau" })

modules/ScriptValidator/validateBatch

validateBatch(scripts, opts)

Validate a batch of scripts and flatten the per-script problem lists into one array.

Parameters

  • scripts any (optional) — Array of script entries from vfsScanner.
  • opts any (optional) — Reserved for future use.
local all = ScriptValidator.validateBatch(bucket.scripts)

modules/ServiceAssetTypeRef/README

require("@builtin::assetTypes.service.behavior") -- ServiceAssetTypeRef

Per-instance methods exposed on every AssetRef<service>. Loaded lazily by assetType.shared.ref. These act on the SPECIFIC service the reference points to — asset.resolve("mesh_gen","service"):invoke{...} runs THAT service.

Usage: local ServiceAssetTypeRef = require("@builtin::assetTypes.service.behavior")

modules/ServiceAssetTypeRef/balance

balance(self): (number?, string?)

Read the caller's current credit balance — the balance every service draws from. Returns (nil, errMsg) when not signed in.

Parameters

  • self any (optional)
local credits = serviceRef:balance()

modules/ServiceAssetTypeRef/cost

cost(self, operation: string?, input: { [string]: any }?): (number?, string?)

The declared credit cost of an operation (the provider's up-front estimate; the exact charge is returned on each generation). Defaults to the primary / only operation.

Parameters

  • self any (optional)
  • operation string? (optional) — Optional operation name.
  • input { [string]: any }? (optional)
local c = serviceRef:cost()

modules/ServiceAssetTypeRef/frameworkInputs

frameworkInputs(self, operation: string?): { { [string]: any } }

The inputs a call on this service takes on top of what the operation declares — operation, naming which of the service's operations to run, and asset_path, naming where the run writes what it generates. Each row reads as a declared input does: { name, type, required, desc, framework = true }.

Parameters

  • self any (optional)
  • operation string? (optional) — The operation these apply to. Omit for the service's default.
local extra = serviceRef:frameworkInputs("sfx")

modules/ServiceAssetTypeRef/getDefinition

getDefinition(self): string?

Read this service's declaration source (init.luau).

Parameters

  • self any (optional)
local src = serviceRef:getDefinition()

modules/ServiceAssetTypeRef/getReadme

getReadme(self): string?

Read this service's README.md body.

Parameters

  • self any (optional)
print(serviceRef:getReadme())

modules/ServiceAssetTypeRef/info

info(self): { [string]: any }

A one-line summary of this service: its offering, what it produces, the operations it offers, and which of them a call runs when it names none.

Parameters

  • self any (optional)
local i = serviceRef:info()

modules/ServiceAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { methods, description }, where methods is this service's declared operations — the top-level keys of its operations = { ... } table, parsed from the declaration's own source text — and description is the declaration's top-level summary, also parsed from source. A service with no readable declaration returns an empty methods list rather than erroring.

Parameters

  • self any (optional)
local methods = asset.inspect(serviceRef).detail.methods

modules/ServiceAssetTypeRef/invoke

invoke(self, input: { [string]: any }?): { [string]: any }

Run a generation for this service. Picks the operation from input.operation (or the service's only / primary operation), runs its declared pipeline as a background task, and returns immediately with a generation handle. An input the operation does not declare is refused here, before anything is charged. Watch the run with the services toolbox status tool (or the watch tool) until it reads completed; that row's asset is the finished, spawnable asset. Consumes credits — check :cost() and :balance() first.

Parameters

  • self any (optional)
  • input { [string]: any }? (optional) — { prompt, ..., operation?, asset_path? } — the operation's inputs.
local g = asset.resolve("mesh_gen","service"):invoke({ prompt = "a treasure chest" })

modules/ServiceAssetTypeRef/onCreate

onCreate(name: string, opts: { [string]: any }): { [string]: string }

Generic-creation hook for asset.create("service", name, opts). Returns the scaffold content files for a new service declaration.

Parameters

  • name string — Service identity (the instance name).
  • opts { [string]: any }
asset.create("service", "mesh_gen", { offering = "origozero/mesh_gen" })

modules/ServiceAssetTypeRef/operations

operations(self): { { [string]: any } }

List this service's callable operations and their declared inputs, output, and credit cost — the per-service surface to read before invoking. framework carries the inputs the CALL takes on top of what the operation declares (:frameworkInputs()).

Parameters

  • self any (optional)
for _, op in ipairs(serviceRef:operations()) do print(op.name, op.cost) end

modules/ServiceAssetTypeRef/resume

resume(self, record: { [string]: any }): { [string]: any }

Pick a recorded run of this service up where an earlier engine left it. Runs on world load for every run that did not finish; the record names the operation, the step in flight and the job it submitted, and the run continues from there without paying for the same work again.

Parameters

  • self any (optional)
  • record { [string]: any } — A run record as the framework's ServiceTask.durable returns it.
asset.resolve("mesh_gen","service"):resume(record)

modules/ServiceFramework/README

require("@builtin::assetTypes.service.shared") -- ServiceFramework

The runtime every .service instance declares against — reached via asset.containing(__FILE__).modules.shared. The TYPE owns all the machinery (metered invoke, the submit→poll→download→write pipeline, the async generation handle, error classification); an instance only declares its surface with Service.define{...}. A normal generation service is pure data — no per-instance logic.

Usage: local ServiceFramework = require("@builtin::assetTypes.service.shared")

modules/ServiceFramework/balance

balance(_self: any): ({ [string]: any }?, string?)

Read what the caller can spend on this service: spendable (the number an operation's cost must fit inside), with pool (the account balance) and agentRemaining (what is left of the caller's own allocation, when it works under one) behind it. Returns (nil, errMsg) when not signed in.

Parameters

  • _self any (optional)
local credits = asset.resolve("mesh_gen","service"):balance().spendable

modules/ServiceFramework/cost

cost(self: any, operation: string?, input: { [string]: any }?): (number?, string?)

The declared credit cost of an operation: what a run of it spends when the call names none of the options that cost extra. This is the up-front figure to budget against — an operation that makes several metered calls declares what the RUN costs, not what one of its steps does. An option documented as costing more adds to it, so a call naming one spends above this figure. Defaults to the primary/only operation.

Parameters

  • self any (optional)
  • operation string? (optional) — Optional operation name.
  • input { [string]: any }? (optional)
local c = asset.resolve("mesh_gen","service"):cost()

modules/ServiceFramework/define

define(spec: { [string]: any }): any

Declare a metered service. An instance's init.luau calls this with its surface — the offering identity and one or more operations — and returns the result. The TYPE runs the pipeline; the instance writes no machinery.

Parameters

  • spec { [string]: any } — { name, description, offering, output?, primary?, retry?, operations }. An operation's result is either a single file (bytes / url) or a SET (files), and a set assembles one asset: files = { { as, url } } fetches each map into the asset's own folder (an entry states bytes instead of url when a step already bound its content), pack = { { as, channels = { r, g, b, a } } } gathers named maps' channels into one raster where a slot samples several properties from one image (a channel is a constant 0-255, a file name for its red, or "file.g" for a stated channel), and bind = { slot = name } binds fetched and packed maps to the asset's slots. A map that reaches no slot is named on the job row and in the log. retry is { attempts?, delay? } — how many times a call refused for a reason that describes the moment (a rate limit, a busy gateway, a write conflict) is sent again, and the first wait in seconds between attempts, which doubles each time. Defaults to 4 attempts starting at 1 second. An operation may declare its own retry to override the service's.
return Service.define({ offering = "origozero/mesh_gen", operations = { generate = { ... } } })

modules/ServiceFramework/frameworkInputs

frameworkInputs(self: any, operation: string?): { { [string]: any } }

The inputs a call on this service takes on top of what the operation declares — operation, naming which of the service's operations to run, asset_path, naming where the run writes what it generates, and durable, stating whether that is filed on canonical /source while play runs. Each row is { name, type, required, desc, framework = true }, the same shape a declared input reads as. operation is required of a service that declares no default; asset_path is offered by an operation that writes a single file, and left out by one that authors a folder of them under a name of its own. The operation row's text names the operation THIS service runs when a call omits it, or the ones a call chooses between.

Parameters

  • self any (optional)
  • operation string? (optional) — The operation these apply to. Omit for the service's default.
local extra = asset.resolve("audio_gen","service"):frameworkInputs("sfx")

modules/ServiceFramework/info

info(self: any): { [string]: any }

A one-line summary of this service for discovery: its offering, what it produces, and the operations it offers.

Parameters

  • self any (optional)
local i = asset.resolve("mesh_gen","service"):info()

modules/ServiceFramework/invoke

invoke(self: any, input: { [string]: any }?): { [string]: any }

Run a generation for this service. Picks the operation from input.operation (or the service's primary / only operation), checks the operation's declared cost against the caller's balance/budget up front — raising with a legible reason if it can't be afforded — then runs the pipeline as a background task. An input the operation does not declare is refused here, before anything is charged. Returns immediately with a generation handle. Watch the task — the services toolbox status tool (or the watch tool) — until status == "completed", then the finished asset is that row's asset. The engine auto-imports the raw output into a spawnable asset (a mesh becomes a .bundle, an image a .texture, a sound a .soundClip); that imported asset, not the raw file, is what the completed result points to, ready to spawn. Consumes credits; check :cost() and :balance() first. A run files its asset where the caller's writes landed when it was requested: started outside the caller's play session, it files the asset on canonical /source even when it completes during play; started inside it, on the play shadow. input.durable states it outright (true: source, false: wherever the writes of that moment land). The finished status record carries durability, where what the run filed stands.

Parameters

  • self any (optional)
  • input { [string]: any }? (optional) — { prompt, ..., operation?, asset_path?, durable? }: the operation's inputs.
local g = asset.resolve("mesh_gen","service"):invoke({ prompt = "a treasure chest" })

modules/ServiceFramework/operations

operations(self: any): { { [string]: any } }

List this service's callable operations and their declared inputs, output, and credit cost — the per-service surface an agent reads before invoking. framework carries the inputs the call takes on top of what the operation declares (:frameworkInputs()). requireOneOf lists the groups of inputs a call must name at least one of.

Parameters

  • self any (optional)
for _, op in ipairs(asset.resolve("mesh_gen","service"):operations()) do print(op.name, op.cost) end

modules/ServiceFramework/resume

resume(self: any, record: { [string]: any }): { [string]: any }

Pick a run of this service up where an earlier engine left it. The record names the operation, its inputs, the step in flight, the bindings the steps before it produced, and the provider or gateway job that step already submitted; the run continues from there, waiting on that job rather than paying for the same work again, and lands its asset the way an uninterrupted run does. Runs on world load for every recorded run that did not finish.

Parameters

  • self any (optional)
  • record { [string]: any } — A run record as ServiceTask.durable returns it.
asset.resolve("mesh_gen", "service"):resume(record)

modules/ServiceFramework/resumeAll

resumeAll(): number

Pick up every recorded run that did not finish: the ones an earlier engine submitted and did not live to collect. Runs when the world has loaded. A run's service is waited for rather than assumed present, because a service can be authored in the world and register a moment after the library's own; each run resumes on its own task, so one that has to wait holds up none of the others.

require("@builtin::assetTypes.service.shared").resumeAll()

modules/ServiceRunAssetTypeRef/README

require("@builtin::assetTypes.serviceRun.behavior") -- ServiceRunAssetTypeRef

Per-instance methods on every AssetRef<serviceRun> — one generation's durable record: what it was asked to make, the gateway job doing the work, and the state that job reached.

Usage: local ServiceRunAssetTypeRef = require("@builtin::assetTypes.serviceRun.behavior")

modules/ServiceRunAssetTypeRef/finished

finished(self): boolean

Whether this run reached a terminal state. A run that is neither completed nor failed was still working when it was last heard from.

Parameters

  • self any (optional)
if not run:finished() then print("still out there") end

modules/ServiceRunAssetTypeRef/jobId

jobId(self): string?

The gateway job this run's work is running under. The job outlives the engine that submitted it, so this is what reaches an already-paid result when the run did not finish here.

Parameters

  • self any (optional)
local id = asset.resolve("gen_4f2a_0", "serviceRun"):jobId()

modules/ServiceRunAssetTypeRef/record

record(self): { [string]: any }

This run's record: { id, service, operation, prompt, jobId, status, progress, assetPath, error }.

Parameters

  • self any (optional)
local rec = asset.resolve("gen_4f2a_0", "serviceRun"):record()

modules/ServicesDescribe/README

require("@builtin::tools.services.shared.describe") -- ServicesDescribe

Fill in the descriptions a world's assets are still missing, many at a call. The assets are the ones a type asks to say what they are (a README, or about.yaml for a type) whose description is missing or still starts with the [[describe]] placeholder. A call of the describe service carries many assets, packed whole into as few calls as fit; the model answers each asset on a line of its own, and the text it returns for each is written into the description file the type declares, so the result is an ordinary file the author can edit.

Usage: local ServicesDescribe = require("@builtin::tools.services.shared.describe")

modules/ServicesDescribe/__configure

__configure(changes: { [string]: number })

Change how the tool waits, for a case that runs it against a stand-in.

Parameters

  • changes { [string]: number } — Any of parallel, pollSeconds, waitSeconds, settleSeconds, budget (bytes a call carries), maxAssets (assets a call carries).
Describe.__configure({ pollSeconds = 0.05 })

modules/ServicesDescribe/describe

describe(options: Options?): Report

Find, or fill in, the descriptions the assets of a world still need. find (the default mode) lists the assets that lack the description their type asks for, the file and keys each needs, and how many calls filling them makes (a call costs 1 credit, so that is also the price), and writes nothing. all fills every asset find lists, and assets fills the ones it names. The default scope is what blocks the current push; scope = "world" takes every asset the world holds that a type asks to describe. A call of the describe service carries many assets, packed whole into as few calls as fit, several calls at once; each asset is answered written, failed (with the reason) or skipped (with the reason), and a reply that is missing or malformed for one asset fails that asset only. What is written is an ordinary file the author can edit; only the description file or keys a type declares are written, text an author wrote is never replaced, and library, installed and machine-local content is never touched.

Parameters

  • options Options? (optional) — { mode?: "find" | "all" | "assets", scope?: "push" | "world", assets?: { string }, instructions?: string, examples?: { string } }. instructions is added to what the model is told, and examples names assets whose descriptions show the shape wanted.
local report = Describe.describe({ mode = "find" })

modules/ShaderAssetTypeRef/README

require("@builtin::assetTypes.shader.behavior") -- ShaderAssetTypeRef

Per-instance methods exposed on every AssetRef<shader>. Loaded lazily by assetType.shared.ref.

Usage: local ShaderAssetTypeRef = require("@builtin::assetTypes.shader.behavior")

modules/ShaderAssetTypeRef/compileKey

compileKey(key: string): string

Compile the shader key key (guid@checksum) for the materials waiting on it: the asset holding exactly that guid at that checksum (the guid's home when its bytes are that version, else a copy of that version held beside it) is compiled through the same path an edit runs. The engine calls this when a material first waits on a key, again when an asset under the key's guid arrives or changes, and again for a key answered "absent" once the asset index holds that version. While a world's content is being replaced, the index can state a version whose files are not all there to read; such a folder has nothing to compile yet, and the key is answered "absent" so it is asked for again.

Parameters

  • key string — The shader key, guid@checksum.
require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("shader").compileKey(key)

modules/ShaderAssetTypeRef/compileStatus

compileStatus(self): { status: string, error: string?, key: string?, pending: string? }

This shader's latest compile outcome, read without the engine log. Returns { status, error?, key?, pending? } for the key the shader compiles under (guid@checksum of the version at its path): status is "compiled", "failed" (with error, the compiler's message) or "pending" (not compiled yet, or compiled with part of its verdict still being reached, which pending names: the material resolve's gate runs off the frame thread). A properties.yaml that getProperties refuses reads "failed", with error the same refusal, because the shader is not compiled against it. Compilation is async (a write queues it), so a "pending" right after editing means check again next frame. A write succeeding and getProperties returning a schema do not mean the WGSL compiled; this does.

Parameters

  • self any (optional)
local s = shaderRef:compileStatus(); if s.status == "failed" then print(s.error) end

modules/ShaderAssetTypeRef/compiledWgsl

compiledWgsl(self): string?

The WGSL the shader compiler received for this shader, exactly as it received it. What getWgsl returns is the body as written; this is what that body became — the generated group(1) material interface above it, the domain's framework and entry points around it, every #include expanded and every #ifdef resolved. A compile error's line numbers, and the handle index naga prints where it has no name, are positions in THIS text, so a compileStatus() of "failed" is read against it. It answers for a failed compile as well as a clean one, and needs no material, entity or draw.

Parameters

  • self any (optional)
local s = shaderRef:compileStatus()
if s.status == "failed" then print(s.error, shaderRef:compiledWgsl()) end

modules/ShaderAssetTypeRef/domain

domain(self): string

The render domain this shader's body declares with its first // @domain: <name> line — what entry points and framework the engine builds around it: "surface" (the default), "post-process", "screen", "sky", "compute". A further spelling (postprocess, screenspace, ui) answers as the domain it names; a name the compiler refuses answers as written.

Parameters

  • self any (optional)
if shaderRef:domain() == "post-process" then print(shaderRef:getSpace()) end

modules/ShaderAssetTypeRef/getGlsl

getGlsl(self): string?

Read the GLSL body, when present. Returns nil for WGSL-only shaders.

Parameters

  • self any (optional)
local glsl = shaderRef:getGlsl()

modules/ShaderAssetTypeRef/getPasses

getPasses(self): { { name: string, entry: string, scale: number, inputs: { string } } }

The passes this post-process shader declares, from passes: in its properties.yaml, in the order they are drawn. Each entry is { name, entry, scale, inputs }; scale is 0 on the last pass, which draws at the view's size. A shader that declares none is one pass.

Parameters

  • self any (optional)
for _, p in ipairs(shaderRef:getPasses()) do print(p.name, p.entry, p.scale) end

modules/ShaderAssetTypeRef/getProperties

getProperties(self): { { [string]: any } }

List this shader's declared material properties (parsed from properties.yaml). Each entry is { name, type, default, min?, max? }. This is the editor-discovery surface, and the SAME parse the compile uses. Raises, naming the file, when properties.yaml is text the YAML decoder refuses (with the line) or one of its lists holds anything but a list of mappings (with the key).

Parameters

  • self any (optional)
for _, p in ipairs(shaderRef:getProperties()) do print(p.name, p.type) end

modules/ShaderAssetTypeRef/getSourceCode

getSourceCode(self): string?

Read the primary shader body (shader.wgsl) as raw text.

Parameters

  • self any (optional)
local src = shaderRef:getSourceCode()

modules/ShaderAssetTypeRef/getSpace

getSpace(self): string

The space this post-process shader's fragment() reads and returns, from space: in its properties.yaml: "light" (the default) or "display". The word the file declares, verbatim — an effect registered from a shader whose space: names something else renders in "light" and the engine log carries the spelling it was handed.

Parameters

  • self any (optional)
print(asset.resolve("@builtin::shaders.post.lut", "shader"):getSpace())

modules/ShaderAssetTypeRef/getWgsl

getWgsl(self): string?

Read the WGSL body directly, ignoring the GLSL fallback. Use when you need to detect "is this shader WGSL-native?" vs the generic getSourceCode lookup that auto-falls-back.

Parameters

  • self any (optional)
local wgsl = shaderRef:getWgsl()

modules/ShaderAssetTypeRef/listBindings

listBindings(self): { string }

List which top-level uniform / storage-buffer block names appear in the shader source — best-effort regex parse. Useful to cross-check against material:getPropertyNames() when debugging a "property doesn't exist" gap. Not a full WGSL parser; complex shaders with macros may report incomplete results.

Parameters

  • self any (optional)
for _, name in ipairs(shaderRef:listBindings()) do print(name) end

modules/ShaderAssetTypeRef/listMaterialsUsing

listMaterialsUsing(self): { string }

List the material identities currently bound to this shader, by scanning the registered material catalogue. O(n) over the material list; cache the result if you call it on a hot path.

Parameters

  • self any (optional)
for _, m in ipairs(shaderRef:listMaterialsUsing()) do print(m) end

modules/ShaderAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)compile the shader whenever its WGSL body or properties.yaml is written, and bring materials already bound to it onto the declared property list when that list changed shape. This is the ONLY thing that compiles a .shader — the implicit "WGSL written → recompile" path is gone — so it fires on the initial create (the template write) AND on every later edit, with no world reload. Convergent: see compileShader.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ShaderAssetTypeRef/onDelete

onDelete(ref)

Asset-type delete callback: release what a removed .shader folder registered. The teardown counterpart of onChange's compile: under the key the shader compiled under, its programs and their variants, its composed WGSL, its compile status and its diagnostic go with the asset, and so do the per-path records this module kept for it. A shader authored afresh at the same path compiles as a new one.

Parameters

  • ref any (optional)

modules/ShaderAssetTypeRef/onDependencyChange

onDependencyChange(ref, _change)

Dependency callback: a reference the shader names bound to its target — an #include of a module that did not exist when the shader last compiled. The shader compiles again against the module now there.

Parameters

  • ref any (optional)
  • _change any (optional)

modules/ShaderAssetTypeRef/previewMaterial

previewMaterial(self): ({ [string]: any }?, string?)

The material a surface preview paints this shader with: a transient one built from the shader at its properties.yaml defaults, which is the shader as it looks before anyone tunes it. Only a surface shader has a surface to paint; a post-process, sky or compute shader answers nil and the reason. ref:preview() paints the result on the mesh type.yaml names under preview: and releases the material after the render.

Parameters

  • self any (optional)
local surface, why = shaderRef:previewMaterial()

modules/ShaderAssetTypeRef/schemaParseCount

schemaParseCount(): number

How many times a shader's properties.yaml has been parsed in this session, across every shader and every surface that reads one — the editor-discovery method, the compile, the alias registration and the alias lookup. A read whose bytes match the parse already held answers from it and leaves this unchanged, so the count rises once per distinct revision of a file rather than once per read.

local m = require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("shader"); local before = m.schemaParseCount(); ref:getProperties(); print(m.schemaParseCount() - before)

modules/ShaderAssetTypeRef/setWgsl

setWgsl(self, source: string): boolean

Overwrite the WGSL body on disk. Hot-reload picks the new body up on the next frame so any material using this shader recompiles. Returns true on success.

Parameters

  • self any (optional)
  • source string — New WGSL source.
shaderRef:setWgsl(myWgsl)

modules/ShaderAssetTypeRef/shadingModel

shadingModel(self): string

Which shading model this shader's body uses, and with it what the engine can take the shader apart into.

"engine-lit" — the body exposes fn surface(...) -> PbrSurface. It hands the engine a surface (albedo, roughness, metallic, emissive, normal, occlusion) and the engine lights it, so the capture tool's PBR debug passes read real material channels.

"self-shading" — the body exposes fn fragment(...) -> vec4<f32> and returns the finished pixel. There is no separate albedo to read, so those same passes render what the body returns.

"unknown" — no readable body, or one that exposes neither entry point.

The test is the literal one the compiler applies to the same source, so this reports the model the shader was actually built as.

Parameters

  • self any (optional)
if shaderRef:shadingModel() == "self-shading" then print("albedo pass shows final colour") end

modules/ShaderModuleAssetTypeRef/README

require("@builtin::assetTypes.shaderModule.behavior") -- ShaderModuleAssetTypeRef

Per-instance methods exposed on every AssetRef<shaderModule>. Loaded lazily by assetType.shared.ref.

Usage: local ShaderModuleAssetTypeRef = require("@builtin::assetTypes.shaderModule.behavior")

modules/ShaderModuleAssetTypeRef/getSource

getSource(self): string?

Read this module's WGSL (module.wgsl) as raw text.

Parameters

  • self any (optional)
local src = moduleRef:getSource()

modules/ShaderModuleAssetTypeRef/onRegister

onRegister(self)

Initial-registration callback: file this module's WGSL under every name it answers to the moment the instance first registers, so a shader that #includes it resolves on its first compile rather than only after the module has been written once this session. Idempotent — the guard below skips a re-register of identical source.

Parameters

  • self any (optional)

modules/ShaderModuleAssetTypeRef/register

register(self): string?

Read this module's WGSL through the VFS and file what it holds now under every name it answers to, so a shader compiled next expands this text. Files unconditionally: the registry answers whether the text moved, and keeps the catalog generation still when the same bytes arrive again.

Parameters

  • self any (optional)
local wgsl = moduleRef:register()

modules/ShaderModuleAssetTypeRef/setSource

setSource(self, src: string): boolean

Overwrite this module's WGSL. Every shader that includes it recompiles on the next frame.

Parameters

  • self any (optional)
  • src string — New WGSL source.
moduleRef:setSource(myWgsl)

modules/Slots/README

require("@builtin::modules.slots") -- Slots

The rules of the slot family: which entities are slots, the path each slot answers to, which part fills it, whether a part fits it and why not, and building or emptying the part that fills it. The Slot, Assembly and Part components call into it; content reads it through them. A slot is a place on an entity that another part fills: an entity carrying a Slot component (its transform is where the part attaches), or a role socket of a Humanoid (righthand, head, ...), which a part fills the way Humanoid:attach seats an item. The nearest Assembly on or above a slot holds what fills it, by the slot's path: the slot's name, under the path of the slot whose part it stands in (engine/turbo). An Assembly on the root of a part built into a slot passes its slots to the Assembly holding that part; any other Assembly below holds its own, so an assembled object parented under another keeps its own configuration.

Usage: local Slots = require("@builtin::modules.slots")

modules/Slots/afterOperation

afterOperation(fn: () -> ())

Run fn now, or once the undoable operation open on this peer closes. A part is built from the configuration that names it, so the building stays out of the step that changed the configuration: undoing the step puts the configuration back and the parts follow it.

Parameters

  • fn () -> () — The work to run.

modules/Slots/apply

apply(entry: any, guid: string?): any?

Make a slot hold guid: leave the fill standing when it already holds that part, otherwise remove the fill and build the part in its place. Only this slot's branch changes.

Parameters

  • entry any (optional) — A slot entry.
  • guid string? (optional) — The guid of the part to hold, or nil to hold nothing.

modules/Slots/currentFill

currentFill(entry: any): (any?, any?)

The entity filling a slot right now, and the record it carries.

Parameters

  • entry any (optional) — A slot entry.

modules/Slots/decide

decide(config: any, entry: any): (string?, string)

What fills a slot under a configuration: the configured part, an explicit empty, or the slot's default.

Parameters

  • config any (optional) — The holder's configuration ({ [path] = guid | false }), or nil.
  • entry any (optional) — A slot entry.

modules/Slots/declaredFits

declaredFits(ref: any): ({ string }, string?)

The slot kinds a part declares it fits: the fits of the Part component at the root of a bundle, or the fits list of the asset's .metadata for any other asset.

Parameters

  • ref any (optional) — The part's AssetRef.

modules/Slots/discover

discover(rootId: string): { any }

Every slot at and under rootId, in breadth-first order: each Slot component, and each role socket of a Humanoid, with its path. A descendant whose own Assembly holds its slots (holdsOwnSlots) is left out with everything under it.

Parameters

  • rootId string — The entity to walk from (an Assembly's entity).

modules/Slots/fits

fits(entry: any, part: any): (boolean, string, any?)

Whether a part fits a slot, and why not. A part fits when it is an asset a scene can hold, the slot's kind is empty or among the kinds the part declares it fits, and the part carries every tag the slot names.

Parameters

  • entry any (optional) — A slot entry (discover), or any table with path, kind, tags.
  • part any (optional) — The part: an AssetRef, identity, guid or path.

modules/Slots/holderOf

holderOf(entityId: string): any?

The Assembly that holds what fills the slots at and under entityId: the nearest one on the entity or its ancestors, and when that one stands on the root of a part built into a slot, the one holding that part, climbing on the same way.

Parameters

  • entityId string — The entity to look from.

modules/Slots/holdsOwnSlots

holdsOwnSlots(e: any): boolean

Whether e carries an Assembly that holds its own slots: one on an entity that is no part built into a slot.

Parameters

  • e any (optional) — The entity.

modules/Slots/nameOf

nameOf(slot: any): string

The name a Slot component answers to: its name, or the name of the entity it is on when that is empty.

Parameters

  • slot any (optional) — The Slot component proxy.

modules/Slots/partRef

partRef(part: any): (any?, string?)

Resolve whatever names a part to its asset: an AssetRef, an identity, a guid or a path.

Parameters

  • part any (optional) — The part to resolve.

modules/Slots/pathOf

pathOf(slot: any): string

The path a Slot answers to under the Assembly that holds it.

Parameters

  • slot any (optional) — The Slot component proxy.

modules/SoundClipAssetTypeRef/README

require("@builtin::assetTypes.soundClip.behavior") -- SoundClipAssetTypeRef

Hooks for .soundClip assets. onCreate is the type's contribution to the generic asset.create("soundClip", name, opts) flow (mirroring texture.assetType). onChange keeps a managed container's data.zaud encoded payload in sync when its settings.yaml (or, for content published before it existed, its legacy .metadata settings) are edited, or its retained source audio is rewritten — the asset type reacting to writes inside its own instances (the type-level analogue of a component's onAssetReload). This hook owns every re-encode after the container exists. The settings themselves are declared in settings.luau and read/written through asset.settings / asset.setSettings.

Usage: local SoundClipAssetTypeRef = require("@builtin::assetTypes.soundClip.behavior")

modules/SoundClipAssetTypeRef/audition

audition(self): string?

Play this clip once through the mixer, unpositioned, replacing any clip auditioning. The voice plays on a temporary host entity the scene never saves and the entity listings leave out; the host is removed when the mixer reports the voice ended, and on the frame after stopAudition or a later audition.

Parameters

  • self any (optional)
clipRef:audition()

modules/SoundClipAssetTypeRef/auditionVoice

auditionVoice(self): any?

This clip's audition in progress: its host entity and the mixer's view of the voice (nil until the mixer has it).

Parameters

  • self any (optional)
local a = clipRef:auditionVoice(); if a and a.voice then print(a.voice.mixerState) end

modules/SoundClipAssetTypeRef/info

info(self): (any?, string?)

Read the header of this soundClip asset's data.zaud payload: how long the clip runs, the rate it plays at, how many channels it interleaves, which codec holds it, how many packets it is in, and where its loop points sit. Read off the header, so a script timing a caption to a line of narration pays a header read per clip where :pcm() would decode every sample. The sampleRate here is the rate the payload holds; :settings().sampleRate is the resample target the encode was ASKED for, where 0 means the source's own rate was kept.

Parameters

  • self any (optional)
local info = clipRef:info(); task.wait(info.durationMs / 1000)

modules/SoundClipAssetTypeRef/lastAuditionEnd

lastAuditionEnd(self): { host: string, reason: string }?

How this clip's most recent audition ended, once its host is gone: "ended" (the mixer reported it played through), "stopped", "replaced" (a later audition), "unvoiced" (no output, a mixer rendering nothing, or the mixer turned it away), "timeout" (the mixer rendered past the clip's length with no end reported), "neverArrived" (the voice never reached the mixer) or "hostGone".

Parameters

  • self any (optional)
local last = clipRef:lastAuditionEnd(); if last then print(last.reason) end

modules/SoundClipAssetTypeRef/loopSeam

loopSeam(self): (any?, string?)

Measure what this clip's samples do where a whole-clip loop wraps — the reading that says whether the clip can be looped without a click. The wrap's own step is reported against the step the signal ordinarily makes between neighbouring samples, so the figure is in the units the signal moves in and a quiet ambience reads the same way as a loud drone. A clip whose partials wrap reads near 1; one carrying a strike at its head and silence at its tail reads in the tens, and seamless is ratio <= threshold. Taken on the decoded payload, so it answers for what the codec left behind — including for a clip that arrived already encoded.

Parameters

  • self any (optional)
local seam = clipRef:loopSeam(); if not seam.seamless then print(seam.ratio) end

modules/SoundClipAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .soundClip/ instance, keeping the encoded data.zaud payload in sync. Rewriting the container's retained source audio — source.<ext>, or an imported file kept under its own name beside data.zaud — re-encodes from the new bytes; a rewrite carrying the bytes the payload was already encoded from, under the same encode settings, does nothing. A settings write — to the declared settings.yaml values file, or a legacy .metadata block for content published before it existed — re-encodes from the container's source when the encode-relevant fields (codec, bitrateKbps, vbr, sampleRate, forceMono, loopStart, loopEnd) actually changed: the retained source, or — for a PCM-baked container — the decoded data.zaud itself. loadType decides how the clip reaches the mixer, not what the payload holds, so it never drives a re-encode on its own — the generic per-asset reload dispatch already reaches every Audio component referencing this clip on any write here, and it asks clipRef:streams afresh on each rebuild, so the next source built from this clip picks up the new mode. data.zaud / README.md writes are ignored. The dispatcher's same-asset guard suppresses the re-dispatch of our own synchronous data.zaud write, so no loop forms.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any } — { path, asset, type, kind, origin, settings, settingsChanged } — path is the written file, asset the container folder, kind "edited"/"seeded", origin "local"/"remote", settings the validated values and settingsChanged what changed when path is the settings values file.

modules/SoundClipAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("soundClip", name, opts). Pure: returns the content-file map; the caller (asset.create) writes it to the authored destination (/source/<name>.soundClip/) and that write registers the soundClip asset.

Two input shapes, mirroring how clips originate:

  • PCM — { pcm, sampleRate, channels }: generated / decoded interleaved f32 samples, in any container audio.encodePcm reads — a buffer, a binary string of little-endian f32, or a flat number array (no source file). Encoded to the canonical data.zaud payload (the engine's ZAUD format) via audio.encodePcm. data.zaud is the primary the runtime decodes — so a generated clip becomes a persistent, reload-stable asset addressed by guid.
  • ENCODED AUDIO — { bytes, ext }: OGG / MP3 / WAV / FLAC source bytes, kept verbatim as source.<ext>, with data.zaud encoded from them against the instance's settings.

Parameters

  • name string — SoundClip identity (the instance name).
  • opts CreateOpts
asset.create("soundClip", "beep", { pcm = buffer.create(960 * 4), sampleRate = 48000, channels = 1 })
asset.create("soundClip", "music", { bytes = oggBytes, ext = "ogg" })

modules/SoundClipAssetTypeRef/onDelete

onDelete(ref: any)

Asset-type delete callback: a removed clip's audition ends, and how its last audition ended is forgotten.

Parameters

  • ref any (optional) — The removed clip (its guid when the removal knew it, and its path).

modules/SoundClipAssetTypeRef/pcm

pcm(self): (string?, number?, number?)

Decode this soundClip asset's data.zaud payload into its samples, the rate they play at, and the number of channels they interleave.

Parameters

  • self any (optional)
local pcm, sr, ch = clipRef:pcm()

modules/SoundClipAssetTypeRef/setSettings

setSettings(self, patch: { [string]: any })

Write a partial settings patch. The patch is checked against this type's declared schema; a violation refuses the write naming every problem. A valid write reaches onChange: an encode-relevant field (codec, bitrateKbps, vbr, sampleRate, forceMono, loopStart, loopEnd) re-encodes data.zaud; loadType never re-encodes — it decides how the next Audio source built from this clip reaches the mixer.

Parameters

  • self any (optional)
  • patch { [string]: any } — Any subset of the schema's fields.
clipRef:setSettings({ bitrateKbps = 64, forceMono = true })

modules/SoundClipAssetTypeRef/settings

settings(self): { [string]: any }

Read this soundClip asset's settings, with every declared default filled in. The settings say how the clip is encoded and where its loop points sit; what its samples do where a whole-clip loop wraps is clipRef:loopSeam().

Parameters

  • self any (optional)
if clipRef:settings().forceMono then ... end

modules/SoundClipAssetTypeRef/stopAudition

stopAudition(self): boolean

Stop this clip's audition: it stops being the audition in progress at once, and its host is removed on the next frame.

Parameters

  • self any (optional)
clipRef:stopAudition()

modules/SoundClipAssetTypeRef/streams

streams(self, bytes: string?): boolean

Whether a source playing this clip holds it streaming: loadType "streaming" does, "decompressOnLoad" does not, and "auto" streams a clip that runs longer than 5 seconds.

Parameters

  • self any (optional)
  • bytes string? (optional) — The clip's data.zaud payload when the caller already holds it.
local s = ecs.AudioSource{ clip = c, streaming = c:streams(bytes) }

modules/StyleAssetTypeRef/README

require("@builtin::assetTypes.style.behavior") -- StyleAssetTypeRef

Per-instance methods exposed on every AssetRef<style>. Loaded lazily by assetType.shared.ref.

Usage: local StyleAssetTypeRef = require("@builtin::assetTypes.style.behavior")

modules/StyleAssetTypeRef/getInitScript

getInitScript(self): string?

Read the style's entry script as raw text.

Parameters

  • self any (optional)
local src = styleRef:getInitScript()

modules/StyleAssetTypeRef/getReadme

getReadme(self): string?

Read the style's README.

Parameters

  • self any (optional)
print(styleRef:getReadme())

modules/StyleAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { tokenCategories }, the top-level keys of the style's own M.tokens = { ... } table literal, parsed from the entry script's source text via luau_introspect.tableLiteralKeys — never requires the style. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A style with no readable entry script returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local tokenCategories = asset.inspect(styleRef).detail.tokenCategories

modules/StyleAssetTypeRef/loadTheme

loadTheme(self): any

Load the style module and return its exported theme table. Raises a Luau error tagged with the style identity if the require fails.

Parameters

  • self any (optional)
local theme = styleRef:loadTheme()

modules/Test.valueText/README

require("@builtin::assetTypes.testSuite.shared.valueText") -- Test.valueText

How the test framework renders a value nested inside a table for test output: structurally, with bounded breadth, depth and string length, in a deterministic order.

Usage: local Test.valueText = require("@builtin::assetTypes.testSuite.shared.valueText")

modules/Test.worldReadings/README

require("@builtin::assetTypes.testSuite.shared.worldReadings") -- Test.worldReadings

Readings the test framework takes of engine-wide state around a test: each facet a queued teardown moves (entities, layers, render features, render targets, loads in flight), and which editor chrome screens the window shows.

Usage: local Test.worldReadings = require("@builtin::assetTypes.testSuite.shared.worldReadings")

modules/Test.worldReadings/residentRenderTargets

residentRenderTargets(): number

Readings the test framework takes of engine-wide state around a test: each facet a queued teardown moves (entities, layers, render features, render targets, loads in flight), and which editor chrome screens the window shows.

modules/TestSuiteAssetTypeRef/README

require("@builtin::assetTypes.testSuite.behavior") -- TestSuiteAssetTypeRef

Per-instance methods exposed on every AssetRef<testSuite>. Loaded lazily by assetType.shared.ref. These methods act on the SPECIFIC asset instance the reference points to — asset.resolve(id, "testSuite"):run() runs only THAT suite. Running every suite is the tests toolbox's job (it iterates asset.list("testSuite") and calls :run() on each); it is deliberately not offered here.

Usage: local TestSuiteAssetTypeRef = require("@builtin::assetTypes.testSuite.behavior")

modules/TestSuiteAssetTypeRef/getReadme

getReadme(self): string?

Read this suite's README.md body.

Parameters

  • self any (optional)
print(suiteRef:getReadme())

modules/TestSuiteAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { testCount, testNames }, parsed from Test.it("<name>", ...) calls in this suite's own init.luau text. A string scan, not a suite run — inspecting a suite never registers or executes its tests. A suite with no readable init.luau returns an empty testNames list rather than erroring.

Parameters

  • self any (optional)
local testNames = asset.inspect(suiteRef).detail.testNames

modules/TestSuiteAssetTypeRef/run

run(self, opts: { quiet: boolean? }?): any

Run THIS test suite (only this one) and return its results. Refuses in play mode — tests are an edit-mode / authoring concern, not gameplay. The run is cooperative (the framework yields a frame between tests) so it never blocks the engine or trips the execute watchdog.

Parameters

  • self any (optional)
  • opts { quiet: boolean? }? (optional) — Optional { quiet }.
local r = asset.resolve("vfs", "testSuite"):run()

modules/TestSuiteAssetTypeRef/tests

tests(self): { { name: string, skip: boolean } }

List the tests this suite declares (loads THIS asset's init.luau to read its registered Test.it / Test.skip names; does not run them).

Parameters

  • self any (optional)
for _, t in ipairs(suiteRef:tests()) do print(t.name) end

modules/TextureAssetTypeRef/README

require("@builtin::assetTypes.texture.behavior") -- TextureAssetTypeRef

Hooks for .texture assets. onCreate is the type's contribution to the generic asset.create("texture", name, opts) flow (mirroring material.assetType). onChange keeps a MANAGED container's data.ztex encoded payload in sync when its settings.yaml (or, for content published before it existed, its legacy .metadata settings) are edited, or its retained source image is rewritten — the asset type reacting to writes inside its own instances (the type-level analogue of a component's onAssetReload). The loose-image → container PROMOTE step is the separate texture.importer (loose-write seam); this hook owns every re-encode AFTER the container exists. The settings themselves are declared in settings.luau and read/written through asset.settings / asset.setSettings.

Usage: local TextureAssetTypeRef = require("@builtin::assetTypes.texture.behavior")

modules/TextureAssetTypeRef/compressionFor

compressionFor(settings: { [string]: any }, isLegacy: boolean): { compression: string, textureType: string }

What a texture is compressed as, and as what type, given its settings. isLegacy says the settings predate compression (stored before it was a setting, whatever older format they name): such a texture is compressed as it should have been — a nearest-filtered one is pixel art, a sprite held exactly as rgba8; a high-precision one (rgba16, rgba32f) keeps its format; every other one is uastc. Settings that name compression keep it, so a texture whose settings say rgba8 is never compressed. The one decision the local-edit heal (settingsHealed) and an on-device compressor of legacy content both make.

Parameters

  • settings { [string]: any } — The texture's settings, as a read reports them.
  • isLegacy boolean — Whether the stored settings predate compression.
M.compressionFor({ compression = "rgba8", filter = "nearest", textureType = "color" }, true) -- { compression = "rgba8", textureType = "sprite" }

modules/TextureAssetTypeRef/handle

handle(self)

Put this texture asset on the GPU under its own guid and return its TextureHandle at once. The asset's bytes are decoded off the frame and the texture lands on the device when the decode finishes, a frame or more later: a material naming the guid draws the shader's default for that slot until then and rebinds when it arrives, and renderer.texture.isResident(guid) reports the arrival. The handle is cached on the interned ref's shared runtime table for as long as something holds the one it handed out, so every consumer and material slot naming the asset over that time gets the SAME handle back → ONE GPU entry. Once the last of them lets go, the next call asks for the asset again and is answered with a handle on the texture the device already holds under that guid — which is also what a mode flip's runtime wipe leads to. The material binds this handle's guid; it never creates the GPU texture itself.

CPU lifecycle: governed by the asset's declared keepCpu setting (texRef:setSettings({ keepCpu = true })). By DEFAULT (false) the decoded pixels are dropped right after the GPU upload (the GPU handle holds no data → no double-memory cost); with keepCpu = true the CPU store keeps them for later pixel reads / edits. A keepCpu change reaches an already-materialised texture too — onChange's applyKeepCpu loads or drops the CPU copy the moment the setting changes, not only here.

Parameters

  • self any (optional)
local h = texRef:handle() -- bind h.guid on a material; it draws once resident

modules/TextureAssetTypeRef/info

info(self): (any?, string?)

Read the header of this texture asset's compiled data.ztex payload: the pixel format it was compiled to, as its compression and colorSpace too, how many texels it is across and down, how many mip levels and array layers it carries, the sampler recorded in it (filter, wrapU, wrapV, anisotropy), and, for a sequence, each layer's display time and the length of one pass. Read off that payload's own header, so a compiled texture answers how big it is for a header read where :load() decodes its pixels and :handle() uploads them. A container whose primary is a source image answers (nil, err), and :load() reads its size. The compression and colorSpace here are what the payload holds; :settings() holds what the encode was ASKED for.

Parameters

  • self any (optional)
local i = texRef:info(); print(i.width .. "x" .. i.height)

modules/TextureAssetTypeRef/load

load(self)

Load this .texture asset's pixels into the guid-keyed CPU store and return a CPU handle for per-pixel access (no GPU readback). The handle holds no pixels — only the guid, dims, texel format, and the read/write/encode/ unload ops. The pixels stay at the precision they were authored with: handle.format is "rgba8", "rgba16" or "rgba32f", and :readPixel reports channels in that format's own units. Upload to the GPU with renderer.texture.create(handle); the DEFAULT is to handle:unload() after. The handle's :encode() re-encodes the (possibly edited) pixels into a fresh ZTEX blob for asset.create("texture", …), at the same format.

Parameters

  • self any (optional)
local cpu = texRef:load(); local r,g,b,a = cpu:readPixel(3, 4); cpu:unload()

modules/TextureAssetTypeRef/normalizeSettings

normalizeSettings(values: { [string]: any }, stored: { [string]: any }?): { [string]: any }

Map every older spelling a stored file or a write may carry to the fields and members the schema declares: an older filter keyword to linear / nearest, an anisotropy given as a number to its string (8 → "8"), compression = "bc7" to "uastc", maxDimension = 0 (no cap) to 8192, and format + srgb to colorSpace + compression (an explicit srgb sets the colour space of an rgba8, uastc or bc7 format; the high-precision formats stay linear). An srgb given without a format applies to the compression the same table names, else to the asset's stored one, so { srgb = true } leaves an rgba16 texture linear. A table that names both an older field and its replacement keeps the replacement's value. A format no schema declared stays under its own name, so the schema check refuses it naming the field. Returns a new table.

Parameters

  • values { [string]: any } — A settings table in any spelling.
  • stored { [string]: any }? (optional) — The asset's current values, when values is a patch over them.
M.normalizeSettings({ format = "bc7_srgb", maxDimension = 0 }) -- { colorSpace = "srgb", compression = "uastc", maxDimension = 8192 }

modules/TextureAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .texture/ instance, keeping the encoded data.ztex payload in sync. Rewriting the container's retained source image — source.<ext>, or an imported image kept under its own name beside data.ztex — re-encodes from the new bytes; a rewrite carrying the bytes the payload was already encoded from does nothing. A settings change that touches only the sampler fields (filter, wrapU, wrapV, anisotropy) on a container holding data.ztex rewrites the payload's header with them (renderer.texture.withSampler): every level keeps its bytes and no preview is rendered, because no pixel changed. A settings change that touches an encode-affecting field (textureType, colorSpace, compression, generateMipmaps, maxDimension, alpha, alphaIsTransparency) re-encodes from the container's source: the managed source.<ext>, a plain container's image primary, or — for a raw-pixel container — the data.ztex payload itself, re-encoded whole (every layer and its display time kept) and read back with straight alpha. A raw-pixel container's payload is its only source, so what an earlier encode discarded stays discarded: after alpha = "none" its alpha is opaque, and after alphaIsTransparency its transparent texels keep the colour dilation gave them. Whatever rewrote the payload — a re-encode here, or a data.ztex written directly or arriving from a peer — the texture this engine has on the GPU under the asset's guid is uploaded again from it, so every material, UI image and effect sampling the guid draws the new pixels, size, format, colour space, mip chain, filter, wrapU, wrapV and anisotropy, and a resident CPU copy is decoded again. A keepCpu change acts on the texture's CPU residency instead (applyKeepCpu) and never re-encodes on its own — and, because CPU residency is PER-ENGINE state, it acts for EVERY origin: a synced peer's write says exactly as much about its own process as a local one does about this one. Everything that WRITES persisted content — the source re-encode and the settings-driven re-encode — stays local-origin-only; a peer receives that content through the ordinary sync instead of reproducing the write itself. It fires on a write to the declared settings.yaml values file only when change.settingsChanged is non-empty — an identical rewrite carries an empty diff and does nothing — or on a legacy .metadata settings block for content published before the values file existed, which carries no diff and so re-encodes unconditionally. README.md writes are ignored. The dispatcher's same-asset guard suppresses the re-dispatch of our own synchronous data.ztex write, so no loop forms.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any } — { path, asset, type, kind, origin, settings, settingsChanged } — path is the written file, asset the container folder, kind "edited"/"seeded", origin "local"/"remote", settings the validated values and settingsChanged what changed when path is the settings values file.

modules/TextureAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("texture", name, opts). Pure: returns the content-file map; the caller (asset.create) writes it to the authored destination (/source/<name>.texture/) and that write registers the texture asset. Disk-only — nothing is uploaded to the GPU here.

Two input shapes, mirroring how textures originate:

  • RAW PIXELS — { rgba, width, height }: a generated / decoded pixel buffer (no source image). Encoded to the canonical data.ztex payload (the engine's ZTEX format; zero_texture::blob) via __texture.encode, at the resolved opts.settings.compression and opts.settings.colorSpace. data.ztex is the primary the renderer uploads — so a generated texture becomes a persistent, reload-stable asset addressed by guid, never an ephemeral GPU handle. compression picks the on-disk storage: "uastc" blocks (a new texture's default), 8 bits per channel with "rgba8" (a sprite's), or "rgba16" / "rgba32f" for a data raster (height / displacement field, baked lightmap, imported elevation) whose values 8 bits would quantize. A cook that drops texels (maxDimension, the power-of-two resample) keeps every one as source.ztex, so a later settings change re-encodes from them.
  • ENCODED IMAGE — { bytes, ext }: PNG / JPEG / WebP / … source bytes, stored verbatim as the <name>.<ext> primary (the renderer image-decodes on upload; the same shape asset.create produced before).

Parameters

  • name string — Texture identity (the instance name).
  • opts CreateOpts
asset.create("texture", "skyGradient", { rgba = pixels, width = 256, height = 256 })
asset.create("texture", "terrain_height", { rgba = heights, width = 512, height = 512, settings = { compression = "rgba16" } })
asset.create("texture", "pixel_icon", { rgba = pixels, width = 16, height = 16, settings = { compression = "rgba8", filter = "nearest" } })
asset.create("texture", "bricks", { bytes = imageBytes, ext = "jpg" })

modules/TextureAssetTypeRef/setSettings

setSettings(self, patch: { [string]: any })

Write a partial settings patch. The patch is checked against this type's declared schema; a violation refuses the write naming every problem. A valid write reaches onChange: an encode-affecting field (textureType, colorSpace, compression, generateMipmaps, maxDimension, effort, uastcRdo, alpha, alphaIsTransparency) re-encodes data.ztex; a write naming only sampler fields (filter, wrapU, wrapV, anisotropy) rewrites the payload's header and keeps every level's bytes; keepCpu loads or drops the CPU copy instead, without touching the encoded bytes.

Parameters

  • self any (optional)
  • patch { [string]: any } — Any subset of the schema's fields.
texRef:setSettings({ filter = "nearest" })

modules/TextureAssetTypeRef/settings

settings(self): { [string]: any }

Read this texture asset's settings, with every declared default filled in. The table also answers the older format and srgb names, read from colorSpace + compression: format is "srgb", "linear", "uastc_srgb", "uastc", "rgba16" or "rgba32f", and srgb is true for the srgb colour space. Both are read-only: a write names colorSpace / compression.

Parameters

  • self any (optional)
if texRef:settings().filter == "nearest" then ... end

modules/TextureAssetTypeRef/settingsCorrections

settingsCorrections(values: { [string]: any }, _name: string, _opts: { [string]: any }?): ({ [string]: any }?, { [string]: string }?)

Cross-field settings correction Settings.applyCorrections applies on every settings write — once a create's layers (schema defaults, this type's own settingsDefaults, the Preset Manager, an importer, the caller) have merged, and on every later asset.setSettings: a high-precision compression stores the raster verbatim and linear, so it carries no mip chain, no max-dimension downsample and no sRGB colour space — the encoder refuses all three structurally. Overrides generateMipmaps / maxDimension / colorSpace to the only values a high-precision compression allows. Whether that lands SILENTLY or REFUSES the write is applyCorrections' call, based on whether the writer named the field being overridden — the second return here supplies the "why" for that refusal message, naming the compression that made it necessary. Reads values; returns the correction rather than writing to it.

Parameters

  • values { [string]: any } — The fully resolved settings, after every layer has merged.
  • _name string
  • _opts { [string]: any }? (optional)
local fix, why = M.settingsCorrections({ compression = "rgba16", generateMipmaps = true }, "height", {})

modules/TextureAssetTypeRef/settingsDefaults

settingsDefaults(name: string, opts: { [string]: any }?, asked: { [string]: any }?): { [string]: any }

The create-time settings this type contributes to a new asset's values, layered above the schema's own declared defaults and below every other layer (Preset Manager, importer, caller) — resolveForCreate calls this once per create, so an explicit override from any later layer replaces what it returns here. textureType follows the created name's own data-map convention: "normalMap" for a _normal/_nrm/_n/... suffixed name, "mask" for a _rough/_metal/_ao/_height/_disp/ _mask/... one, "color" otherwise; the colour space then follows the type (settingsDerivedDefaults). A new texture is uastc with a maxDimension of 4096; a sprite (a textureType a layer above names) is rgba8, drawn exactly as given; a high-precision source (a 16-bit PNG, a Radiance .hdr) leaves compression to the layers above. When opts.ztex is a pre-encoded payload whose header reads, its colorSpace, compression, filter, wrapU, wrapV, anisotropy, textureType, alpha and alphaIsTransparency are what was actually written, and the settings describe what is stored.

Parameters

  • name string — The instance name being created.
  • opts { [string]: any }? (optional) — The creation opts passed to asset.create.
  • asked { [string]: any }? (optional) — What the layers above this one (Preset Manager, importer, caller) name.
local layer = M.settingsDefaults("brick_normal", {}, {})  -- { textureType = "normalMap", compression = "uastc", maxDimension = 4096 }

modules/TextureAssetTypeRef/settingsDerivedDefaults

settingsDerivedDefaults(

The fields whose default follows textureType, for a write that set or changed it: colorSpace (srgb for color and sprite, linear for normalMap and mask), alphaIsTransparency (true for sprite) and wrapU / wrapV (clamp for sprite — drawn whole — and repeat otherwise). The settings framework applies a returned field only where the same write did not name it, so a create or patch that names its own colour space keeps it.

M.settingsDerivedDefaults({ textureType = "sprite" }, {}, { textureType = true }) -- { colorSpace = "srgb", alphaIsTransparency = true, wrapU = "clamp", wrapV = "clamp" }

modules/TextureAssetTypeRef/settingsHealed

settingsHealed(values: { [string]: any }, stored: { [string]: any }): { [string]: any }?

The settings a local edit heals for a texture whose stored settings predate compression (they name no compression): the compression and texture type compressionFor answers for it. maxDimension heals as an older "no cap" reads (normalizeSettings), so a healed 8K texture stays 8K. Nothing else changes. Settings that name compression heal nothing.

Parameters

  • values { [string]: any } — The texture's settings, as a read reports them.
  • stored { [string]: any } — The settings exactly as stored.
M.settingsHealed({ compression = "rgba8", textureType = "color", filter = "linear" }, { format = "srgb" }) -- { compression = "uastc", textureType = "color" }

modules/TextureRef/README

require("@builtin::modules.texture_ref") -- TextureRef

The GPU key a material's texture slot binds by, resolved from whatever form the author wrote it in. Every path that accepts a texture reference — the .material assetType, renderer.material.create, renderer.material.setTexture — resolves through here, so the same string binds the same texture wherever it is written.

Usage: local TextureRef = require("@builtin::modules.texture_ref")

modules/TextureRef/isProcedural

isProcedural(ref: string): boolean

Whether a reference is one the GPU texture cache resolves on its own (color: / default: / runtime:), so it must be passed through untouched rather than looked up as an asset.

Parameters

  • ref string — The reference string.
TextureRef.isProcedural("color:1,0,0,1") -- true

modules/TextureRef/resolve

resolve(ref: any, altIdentity: string?, site: string?): (string, string)

Resolve a texture reference to the resident GPU key the renderer binds a slot by, materialising the texture on the way. Accepts a guid, an asset identity, a bare name, a .texture path, or the image path a texture was imported from. color: / default: / runtime: forms and live GPU handles (a video frame, a render target) pass through untouched. An array:a|b|c reference resolves each layer the same way, materialising every layer texture the array is assembled from, and answers the array keyed by the layers' resolved keys.

Parameters

  • ref any (optional) — The authored reference.
  • altIdentity string? (optional) — Optional second candidate, tried when ref resolves to nothing — a slot's stable identity, so a binding whose guid was orphaned by a delete + recreate still finds the texture the author named.
  • site string? (optional) — Optional VFS path of the asset whose file holds the reference — a material resolving its own mat.yaml slots. A package-relative ~.textures.x then expands against the package enclosing that asset, and a reference that asset's .refs recorded resolves to the texture recorded there. A site inside a carried mount resolves the reference within that mount.
TextureRef.resolve("wall.texture") -- "9f2c…", "asset"
TextureRef.resolve("array:grass.texture|rock.texture") -- "array:9f2c…|41ab…", "array"

modules/TextureRef/unresolvedMessage

unresolvedMessage(ref: string, what: string): string

The message describing a reference that names no texture. Bound anyway, the slot renders the shader's declared fallback, so the caller says this rather than letting the material render as though nothing was asked for.

Parameters

  • ref string — The unresolved reference.
  • what string — The call being made, for the message's subject.
TextureRef.unresolvedMessage("sky.jpg", "renderer.material.setTexture")

modules/ToolAssetTypeRef/README

require("@builtin::assetTypes.tool.behavior") -- ToolAssetTypeRef

Per-instance methods exposed on every AssetRef<tool>. Loaded lazily by assetType.shared.ref.

Usage: local ToolAssetTypeRef = require("@builtin::assetTypes.tool.behavior")

modules/ToolAssetTypeRef/getDefinition

getDefinition(self): string?

Read the tool's definition — its init.luau source, which carries the typed signature and the --!desc/--!arg/--!return/--!example docstring that together form the tool's schema.

Parameters

  • self any (optional)
local raw = toolRef:getDefinition()

modules/ToolAssetTypeRef/getInitScript

getInitScript(self): string?

Read the tool's entry script as raw text.

Parameters

  • self any (optional)
local src = toolRef:getInitScript()

modules/ToolAssetTypeRef/getReadme

getReadme(self): string?

Read the tool's README body.

Parameters

  • self any (optional)
print(toolRef:getReadme())

modules/ToolAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { desc, args, returns, examples, signature }, parsed from the tool's own entry script (getInitScript) via luau_introspect.docstrings — the same --!desc/--!arg/--!return/--!example docstring that documents the tool's schema. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A tool with no readable entry script returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local args = asset.inspect(toolRef).detail.args

modules/ToolAssetTypeRef/run

run(self, args: any): any

Invoke the tool's entry script. Requires the tool's identity and calls the first function it exports — the canonical function M.<name>(args) shape every tool ships. args is forwarded verbatim. Raises a Luau error (tagged with the tool's identity) on load or invocation failure rather than returning a swallowed nil — failing loudly matches the rest of the engine's tool dispatch surface.

Parameters

  • self any (optional)
  • args any (optional) — Optional table of arguments to pass to the tool's entry.
toolRef:run({ subject = "world" })

modules/ToolAssetTypeRef/wrap

wrap(fn: (...any) -> ...any, regionName: string): (...any) -> ...any

Standardize a single tool function into a ZmToolResult-returning call. THIS is where the tool-result contract lives — the engine's tool-bind paths (boot + runtime) apply wrap to every tool function so the result type is a system guarantee, never the individual tool's choice. The tool body returns its raw value, returns the Lua failure convention (nil, reason), or raises with error(msg); wrap produces the canonical envelope { ok, value | error, durationMs, tool } from all three — a (nil, reason) return becomes { ok = false, error = reason } instead of silently dropping the reason and reporting a bare success with no value. Timing is measured with the engine profiler: a region named for the tool is opened when the call is invoked and closed when it returns, so each invocation is both timed (region elapsed -> durationMs) and visible as a profiler region. Each envelope is also reported into the running task's tool-result buffer, which execute() auto-surfaces as the response's toolResults.

A tool that states MORE than one value on success — a path AND what that path holds — keeps them: the envelope carries the first as value, and every value past it follows the envelope out, so local a, b, c = tools.use(...) reads the tool the way the tool's own signature declares it. Keeping only the first turns every such declaration into a promise nobody can read.

Parameters

  • fn (...any) -> ...any
  • regionName string

Returns ...any

modules/ToolAssetTypeRef/wrapToolbox

wrapToolbox(box: { [string]: any }, namespace: string): { [string]: any }

Wrap every function on a toolbox table so each tool call returns a ZmToolResult (see wrap). Non-function fields pass through untouched.

Parameters

  • box { [string]: any }
  • namespace string

modules/ToolboxAssetTypeRef/README

require("@builtin::assetTypes.toolbox.behavior") -- ToolboxAssetTypeRef

Per-instance methods exposed on every AssetRef<toolbox>. Loaded lazily by assetType.shared.ref.

Usage: local ToolboxAssetTypeRef = require("@builtin::assetTypes.toolbox.behavior")

modules/ToolboxAssetTypeRef/getReadme

getReadme(self): string?

Read the toolbox's README.md body.

Parameters

  • self any (optional)
print(toolboxRef:getReadme())

modules/ToolboxAssetTypeRef/hasShared

hasShared(self): boolean

Check whether the toolbox ships a shared.module/.

Parameters

  • self any (optional)
if toolboxRef:hasShared() then ... end

modules/ToolboxAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { tools }, where tools is { name, desc } for every *.tool/ child the toolbox owns, walked directly from the toolbox's own folder — desc is the child tool's own --!desc docstring line, read from its entry script without requiring the tool module. A toolbox with no readable folder returns an empty tools list rather than erroring.

Parameters

  • self any (optional)
local boxTools = asset.inspect(toolboxRef).detail.tools

modules/ToolboxAssetTypeRef/listTools

listTools(self): { any }

List the tool refs the toolbox carries. Walks the toolbox folder, finds every *.tool/ child, and resolves each to an AssetRef<tool>.

Parameters

  • self any (optional)
for _, t in ipairs(toolboxRef:listTools()) do print(t.identity) end

modules/Trails/README

require("@builtin::systems.trails.trails") -- Trails

Continuous ribbon geometry that follows a moving point.

A trail is a strip of quads threaded along the positions something occupied over the last few seconds. Each recorded point contributes two vertices, offset either side of the direction of travel, and consecutive points are joined into a continuous surface — so a spark, a wingtip vapour trail or a sword arc is one mesh rather than a queue of sprites that betrays its spacing when the subject moves fast. Width, colour and opacity run from the head (the newest point) to the tail (the oldest), indexed by each point's age rather than its position in the list, so the fade reads the same whether the subject is crawling or sprinting. ribbon is pure: the same points, camera and settings produce the same arrays, so a trail's geometry can be checked without a scene.

Usage: local Trails = require("@builtin::systems.trails.trails")

modules/Trails/expire

expire(path: table, now: number) -> number

Drop points older than lifetime. Returns how many went.

Parameters

  • path table
  • now number

Returns number

modules/Trails/newPath

newPath(opts: table?) -> table

A fresh path. lifetime seconds a point survives, minDistance metres before a new one is recorded, maxPoints the ring's bound.

Parameters

  • opts table? (optional)

Returns table

modules/Trails/pathOf

pathOf(id: string) -> table?

Parameters

  • id string

Returns table?

modules/Trails/push

push(path: table, x: number, y: number, z: number, now: number) -> boolean

Record the head position when it has travelled minDistance from the last point. Returns whether a point was recorded.

Parameters

  • path table
  • x number
  • y number
  • z number
  • now number

Returns boolean

modules/Trails/register

register(id: string, path: table)

Parameters

  • id string
  • path table

modules/Trails/ribbon

ribbon(path: table, opts: table) -> table?

Build the ribbon over a path's points — flat positions / normals / uvs / colors / indices, the shape renderer.mesh.create takes. nil under two points, which is no surface.

Parameters

  • path table
  • opts table

Returns table?

modules/Trails/stats

stats() -> table

Returns table

modules/Trails/unregister

unregister(id: string)

Parameters

  • id string

modules/Transform/README

require("@builtin::modules.transform") -- Transform (also available as global 'Transform')

Math helpers for positions, rotations, and directions on transforms. Exposed as the global Transform table; entity-aware helpers accept an id string or an entity proxy.

Usage: local Transform = require("@builtin::modules.transform") Also available as global: Transform

modules/Transform/direction

direction(fromX: number, fromY: number, fromZ: number, toX: number, toY: number, toZ: number): (number, number, number)

Normalized direction vector from point A to point B. Returns zeros when the two points coincide (within ~0.001 units).

Parameters

  • fromX number — From x.
  • fromY number — From y.
  • fromZ number — From z.
  • toX number — To x.
  • toY number — To y.
  • toZ number — To z.
local dx, dy, dz = Transform.direction(0, 0, 0, 1, 0, 0)

modules/Transform/directionBetween

directionBetween(entityA: string | EntityRef, entityB: string | EntityRef): (number, number, number)

Normalized world-space direction from one entity to another, read from their world positions. Returns zeros if either entity can't be resolved.

Parameters

  • entityA string | EntityRef — Source entity (id string or proxy).
  • entityB string | EntityRef — Target entity (id string or proxy).
local dx, dy, dz = Transform.directionBetween("cam", "target")

modules/Transform/distance

distance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number

Euclidean distance between two world-space positions.

Parameters

  • x1 number — First point x.
  • y1 number — First point y.
  • z1 number — First point z.
  • x2 number — Second point x.
  • y2 number — Second point y.
  • z2 number — Second point z.
local d = Transform.distance(0, 0, 0, 1, 1, 1)

modules/Transform/distanceBetween

distanceBetween(entityA: string | EntityRef, entityB: string | EntityRef): number?

Distance between two entities in world space. Each entity's world position is what is measured, so a parent's offset counts toward the distance the way the scene shows it.

Parameters

  • entityA string | EntityRef — First entity (id string or proxy).
  • entityB string | EntityRef — Second entity (id string or proxy).
local d = Transform.distanceBetween("cam", "box")

modules/Transform/euler

euler(qx: number, qy: number, qz: number, qw: number): (number, number, number)

Convert quaternion to euler angles (yaw, pitch, roll) in radians.

Parameters

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
local yaw, pitch, roll = Transform.euler(0, 0, 0, 1)

modules/Transform/eulerToQuat

eulerToQuat(yaw: number, pitch: number?, roll: number?): (number, number, number, number)

Identity-aware overload of euler-to-quaternion. Uses the negative-yaw convention shared with quatFromYaw, quatFromYawPitch, lookAtQuat, and T.euler extraction — so T.euler(T.eulerToQuat(y, p, r)) returns (y, p, r). Order is yaw (Y) then pitch (X) then roll (Z).

Parameters

  • yaw number — Y-axis rotation in radians.
  • pitch number? (optional) — X-axis rotation in radians. Defaults to 0.
  • roll number? (optional) — Z-axis rotation in radians. Defaults to 0.
local qx, qy, qz, qw = Transform.eulerToQuat(math.pi / 2)

modules/Transform/lerp

lerp(ax: number, ay: number, az: number, bx: number, by: number, bz: number, t: number): (number, number, number)

Linearly interpolate between two positions.

Parameters

  • ax number — Start x.
  • ay number — Start y.
  • az number — Start z.
  • bx number — End x.
  • by number — End y.
  • bz number — End z.
  • t number — Interpolation factor [0, 1].
local x, y, z = Transform.lerp(0, 0, 0, 1, 1, 1, 0.5)

modules/Transform/lerp1

lerp1(a: number, b: number, t: number): number

Linearly interpolate two scalars.

Parameters

  • a number — Start value.
  • b number — End value.
  • t number — Interpolation factor [0, 1].
local v = Transform.lerp1(0, 10, 0.5)

modules/Transform/lerpAngle

lerpAngle(a: number, b: number, t: number): number

Lerp between two angles via the shortest arc; returns a value in [-pi, pi].

Parameters

  • a number — Start angle in radians.
  • b number — End angle in radians.
  • t number — Interpolation factor [0, 1].
local a = Transform.lerpAngle(0, math.pi, 0.5)

modules/Transform/localToWorld

localToWorld(px: number, py: number, pz: number, pqx: number, pqy: number, pqz: number, pqw: number, lx: number, ly: number, lz: number): (number, number, number)

Transform a local-space position into world space using a parent pose.

Parameters

  • px number — Parent position x.
  • py number — Parent position y.
  • pz number — Parent position z.
  • pqx number — Parent rotation x.
  • pqy number — Parent rotation y.
  • pqz number — Parent rotation z.
  • pqw number — Parent rotation w.
  • lx number — Local x.
  • ly number — Local y.
  • lz number — Local z.
local wx, wy, wz = Transform.localToWorld(px, py, pz, pqx, pqy, pqz, pqw, lx, ly, lz)

modules/Transform/lookAt

lookAt(entityOrId: string | EntityRef, txOrTarget: any, ty: any?, tz: number?, up: any?): (boolean, string?)

Make an entity face a world position. The target slot accepts three explicit coordinates, one point as { x, y, z } / { x =, y =, z = } / a vector, or an entity — an id string, an entity NAME, or a proxy — whose WORLD position is resolved. A table carrying an entity id reads as that entity; any other table reads as the point it spells. The subject slot takes the three entity spellings. Everything here is world space: the subject and the target are read as entity(id).position and the aim is written as entity(id).rotation, so a parent under either one moves the entity and the aim still lands on the point named. Returns whether the rotation was written, so a caller that named an entity the scene does not carry learns the aim did not happen instead of reading a stale orientation back as the answer.

Parameters

  • entityOrId string | EntityRef — Entity id, name, or proxy for the entity to rotate.
  • txOrTarget any (optional) — A number (world x), a point table, or an entity id / name / proxy whose world position is resolved as the look-at target.
  • ty any? (optional) — World y of the target. Omitted when txOrTarget is a point or an entity.
  • tz number? (optional) — World z of the target. Omitted when txOrTarget is a point or an entity.
  • up any? (optional) — Optional world up hint deciding the roll — { x, y, z }, { x =, y =, z = } or a vector. World +Y when omitted. It never bends the aim; it only says which way is up around it. When the target slot is an entity or a point this is the third argument, and when it is coordinates the fifth.
Transform.lookAt("cam", 0, 1, 0)
Transform.lookAt("cam", "box")  -- resolve target entity position
Transform.lookAt(cam, box)      -- entity proxies for both
Transform.lookAt("cam", { 0, 1, 0 })         -- one point table
Transform.lookAt("cam", "box", { 0, 0, 1 })  -- rolled to a +Z up

modules/Transform/lookAtQuat

lookAtQuat(fx: number, fy: number, fz: number, tx: number, ty: number, tz: number): (number?, number?, number?, number?)

Compute quaternion to look from origin position toward a target. Returns four components (qx, qy, qz, qw), or nil when the from and to points are too close to derive a meaningful direction.

Parameters

  • fx number — Origin x.
  • fy number — Origin y.
  • fz number — Origin z.
  • tx number — Target x.
  • ty number — Target y.
  • tz number — Target z.
local qx, qy, qz, qw = Transform.lookAtQuat(0, 0, 0, 1, 0, 1)

modules/Transform/lookRotation

lookRotation(fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, ux: number?, uy: number?, uz: number?): (number?, number?, number?, number?)

The rotation that aims an entity standing at one world point at another, with a world up hint deciding the roll. Where lookAtQuat derives the aim from yaw and pitch alone — clamping the pitch just short of vertical, so a point directly overhead comes back a twentieth of a degree off — this builds all three axes, so the aim lands on the point at any elevation and straight up and straight down are ordinary cases. The aimed axis is the entity's local -Z, the same forward quatFromBasis, Transform.lookAt and entity(id):lookAt state and the direction entity(id).transform.forward reads back. The up hint is a world direction the entity's own +Y is turned toward as far as the aim allows; it never bends the forward axis. A hint parallel to the aim leaves the roll undetermined, and a hint of no length names no direction — both fall back to a stable roll rather than a NaN.

Parameters

  • fx number — Eye x — where the entity stands.
  • fy number — Eye y.
  • fz number — Eye z.
  • tx number — Target x — the world point it faces.
  • ty number — Target y.
  • tz number — Target z.
  • ux number? (optional) — Up hint x. World +Y when the hint is omitted.
  • uy number? (optional) — Up hint y.
  • uz number? (optional) — Up hint z.
local qx, qy, qz, qw = Transform.lookRotation(0, 2, 10, 0, 1, 0)
entity("cam").rotation = { Transform.lookRotation(0, 2, 10, 0, 1, 0) }
-- a dutch tilt: the same aim, rolled by leaning the up hint
local q = { Transform.lookRotation(0, 2, 10, 0, 1, 0, 0.2, 1, 0) }

modules/Transform/normalizeAngle

normalizeAngle(a: number): number

Normalize an angle into [-pi, pi].

Parameters

  • a number — The angle in radians.
local a = Transform.normalizeAngle(3 * math.pi)

modules/Transform/orbit

orbit(centerX: number, centerY: number, centerZ: number, radius: number, height: number, angle: number): (number, number, number, number, number, number, number)

Position + rotation for orbiting around a center point. Returns the world position followed by the orientation that faces the center.

Parameters

  • centerX number — Center x.
  • centerY number — Center y.
  • centerZ number — Center z.
  • radius number — Horizontal distance from the center.
  • height number — Vertical offset from centerY.
  • angle number — Orbital angle in radians.
local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t)

modules/Transform/quatFromAxisAngle

quatFromAxisAngle(ax: number, ay: number, az: number, angle: number): (number, number, number, number)

Create quaternion from axis and angle (radians). Returns the identity quaternion when the axis is degenerate (length < 0.001).

Parameters

  • ax number — Axis x.
  • ay number — Axis y.
  • az number — Axis z.
  • angle number — Rotation angle in radians.
local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.pi)

modules/Transform/quatFromBasis

quatFromBasis(rx: number, ry: number, rz: number, ux: number, uy: number, uz: number, fx: number, fy: number, fz: number): (number, number, number, number)

Build the rotation whose right, up and forward ARE the given axes. Where lookAtQuat derives a rotation from a direction alone — yaw and pitch, with pitch clamped just short of straight up or down and no say in the roll — this states all three axes, so a view straight down has a defined image-up instead of whatever the yaw implied. The axes are expected orthonormal and are used as given: right and up are the entity's local +X and +Y, forward its local -Z (the direction it faces).

Parameters

  • rx number — Right axis x.
  • ry number — Right axis y.
  • rz number — Right axis z.
  • ux number — Up axis x.
  • uy number — Up axis y.
  • uz number — Up axis z.
  • fx number — Forward axis x.
  • fy number — Forward axis y.
  • fz number — Forward axis z.
local qx, qy, qz, qw = Transform.quatFromBasis(1,0,0, 0,1,0, 0,0,-1) -- identity
-- looking straight down with the subject's front toward the top of frame
local qx, qy, qz, qw = Transform.quatFromBasis(1,0,0, 0,0,-1, 0,-1,0)

modules/Transform/quatFromYaw

quatFromYaw(yaw: number): (number, number, number, number)

Create quaternion from yaw (Y-axis rotation) in radians. Uses the negative-yaw convention shared with quatFromYawPitch, lookAtQuat, and T.euler extraction — so T.euler(T.quatFromYaw(y)) round-trips to y.

Parameters

  • yaw number — Rotation in radians around the Y axis.
local qx, qy, qz, qw = Transform.quatFromYaw(math.pi / 2)

modules/Transform/quatFromYawPitch

quatFromYawPitch(yaw: number, pitch: number): (number, number, number, number)

Create quaternion from yaw and pitch in radians.

Parameters

  • yaw number — Y-axis rotation in radians.
  • pitch number — X-axis rotation in radians.
local qx, qy, qz, qw = Transform.quatFromYawPitch(0, math.pi / 4)

modules/Transform/quatIdentity

quatIdentity(): (number, number, number, number)

Identity quaternion (0, 0, 0, 1).

local qx, qy, qz, qw = Transform.quatIdentity()

modules/Transform/quatInverse

quatInverse(qx: number, qy: number, qz: number, qw: number): (number, number, number, number)

Quaternion inverse. Equal to the conjugate for unit quaternions.

Parameters

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
local ix, iy, iz, iw = Transform.quatInverse(qx, qy, qz, qw)

modules/Transform/quatMul

quatMul(ax: number, ay: number, az: number, aw: number, bx: number, by: number, bz: number, bw: number): (number, number, number, number)

Quaternion multiplication: returns qa * qb (composition: rotate by qb then qa).

Parameters

  • ax number — Left quat x.
  • ay number — Left quat y.
  • az number — Left quat z.
  • aw number — Left quat w.
  • bx number — Right quat x.
  • by number — Right quat y.
  • bz number — Right quat z.
  • bw number — Right quat w.
local qx, qy, qz, qw = Transform.quatMul(ax, ay, az, aw, bx, by, bz, bw)

modules/Transform/quatRotateVec

quatRotateVec(qx: number, qy: number, qz: number, qw: number, vx: number, vy: number, vz: number): (number, number, number)

Rotate a 3-vector by a quaternion.

Parameters

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
  • vx number — Vector x.
  • vy number — Vector y.
  • vz number — Vector z.
local rx, ry, rz = Transform.quatRotateVec(qx, qy, qz, qw, 1, 0, 0)

modules/Transform/quatToEuler

quatToEuler(qx: number, qy: number, qz: number, qw: number): (number, number, number)

Convert quaternion to (yaw, pitch, roll). Alias of euler with the explicit name so callers don't have to remember the order.

Parameters

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
local yaw, pitch, roll = Transform.quatToEuler(qx, qy, qz, qw)

modules/Transform/readVec3

readVec3(value: Vec3Input, label: string?): { number }

Normalize a vector a caller wrote to a plain { x, y, z } array. Accepts a positional array {1, 2, 3}, a keyed table {x =, y =, z =}, or a live vec handle. Missing components read as 0. Raises when the value is not a vector; label names the caller in that error.

Parameters

  • value Vec3Input — The vector to normalize.
  • label string? (optional) — Name reported in the error when the value is not a vector. Defaults to "Transform".
local v = Transform.readVec3({ x = 1, y = 2, z = 3 })

modules/Transform/slerp

slerp(ax: number, ay: number, az: number, aw: number, bx: number, by: number, bz: number, bw: number, t: number): (number, number, number, number)

Spherical linear interpolation between two quaternions. Picks the shortest path (flips sign if dot < 0). Falls back to lerp+normalize when the two quats are very close (avoids div-by-zero on near-parallel inputs).

Parameters

  • ax number — Start quaternion x.
  • ay number — Start quaternion y.
  • az number — Start quaternion z.
  • aw number — Start quaternion w.
  • bx number — End quaternion x.
  • by number — End quaternion y.
  • bz number — End quaternion z.
  • bw number — End quaternion w.
  • t number — Interpolation factor [0, 1].
local qx, qy, qz, qw = Transform.slerp(0, 0, 0, 1, 1, 0, 0, 0, 0.5)

modules/Transform/snapVec3

snapVec3(v: { number }, step: number | Vec3Input): { number }

Quantize each component of a vector to the nearest multiple of step — a number for uniform steps, or a vector for per-axis steps. A step of 0 on an axis leaves that axis at its exact value.

Parameters

  • v { number } — The vector to quantize, as { x, y, z }.
  • step number | Vec3Input — Uniform step size, or a per-axis vector of step sizes.
local v = Transform.snapVec3({ 1.4, 2.6, -0.4 }, 1)

modules/Transform/toQuaternion

toQuaternion(rotation: any, label: string?): { number }

Normalize a rotation a caller wrote to a { qx, qy, qz, qw } quaternion. Accepts a quaternion ({x,y,z,w} or {x=,y=,z=,w=}) or euler DEGREES ({pitch,yaw,roll} or {pitch=,yaw=,roll=}), so one call site takes whichever form the caller finds natural. This is the reading every rotation-taking surface in the engine shares, so a quaternion and euler degrees mean the same thing at all of them. Raises when the value matches no form; label names the caller in that error, and a value that is one of the shapes a quaternion helper returns is named as such along with the packing it goes in as.

Parameters

  • rotation any (optional) — The rotation to normalize, in any form of the RotationInput union.
  • label string? (optional) — Name reported in the error when the value is not a rotation. Defaults to "Transform".
local q = Transform.toQuaternion({ pitch = 0, yaw = 90, roll = 0 })

modules/Transform/tryQuaternion

tryQuaternion(rotation: any, label: string?): ({ number }?, string?)

Read a rotation a caller wrote WITHOUT raising: returns the canonical { qx, qy, qz, qw }, or nil and the message describing what arrived. The forms are the RotationInput union — a quaternion ({x,y,z,w} or {x=,y=,z=,w=}) or euler DEGREES ({pitch,yaw,roll} or {pitch=,yaw=,roll=}). Takes any value because reporting on a value that is none of those forms is the whole job; a setter built on this raises the returned message itself, so the error points at the line that wrote the value rather than at the reading.

Parameters

  • rotation any (optional) — The value to read as a rotation.
  • label string? (optional) — Name reported in the message. Defaults to "Transform".
local q, why = Transform.tryQuaternion(value, "myTool")

modules/Transform/vec.add

vec.add(ax: number, ay: number, az: number, bx: number, by: number, bz: number): (number, number, number)

Component-wise vec3 addition.

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local x, y, z = Transform.vec.add(1, 2, 3, 4, 5, 6)

modules/Transform/vec.cross

vec.cross(ax: number, ay: number, az: number, bx: number, by: number, bz: number): (number, number, number)

Cross product a x b.

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local cx, cy, cz = Transform.vec.cross(1, 0, 0, 0, 1, 0)

modules/Transform/vec.dot

vec.dot(ax: number, ay: number, az: number, bx: number, by: number, bz: number): number

Dot product of two vec3s.

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local d = Transform.vec.dot(1, 0, 0, 0, 1, 0)

modules/Transform/vec.length

vec.length(x: number, y: number, z: number): number

Euclidean length of a vec3.

Parameters

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
local len = Transform.vec.length(1, 2, 3)

modules/Transform/vec.normalize

vec.normalize(x: number, y: number, z: number): (number, number, number)

Normalize a vec3. Returns zeros when the input is degenerate (length < 1e-8).

Parameters

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
local nx, ny, nz = Transform.vec.normalize(0, 5, 0)

modules/Transform/vec.scale

vec.scale(x: number, y: number, z: number, s: number): (number, number, number)

Component-wise scalar multiplication of a vec3.

Parameters

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
  • s number — Scalar factor.
local x, y, z = Transform.vec.scale(1, 2, 3, 2)

modules/Transform/vec.sub

vec.sub(ax: number, ay: number, az: number, bx: number, by: number, bz: number): (number, number, number)

Component-wise vec3 subtraction (a - b).

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local x, y, z = Transform.vec.sub(4, 5, 6, 1, 2, 3)

modules/Transform/worldToLocal

worldToLocal(px: number, py: number, pz: number, pqx: number, pqy: number, pqz: number, pqw: number, wx: number, wy: number, wz: number): (number, number, number)

Transform a world-space position into a parent's local space.

Parameters

  • px number — Parent position x.
  • py number — Parent position y.
  • pz number — Parent position z.
  • pqx number — Parent rotation x.
  • pqy number — Parent rotation y.
  • pqz number — Parent rotation z.
  • pqw number — Parent rotation w.
  • wx number — World x.
  • wy number — World y.
  • wz number — World z.
local lx, ly, lz = Transform.worldToLocal(px, py, pz, pqx, pqy, pqz, pqw, wx, wy, wz)

modules/UiMotionCapture/README

require("@builtin::modules.ui_motion_capture") -- UiMotionCapture

Publishes the UI-animation capture views — the UI analogues of the motion_vectors pass. capture pass=ui_motion shows WHERE the UI is animating (change-magnitude heatmap); capture pass=ui_motion_flow shows WHICH WAY it is moving (directional optical flow). Each view's drawing feature is created on demand the first time it is captured, so a session that never selects one pays nothing.

Usage: local UiMotionCapture = require("@builtin::modules.ui_motion_capture")

modules/UiMotionCapture/register

register()

Register the ui_motion (change magnitude) and ui_motion_flow (directional flow) capture views. Idempotent — safe to call at boot and again later; re-registering keeps each view's channel.

require("@builtin::modules.ui_motion_capture").register()

modules/Validator/README

require("@builtin::systems.worldValidation.validator") -- Validator

Orchestrator for the world validator. Composes vfsScanner + scriptValidator + assetValidator + reportFormatter into one call that produces a Report with world and library buckets cleanly separated.

The validator does ONE pass over the VFS to discover every script and asset, then dispatches each entry to the appropriate per-type validator (scripts → scriptValidator → lsp.check; asset folders → assetValidator → asset.validate). It does not spawn entities or allocate GPU resources. Repeated calls are independent — there is no shared mutable state across runs. The four helper modules (vfsScanner, scriptValidator, assetValidator, reportFormatter) sit alongside this module at the package root and are pulled in via the ~.X package-relative form so the whole package is position-independent — moving it to any identity continues to resolve correctly without code edits.

Usage: local Validator = require("@builtin::systems.worldValidation.validator")

modules/Validator/check

check(opts)

Validate authored content. By DEFAULT scopes to YOUR world — everything under /source/ EXCEPT /source/libs/ — because imported libraries and engine builtins are not yours to validate (and scanning the whole builtin tree is slow and noisy). Pass opts.scope to widen: "libraries" for every imported library, "library:<name>" for one, "all" for world + libraries together. The cargo check equivalent for a Zero world.

Parameters

  • opts any (optional) — Optional table — scope?: "world"|"libraries"|"library:<name>"|"all" (default "world") plus filter fields forwarded to the formatter: { severity, category, code, source, path, includePlaceholders, limit }.
local r = Validator.check()                         -- your world only
local r = Validator.check({ severity = "error" })   -- your world, errors only
local r = Validator.check({ scope = "all" })        -- world + imported libraries

modules/Validator/filter

filter(report, opts)

Re-filter an existing Report without re-scanning the VFS. Forwards to reportFormatter.filter; the resulting Report's counts are recomputed from the visible problems.

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
  • opts any (optional) — FilterOpts — { severity, category, code, source, path, includePlaceholders, limit }.
local errs = Validator.filter(report, { severity = "error" })

modules/Validator/format

format(report, format)

Render a Report into a string. format selects the renderer.

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
  • format any (optional) — "human" (default) / "markdown" / "json" / "summary".
print(Validator.format(report, "human"))
local md = Validator.format(report, "markdown")

modules/Validator/placeholders

placeholders()

Enumerate the registered placeholder checks. Each placeholder is a check the validator runs today but with a stub implementation — listing them tells callers which validations still need real primitives wired up.

for _, ph in ipairs(Validator.placeholders()) do print(ph.code) end

modules/Validator/saveReport

saveReport(report, path, opts)

Render a Report and write it to path. Format is taken from opts.format if present, otherwise inferred from the destination extension (.md → markdown, .json → json, anything else → human). Returns (ok, message?).

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
  • path any (optional) — Destination VFS path (e.g. /source/.validation/run.md).
  • opts any (optional) — Optional { format } override.
Validator.saveReport(report, "/source/.validation/run.md")
Validator.saveReport(report, "/source/audit.json", { format = "json" })

modules/Validator/summary

summary(report)

Compact one-line health summary — world: NE/NW libraries: NE/NW total: OK|FAIL.

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
print(Validator.summary(Validator.check()))

modules/Validator/validateLibraries

validateLibraries(opts)

Validate every imported library under /source/libs/. The returned Report has world = nil and libraries populated for every library that exists on disk.

Parameters

  • opts any (optional) — Optional filter table — same shape as M.check.
local r = Validator.validateLibraries()
local r = Validator.validateLibraries({ source = "library:@builtin" })

modules/Validator/validateLibrary

validateLibrary(name, opts)

Validate ONE named library under /source/libs/<name>/. When the library does not exist, the Report carries a single library.missing error against that name so callers can tell a clean run from a missing-dependency run.

Parameters

  • name any (optional) — Library directory name (e.g. "@builtin", "@mylib").
  • opts any (optional) — Optional filter table — same shape as M.check.
local r = Validator.validateLibrary("@builtin")
local r = Validator.validateLibrary("@mylib", { severity = "error" })

modules/Validator/validateWorld

validateWorld(opts)

Validate ONLY the world's authored content (/source/ excluding /source/libs/). The returned Report has world populated and libraries = {}.

Parameters

  • opts any (optional) — Optional filter table — same shape as M.check.
local r = Validator.validateWorld()
local r = Validator.validateWorld({ severity = "error" })

modules/ValueType/README

require("@builtin::modules.value_type") -- ValueType

Converts a handle-backed value type — ColorSequence, NumberSequence — between the live object a session holds and the durable payload its serialize() produces. Every boundary that writes a component field to a record, or applies a record back onto a component, converts here.

Usage: local ValueType = require("@builtin::modules.value_type")

modules/ValueType/bind

bind(v: any, kind: string): any

Put a value type's methods back on a value read out of a component field. Field.table keeps the table it is handed without its metatable and gives that stored table back on every read, so binding once makes the field answer :evaluate / :keypoints for the rest of the session.

Parameters

  • v any (optional) — A live value carrying a handle.
  • kind string — The kind to bind as when v names none itself.
ValueType.bind(component.color, "ColorSequence"):evaluate(0)

modules/ValueType/isLive

isLive(v: any): boolean

Whether a value is a live handle-backed value type — the form that holds a session-local curve handle.

Parameters

  • v any (optional) — Any component field value.
ValueType.isLive(NumberSequence.new(0, 1)) -- true

modules/ValueType/isPayload

isPayload(v: any): boolean

Whether a value is the durable payload of a handle-backed value type — the form a record carries.

Parameters

  • v any (optional) — Any component field value.
ValueType.isPayload(NumberSequence.new(0, 1):serialize()) -- true

modules/ValueType/keypoints

keypoints(v: any, kind: string): { any }?

The keypoint list a value holds, whichever of the two forms it is in. A caller that knows the field's type passes it as kind so a bare { __h = n } — a handle written by a session that named no kind — is still read as that type.

Parameters

  • v any (optional) — A live value, a durable payload, or a bare handle table.
  • kind string — The kind to read v as when v names none itself.
ValueType.keypoints(field, "ColorSequence")

modules/ValueType/kindOf

kindOf(v: any): string?

The kind name a value declares, when it is one this module converts.

Parameters

  • v any (optional) — Any component field value.
ValueType.kindOf(ColorSequence.new({1, 0, 0})) -- "ColorSequence"

modules/ValueType/revive

revive(v: any): any?

The live value a durable payload names, rebuilt in this session.

Parameters

  • v any (optional) — Any component field value.
ValueType.revive(record.color)

modules/ValueType/serialize

serialize(v: any): any?

The durable payload for a live value — what a record stores in place of the session-local handle.

Parameters

  • v any (optional) — Any component field value.
ValueType.serialize(field) -- { kind = "ColorSequence", keypoints = {...} }

modules/ValueType/withRevived

withRevived(data: any): (any, number)

The component field map to apply, with every durable payload rebuilt as the live value it names — the counterpart of the saver writing payloads in place of handles.

Parameters

  • data any (optional) — A component's { field = value } map from a record.
ValueType.withRevived(record.data)

modules/VfsScanner/README

require("@builtin::systems.worldValidation.vfsScanner") -- VfsScanner

Recursive /source/ walker for the world validator. Separates world content from imported libraries so downstream validators can attribute every problem to the right bucket.

An asset root is any folder whose name has a registered asset type suffix (Foo.component, bar.module, Baz.toolbox, …). When the scanner crosses one it records the root and does NOT recurse into it as raw files — the asset validator handles the interior. Scripts (.luau, .lua) discovered outside any asset root are also recorded (e.g. _shared.luau siblings inside a toolbox, or stray top-level scripts).

Usage: local VfsScanner = require("@builtin::systems.worldValidation.vfsScanner")

modules/VfsScanner/assetSuffixes

assetSuffixes(): { string }

Expose the registered asset-suffix list (read-only). External callers that want to recognise asset folders the same way the scanner does can iterate this list.

for _, s in ipairs(VfsScanner.assetSuffixes()) do print(s) end

modules/VfsScanner/listLibraryNames

listLibraryNames(): { string }

Enumerate the immediate children of /source/libs/. Each child is a library identity (e.g. @builtin, @mylib). Returns an empty array if /source/libs/ does not exist.

for _, name in ipairs(VfsScanner.listLibraryNames()) do print(name) end

modules/VfsScanner/scanAll

scanAll()

Convenience: scan world + every imported library in one call.

local scan = VfsScanner.scanAll(); print(#scan.world.assets)

modules/VfsScanner/scanLibraries

scanLibraries(): { [string]: any }

Walk every imported library and return a map keyed by name.

local libs = VfsScanner.scanLibraries(); for n, b in pairs(libs) do print(n, #b.assets) end

modules/VfsScanner/scanLibrary

scanLibrary(name: string)

Walk one named library under /source/libs/<name>/. The returned Bucket's rootPath is the library root so callers can derive relative paths cheaply.

Parameters

  • name string — Library directory name (e.g. "@builtin").
local bucket = VfsScanner.scanLibrary("@builtin")

modules/VfsScanner/scanWorld

scanWorld()

Walk only the world bucket — everything under /source/ except /source/libs/. The scanner stops recursing whenever it reaches an asset-suffixed folder; asset interiors are handled by the assetValidator.

local bucket = VfsScanner.scanWorld()

modules/WorkflowAssetTypeRef/README

require("@builtin::assetTypes.workflow.behavior") -- WorkflowAssetTypeRef

Per-instance methods exposed on every AssetRef<workflow>. Loaded lazily by asset_ref.module.

Usage: local WorkflowAssetTypeRef = require("@builtin::assetTypes.workflow.behavior")

modules/WorkflowAssetTypeRef/getManifest

getManifest(self): { [string]: any }

Read and parse workflow.yaml: what this workflow is, when it is reached for, and the phases it moves through.

Parameters

  • self any (optional)
local m = wf:getManifest()

modules/WorkflowAssetTypeRef/getSource

getSource(self): string?

Read the program itself — the JavaScript body that runs when the workflow is started.

Parameters

  • self any (optional)
local src = wf:getSource()

modules/WorkflowAssetTypeRef/inspect

inspect(self): { [string]: any }

What this workflow is, without running it: its description, when to reach for it, the phases it moves through, and what it expects in args.

Parameters

  • self any (optional)
local info = wf:inspect()

modules/WorkflowAssetTypeRef/start

start(self, args: { [string]: any }?): { [string]: any }

Start a run of this workflow. Returns immediately: the run is already executing, and it parks the moment it asks its first question. Answer what it asks through the workflow toolbox — the run decides what comes next.

Parameters

  • self any (optional)
  • args { [string]: any }? (optional) — Optional table handed to the program as its args.
local run = wf:start({ concept = "outrun a tornado" })

modules/World/README

require("@builtin::modules.world") -- World (also available as global 'world')

Public Luau API for the bound world. Composes internal __world FFI primitives with per-mode slot defaults (world_defaults), source-control toolbox (world_vcs), and the connected-users registry (connected_users) into a single namespace. Usage: world.guid() -- current world GUID, nil if none world.swap(guid) -- bind to another world (promise) world.avatar_default_edit = ref -- per-mode avatar default world.on("player_join", cb) -- world-level event hooks Implemented as a thin Luau wrapper. The world global is a plain table whose __index metatable falls through to the internal __world namespace for unknown reads. Other library modules (world_defaults, world_vcs, connected_users) attach their surfaces via installInto(world) from prelude.luau; their metatable wrappers chain through this base layer correctly.

Usage: local World = require("@builtin::modules.world") Also available as global: world

modules/Yaml/README

require("@builtin::modules.yaml") -- Yaml

YAML decode + encode for authored engine content. Supports the YAML subset engine configs use: block mappings + sequences (a sequence may sit at its key's own column), flow collections (on one line or continued over several), typed plain scalars, quoted strings, comments, literal and folded block scalars, an optional leading ---. Unsupported constructs (anchors, aliases, tags, directives, multi-document streams, tab indentation) raise with the offending line number.

Usage: local Yaml = require("@builtin::modules.yaml")

modules/Yaml/decode

decode(text: string): any

Decode a YAML document into a Luau value. Raises (with the line number) on malformed input or constructs outside the supported subset — never misparses silently.

Parameters

  • text string — The YAML document text.
local doc = Yaml.decode(vfs.read(path))

modules/Yaml/decodeWithComments

decodeWithComments(text: string): (any, boolean)

Decode a YAML document and report whether it carries a comment — by the decoder's own rule: a # at a line's start or after whitespace, outside a quoted scalar (a quote opens one only where a scalar can begin, so a plain scalar may carry an apostrophe) and outside a block scalar. What re-encoding the value would drop.

Parameters

  • text string — The YAML document text.
local doc, commented = Yaml.decodeWithComments(vfs.read(path))

modules/Yaml/encode

encode(value: { [any]: any }): string

Encode a Luau table as a YAML document (block style, two-space indent, sorted keys). Raises on values YAML can't represent (functions, userdata, non-string mapping keys).

Parameters

  • value { [any]: any } — The table to encode.
vfs.write(path, Yaml.encode({ contract = "weapon", values = v }))

modules/Yaml/flowEntries

flowEntries(text: string): ({ FlowEntry }, string)

Split a flow collection written on one line, { a: 1, b: [2, 3] } or [x, { y: 2 }], into its top-level entries as written: each entry's key (unquoted; nil for a sequence item) and its value text, trimmed, with nested collections, quoted scalars and the literal spelling of every value kept. Raises with the decoder's own message on text that is not one well-formed flow collection, so it accepts exactly what decode reads as a flow value.

Parameters

  • text string — The flow collection's text.
for _, e in Yaml.flowEntries("{ exposure: 1.5, tint: [1, 0, 0] }") do print(e.key, e.value) end
local entries, trailing = Yaml.flowEntries("{ exposure: 1.5 } # brighter")

modules/ZJsAssetTypeRef/README

require("@builtin::packages.zerojs.zJs.behavior") -- ZJsAssetTypeRef

Per-instance methods exposed on every AssetRef<zJs>. Loaded lazily by asset_ref.module the first time a zJs ref is touched in a VM. A zJs asset is a <name>.zJs/ folder whose main.js holds a JavaScript ES module. The methods here surface the static write-time diagnostics the type computes for that module and execute it: run / runAsync for a script or module, and exports to read a module's export namespace as a Luau table.

Usage: local ZJsAssetTypeRef = require("@builtin::packages.zerojs.zJs.behavior")

modules/ZJsAssetTypeRef/check

check(self): { ok: boolean, diagnostics: { Diagnostic } }

Static diagnostics for THIS module's main.js: parse errors plus imports that do not resolve to a zJs module. Reads the cache onChange maintains, running the parse+resolve pipeline once on a cold reference.

Parameters

  • self any (optional)
local r = asset.resolve("greeter", "zJs"):check(); if not r.ok then ... end

modules/ZJsAssetTypeRef/exports

exports(self, opts: { env: { [string]: any }? }?): any

Evaluate THIS module as an ES module on a fresh VM and return its export namespace marshaled to a Luau table: each named export plus default. Cross-asset imports resolve through the live asset index, so a JS library built from several .zJs assets is consumed as one table. Refuses when the module carries diagnostics, naming the first; a cycle, an unresolved import, or a thrown module body surfaces as a Luau error.

Parameters

  • self any (optional)
  • opts { env: { [string]: any }? }? (optional)
local lib = asset.resolve("mathlib", "zJs"):exports(); print(lib.add(1, 2))

modules/ZJsAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: recompute this module's static diagnostics whenever its main.js is edited or the whole instance is seeded. Convergent: it reads the source and asset index and replaces the ref's cached pipeline result; it never writes to the VFS. Edits to sidecars (README.md, .metadata) leave the cache untouched.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ZJsAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("zJs", name, opts). Pure: returns the instance's content file map for the caller to persist. opts.source, when given, becomes main.js verbatim; otherwise a hello-world module naming the instance is scaffolded from the template.

Parameters

  • name string — zJs module identity (the instance name).
  • opts CreateOpts
asset.create("zJs", "greeter", { source = "export const x = 1" })

modules/ZJsAssetTypeRef/run

run(self, opts: { env: { [string]: any }? }?): any

Execute THIS module on a fresh JavaScript VM and marshal the result to Luau. A plain script (no imports or exports) runs as a script and returns its completion value; a module evaluates through the module graph and returns nil, since module evaluation has no completion value. opts.env seeds host values as JavaScript globals. Refuses when the module carries diagnostics, naming the first, and directs a script that leaves asynchronous work pending to runAsync.

Parameters

  • self any (optional)
  • opts { env: { [string]: any }? }? (optional)
local sum = asset.resolve("calc", "zJs"):run({ env = { base = 10 } })

modules/ZJsAssetTypeRef/runAsync

runAsync(self, opts: { env: { [string]: any }?, timeoutMs: number? }?): any

Execute THIS module's program on a fresh VM under the asynchronous runner, driving armed timers and pending promises to completion. Returns the engine run handle: poll handle.done, then read handle.ok / handle.value / handle.error, or call handle:await() from a yieldable context. opts.timeoutMs bounds a never-settling run. Refuses when the module carries diagnostics, naming the first.

Parameters

  • self any (optional)
  • opts { env: { [string]: any }?, timeoutMs: number? }? (optional)
local h = asset.resolve("job", "zJs"):runAsync({ timeoutMs = 1000 }); h:await()

modules/ZinputActions/README

require("@builtin::modules.zinput.actions") -- ZinputActions

DEPRECATED: A control asset carries what an action carried, plus the gamepad and touch an action never had. Author an .inputMap with .inputBinding children, activate it, and subscribe: local map = public.inputMap:activate() then map.jump:onPressed(fn). See man topics/input.

Usage: local ZinputActions = require("@builtin::modules.zinput.actions")

modules/ZinputActions/_clearHandlers

_clearHandlers()

Test-only: clear every handler (does NOT touch action definitions).

Zin.actions._clearHandlers()

modules/ZinputActions/_dispatchHandlers

_dispatchHandlers(firstTickThisFrame: boolean?)

Internal: dispatch action handlers. Called by Zin.tick after axes/chords advance. Fires Begin/End/Change/Held handlers based on each action's polling state this tick. firstTickThisFrame is false on a same-engine-frame re-tick (e.g. the autoTick worker and an explicit Zin.tick both land in one frame): edge fires (Begin/End/Change) are per-frame events and must not fire twice, so they are gated to the first tick of the frame. Held is a per-tick redeliver and still fires every call. A nil argument is treated as the first tick (edges fire).

Parameters

  • firstTickThisFrame boolean? (optional) — Whether this is the frame's first dispatch pass.
Zin.actions._dispatchHandlers()

modules/ZinputActions/_owns

_owns(handle: number): boolean

Internal: cross-API ownership probe. Used by Zin.disconnect to route handles to the right disconnect implementation, since Zin.input.on* and Zin.actions.bind share a handle namespace.

Parameters

  • handle number — The numeric handle to probe.
if Zin.actions._owns(h) then Zin.actions.disconnect(h) end

modules/ZinputActions/_resetUnknownWarnings

_resetUnknownWarnings()

Test-only: forget which names have already been reported as unregistered, so a fresh suite sees the warning again.

Zin.actions._resetUnknownWarnings()

modules/ZinputActions/_setAllocator

_setAllocator(fn: () -> number)

Internal: wire a shared id allocator, so handles from this module and from Zin.input.on* never collide. Called once at module-load time from zinput/init.luau.

Parameters

  • fn () -> number — The allocator function that returns the next handle id.
M._setAllocator(allocateZinHandle)

modules/ZinputActions/_setEnsureBindingsFn

_setEnsureBindingsFn(fn: () -> ())

Internal: wire the lazy-default-map hook. Called once at module- load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on a cold read (name not in the registry).
M._setEnsureBindingsFn(ensureDefaultBindings)

modules/ZinputActions/_setEnsureLiveFn

_setEnsureLiveFn(fn: () -> ())

Internal: wire the liveness hook. Called once at module-load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on every cold-checked read below.
M._setEnsureLiveFn(ensureInputLive)

modules/ZinputActions/_settled

_settled(): boolean

Internal: whether the most recent dispatch pass delivered nothing and saw nothing held. The tick's quiescence gate reads it.

if Zin.actions._settled() then ... end

modules/ZinputActions/active

active(name: string): boolean

True if an action is currently active in the input context stack. An action is "active" iff its declared context matches the top of the context stack (so push("ui") suppresses every non-"ui" action).

Parameters

  • name string — The action name.
if Zin.actions.active("jump") then ... end

modules/ZinputActions/bind

bind(name: string, fn, opts: BindOpts?): ActionHandle?

Register a handler. Returns a numeric handle (also accepted by Zin.input.disconnect). Returns nil if name is not a defined action. The handler ends with the context that bound it as well: a component's with that component instance, a module body's with the module's next run.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe) -> "sink" | any.
  • opts BindOpts? (optional) — Optional { priority, fire, once }.
local h = Zin.actions.bind("jump", function() print("jump") end)

modules/ZinputActions/clear

clear()

Wipe every defined action. Primarily for tests.

Zin.actions.clear()

modules/ZinputActions/define

define(spec: ActionSpec)

Define one or more actions. Each entry replaces any existing action under the same name; other actions are preserved.

Parameters

  • spec ActionSpec — Map of name -> binding | { binding... } | { context, binding(s) }.
Zin.actions.define({ jump = Zin.bindings.key("Space") })

modules/ZinputActions/disconnect

disconnect(handle: ActionHandle): boolean

Tear down a handler returned by bind. Idempotent.

Parameters

  • handle ActionHandle — The handle from bind (or onPressed/onReleased/etc).
Zin.actions.disconnect(h)

modules/ZinputActions/get

get(name: string): ActionEntry?

Internal: the registry record behind a name, for profile capture and conflict indexing. Returns nil if the action is not defined.

Parameters

  • name string — The action name.
local entry = Zin.actions.get("jump")

modules/ZinputActions/handlerCount

handlerCount(name: string): number

Number of registered handlers for an action (0 if none / unknown).

Parameters

  • name string — The action name.
assert(Zin.actions.handlerCount("jump") == 1)

modules/ZinputActions/has

has(name: string): boolean

True if an action with this name is defined.

Parameters

  • name string — The action name to test.
if Zin.actions.has("jump") then ... end

modules/ZinputActions/held

held(name: string): boolean

True if any binding on the action is currently delivering input. For boolean bindings: any held. For axis/vector bindings: non-zero magnitude. Suppressed by context gating.

Parameters

  • name string — The action name.
if Zin.actions.held("attack") then print("swing") end

modules/ZinputActions/heldTime

heldTime(name: string): number?

Seconds the action has been held, taken as the MAX held-time across the action's boolean bindings. Returns nil if no binding is held or if the action is gated off by the current input context. Vector / axis bindings are skipped — use a held-time threshold against Zin.axes.value for held-direction analogs.

Parameters

  • name string — The action name.
local t = Zin.actions.heldTime("interact")

modules/ZinputActions/names

names(): { string }

All defined action names, in arbitrary order.

for _, n in ipairs(Zin.actions.names()) do print(n) end

modules/ZinputActions/onChanged

onChanged(name: string, fn, opts: BindOpts?): ActionHandle?

Fire on axis/vector value delta (state = "Change"). For boolean actions Change fires on every press AND release transition — use onPressed / onReleased instead if you only want edges.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onChanged("move", function(_, _, io) print(io.value) end)

modules/ZinputActions/onHeld

onHeld(name: string, fn, opts: BindOpts?): ActionHandle?

Fire every tick while held (state = "Held"). Fires whenever held() is true at dispatch time, regardless of value change. Inherits the action's context constraint (no per-handler context filter).

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onHeld("interact", function(_, _, io) print("charge", io.value) end)

modules/ZinputActions/onPressed

onPressed(name: string, fn, opts: BindOpts?): ActionHandle?

Fire on rising edge (state = "Begin"). Sugar for bind with fire = {"Begin"}.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onPressed("jump", function() ... end)

modules/ZinputActions/onReleased

onReleased(name: string, fn, opts: BindOpts?): ActionHandle?

Fire on falling edge (state = "End"). Sugar for bind with fire = {"End"}.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onReleased("attack", function() ... end)

modules/ZinputActions/pressed

pressed(name: string): boolean

True if any boolean binding on the action was just pressed this frame — the press EDGE, so it fires once per press no matter how long the input is held. Use it for one-shot input: fire, jump, pause, undo. For continuous input that should repeat every frame the input is down, read the level instead with Zin.state.keyDown(key). Axis/vector bindings do not contribute to press edges. Suppressed by context gating.

Parameters

  • name string — The action name.
if Zin.actions.pressed("jump") then ... end

modules/ZinputActions/reason

reason(name: string): string

Why a named action is not delivering, from the closed set this registry can distinguish: unknownControl (no action is registered under the name — the value readers answer their neutral value, which is not a reading), contextInactive (registered, but its context is not on top of the stack), atRest (live, and the devices it binds are not being driven), or delivering.

Parameters

  • name string — The action name.
if Zin.actions.reason("jump") == "unknownControl" then ... end

modules/ZinputActions/released

released(name: string): boolean

True if any boolean binding on the action was just released this frame. Suppressed by context gating.

Parameters

  • name string — The action name.
if Zin.actions.released("attack") then ... end

modules/ZinputActions/remove

remove(name: string)

Remove an action by name. No-op if not defined.

Parameters

  • name string — The action name to remove.
Zin.actions.remove("jump")

modules/ZinputActions/repeated

repeated(name: string, opts: { delay: number?, period: number? }?): boolean

Should a synthetic repeat fire this frame for the action? Returns true if any of the action's boolean key bindings reports State.keyRepeatFired. Mouse bindings are skipped (use a hold- time threshold for press-and-hold UX). Suppressed by context gating.

Parameters

  • name string — The action name.
  • opts { delay: number?, period: number? }? (optional) — Optional { delay, period } override of the global repeat defaults.
if Zin.actions.repeated("scrollLeft") then ... end

modules/ZinputActions/value

value(name: string): any

Read the action's current value.

  • Axis binding → number in [-1, 1]
  • Vector binding → { x, y } numbers in [-1, 1]
  • Boolean binding → 1 when held, 0 when not (consumers usually use held() instead; this exists so a single API works for any kind) When multiple bindings exist, the first one whose kind matches the caller's expectation wins (axis > vector > boolean in declaration order). When suppressed by context, returns the identity value for the first binding's kind: 0 for axis/boolean, { x = 0, y = 0 } for vector.

Parameters

  • name string — The action name.
local mv = Zin.actions.value("move")  -- { x, y }

modules/ZinputAxes/README

require("@builtin::modules.zinput.axes") -- ZinputAxes

DEPRECATED: An axis1 / axis2 control carries the same deadzone, smoothing, curve and invert, and reads a stick and a dragging thumb as well as a key. Author an .inputMap with .inputBinding children, activate it, and subscribe: local map = public.inputMap:activate() then map.move:onInput(fn). See man topics/input.

Usage: local ZinputAxes = require("@builtin::modules.zinput.axes")

modules/ZinputAxes/_resetGateWarnings

_resetGateWarnings()

Test-only: reset the once-per-axis gate-error warning state so a fresh suite can verify warning behavior again.

Zin.axes._resetGateWarnings()

modules/ZinputAxes/_resetUnknownWarnings

_resetUnknownWarnings()

Test-only: forget which names have already been reported as unregistered, so a fresh suite sees the warning again.

Zin.axes._resetUnknownWarnings()

modules/ZinputAxes/_setEnsureBindingsFn

_setEnsureBindingsFn(fn: () -> ())

Internal: wire the lazy-default-map hook. Called once at module- load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on a cold read (name not in the registry).
M._setEnsureBindingsFn(ensureDefaultBindings)

modules/ZinputAxes/_setEnsureLiveFn

_setEnsureLiveFn(fn: () -> ())

Internal: wire the liveness hook. Called once at module-load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on every cold-checked read below.
M._setEnsureLiveFn(ensureInputLive)

modules/ZinputAxes/_settled

_settled(): boolean

Internal: whether every axis sits at zero, current and target alike. The tick's quiescence gate reads it: the axes are read again after an evaluation or a change to the registry, and every call between those answers from the last read.

if Zin.axes._settled() then ... end

modules/ZinputAxes/advance

advance(dt: number)

Parameters

  • dt number

modules/ZinputAxes/clear

clear()

Wipe every axis and its state. Primarily for tests.

Zin.axes.clear()

modules/ZinputAxes/define

define(spec: AxisSpec)

Define one or more axes. Each entry replaces any existing axis of the same name; other axes are preserved.

Parameters

  • spec AxisSpec — Map of name -> { binding, deadzone?, smoothing?, curve?, invert?, context?, gate? }.
Zin.axes.define({ aim_x = { binding = ..., deadzone = 0.05 } })

modules/ZinputAxes/get

get(name: string): AxisDef?

Internal introspection: definition record (or nil).

Parameters

  • name string — The axis name.
local def = Zin.axes.get("aim_x")

modules/ZinputAxes/has

has(name: string): boolean

True if an axis with this name is defined.

Parameters

  • name string — The axis name.
if Zin.axes.has("aim_x") then ... end

modules/ZinputAxes/names

names(): { string }

All defined axis names, in arbitrary order.

for _, n in ipairs(Zin.axes.names()) do print(n) end

modules/ZinputAxes/raw

raw(name: string): any

Read the raw, unsmoothed, unshaped value of the underlying binding. For vector bindings returns {x,y}; for everything else returns a number in [-1, 1]. Useful for debugging or comparing pre/post processing.

Parameters

  • name string — The axis name.
print(Zin.axes.raw("aim_x"))

modules/ZinputAxes/reason

reason(name: string): string

Why a named axis is not delivering, from the closed set this registry can distinguish: unknownControl (no axis is registered under the name — the value readers answer zero, which is not a reading), contextInactive (registered, but its context is not on top of the stack), gateRefused (its own gate answered no), atRest (live, and the binding it reads is not being driven), or delivering.

Parameters

  • name string — The axis name.
if Zin.axes.reason("look") == "gateRefused" then ... end

modules/ZinputAxes/remove

remove(name: string)

Remove an axis. No-op if not defined.

Parameters

  • name string — The axis name.
Zin.axes.remove("aim_x")

modules/ZinputAxes/value

value(name: string): any

Read the smoothed, shaped, context-gated value. Scalar axes return a number in [-1, 1]. Vector axes return {x, y} numbers in [-1, 1]. Returns 0 / {x=0,y=0} if the axis isn't defined.

Parameters

  • name string — The axis name.
local mv = Zin.axes.value("move")  -- { x, y }

modules/ZinputMap/README

require("@builtin::modules.zinput.map") -- ZinputMap

DEPRECATED: An .inputMap asset with .inputBinding children is the map, and Zin.scheme is the live set: several can be live at once, each declaring the groups it suppresses. Zin.map.bake(name) writes the active map out as one, which is the migration. See man topics/input.

Usage: local ZinputMap = require("@builtin::modules.zinput.map")

modules/ZinputMap/_reactivate

_reactivate(record: any): any

Re-apply the active map from a changed record — the edit-in-place path an .inputMap asset takes when its source is written while it is live. Who asked for the map is carried across: a write to its source is the same map with new bindings, not a caller taking it up.

Parameters

  • record any (optional) — The map record, freshly read from its source.
Zin.map._reactivate(record)

modules/ZinputMap/_reset

_reset()

Test-only: clear active-map state (the profile registry keeps whatever was applied).

Zin.map._reset()

modules/ZinputMap/activate

activate(record: any): any

Activate a map record: materialize it and apply the flattened result as the live binding set (through the profile registry, so persistence and conflict surfaces keep working). Per-class axis bindings beyond the primary register as <axis>@<class> sibling axes; consumers that combine device values read both (e.g. look + look@touch).

Parameters

  • record any (optional) — The map (or profile) record, or an inputMap asset ref.
Zin.map.activate(require("@builtin::inputMaps.default"))

modules/ZinputMap/activeName

activeName(): string?

The active map's name, or nil.

if Zin.map.activeName() == "default" then ... end

modules/ZinputMap/addTouchButton

addTouchButton(actionName: string, buttonOpts: Bindings.TouchButtonOpts, emitKey: string?): any

Add a touchButton binding to an action's touch class on the active effective map, then re-flatten and re-activate so it takes effect immediately. Creates the action entry if actionName doesn't exist yet (the overlay's synthetic emit:<code> buttons).

Parameters

  • actionName string — The action to attach the button to.
  • buttonOpts Bindings.TouchButtonOpts — The options Zin.bindings.touchButton takes - the button's zone, label, icon glyph, layout and appearance.
  • emitKey string? (optional) — Optional key code — when set and the action has no kbm class yet, seeds it with B.key(emitKey) so a synthetic action is self-contained from the moment it's created.
Zin.map.addTouchButton("emit:KeyF", { label = "Cast" }, "KeyF")

modules/ZinputMap/bake

bake(name: string): any

Write the active effective map as a new inputMap asset — synthesis made explicit and editable. The snapshot includes every live entry, overlay-registered emit:<code> actions included. Returns the created ref.

Parameters

  • name string — The new asset's name.
Zin.map.bake("my_scheme")

modules/ZinputMap/bindingsFor

bindingsFor(eff: any, name: string, class: string): any

The effective bindings for one action or axis and device class.

Parameters

  • eff any (optional) — An effective map (from materialize/effective).
  • name string — The action or axis name.
  • class string — "kbm" | "gamepad" | "touch".
local touch = Zin.map.bindingsFor(eff, "jump", "touch")

modules/ZinputMap/effective

effective(): any

The active map's effective form, or nil before any activation.

local eff = Zin.map.effective()

modules/ZinputMap/ensureActive

ensureActive(): any

Ensure a map is active: keeps the current one, else activates the builtin default map. The bootstrap the on-screen controls and controllers call.

Zin.map.ensureActive()

modules/ZinputMap/isFallback

isFallback(): boolean

Whether the active map is the fallback ensureActive armed on its own, rather than one a caller activated. A reader that presents the map to a player — the on-screen controls — asks this to tell a scheme a world offered from the keyboard floor under a name that was read.

if not Zin.map.isFallback() then print(Zin.map.effective()) end

modules/ZinputMap/materialize

materialize(record: any): any

Materialize a map record into its effective form: extends chain resolved (child wins per action/axis/class), then the touch class synthesized from the kbm shape wherever absent. Returns { name, description, actions = { [name] = { context, classes, synthesized = { touch = true? } } }, axes = { ... } }.

Parameters

  • record any (optional) — The map (or profile) record.
local eff = Zin.map.materialize(require("@builtin::inputMaps.default"))

modules/ZinputMap/removeTouchButton

removeTouchButton(actionName: string, binding: any): boolean

Remove a touchButton binding previously added via addTouchButton, then re-flatten and re-activate. Drops the action entry entirely once every class is empty — cleanup for synthetic emit:<code> actions the overlay created.

Parameters

  • actionName string — The action the binding was added to.
  • binding any (optional) — The binding table returned by addTouchButton.
Zin.map.removeTouchButton("emit:KeyF", binding)

modules/ZinputObserve/README

require("@builtin::modules.zinput.observe") -- ZinputObserve

Usage: local ZinputObserve = require("@builtin::modules.zinput.observe")

modules/ZinputObserve/_publish

_publish()

Internal: publish this layer's half of the engine's input observation for the current frame. Called once per frame from Zin.tick while Zin.observe.wanted() holds.

Zin.observe._publish()

modules/ZinputObserve/arm

arm(on: boolean?)

Hold the engine's input observation open, so /runtime/input and input.observe() carry this layer's half of the document every frame. A read of either arms it for a window of frames on its own; this is for a test or a tool that wants it building continuously.

Parameters

  • on boolean? (optional) — Arm (the default) or disarm.
Zin.observe.arm(true)

modules/ZinputObserve/armedFrames

armedFrames(): number

How many more frames the arming window has left. A read of Zin.observe.frame(), input.observe() or /runtime/input sets it back to the full window; every frame that passes takes one off it, and 0 means nothing is observing.

print(Zin.observe.armedFrames())

modules/ZinputObserve/control

control(name: string): any

Everything known about one named control in a single call: which maps contribute it, its bindings per device class, its subscriber count, the value it reported on the most recent tick, whether that reached a subscriber, and — when it is live and silent — why.

Parameters

  • name string — The control name.
local c = Zin.observe.control("move")

modules/ZinputObserve/frame

frame(): any

The mapping layer's account of the most recent tick: every live map, every live control with what it did and why, and what the tick cost.

The window is ONE tick — the most recent one. Reading consumes nothing, so any number of observers in the same frame all get the same answers.

local f = Zin.observe.frame()

modules/ZinputObserve/means

means(reason: string): string?

What one reason name means, or nil for a name outside the set.

Parameters

  • reason string — The reason name.
print(Zin.observe.means("gateRefused"))

modules/ZinputObserve/reasons

reasons(): { any }

The closed set of reasons a control resolves to, each with what it means and what to do about it. The resolver answers with exactly one of these names.

for _, r in ipairs(Zin.observe.reasons()) do print(r.name, r.means) end

modules/ZinputObserve/wanted

wanted(): boolean

Whether the engine wants this layer's half of the input observation built this frame — true while something has read input.observe() or /runtime/input recently enough.

if Zin.observe.wanted() then ... end

modules/ZinputObserve/whySilent

whySilent(name: string): any

Why a named control is not reaching the game right now, as one reason from the closed set Zin.observe.reasons() lists, with the particulars behind it.

Resolves against the devices as they are at the moment of the call, so it answers for a control the tick has never reached and for one that does not exist. The layer field says which of the three naming layers answered — the live maps' controls, the action registry, or the axis registry — since a name can be live in one and unknown in the others.

Parameters

  • name string — The control name.
local why = Zin.observe.whySilent("look")

modules/animGraph/README

require("@builtin::modules.api.engine.animGraph") -- animGraph (also available as global 'animGraph')

Per-entity engine animation graph over the __animGraph FFI: clips, mixers, 2D blend spaces, crossfade, weights, and graph state. Each call routes onto the entity's AnimGraphComponent (auto-resolved from script context or passed explicitly).

Usage: local animGraph = require("@builtin::modules.api.engine.animGraph") Also available as global: animGraph

modules/animation/README

require("@builtin::modules.api.engine.animation") -- animation (also available as global 'animation')

Observe what the engine is animating: every body it is posing, the clips driving each one and where their playheads are, how many of a body's bones a clip's retarget reached, and — for a body that is not moving — the one reason why, from a closed set. Ragdoll a skinned character or creature (animation.ragdoll): its skeleton carried by physics bodies for a death, a knockout, a hit reaction or a limp limb, every bone body a body the Physics calls take.

Usage: local animation = require("@builtin::modules.api.engine.animation") Also available as global: animation

modules/animation/animating

animating(): { AnimationBody }

The bodies the engine measured a changing pose on — what is animating right now.

for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end

modules/animation/bodies

bodies(): { AnimationBody }

Every body the engine holds animation state for.

for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end

modules/animation/body

body(entityId: string | EntityRef): AnimationBody?

The report for one body, or nil when the engine holds no animation state for it. Accepts the body itself or any ancestor of it, so a character root answers for the skinned body underneath it.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local b = animation.body(hero.id); print(b and b.reason)

modules/animation/clips

clips(entityId: string | EntityRef): { AnimationClip }

The clips contributing to a body's pose right now, with their playheads and their retarget coverage.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end

modules/animation/coverage

coverage(entityId: string | EntityRef): (number, number)

How many of a body's bones the clips driving it actually reach. Returns (matched, total). A clip that retargets onto nothing reads (0, 50) while its playhead advances; a partial retarget reads its own count, so 3 of 50 is as visible as none.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local m, t = animation.coverage(hero.id); print(m .. "/" .. t)

modules/animation/declare

declare(entityId: string, facts: { [string]: any })

Publish what an animator is running on a body, so the observation names its clips, playheads and retarget coverage beside the pose the engine measures. The shipped animators declare through AnimGraph:publish; a custom animator calls this itself, once per frame it runs.

Parameters

  • entityId string — The body the animator drives.
  • facts { [string]: any } — { driver, bound, playing, outputKind, failure, clips }, where each clip is { name, nodeKind, time, duration, playing, finished, looping, weight, matched, total, unmatched }.
animation.declare(body.id, { driver = "MyAnimator", playing = true, clips = {} })

modules/animation/forget

forget(entityId: string)

Drop the declaration and the pose evidence the engine holds for one body. An animator calls this when it releases a body, so the observation reports the body as undriven from the next frame.

Parameters

  • entityId string — The body to drop.
animation.forget(body.id)

modules/animation/observe

observe(): AnimationObservation

Report what the engine is posing right now and why a body is not moving. One read covering every body the engine holds animation state for, each with the pose evidence the engine measured on its armature beside the clips the animator driving it declared. Answers in edit mode as well as play mode.

local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end

modules/animation/ragdoll

ragdoll(entityId: string | EntityRef, opts: (boolean | RagdollOptions)?): string

Ragdoll a skinned body: its skeleton is carried by physics bodies that fall, tumble and pose the mesh, for a death, a knockout, a hit reaction, a stunt fall or a limp limb. Works on any rigged skeleton: a humanoid (the built-in avatar and any imported humanoid rig) gets bodies sized for each body part and anatomical joints (hinged knees and elbows, cones at the shoulders, hips and neck); any other rig (a quadruped, a bird, a tail, a robot arm) gets a capsule along every bone and ball joints. The first call builds the ragdoll on the next frame, false included (its bodies follow the animation until it is switched on, so its bones can be read and jointed first); animation.ragdollState reads it back. false hands the body back to its animation, and true ragdolls it again.

The options table takes active (default true), limp (bone names: only those bones and the bones below them go limp while the rest keep animating), mass (the whole body's mass in kilograms; by default bodies weigh 1000 kg per cubic metre of collider, about 120 kg for the built-in avatar, so pass a person's 60 to 90 kg for a lifelike one) and bones (bone name -> { shape = "capsule" | "box" | "sphere" | "none", radius, length, size = {x, y, z}, mass, joint = "ball" | "hinge" | "fixed", swing, twist, hinge = {min, max}, strength, damping }, sizes in metres and angles in degrees). A description different from the last one rebuilds the ragdoll. On a humanoid a bone is also named by its humanoid role (hips, spine, chest, neck, head, leftshoulder, leftarm, leftforearm, lefthand, leftupleg, leftleg, leftfoot and the right ones), so one description fits every humanoid rig.

Each bone body is the body of that bone's armature entity (its id in ragdollState), so every Physics call takes it: Physics.addJoint to hold a hand to a ledge (breakable with breakForce), Physics.applyImpulseAtPoint for a hit, Physics.getVelocity, Physics.bodyState and Physics.contacts to measure a fall.

Parameters

  • entityId string | EntityRef — The skinned entity, or any entity above it (a character root).
  • opts (boolean | RagdollOptions)? (optional) — true / false, or a RagdollOptions table.
animation.ragdoll(enemy.id)                       -- collapse it
animation.ragdoll(enemy.id, false)                -- back to its animation
animation.ragdoll(hero.id, { limp = { "leftarm" } })  -- one limp arm
animation.ragdoll(dummy.id, { mass = 75, bones = { head = { mass = 5, swing = 30 }, leftleg = { hinge = { -140, 0 } } } })
local hand = animation.ragdollBone(hero.id, "righthand"); Physics.addJoint(hand, ledge, { kind = "ball", breakForce = 3000 })

modules/animation/ragdollBone

ragdollBone(entityId: string | EntityRef, bone: string): string?

The entity a ragdoll's bone body answers to, by bone name or, on a humanoid, by humanoid role (head, righthand, ...): the id to pass Physics.addJoint, Physics.applyImpulseAtPoint, Physics.getVelocity, Physics.bodyState, Physics.contacts and every other Physics call. nil until the ragdoll is built, or when that bone has no body of its own.

Parameters

  • entityId string | EntityRef — The skinned entity, or any entity above it.
  • bone string — The bone's name in the skeleton.
local head = animation.ragdollBone(dummy.id, "head"); print(Physics.getVelocity(head))

modules/animation/ragdollState

ragdollState(entityId: string | EntityRef): RagdollState?

The ragdoll built on a skinned body: whether the simulation drives it, its total mass, and every bone body with the entity it answers to (the bone's armature entity), its shape and size in metres, its mass read off the solver, whether it is simulated, and its joint. built reads false until the engine has built it (the frame after animation.ragdoll), and problems names anything the description asked for that the skeleton could not give. Under multiplayer replication reads which peer simulates the ragdoll (the owner of the synced entity at or above the body; every other peer shows it from that peer's bone poses) and what it costs on the wire.

Parameters

  • entityId string | EntityRef — The skinned entity, or any entity above it.
local r = animation.ragdollState(hero.id); for _, b in r.bones do print(b.bone, b.mass, b.joint and b.joint.kind) end

modules/animation/whyStill

whyStill(entityId: string | EntityRef): (string?, string?)

Why the body on an entity is not animating. Returns nil when it IS animating, and otherwise one of deactivated, noRiggedSkeleton, noGraph, clipUnreadable, noOutputNode, retargetPending, retargetMatchedNoRoles, stopped, finished, paused, poseNotApplied, poseUnchanged — the nearest cause, so the answer names the thing to change. A second return carries the animator's own words when it could not build a graph.

An entity the engine holds no animation state for is answered from the entity itself, in the same order the engine resolves a body it does hold: one carrying no rigged Skeleton is noRiggedSkeleton, and a rigged one nothing drives is noGraph. An id no entity carries is neither — the reason is nil and the detail says so.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end

modules/antiAliasing/README

require("@builtin::systems.antiAliasing.antiAliasing") -- antiAliasing

Edge antialiasing from the finished frame — the stair-stepping along a high-contrast silhouette is replaced by a gradient, without softening the regions on either side of it.

Usage: local antiAliasing = require("@builtin::systems.antiAliasing.antiAliasing")

modules/antiAliasing/active

active(): boolean

Whether the antialiasing pass is running this frame.

if antiAliasing.active() then ... end

modules/antiAliasing/disable

disable()

Turn edge antialiasing off and release the pass. The settings are kept, so a later enable() brings back the same tuning.

antiAliasing.disable()

modules/antiAliasing/enable

enable(opts: AntiAliasingOpts?): AntiAliasingState

Turn edge antialiasing on and set it. Any omitted field keeps its current value.

Parameters

  • opts AntiAliasingOpts? (optional) — Antialiasing settings — see AntiAliasingOpts.
antiAliasing.enable({ edgeThreshold = 0.125 })

modules/antiAliasing/get

get(): AntiAliasingState

The antialiasing settings currently in force.

local t = antiAliasing.get().edgeThreshold

modules/antiAliasing/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = antiAliasing.paramsBuffer()

modules/ao.shared/README

require("@builtin::systems.ambientOcclusion.aoShared") -- ao.shared

The parts every ambient-occlusion technique has in common: the WGSL prelude its kernel is written against, and the render targets, filter and passes that turn a per-pixel visibility term into shading.

A technique differs from its siblings in one thing only: how it estimates, for one pixel, what fraction of the hemisphere above the surface is visible. Everything either side of that estimate — reconstructing the surface from depth and normal, sizing the screen-space reach of a world-space radius, filtering the result across the noise the estimate carries, and multiplying it into the frame — is the same work whichever estimate produced it. So it lives here once. A technique file is its kernel and nothing else, which is what makes the three readable against each other.

Usage: local ao.shared = require("@builtin::systems.ambientOcclusion.aoShared")

modules/ao.shared/feature

feature(name: string, kernel: AssetRef<computeShader>) -> table

Build the render feature for one technique from its kernel shader.

Parameters

  • name string
  • kernel AssetRef<computeShader>

Returns table

modules/ao.shared/scale

scale() -> number

The fraction of the frame's resolution the occlusion is computed at.

Returns number

modules/ao.shared/setScale

setScale(scale: number)

Parameters

  • scale number

modules/ao/README

require("@builtin::systems.ambientOcclusion.ao") -- ao

Screen-space ambient occlusion — the contact darkening in corners, creases and where objects meet. Without it every concave region is lit exactly like a flat one.

Usage: local ao = require("@builtin::systems.ambientOcclusion.ao")

modules/ao/active

active(): boolean

Whether the occlusion pass is running this frame.

if ao.active() then ... end

modules/ao/clear

clear()

Turn occlusion off and release the pass. The other settings are kept, so a later set({ intensity = ... }) brings back the same look.

ao.clear()

modules/ao/get

get(): AoState

The occlusion settings currently in force.

local r = ao.get().radius

modules/ao/paramsBuffer

paramsBuffer(): any?

The buffer the AO kernel reads. It belongs to whichever technique is running, and is remade when the technique swaps, so the pass binds what this hands it rather than looking it up.

local p = ao.paramsBuffer()

modules/ao/qualityLevels

qualityLevels(): { [string]: { slices: number, steps: number, scale: number } }

What each quality level costs: slices × steps taps per pixel (total slices * steps * 2, the same for every technique), computed on a grid scale of the frame's resolution.

local cost = ao.qualityLevels().ultra

modules/ao/set

set(opts: AoOpts?): AoState

Set the scene's ambient occlusion. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. An intensity of 0 turns occlusion off and releases the pass.

Parameters

  • opts AoOpts? (optional) — Occlusion settings — see AoOpts.
ao.set({ technique = "hbao", intensity = 0.9, radius = 1.5 })

modules/ao/techniques

techniques(): { [string]: string }

The techniques that can be selected, each with what it does.

for name in pairs(ao.techniques()) do print(name) end

modules/asset/README

require("@builtin::modules.api.engine.asset") -- asset (also available as global 'asset')

Asset resolver, ref envelope builder, sidecar metadata. Public Luau surface over the __asset Internal FFI namespace.

Usage: local asset = require("@builtin::modules.api.engine.asset") Also available as global: asset

modules/asset/add_tag

add_tag(ref: RefArg, tag: string)

Add a tag to the asset's .metadata.tags. Idempotent. Creates the sidecar and the tags array if missing.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to add.
asset.add_tag("brick", "wip")

modules/asset/alias

alias(ref: RefArg, alias: string): boolean

Add a name the asset answers to. asset.resolve, leaf shorthand, and the typed-argument coercion that content refs travel through all reach the asset by the alias from here on, exactly as they do by its identity — so a material naming a shader by its alias resolves, and content written against an older name keeps working after a rename. The name survives writes to the asset's own files. Raises when the name already resolves to a DIFFERENT asset — an alias extends the identity namespace and never takes a name out of another asset's hands — and when it is shaped like a guid or a VFS path, forms that resolve before identity lookup, so an alias in that shape could never answer.

Parameters

  • ref RefArg — The asset gaining the name.
  • alias string — The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
asset.alias("@builtin::shaders.pbr", "standard")

modules/asset/aliases

aliases(ref: RefArg): { string }

The additional names this asset answers to, beyond its own identity — what asset.alias registered, plus the package-relative ~pkg.tail form when the asset lives inside a package.

Parameters

  • ref RefArg — Any name the asset has.
for _, n in asset.aliases("pbr") do print(n) end

modules/asset/canCreate

canCreate(typeName: string): boolean

Whether asset.create can instance typeName: the type declares creation logic (a behavior.luau onCreate hook) or ships a template/ skeleton the hookless fallback clones. A type with neither — one whose instances only arrive by import — answers false. The query a creation UI derives its offering from, so what it offers is what asset.create accepts.

Parameters

  • typeName string — Registered asset type (e.g. "material", "scene").
if asset.canCreate(kind) then asset.create(kind, name) end

modules/asset/categories

categories(): { string }

List every asset category the engine currently recognises. Use to discover valid type argument values for the rest of asset.*.

for _, c in asset.categories() do print(c) end

modules/asset/containing

containing(path: string): AssetRef?

Walk path and its ancestors and return an AssetRef handle for the INNERMOST registered category-folder containing it — the typed asset that directly owns the path (e.g. main.scene for "/source/scenes/main.scene/scene.json"; for a folder that is itself a registered asset, that asset). Returns nil for paths outside any registered asset.

Parameters

  • path string — VFS path to inspect.
local a = asset.containing("/source/scenes/main.scene/scene.json")

modules/asset/cpuResident

cpuResident(ref: RefArg, typeName: string?): boolean

True when the asset is CPU-resident — a live script-component context holds it (a component's assetRef field, or an imperative asset.resolve/ref made while a component is the caller), which is what warms its bytes into memory. The CPU pool is a different pool from the device's: asset.observe().cpu lists it, asset.observe().textures / .meshes list what the device holds, and an asset can be in one and not the other.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
if asset.cpuResident(ref) then print("bytes are warm") end

modules/asset/create

create(typeName: string, name: string, opts: { [string]: any }?): (AssetRef, { durable: boolean, survivesRestart: boolean?, warning: string?, playShadow: string?, shadowed: { string }?, scratch: { string }? })

Instance a new asset of an existing type. Runs the type's behavior.luau onCreate(name, opts) hook to produce the asset's files, then writes them under /source/<folder>/<name>.<type>/ (/source/<name>.<type>/ when no folder is given). This is the single generic asset-creation API. Refuses to clobber an existing edit-mode asset unless opts.overwrite = true, which re-authors it in place and keeps the existing guid (only the checksum changes). Pairs with asset.exists for content generators that re-run over the same names.

A create made from a script component's callback or a scene entrypoint is output the world reproduces on every load, so it is filed in the ephemeral /runtime/assets/ store instead, where the saved manifest never carries a second copy of it. name and folder spell the same IDENTITY in either store, so a reference written against that identity resolves the asset wherever the call filed it, and one generator run from an execute and from a component names one asset.

Parameters

  • typeName string — The asset category to instance — one of asset.categories().
  • name string — The new asset's name.
  • opts { [string]: any }? (optional) — Optional table forwarded to the type's onCreate hook, minus five framework keys consumed here and never seen by the hook: folder (a relative subfolder path under /source to author the asset in, with / between its folders as in game/materials, so generated content groups instead of accumulating at the source root; those folders joined by dots prefix the asset's identity, game.materials.<name>), into (author INSIDE a resolved container ref), dest (an absolute destination path), overwrite (re-author in place, keeping the guid), and durable (file every content file this create writes on canonical /source with play still running, in place of the play shadow, and raise with the reason when they cannot become source, the same key, meaning the same thing, that vfs.write takes). Every type accepts all five whatever its own contract declares; asset.describe(typeName).frameworkParams names them, and a rejection prints them under the type's own parameters.
local m = asset.create("material", "brick", { shader = "pbr" })
local s = asset.create("shader", "glow", { folder = "fx" }) -- s.path: /zero/source/fx/glow.shader, s.primary: /zero/source/fx/glow.shader/shader.wgsl
local wgsl = vfs.read(s.primary) -- the created asset's content file, read by the path the ref names
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild
local clip, d = asset.create("soundClip", "hit", { pcm = buf, sampleRate = 48000, channels = 1 })
if not d.durable then log.warn(d.warning) end
local c = asset.create("soundClip", "step", { pcm = buf, sampleRate = 48000, channels = 1, durable = true }) -- lands on source during play

modules/asset/declareReferenceArg

declareReferenceArg(call: string, position: number, assetType: string)

Declare that call's argument at 1-based position names an asset of type, so a string literal written there is recorded as a reference. The positional counterpart of asset.declareReferenceField, for a call that takes its asset as a plain argument — including a world's own spawn helper, which is where a name most often stops being visible to the reference graph. A lookup whose asset is its FIRST argument (asset.resolve and its siblings) is already read and needs no declaration. Only a literal — or a name the file holds in a top-level string constant — is recorded; anything computed is reported as a dynamic resolve. An asset type states the same declaration as content, in the references.args of its type.yaml, which every reference table reads from the moment the type registers.

Parameters

  • call string — The callee as it is written at a call site.
  • position number — Which argument holds the name, counting from 1.
  • assetType string
asset.declareReferenceArg("spawnModel", 3, "mesh")

modules/asset/declareReferenceField

declareReferenceField(call: string, field: string, assetType: string)

Declare that call's options table names an asset of type in its field, so a string literal written there is recorded as a reference by whatever writes the file. This is what puts an API that takes an asset BY NAME into the reference graph: the named asset becomes a dependency, travels with the content that names it into a pack or a pull, and a name nothing answers to becomes an unresolved dependency worldValidation reports and the push gate refuses. A field holding a TABLE of names — a material's textures — records every name in it. Declare once, beside the API; a call taking its asset as the FIRST positional argument is already read and needs no declaration. Only a literal is recorded; a computed name resolves at runtime and is reported as a dynamic resolve. An asset type states the same declaration as content, in the references.fields of its type.yaml, which every reference table reads from the moment the type registers.

Parameters

  • call string — The callee as it is written at a call site.
  • field string — The options-table field holding the name, read at the table's own level.
  • assetType string
asset.declareReferenceField("fx.beam", "material", "material")

modules/asset/declareReferenceKey

declareReferenceKey(assetType: string, key: string, refType: string, via: ("asset_ref" | "settings_of")?)

Declare that, in a data file belonging to an assetType asset, the top-level key names an asset of type ("*" for any type) — a .material's mat.yaml naming the shader it draws with and the textures it binds. The names a format holds are references as surely as ones written in code: recording them carries a material's shader along with the material into a pack or a pull, and turns a name nothing answers to into an unresolved dependency instead of a surface that renders as the magenta error material. A key holding a table of names records one per entry; a { __ref = <guid> } envelope names its guid. via is the provenance the file's .refs records: "asset_ref" (default) for an asset the file uses, "settings_of" for the declarer whose settings schema the file's values follow. A type states the keys of its own files as content, in the references.keys of its type.yaml, which every reference table reads from the moment the type registers.

Parameters

  • assetType string — The category owning the file, e.g. "material".
  • key string — The top-level key holding the name(s).
  • refType string
  • via ("asset_ref" | "settings_of")? (optional) — "asset_ref" or "settings_of".
asset.declareReferenceKey("material", "shader", "shader")
asset.declareReferenceKey("preset", "target", "*", "settings_of")

modules/asset/deprecation

deprecation(ref: RefArg): Deprecation.Deprecation?

Whether an asset is deprecated, why, and what replaces it. An asset is deprecated when its own .metadata carries "deprecated": { "reason": "<one line why>", "replacedBy": "<identity>" }, when a container holding it at any depth does (a package, a toolbox, a folder asset), or when its asset type does. A deprecated asset still resolves and loads by identity and guid; search surfaces do not offer it, and resolving it warns once per session.

Parameters

  • ref RefArg — Asset handle, identity, guid or VFS path.
local d = asset.deprecation("@builtin::deprecated.deprecatedParticles.DeprecatedParticleEmitter")

modules/asset/deps

deps(ref: RefArg, type: string?): DepsResult

Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end

modules/asset/describe

describe(typeName: string): DescribeResult

The creation contract for an asset type: the parameters its onCreate(name, opts) hook accepts, as data. kind is "schema" (typed contract), "legacy" (untyped opts — anything passes), "none" (template scaffold — takes no opts), or "error" (the type's schema failed to parse; error says why). contract is the human-readable rendering validation errors print. frameworkParams and frameworkContract carry the options asset.create consumes itself: folder, into, dest, overwrite, durable, which every type accepts and none declares.

Parameters

  • typeName string — Registered asset type to describe (e.g. "texture").
local contract = asset.describe("texture").contract

modules/asset/diagnose

diagnose(ref: RefArg): any

Why one asset can or cannot be used, read from the engine rather than from what the caller asked for. Always carries usable; when false, reason is one of asset.unusableReasons() and detail is the engine's own message. primary names the file the type's declared primary list resolved to, the file a load of the asset reads. The payload's bytes are read by the engine's own decoder wherever it has one for that container, so usable is the verdict a load would reach and the call costs that decode.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end

modules/asset/exists

exists(name: string, typeName: string): boolean

Parameters

  • name string
  • typeName string

modules/asset/get_field

get_field(ref: RefArg, key: string): any

Read one top-level field from the asset's .metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
local author = asset.get_field("brick", "author")
local tags = asset.get_field("tree", "tags") -- → a Luau table

modules/asset/gpuResident

gpuResident(ref: RefArg): boolean

True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))

modules/asset/guid

guid(ref: RefArg, type: string?): string

Return the guid for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local g = asset.guid("@builtin::components.Camera")

modules/asset/has_field

has_field(ref: RefArg, key: string): boolean

True when the asset's .metadata carries the named field.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
if asset.has_field("brick", "author") then end

modules/asset/has_tag

has_tag(ref: RefArg, tag: string): boolean

True when the asset's .metadata.tags contains tag.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to check for.
if asset.has_tag("brick", "wip") then end

modules/asset/identity

identity(ref: RefArg, type: string?): string

Return the canonical identity for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local id = asset.identity("brick")

modules/asset/import

import(path: string, opts: { importerSettings: { [string]: { [string]: any } }? }?): string?

Import a raw source file NOW and return the produced asset path (a .bundle for a model, .texture for an image, .audio for a sound, …), or nil if no importer claims it. While play runs and holds the source where it stands (vfs.playHoldsInPlace), the import is owed: this returns nil, the source's job in tools.use("importers", "status") reads owed, and the import runs when play ends or pauses. This is the deterministic, on-demand counterpart to the engine's automatic import-on-write: it runs in the calling task and returns only when the import is complete. Pair it with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the raw bytes without firing the automatic importer, then asset.import(path) imports them under your control, so you can act on the result instead of polling for the import to appear.

Parameters

  • path string — The raw source VFS path to import (e.g. a just-written .glb).
  • opts { importerSettings: { [string]: { [string]: any } }? }? (optional) — Optional { importerSettings = { <importer name> = { <setting> = value } } }: settings for this import only, laid over the claiming importer's own. A setting that importer does not declare is an error.
local bundle = asset.import("/zero/source/generated/chest.glb")
local bundle = asset.import("/zero/source/generated/chest.glb", { importerSettings = { model = { keepSource = false } } })

modules/asset/inspect

inspect(ref: RefArg, type: string?): InspectRecord

Everything known about one asset in a single record: identity, guid, source, type, scope and origin, its description and tags, the ref methods its type exposes, and the type's own inspect detail when it declares one. The read-everything counterpart to asset.resolve, which hands back a ref.

Parameters

  • ref RefArg — An AssetRef, an identity string, or a path.
  • type string? (optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)

modules/asset/interface

interface(ref: RefArg, category: string?): AssetInterface?

What the asset declares it takes and gives, read from its source without running it: the interface its type's interface: block says its sources declare (a .procNode declares one). description is what it does; each of inputs carries its name, type, default, min, max, options, optional and description as the declaration states them (a vector default reads as { ["$vector"] = { x, y, z } }); each of outputs its name and from, what makes it. problems lists each part of the declaration that is not written as literals, which a push refuses. The same data travels with the source's reference table to ZeroMind.

Parameters

  • ref RefArg — Any name the asset has.
  • category string? (optional) — Category hint (optional).
local iface = asset.interface("@builtin::systems.procgen.nodes.points.disc", "procNode")

modules/asset/isPrivateSubasset

isPrivateSubasset(path: string): boolean

Whether the asset at path is a private subasset: nested inside another asset whose type does not broadcast its subassets — a .bundle's interior meshes, materials and textures, an effect's own materials. A private subasset is part of its parent: it is not listed, searched or installed on its own, and it is excused from the files that describe an asset. Whether nested assets are private is the enclosing asset's type's to say: broadcasts_subassets: true in its type.yaml (a .package) makes its members public, and an asset nested in no other asset is never private. The same rule asset.list applies, where subassets = true is what reaches private subassets.

Parameters

  • path string — The asset's VFS folder path.
asset.isPrivateSubasset("/zero/source/models/tree.bundle/material/Bark.material") -- true

modules/asset/list

list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?): ListResult

Query registered assets, returning each match as a resolved AssetRef handle. Every filter narrows the same enumeration and they compose: path selects a VFS subtree (the folder and everything under it), type keeps only those asset types within it, scope keeps only that scope, and fields keeps only assets whose .metadata matches. type and path each take one value or a list matching any of its entries, and all / any / none group whole filters — none excludes what it matches. order, limit, and offset shape the result: matches come back ordered by identity unless order names another field (name / path / type / guid). subassets decides how deep the enumeration reaches into composite assets. An enumeration returns the assets each container broadcasts, which its type declares: a .package and a .toolbox broadcast theirs, so their members are listed on their own, and a .bundle keeps its interior meshes, materials and rigs to itself, so they are reached through the bundle and a list of the folder holding it comes back without them. subassets = true enumerates those private interiors as well — what an asset is built out of, beside what the world offers. Each entry is the same envelope asset.resolve returns (__ref / type / name / guid / identity / path), so it can be passed anywhere an AssetRef is accepted, and the result carries :first() / :random() / :filter() / :sort() and friends. type takes the same values asset.categories() lists. The first positional argument is a path when it is absolute, a type otherwise. A type naming none of those values raises, naming it and any near spelling, so an empty result means a type the engine has. A type-group name stands for a family of those types rather than naming one (model for glb/gltf/fbx/obj, and image, text, config and script for theirs), so it lists whichever members the engine carries, and raises naming them when it carries none. An unknown key raises, as does a table setting both type and its older spelling category. A static (literal) type or path makes the enumeration part of the calling file's content dependencies when it is saved — the set travels with published content, so consumers get at-least the authoring world's assets.

Parameters

  • type_or_opts (AssetCategory | ListOpts)? (optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.
  • scope string? (optional) — Scope filter (when first arg is a type).
  • opts ListOpts? (optional) — The query table — see ListOpts.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()
local parts = asset.list({ path = "/zero/source/models", type = "mesh", subassets = true }) -- a bundle's interior meshes too

modules/asset/listRevision

listRevision(): (number, number)

Two numbers that together move whenever what asset.list could answer changes: an asset made, removed, moved or renamed, a name it answers to added, a file created in or removed from an asset's folder (a new entity in the open scene writes one), or the index rebuilt. Rewriting a file that already exists leaves them still, and so does a write to a path no asset can sit at. The copies play makes resident in the runtime-asset layer move neither number. Hold the pair beside something derived from a listing and list again only when either differs, so a per-frame reader lists only on the frames where the answer can have changed.

local e, g = asset.listRevision() if e ~= held.e or g ~= held.g then held.list = asset.list("texture") end

modules/asset/list_field_values

list_field_values(key: string): { any }

Distinct values seen for the named field across every asset's .metadata.

Parameters

  • key string — Field name.
local authors = asset.list_field_values("author")

modules/asset/list_fields

list_fields(): { string }

Distinct top-level field keys observed across every asset's .metadata. Useful for tooling discovering custom keys in use.

for _, k in asset.list_fields() do print(k) end

modules/asset/meta

meta(ref: RefArg, type: string?): AssetMeta

Read the asset's engine-owned identity record (guid / checksum). Distinct from .metadata (agent-editable); for that use asset.metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local m = asset.meta("brick") -- { guid = ..., checksum = ... }

modules/asset/metadata

metadata(ref: RefArg, type: string?): AssetMeta

Read the asset's agent-editable .metadata sidecar as a Lua table. Missing sidecar returns {}. Distinct from asset.meta (engine-owned).

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local md = asset.metadata("brick")

modules/asset/observe

observe(): any

What the engine is holding for content right now, in one reading: textures and meshes (one row per resource the device holds, each with the bytes it costs, its dimensions or buffer split, and where it came from), cpu (one row per asset a live script-component context holds), offDevice (one row per mesh nothing displays, whose GPU buffers were given back: held is "definition" when the engine keeps the mesh's whole CPU definition and "source" when it keeps only the stored file source it reads the mesh from again, and bytes is the CPU memory that costs), and totals: the aggregates those rows sum to, so the listing reconciles against renderer.textureMemory() and renderer.gpuMemory() (offDeviceBytes sums the offDevice rows).

Each pool is named because they are different pools: an asset can be on the device and not CPU-resident, or the reverse. devicePublished is false when no renderer has published an inventory and cpuPublished when the scripting VM has not published its pool — an engine that cannot answer reads differently from one answering with nothing resident.

local r = asset.observe() print(#r.textures, r.totals.textureBytes)

modules/asset/pathHolding

pathHolding(guid: string, checksum: string): string?

The VFS path holding the asset guid at exactly the version whose content checksum is checksum (the pair asset.meta reports): the guid's home when its bytes are that version, else a copy of that version held beside it, such as an installed dependency's. Returns nil when this world holds that asset at no such version.

Parameters

  • guid string — The asset's guid.
  • checksum string — The content checksum of the version wanted.
local m = asset.meta(asset.resolve("@builtin::shaders.pbr", "shader")); local path = asset.pathHolding(m.guid, m.checksum)

modules/asset/pending

pending(): { count: number, entries: { PendingReference } }

Every asset reference a component on this engine holds that no asset answers yet. A reference arrives ahead of its bytes when a peer delivers a component whose asset has not synced here, when the carried mount it names is still filling, or when a scene applies before the content it names. Each one applies by itself the moment its asset resolves: a component waiting on it runs its awake() then, and a write held for a component already running (held) lands then and reaches its onPropertyChanged. state is pending, or refused when the carried mount the reference names settled without its content; reason and detail say why. A refused reference applies if a later mount of that key brings the content. A reference inside a list or struct field waits the same way at its place in the field (path, such as skins[2] or look.skin), the field keeps every other value it holds, and the reference applies in place. Every peer of a world holds and applies the references it was delivered the same way.

local waiting = asset.pending()
if waiting.count > 0 then print(waiting.entries[1].reference, waiting.entries[1].state) end
repeat task.wait() until asset.pending().count == 0

modules/asset/preview

preview(ref: RefArg, opts: { [string]: any }?, type: string?): { [string]: any }

Render a preview of an asset. Resolves the ref and dispatches to its type's preview ref-method when present; otherwise returns the { available = false } sentinel ("no preview available for this type").

Parameters

  • ref RefArg — Any name the asset has.
  • opts { [string]: any }? (optional) — Optional { size = { width, height }, angle = { yaw, pitch } }.
  • type string? (optional) — Category hint (optional).
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })

modules/asset/primaryFile

primaryFile(ref: RefArg): any

The file the asset type's declared primary list resolves to inside this asset, as the loader itself resolves it. A *.<ext> entry resolves the lowest-named file with that extension, and never the asset's preview.png, the picture rendered from it. resolved is false when no declaration matched and path is then absent; declared is the type's own primary list in match order.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
print(asset.primaryFile("myTex").path)

modules/asset/ref

ref(ref: RefArg, type: string?): AssetRef

Build a reference handle for an asset — the canonical ref envelope constructor. Identical shape to asset.resolve; preferred name for the author-side use case (embedding refs in YAML / JSON / Luau output).

Naming the asset here reads exactly as naming it in asset.resolve, down to raising on a miss: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to reference — an identity, a guid, a VFS path, or a handle.
  • type string? (optional) — Category hint (optional).
local r = asset.ref("animations.idle", "animation")

modules/asset/reloadPending

reloadPending(ref: RefArg, typeName: string?): boolean

True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
repeat task.wait() until not asset.reloadPending(ref)

modules/asset/reloadSeq

reloadSeq(ref: RefArg, typeName: string?): number

How many content-change reloads this asset has been through — the count of onAssetReload dispatches the engine has RUN for it. A write to a file inside an asset does not reload it on the spot: the writes of one authoring step are collected for a settle window and the reload runs on a later frame. Read this, write, then poll for a larger number to learn the write's reload has actually reached subscribers. Monotonic per asset and session-scoped; 0 for an asset whose content has not changed since boot.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at

modules/asset/remove_field

remove_field(ref: RefArg, key: string)

Remove one top-level field from the asset's .metadata. No-op when the field isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
asset.remove_field("brick", "author")

modules/asset/remove_tag

remove_tag(ref: RefArg, tag: string)

Remove a tag from the asset's .metadata.tags. No-op when the tag isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to remove.
asset.remove_tag("brick", "wip")

modules/asset/resolve

resolve<C>(ref: RefArg, type: (C & string)?): AssetRef<C>

Find an asset. The returned handle carries every name form the asset has (guid, identity, path, type) so downstream code can read any one of them without calling resolve again. Raises when ref resolves to no asset — or, with a type, to no asset of that type — and when ref reaches more than one asset, where it names the candidates for you to pick from instead of picking one of them. A <scope>::-qualified identity reaches exactly one: @root::name for the asset this world holds at its source root, the library identity (@builtin::…) for a library's. For the same lookup answering a miss with nil, use asset.tryResolve(ref, type).

A name written as a string LITERAL is recorded as this source's dependency on that asset, so the asset travels with the content and still resolves once someone installs it in another world. A COMPUTED name cannot be written down, so nothing pins what it reaches: that is a dynamic resolve — free in a tool, refused on the gameplay path (a component or scene entrypoint). asset.tryResolve, asset.ref and asset.source read the name they are given exactly this way too, so which of the four you reach for changes neither answer. To ask whether a computed name has files without reaching a handle, use asset.exists(name, type).

Parameters

  • ref RefArg — The asset to find — an identity, a guid, a VFS path, or a handle.
  • type (C & string)? (optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.png after the texture importer turned it into wall.texture) resolves to the promoted asset, and says so in the log once per reference.
local a = asset.resolve("@builtin::components.Camera")

modules/asset/rootKinds

rootKinds(): { string }

The kind of every asset folder that is a root of its contents (its category declares root: true; a package is one too): a ~ written in a file inside such a folder names that folder. The same answer in every VM, the engine thread's and each worker's.

local roots = asset.rootKinds() -- { "effect", ... }

modules/asset/schema

schema(ref: RefArg): any?

The typed settings schema governing this asset — the Field descriptors its type declares for every asset of that type, or the ones the asset declares for itself. Each descriptor carries its kind, default, constraint and description, so an editor renders it and an agent reads what a setting takes without guessing.

Parameters

  • ref RefArg — The asset (ref, identity, guid or path).
local schema = asset.schema(texRef); print(schema.filter.constraint.values)

modules/asset/setSettings

setSettings(ref: RefArg, patch: { [string]: any })

Write a partial settings patch. The patch is merged over the asset's current values, the type's cross-field constraints are applied — a field the patch does not name moves to the only value the rest allows (a texture given a high-precision compression stops generating mipmaps), and a field the patch names that cannot hold refuses the write, saying why — and the result is checked against its schema; a violation refuses the whole write, naming every problem, and leaves the asset on its last valid settings. A valid write re-runs the type's onChange, which is where the type re-cooks whatever the settings describe. Older field names are accepted and stored under the current ones, and a field whose default follows another moves with it when the write does not name it.

Parameters

  • ref RefArg — The asset (ref, identity, guid or path).
  • patch { [string]: any } — Any subset of the schema's fields.
asset.setSettings(texRef, { filter = "nearest" })

modules/asset/set_field

set_field(ref: RefArg, key: string, value: any)

Set one field in the asset's .metadata, creating the sidecar if missing. Sibling fields are preserved. When the new value AND the existing value are both maps (objects), the new value DEEP-MERGES into the existing one, so writing one sub-key never drops the others — set_field(ref, "provenance", { bakedBy = "agent" }) keeps every other provenance key. Arrays and scalars replace. Clear a whole field with asset.remove_field; replace the entire sidecar with asset.set_metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
  • value any (optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "provenance", { bakedBy = "agent" }) -- merges; sibling keys kept

modules/asset/set_metadata

set_metadata(ref: RefArg, data: AssetMeta)

Replace the asset's .metadata sidecar with the given table. Pass an empty table to clear all fields.

Parameters

  • ref RefArg — Any name the asset has.
  • data AssetMeta — Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })

modules/asset/settings

settings(ref: RefArg): any?

This asset's settings values, with every declared default filled in.

Parameters

  • ref RefArg — The asset (ref, identity, guid or path).
if asset.settings(texRef).filter == "nearest" then ... end

modules/asset/source

source(ref: RefArg, type: string?): string

Return the VFS source path for an asset.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local p = asset.source("brick") -- "/source/brick.material"

modules/asset/storeHoldsIdentity

storeHoldsIdentity(identity: string, typeName: string, reproduced: boolean): boolean

Whether an asset of type typeName that name reaches has files in the store asset.create writes from this context — the authored /source tree, plus the ephemeral /runtime/assets/ store when the caller is a script component or a scene entrypoint, which is where a create there files it. name is any name the registry answers to for that asset: its canonical identity ("a.b.thing"), a registered alias, or the bare leaf ("thing") — the spelling asset.list publishes as an entry's name and the spelling asset.create was given alongside opts.folder. So the if not asset.exists(n, t) then asset.create(t, n, opts) end pairing is satisfied by its own create, spelled the way the create was, in either store and at any folder depth. A plain existence probe — it does NOT resolve a handle or pin a content dependency, so it is safe to call with a COMPUTED name (unlike asset.resolve, whose handle would become a static-pinned dependency).

The name is read the way asset.resolve reads one; the ANSWER comes from the store, so a registered asset whose files live outside the store this context writes — every @builtin:: asset among them — is false. asset.tryResolve(name, typeName) asks the registry the wider question and hands back the handle — and because it reaches the asset, the name it is given follows the reference rule every lookup follows: a literal is pinned, a computed one is a dynamic resolve. So a name this source computed is what asset.exists is for. This call names the asset FIRST and its category second, as every asset.* call taking both does except asset.create(category, name, opts). A call carrying a category where the asset goes and a non-category where the category goes is refused, naming which way round the call reads, rather than reporting the asset absent.

Parameters

  • identity string
  • typeName string — The asset type (e.g. "mesh", "texture", "material") — one of asset.categories().
  • reproduced boolean
if not asset.exists(meshName, "mesh") then asset.create("mesh", meshName, geo) end
if not asset.exists(n, "mesh") then asset.create("mesh", n, { folder = "props", positions = p, indices = i }) end

modules/asset/tags

tags(ref: RefArg): { string }

Convenience read of the .metadata.tags array.

Parameters

  • ref RefArg — Any name the asset has.
for _, t in asset.tags("brick") do print(t) end

modules/asset/tryResolve

tryResolve(ref: RefArg, type: string?, base: string?): AssetRef?

The same lookup asset.resolve performs, answering a miss with nil instead of raising. Every name form, the same type narrowing, and the same handle on success — so "use it if it is there" needs no pcall around a call whose failure would otherwise be indistinguishable from a real error.

This consults the asset REGISTRY, so it sees registered assets wherever their files live, @builtin:: ones included. asset.exists(name, type) answers the narrower question of whether an asset's files are present in the current mode's store.

A name that reaches more than one asset of type (or more than one asset, with no type) still raises: that is a question about the reference, and a nil there would report absence for content that is present twice. A name that only assets of other types answer to reaches no asset of type, and answers nil.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to look up — an identity, a guid, a VFS path, or a handle.
  • type string? (optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same as asset.resolve.
  • base string? (optional) — Optional VFS path of the asset the reference is written in, or of its file: a data asset resolving the references it holds, such as a material resolving its mat.yaml. A ~ / ~.tail ref expands against the root enclosing it, and a ref its .refs recorded reaches the asset recorded there. Without it, both are read at the calling Luau source, as asset.resolve reads them.
local mat = asset.tryResolve(name, "material")

modules/asset/typeRef

typeRef(target: RefArg): string?

Return the pinned asset_type reference (the type's guid) that the asset is an instance of. Resolve the full type with asset.resolve(asset.typeRef(target)). Returns nil for loose files / assets with no pinned type.

Parameters

  • target RefArg — Asset handle / identity / guid / VFS path.
local t = asset.resolve(assert(asset.typeRef("brick")))

modules/asset/unusableReasons

unusableReasons(): { string }

Every reason asset.diagnose can report an asset unusable for, sorted.

for _, r in ipairs(asset.unusableReasons()) do print(r) end

modules/asset/validate

validate(ref: RefArg, type: string?): ValidateResult

Validate an asset folder against its type's type.yaml, plus the type's own semantic validation. Structural problems come from type.yaml — missing required files, unsatisfied one_of_group alternatives, and (when allow_unlisted: false) unexpected children. validated = false when no type.yaml is registered — nothing structural to check. On top of that, when the asset's type ships a behavior.luau exporting a top-level validate(assetRef) -> { { code, message, severity? } }, its reported problems (severity defaults to "error") are appended to problems; error-severity problems flip ok to false, warnings leave it untouched. A hook that raises or returns a non-table is itself reported as a validate.hook_failed error problem — a broken hook blocks. A type with no validate export behaves exactly as the structural check alone. world.push calls this per user asset, so a type's semantic validation is enforced at publish time with no further wiring.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local v = asset.validate("@builtin::components.Camera")

modules/asset/warmup

warmup(ref: RefArg, opts: WarmupOpts?): WarmupResult

Warm an asset's bytes into CPU memory and follow its declared content dependencies to each referenced asset, deduped by guid. Type-agnostic (reads the generic ref graph) and CPU-only — never touches the GPU. Side-effect-free name resolution (uses asset.guid/asset.deps, not asset.resolve).

Parameters

  • ref RefArg — Any name the root asset has — handle, identity, guid, or path.
  • opts WarmupOpts? (optional) — Optional { vias, max } — restrict ref-edge kinds / cap closure size.
local w = asset.warmup("@builtin::scenes.test_arena")

modules/asset_tag/README

require("@builtin::modules.asset_tag") -- asset_tag

The assetTag field-constraint validator: a constrained value must be an asset carrying constraint.tag in its .metadata.tags. This is what lets a slot declare the KIND of asset it takes — a camera behavior, a player visual — without naming the assets themselves, so a new asset becomes assignable the moment it is tagged. Registers itself with the generic field_constraints registry on load. nil (no asset) passes — the field is optional.

Usage: local asset_tag = require("@builtin::modules.asset_tag")

modules/atmosphere/README

require("@builtin::systems.atmosphere.atmosphere") -- atmosphere

Physically-based atmospheric scattering — aerial perspective on distant geometry, and a sky whose colour follows from the sun's elevation rather than from an authored gradient.

Usage: local atmosphere = require("@builtin::systems.atmosphere.atmosphere")

modules/atmosphere/active

active(): boolean

Whether the atmosphere passes are currently running.

if atmosphere.active() then print("scattering") end

modules/atmosphere/clear

clear()

Turn the atmosphere off and release its passes. The other settings are kept, so a later set({ aerial = ... }) brings back the same look.

atmosphere.clear()

modules/atmosphere/get

get(): AtmosphereState

The atmosphere settings currently in force.

local km = atmosphere.get().worldUnitsPerKm

modules/atmosphere/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = atmosphere.paramsBuffer()

modules/atmosphere/set

set(opts: AtmosphereOpts?): AtmosphereState

Set the scene's atmosphere. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. With aerial at 0 and sky off nothing is wanted and the passes are released.

Parameters

  • opts AtmosphereOpts? (optional) — Atmosphere settings — see AtmosphereOpts.
atmosphere.set({ aerial = 1, sky = true, worldUnitsPerKm = 1000 })

modules/audio/README

require("@builtin::modules.api.engine.audio") -- audio (also available as global 'audio')

Engine-native audio: encode audio (or raw PCM) into the ZAUD compressed payload, decode/inspect it, play PCM a script synthesizes while it sounds (audio.stream), set what the mix is heard at — a level per named channel, a master level and a mute — and observe what the mixer is making audible right now: live voices, why a source is silent, master levels, mixer voice accounting, listener state and subsystem cost.

Usage: local audio = require("@builtin::modules.api.engine.audio") Also available as global: audio

modules/audio/audiostream_close

audiostream_close(self: AudioStream): boolean

Close the stream: its voice falls silent and the entity's audio source comes off. Despawning the entity, removing its audio source, putting another audio source in its place or rewriting the source's stream field closes the stream the same way.

Parameters

  • self AudioStream — The stream audio.stream returned.
s:close()

modules/audio/audiostream_flush

audiostream_flush(self: AudioStream): boolean

Drop every frame queued on the stream, on the mixer's next buffer.

Parameters

  • self AudioStream — The stream audio.stream returned.
s:flush()

modules/audio/audiostream_pause

audiostream_pause(self: AudioStream)

Pause the stream's voice. The queue holds what it has, and wanted() stays at 0 while it is full.

Parameters

  • self AudioStream — The stream audio.stream returned.
s:pause()

modules/audio/audiostream_play

audiostream_play(self: AudioStream)

Start or resume the stream's voice. A voice started after an earlier one ended reads a fresh, empty queue.

Parameters

  • self AudioStream — The stream audio.stream returned.
s:play()

modules/audio/audiostream_push

audiostream_push(self: AudioStream, pcm: any, frames: number?): (number?, string?)

Queue interleaved samples on the stream. pcm is a buffer or a string holding samples in the stream's format, or an array of numbers; frames takes only that many frames from its start, so one buffer can be reused frame after frame. Every sample must be finite and the samples a whole number of frames.

Parameters

  • self AudioStream — The stream audio.stream returned.
  • pcm any (optional) — A buffer or string of samples, or an array of numbers.
  • frames number? (optional) — How many frames from the start of pcm to push; all of them when omitted.
local n = s:wanted(); fill(buf, n); s:push(buf, n)

modules/audio/audiostream_set

audiostream_set(self: AudioStream, settings: { [string]: any })

Change the stream's voice: any of volume, pitch, spatial, maxDistance, rolloff, doppler, channel, sourceType, pitchValue and volumeValue, as an Audio component carries them. A voice already sounding takes the change on the next frame, except spatial, which a voice takes as it starts: placed and attenuated, or played unplaced.

Parameters

  • self AudioStream — The stream audio.stream returned.
  • settings { [string]: any } — The settings to change.
s:set({ pitch = 1.2, volume = 0.5 })

modules/audio/audiostream_status

audiostream_status(self: AudioStream): AudioStreamStatus?

Where the stream stands, nil once it is closed: sampleRate, channels, format; queuedFrames and queuedMs (what is queued and how far ahead of the mixer that is); capacityFrames; targetFrames (the queue it asks for) and producerGapMs (the longest recent gap between pushes behind it); wantedFrames; framesPushed, framesRefused (frames the full ring could not take) and framesPlayed; underruns (times it ran dry while it played) and starvedFrames (the silence played in their place); voice ("waiting", "playing", "paused" or "ended"); rate (the rate the mixer reads it at, pitch times Doppler).

Parameters

  • self AudioStream — The stream audio.stream returned.
local st = s:status(); print(st.queuedMs, st.underruns)

modules/audio/audiostream_wanted

audiostream_wanted(self: AudioStream): number

How many frames to push now to bring the queue up to its target: the stream's latency, or one and a half times the longest recent gap between pushes when that is longer. Push this many every frame and the stream stays fed through frames as long as the ones before them. 0 once the stream is closed.

Parameters

  • self AudioStream — The stream audio.stream returned.
local n = s:wanted()

modules/audio/decode

decode(zaud: buffer | string): (string?, number?, number?)

Decode a ZAUD payload into interleaved f32 PCM. A PCM payload comes back as the frames its header accounts for, held to the whole frames the bytes behind it fill, so the sample count is always a whole number of channels and a consumer walking it channels at a time ends on a frame.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

modules/audio/device

device(): AudioDeviceStatus

What the engine's audio output is doing: state is "open" while a stream is running on an output device and "silent" while none is, and device names the device an open stream runs on. The counters record what the engine has been through keeping one open — faults a live stream reported, changes of the host's default output, reopens the engine made, failedOpens the platform refused, and the glitches a listener heard as dropouts, with lastError carrying what the platform said. A device that goes away leaves the mixer running and the engine opening a stream again as soon as one is there. parked is true while nothing plays: the output stream is paused and the mixer renders nothing, so the device rests, and the first sound started resumes it, reading false once its first period plays; parks counts the times it parked and resumeLatencyMs is how long the last resume took to play its first period.

local d = audio.device(); print(d.state, d.device, d.reopens)

modules/audio/encode

encode(sourceBytes: buffer | string, opts: { [string]: any }?): (string?, string?)

Encode container audio bytes (ogg / mp3 / wav / flac) into a ZAUD payload. Every decoded sample must be finite; a source whose samples carry a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits.

Parameters

  • sourceBytes buffer | string — Encoded source audio bytes — a buffer or a binary string.
  • opts { [string]: any }? (optional) — { codec: "opus"|"pcm"?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loopStart: number?, loopEnd: number? }
local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })

modules/audio/encodePcm

encodePcm(pcm: any, sampleRate: number, channels: number, opts: { [string]: any }?): (string?, string?)

Encode raw interleaved f32 PCM into a ZAUD payload. Every sample must be finite; a buffer carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so a filter that diverged over part of a bake is caught before it is written. The sample count is a whole number of channels: a buffer with a tail over comes back as (nil, err) naming the whole frames it holds and the samples past them.

Parameters

  • pcm any (optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shape microphone.samples and audio.decode hand back, or a flat number array. A byte payload's samples are its 4-byte lanes, and a length that stops partway through one comes back as (nil, err) naming the whole samples it holds and the bytes past them.
  • sampleRate number — Source sample rate in Hz.
  • channels number — 1 or 2, and a divisor of the sample count.
  • opts { [string]: any }? (optional) — Same shape as audio.encode.
local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)

modules/audio/info

info(zaud: buffer | string): (AudioInfo?, string?)

Read a ZAUD payload's header. A PCM payload's samples are its bytes, and the header is read against them: a sample count differing from frames * channels comes back as (nil, err) naming both counts, and a sample carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so the header handed back describes a clip that is as long as it says and can sound. The header describes the clip's shape — rate, channels, frames, duration, codec, loop points. What the samples do where a whole-clip loop wraps is a reading of its own, audio.loopSeam, which is the call that answers whether a bed cycles without a click.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
local info = audio.info(zaud); print(info.durationMs)

modules/audio/levels

levels(): AudioLevels

The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.

local l = audio.levels(); print(l.peak, l.rms, l.windowMs)

modules/audio/listener

listener(): AudioListenerState

Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.

local l = audio.listener(); print(l.present, l.count, l.entity)

modules/audio/loopSeam

loopSeam(zaud: buffer | string): (AudioLoopSeam?, string?)

Measure what a clip's samples do where a whole-clip loop wraps, so a bed can be judged before anyone hears it tick. The wrap's own step (|x[1] - x[frames]|) is reported against the step the signal ordinarily makes between neighbouring samples, as ratio = step / meanStep — a figure in the units the signal itself moves in, so a quiet ambience and a loud drone are read the same way. A bed whose partials wrap reads near 1; one carrying a strike at its head and silence at its tail reads in the tens. ratio and the step / meanStep / maxStep beside it belong to the worst channel, channel names it, and channels carries every channel's own reading. seamless is ratio <= threshold, the same threshold asset.create("soundClip", ...) warns past. The reading is taken on the DECODED samples, so it answers for what the codec left behind and for a clip that arrived already encoded and whose source buffer nobody holds. Costs a decode of the whole payload; audio.info reads a header without one.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)

modules/audio/mixer

mixer(): AudioMixerLevels

The levels the mixer is applying to the mix right now: the master level, whether the mix is muted, and the level of every channel one has been set on. A channel absent from channels plays at unity, so a source naming it is heard at the volume it asks for.

local m = audio.mixer(); print(m.master, m.muted, m.channels.music)

modules/audio/observe

observe(): AudioObservation

Report what the mixer is making audible right now, and why a source is not. One read covering every live voice with the mixer's own playback state and effective gain, the master mix's level, the mixer's voice accounting, the listener, the output device the mix is reaching, and what the subsystem costs. Answers in edit mode as well as play mode.

local a = audio.observe(); print(a.audibleCount, a.levels.rms)
for _, v in audio.observe().voices do print(v.entity, v.mixerState, v.silence) end

modules/audio/peakSince

peakSince(window: number): number?

The loudest peak the master mix reached across the meter's windows that closed after its windows count stood at window. audio.levels() carries the window that closed last, so a reader sees the windows its own frames happen to land on; this spans all of them, which is what measuring a sound shorter than the gap between two reads takes. Take the mark from audio.levels().windows before the sound starts, wait until windows has advanced past the sound's length, then read the span.

Parameters

  • window number — A windows count taken from audio.levels() earlier.
local mark = audio.levels().windows
local peak = audio.peakSince(mark)

modules/audio/peaks

peaks(zaud: buffer | string, buckets: number): ({ number }?, string?)

The loudest sample in each of buckets equal spans of a clip, across its channels, as an absolute value held to 1: the shape a waveform is drawn from. A span is frames // buckets frames (at least one), and the frames past buckets * span fall in no bucket; a clip shorter than buckets frames fills one bucket per frame and leaves the rest at 0. The payload is decoded a block at a time, so the work never holds the clip's whole PCM, however long it runs.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
  • buckets number — How many spans to divide the clip into: an integer from 1 to 4096.
local bars = audio.peaks(clipRef:getBytes(), 96)

modules/audio/profile

profile(): AudioProfile

What the audio subsystem has cost since the profiling window opened — the streaming pump, clip decode, clip encode, voice starts, and building the observation itself. Every total is a SUM across that window rather than a per-frame figure, and the window runs from the last audio.resetProfile() or from engine start. For what a frame costs now, reset, let frames pass, then divide by the frames the window reports.

audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)

modules/audio/resetProfile

resetProfile()

Open a new audio profiling window, discarding what the previous one measured. Call this before timing a stretch of frames: without it audio.profile() reports totals reaching back to engine start.

audio.resetProfile()

modules/audio/setChannelVolume

setChannelVolume(channel: string, volume: number)

Set the level of one mixer channel — the channel an Audio component names, such as "sfx", "music" or "ambient", or any name the scene invents. It scales every voice on that channel and nothing else, reaches voices that are already playing, and comes back per voice as gain.channel. A channel no level has been set on plays at unity.

Parameters

  • channel string — The channel name, matching Audio.channel.
  • volume number — Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end

modules/audio/setMasterVolume

setMasterVolume(volume: number)

Set the master level of the mix, on the engine's 0..1 amplitude scale. It scales every voice whatever channel it plays on, reaches voices that are already playing, and comes back per voice as gain.master.

Parameters

  • volume number — Master level, 0..1.
audio.setMasterVolume(0.5)

modules/audio/setMuted

setMuted(muted: boolean)

Silence or unsilence the whole mix. A muted mix sounds nothing whatever its master and channel levels read, every voice reports masterSilent, and unmuting hands the levels back untouched.

Parameters

  • muted boolean — Whether the mix is silenced.
audio.setMuted(true)

modules/audio/stream

stream(entity: any, options: AudioStreamOptions?): (AudioStream?, string?)

Play PCM a script produces while it sounds — synthesis, an emulator's sound chip, procedural engine or wind noise, decoded voice — through the voice of entity. Opens a stream at the options' sample rate and channels and puts an audio source on the entity that plays it, so the voice is placed, attenuated and Doppler shifted when spatial, follows pitchValue / volumeValue, and is levelled by its channel and the master mix like every other source. Push samples each frame with stream:push, as many as stream:wanted() asks for, and the stream stays latencyMs ahead of the mixer, further on a host whose frames run long; a frame longer than the audio queued runs it dry, which stream:status().underruns counts. Frames pushed before the voice starts are the first it plays, and a stream not yet fed counts no underrun. Any sample rate from 4000 to 192000 Hz plays at its own pitch. The entity carries one voice: one already carrying an audio source is refused. The stream lives while the entity's audio source names it: removing the source, despawning the entity, putting another source in its place or rewriting the source's stream field closes it.

Parameters

  • entity any (optional) — The entity whose voice plays the stream: a proxy or its id.
  • options AudioStreamOptions? (optional) — An AudioStreamOptions, every field optional. The PCM: sampleRate (4000 to 192000 Hz, default the mixer's rate), channels (1, the default, or 2 interleaved left then right), format ("f32", the default, or "i16": how a pushed buffer or string is read), latencyMs (the least audio kept queued ahead of the mixer, default 100), bufferMs (the most the ring holds, default four times latencyMs and at least 500). The voice, as an Audio component carries it: volume, pitch, spatial, maxDistance, rolloff, doppler, channel, sourceType, pitchValue, volumeValue, and autoplay (default true).
local synth = audio.stream(e, { sampleRate = 22050, channels = 2, latencyMs = 80, spatial = true })
local n = s:wanted(); for i = 0, n - 1 do buffer.writef32(buf, i * 4, math.sin(phase)); phase += step end; s:push(buf, n)

modules/audio/voice

voice(entityId: string): AudioVoice?

The voice on one entity, or nil when that entity carries no audio source. An entity despawned since the frame the observation describes answers with its report from that frame, so an onDestroyed handler reads how the entity's voice stood as it went.

Parameters

  • entityId string — The entity's stable id.
local v = audio.voice(e.id); print(v and v.mixerState)

modules/audio/voiceAccounting

voiceAccounting(): AudioVoiceAccounting

How many voices the mixer can hold, how many are in use, how many are free — read off the mixer's own tracks, so the free count is the one a play call is granted or refused against. The two pools are reported apart: capacity / inUse / free are the main track, which carries the NON-spatial voices, while a spatial voice plays through its own sub-track and is counted by spatialInUse instead. sourcesHolding counts both pools from the sources that own them, so it equals inUse + spatialInUse while every voice answers to a source.

local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))

modules/audio/voices

voices(): { AudioVoice }

Every live audio source with the mixer's opinion of it.

for _, v in audio.voices() do print(v.clip, v.gain.effective) end

modules/audio/whySilent

whySilent(entityId: string): (string?, string?)

Why the source on an entity is making no sound. Returns nil when it IS sounding, and one of noBackend, noDevice, notResident, neverStarted, refused, paused, ended, gainZero, channelSilent, masterSilent, outOfRange when it is not — the nearest cause, so the answer names the thing to change. A second return carries the mixer's own words when it refused the source, and "no audio source on this entity" when nothing there plays at all.

Parameters

  • entityId string — The entity's stable id.
local why = audio.whySilent(e.id); if why then print(why) end

modules/av/README

require("@builtin::modules.api.engine.av") -- av (also available as global 'av')

Audio/video encode + mux control — live streaming, recording, encoder/muxer primitives. Public Luau surface over the __av Internal FFI namespace.

Usage: local av = require("@builtin::modules.api.engine.av") Also available as global: av

modules/av/is_live

is_live(): boolean

True if a live-stream session is currently active.

if av.is_live() then av.stop_live() end

modules/av/is_recording

is_recording(): boolean

True if a recording session is currently active.

print("recording:", av.is_recording())

modules/av/live

live(opts: LiveOpts?): (string?, string?)

Start the live-stream encoder. The stream is served at /engine/live.stream and reverse-proxied at /stream/<instance>/live.stream as a binary length-prefixed protocol consumed by the multiviewer UI's WebCodecs decoder. A browser engine encodes it with the browser's WebCodecs encoder, and the proxy reads it over the ZeroMind bridge and serves it at the same /stream/<instance>/live.stream. When texture_handle is set, the encoder reads from that GPU texture's guid (a Camera pointed at it via setTargetTexture) instead of the scene's viewport: that's how spectator cameras work. A stream runs beside an av.record take. Returns the stream URL once the encoder is open and streaming, or nil and the reason it is not: no encoder on this engine that streams H.264, a stream already running, an odd dimension, a session the encoder refuses (a size it will not encode, a spectator texture with no size to stream at). The engine opens the encoder, so the call waits for it: run it where it can yield, wrapping it in task.spawn from a callback that cannot. The reason also stands on av.status().liveError, which is where a stream that ends on a failure later says why.

Parameters

  • opts LiveOpts? (optional) — Encoder options.
local url, why = av.live({ width = 1280, height = 720, fps = 60 })

modules/av/record

record(path: string, opts: RecordOpts?): (string?, string?, Durability?)

Start recording the engine output to a VFS path. Default dir is /zero/runtime/recordings/ when path is not absolute: that directory is writable in every mode and stays out of the world's sync, so a take named relative to it is kept for the session and read back with vfs.read. A destination under /zero/source/ is world content, which the world's sync uploads when the take is filed. The take runs until av.stop_recording() unless opts bounds it with max_duration_sec (seconds of the take's own clock) or frames (captured frames); max_duration_sec wins when both are given, and the bound in force reads back as av.status().recordingBound. With no chroma/range opts the format defaults to full-range 4:4:4 HEVC where the GPU supports it, else 4:2:0. In the browser the browser's WebCodecs encoder converts the picture itself, so the take is 4:2:0 in the code range that encoder chooses, which an H.264 or AV1 file states, and grain above 0, chroma = "444" and any stated range are refused by name, and the master mix is encoded by the browser's WebCodecs AudioEncoder. On the "software" backend the take is H.264 encoded on the CPU, which costs the run it records: read av.status().recordingAchievedFps against recordingRequestedFps to see the rate it reached. What the take did with the master mix reads back as av.status().recordingAudio. cadence picks the clock the take stamps its frames from. "realtime" (the default) stamps each frame with the wall-clock slot it was captured in, so a recorded session is watched back at the speed it happened and an engine ticking under fps leaves slots empty. "frame" stamps every rendered frame one fixed slot after the last, so a timeline whose own clock advances a step per rendered frame — a cutscene, a scripted demo, anything on a fixed timestep — is delivered at the length that timeline runs to, however slowly the engine drew it: a take of frames = n at fps is n / fps seconds of film, and a max_duration_sec bound counts that film's seconds. A "frame" take records silent, because the master mix plays in wall-clock seconds and cannot share a file with a fixed-step picture; recordingAudio says so, and an explicit audio = true beside it is refused. Record the sound as a second "realtime" take. camera names the camera the take draws its film from — an entity proxy, an entity id, or an entity name. That camera holds the viewport for as long as the take runs, above the priority contest and above camera.setEditorOverride, so the film is its view and the frames carry everything the presented frame carries. Only frames that camera drew go into the film, and a camera that never draws the viewport ends the take with the reason on av.status().recordingError — so a take is the view it named or it is no take. The camera belongs to the take: nothing is written to it, and the viewport is back under its own contest the moment the take ends. It reads back as av.status().recordingCamera while the take runs. Omitted, the take records whichever camera holds the viewport, which in an engine on the editor profile is the editor's own fly camera rather than the scene's. renderLayers is the render-layer include spec the viewport is drawn under for as long as the take runs — the same token string a capture takes: all seeds every layer, name adds one and !name drops one, so "all !EditorUI !debug" films the scene without the editor's chrome or the authoring overlays (gizmos, light and probe icons, frustums, collider wireframes) over it, and "all !ui !EditorUI !debug" drops the authored HUD as well. The viewport admits geometry and screens by that one spec, so it states the whole picture. It belongs to the take: nothing is written to the camera it is stated against, and the moment the take ends — its bound reached, stopped, or refused — the viewport is back under the camera's own spec. While a take states layers, the window shows what the film holds, and av.status().recordingLayers reads the spec back. Omitted, the take records the engine output as presented. width / height are the film's size: the whole of the presented frame (in an editor layout, the rect the scene is drawn into) is resampled to it, and a side left out takes the presented frame's. Returns the destination path of a session that is open and recording, or nil and the reason it is not: an adapter that cannot encode, a take already running, an option the encoder rejects, a resolution the device refuses, a destination the VFS cannot write (a read-only path, a directory, a host filesystem path). The engine opens the session, so the call waits for it: run it where it can yield, wrapping it in task.spawn from a callback that cannot. How a take finished reads back as av.status().recordingEnd; a refused request puts its reason there and on recordingError and leaves no take report behind, while a request refused because a take is already running leaves that take's report as it is.

Parameters

  • path string — VFS destination path, returned in its /zero form; a relative name records under /zero/runtime/recordings/.
  • opts RecordOpts? (optional) — Encoder options (optional).
local clip = av.record("intro.mp4", { fps = 60 })
local film = av.record("cut.mp4", { fps = 24, frames = 24 * 181, cadence = "frame" })
local clean = av.record("take.mp4", { fps = 24, renderLayers = "all !EditorUI !debug" })
local shot = av.record("film.mp4", { fps = 24, frames = 240, camera = "FilmCamera" })
local take, why, d = av.record("take.mp4", { fps = 24 })
if d and d.playShadow then log.warn(d.warning .. " " .. d.playShadow) end

modules/av/save

save(path: string, opts: SaveOpts?): (boolean, string, SaveReport?)

Save a finished take to the device of the person at the screen, so the recording outlives the session: /zero/runtime/recordings/ is gone when the engine stops (and, in the browser, when the tab closes). path names the take the way av.record did: a relative name is the take of that name under /zero/runtime/recordings/, and any VFS path works too. The person decides where it lands. In the browser the take downloads at once when the save rides a click or key press of the person at the screen, so call it from a button's click handler; a save with no such gesture behind it (an agent, a timer, the end of a take) makes the page show the person an offer they save with a click of their own, because a browser starts a download only for a person who just acted. A native engine opens the host's save dialog in their downloads folder, with the take's name filled in, and writes it where they choose; an engine with no window has no person at its screen and refuses. The call waits for the first answer, so run it where it can yield (task.spawn from a callback). Returns true, a sentence saying where the take went, and the report { state, detail, name, size, path } with state "saved" (on the device) or "offered" (in front of the person, as the page's offer or the save dialog); or false and why it was refused: a take still recording (it is filed when it ends), no file at the path, a host path, a name with a folder in it, no window, or a take too large for the browser to hold, which the page shows the person as well. The saved file is the take byte for byte. userfile.save saves any other file the engine holds.

Parameters

  • path string — The take: its name under /zero/runtime/recordings/, or a VFS path.
  • opts SaveOpts? (optional) — { name }, the file name the device gets (the take's own when omitted).
local ok, where = av.save("take.mp4")
local ok, where, report = av.save("take.mp4", { name = "boss-fight.mp4" })

modules/av/status

status(): AvStatus

Report the encoder subsystem's state. Always available regardless of GPU support. backend is the encode backend in use: "vulkan" or "vaapi" on an adapter with a media engine, "software" where a native engine encodes on the CPU, and "webcodecs" in the browser, where the browser's own WebCodecs encoder takes the frames. hardware is true for the first two, and in the browser when the browser reports a hardware encoder for the default codec, so a caller that pays for the take in engine time knows which it is getting. codecs lists what the backend encodes with the recording default first (h264 in the browser, which every engine plays back with video.create). reason is "supported", "not_probed" before the engine has asked its encoder, "unsupported_adapter" for a native adapter with no encoder, or "unsupported_browser" for a browser whose WebCodecs encodes none of the codecs the engine offers. live is true while the av.live stream is running, and liveError is why the most recent stream was refused or ended on a failure: nil while one runs and after av.stop_live() ended one that had opened, and the reason that names av.stop_live() after it ended one still opening. A take of its own reads back on the recording fields: recording is the destination of the take in flight, recordingBound what will end it, recordingEnd how the most recent one ended, and recordingError why one produced no file. recordingAudio is the codec the take is writing the master mix with ("opus"), or the reason the file carries no audio track — read it to tell a film with a soundtrack from a silent one. recordingLayers is the render-layer include spec the armed take is drawing the viewport under, in the words its caller wrote, and nil for a take that stated none — the reading that answers what is in the picture rather than how much of it there is. recordingCamera is the entity id of the camera the take's most recent captured frame was drawn from, and stands as the source of the most recent take once that take has ended — it is read off the frame the engine drew, so it answers which view a film holds whether or not the take named a camera. recordingCadence is the clock the take stamps its frames from, "realtime" or "frame", and so what the tally below is a reading against. What the take produced reads off recordingFrames, recordingBytes (every byte the take has produced so far, climbing while it runs and ending equal to the size of the file), recordingSeconds (the timeline those frames cover), recordingAchievedFps (the rate they arrived at) and recordingRequestedFps (the rate asked for) — live while a take runs, and its final tally once it ends. On "realtime" the requested rate is a ceiling and an engine ticking under it reaches less; on "frame" every rendered frame is a slot of the recorded timeline, so recordingSeconds is that timeline's length and recordingAchievedFps is the rate it plays back at. recordingDurability is where the film in flight lands on the terms standing now — the report av.record returns as its third value, asked again for the take still running, so a caller polling status learns the play shadow is taking the film in time to keep it, a take that entered play after it opened included. It is present while a take names a destination.

local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)

modules/av/stop_live

stop_live(): boolean

Stop any active live-stream session.

av.stop_live()

modules/av/stop_recording

stop_recording(handle: string?): (boolean, string?)

Stop the active recording (or the one for the given promise handle). Returns true when a recording was armed at call time. A false return carries a second value naming how the most recent recording already ended (the bound it reached, or the failure that cut it short), and nil when no recording has run at all. The engine finalizes the take once the call returns. A take stopped before its first frame is captured runs on until that frame lands and ends holding it, and one whose first frame has not landed 120 frames after the stop ends with none. Wait for av.is_recording() to go false, then read what it produced off av.status().

Parameters

  • handle string? (optional) — Promise handle of a specific recording (optional).
local stopped, ended = av.stop_recording()

modules/base64/README

require("@builtin::modules.api.engine.base64") -- base64 (also available as global 'base64')

Base64 encode / decode between binary and text Luau strings (standard RFC 4648 alphabet, padded). Public Luau surface over the __base64 Internal FFI namespace.

Usage: local base64 = require("@builtin::modules.api.engine.base64") Also available as global: base64

modules/base64/decode

decode(text: string): (string?, string?)

Decode standard-alphabet base64 text back to the original binary string.

Parameters

  • text string — Base64 text to decode.
local bytes = base64.decode(encoded)

modules/base64/encode

encode(bytes: buffer | string): string

Encode a binary string to standard-alphabet (padded) base64 text.

Parameters

  • bytes buffer | string — Binary bytes to encode.
local text = base64.encode(jpegBytes)

modules/bitmask_bits/README

require("@builtin::modules.bitmask_bits") -- bitmask_bits

The bitmask field-constraint validator: a constrained value must be a whole number that fits the declared width, so what the field reads back is a mask the system consuming it can actually address. A rejection names the width and why the value is not a mask of it. Registers itself with the generic field_constraints registry on load. nil passes, so a mask field may be left unset.

Usage: local bitmask_bits = require("@builtin::modules.bitmask_bits")

modules/blend/README

require("@builtin::modules.api.engine.blend") -- blend (also available as global 'blend')

Record-stride blend primitives over Buffer slices — layout registry + weighted/lerp combiners. Public Luau surface over the __blend Internal FFI namespace.

Usage: local blend = require("@builtin::modules.api.engine.blend") Also available as global: blend

modules/blend/destroyLayout

destroyLayout(handle: number): boolean

Drop the layout from the registry.

Parameters

  • handle number — Layout handle.

modules/blend/layout

layout(slots: { BlendSlot }, totalStride: number?): number?

Register a record-stride layout. Each slot is { offset, stride, op } where op is "lerp" / "slerp" / "sum" / "step". Slerp slots must have stride 4. totalStride defaults to max(offset + stride) across slots; pass an explicit value when records contain padding past the last slot.

Parameters

  • slots { BlendSlot } — Array of slot tables.
  • totalStride number? (optional) — Optional explicit record stride.
local l = blend.layout({ { offset = 0, stride = 3, op = "lerp" } })

modules/blend/lerpInto

lerpInto(outBuffer: Substrate.TypedBuffer, layout: number, aBuffer: Substrate.TypedBuffer, bBuffer: Substrate.TypedBuffer, t: number): boolean

Two-input crossfade shortcut. Equivalent to blend.weightedInto(out, layout, { {a, 1-t}, {b, t} }). Faster for the common A/B fade case because it skips the inputs-table walk.

Parameters

  • outBuffer Substrate.TypedBuffer — The buffer written into.
  • layout number — Layout handle.
  • aBuffer Substrate.TypedBuffer — The A side of the fade.
  • bBuffer Substrate.TypedBuffer — The B side of the fade.
  • t number — Crossfade weight on B (0..1).

modules/blend/weightedInto

weightedInto(outBuffer: Substrate.TypedBuffer, layout: number, inputs: { BlendInput }): boolean

Combine N weighted input buffers into the output buffer using the layout's slot ops. The output buffer's length must be a whole multiple of layout.totalStride; every input buffer must be at least as long as the output. Returns false on any handle / size mismatch.

Parameters

  • outBuffer Substrate.TypedBuffer — The buffer written into.
  • layout number — Layout handle.
  • inputs { BlendInput } — Array of { buffer, weight }.

modules/buoyancy/README

require("@builtin::systems.oceans.modules.buoyancy") -- buoyancy

Floats a rigid body on the water Physics.water answers for (a sea, a lake, a river, a pool) — displacement, damping and the righting moment that keeps a hull upright.

Usage: local buoyancy = require("@builtin::systems.oceans.modules.buoyancy")

modules/buoyancy/apply

apply(self, dt: number): boolean

Step the body once. The Buoyancy component calls this every frame; call it yourself only when driving a body without that component.

Parameters

  • self any (optional)
  • dt number — Seconds since the last step.
float:apply(1 / 60)

modules/buoyancy/attach

attach(target: any, options: any?): (any?, string?)

Float a rigid body on the water. The entity needs a dynamic Physics body; the hull is measured from what it draws unless sizeX/Y/Z say otherwise.

Parameters

  • target any (optional) — The entity id or proxy to float.
  • options any? (optional) — { density, damping, angularDamping, columns, sizeX, sizeY, sizeZ, mass, gravityScale, ocean }. density is relative to water, and the lower it is the higher the hull rides.
local float = buoyancy.attach(barrel, { density = 0.4 })

modules/buoyancy/clear

clear()

Stop floating every body. Each one is released, so a body driven by a Buoyancy component stops being pushed as well.

buoyancy.clear()

modules/buoyancy/columns

columns(size: any, columns: number): { { x: number, z: number } }

The local-space footprint of a hull's columns: an evenly spaced grid over its X/Z extent, each column standing for an equal share of the hull.

Parameters

  • size any (optional) — { x, y, z } full extents of the hull in its own space.
  • columns number — Grid resolution per horizontal axis. 1 gives a single central column, which floats but cannot right itself; 2 is the smallest grid that produces a righting moment.
local cols = buoyancy.columns({ x = 4, y = 1, z = 2 }, 2)

modules/buoyancy/configure

configure(self, options: any)

Reconfigure a floating body. Any field may be passed; anything omitted is left as it was.

Parameters

  • self any (optional)
  • options any (optional) — { density, damping, angularDamping, columns, sizeX, sizeY, sizeZ, mass, gravityScale, ocean }. mass and gravityScale stand in for the rigid body's own, which is what they are read from otherwise.
float:configure({ density = 0.8 })

modules/buoyancy/destroy

destroy(self)

Stop floating this body. The rigid body keeps everything else about it.

Parameters

  • self any (optional)
float:destroy()

modules/buoyancy/forEntity

forEntity(entityId: string): any

The floating body on an entity, if it has one.

Parameters

  • entityId string — The entity's id.
local float = buoyancy.forEntity(barrelId)

modules/buoyancy/list

list(): { any }

Every body currently floating.

print(#buoyancy.list())

modules/buoyancy/restingY

restingY(waterY: number, height: number, density: number): number

The height a hull of this density displaces its own weight at on flat water — the waterline its lift balances at, with no wave motion in it.

Parameters

  • waterY number — World Y of the undisturbed surface.
  • height number — The hull's full height.
  • density number — The hull's density relative to water.
local y = buoyancy.restingY(0, 1, 0.45)

modules/buoyancy/submergedFraction

submergedFraction(centreY: number, waterY: number, height: number): number

How much of a column stands under water: 0 clear of it, 1 fully under.

Parameters

  • centreY number — World Y of the column's mid-height.
  • waterY number — World Y of the surface above it.
  • height number — The column's full height.
local f = buoyancy.submergedFraction(0.2, 0.0, 1.0)

modules/buoyancy/submersion

submersion(self): number

The share of the hull that was under water on the last step, averaged over its columns. 0 is clear of the sea, 1 is fully submerged.

Parameters

  • self any (optional)
print(float:submersion())

modules/buoyancy/waterline

waterline(self): number?

The world Y of the water surface under the hull's centre on the last step — where the waterline stood, wave motion included.

Parameters

  • self any (optional)
print(float:waterline())

modules/cachedIndirect/README

require("@builtin::systems.radianceCache.cachedIndirect") -- cachedIndirect

Indirect diffuse light that is gathered once per patch of world and reused, rather than re-gathered for every pixel of every frame. It is the scene-wide user of @builtin::systems.radianceCache.radianceCache and the worked example of what a cache buys a lighting technique: the same bounce, converged over frames instead of within one, at a fraction of the gathering per frame.

Usage: local cachedIndirect = require("@builtin::systems.radianceCache.cachedIndirect")

modules/cachedIndirect/active

active(): boolean

Whether the cached-indirect passes are running this frame.

if cachedIndirect.active() then ... end

modules/cachedIndirect/cache

cache(): any

The cache this effect drives. The render feature takes its passes from here, and a script can read its counters or invalidate it through the same handle.

cachedIndirect.cache():stats()

modules/cachedIndirect/clear

clear()

Turn the cached bounce off and release the passes, the cache table and the resolve target. The other settings are kept, so a later set({ intensity = ... }) brings the effect back at the same settings and refills the table over history * stride frames.

cachedIndirect.clear()

modules/cachedIndirect/get

get(): { [string]: number }

The cached-indirect settings currently in force.

local s = cachedIndirect.get().stride

modules/cachedIndirect/invalidate

invalidate()

Declare that the lighting changed, so every patch takes its next gather whole instead of averaging it into light that is gone. The cache is rebuilt within stride frames.

cachedIndirect.invalidate()

modules/cachedIndirect/set

set(opts: CachedIndirectOpts?): { [string]: number }

Set the scene's cached indirect light. Any omitted field keeps its current value. An intensity of 0 turns it off and releases the passes.

Parameters

  • opts CachedIndirectOpts? (optional) — Cached-indirect settings — see CachedIndirectOpts.
cachedIndirect.set({ intensity = 1.0, stride = 8 })

modules/camera/README

require("@builtin::modules.api.engine.camera") -- camera (also available as global 'camera')

Script-facing camera queries: the main scene camera, the on-screen render camera, the editor fly-camera, per-frame view data, and the camera observation — which cameras drew this frame, with what projection, into what, and at what cost. Public Luau surface over the __camera Internal FFI namespace. A physical camera on the machine (a webcam, a phone's front or back camera) is cameraInput, which shows its live picture as a texture.

Usage: local camera = require("@builtin::modules.api.engine.camera") Also available as global: camera

modules/camera/active

active(): string?

Entity id of the on-screen render camera this frame — whichever camera wins the viewport by priority (the editor fly-camera in edit mode, the gameplay camera in play). Render features, billboards, and input bases that must follow the human's on-screen view read this.

local camId = camera.active()

modules/camera/cut

cut()

Declare that the camera on screen cuts: the next frame it draws stands somewhere it did not travel to. Motion vectors are the difference between where a surface projects now and where it projected on the camera's previous frame, and everything temporal reads that difference — the shutter reconstructs the frame by walking it, a temporal resolve reprojects its history along it. Across a cut that difference describes a displacement no surface made, so the frame is reconstructed from taps a whole screen away and belongs to neither shot. A declared cut leaves the camera with no previous frame for exactly one frame, which is the state its very first frame is already in, so every consumer reads zero motion across the cut. Declare it in the same step that places the camera at the new station; declaring it again before that frame draws still costs the one frame. Handing the viewport from one camera to another is already a cut without being declared one: the incoming camera stands where it always stood, and the engine performs the handover, so it is what states it.

camera.cut(); entity(camId).position = { 40, 6, -12 }

modules/camera/editor

editor(): string?

Entity id of the editor fly-camera (the EditorOnly authoring camera), or nil if the scene has none. This is the camera the editor viewport renders through, so it is the one a capture of the screen sees. Its pose is its entity transform: assign entity(id).position to move it and aim it with the camera toolbox's lookAt, which makes a screen capture repeatable instead of whatever pose the instance booted with.

local camId = camera.editor()
local id = camera.editor(); entity(id).position = { 12, 8, 12 }; tools.use("camera", "lookAt", id, { 0, 0, 0 })

modules/camera/editorOverride

editorOverride(): string?

Entity id currently overriding viewport selection, or nil when the viewport is decided by highest-priority-wins.

local owner = camera.editorOverride()

modules/camera/get

get(target: (string | EntityRef)): CameraReport?

One camera's report from the observation — the same record camera.list yields, for the camera the caller names. Takes an entity id, an entity name, or an entity proxy, the same way the camera tools do.

Parameters

  • target (string | EntityRef) — Entity id, entity name, or entity proxy of the camera to report on.
local c = camera.get(camera.active()); print(c.frame.far, c.authored.far)
local c = camera.get("minimapCam"); print(c.rendering, c.reason)

modules/camera/list

list(): { CameraReport }

Every camera in the world as a compact row each, ordered the way the renderer resolves the on-screen camera: highest priority first. Reads the same observation camera.observe does, so a row can never disagree with the full report about whether a camera is enabled or which one drew.

for _, c in ipairs(camera.list()) do print(c.name, c.enabled, c.rendering) end

modules/camera/main

main(): string?

Entity id of the main scene camera — the scene camera the viewport is drawn from, and while the editor fly-camera holds the screen, the scene camera that would take it. The gameplay/PlayerPrototype camera, an agent-placed scene camera, or a cutscene camera. Never the editor camera; nil if the scene has only the editor camera. It comes off the same selection the frame does, so writing a pose to it moves what is drawn whenever a scene camera is on screen. The scene camera with the highest authored priority takes it; cameras tied on priority settle on the order the frame visits them, so a scene that needs a specific camera — a prototype and the clone play makes of it both stand at 0 — states a distinct priority rather than resting on that order. For the camera drawn on screen whichever partition owns it, use camera.active().

local camId = camera.main(); local cam = camId and entity(camId)

modules/camera/motionTally

motionTally(): { frames: number, withoutHistory: number }

Frames the camera on screen has drawn, and how many of them had no previous frame to difference their motion vectors against — its first frame, every declared cut, and every frame the viewport changes hands on. Both counts are monotonic across the session, so two readings either side of a run say what happened in between.

local before = camera.motionTally().withoutHistory

modules/camera/observe

observe(): CameraObservation?

Every camera in the world, for the frame that has just been drawn. lastRender retains the latest submitted frame, target and pending build count while the camera exists, including frames when it is disabled. engineFrame identifies the script frame (time.frameCount()) that drew. Answers "why is this camera not showing what I expect" in one call: rendering says whether each camera drew and reason names the single cause when it did not — "disabled", "entityInactive", "targetMissing", "noLayers", "outranked", "notDrawn", "paced" for a texture camera whose per-frame render waits for the next frame the frame pacer admits, or "awaitingPipelines" for a deferrable render queued behind pipeline builds. Explicit camera renders draw the requested frame and report its pending builds in lastRender. pipelinesPending counts the builds a camera is waiting on: what a held render is queued behind, and — for a camera that DID draw — what the frame it drew was drawn WITHOUT. It is absent once a camera's frame was drawn with everything the scene asked for. Each camera carries both projections: authored is what the Camera component holds and frame is what the renderer actually built, with mismatch naming every field the two disagree on — so a clip range or a lens the frame did not use is one field read. frame, viewProj, frustum and the cost numbers describe a camera that drew; every cost is for that one frame. One snapshot is published per drawn frame, from after the frame is drawn, so a read describes the last frame rather than the world at the instant of the call — a write and a read in one script step return the frame that ran before the write. Put a task.wait() between them to compare a camera either side of a change; frame counts the frames observed, so a poll can wait for it to advance.

local obs = camera.observe(); for _, c in ipairs(obs.cameras) do print(c.name, c.rendering, c.reason) end
local dark = {}; for _, c in ipairs(camera.observe().cameras) do if not c.rendering then table.insert(dark, c.name .. ": " .. tostring(c.reason)) end end

modules/camera/setEditorOverride

setEditorOverride(entityId: string?)

Give one camera the viewport outright, or pass nil to clear it. While set, that camera IS the on-screen camera and priority is never consulted, so no authored priority can take the viewport from it — which is what makes an authoring camera safe to fly over a scene holding a camera at any priority. An override naming a camera that is despawned or disabled falls back to highest-priority-wins rather than blanking the screen. The override holds across a change of engine.mode: the flip rebuilds the scene and materialises its cameras again, and an override whose camera the rebuilt scene holds under the same entity id names it again once the rebuild has ended. One whose camera the rebuilt scene does not hold (a temporary camera, or one play replaces with a clone of its own) ends there: a warning notice and the engine log name the camera it held, and the wld.play / wld.edit result reports it under viewportOverride. An override on a camera that draws nothing in the new mode (an EditorOnly camera sits out play) stands, and the highest-priority camera holds the viewport until its camera draws again; viewportOverride.onScreen names the camera holding it.

Parameters

  • entityId string? (optional) — Entity id of the camera to route the viewport to, or nil to clear.
camera.setEditorOverride(camera.editor())
camera.setEditorOverride(nil)

modules/camera/viewData

viewData(target: ((string | EntityRef)?)): CameraView?

Camera render data. Called with no argument it is the active viewport camera's data for this frame: world position, which projection it drew and the field describing that frame, viewport pixel size, the 6 world-space frustum planes (the same inward-pointing, normalized planes the renderer culls with), and the view-projection matrix. The camera state a render feature needs for camera-relative work — LOD selection, frustum culling, billboards. Render features also get it as ctx.camera. Called with an entity id it is that camera's data, read from the frame's camera observation, and carries the identity the bare form has no room for: which camera it describes, which frame it was built for, what it rendered into, and the render layers it resolved to.

Parameters

  • target ((string | EntityRef)?) (optional) — Entity id, name, or proxy of the camera to read, or nil for the viewport camera.
local c = camera.viewData(); if c then print(c.position.x, c.fovY) end
local minimap = camera.viewData("minimapCam"); print(minimap.output.target, minimap.viewport.w)

modules/cameraInput/README

require("@builtin::modules.api.engine.cameraInput") -- cameraInput (also available as global 'cameraInput')

Physical cameras as live textures and pixel sources: list the capture devices (a webcam, a phone's front and back cameras), open one as a feed, show its live picture anywhere a texture goes (a material, a UI image, a recording), and read its pixels. Public Luau surface over the __cameraInput Internal FFI namespace.

Usage: local cameraInput = require("@builtin::modules.api.engine.cameraInput") Also available as global: cameraInput

modules/cameraInput/awaitRunning

awaitRunning(self: CameraFeed, timeoutSeconds: number?): (boolean, string?)

Wait until the feed is running, and report whether it got there. Returns as soon as the feed leaves starting and permissionPending, or when timeoutSeconds have passed, whichever comes first: a permission prompt nobody answers never settles, so the wait is always bounded.

Parameters

  • self CameraFeed
  • timeoutSeconds number? (optional) — Seconds to wait at most. Defaults to 10.
local ok, why = feed:awaitRunning(15); if not ok then print(why) end

modules/cameraInput/devices

devices(): { CameraDevice }

Every capture device the platform offers: id is what cameraInput.open takes as device, name the label a person recognises, facing which way it points. Until camera access has been granted, id and name read as empty strings and facing as "unknown": the list is part of what the permission protects, so a first open takes the default camera or a facing, and the list names the devices after that.

for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end

modules/cameraInput/feeds

feeds(): { CameraFeedEntry }

Every open feed, with the owning context that opened it (scope, the same tag scopes.list() shows), whose end stops the feed.

for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end

modules/cameraInput/onDeviceChange

onDeviceChange(callback: (devices: { CameraDevice }) -> ()): () -> ()

Call callback with the new device list every time a camera is added or removed, or the platform names its devices after access is granted. Each listener is watched by a task of the context that registered it, so a listener a component registered ends when that component is removed, and every other listener goes on hearing changes.

Parameters

  • callback (devices: { CameraDevice }) -> () — Receives the device list, as cameraInput.devices() returns it.

Returns () — A function that removes this listener.

local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)

modules/cameraInput/open

open(opts: CameraOpenOpts?): (CameraFeed?, string?)

Open a camera as a feed. Returns the feed at once; it reads permissionPending while the platform asks for access, then running (or denied / failed with a reason). Wait with feed:awaitRunning().

feed.texture is a texture handle that shows the camera's current frame: put it on a surface as a session material's base_color_texture (renderer.material.create, worn with Model:applySessionMaterial) or in a slot of a material that exists (renderer.material.setTexture(key, "base_color_texture", feed.texture)), in UI as { type = "image", props = { src = feed.texture } }, and av.record films it like any other scene content.

device names a camera by the id or name cameraInput.devices() lists; facing asks for the "front" or "back" camera instead; with neither, the platform's default camera opens. width, height and fps are hints: the device delivers the nearest it can, and feed:status() reports what it delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is touched: a hint outside its range, a facing other than front or back, a device the platform does not offer.

Parameters

  • opts CameraOpenOpts? (optional) — { device, facing, width, height, fps }, every field optional.
local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")

modules/cameraInput/readPixels

readPixels(self: CameraFeed, rect: CameraRect?): (any, string?)

Read the pixels of the frame the feed is showing now, without stopping it: the whole frame, or the rectangle { x, y, w, h } of it in pixels from the top-left corner. The answer is a TextureCpuHandle holding RGBA8 pixels (:readPixel(x, y), :rgba(), :encodePng()); :unload() it once done. Returns nil and the reason while no frame has arrived, after the feed stopped, or for a rectangle outside the frame.

Parameters

  • self CameraFeed
  • rect CameraRect? (optional) — Optional { x, y, w, h }.
local cpu = feed:readPixels(); if cpu then print(cpu:readPixel(0, 0)); cpu:unload() end

modules/cameraInput/status

status(self: CameraFeed): CameraStatus

Where the feed stands. width, height and fps are what the device delivers, whatever the open asked for. frames advances once per new frame, so two reads that show the same count saw the same picture. rotation is the degrees clockwise the picture turns to stand upright on the display, and follows a phone's screen as it turns; mirrored says the picture, once turned, reads as the person expects only when shown mirrored, which is how a front camera is shown. reason carries the platform's own message for denied and failed, and what is being waited on for permissionPending.

Parameters

  • self CameraFeed
local s = feed:status(); print(s.state, s.width, s.height, s.frames)

modules/cameraInput/stop

stop(self: CameraFeed): boolean

Stop the feed. The camera is released once no other feed is reading it, which is what turns the platform's camera indicator off. True when the feed was open.

Parameters

  • self CameraFeed
feed:stop()

modules/cameraInput/stopAll

stopAll(): number

Stop every open feed and release every camera, whatever opened them.

cameraInput.stopAll()

modules/camera_spawner/README

require("@builtin::modules.api.engine.camera_spawner") -- camera_spawner

DEPRECATED: v7 scenes author their own Camera entity. This procedural primary-camera spawner serves legacy v6 scenes only; M.ensure stands down (returns early) for any scene carrying a string playerIntent.

Spawns the scene's primary Camera entity on non-additive layers.onLoad, for legacy v6 scenes. The per-mode world default (world.camera_default_<mode>) is the fallback; scene-level overrides via sceneProxy.settings.camera win when present. Missing refs → log.error + skip (no hardcoded fallback).

Usage: local camera_spawner = require("@builtin::modules.api.engine.camera_spawner")

modules/carried/README

require("@builtin::modules.api.engine.carried") -- carried (also available as global 'carried')

Carried mounts: content of another world, mounted read-only for this session at /zero/runtime/carried/<key>/ without installing it. A closure is named by the address of its manifest (closureSha, the sha-256 ZeroMind serves the manifest at); the engine reads the manifest and every file it names through the device's content cache, lays the files out the way their world places them, and states where the mount stands in <key>/status. Nothing a mount holds is content of the open world: it is in no listing of its assets, no dependency table, no push and no zm status, and the open world's code cannot require from it. A mount lives while some caller holds it and the session lasts.

carried.mount(closureSha, { key = "ada.avatar" }) holds a mount and starts filling it; carried.wait(key) yields until it settles; carried.status(key) reads where it stands; carried.release(key) lets go of it. carried.mountDraft(assetPath) mounts the closure of an asset of the open world's working tree the same way, for a preview on this engine. carried.resolve(key, reference) answers what a reference written inside the closure names. guides { path: "core/carried" } covers the whole surface.

Usage: local carried = require("@builtin::modules.api.engine.carried") Also available as global: carried

modules/carried/heldBytes

heldBytes(): number

The bytes every carried mount holds in this engine's memory.

print(carried.heldBytes())

modules/carried/list

list(): { Status }

The status of every carried mount of this session.

for _, m in carried.list() do print(m.key, m.state) end

modules/carried/manifest

manifest(key: string): string?

The manifest of the ready mount at key: the JSON bytes the content store served ($schema zero/closure/v1, root, and entries, one per file and folder with its world path, asset_guid, checksum, size_bytes, kind and origin, plus one engine entry per builtin it was built against). Their sha-256 is the mount's closureSha. Nil until the mount is ready.

Parameters

  • key string — The mount's key.
local entries = require("@builtin::modules.json").decode(carried.manifest("ada.avatar")).entries

modules/carried/mount

mount(closureSha: string, options: MountOptions?): Status

Hold the carried mount of the closure whose manifest address is closureSha and start filling it, unless it is already held. The mount stands at /zero/runtime/carried/<key>/; a key holds one closure for as long as anyone uses it, and every mount of the same closure at the same key holds it once more. Answers at once with the mount's status (state resolving while it fills); carried.wait yields until it settles. Raises when the key is malformed, closureSha is no sha-256, or the key holds another closure.

Parameters

  • closureSha string — The sha-256 hex digest of the closure's manifest.
  • options MountOptions? (optional) — key: the mount's folder name (one path segment of letters, digits, ., _, -), default closure-<first 16 hex digits>; commit: the commit the closure was published at, stated in the status.
local m = carried.mount(sha, { key = "ada.avatar" }); local s = carried.wait(m.key)

modules/carried/mountDraft

mountDraft(assetPath: string, options: MountOptions?): Status

Hold a draft carried mount of the asset at assetPath, an asset of the open world's working tree (or of your home folder): the engine walks the asset and every asset its files reference, writes that closure and its manifest into the device's content cache, and mounts it from there the way a published closure mounts, labelled draft = true. It is this engine's alone. Mounting it again rebuilds it from the tree as it stands. A draft holding a home folder's content is that folder's: it is made for code that reaches the folder, and its files, listing, status and manifest answer that code alone. Raises, naming the folder, when the asset or anything it reaches is in a home folder the calling code does not reach.

Parameters

  • assetPath string — The asset, e.g. /zero/source/chars/ada.avatar.
  • options MountOptions? (optional) — key: the mount's folder name, default draft-<asset name>.
local d = carried.mountDraft("/zero/source/props/lamp.model"); carried.wait(d.key)

modules/carried/release

release(key: string): number

Let one holder of the mount at key go. The last one takes it down: its folder leaves /zero/runtime/carried/ and its bytes leave this engine's memory, while the device's content cache keeps them for the next mount. Raises when no mount is held at key.

Parameters

  • key string — The mount's key.
carried.release("ada.avatar")

modules/carried/resolve

resolve(key: string, reference: string): string?

What reference, written by content of the closure at key, names: a guid the closure carries, a path of its world (props/lamp.model, /zero/source/props/lamp.model) or an identity of its world (props.lamp) answers the file inside the mount; a reference to the engine library answers the library asset, taken to the version of an engine builtin the closure was built against when the mount holds that version. The open world's own content answers none of it. Nil when nothing answers.

Parameters

  • key string — The mount's key.
  • reference string — A guid, path, identity or library reference.
local tex = carried.resolve("ada.avatar", "chars.ada_albedo")

modules/carried/status

status(key: string): Status?

Where the mount at key stands, as <key>/status states it: state is resolving, fetching, measuring, ready, refused or none; reason is a word saying why nothing is mounted (unreachable, notFound, incomplete) or why the closure was refused, with detail the sentence; bytesFetched / bytesTotal count the files read; measured counts files, folders and bytes per kind once ready; builtin names how each engine builtin the closure was built against resolves (host, mount, or hostOtherVersion with why). Nil when no mount is held at key, and when the mount holds a home folder's content the calling code does not reach.

Parameters

  • key string — The mount's key.
print(carried.status("ada.avatar").state)

modules/carried/wait

wait(key: string, seconds: number?): Status

Yield until the mount at key settles (ready, refused or none) or seconds pass, and answer its status then. Raises when no mount is held at key, or when it holds a home folder's content the calling code does not reach.

Parameters

  • key string — The mount's key.
  • seconds number? (optional) — How long to wait, default 120.
local s = carried.wait("ada.avatar", 30); assert(s.state == "ready", s.detail)

modules/channel/README

require("@builtin::modules.api.engine.channel") -- channel (also available as global 'channel')

Keyframe-channel sampling primitives — registry + sampleInto variants. Public Luau surface over the __channel Internal FFI namespace.

Usage: local channel = require("@builtin::modules.api.engine.channel") Also available as global: channel

modules/channel/create

create(opts: ChannelOpts): number?

Register a keyframe channel. times is the sorted keyframe time array; values is the packed value array (layout depends on interp); stride is the floats-per-sample width; interp is "step" | "linear" | "slerp" | "cubicHermite". Returns the channel handle, or nil on malformed input.

Parameters

  • opts ChannelOpts — { times, values, stride, interp }.
local h = channel.create({ times = ts, values = vs, stride = 3, interp = "linear" })

modules/channel/destroy

destroy(handle: number): boolean

Drop the channel from the registry.

Parameters

  • handle number — Channel handle.

modules/channel/sampleInto

sampleInto(ch: number, time: number, buf: Substrate.TypedBuffer, offset: number): boolean

Sample the channel at time and write stride floats into the buffer starting at f32 index offset. Returns false on unknown handle, layout mismatch, or out-of-bounds; the buffer is unchanged on failure.

Parameters

  • ch number — Channel handle.
  • time number — Sample time in seconds.
  • buf Substrate.TypedBuffer — The buffer written into.
  • offset number — Starting f32 index in the buffer.

modules/channel/sampleManyInto

sampleManyInto(ch: number, time: number, buf: Substrate.TypedBuffer, offsets: { number }): boolean

Sample once, blit the result into every position in offsets. Saves the per-offset binary search when one channel feeds many bones / particles / parameters.

Parameters

  • ch number — Channel handle.
  • time number — Sample time in seconds.
  • buf Substrate.TypedBuffer — The buffer written into.
  • offsets { number } — Array of f32 indices.

modules/channel/sampleQuat

sampleQuat(ch: number, time: number): (number?, number?, number?, number?)

Convenience accessor for stride-4 quaternion channels.

Parameters

  • ch number — Channel handle.
  • time number — Sample time.

modules/channel/sampleVec3

sampleVec3(ch: number, time: number): (number?, number?, number?)

Convenience accessor for stride-3 channels. Returns the three components as multiret, or nil if the channel is unknown / has a different stride.

Parameters

  • ch number — Channel handle.
  • time number — Sample time.
local x, y, z = channel.sampleVec3(h, t)

modules/color/README

require("@builtin::modules.api.engine.color") -- color (also available as global 'color')

Color construction, conversion, and perceptual ops (RGB / HSL / HSV / Oklch / hex). Public Luau surface over the __color Internal FFI namespace.

Usage: local color = require("@builtin::modules.api.engine.color") Also available as global: color

modules/color/coerce

coerce(value: any): Color?

Read a value written in any of the shapes a colour is authored in — a hex string, an {r=,g=,b=,a=} map, or an {r,g,b,a} array — as an sRGB color table. Returns nil when the value does not describe a colour, so a caller can name the value it was handed instead of substituting one. Channels absent from a map or array read as 0; alpha absent reads as 1.

Parameters

  • value any (optional) — Value to read as a colour.
local c = color.coerce("#5a5a62") or color.coerce({ 0.2, 0.7, 0.2 })

modules/color/complementary

complementary(c: Color): Color

Complementary color — rotate hue 180° in Oklch space.

Parameters

  • c Color — Input color.
local accent = color.complementary(primary)

modules/color/darken

darken(c: Color, amount: number): Color

Decrease the lightness of a color in Oklch perceptual space.

Parameters

  • c Color — Input color.
  • amount number — Lightness decrease 0-1.
local pressed = color.darken(base, 0.1)

modules/color/desaturate

desaturate(c: Color, amount: number): Color

Decrease the chroma (saturation) of a color in Oklch space.

Parameters

  • c Color — Input color.
  • amount number — Chroma decrease (typically 0-0.2).
local muted = color.desaturate(base, 0.05)

modules/color/hex

hex(hexString: string): Color?

Parse a hex color string into an sRGB color table. Accepts 3, 4, 6, or 8 hex digits with or without a leading # (e.g. "#f00", "f00f", "#ff0000", "ff000080"). Returns nil on parse failure.

Parameters

  • hexString string — Hex color string.
local fromCss = color.hex("#ff8800")

modules/color/hsl

hsl(h: number, s: number, l: number): Color

Build a color from HSL (h: 0-360, s: 0-1, l: 0-1). Returned as sRGB.

Parameters

  • h number — Hue (degrees, 0-360).
  • s number — Saturation (0-1).
  • l number — Lightness (0-1).
local teal = color.hsl(180, 0.5, 0.5)

modules/color/hsla

hsla(h: number, s: number, l: number, a: number): Color

Build a color from HSLA, returned as sRGB.

Parameters

  • h number — Hue (0-360).
  • s number — Saturation (0-1).
  • l number — Lightness (0-1).
  • a number — Alpha (0-1).
local fadedTeal = color.hsla(180, 0.5, 0.5, 0.3)

modules/color/hsv

hsv(h: number, s: number, v: number): Color

Build a color from HSV (h: 0-360, s: 0-1, v: 0-1).

Parameters

  • h number — Hue (0-360).
  • s number — Saturation (0-1).
  • v number — Value / brightness (0-1).
local primary = color.hsv(220, 0.7, 0.9)

modules/color/lighten

lighten(c: Color, amount: number): Color

Increase the lightness of a color in Oklch perceptual space.

Parameters

  • c Color — Input color.
  • amount number — Lightness increase 0-1.
local hover = color.lighten(base, 0.1)

modules/color/linear

linear(r: number, g: number, b: number, a: number?): Color

Build a color from linear RGB values (not gamma-corrected), output converted to sRGB. Useful for GPU-correct blending. Alpha defaults to 1.

Parameters

  • r number — Linear red (0-1).
  • g number — Linear green (0-1).
  • b number — Linear blue (0-1).
  • a number? (optional) — Alpha (0-1, default 1).
local gpuBlue = color.linear(0.0, 0.0, 1.0)

modules/color/mix

mix(c1: Color, c2: Color, t: number): Color

Perceptually blend two colors in Oklch space — better than RGB mixing for gradients.

Parameters

  • c1 Color — First color.
  • c2 Color — Second color.
  • t number — Blend factor 0-1 (0 = c1, 1 = c2).
local mid = color.mix(color.rgb(255, 0, 0), color.rgb(0, 0, 255), 0.5)

modules/color/mixRgb

mixRgb(c1: Color, c2: Color, t: number): Color

Linearly blend two colors in sRGB space — simple, but not perceptually uniform. Prefer color.mix for natural gradients.

Parameters

  • c1 Color — First color.
  • c2 Color — Second color.
  • t number — Blend factor 0-1.
local plain = color.mixRgb(a, b, 0.5)

modules/color/oklch

oklch(l: number, c: number, h: number): Color

Build a color from Oklch perceptual color space (l: 0-1, c: 0-0.4, h: 0-360). Ideal for perceptually uniform gradients and color manipulation.

Parameters

  • l number — Lightness (0-1).
  • c number — Chroma / saturation (0-0.4).
  • h number — Hue (0-360).
local accent = color.oklch(0.7, 0.15, 30)

modules/color/rgb

rgb(r: number, g: number, b: number): Color

Build an sRGB color from CSS-style 0-255 RGB channels. Alpha defaults to 1. Channels are normalised to 0-1 on the way out so the result composes with every other color helper.

Parameters

  • r number — Red channel (0-255).
  • g number — Green channel (0-255).
  • b number — Blue channel (0-255).
local red = color.rgb(255, 0, 0)

modules/color/rgba

rgba(r: number, g: number, b: number, a: number): Color

Build an sRGB color from CSS-style 0-255 RGB channels with explicit alpha. RGB are normalised to 0-1; alpha is taken as-is in the 0-1 range.

Parameters

  • r number — Red channel (0-255).
  • g number — Green channel (0-255).
  • b number — Blue channel (0-255).
  • a number — Alpha (0-1).
local halfRed = color.rgba(255, 0, 0, 0.5)

modules/color/rotateHue

rotateHue(c: Color, degrees: number): Color

Rotate the hue of a color by a given number of degrees in Oklch space.

Parameters

  • c Color — Input color.
  • degrees number — Hue rotation (positive or negative).
local triadic = color.rotateHue(base, 120)

modules/color/saturate

saturate(c: Color, amount: number): Color

Increase the chroma (saturation) of a color in Oklch space.

Parameters

  • c Color — Input color.
  • amount number — Chroma increase (typically 0-0.2).
local pop = color.saturate(base, 0.05)

modules/color/toHex

toHex(c: Color): string

Convert a color to a hex string. Returns "#rrggbb" or "#rrggbbaa" if alpha is not 1.

Parameters

  • c Color — Input color.
print(color.toHex(color.rgb(255, 136, 0))) -- "#ff8800"

modules/color/toHsl

toHsl(c: Color): HslColor

Convert a color to HSL.

Parameters

  • c Color — Input color.
local hsl = color.toHsl(base)

modules/color/toLinear

toLinear(c: Color): Color

Convert a color from sRGB to linear RGB space — useful for GPU calculations that need linear-space values.

Parameters

  • c Color — Input sRGB color.
local gpu = color.toLinear(base)

modules/color/toOklch

toOklch(c: Color): OklchColor

Convert a color to Oklch perceptual color space.

Parameters

  • c Color — Input color.
local okl = color.toOklch(base)

modules/color/withAlpha

withAlpha(c: Color, a: number): Color

Return a copy of a color with a different alpha value.

Parameters

  • c Color — Input color.
  • a number — New alpha (0-1).
local ghost = color.withAlpha(base, 0.3)

modules/compute/README

require("@builtin::modules.api.engine.compute") -- compute (also available as global 'compute')

GPU compute pipelines — compile shaders, dispatch workgroups, read back results. Public Luau surface over the __compute Internal FFI namespace. A buffer belongs to the shader that owns it (shaderRef:createBuffer) or to the substrate (substrate.createBuffer), and reaches a dispatch as a handle.

Usage: local compute = require("@builtin::modules.api.engine.compute") Also available as global: compute

modules/compute/absentReasons

absentReasons(): { string }

Every reason compute.diagnose reports, sorted. resident is the one that means the resource is there.

for _, r in ipairs(compute.absentReasons()) do print(r) end

modules/compute/beginBvh

beginBvh(instances: { any }, opts: { [string]: any }?): (number?, string?)

Start the build compute.buildBvh runs, without running any of it. Takes the same instances and options and reports the same non-resident guids, and returns an id compute.stepBvh advances a bounded slice at a time and compute.finishBvh collects. Each mesh the instances name is copied as this is called — once per guid however many instances share it — so the CPU mesh may be unloaded on the next line and the build still finishes on the copy it holds. compute.buildBvhSliced is the whole loop as one call.

Parameters

  • instances { any } — Array of { guid, transform, attributes? } mesh instances.
  • opts { [string]: any }? (optional) — Optional { maxLeaf? } — max triangles per leaf.
local id = compute.beginBvh(gather.instances)

modules/compute/buildBvh

buildBvh(instances: { any }, opts: { [string]: any }?): (any, string?)

Build a bounding-volume hierarchy over the world-space triangles of a set of mesh instances and upload it as two named compute buffers — geometry never passes through the scripting heap. Each instance is { guid, transform, attributes? }: guid names a mesh resident in the meshcpu store (materialise with ref:load() / meshcpu.load), transform is 16 numbers, row-major, translation in slots 4/8/12, and attributes is up to 40 floats stamped onto every triangle of that instance (surface colors, material ids, physics tags — whatever the consuming shader wants per-surface). Triangles pack 18 vec4 each (v0/v1/v2, n0/n1/n2, uvs, then 10 attribute vec4s — float slots 32.. carry the instance attributes, zero when absent) in BVH leaf order; nodes 2 vec4 each (min + first-or-left, max + leaf-tagged count-or-right). Consumers: GI baking, ray-traced passes, GPU picking, navmesh and SDF generation.

Parameters

  • instances { any } — Array of { guid, transform, attributes? } mesh instances.
  • opts { [string]: any }? (optional) — Optional { maxLeaf? } — max triangles per leaf.
local bvh = compute.buildBvh(instances)
asset.resolve("raytrace", "computeShader"):dispatch({ buffers = { bvh.nodes, bvh.tris }, workgroups = { 64, 1, 1 } })

modules/compute/buildBvhSliced

buildBvhSliced(instances: { any }, opts: { [string]: any }?): (any, string?)

The hierarchy compute.buildBvh builds, spread over as many frames as it takes: a slice of the build per frame, so a scene's triangle count costs the frame loop budgetMs at a time instead of the whole build at once. Yields, so it is called from a task. The result is the same pair of buffers and the same counts compute.buildBvh returns.

Parameters

  • instances { any } — Array of { guid, transform, attributes? } mesh instances.
  • opts { [string]: any }? (optional) — Optional { maxLeaf?, budgetMs? } — max triangles per leaf, and the wall time one frame may spend on the build (default 4 ms).
local built, err = compute.buildBvhSliced(gather.instances, { budgetMs = 4 })

modules/compute/bvhBuilds

bvhBuilds(): { any }

What the builds started by compute.beginBvh and not yet finished are costing, oldest id first. Each row is { id, phase, triangles, units, slices, cpuMs, uploadedBytes }: phase is "gather", "build", "serialize", "upload" or "ready", triangles how many have been gathered, units the work units run, slices the compute.stepBvh calls they ran in, cpuMs the wall time spent inside those calls, and uploadedBytes how much of the hierarchy has reached the GPU.

print(#compute.bvhBuilds(), "hierarchies in flight")

modules/compute/cancelBvh

cancelBvh(id: number): boolean

Drop a build along with the triangles it has gathered.

Parameters

  • id number — Build id from compute.beginBvh.
compute.cancelBvh(id)

modules/compute/compile

compile(nameOrHandle: string | { [string]: any } | AssetRef, opts: { [string]: any }?): (boolean, string?)

Compile a compute shader from inline WGSL + a declarative binding schema — the same codegen a .computeShader asset uses. The engine generates the @group/@binding declarations from bindings/params, so the source writes only @compute fn main. Symmetric with registerShader, but with zero-scaffolding bindings (incl. textures, samplers, storage textures and a params uniform). For asset-backed shaders prefer authoring a .computeShader (compiled automatically); use this for dynamic/generated compute shaders.

Parameters

  • nameOrHandle string | { [string]: any } | AssetRef — Shader identity to register under (string or asset handle).
  • opts { [string]: any }? (optional) — { source, entryPoint?, bindings, params? } — bindings is an ordered list of { name, kind, access?, element?, format?, array?, sample? }.
compute.compile("my_sim", { source = WGSL, bindings = { { name = "data", kind = "buffer", access = "read_write", element = "f32" } } })

modules/compute/compileByName

compileByName(ref: string | { [string]: any } | AssetRef)

Optional explicit pre-warm for a .computeShader asset (idempotent — fingerprint-guarded). NORMALLY UNNECESSARY: compute.dispatch / dispatchEx auto-compile a .computeShader on first use. Reach for this only to avoid the one-frame first-dispatch warm-up in a latency-critical spot. Accepts an identity/guid string or a resolved asset handle (its .identity is used).

Parameters

  • ref string | { [string]: any } | AssetRef — A .computeShader identity/guid string, or a resolved asset handle.
compute.compileByName("@builtin::shaders.compute_double")

modules/compute/copyBufferToTexture

copyBufferToTexture(bufferName: string, textureKey: string, width: number, height: number, format: string?, rowBytes: number?): boolean

Copy a compute buffer into a cached GPU texture under textureKey, staying on the GPU. The path for an image a compute pass produced: the buffer holds tightly-packed rows in the format's texel layout, and the result is an ordinary cached texture — sample it from a material, or pack it into the shared feature-texture array. The copy reads rows 256-byte aligned: rows packed tight must be a multiple of 256 bytes (at rgba16f, any width that is a multiple of 32), and any other width is laid out with its rows padded to that alignment and rowBytes saying how far apart they are.

Parameters

  • bufferName string — Source compute buffer.
  • textureKey string — Cache key to register the texture under.
  • width number — Texture width in texels.
  • height number — Texture height in texels.
  • format string? (optional) — Texel format: "rgba16f" (default), "rgba32f", "rgba8".
  • rowBytes number? (optional) — Bytes from one row's start to the next when rows are padded past width texels — a multiple of 256 at least a row's texels long.
compute.copyBufferToTexture("gi_resolved", "lm_wall", 128, 128)
compute.copyBufferToTexture("heights_packed", "heights", 300, 300, "rgba16f", 2560)

modules/compute/createBuffer

createBuffer(name: string, opts: { [string]: any }): boolean

Allocate a buffer under name, sized in bytes.

Parameters

  • name string — The name a dispatch binds it by.
  • opts { [string]: any } — { size, readback? } — size in bytes.

modules/compute/createSampler

createSampler(name: string, opts: { [string]: any }?): boolean

Create a named GPU sampler. opts: filter/wrap settings.

Parameters

  • name string
  • opts { [string]: any }? (optional)

modules/compute/createStorageTexture2D

createStorageTexture2D(name: string, opts: { [string]: any }): boolean

Create a 2D storage texture (compute-writable render target). opts: { width, height, format? }.

Parameters

  • name string
  • opts { [string]: any }

modules/compute/createTexture3D

createTexture3D(name: string, opts: { [string]: any }): boolean

Create a 3D texture volume. opts: { width, height, depth, format?, storage? }.

Parameters

  • name string — Unique volume name.
  • opts { [string]: any } — Dimensions + format (r8/r16f/r32f/rgba8/rgba16f/rgba32f).

modules/compute/createTextureHistory

createTextureHistory(name: string, opts: { [string]: any }): boolean

Create a temporal history buffer (ping-pong textures) for a target. opts: { width, height, format? }.

Parameters

  • name string
  • opts { [string]: any }

modules/compute/destroyBuffer

destroyBuffer(name: string): boolean

Release the buffer allocated under name.

Parameters

  • name string — The name it was created under.

modules/compute/destroySampler

destroySampler(name: string): boolean

Release a named sampler created by compute.createSampler and free it. The counterpart to that call, alongside destroyBuffer, destroyTexture, destroyTexture3D, destroyStorageTexture2D and destroyTextureHistory. The manager's own defaults (linear_clamp, linear_repeat, nearest_clamp) are kept for the session, since a compute pass binds them by name.

Parameters

  • name string — Sampler name.
compute.createSampler("soft", { filter = true }); compute.destroySampler("soft")

modules/compute/destroyShader

destroyShader(name: string): boolean

Destroy a named compute shader pipeline.

Parameters

  • name string — Shader name.

modules/compute/destroyShaderEx

destroyShaderEx(name: string): boolean

Destroy a shader registered via registerShaderEx.

Parameters

  • name string

modules/compute/destroyStorageTexture2D

destroyStorageTexture2D(name: string): boolean

Destroy a named 2D storage texture.

Parameters

  • name string

modules/compute/destroyTexture

destroyTexture(textureKey: string): boolean

Release the cached GPU texture copyBufferToTexture registered under textureKey, freeing its memory. Call it once the image is no longer sampled. Writing the same key again replaces the texture, so a key you keep re-using holds one allocation.

Parameters

  • textureKey string — Cache key the texture was registered under.
compute.destroyTexture("lm_wall")

modules/compute/destroyTexture3D

destroyTexture3D(name: string): boolean

Destroy a named 3D volume and free its GPU memory.

Parameters

  • name string

modules/compute/destroyTextureHistory

destroyTextureHistory(name: string): boolean

Destroy a named texture-history buffer.

Parameters

  • name string

modules/compute/diagnose

diagnose(key: string): { [string]: any }

Whether a resource is filed under key right now, and when none is, which state the inventory says the key is in. A key out of a dispatch failure resolves here; a mistyped one reports why it does not.

Parameters

  • key string — The resource key, verbatim.
local d = compute.diagnose(key)
if not d.exists then print(d.reason, d.current) end

modules/compute/dispatch

dispatch(shaderNameOrHandle: string | { [string]: any } | AssetRef, opts: DispatchOpts): boolean

Dispatch a compute shader with bound buffers. Accepts a shader name string or an asset handle from asset.load().

Parameters

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts DispatchOpts — { buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).
compute.dispatch("blur", { buffers = { "src", "dst" }, workgroups = { 8, 8 } })

modules/compute/dispatchEx

dispatchEx(shaderNameOrHandle: string | { [string]: any } | AssetRef, opts: { [string]: any }): boolean

Dispatch a compute shader with extended texture/storage/sampler bindings. Asset-backed .computeShaders resolve to their stable guid (collision-safe, lazily compiled on first dispatch); raw registerShaderEx names pass through. resources covers the bindings the shader DECLARES. A params: block's uniform is engine-owned — the compile creates and packs it, setParam writes it, and the dispatch binds it — so it takes no entry here.

Parameters

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts { [string]: any } — { resources, workgroups } — each resource is { kind, name }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

modules/compute/dispatchOnVertices

dispatchOnVertices(shaderNameOrHandle: string | { [string]: any } | AssetRef, opts: DispatchOnVerticesOpts): boolean

Dispatch a compute shader with a model's vertex buffer bound at binding 0 (read_write). Use to mutate vertex positions directly. Asset-backed .computeShaders resolve to their stable guid (collision-safe, lazily compiled on first dispatch); raw registerShader names pass through.

Parameters

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts DispatchOnVerticesOpts — { model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).
compute.dispatchOnVertices("flatten", { model = "@user/mesh", workgroups = { 64 } })

modules/compute/failing

failing(): { { [string]: any } }

Every compute dispatch whose most recent run FAILED, one record per (shader, target) pair. A dispatch is recorded into a command encoder frames after the call that asked for it returned, so a pass that stops running reports here rather than through that call's return value: each record carries the shader key, the target it writes (a mesh guid for a dispatch over vertices, the buffers it bound for one that writes only those), how many dispatches and failures it has had, and lastError. An empty result means every dispatch the engine has been given is running.

for _, d in ipairs(compute.failing()) do print(d.shader, d.lastError) end

modules/compute/finishBvh

finishBvh(id: number): (any, string?)

Hand over a finished build's hierarchy as the same two buffers compute.buildBvh returns, and release the build. The slices put every byte of it on the GPU as they ran, so this costs the frame it is called in the handover and nothing of the scene.

Parameters

  • id number — Build id from compute.beginBvh, stepped until "ready".
local built = compute.finishBvh(id)

modules/compute/getReadbackResult

getReadbackResult(resultKey: string): { number }?

Poll for a completed read-back and return its bytes as a 1-indexed array of f32 values, nil if pending. The f32 reinterpretation applies to whatever the buffer holds: bytes written as u32 1, 2, 3, 4 read back here as 1.4e-45, 2.8e-45, 4.2e-45, 5.6e-45 — use getReadbackResultU32() for those, or getReadbackResultBytes() for a buffer the rest of the buffer surface accepts. Result is consumed on retrieval, and polling a key that was never issued raises rather than reading as forever-pending.

Parameters

  • resultKey string — Key returned by readBuffer().
local floats = compute.getReadbackResult(key)

modules/compute/getReadbackResultBytes

getReadbackResultBytes(resultKey: string): buffer?

Poll for a completed read-back and get its raw bytes as a buffer, copied once. The read counterpart of writeBufferBytes: read values out with buffer.readf32 / buffer.readu32, or hand the buffer straight to writeBuffer — a payload that stays packed never becomes a table. Result is consumed on retrieval.

Parameters

  • resultKey string — Key returned by readBuffer().
local bytes = compute.getReadbackResultBytes(key)
local firstTexel = buffer.readf32(bytes, 0)

modules/compute/getReadbackResultU32

getReadbackResultU32(resultKey: string): { number }?

Poll for a completed read-back interpreting bytes as u32. Returns array of integer values if ready, nil if pending.

Parameters

  • resultKey string — Key returned by readBuffer().

modules/compute/isReadbackReady

isReadbackReady(resultKey: string): boolean

Check if a readback result is available without consuming it. Raises for a key this engine never issued, or whose result was already drained — nil/false already means "still in flight", so a mistyped key reports itself instead of polling forever. Use readbackState() to test that case without raising.

Parameters

  • resultKey string — Key returned by readBuffer().

modules/compute/markBaseline

markBaseline(): number

Record which compute resources are resident right now, and make that the point compute.resetToBaseline returns to.

A caller that runs work in someone else's engine — a sweep of test suites sharing one, an editor tool driving a session — allocates as it goes: a pass builds the grid its quality asks for and keeps it, a shader files the uniform it was compiled with. Kept is right for a frame that draws again and wrong for the next caller, which then reads an inventory it did not make. This is how a caller says where its own allocations start.

local mark = compute.markBaseline() -- then run, then compute.resetToBaseline(mark)

modules/compute/observe

observe(): { [string]: any }

Every GPU resource the compute subsystem is holding right now — its storage and uniform buffers, its 3D textures, its 2D storage targets, its history pairs and its samplers — with what each one costs and which shader asked for it. This is the call to reach for when compute is holding memory and you do not know what, or when a key out of a dispatch failure needs matching against what exists.

local r = compute.observe()
print(r.totals.count, r.totals.bytes)
for _, res in ipairs(r.resources) do print(res.key, res.kind, res.bytes) end

modules/compute/program.compile

program.compile(key: string, spec: { [string]: any }): (boolean, string?)

Register a compiled program under key from WGSL plus a declared binding schema. The engine generates the @group/@binding declarations from the schema, expands #includes, naga-validates, and registers the result.

Parameters

  • key string — The key to register under.
  • spec { [string]: any } — { source, entryPoint?, bindings, params, ranges? } — the parsed schema. A buffer binding stating range = true (or every array buffer binding, with ranges = true) takes a range of a buffer: a dispatch binds it with zero_base_NAME / zero_len_NAME in its params, and the body's NAME[i] and arrayLength(&NAME) address that range. One that states no range binds the whole buffer.

modules/compute/program.destroy

program.destroy(key: string): boolean

Release the program registered under key.

Parameters

  • key string — The program's key.

modules/compute/program.dispatch

program.dispatch(key: string, opts: { [string]: any }): boolean

Dispatch the program under key with one buffer per declared storage binding, in declaration order.

Parameters

  • key string — The program's key.
  • opts { [string]: any } — { buffers, workgroups, params? }. params gives values of the program's params: uniform, by name, to this dispatch alone: two dispatches of one program in a frame each read their own, and a value left out reads what the program holds. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

modules/compute/program.dispatchEx

program.dispatchEx(key: string, opts: { [string]: any }): boolean

Dispatch the program under key with explicit resources — one { kind, name } per declared binding, in declaration order.

Parameters

  • key string — The program's key.
  • opts { [string]: any } — { resources, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

modules/compute/program.dispatchOnVertices

program.dispatchOnVertices(key: string, opts: { [string]: any }): boolean

Dispatch the program under key over a mesh's vertices. The mesh opts.model names fills the shader's vertices binding, and opts.buffers fills the remaining storage bindings.

Parameters

  • key string — The program's key.
  • opts { [string]: any } — { model, buffers?, workgroups, params? }. params gives values of the program's params: uniform to this dispatch alone. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

modules/compute/program.dispatches

program.dispatches(): number

How many dispatches the running thread has queued since it began, a run of a compute list counting each dispatch the list holds. Two readings taken around a piece of work give what that work dispatched.

modules/compute/program.dropList

program.dropList(key: string): boolean

Forget the compute list under key.

Parameters

  • key string — The list's key.

modules/compute/program.forPlugin

program.forPlugin(key: string, bindings: number | { string }): any

The program under key as a handle a script passes a plugin in a call, for it to dispatch over the GPU buffers and textures it was handed. bindings is how many storage buffers the shader binds, or the kind of each binding in declaration order as bindings.yaml spells them (buffer, texture3d, storage3d, storage2d).

Parameters

  • key string — The program's key.
  • bindings number | { string } — A count of buffers, or each binding's kind.

modules/compute/program.list

program.list(key: string, entries: { { [string]: any } }): boolean

Register the compute list under key, replacing one already there: an ordered run of dispatches and buffer writes that runList queues in one call, each value a run supplies named as a slot.

Parameters

  • key string — The key to register the list under.
  • entries { { [string]: any } } — The dispatches and writes, in the order they run. A dispatch is { program, buffers, workgroups, params?, slots?, runs?, writes?, when? }, slots mapping a param name to the slot that gives it its value. runs names the runs the dispatch reaches through a page it binds and writes the ones of those it writes: the list holds them standing (a replay over a run that was let go of is refused, not run) and moves the write revision of the ones it writes. A write is { write = bufferName, offset?, values, unsigned?, slots?, when? }: offset in 32-bit words, values written as f32 (u32 when unsigned), slots mapping a value's place, from 1, to the slot that gives it. when names a slot: a run giving it 0 leaves the entry out.

modules/compute/program.listCost

program.listCost(key: string): (number?, number?)

What recording the runs of the compute list under key has cost the render side so far. A run is recorded on a frame after the one that queued it, so a cost read just after runList holds the runs before that one.

Parameters

  • key string — The list's key.

modules/compute/program.record

program.record(fn: ({ { [string]: any } }) -> ()): { { [string]: any } }

Run fn and answer every compute.program.dispatch it made, in order, as the entries of a compute list: { program, buffers, workgroups, params } each. The dispatches are queued as they always are; the answer is what compute.program.list takes to replay them. fn is handed the entries as they are made, so it can add slots to one it just dispatched.

Parameters

  • fn ({ { [string]: any } }) -> () — The work to run, given the entries recorded so far.

modules/compute/program.runList

program.runList(key: string, slots: { [string]: number }?, label: string?): boolean

Queue the compute list under key: every entry in order, each value an entry names a slot for taken from slots, every other value as the list holds it. While the GPU profiler is read the run is recorded as a compute pass of its own, and its GPU time is the compute.list:<label> row of the gpu report.

Parameters

  • key string — The list's key.
  • slots { [string]: number }? (optional) — The value of each slot for this run, by name.
  • label string? (optional) — The name the run's GPU time is reported under; the list's key when absent.

modules/compute/program.setParam

program.setParam(key: string, prop: string, value: number): boolean

Write one scalar of the program's params: uniform. A value set before the program's first compile is the value it starts with.

Parameters

  • key string — The program's key.
  • prop string — Parameter name as declared.
  • value number — New scalar value.

modules/compute/program.status

program.status(key: string): { { [string]: any } }

What the engine did with the dispatches of the program under key, one record per target.

Parameters

  • key string — The program's key.

modules/compute/programState

programState(ref: string | { [string]: any } | AssetRef): (string, string?)

Where a shader's compiled program stands. A registration is queued from script and the pipeline is built on the render side frames later, so the call that asked for the compile cannot say whether it produced a program: "absent" (the engine holds nothing under this key and nothing is in flight — never asked for, or released), "pending" (asked for, not on the device yet — a recompile of a resident program reads pending too, because what it produces is a different program from the one bound now), "ready" (compiled and resident, so a dispatch binds it), or "failed" (the most recent registration produced no program), returned with the reason as a second value. A .computeShader whose bindings.yaml its compile refuses reads "failed", with the reason naming the file and the line, because no program is compiled from the file as it stands. Wait for "ready" before a dispatch whose result is read back, rather than for a count of frames.

Parameters

  • ref string | { [string]: any } | AssetRef — A .computeShader identity/guid string, a resolved asset handle, or the name a raw registration chose.
if compute.programState(carve) == "ready" then carve:dispatch(opts) end

modules/compute/readBuffer

readBuffer(name: string): string

Start a GPU→CPU read of the buffer under name.

Parameters

  • name string — The name it was created under.

modules/compute/readTexture3D

readTexture3D(name: string): string

Start a GPU→CPU readback of a named 3D volume. Returns a result key to poll.

Parameters

  • name string

modules/compute/readbackPromise

readbackPromise(resultKey: string): any

The promise that settles when a read-back reaches a terminal state, delivered or given up on. task.await it to wait for the read without polling: where the engine can wait on the device the coroutine resumes in the frame the device answered, so a task that reads, uses the answer and reads again does all of it in one frame; elsewhere it resumes on the next frame the answer reaches the engine. Answers nil when the read-back is not in flight (already delivered, failed, drained or never issued), and readbackState() then says which.

Parameters

  • resultKey string — Key returned by readBuffer().
local promise = compute.readbackPromise(key); if promise then task.await(promise) end

modules/compute/readbackState

readbackState(resultKey: string): string

Where a readback key stands, without consuming it and without raising: "pending" (issued, GPU has not delivered), "ready" (delivered, waiting to be drained), "failed" (given up on, with the reason as a second return), or "unknown" (never issued by readBuffer(), or already drained — a result is delivered once).

Every read leaves "pending": one the GPU does not answer inside the bound the engine holds it to reaches "failed" naming the buffer. A wait on readbackState(key) ~= "pending" therefore ends on what the read did, which a count of engine frames cannot say.

Parameters

  • resultKey string — Key returned by readBuffer().
if compute.readbackState(key) == "ready" then ... end

modules/compute/registerShader

registerShader(nameOrHandle: string | { [string]: any } | AssetRef, opts: ShaderOpts?): boolean

Register a compute shader. Accepts an asset handle from asset.load(), or (name, opts) with inline WGSL source.

Parameters

  • nameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts ShaderOpts? (optional) — Shader options. bindings comes from the source's own @group(0) @binding(n) declarations when omitted; supplying a count that disagrees with them raises. Every readOnlyBindings entry names one of those declared bindings, as a whole number from 0 to bindings - 1; an entry outside that run raises.
compute.registerShader("blur", { source = WGSL, entryPoint = "main" })

modules/compute/registerShaderEx

registerShaderEx(nameOrHandle: string | { [string]: any } | AssetRef, opts: { [string]: any }?): boolean

Register a compute shader with extended texture/sampler bindings. Accepts a name + opts or an asset handle.

Parameters

  • nameOrHandle string | { [string]: any } | AssetRef
  • opts { [string]: any }? (optional)

modules/compute/releaseBaseline

releaseBaseline(generation: number): boolean

Drop the mark generation names, and every mark taken after it, once the caller that took it is done resetting to it. It touches the marks alone.

Parameters

  • generation number — The mark to drop, as compute.markBaseline returned it.
compute.releaseBaseline(mark) -- the run is over

modules/compute/resetToBaseline

resetToBaseline(generation: number?): number

Release every compute resource allocated since compute.markBaseline, leaving the ones it recorded where they are.

Each kind this closes is one a caller allocated under a name it chose, so the next caller finds that name free. A storage buffer is such a resource and is not closed here: every one a caller can make is a substrate buffer, and substrate.resetToBaseline is what puts those back. A compiled shader's params uniform is not, and is left where it is: it is filed under its shader's guid, which no other caller can ask for, and it belongs to the .computeShader asset that compiled it, released when that is. mark is closed; nothing the mark recorded is touched.

Parameters

  • generation number? (optional) — The mark to restore to, as compute.markBaseline returned it; the newest mark when omitted. The marks taken after it go with the reset.
local released = compute.resetToBaseline(mark)

modules/compute/resources

resources(owner: any?): { any }

The resource rows on their own, optionally narrowed to what one shader owns.

Parameters

  • owner any? (optional) — A .computeShader ref, its guid, or its asset identity. Omit for every resource compute holds. A value carrying no shader raises, so a narrowing that cannot be done reads as an error rather than as the whole inventory. A guid stands for itself, so resources outlive the asset that made them and stay reachable by their owner.
for _, r in ipairs(compute.resources(shaderRef)) do print(r.key, r.bytes) end

modules/compute/setParam

setParam(nameOrHandle: string | { [string]: any } | AssetRef, prop: string, value: number): boolean

Set a named scalar parameter on a .computeShader (a params: entry in its bindings.yaml). Updates the shader's params uniform in place; the next dispatch sees the new value. No effect on raw registerShader shaders, which have no params block.

Parameters

  • nameOrHandle string | { [string]: any } | AssetRef — Shader identity (the .computeShader asset name), or the handle asset.load / asset.resolve returns — the same forms dispatch takes.
  • prop string — Parameter name as declared in bindings.yaml.
  • value number — New scalar value (numbers only).
compute.setParam("my_sim", "scale", 4.0)

modules/compute/stepBvh

stepBvh(id: number, budgetMs: number?): (string?, string?)

Advance a build by as many work units as budgetMs buys, and report whether it has finished: "pending" means there is work left, "ready" means compute.finishBvh will hand over the buffers. The slices carry the hierarchy onto the GPU as well as building it, so a build that reads "ready" has already uploaded every byte of itself. A slice always runs at least one unit, so a budget of 0 advances the build by exactly one and the largest single unit sets the floor under a slice.

Parameters

  • id number — Build id from compute.beginBvh.
  • budgetMs number? (optional) — Wall time this slice may spend, in milliseconds (default 4).
while compute.stepBvh(id, 4) == "pending" do task.wait() end

modules/compute/textureForPlugin

textureForPlugin(name: string): any

The texture this script created under name as a handle a script passes a plugin in a call. The handle holds the texture by an id of its own, never by the name: once the texture is destroyed the handle refuses, even when the name is made again. Only a texture made by compute.createTexture3D or compute.createStorageTexture2D can be handed over.

Parameters

  • name string — The texture's name.

modules/compute/textureFormatBytes

textureFormatBytes(format: string): number

Bytes-per-voxel for a texture format string (rgba16f, r8, ...).

Parameters

  • format string

modules/compute/writeBuffer

writeBuffer(name: string, values: { number } | buffer | string, offset: number?): boolean

Write words into the buffer under name.

Parameters

  • name string — The name it was created under.
  • values { number } | buffer | string — The floats to write, or a buffer / binary string already holding them.
  • offset number? (optional) — 32-bit word offset to write at.

modules/compute/writeBufferBytes

writeBufferBytes(name: string, bytes: buffer | string, offsetBytes: number?): boolean

Write packed bytes into the buffer under name.

Parameters

  • name string — The name it was created under.
  • bytes buffer | string — The payload.
  • offsetBytes number? (optional) — Byte offset to write at.

modules/compute/writeBufferU32

writeBufferU32(name: string, values: { number } | buffer | string, offsetBytes: number?): boolean

Write 32-bit words into the buffer under name.

Parameters

  • name string — The name it was created under.
  • values { number } | buffer | string — The words to write.
  • offsetBytes number? (optional) — Byte offset to write at.

modules/compute/writeFloatsTexture3D

writeFloatsTexture3D(name: string, floats: { number }, formatOrOpts: (string | { [string]: any })?): boolean

Upload float values into a named 3D volume, packed via the given format (default rgba16f).

Parameters

  • name string
  • floats { number }
  • formatOrOpts (string | { [string]: any })? (optional)

modules/compute/writeTexture3D

writeTexture3D(name: string, data: buffer | string | { number }): boolean

Upload raw bytes (u8) into a named 3D volume. A buffer or a binary string holds the volume's byte layout verbatim and crosses in one copy — the shape a file's voxel payload arrives in; an array carries one byte value (0..255) per entry.

Parameters

  • name string — Volume name.
  • data buffer | string | { number } — Voxel bytes as a buffer, a binary string, or an array of bytes.

modules/compute_abi/README

require("@builtin::modules.compute_abi") -- compute_abi

The kernel ABI of a .computeShader: the bindings.yaml extensions (columns bindings, output count / capacity / append with a live counter, specialize compile-time parameters, determinism), lowered to the plain bindings and WGSL the engine compiles, and the host side that allocates outputs, packs column tables and dispatches a variant.

Usage: local compute_abi = require("@builtin::modules.compute_abi")

modules/compute_abi/accessorLines

accessorLines(name: string, element: string, lanes: number?, writable: boolean): { string }?

The lines lower generates for one buffer binding: its length, its read and, when it is written, its write.

Parameters

  • name string — The binding's name.
  • element string — The element type as the schema states it after specialization (vec3<f32>).
  • lanes number? (optional) — How many scalars the element holds.
  • writable boolean — Whether the binding is written.
local lines = abi.accessorLines("src", "vec3<f32>", 3, false)

modules/compute_abi/allocationCount

allocationCount(): number

How many device buffers this module has made (a table of columns, an output, a live counter) since the module state began; the buffers a kernel dispatch allocates are in it.

local before = abi.allocationCount()

modules/compute_abi/buffer

buffer(self: any): any

The GPU buffer holding the table.

Parameters

  • self any (optional)

modules/compute_abi/compile

compile(baseKey: string, lowered: { [string]: any }, label: string?): string

Compile a lowered variant under <baseKey>#<hash of what it lowered to>; one variant compiles once.

Parameters

  • baseKey string — The shader's registration key.
  • lowered { [string]: any } — A value from lower.
  • label string? (optional) — The name profiling rows carry.
local key = abi.compile(guid, lowered, "kernels.map")

modules/compute_abi/compileCount

compileCount(): number

How many variants compile has registered since the module state began: a count that grows while the same work repeats says the work asks for a program of its own each time.

local before = abi.compileCount()

modules/compute_abi/compiledSince

compiledSince(since: number): { { label: string, key: string } }

What compile registered after the count stood at since (compileCount), oldest first, for the latest 4096 compiles: each variant's label and the key it is dispatched under (loweredOf(key) reads what it lowered to).

Parameters

  • since number — A count compileCount answered.
local before = abi.compileCount(); work(); for _, c in ipairs(abi.compiledSince(before)) do print(c.label) end

modules/compute_abi/copyWords

copyWords(src: string, dst: string, words: number, first: number?): boolean

Queue a word-for-word copy of the first words words of the buffer src into the buffer dst, from word first on.

Parameters

  • src string — The source buffer's name.
  • dst string — The destination buffer's name.
  • words number — How many words.
  • first number? (optional) — The word of dst the first one lands at. Default 0.
abi.copyWords(a:bufferName(), b:bufferName(), 64)

modules/compute_abi/directLimit

directLimit(schema: { [string]: any }, binding: string): number

How many columns of the columns binding binding a variant can bind directly, each as a binding of its own run: the storage buffers a shader stage holds on every device, less the buffers the schema's other bindings take and the binding's own table.

Parameters

  • schema { [string]: any } — A parsed schema.
  • binding string — The name of a columns binding that reads.
local limit = abi.directLimit(schema, "cols")

modules/compute_abi/directLimitOf

directLimitOf(program: any, binding: string): number

Program.directLimit for a program made by an earlier load of this module.

Parameters

  • program any (optional) — A program from abi.program.
  • binding string — The name of a columns binding that reads.
local limit = abi.directLimitOf(program, "cols")

modules/compute_abi/directPlan

directPlan(columns: { { [string]: any } }, limit: number): { number }

The column slots, in increasing order, that a table of columns takes as direct columns: the first limit columns that name a device buffer and hold no values of their own. A dispatch binds each buffer at one slot, so a column whose buffer an earlier direct column holds stays in the table.

Parameters

  • columns { { [string]: any } } — The array abi.table is given.
  • limit number — How many direct columns the variant can bind (abi.directLimit).
local direct = abi.directPlan(columns, abi.directLimit(schema, "cols"))

modules/compute_abi/directs

directs(self: any): { slots: { number }, buffers: { string } }

The column slots the table binds as direct columns, and the names of the buffers they are read from, in the order a variant binds them.

Parameters

  • self any (optional)

modules/compute_abi/evaluate

evaluate(text: string, vars: { [string]: number }, lengths: (string) -> number): number

Evaluate a count / capacity expression.

Parameters

  • text string — The expression.
  • vars { [string]: number } — The names it may use, with their values.
  • lengths (string) -> number — len(binding): the element count of a bound run.
abi.evaluate("n * 4", { n = 10 }, function() return 0 end) --> 40

modules/compute_abi/fingerprint

fingerprint(text: string): string

A fingerprint of a text, the one lowered variants and fused kernels are keyed by.

Parameters

  • text string — Any text.
local tag = abi.fingerprint(source)

modules/compute_abi/forget

forget()

Forget every compiled variant, so the next compile of each registers it again; for a device that was rebuilt.

modules/compute_abi/isExtended

isExtended(schema: { [string]: any }): boolean

Whether a schema uses any extension, which is what makes a shader lowered rather than compiled as written.

Parameters

  • schema { [string]: any } — A parsed schema.
if abi.isExtended(schema) then ... end

modules/compute_abi/lower

lower(schema: { [string]: any }, wgsl: string, spec: { [string]: any }?): { [string]: any }

Lower a shader's extensions to the bindings and WGSL the engine compiles.

Parameters

  • schema { [string]: any } — A parsed schema.
  • wgsl string — The shader body, with its #include literals already canonicalized.
  • spec { [string]: any }? (optional) — { specialize = { NAME = value }, columns = { [binding] = { names } }, direct = { [binding] = { slot } } }. direct names, per read columns binding, the column slots read from a binding of their own run (NAME_d0, NAME_d1, ... after the table), so those columns are not copied into the table.
local lowered = abi.lower(schema, wgsl, { specialize = { T = "vec3<f32>" } })

modules/compute_abi/loweredOf

loweredOf(key: string): { [string]: any }?

What compile lowered the program under key from: { lowered, label, base }, the lowered value lower answered (its source, bindings, params, slots, workgroup and fusion). Nil for a key this module did not compile, or one compiled before the device was rebuilt.

Parameters

  • key string — A key compile answered.
local made = abi.loweredOf(entry.program)

modules/compute_abi/names

names(self: any): { string }

The names of the table's columns, in slot order.

Parameters

  • self any (optional)

modules/compute_abi/noteWrite

noteWrite(name: string, method: string, payload: any, offset: number?)

Tell whoever is recording that the host wrote payload into the device buffer name.

Parameters

  • name string — The buffer's name.
  • method string — write (words as f32, offset in words), writeU32 or writeBytes (offset in bytes).
  • payload any (optional) — What was handed to the buffer's method.
  • offset number? (optional) — The method's offset argument.
abi.noteWrite(handle.name, "writeU32", { 0 })

modules/compute_abi/observeWrites

observeWrites(fn: (any) -> ()): () -> ()

Hear the writes the host makes into device buffers: fn(entry) is called with each write as a compute list entry. Every call returns a function that stops listening.

Parameters

  • fn (any) -> () — Called with { write = bufferName, offset = words?, values, unsigned? }.

Returns () — The function that stops listening.

local stop = abi.observeWrites(function(entry) entries[#entries + 1] = entry end)

modules/compute_abi/pageBinding

pageBinding(lowered: { [string]: any }, runs: { [string]: any }): { [string]: any }

What a dispatch of a lowered kernel with page and column bindings binds: the arena page each page binding names and the base and length of each column's run in it.

runs maps each column name to a buffer handle or name. A column's run stands in one arena page (substrate.placement); every column the kernel declares in one page binding must stand in one page, and a column outside an arena page (a buffer of its own) is a page of one column. A column in another page than the rest of its binding, or a page two bindings name, raises naming both: the kernel is lowered for the partition of columns it was given, and a partition that differs is a different kernel.

Parameters

  • lowered { [string]: any } — A value from lower.
  • runs { [string]: any } — { [column] = handle | name }.
local bound = abi.pageBinding(lowered, { pos = positions, vel = velocities })

modules/compute_abi/pageEntry

pageEntry(key: string, lowered: { [string]: any }, runs: { [string]: any }, opts: { [string]: any }): { [string]: any }

The entry of a compute list (compute.program.list) that dispatches a page-bound kernel over runs: the program, the pages it binds, the base and length of each column, and the runs it reaches through those pages, with the ones its column declarations write. A list holding the entry keeps those runs standing and moves the write revision of the ones the kernel writes, so a replay never runs over a run that was let go of, and what reads a written run sees it changed.

Parameters

  • key string — The key compile answered for the kernel.
  • lowered { [string]: any } — A value from lower.
  • runs { [string]: any } — { [column] = handle | name }.
  • opts { [string]: any } — { workgroups, params?, buffers?, slots?, when? } — params are the kernel's own, buffers names the buffer of each non-page binding, slots and when are a list entry's.
compute.program.list("step", { abi.pageEntry(key, lowered, runs, { workgroups = { 1, 1, 1 } }) })

modules/compute_abi/parse

parse(content: string, file: string?): { [string]: any }

Parse bindings.yaml into { bindings, params, specialize, determinism, workgroup }. A binding without a kind is a buffer. Every YAML list form reads: flow mappings, block mappings and flow lists. Raises, naming file, on text the YAML decoder refuses (with the line) and on a list key holding anything but a list of mappings (with the key).

Parameters

  • content string — The text of bindings.yaml.
  • file string? (optional) — The file named in an error; bindings.yaml when omitted.
local schema = abi.parse(vfs.read(path .. "/bindings.yaml"), path .. "/bindings.yaml")

modules/compute_abi/program

program(def: { [string]: any }): any

A shader's ABI, ready to specialize and dispatch.

Parameters

  • def { [string]: any } — { key, label?, schema, wgsl, canonicalize? }: the registration key, the parsed schema, the shader body, and a function that resolves the #include literals of the lowered source.
local program = abi.program({ key = guid, schema = schema, wgsl = wgsl })

modules/compute_abi/read

read(handle: any, asU32: boolean?): { number }?

Read a buffer's words back, waiting for the frames the read takes.

Parameters

  • handle any (optional) — A buffer handle created with usage = { "readback" }.
  • asU32 boolean? (optional) — Answer the words as u32 (default f32).
local values = abi.read(out.buffer)

modules/compute_abi/release

release(baseKey: string): number

Release every variant compile registered under baseKey, and the program program made for it: each variant's compiled program and params uniform are destroyed, and the next compile of one registers it again. The teardown of a shader whose asset is gone.

Parameters

  • baseKey string — The shader's registration key.
local released = abi.release(guid)

modules/compute_abi/repack

repack(self: any)

Copy the device buffers the table was packed from into it again, where they stand now: what a rewrite of those buffers in place leaves the table to read. The numbers it was given on the CPU stay as written.

Parameters

  • self any (optional)
cols:repack()

modules/compute_abi/run

run(self: any, env: { [string]: any }): { [string]: any }

Dispatch the variant over env.n elements.

Parameters

  • self any (optional)
  • env { [string]: any } — { n, inputs = { [binding] = buffer | name | table }, outputs?, params?, values?, class?, owner? }: n is the element count; params sets the shader's own params; values adds names to count expressions; class ("effect" or "persistent") makes every buffer the run allocates a run of the device arenas, held for owner (substrate.createBuffer). Outputs with count or capacity are allocated here, except those outputs names: outputs[binding] is a buffer (or { buffer, count?, capacity?, counter? }) the dispatch writes in place, and an appended output given one needs its counter, whose count the dispatch keeps appending to. The first run of a variant waits for its program to be built, so a read of an output asked for after the run sees what the dispatch wrote.
local r = variant:run({ n = 10, inputs = { src = src } })

modules/compute_abi/slot

slot(self: any, name: string): number

The slot a named column holds.

Parameters

  • self any (optional)
  • name string

modules/compute_abi/specKey

specKey(spec: { [string]: any }?): string

The variant for a specialization: lowered and compiled once.

Parameters

  • spec { [string]: any }? (optional) — { specialize = { NAME = value }, columns = { [binding] = { names } }, direct = { [binding] = { slot } } }.
local v = program:variant({ specialize = { T = "vec3<f32>" } })

modules/compute_abi/table

table(columns: { { [string]: any } }, opts: { [string]: any }?): any

Pack columns into one buffer a columns binding reads.

Parameters

  • columns { { [string]: any } } — An array of { name, kind = "f32" | "i32" | "u32", lanes?, count, values? | buffer? }. values are numbers a column holds, element after element (lanes contiguous); buffer is a GPU buffer whose words are copied in on the device.
  • opts { [string]: any }? (optional) — { name = string, direct = { slot }, class?, owner? }: the name the buffer is created under, and the 0-based slots of the columns a variant binds as direct columns (abi.directPlan). A direct column is not copied: its buffer is bound beside the table, and the table holds its header alone. class ("effect" or "persistent") makes the table a run of the device arenas, held for owner (substrate.createBuffer), which the substrate names in place of name.
local t = abi.table({ { name = "a", kind = "f32", count = 4, values = { 1, 2, 3, 4 } } }, { name = "cols" })

modules/compute_abi/wordCopyOf

wordCopyOf(entry: any): (string?, string?)

The source and destination of a recorded dispatch that is a copyWords from word 0: the copy of the first words words of src into the same words of dst. Nil for any other entry.

Parameters

  • entry any (optional) — An entry of a recorded list.
local src, dst = abi.wordCopyOf(entry)

modules/connected_users/README

require("@builtin::modules.connected_users") -- connected_users

world.connectedUsers.* — server-replicated user registry. This module is user-scope and user-editable. The file you're reading is /zero/source/libs/@builtin/modules/connected_users.module/init.luau — anyone (any script, any agent, any user with VFS write) can rewrite it; hot-reload picks the change up and the engine uses the new version. There is no security boundary here. The read-only metatable below catches accidental writes loudly (API hygiene); it doesn't enforce anything. Trust boundaries that DO exist: - The Rust FFI (__connected_users.local_identity()) is registered into the VM state by the engine at boot and cannot be replaced from Luau. When it's called, it returns the real JWT sub. - The trusted VM (src/lua/trusted/**) is a separate Luau state, not on the user-writable VFS root, holds the auth.* namespace. User-scope Luau cannot reach it. - The server verifies the JWT on every RPC. Anything the server gates on flows through that check, not through whatever this module reports. What IS load-bearing: - __connected_users.local_identity() (Rust FFI) cannot be replaced. It always returns the real JWT sub of the engine's UserCredential, or nil for anonymous sessions. - The JWT bytes and session token NEVER cross any FFI exposed here; only the extracted identity string does. A malicious user-scope script can lie about identity but cannot exfiltrate the bearer token. - Authority on the server is established by the JWT the SDK presents on every request, not by anything this module reports. Multi-user replication plugs in on top of the same User metatable shape: the registry holds one record per person the published presence roster names (__presence.roster()), each carrying the engines they're connected through and the agents working for them (__presence.revision() gates the resync, so a re-send that changed nothing costs nothing). The local person always has a record — the roster view this engine reads folds it in even before any frame has arrived.

Usage: local connected_users = require("@builtin::modules.connected_users")

modules/contactShadows/README

require("@builtin::systems.contactShadows.contactShadows") -- contactShadows

Short screen-space shadow traces that supply the small-scale contact a shadow map cannot resolve — the grounding under a chair leg, a prop on a table, anything whose contact is finer than one shadow texel.

Usage: local contactShadows = require("@builtin::systems.contactShadows.contactShadows")

modules/contactShadows/active

active(): boolean

Whether the contact-shadow pass is running this frame.

if contactShadows.active() then ... end

modules/contactShadows/clear

clear()

Turn contact shadows off and release the pass. The other settings are kept, so a later set({ strength = ... }) brings back the same look.

contactShadows.clear()

modules/contactShadows/get

get(): ContactState

The contact-shadow settings currently in force.

local l = contactShadows.get().length

modules/contactShadows/lights

lights(): { ContactLight }

The point, spot and distant lights the last refresh packed for the trace pass, ranked by what each is worth at the main camera.

local n = #contactShadows.lights()

modules/contactShadows/lightsBuffer

lightsBuffer(): any?

The light buffer the trace pass reads — one row per light the scene's contact traces reach toward, packed by the last refresh.

local l = contactShadows.lightsBuffer()

modules/contactShadows/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = contactShadows.paramsBuffer()

modules/contactShadows/refresh

refresh()

Re-read the scene's lights and re-push what the trace pass reads: the sun's direction and weight, and the lights supplying contact of their own. The render feature calls this every frame, so the traces follow lights that move, brighten or are spawned while the scene runs.

contactShadows.refresh()

modules/contactShadows/set

set(opts: ContactOpts?): ContactState

Set the scene's contact shadows. Any omitted field keeps its current value. A strength of 0 turns them off and releases the pass.

Parameters

  • opts ContactOpts? (optional) — Contact-shadow settings — see ContactOpts.
contactShadows.set({ strength = 0.9, length = 0.35, steps = 16 })

modules/data_contract/README

require("@builtin::modules.data_contract") -- data_contract

The dataContract field-constraint validator: a constrained value must be a .data instance whose dataType contract chain includes constraint.contract. Registers itself with the generic field_constraints registry on load.

Usage: local data_contract = require("@builtin::modules.data_contract")

modules/debanding/README

require("@builtin::systems.debanding.debanding") -- debanding

Gradient debanding. Rebuilds a shallow ramp that eight-bit storage flattened into shelves, so a sky or a light falloff reads as continuous instead of as a stack of contour lines.

Usage: local debanding = require("@builtin::systems.debanding.debanding")

modules/debanding/active

active(): boolean

Whether the debanding pass is running this frame.

if debanding.active() then ... end

modules/debanding/disable

disable()

Turn debanding off and release the pass.

debanding.disable()

modules/debanding/enable

enable(strength: number?): State

Turn debanding on at a given strength.

Parameters

  • strength number? (optional) — Strength in [0, 1]. Omit to keep the current value.
debanding.enable()

modules/debanding/get

get(): State

The debanding settings currently in force.

local s = debanding.get().threshold

modules/debanding/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = debanding.paramsBuffer()

modules/debanding/set

set(opts: DebandOpts?): State

Set the debanding settings. Any omitted field keeps its current value.

Parameters

  • opts DebandOpts? (optional) — Debanding settings — see DebandOpts.
debanding.set({ threshold = 3, radius = 16 })

modules/debugger/README

require("@builtin::modules.api.engine.debugger") -- debugger (also available as global 'debugger')

Luau debugger — breakpoints, stepping, stack inspection, watches. Public Luau surface over the __debugger Internal FFI namespace.

Usage: local debugger = require("@builtin::modules.api.engine.debugger") Also available as global: debugger

modules/debugger/__diagnostics

__diagnostics(): DebuggerDiagnostics

Internal diagnostic counters for debugging the debugger itself: { installs, debugbreakHits }.

modules/debugger/addWatch

addWatch(expr: string): number

Register an expression to re-evaluate on every pause.

Parameters

  • expr string — Luau expression.

modules/debugger/continue_

continue_(): boolean

Resume the paused thread. The thread resumes on the next scheduler tick. A task's thread settles like any resumed task: a wait it reaches is armed, what it returns is its result, and an error it raises is the task's error, reported the way every task error is. A task cancelled while paused does not run on. A coroutine a task resumed itself runs on with nothing waiting for it, and an error it raises is logged.

modules/debugger/disableAll

disableAll()

Disable every registered breakpoint. Records persist; bytecode BREAK ops are cleared.

modules/debugger/disconnect

disconnect(handle: number): boolean

Disconnect an onBreak or onResume callback.

Parameters

  • handle number — Handle returned by onBreak/onResume.

modules/debugger/enableAll

enableAll()

Enable every registered breakpoint and re-install them in the VM bytecode.

modules/debugger/evaluate

evaluate(expr: string, frame: number?): (string?, string?)

Evaluate an expression against the paused frame's environment. Returns (value, error).

Parameters

  • expr string — Luau expression.
  • frame number? (optional) — 1-based frame index (default 1).

modules/debugger/getLocals

getLocals(frame: number?): { [string]: string }

Locals captured at the active pause for the given frame index (1 = top). Values are stringified for safe display.

Parameters

  • frame number? (optional) — 1-based frame index (default 1).

modules/debugger/getPauseInfo

getPauseInfo(): PauseInfo?

Info about the active pause, or nil if nothing is paused.

modules/debugger/getStack

getStack(): { Frame }

Captured stack from the active pause, top frame first. Empty when nothing is paused.

modules/debugger/getUpvalues

getUpvalues(frame: number?): { [string]: string }

Upvalues captured at the active pause for the given frame.

Parameters

  • frame number? (optional) — 1-based frame index.

modules/debugger/getWatchValue

getWatchValue(id: number): (string?, string?)

Re-evaluate the watch expression against the paused frame's environment and return (value, error).

Parameters

  • id number — Watch id.

modules/debugger/getWatches

getWatches(): { Watch }

Snapshot of all watches with their last evaluated value and error, sorted by id.

modules/debugger/isPauseOnError

isPauseOnError(): boolean

Current pause-on-error toggle state for this VM.

modules/debugger/isPaused

isPaused(): boolean

Whether the debugger currently has a paused thread.

modules/debugger/listBreakpoints

listBreakpoints(): { Breakpoint }

Snapshot of every registered breakpoint, sorted by id ascending. Each entry reports whether it is installed: chunkNames lists the loaded chunks carrying it, and pendingReason says why an empty list is empty.

modules/debugger/onBreak

onBreak(fn: (PauseInfo) -> ()): number

Register a callback invoked on every pause with { path, line, reason }. Returns a handle usable with debugger.disconnect.

Parameters

  • fn (PauseInfo) -> () — Callback.

modules/debugger/onResume

onResume(fn: () -> ()): number

Register a callback invoked when the paused thread is resumed.

Parameters

  • fn () -> () — Callback.

modules/debugger/removeBreakpoint

removeBreakpoint(id: number): boolean

Remove the breakpoint with the given id.

Parameters

  • id number — Breakpoint id returned by setBreakpoint.

modules/debugger/removeWatch

removeWatch(id: number): boolean

Remove the watch with the given id.

Parameters

  • id number — Watch id.

modules/debugger/setBreakpoint

setBreakpoint(path: string, line: number, opts: BreakpointOpts?): Breakpoint

Set a breakpoint at line in the script path names — its VFS path, its require identity, or the chunk name it loaded under. An installed breakpoint carries resolvedLine and lists the loaded chunks holding it in chunkNames; one whose script is not loaded carries pendingReason, an empty chunkNames, and installs itself when that script loads.

Parameters

  • path string — VFS path, require identity, or chunk name.
  • line number — 1-based source line.
  • opts BreakpointOpts? (optional) — { condition?, logMessage?, hitCount?, notify?, enabled? }. notify makes it a TRACEPOINT: the hit fires onBreak handlers against the frame it stopped in — getStack, getLocals, getUpvalues and evaluate all answer for that frame — and then runs on, instead of holding the thread until something resumes it. That is how code observes a moment without stopping it; halting is for a person at a keyboard. A breakpoint on any line drops the enclosing function out of native code until its chunk reloads, so one left armed on a per-frame function costs more than it reports.
local bp = debugger.setBreakpoint("/zero/source/main.luau", 42)
print(bp.pendingReason or ("installed in " .. bp.chunkNames[1]))

modules/debugger/setPauseOnError

setPauseOnError(enabled: boolean)

When true, uncaught Luau errors fire the onBreak callback (observation only — the error still propagates).

Parameters

  • enabled boolean — Toggle state.

modules/debugger/stepInto

stepInto(): boolean

Run until the next line, descending into any function call.

modules/debugger/stepOut

stepOut(): boolean

Run until the current frame returns; pauses in the caller.

modules/debugger/stepOver

stepOver(): boolean

Run until the next line in the current frame. Calls inside the current line are skipped.

modules/debugger/toggleBreakpoint

toggleBreakpoint(path: string, line: number): Breakpoint?

Toggle a breakpoint at the given line: removes if present, adds otherwise.

Parameters

  • path string — VFS path, require identity, or chunk name.
  • line number — 1-based line.

modules/denoiser/README

require("@builtin::systems.denoising.denoiser") -- denoiser

Edge-aware denoising a render feature routes a noisy signal through. A feature that shades from a few stochastic samples per pixel writes a result carrying that sampling noise, and the only way to quieten it in the feature itself is to cast more rays. Filtering the result instead buys the same quality far more cheaply, and the filter is the same one for every such feature, so it lives here rather than being rewritten per effect. A filter smooths in two directions. Across the frame it is an à-trous wavelet: successive passes with a doubling tap stride, so a handful of 5x5 passes reach the radius a single wide blur would need hundreds of taps for. Each tap is weighted by how much the surface under it resembles the surface under the centre pixel — its world position from @scene.depth and its normal from @scene.normal — so the smoothing follows geometry and stops at depth and normal discontinuities instead of bleeding an object's occlusion onto the wall behind it. Across time — with temporal set — it first carries the previous frame's estimate forward through @scene.motion, so the standing average holds far more samples than any one frame casts and the feature feeding it can trace fewer rays for the same quietness. A reprojected estimate is admitted only where the surface it was written on is the surface being shaded now, and never for longer than historyFrames. Each filter owns its targets and its compiled passes, keyed by the name it was created with, so two features denoising in the same frame do not interfere.

Usage: local denoiser = require("@builtin::systems.denoising.denoiser")

modules/denoiser/create

create(name: string, opts: DenoiseOpts?): Filter

Create a filter that owns its own targets and passes. name keys those resources, so two features denoising in the same frame each pass their own name and never share state.

Parameters

  • name string — Identifies this filter's resources. Unique per feature.
  • opts DenoiseOpts? (optional) — Filtering settings — see DenoiseOpts.
local d = denoiser.create("rt_ao", { iterations = 4, worldSigma = 0.5, temporal = true })

modules/denoiser/destroy

destroy(self: Filter)

Release the filter's targets. The filter rebuilds on its next run.

Parameters

  • self Filter
filter:destroy()

modules/denoiser/passes

passes(self: Filter): number

Consecutive order slots run occupies, counted from the order it is given, so a caller knows where its own next pass can sit.

Parameters

  • self Filter
local apply = 51 + filter:passes()

modules/denoiser/reset

reset(self: Filter)

Drop what the accumulation holds, so the next frame starts from its own samples. Call it at a camera cut, where nothing on screen was on the last frame and no reprojection could find it.

Parameters

  • self Filter
filter:reset()

modules/denoiser/run

run(self: Filter, ctx: any, source: string, opts: { phase: string?, order: number? }?): string

Enqueue this filter's passes over source, and answer the guid holding the filtered result. Call it from a render feature's render, passing the same ctx; the result is ready for the phase and order given.

Parameters

  • self Filter
  • ctx any (optional) — The render context the calling feature received.
  • source string — Guid of the texture holding the noisy signal.
  • opts { phase: string?, order: number? }? (optional) — { phase, order } — where the filter's passes run. order is the first of passes() consecutive slots.
local clean = filter:run(ctx, noisy.guid, { phase = "afterLighting", order = 55 })

modules/denoiser/stats

stats(self: Filter): DenoiseStats

What the filter is doing right now — whether it accumulates, whether it holds an accumulation, how many times that has been dropped, and the size its targets are built for.

Parameters

  • self Filter
if not filter:stats().warmed then print("first frame of the accumulation") end

modules/deprecatedParticles/README

require("@builtin::deprecated.deprecatedParticles.deprecatedParticles") -- deprecatedParticles

Particle emitters: create one, list the ones that are live — every emitter, or one creator's own — and read what each is simulating and drawing right now: whose it is, how many particles are alive, what it costs on the GPU, and when it is producing nothing, which reason explains it.

Usage: local deprecatedParticles = require("@builtin::deprecated.deprecatedParticles.deprecatedParticles")

modules/deprecatedParticles/create

create(spec: table?): any

Create a GPU particle system from a spec, allocating its buffers and registering it with the auto-update driver. The handle it returns carries :emit, :update, the setters, :observe, and :getCreator.

Parameters

  • spec table? (optional) — { maxCount, rate, lifetime, speed, shape, ... } — every field optional, each falling back to the emitter's default. owner and name say whose the emitter is: owner is the key list(owner) matches, so a creator reaches exactly its own emitters after it has lost their handles, and name says which of them this one is. An emitter that states neither is still attributed to the module and line it was created from. optional, each falling back to the emitter's default. A field that names one of a closed set takes a name from it and raises with the whole set otherwise; a key the spec does not define is reported on its own, naming the key that writes what it was written for. man particles.create lists every key the spec defines.
local fire = particles.create({ maxCount = 2000, rate = 100 })
local star = particles.create({ owner = "starfield", name = "shell" })

modules/deprecatedParticles/list

list(filter: (string | ParticleCreatorFilter)?): { any }

Every particle system this VM has created and not destroyed, in creation order — or, given a filter, the ones whose creator matches it. Answered from the emitter registry, so finding an emitter costs nothing per entity in the scene.

Called with nothing it answers with every emitter in the VM, which is what makes it the way to reach one whose creator has lost its handle, and :getCreator() on an entry says whose that one is. A filter narrows it to one creator's own, so a module clears what a previous load of it left behind and leaves every other emitter in the world standing.

Parameters

  • filter (string | ParticleCreatorFilter)? (optional) — Optional. A string matches the owner key a creator stated; a table matches every one of owner, name and source that it names.
for _, sys in ipairs(particles.list()) do print(sys:getActiveCount()) end
for _, sys in ipairs(particles.list("starfield")) do sys:destroy() end
local mine = particles.list({ source = debug.info(1, "s") })

modules/deprecatedParticles/observe

observe(system: any?): { [string]: any }

What the engine is simulating and drawing for particles right now. With no argument, every live emitter plus the totals they sum to; with an emitter, that one's reading. An engine holding no emitters answers count = 0 with an empty list, which reads differently from an engine whose emitters are all silent (count > 0, silent = count).

Parameters

  • system any? (optional) — Optional particle system handle to read on its own.
local o = particles.observe(); print(o.count, o.alive, o.silent)
local r = particles.observe(fire); print(r.alive, r.bytes.total)

modules/deprecatedParticles/silenceReasons

silenceReasons(): { { reason: string, means: string } }

The closed set of reasons an emitter can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every observe().reason is one of these.

for _, r in ipairs(particles.silenceReasons()) do print(r.reason, r.means) end

modules/deprecatedParticles/whySilent

whySilent(system: any): (string?, string?)

Why one emitter is producing nothing, from the closed set silenceReasons() enumerates — or nil when it is producing. The second return is the detail line naming what the reason is about.

Parameters

  • system any (optional) — The particle system handle to ask about.
local why, detail = particles.whySilent(fire)

modules/deprecation/README

require("@builtin::modules.deprecation") -- deprecation

Whether an asset is deprecated, why, and what replaces it. An asset is deprecated when its own .metadata carries a deprecated marker, when any container holding it at any depth does (a package, a toolbox, a folder asset), or when its asset type does (every instance of that type). The marker is "deprecated": { "reason": "<one line why>", "replacedBy": "<identity of what to use instead>" }. of is the one function that answers it; search surfaces ask indexed, and resolving an asset asks warnOnce, which warns on the next frame.

Usage: local deprecation = require("@builtin::modules.deprecation")

modules/deprecation/describe

describe(subject: string, found: Deprecation): string

The sentence that names a deprecated asset, its reason and what replaces it.

Parameters

  • subject string — What the deprecation is of: an identity or a name.
  • found Deprecation — The deprecation of answered.
log.warn(deprecation.describe(identity, d))

modules/deprecation/forget

forget()

Forget which assets have warned, which were found clean and what each folder's .metadata was read to state, so the next use of each is read again.

deprecation.forget()

modules/deprecation/indexed

indexed(ref: any, memo: { [string]: any }?): boolean

Whether a search may index and offer an asset: true unless the asset is deprecated. Every search surface asks this before it reads an asset, so a deprecated asset is never read, ranked or listed.

Parameters

  • ref any (optional) — An asset handle, identity, guid or path.
  • memo { [string]: any }? (optional) — The table shared across one pass, as of takes it. Optional.
if deprecation.indexed(ref, memo) then print(deprecation.of(ref)) end

modules/deprecation/of

of(ref: any, memo: { [string]: any }?, site: string?): Deprecation?

The deprecation an asset stands under, from its own marker, a container's or its asset type's; nil when it is not deprecated. The nearest marker wins: the asset's own, then each enclosing folder outward, then its asset type's.

Parameters

  • ref any (optional) — An asset handle, identity, guid or path.
  • memo { [string]: any }? (optional) — A table shared across the calls of one pass, so a folder's .metadata is read once for every asset beneath it. Optional.
  • site string? (optional) — The source a name given as ref was written in (__asset.callerSite(), read where the call came in): a ~.tail name expands against the root enclosing it and a name its reference table recorded reaches the asset recorded there. Optional.
local d = deprecation.of("@builtin::deprecated.deprecatedParticles.DeprecatedParticleEmitter")

modules/deprecation/settled

settled(): boolean

Whether every use warnOnce queued has been asked about, so each deprecated one among them has warned.

while not deprecation.settled() do task.wait() end

modules/deprecation/warnOnce

warnOnce(ref: any): boolean

Warn that an asset is deprecated, once per session per asset, naming the asset, the reason and the replacement. The use is asked about on the next frame, so the call that named the asset reads nothing beyond the index: a .metadata is read then, once for as long as it stands as it is. An asset found clean is not asked again for a few seconds, so a hot path resolving it pays one lookup.

Parameters

  • ref any (optional) — An asset handle.
deprecation.warnOnce(handle)

modules/depthOfField/README

require("@builtin::systems.depthOfField.depthOfField") -- depthOfField

Depth of field from a lens — focus distance, focal length and aperture, so the falloff behaves the way a camera's does.

Usage: local depthOfField = require("@builtin::systems.depthOfField.depthOfField")

modules/depthOfField/active

active(): boolean

Whether the defocus passes are currently running.

if depthOfField.active() then print("shallow") end

modules/depthOfField/clear

clear()

Turn defocus off and release the passes. The lens is kept, so a later set({ fStop = ... }) brings back the same look.

depthOfField.clear()

modules/depthOfField/get

get(): DepthOfFieldState

The lens currently in force.

local f = depthOfField.get().focusDistance

modules/depthOfField/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = depthOfField.paramsBuffer()

modules/depthOfField/set

set(opts: DepthOfFieldOpts?): DepthOfFieldState

Set the lens. Any omitted field keeps its current value, so a call can rack focus without restating the rest. An fStop of 0 turns defocus off and releases the passes.

Parameters

  • opts DepthOfFieldOpts? (optional) — Lens settings — see DepthOfFieldOpts.
depthOfField.set({ focusDistance = 8, fStop = 1.8, focalLength = 85 })

modules/discovery/README

require("@builtin::modules.discovery") -- discovery

Content search in this engine: keyword mode over the files each asset type's type.yaml names, semantic mode through ZeroMind over the scope this world can reach. One question, one answer shape, two modes.

search { q, assetType, match, mode, limit } is the question; match applies to keyword mode only. q is a phrase or a list of them and the answer carries one results entry per query, in the order they were asked. searchGuides asks it the way a guides query does: over the documentation types DOC_TYPES names when it names no assetType, and an answer that found nothing names the surface that searches every other kind. Keyword mode reads the files each asset type declares under indexing.data in its type.yaml, for the slot match selects: identity is the identity role, capability is the capability, interface and implementation roles together, any is either and all is both. The query's words are matched against that text, each word counting once for the asset that carries it, and an asset ranks by what the words it carries as words of their own weigh, a word weighing more the fewer of the assets read carry it, then by how many it carries side by side in the query's order, then by how many it carries anywhere in its text. It runs on the files this engine holds, so unpushed content and a private world's own content are in it, and from code that can yield it reads them across frames, giving the frame back every SLICE_MS. Semantic mode is ZeroMind's ranking over what ZeroMind may embed. Say what you need in q, in plain words. The search decides which asset types the words are about and orders what it keeps by how relevant a judge read each hit to be. match does not apply to it, and naming an assetType replaces that decision: only the named types are searched, so a type guessed wrong answers nothing. Name one only when the task fixes the type. The answer names the worlds it covered. Every answer states the scope it ran from — from, the bound world, and the searched worlds — and every hit carries the local form of the asset this engine holds: its path, the identity a call names a tool by, the identity guides reads a guide or an assetType's README by.

Usage: local discovery = require("@builtin::modules.discovery")

modules/discovery/answer

answer(fields: {

Assemble an answer: the mode and match that ran, the scope it ran from, the results by query position, and the status they hold. A refused request ran no mode and names none. An answered semantic search from a private world is promoted to private_world and carries the note saying what was searched; every other status stands as given. An answered semantic search whose narrowed is false ran naming no kind, and carries the note saying the ranking covered every kind ZeroMind holds. An answered search whose results hold no hit at all carries emptyNote, the sentence its surface gives for having found nothing. The notes stand in one note, the private-world sentence first: status names that promotion, so the sentence it reads for opens what the answer carries. A keyword answer states scanned, the most assets one of its queries read, and cut, whether any query's walk stopped at KEYWORD_SCAN_MAX. A library answer ranked over the published library and carries the note saying so, and is not promoted to private_world, since it lists this world's own matches itself; kinds names the kinds the search read, and the note then says it read only those; librarySkipped is the reason a library search could not ask ZeroMind, and adds the note saying the hits are this world's own.

local out = discovery.answer({ mode = "keyword", match = "any", status = "answered", results = results, scope = scope })

modules/discovery/awaitJob

awaitJob(poll: () -> any, budgetMs: number): (any, number)

Poll poll until it reports a terminal status, giving up after budgetMs of wall clock. The budget is measured against the clock, so it means the same span whatever the frame rate is.

Parameters

  • poll () -> any — Reads the job's current state.
  • budgetMs number — How long to wait, in milliseconds.
local settled, ms = discovery.awaitJob(poll, 15000)

modules/discovery/boundGuid

boundGuid(): string?

The guid of the world this engine is bound to, or nil on an engine bound to none. Every read of the binding in this module goes through it.

local guid = discovery.boundGuid()

modules/discovery/boundWorld

boundWorld(): BoundWorld?

The world this engine is bound to, with whether it is private — which is what decides whether its own content is anything ZeroMind may embed. Reads the visibility through visibility, and holds a world whose visibility cannot be read as private. Nil when no world is bound.

local here = discovery.boundWorld()

modules/discovery/defaultMode

defaultMode(): string

The mode a call runs when it names none: semantic on a live engine, keyword on an offline boot.

local mode = discovery.defaultMode()

modules/discovery/fetch

fetch(

One semantic fetch: the trusted job for one query over worlds, read into { ok, hits, total?, types?, containers?, elapsedMs } or { ok = false, error, reason, elapsedMs }. semantic asks every query through it, so the answer ZeroMind sent is read in one place.

local answer = discovery.fetch("outline", {}, nil, 10, 15000, false)

modules/discovery/forget

forget()

Drop every held text. The next query reads its files again.

discovery.forget()

modules/discovery/guideTypes

guideTypes(): { string }

The types a guides query searches: the ones types() lists that DOC_TYPES names — the documentation this engine holds. In name order.

local names = discovery.guideTypes()

modules/discovery/identityOf

identityOf(hit: Hit): string

The name the surface acting on a tool-family hit takes: a tool's <toolbox>.<tool> identity, which tools.use calls, and a toolbox's name, which a search_tools { toolbox } drill-in lists. A tool the registry does not hold is named by the toolbox its folder sits in and the name of its own .tool folder; a hit this engine does not hold, by its name.

Parameters

  • hit Hit — One hit of a search answer.
local identity = discovery.identityOf(hits[1])

modules/discovery/indexing

indexing(typeName: string): { IndexingEntry }?

The indexing recipe a type declares in its type.yaml, as { { role, extractor, files, order } } in declared order, or nil when the type declares none, is deprecated, or does not exist. A deprecated type is not indexed, so none of its instances is read. order is the entry's place in indexing.data, which is what ranks two hits that matched the same number of words.

Parameters

  • typeName string — The asset type's name, in any casing ("guide").
local recipe = discovery.indexing("guide")

modules/discovery/installedOrigin

installedOrigin(row: any): SearchedWorld?

One searched entry for an installed row's origin world, or nil when the row names none. why is library for a package, import otherwise. title is read from the row's own record of its origin world's title; a row whose install carried none leaves it nil, so that origin is named by guid alone.

Parameters

  • row any (optional) — One world.installedAssets() row.
local entry = discovery.installedOrigin({ origin_world_guid = "...", kind = "material" })

modules/discovery/installedPaths

installedPaths(): { [string]: boolean }

The paths of everything this world carries from ZeroMind, as installedRows lists them: every library, imported asset and dependency a pull wrote. Empty on an engine bound to no world.

local pulled = discovery.installedPaths()

modules/discovery/installedRows

installedRows(): { any }

What this world carries from ZeroMind, as world.installedAssets() lists it: one row per library, imported asset and dependency a pull wrote. Every read of the installed set in this module goes through it. Empty on an engine bound to no world, or when the list cannot be read.

local rows = discovery.installedRows()

modules/discovery/keyword

keyword(

Keyword mode over the assets this engine holds: the assets of the named types, read through each type's recipe for the slot match selects, ranked by what the query's words the asset carried as words of their own weigh together (hand in "hand it over", spawn in "spawns", and not off in "offline"). A word weighs more the fewer of the assets the walk read carry it, so in "roster who is here" the asset saying "roster" ranks above the many saying "who is here". Then by the most of them it carried side by side in the query's order, so a tool saying "hand off" ranks above one saying "hand" and "off" apart, then by how many it carried anywhere in its text, then by the order the type declares the role that matched, then by name. The walk takes this world's own assets first and the library's after them, each the types' assets in turn (the first of each type, then the second of each), and reads at most KEYWORD_SCAN_MAX of them. Called from code that can yield, it gives the frame back every SLICE_MS while it reads.

local hits, corpus, walk = discovery.keyword("outline", { assetType = "guide", match = "any", limit = 5 })

modules/discovery/keywordScope

keywordScope(corpus: Corpus): Scope

The scope a keyword answer names: the bound world, and the worlds whose content the query actually read. It asks ZeroMind nothing.

Parameters

  • corpus Corpus — Where the assets a query read came from.
local scope = discovery.keywordScope({ builtin = true, world = false })

modules/discovery/kindParam

kindParam(assetType: string?): string?

The kind a semantic request names for a comma list of type names: each registered type travels as the guid of its .assetType, the identity ZeroMind files it under wherever it was declared, builtin types included. A name no registered type answers to travels as written in lowercase, so ZeroMind names the one it does not know.

Parameters

  • assetType string? (optional) — A comma list of type names, or nil.
local kind = discovery.kindParam("guide,module")

modules/discovery/libraryScope

libraryScope(): Scope

The scope a library search names: no world at all. Naming no world is what widens the ranking from the content this engine can see to everything published, because the request carries a world filter only when it has worlds to name. The bound world still travels, so an answer can say where the search was made from.

local scope = discovery.libraryScope()

modules/discovery/newSlice

newSlice(): () -> ()

A function to call between the pieces of a long walk: it yields one frame once SLICE_MS has run since the slice began, and does nothing for a caller that cannot yield. Each walk makes its own.

Returns () — The function to call between pieces.

local between = discovery.newSlice(); for _, ref in ipairs(asset.list("tool")) do between() end

modules/discovery/onlyKinds

onlyKinds<T>(hits: { T }, kinds: { [string]: boolean }, limit: number): { T }

The hits of hits whose kind kinds holds, in their order, at most limit of them.

Parameters

  • hits { T } — Hits, each carrying kind.
  • kinds { [string]: boolean } — Lowercase kind names to keep.
  • limit number — How many to keep.
local kept = discovery.onlyKinds(hits, discovery.DOC_TYPES, 10)

modules/discovery/resolve

resolve(guid: string): LocalForm?

The local form of an asset this world holds, by guid: its path, the identity a call names a tool by, the name a drill-in lists a toolbox by (for a tool, its own toolbox's), whether the registry holds what a tool or toolbox hit names, the identity guides reads a guide or an assetType's README by, and the VFS path of the asset's README when it holds one. Nil when this world holds no asset under that guid.

Parameters

  • guid string — The published guid.
local here = discovery.resolve(hit.guid)

modules/discovery/resting

resting(): (number?, string?)

How long the semantic leg is still resting, in seconds, and the failure it is resting after. Nil when it is not resting; the window closes as soon as it has run out.

local seconds, why = discovery.resting()

modules/discovery/scope

scope(): Scope

The worlds a semantic search covers from this engine, and the bound world it runs from. The builtin library is always in reach; this world is in when it is public; the origin world of every installed library and imported asset is in through the public world that published it.

local scope = discovery.scope()
search(req: { [string]: any }): { [string]: any }

Search this engine's content. req is { q, assetType?, match?, mode?, from?, limit?, budgetMs?, within? }: q is a phrase or a list of them, assetType a comma list of type names as types() lists them (a name types() does not list is refused, and the refusal names every type it lists; in semantic mode leave it out unless the task fixes the type, since naming one replaces the search's own choice of types), match (keyword mode only) one of identity / capability / any / all, mode one of keyword / semantic, within a folder path or a list of them that the walk of this engine's own files is held to (the library ranking is not), and from where results may come from: all is everything published (what library = true asks), bound the current world and its installed libraries, with no published-library leg, in either mode. Naming no from keeps a library the request states; stating neither searches everything published in semantic mode and this engine's own files in keyword mode. A name other than those two is refused, and from = "all" with mode = "keyword" is refused, since keyword mode reads this engine's own files. Every answer carries mode, from, status, the bound world, the searched worlds, and one results entry per query; a keyword answer also carries match.

Parameters

  • req { [string]: any } — The request table. excerpts asks each hit to carry the passage it was ranked on, which costs the fuller hit payload; documentation searches ask for them already. library ranks over the whole published library instead of the content this engine can see, for a caller looking for something to install rather than something already here, and lists this world's own matches by their words after the library's hits, each marked from = "world"; when ZeroMind cannot be asked, those own matches still answer and the note says why the library was not searched. It is off by default, and mode: keyword refuses it, since keyword mode reads this engine's own files.
local out = discovery.search({ q = "outline", assetType = "guide", mode = "keyword" })
local out = discovery.search({ q = "outline", excerpts = true })

modules/discovery/searchGuides

searchGuides(req: { [string]: any }): { [string]: any }

Search as a guides query asks: search over the documentation types DOC_TYPES names, or over exactly the ones of them req.assetType names. Naming no type, keyword mode walks the types guideTypes() lists, and semantic mode asks ZeroMind over them (semanticKinds) and keeps only those kinds of its hits. An answered search that found nothing carries NO_DOCS_NOTE, saying which kinds it matched against. An assetType naming a type TOOL_TYPES holds is refused with a sentence that points to search_tools, and one naming any other type types() lists with a sentence that points to discovery.search. The answer is search's, in rank order.

Parameters

  • req { [string]: any } — { q, assetType?, match?, mode?, from?, limit?, budgetMs?, excerpts? }, as search takes it. A semantic search here carries excerpt on each hit unless excerpts = false says not to: a reader asking the documentation a question is answered by the passage that answered it, not only by what the guide is.
local out = discovery.searchGuides({ q = "outline", mode = "keyword" })

modules/discovery/searchTools

searchTools(req: { [string]: any }): { [string]: any }

Search as a search_tools query asks: search over the types TOOL_TYPES names (tools, toolboxes and workflows), or over exactly the ones of them req.assetType names. Naming no type, keyword mode walks the types toolTypes() lists, and semantic mode asks ZeroMind over them (semanticKinds) and keeps only those kinds of its hits. An assetType naming any other type is refused with a sentence that points to the surface that searches it. The answer is search's, in rank order.

Parameters

  • req { [string]: any } — { q, assetType?, match?, mode?, from?, limit?, budgetMs?, library?, within? }, as search takes it.
local out = discovery.searchTools({ q = "orbit the camera", mode = "keyword" })

modules/discovery/semantic

semantic(

Semantic mode: ZeroMind's ranking over scope for the types opts.assetType names (none: the search decides which types the words are about), spelled for the request through kindParam, each hit carrying the local form of the asset when this engine holds it. opts.only names the kinds the answer keeps: the request then names opts.kinds, asks for SEMANTIC_FETCH_MAX hits, and keeps the first limit of those kinds in ZeroMind's order, so a ranking that could not be narrowed on the request is narrowed here. A failure that cost more than EXPENSIVE_FAILURE_MS reached ZeroMind, and opens the rest window resting() reports.

local results, status, err = discovery.semantic({ "outline" }, opts, discovery.scope())

modules/discovery/semanticKinds

semanticKinds(names: { string }): string?

The kind a semantic request names to cover names: the guid of each one's .assetType, as kindParam spells it. A name no registered type answers to is left out. Nil when none of them resolves, which is the request that names no kind and ranks over all of them.

Parameters

  • names { string } — Type names, as types() spells them.
local kind = discovery.semanticKinds(discovery.guideTypes())

modules/discovery/shape

shape(answer: { [string]: any }, form: string, query: string?): { [string]: any }

A search answer in the shape a surface hands it on, built where the search ran so every way of collecting it reads the same value. "answer" is the answer itself with each hit's local form named local. "guides" is { kind = "search", query, match_count, matches }, a row per hit: a guide's (assetType guide) or an asset type's (assetType assettype) names the guides { path } identity as path and carries summary, and the excerpt it was ranked on where the search asked for one; any other type's names its assetType, a one-line purpose and the VFS path of its README as vfs_path. Every row of either list form carries guid, installed (whether this world holds it), skill on an agent skill and workflow on a workflow this world holds, p when the search core judged it, and from = "world" on a match found in this world's own content after a library ranking. Every row of every form whose hit the search core ranked carries container_key and system_key, keys into the answer's containers: the container the hit ships in and the highest one above it (a tool's toolbox and the package it ships in), or the world it belongs to; each container entry names the one above it by parent_key. "answer" keeps containers on each query's result; both list forms carry it once, beside the rows. "tools" is { type = "functions", count, functions }, a row per hit naming its assetType (tool, toolbox or workflow), its identity (discovery.identityOf: the tools.use identity of a tool, the drill-in name of a toolbox), its toolbox, a one-line purpose and whether the registry holds what the identity names; a tool row carries tool, the tools.use identity while the registry holds it and empty while it does not. Both list forms carry the answer's mode, match, from, status, world, searched, note, error, retry_in_s, scanned and cut beside the rows, and total, how many the search matched of which the rows are the best, when it answered one query and counted them.

Parameters

  • answer { [string]: any } — What search answered.
  • form string — "answer", "guides" or "tools".
  • query string? (optional) — The query a "guides" answer echoes.
return discovery.shape(discovery.searchGuides({ q = "outline" }), "guides", "outline")

modules/discovery/stats

stats(): { reads: number, bytes: number, visibilityReads: number }

What this module has read and what it is holding. reads is the number of vfs.read calls it has made — a query that reads content it already holds against an unchanged checksum leaves it where it stood. bytes is the asset text held right now, which stays under TEXT_BUDGET_BYTES. visibilityReads is the number of times it has asked ZeroMind for a world's visibility.

local before = discovery.stats().reads

modules/discovery/toolTypes

toolTypes(): { string }

The types a search_tools query searches: the ones types() lists that TOOL_TYPES names, the things an agent runs. In name order.

local names = discovery.toolTypes()

modules/discovery/toolboxKey

toolboxKey(path: string): string?

The name the tool registry carries a toolbox under, for a .tool at path. A toolbox under a library root other than the builtin one is registered scope-qualified (@lib::ns); every other toolbox is registered under its bare name.

Parameters

  • path string — The tool's VFS path.
discovery.toolboxKey("/zero/source/libs/@builtin/tools/zm.toolbox/status.tool")

modules/discovery/types

types(): { string }

Every asset type whose type.yaml declares an indexing recipe — the types a search can read, in name order.

for _, name in ipairs(discovery.types()) do print(name) end

modules/discovery/visibility

visibility(guid: string): string?

A world's visibility as ZeroMind reports it, read through the trusted world read and waiting up to VISIBILITY_BUDGET_MS. An answer is held for the guid, so each world is asked once; nil when the engine cannot read it.

Parameters

  • guid string — The world's guid.
local visibility = discovery.visibility(world.guid())

modules/discovery/withWorldMatches

withWorldMatches(results: { QueryResult }, own: { { Hit } }): { QueryResult }

A library ranking with this world's own matches listed after it, query by query: own[i] are the hits keyword mode found for results[i] in this world's own content. A hit the library already returned stays where the library put it; every other one is appended in the order keyword mode ranked it, marked from = "world". The library's hits keep the order the search core gave them, and no score is changed or combined.

Parameters

  • results { QueryResult } — The library ranking, by query position.
  • own { { Hit } } — This world's own matches, by the same positions.
local merged = discovery.withWorldMatches(results, { ownHits })

modules/discovery/worldHere

worldHere(): BoundWorld?

The world this engine is bound to, as the engine itself knows it: its guid and title, and private when a semantic search has already read the world's visibility. Nothing here waits on ZeroMind, which is what lets a keyword answer and a refusal name the world at once. Nil when no world is bound.

local here = discovery.worldHere()

modules/discovery/worldOwn

worldOwn(path: string, installed: { [string]: boolean }): boolean

Whether an asset at path is this world's own content: not the builtin library, not an installed library root (libs/@<name>/), not a dependency a pull brought (/source/deps/), and not at or inside a path installed holds. Those are the content a library ranking already covers; what is left is what this world's authors made here.

Parameters

  • path string — The asset's VFS path.
  • installed { [string]: boolean } — What installedPaths() answered.
local own = discovery.worldOwn("/source/tools/mine.toolbox/go.tool", discovery.installedPaths())

modules/diverged

modules.diverged() -> { { module: string, reason: string } }

The modules whose running code differs from their source: each one's hot reload failed (its new source did not compile, or its body raised while it ran), so the code it ran before that edit is still running. Each entry names the module by the key it is required under and gives the reason its reload failed. A module leaves the list once it loads cleanly, and each failure also posts an error notice that stands until then.

Returns { { module: string, reason: string } } — One entry per module whose running code differs from its source; empty when every module runs its source

modules/editor.asset_instance_inspector/README

require("@builtin::modules.editor.asset_instance_inspector") -- editor.asset_instance_inspector

The entity inspector's view of an Asset component — an instance of an asset placed in the scene. It says which asset the instance is linked to and how it stands against it: every part the asset composed, whether it is as the asset has it or was edited, removed or added in the scene, and what the edit changed. Each edited part can be reverted to the asset, selected, or the whole instance reverted or unpacked into plain scene entities.

Usage: local editor.asset_instance_inspector = require("@builtin::modules.editor.asset_instance_inspector")

modules/editor.asset_instance_inspector/build

build(ctx: any): { any }

The Asset component's card body on an entity (edui.inspector): the summary badges, the source field as the component's typed, undoable setting, the Overrides and Linked parts lists, and the revert / unpack actions, built from M.sections with ctx.widgets.

Parameters

  • ctx any (optional) — The inspector Context of the Asset component on its entity.

modules/editor.asset_instance_inspector/sections

sections(entityId: string, proxy: any): any

The view's sections for one Asset component: a summary of how the instance stands against its asset, its source field, the parts that differ from the asset and the parts that match it, and the actions that revert or unpack it.

Parameters

  • entityId string — The entity the component is on.
  • proxy any (optional) — The live Asset component.

modules/edui.app/README

require("@builtin::editor.edui.app") -- edui.app

The edui reactive core: an App owns one ui.* screen, maps author closures to ui.* string callback ids, dispatches the broadcast that ui.registerCallbackEnv delivers, and rebuilds the widget tree on state change. This is the layer that lets editor chrome be written with closures (onClick = function() ... end) instead of hand-managed string ids — the footgun the deprecated zui grew three shapes for.

Usage: local edui.app = require("@builtin::editor.edui.app")

modules/edui.app/adoptHandlers

adoptHandlers(self: any, bucket: { [string]: (any) -> () })

Re-register a bucket of handlers collected by collectHandlers into the current build, keeping a cached subtree's callbacks live.

Parameters

  • self any (optional)
  • bucket { [string]: (any) -> () } — The handler bucket to adopt.
app:adoptHandlers(cached.bucket)

modules/edui.app/cb

cb(self: any, closure: (any) -> (), key: (string | number)?): string

Allocate (or reuse) a callback id bound to closure for this build. Pass a stable key so the id is identical across rebuilds — required for anything the renderer tracks by id (focus, drag, ui.widgetState). Without a key the id is a per-build sequence number (fine for a fire-and-forget button).

Parameters

  • self any (optional)
  • closure (any) -> () — The handler (data) -> () the id dispatches to.
  • key (string | number)? (optional) — Optional stable key (unique within this screen).
props = { onClick = app:cb(function() doThing() end, "save") }

modules/edui.app/cbRaw

cbRaw(self: any, id: string, closure: (any) -> ()): string

Register closure under the EXACT id id, without the screen-name prefix cb adds. For wiring engine-emitted interaction ids that a widget publishes itself (e.g. a dockArea's <panelId>-close), which arrive unprefixed. Re-issue it each build, like cb.

Parameters

  • self any (optional)
  • id string — The exact callback id the engine will deliver.
  • closure (any) -> () — The handler (data) -> ().
app:cbRaw(panelId .. "-close", function() closePanel(panelId) end)

modules/edui.app/collectHandlers

collectHandlers(self: any, fn: () -> any): (any, { [string]: (any) -> () })

Collect every callback registered while fn runs into a named bucket, returned alongside fn's result. A host that CACHES the subtree fn built re-adopts the bucket on later builds (adoptHandlers), so the cached tree's callback ids stay live across rebuilds that skipped it.

Parameters

  • self any (optional)
  • fn () -> any — The builder to run.

Returns () }) — fn's result, and the bucket of handlers it registered.

local tree, bucket = app:collectHandlers(function() return panel.build() end)

modules/edui.app/current

current(): any

The app whose builder is currently running, or nil outside a build. The edui.* primitive builders read this to register their closures, so authors don't thread the app through every call.

local app = edui.app.current()

modules/edui.app/dispatch

dispatch(self: any, id: string, data: any): boolean

Route a callback id to its closure and schedule a rebuild. This is what the screen's onCallback broadcast calls. Returns true when the id was handled by this app (an id from another surface returns false so a host can keep routing).

Parameters

  • self any (optional)
  • id string — The callback id the broadcast delivered.
  • data any (optional) — The engine event envelope { value, eventType, widgetId, button, mouseX, mouseY }. The handler receives its value: a scalar for onChange (checkbox bool, input string, select value), a { dx, dy, shift, ctrl, alt } table for a canvas onDrag/onScroll, nil for a bare onClick.
function onCallback(id, data) app:dispatch(id, data) end

modules/edui.app/markDirty

markDirty(self: any)

Mark the app dirty and rebuild its tree. Coalesces a rebuild requested WHILE a build is running into a single follow-up build (so a handler that mutates state and a builder that reads it never recurse), and one requested WHILE a dispatch's handler runs into the single rebuild that dispatch performs after the handler returns.

Parameters

  • self any (optional)

modules/edui.app/mount

mount(self: any, builderFn: (any) -> any): any

Mount the app: register the broadcast callback env and do the first build. builderFn(app) -> widgetTree is called now and on every rebuild; inside it, app:cb(...) (or the edui.* primitives) wire closures.

Parameters

  • self any (optional)
  • builderFn (any) -> any — (app) -> widgetTree.
app:mount(function(a) return { type = "vertical", children = { ... } } end)

modules/edui.app/new

new(screenName: string, opts: { [string]: any }?): any

Create an editor app that owns the ui.* screen screenName.

Parameters

  • screenName string — Unique screen id (also the callback-id namespace prefix).
  • opts { [string]: any }? (optional) — { layer? = <renderLayerMask>, order? = <number>, callbackKey? = <string> }. layer is a render-layer membership mask (e.g. the EditorUI bit) applied via ui.setScreenRenderLayer on first register; omit for the default. order is the screen's Z-ORDER — higher paints on top; a floating surface (the command palette) states one to stand over the dock.
local app = edui.app.new("myPanel", { layer = editorMask })

modules/edui.app/rebuild

rebuild(self: any)

Run the builder and push the resulting tree to the screen. First call registers the screen (+ render layer); later calls update it. Builder errors are logged, never thrown, so one bad build never wedges the editor.

Parameters

  • self any (optional)

modules/edui.app/unchanged

unchanged(self: any, sig: string): boolean

Whether the tree this build would produce is the one the app's screen already shows. sig names every piece of state the build reads: an equal signature means the published tree already says it, so the builder returns nil and the App keeps what it published, at O(1) cost however large the tree. The signature belongs to the App that published the tree, so an App with nothing published reports a change and gets a tree to register.

Parameters

  • self any (optional)
  • sig string — The signature of the state this build reads.
if app:unchanged(buildSignature()) then return nil end

modules/edui.app/unmount

unmount(self: any)

Tear the app down: release the callback env and unregister the screen.

Parameters

  • self any (optional)

modules/edui.argForm/README

require("@builtin::editor.edui.widgets.argForm") -- edui.argForm

The typed argument form — a declared argument list becomes a form of REAL controls, and the filled form becomes the typed values a call takes. A person never types a Luau literal: strings get a text field, numbers a numeric field, booleans a checkbox, string-literal unions a chip row, colours a colour swatch, Vec3 three axis fields, and an options table expands one level into typed rows of its own. Only a type outside the model falls back to the literal field. The command palette renders every tool through this; the Inspector renders an asset type's operations through the same form, so any surface that knows a signature can offer it.

Usage: local edui.argForm = require("@builtin::editor.edui.widgets.argForm")

modules/edui.argForm/fieldRows

fieldRows(self: any, path: string, t: string, optional: boolean,

The label + control rows for one argument (or one expanded table field). depth indents expanded option fields under their parent.

modules/edui.argForm/make

make(W: any): any

Bind the form builder to the widgets barrel W. The barrel calls this once; consumers reach the result as edui.argForm.

Parameters

  • W any (optional)

modules/edui.argForm/new

new(o: { [string]: any }): any

A new form instance. o.ns (REQUIRED — the id namespace every control keys under), o.typeDefs (the schema's named definitions, { { name, definition } }), o.onSubmit (fired when Enter lands in a text field — the run affordance), o.onChanged (fired after any control writes a value, for hosts that gate rebuilds on an epoch).

Parameters

  • o { [string]: any }

modules/edui.argForm/parseSignature

parseSignature(sig: string): { any }

Parse a method signature string — "(self, name: string, opts: T?)" — into the argument list a form renders: { { name, type, optional } }. A leading self is the receiver, not an argument, and is dropped. The arguments are the balanced parenthesised run at the head of the text, so a signature that goes on to declare what the call hands back — "(self, name: string): boolean" — renders the same form as one that stops at the arguments.

Parameters

  • sig string — The signature text.

modules/edui.argForm/rows

rows(self: any, args: { any }): { any }

The rows for a whole argument list (each { name, type, optional, description? }), in order.

Parameters

  • self any (optional)
  • args { any }

modules/edui.argForm/values

values(self: any, args: { any }): (boolean, any, number?)

Assemble the positional values for args from the filled form. Returns (true, values, n) — n the last non-nil position for table.unpack(values, 1, n) — or (false, message).

Parameters

  • self any (optional)
  • args { any }

modules/edui.cmdPalette/README

require("@builtin::editor.edui.cmdPalette") -- edui.cmdPalette

The command palette — search everything under one keystroke. Ctrl+K (or the topbar's Tools button) opens a floating surface whose one query reaches the scene's entities (select + frame), the registered assets (select — the Inspector shows it), the editor's panels (focus), and the whole tool registry (pick one and its typed argument schema becomes a form, Run executing it in place). A t: / e: / a: / p: prefix narrows to one domain. Every domain reads its live registry, so what exists is findable with no UI work per addition.

Usage: local edui.cmdPalette = require("@builtin::editor.edui.cmdPalette")

modules/edui.cmdPalette/isOpen

isOpen(): boolean

Whether the palette is up. The screen's live visibility is the one answer every scope shares — the menu bar, the entrypoint, and a panel each hold their own copy of this module, and a module-local flag left them disagreeing about whether the palette was open, so a Close click handled by one copy re-showed what another copy had shown.

modules/edui.cmdPalette/mount

mount(layerMask: number?)

Mount the palette (idempotent): its own screen, hidden until toggled.

Parameters

  • layerMask number? (optional) — Optional EditorUI render-layer mask (the menu bar passes its own).

modules/edui.cmdPalette/openTool

openTool(name: string)

Open the palette directly at a tool's form — the deep link another surface uses to hand a person a ready-to-fill tool ("run this operation" from an inspector, a docs page, an agent suggestion). Opens the palette if it is closed, then loads name's schema as the form.

Parameters

  • name string — The tool's full name, toolbox.tool.

modules/edui.cmdPalette/screenName

screenName(): string

The palette's screen name (capture / element addressing).

modules/edui.cmdPalette/toggle

toggle()

Open / close the palette. Opening starts fresh at the search and puts the caret in it. The decision reads the screen's live visibility, never a module-local flag.

modules/edui.cmdPalette/unmount

unmount()

Tear the palette down.

modules/edui.confirmDialog/README

require("@builtin::editor.edui.confirmDialog") -- edui.confirmDialog

The editor's confirmation dialog: a question over the whole editor with a row of answers. One question shows at a time. Opening another while one is up puts the new one in front, and the earlier one comes back once the new one is answered. An answer's run decides whether its question closes: returning false, why keeps it up with why shown under it, anything else closes it.

Usage: local edui.confirmDialog = require("@builtin::editor.edui.confirmDialog")

modules/edui.confirmDialog/choose

choose(key: string): boolean

Answer the question on screen with the action keyed key — what clicking its button does. The action's run returning false, why keeps the question up showing why; otherwise it closes, and the question it stood in front of comes back.

Parameters

  • key string — An action key of the question on screen.
edui.confirmDialog.choose("cancel")

modules/edui.confirmDialog/close

close(key: string?)

Close a question without answering it and run its onClose once: the one keyed key wherever it waits, or the one on screen when key is omitted.

Parameters

  • key string? (optional) — The key the question was opened with.
edui.confirmDialog.close("close:code")

modules/edui.confirmDialog/current

current(): string?

The key of the question on screen, or nil while closed or when it was given none.

if edui.confirmDialog.current() == "close:code" then end

modules/edui.confirmDialog/errorText

errorText(): string?

Why the last answer to the question on screen kept it up, or nil.

modules/edui.confirmDialog/isAsking

isAsking(key: string): boolean

Whether a question with key is on screen or waiting behind one.

Parameters

  • key string — The key the question was opened with.

modules/edui.confirmDialog/isOpen

isOpen(): boolean

Whether the dialog is on screen.

modules/edui.confirmDialog/mount

mount(layerMask: number?)

Create the dialog's screen (idempotent): hidden until a question is opened. Order 5100, over every dock panel, the bars and the other editor dialogs.

Parameters

  • layerMask number? (optional) — The EditorUI render-layer mask; computed when omitted.

modules/edui.confirmDialog/open

open(request: Request)

Ask a question. It goes in front of any question already up, which comes back once this one is answered; a question opened with the key of one already open takes that one's place. Each action is a button, keyed edui_confirmdialog:cd:<action key> for ui.click; dismiss names the action Escape or a click outside runs, and without it the question can only be answered. onClose runs once when the question closes.

Parameters

  • request Request — { key?, title, message, actions = { { key, text, primary?, run? } }, dismiss?, onClose? }.
edui.confirmDialog.open{ title = "Delete?", message = "It goes for good.", dismiss = "no", actions = { { key = "yes", text = "Delete", run = del }, { key = "no", text = "Keep" } } }

modules/edui.confirmDialog/screenName

screenName(): string

The dialog's screen name, for captures and element addressing.

modules/edui.confirmDialog/unmount

unmount()

Tear the dialog's screen down, closing every open question.

modules/edui.framework/README

require("@builtin::editor.edui.framework") -- edui.framework

Editor UI framework on the CSS-parity ui.* surface. A small, editor-ONLY toolkit: a reactive app that maps author closures to ui.* callback ids, a dock shell over ui.* native docking, and editor-chrome primitives (panels, trees, toolbars, inspector rows, context menus, drag-and-drop). Gameplay / normal UI authors raw ui.* trees and needs none of this — edui earns its keep only for the editor.

Usage: local edui.framework = require("@builtin::editor.edui.framework")

modules/edui.framework/createApp

createApp(name: string, opts: { [string]: any }?): any

Create + return an editor app that owns the ui.* screen name. Shorthand for edui.app.new. Call app:mount(builderFn) to bring it up.

Parameters

  • name string — Unique screen id (also the callback-id namespace).
  • opts { [string]: any }? (optional) — { layer? = <renderLayerMask> }.
local a = edui.createApp("myPanel"); a:mount(function(app) return tree end)

modules/edui.framework/current

current(): any

The app whose builder is currently running (nil outside a build). The edui.* primitives read this to register their closures.

modules/edui.inspector.assetSections/README

require("@builtin::editor.edui.inspector.assetSections") -- edui.inspector.assetSections

The generic sections every asset's Inspector shows, in default order: identity (name, type, scope, guid, description), problems (what asset.validate reports, present only when it reports any), tags (removable chips and an add row), move (relocate the asset's folder), settings (the typed, editable settings), preview (the live or still picture) and operations (every ref method the type declares, each a typed argument form run against the asset in place).

Usage: local edui.inspector.assetSections = require("@builtin::editor.edui.inspector.assetSections")

modules/edui.inspector.assetSections/generic

generic(ctx: any, only: { [string]: boolean }?): { any }

The generic sections for the asset ctx inspects, in default order. A section with nothing to show is left out: Move for an asset outside the world's own source tree, Settings for an asset with neither values nor a schema problem, Preview for an asset with no picture, Operations for a type with no ref methods. Operations lists every method but setSettings, and starts collapsed on a type that declares an inspector of its own.

Parameters

  • ctx any (optional) — The inspector Context (asset scope).
  • only { [string]: boolean }? (optional) — The ids of the sections to build; every one when nil. A section left out is not read at all, so an inline editor that shows Settings alone runs no validation, preview lookup or behavior load.
local sections = AssetSections.generic(ctx)

modules/edui.inspector.assetSections/releaseExcept

releaseExcept(keep: string)

Drop the interaction state (open operation rows, argument forms, the last result, pending tag and move inputs) and the validation problems read for every asset but keep.

Parameters

  • keep string — The asset id whose state stays.
AssetSections.releaseExcept(guid)

modules/edui.inspector.parts/README

require("@builtin::editor.edui.inspector.parts") -- edui.inspector.parts

The building blocks a type's inspector.luau composes its sections from, reached as ctx.parts: fact rows, lists, tables, asset and entity links, actions, a file opener into the Code panel, problem lists, colour swatches, a cache keyed on content versions (and one for values read by running an asset's code), an lsp check, and typed controls over any values. Every function takes the section's Context first and returns edui nodes; every callback key starts with ctx.keyPrefix.

Usage: local edui.inspector.parts = require("@builtin::editor.edui.inspector.parts")

modules/edui.inspector.parts/action

action(ctx: any, id: string, label: string, run: () -> (), opts: any?): { any }

A button that runs run. A run that raises shows its message beneath the button until the next run.

Parameters

  • ctx any (optional) — The Context.
  • id string — Action id within this subject.
  • label string — Button text.
  • run () -> () — The action.
  • opts any? (optional) — { primary? }.
ctx.parts.action(ctx, "compile", "Compile", function() ctx.ref:compile() end)

modules/edui.inspector.parts/bytes

bytes(n: number): string

A byte count as text: B below 1 KiB, KiB to one decimal below 1 MiB, else MiB to two.

Parameters

  • n number — Bytes.
parts.bytes(1536) --> "1.5 KiB"

modules/edui.inspector.parts/cached

cached(key: string, paths: { string }, compute: () -> any): any

compute()'s value, computed again only when the content version of any of paths moves. Read through a Context's ctx.parts, the value is held while that Context, or another that read it, stays open; read through the module itself, it is held until the next Inspector.release.

Parameters

  • key string — Cache key (make it unique per asset and purpose).
  • paths { string } — The files the value is derived from.
  • compute () -> any — Produces the value.
local geom = ctx.parts.cached("mesh:" .. guid, { path .. "/data.zmsh" }, decode)

modules/edui.inspector.parts/cachedCode

cachedCode(key: string, ref: any, compute: () -> any, opts: any?): any

compute()'s value where computing it runs ref's own code (a graph built from its init.luau, a module's declared settings): held until a file of ref's folder changes, it moves, a module its code requires changes (one it required before that module was known is asked about again at most every quarter second), a file opts.dependsOn(value) names changes, or a name opts.awaits(value) lists — one the value was read without because no asset answered to it — comes to answer (asked again when an asset is made, removed or renamed). A compute that raises holds nothing and raises, and a value opts.hold(value) answers false for is returned and not held, so either is read again on the next build. Read through a Context's ctx.parts, the value is held while that Context, or another that read it, stays open. Fold codeSignature(key) into the declaration's signature so a change to what the value came from rebuilds the section.

Parameters

  • key string — Cache key (make it unique per asset and purpose).
  • ref any (optional) — The asset whose code compute runs.
  • compute () -> any — Produces the value.
  • opts any? (optional) — { hold?, dependsOn?, awaits? }.
local graph = ctx.parts.cachedCode("graph:" .. guid, ctx.ref, function() return vfs.read(ctx.ref.path .. "/graph.json") end)

modules/edui.inspector.parts/check

check(ctx: any, path: string): { any }

A "Check" action running lsp.check(path), and the result of the last check of the file's current content.

Parameters

  • ctx any (optional) — The Context.
  • path string — The .luau file.
ctx.parts.check(ctx, ctx.ref.path .. "/init.luau")

modules/edui.inspector.parts/codeSignature

codeSignature(key: string): string

Text that moves when what the value behind cachedCode(key, ...) came from beyond its own folder changes: the modules its code requires, one of them becoming known, the files dependsOn named, and a name awaits listed coming to answer. Empty before the first read.

Parameters

  • key string — The key cachedCode was read with.
signature = function(ref) return ctx.parts.codeSignature("graph:" .. guid) end
entityLinks(ctx: any, ids: { string }, opts: any?): { any }

One button per entity — its name — that selects it. An id whose entity no longer exists is listed as text, marked despawned.

Parameters

  • ctx any (optional) — The Context.
  • ids { string } — Entity ids.
  • opts any? (optional) — { max?, empty? }.
ctx.parts.entityLinks(ctx, compRef:listInstances())

modules/edui.inspector.parts/facts

facts(ctx: any, rows: { any }, empty: string?): { any }

Label/value rows; the value takes the width its label leaves and wraps within it. A row whose value is nil shows "—", so a value that could not be read is seen to be missing; a row that does not apply to the subject is left out of rows by the section. With no rows, empty (or "Nothing recorded.") is shown.

Parameters

  • ctx any (optional) — The Context.
  • rows { any } — { { label, value, mono? } }.
  • empty string? (optional) — Text for no rows.
ctx.parts.facts(ctx, { { label = "Vertices", value = 24 } })

modules/edui.inspector.parts/forContext

forContext(keyPrefix: string): any

The parts as the Context keyPrefix names reads them — what ctx.parts holds. Its cached records that Context as one that read the value, so the value goes when every Context that read it is released; every other part is the module's own.

Parameters

  • keyPrefix string — The Context's keyPrefix.
ctx.parts = Parts.forContext(ctx.keyPrefix)

modules/edui.inspector.parts/format

format(v: any): string

A value as display text: integers plain, other numbers to 4 significant digits, booleans yes/no, a list as its length, a map as its entry count.

Parameters

  • v any (optional) — The value.
parts.format(3) --> "3"
links(ctx: any, refs: { any }, opts: any?): { any }

One button per distinct asset — its name and type — that selects it; an asset named twice is listed once. An item is an AssetRef, a { __ref = guid } envelope, or a guid or name; one that does not resolve is listed as text.

Parameters

  • ctx any (optional) — The Context.
  • refs { any } — AssetRefs, { __ref } envelopes, or guids.
  • opts any? (optional) — { max?, empty? }.
ctx.parts.links(ctx, { meshRef, materialRef })

modules/edui.inspector.parts/list

list(ctx: any, items: { string }, opts: any?): { any }

One text row per item, at most max (default 40), then "… and N more".

Parameters

  • ctx any (optional) — The Context.
  • items { string } — The texts.
  • opts any? (optional) — { max?, empty?, mono? }.
ctx.parts.list(ctx, boneNames, { max = 30 })

modules/edui.inspector.parts/openFile

openFile(ctx: any, path: string, label: string?): { any }

An action opening path in the Code panel. Nothing when the file is absent. Its id names the file relative to the subject's folder, so two files of one name in different folders keep separate actions.

Parameters

  • ctx any (optional) — The Context.
  • path string — VFS path.
  • label string? (optional) — Button text (default "Open ").
ctx.parts.openFile(ctx, ctx.ref.path .. "/init.luau")

modules/edui.inspector.parts/problems

problems(ctx: any, problems: { any }, clean: string?): { any }

One notice per problem (error or warning tone), or clean when there are none.

Parameters

  • ctx any (optional) — The Context.
  • problems { any } — { { message, code?, severity?, line? } }.
  • clean string? (optional) — Text for no problems (default "No problems.").
ctx.parts.problems(ctx, asset.validate(ctx.ref).problems)

modules/edui.inspector.parts/release

release(keyPrefix: string)

Forget what the parts hold for the Context keyPrefix names and every key nested beneath it: the last failure of each action, each file's last check, its sections' state, and every cached value (a cached one or a cachedCode one) no other open Context read. A value read through the module itself, by no Context, goes too.

Parameters

  • keyPrefix string — A Context's keyPrefix.
Parts.release(ctx.keyPrefix)

modules/edui.inspector.parts/state

state(ctx: any): { [string]: any }

The table a Context's sections keep state in between builds (a picked zoom, a chosen tab), forgotten when the Context is released.

Parameters

  • ctx any (optional) — The Context.
local s = ctx.parts.state(ctx); s.zoom = s.zoom or "1"

modules/edui.inspector.parts/swatch

swatch(ctx: any, color: any, label: string): any

A colour square beside a label.

Parameters

  • ctx any (optional) — The Context.
  • color any (optional) — { r, g, b, a? } in 0..1.
  • label string — The label.
ctx.parts.swatch(ctx, { r = 1, g = 0, b = 0 }, "accent")

modules/edui.inspector.parts/table

table(ctx: any, key: string, columns: { any }, rows: { any }, empty: string?): { any }

A table of records, each value shown through format.

Parameters

  • ctx any (optional) — The Context.
  • key string — Table id within this section.
  • columns { any } — { { key, label?, align?, mono? } }.
  • rows { any } — Records keyed by column key.
  • empty string? (optional) — Text for no rows.
ctx.parts.table(ctx, "props", { { key = "name" }, { key = "type" } }, rows)

modules/edui.inspector.parts/values

values(ctx: any, opts: any): { any }

Typed controls over any values: one control per field of schema, validated with Field.check and written through write inside one undoable operation. undoable = false is for values that are session state, not content — a running render feature's live settings: each write goes through write with no undo operation, so undo and redo never reach it, and what it changes lasts as long as the thing it changed. readOnly shows the values with no control that writes. The controls' keys and rejections live under ctx.keyPrefix and subject, so the same subject shown by two Contexts keeps two states.

Parameters

  • ctx any (optional) — The section's Context.
  • opts any (optional) — { subject, schema?, values?, write?, undoable?, readOnly? }.
ctx.parts.values(ctx, { subject = guid .. ":live", schema = s, values = v, write = w })

modules/edui.inspector.presetActions/README

require("@builtin::editor.edui.inspector.presetActions") -- edui.inspector.presetActions

The Settings section's preset actions. "Save as preset" captures the values a Settings section shows as a typed preset targeting the declarer that governs them, named in a text field beneath the section, and selects the new preset. "Apply preset…" lists the presets whose target is that declarer and writes the one picked into the section's subject as one undoable operation. Both act on an asset's Settings section and on a component card's.

Usage: local edui.inspector.presetActions = require("@builtin::editor.edui.inspector.presetActions")

modules/edui.inspector.presetActions/applyInto

applyInto(p: any, name: string, targets: { any }): { text: string, tone: string }

Write preset p into each of targets (an asset ref, or { entity, component }) as one undoable operation — the operation already open, when one is, else its own. A target the preset refuses is reported by name and the rest are still written.

Parameters

  • p any (optional) — The preset.
  • name string — The preset's name as the outcome names it.
  • targets { any } — What to write into.
local outcome = PresetActions.applyInto(p, "pixel_art", { texRef })

modules/edui.inspector.presetActions/outcomeNotice

outcomeNotice(ctx: any): any?

The outcome of the last save or apply under ctx, as a notice, or nil.

Parameters

  • ctx any (optional) — The inspector Context.
local note = PresetActions.outcomeNotice(ctx)

modules/edui.inspector.presetActions/presetsFor

presetsFor(declarer: any): { Candidate }

Every preset whose target is declarer, by name.

Parameters

  • declarer any (optional) — The declarer.
for _, c in ipairs(PresetActions.presetsFor(texTypeRef)) do print(c.name) end

modules/edui.inspector.presetActions/recordOutcome

recordOutcome(keyPrefix: string, outcome: { text: string, tone: string })

Record outcome as the last save or apply under keyPrefix, which outcomeNotice shows until the next one or a release.

Parameters

  • keyPrefix string — A Context's keyPrefix.
  • outcome { text: string, tone: string } — { text, tone }, as applyInto answers.
PresetActions.recordOutcome(ctx.keyPrefix, PresetActions.applyInto(p, name, { tex }))

modules/edui.inspector.presetActions/release

release(keyPrefix: string)

Forget the open action, typed name, listed presets and outcome kept under keyPrefix and every prefix nested in it.

Parameters

  • keyPrefix string — A Context's keyPrefix.
PresetActions.release(ctx.keyPrefix)

modules/edui.inspector.presetActions/subjectOf

subjectOf(ctx: any): (any?, any?)

What a Settings section's preset actions write from and into, and the declarer that governs it: an asset's own ref, or { entity, component } for a component card. Nil for a Context whose values no preset targets — a preset's own values, a subject with no declarer.

Parameters

  • ctx any (optional) — The inspector Context.
local subject, declarer = PresetActions.subjectOf(ctx)

modules/edui.inspector.settingsSection/README

require("@builtin::editor.edui.inspector.settingsSection") -- edui.inspector.settingsSection

The Settings section: every field of ctx.schema as its typed control, ctx.settings as the values, ctx.setSettings as the one write, each field's last rejection shown on it, and ACTIONS beneath.

Usage: local edui.inspector.settingsSection = require("@builtin::editor.edui.inspector.settingsSection")

modules/edui.inspector.settingsSection/actionRow

actionRow(ctx: any): any?

The row of ACTIONS that apply to ctx (only those ctx.actions names, when it is set), each acting on the whole of ctx.schema, with the panel of each open action and the outcome of the last preset save or apply beneath it; nil when no action applies.

Parameters

  • ctx any (optional) — The inspector Context.
local row = SettingsSection.actionRow(ctx)

modules/edui.inspector.settingsSection/build

build(ctx: any): { any }

The section body for ctx. Reads only ctx.schema, ctx.schemaProblem, ctx.settings, ctx.setSettings, ctx.rejected, ctx.nameOf, ctx.widgets and ctx.keyPrefix, so any source of values (an asset, a component, a preset) renders through it. Fields show in the order the schema declares them.

Parameters

  • ctx any (optional) — The inspector Context.
local nodes = SettingsSection.build(Inspector.valuesContext{ subject = key, schema = s, values = v, write = w, selection = sel })

modules/edui.inspector.settingsSection/fieldRows

fieldRows(ctx: any, fields: { [string]: any }): { any }

One typed control per field of fields (a schema, or part of ctx.schema), in declaration order, each writing through ctx.setSettings. Aliases are skipped. ctx.admit narrows every asset chip, asked with the chip's path from the field (presets[2].preset); ctx.openFields shows structs and lists open.

Parameters

  • ctx any (optional) — The inspector Context.
  • fields { [string]: any } — The field descriptors to show.
local rows = SettingsSection.fieldRows(ctx, ctx.schema)

modules/edui.inspector.settingsSection/fields

fields(ctx: any): { any }

The field rows alone — one typed control per schema field — for a caller that shows values without the actions beneath them. With no ctx.schema, the one row naming the schema problem, or that none is declared.

Parameters

  • ctx any (optional) — The inspector Context.
local rows = SettingsSection.fields(ctx)

modules/edui.inspector.settingsSection/release

release(keyPrefix: string)

Forget what the Settings actions keep under keyPrefix (an open panel, a typed name, an outcome) and every prefix nested in it.

Parameters

  • keyPrefix string — A Context's keyPrefix.
SettingsSection.release(ctx.keyPrefix)

modules/edui.inspector/README

require("@builtin::editor.edui.inspector") -- edui.inspector

The Inspector as an ordered list of sections. A subject (an asset, or a component on an entity) gets generic sections, merged with the sections its declarer's inspector.luau adds or replaces and put in the order it names; each section's build(ctx) receives one Context: the ref, its schema, its values with a validating, undoable setSettings, the selection and the edui widget set.

A section is { id, title, icon?, open?, build(ctx) -> { node } }. A type declares its own in the file its type.yaml names under inspector: (see @builtin::assetTypes.assetType.shared.inspector); material.assetType/ inspector.luau is a worked example. The Context every build receives:

FieldWhat it is
refthe inspected asset (on an entity, the component asset)
infoasset.inspect(ref)
schemaasset.schema(ref), settingsOf resolved; nil when none is declared
schemaProblemwhy a declared schema did not resolve
settingsthe current values
setSettings(patch)Field.checkSchema, then one undoable operation named for the field; answers (ok, violations)
rejectedfield path -> the message of its last rejected edit
selection{ scope = "asset" | "entity", id, component?, name? }; on an entity, component is the component's identity and name the name the entity lists it by
componentthe live component proxy (entity scope)
nameOf(id)the name an entity-reference field shows for an entity id (entity scope)
widgetsthe edui framework table (field, schemaField, section, button, text, ...)
keyPrefixthe callback-key namespace for this subject
refresh()rebuild the Inspector after a write that moves no revision
follow()keep rebuilding while this section is shown: what it shows moves with nothing writing it (a clock that runs)
actionswhen set, the ids of the Settings actions offered; nil offers every one that applies
partsthe building blocks a section composes (edui.inspector.parts)
The generic asset sections, in default order: identity, problems
(present when asset.validate reports any), tags, move, settings,
preview, operations. A declared section is added before
operations. settingsSection is the Settings section's body and the
actions offered beneath it.
A component on an entity (componentSections) has one generic section,
settings: its public fields as typed controls over the live component,
each write one undoable operation. A component that ships the file its
type names as inspector: { instance } adds, replaces and orders
sections the same way.

Usage: local edui.inspector = require("@builtin::editor.edui.inspector")

modules/edui.inspector/assetSections

assetSections(ref: any, info: any, keyPrefix: string?): ({ any }, any)

Generic plus declared sections for an asset, and the Context they share.

Parameters

  • ref any (optional) — The asset ref.
  • info any (optional) — Its asset.inspect record.
  • keyPrefix string? (optional) — The Context's key prefix; one per asset when omitted.
local sections, ctx = Inspector.assetSections(ref, asset.inspect(ref))

modules/edui.inspector/buildSection

buildSection(section: any, ctx: any): ({ any }, string?)

Run one section's build for ctx. A build that raises, or returns anything but a list, yields one error row and the problem text.

Parameters

  • section any (optional) — The section.
  • ctx any (optional) — The Context.
local rows, problem = Inspector.buildSection(section, ctx)

modules/edui.inspector/componentSections

componentSections(entityId: string, componentName: string): ({ any }, any)

The sections of one component on one entity: the generic settings section (its public fields as typed controls over the live component, each write validated and one undoable operation) merged with what the file the component ships as its inspector: { instance } declares.

Parameters

  • entityId string — The entity the component is on.
  • componentName string — The component's name as the entity lists it.
local sections, ctx = Inspector.componentSections(id, "Camera")

modules/edui.inspector/componentSettingsRows

componentSettingsRows(ctx: any): { any }

The body of a component's generic Settings section: each three-number alias (a colour or a vector written as three channels) as one grouped control, then every other field through settingsSection.fieldRows, then the Settings actions over every declared field. A component that declares no fields reads "(no editable fields)".

Parameters

  • ctx any (optional) — The component's Context.
local rows = Inspector.componentSettingsRows(ctx)

modules/edui.inspector/componentValuesSignature

componentValuesSignature(entityId: string, componentName: string): string

Text that moves whenever a declared field of componentName on entityId holds a different value — an edit, an undo, a redo, a script's write. Fold it into a panel's build signature to rebuild a card whose values moved without the entity revision moving.

Parameters

  • entityId string — The entity the component is on.
  • componentName string — The component's name as the entity lists it.
parts[#parts + 1] = Inspector.componentValuesSignature(id, "Camera")

modules/edui.inspector/epoch

epoch(): number return epoch end

Moves on every interaction the inspector's own state records (a disclosure, a rejection, a write). Fold it into a panel's build signature.

parts[#parts + 1] = tostring(Inspector.epoch())

modules/edui.inspector/following

following(): boolean

Whether a section built since the last call asked, through its Context's follow(), to be rebuilt while it is shown — what a panel reads after rendering to keep a running clock moving on screen.

if Inspector.following() then live = true end

modules/edui.inspector/isGenericSection

isGenericSection(id: string): boolean

Whether id names a section the generic builder adds where it applies (identity, problems, tags, move, settings, preview, operations), as opposed to one a type's own inspector declares.

Parameters

  • id string — A section id.
if not Inspector.isGenericSection(s.id) then print("declared", s.id) end

modules/edui.inspector/mergeSections

mergeSections(generic: { any }, decl: any?, problem: string?, file: string?): { any }

Merge generic sections with a declaration: replace swaps a generic section by id, sections add (before operations when present, else at the end), order puts the ids it names first (a generic id this asset does not show is passed over). A problem becomes a first section naming it; every other section still renders.

Parameters

  • generic { any } — Sections in default order.
  • decl any? (optional) — The declaration, or nil.
  • problem string? (optional) — Why the declaration could not be read, or nil.
  • file string? (optional) — The declaring file, named in problems.
local sections = Inspector.mergeSections(generic, decl, problem, file)

modules/edui.inspector/release

release(keyPrefix: string)

Forget the rejections, section disclosures, Settings-action state and parts state (action failures, file checks, cached values no other open Context read) recorded under keyPrefix and every key nested beneath it — what a closed inline editor, or a subject the Inspector no longer shows, leaves behind.

Parameters

  • keyPrefix string — A Context's keyPrefix.
Inspector.release(ctx.keyPrefix)

modules/edui.inspector/render

render(sections: { any }, ctx: any, opts: any?): { any }

Render sections as cards (or, nested, as titled groups inside a card). Each build runs through buildSection, so one that raises renders its error inside its own section.

Parameters

  • sections { any } — The ordered sections.
  • ctx any (optional) — The Context every build receives.
  • opts any? (optional) — { nested? }.
local nodes = Inspector.render(sections, ctx, { nested = true })

modules/edui.inspector/renderAsset

renderAsset(assetId: string): { any }?

The rendered sections for the asset named by assetId, or nil when it cannot be read. A different asset than the last one clears the last one's rejections and its operation state.

Parameters

  • assetId string — The asset's guid or identity.
local nodes = Inspector.renderAsset(guid)

modules/edui.inspector/signature

signature(assetId: string): string

Text that moves whenever what the Inspector shows for assetId can have moved: the asset folder's content version, which the change dispatcher moves after any write inside the asset has reached its type's hook — an edit, an undo or redo, a peer's synced write — and its settings values file's. A type's declaration adds what its own sections show beyond the asset's files (a render feature's running state) through a signature(ref) member. Fold it into a panel's build signature.

Parameters

  • assetId string — The asset's guid or identity.
parts[#parts + 1] = Inspector.signature(guid)

modules/edui.inspector/typeSections

typeSections(ref: any, keyPrefix: string?): ({ any }?, any?)

The declared sections of ref's type, plus Settings when it has values — what an asset reference's inline editor shows. Nil when the type declares no type: inspector.

Parameters

  • ref any (optional) — The asset ref.
  • keyPrefix string? (optional) — The Context's key prefix — the inline editor's own, so two editors of one asset keep their own state and callback keys.
local sections, ctx = Inspector.typeSections(matRef, chipKey .. ":ed")

modules/edui.inspector/valuesContext

valuesContext(opts: any): any

The one Context builder. write(patch) performs a validated patch; this wraps it in Field.checkSchema, an undo operation named for the fields it sets, and the rejection record the Settings section shows. undoable = false names values that are session state rather than content (a running feature's live settings): their writes open no undo operation, so none reaches the undo history. keyPrefix names the callback keys and the state this Context keeps; it defaults to one per subject, and an inline editor passes its chip's own.

Parameters

  • opts any (optional) — { subject, keyPrefix?, schema?, schemaProblem?, values?, write, undoable?, ref?, info?, selection, component?, nameOf?, actions?, admit?, openFields? }. admit(record, at) narrows what every asset chip offers and accepts (at = { path, parent }); openFields shows structs and lists open.
local ctx = Inspector.valuesContext{ subject = guid, schema = s, values = v, write = w, selection = sel }

modules/edui.query/README

require("@builtin::editor.edui.query") -- edui.query

Resolves the engine-provided query globals edui panels read — queryEntitiesTable, queryLogsTable, getProfilingDataTable, and the like. The engine installs these on _G at boot (before the global table is sealed read-only), so panels read them through this one indirection rather than each touching _G directly. override/restore swap a source for a stub or a mock feed without writing the sealed _G.

Usage: local edui.query = require("@builtin::editor.edui.query")

modules/edui.query/override

override(name: string, fn: any)

Shadow an engine query global with a function until restored.

Parameters

  • name string — The global name.
  • fn any (optional) — The function to resolve in its place.

modules/edui.query/resolve

resolve(name: string): any

Resolve a named engine query global, honouring any active override.

Parameters

  • name string — The global name (e.g. "queryEntitiesTable").

modules/edui.query/restore

restore(name: string)

Clear an override so resolve falls back to the engine global again.

Parameters

  • name string — The global name.

modules/edui.sceneInfo/README

require("@builtin::editor.edui.sceneInfo") -- edui.sceneInfo

Which scene asset each loaded layer came from, as the editor shows it. A layer is a loaded instance of a scene asset: its guid is the asset's guid, its name is the asset's display name, and its identity names the asset to open, save or reload (@builtin::scenes.test_arena, a world scene's name). The open scene is the one non-additive layer.

Usage: local edui.sceneInfo = require("@builtin::editor.edui.sceneInfo")

modules/edui.sceneInfo/describe

describe(guid: string): { name: string, identity: string?, path: string?, loaded: boolean }

The scene asset behind a layer guid — a loaded layer's info, or for a guid no layer is loaded under, the scene asset that guid names.

Parameters

  • guid string — A layer guid (as entity.tree groups rows by).

modules/edui.sceneInfo/layers

layers(): { LayerInfo }

Every loaded layer with the scene asset it came from, in the order layers.list() reports them.

modules/edui.sceneInfo/open

open(): LayerInfo?

The open scene: the root (non-additive) layer and the asset it was loaded from.

modules/edui.sceneInfo/shortName

shortName(identity: string?): string?

A scene identity's last segment: @builtin::scenes.test_arena reads test_arena, /source/scenes/level01.scene reads level01.

Parameters

  • identity string? (optional) — A scene identity or VFS path.

modules/edui.sceneInfo/signature

signature(): string

A one-line signature of the loaded layers — which are loaded, which is open, and what each is named — for a watcher to compare frame to frame.

modules/edui.shell/README

require("@builtin::editor.edui.shell") -- edui.shell

The editor dock shell on the CSS-parity ui.* surface. One edui app owns a ui.* native dockArea whose children are a dockPanel per registered edui panel — real egui_dock tabs, splits and drag. The shell keeps its OWN panel registry (separate from the deprecated zui editor registry) so the zui editor stays untouched while panels migrate one at a time; the two registries fold into one at cutover.

Usage: local edui.shell = require("@builtin::editor.edui.shell")

modules/edui.shell/activeLayout

activeLayout(): { name: string?, title: string, slot: string? }

The layout in force, as the layout switch shows it.

edui.shell.activeLayout().title

modules/edui.shell/addPanel

addPanel(spec: any): boolean

Register (or replace) an edui dock panel. A duplicate id replaces the prior registration; the live shell rebuilds so the change shows at once.

Parameters

  • spec any (optional) — { id (REQUIRED), build (REQUIRED, () -> widget), title?, order?, dock?, badge?, badgeColor?, menu?, menuHint?, startClosed?, transient?, close?, dirty?, save?, discard? }. build is an edui builder: it may call the edui.* primitives, whose closures register on the shell app automatically. startClosed = true registers into the Window-menu catalog without opening a tab; the panel opens when someone opens it. title is text or an i18n.text value, which the tab, the menus and the catalog read in the editor's language each time they build. menuHint is a short note the menu entry carries on the right — a string, or a function read each time the menu opens. transient = true makes a panel that exists for this session alone: closing its tab forgets it, a layout or mode change keeps it open, and no saved arrangement records it. dirty: () -> boolean says whether the panel holds unsaved edits: its tab title ends in a dot while it does, and closing its tab asks first (Save & close / Discard / Cancel) — call refreshChrome when the answer changes. save: () -> (boolean, string?) writes the edits, returning false and why when it could not; discard: () -> () drops them before a Discard closes the tab. A layout or mode change closes a non-transient panel without asking, so a panel declaring dirty keeps its edits through its close hook and shows them again when it reopens.
edui.shell.addPanel{ id = "entities", title = "Entities", build = fn }

modules/edui.shell/app

app(): any

The mounted shell App, or nil when the shell is not up.

modules/edui.shell/applyLayout

applyLayout(name: string, opts: any?): (boolean, string?)

Apply the layout named: the world's .editorLayout of that name, else the built-in one. A layout listing panels rebuilds the dock from its panel set and region hints, so it comes out the same at any window size; one holding a captured arrangement restores it. The name is recorded under /source/local as the layout this world's editor comes up in.

Parameters

  • name string — A name layouts reports.
  • opts any? (optional) — { persist? = false, arrangement? } — persist = false applies without recording it (a boot's --layout); arrangement is a captured arrangement to restore in place of the layout's own seed, which is how the layout switch brings a layout back as it was left.
edui.shell.applyLayout("recording")

modules/edui.shell/assignLayout

assignLayout(side: string, name: string): (boolean, string?)

Assign a layout to side A or side B of the layout switch. The assignment is kept per world on this machine. A layout already standing for the other side swaps places with the one this side gave up. When this side is the one in force, the layout takes the editor at once.

Parameters

  • side string — "a" | "b".
  • name string — A name layouts reports.
edui.shell.assignLayout("a", "recording")

modules/edui.shell/catalog

catalog(): { any }

Every catalogued panel (open or closed), sorted by (order, id), each { id, title, order, dock, open } — the Window menu's source.

modules/edui.shell/catalogRevision

catalogRevision(): number

How many times the catalog's membership has changed — a registration, a re-registration or a removal each count once. A surface built from the catalog (the menu bar) compares this between frames and rebuilds when it moves, rather than walking the catalog every frame.

if edui.shell.catalogRevision() ~= seen then print("the panel catalog changed") end

modules/edui.shell/confirmClose

confirmClose(id: string): boolean

Ask the user whether to save a panel's unsaved edits before its tab closes: a dialog titled "Unsaved changes" offering Save & close (when the spec declares save), Discard and Cancel. While the dialog asks about another panel this one waits its turn.

Parameters

  • id string — The panel id passed to addPanel.
edui.shell.confirmClose("code")

modules/edui.shell/confirmDiscard

confirmDiscard(request: any)

Ask whether to keep unsaved edits before something drops or replaces them: a dialog titled "Unsaved changes" saying " has unsaved changes.", offering the save (when <code>save</code> is given), Discard and Cancel. A save that succeeds, or a discard, runs <code>andThen</code>; a save that fails keeps the dialog up with its reason; Cancel runs nothing. Closing a dirty tab asks through this, and a panel asks through it before it replaces or drops a document of its own.</p> <p><strong>Parameters</strong></p> <ul> <li><code>request</code> <code>any</code> <em>(optional)</em> — <code>{ key, title, save?: () -> (boolean, string?), discard?: () -> (), saveText? = "Save & continue", andThen: () -> (), onClose?: () -> () }</code> — <code>key</code> names the question (<code>edui.confirmDialog.current()</code>), and <code>onClose</code> runs once when it closes by any answer.</li> </ul> <pre><code class="language-lua">edui.shell.confirmDiscard{ key = "code:open", title = "notes.md", save = save, andThen = open } </code></pre> <h2 id="shell-exportlayout">modules/edui.shell/exportLayout</h2> <pre><code class="language-lua">exportLayout(): string? </code></pre> <p>The live dock arrangement, serialized without its transient tabs — what saveLayout writes, what the next boot restores, and what loadLayout applies. Nil before the dock's first render.</p> <h2 id="shell-focuspanel">modules/edui.shell/focusPanel</h2> <pre><code class="language-lua">focusPanel(id: string): boolean </code></pre> <p>Bring a panel's tab to the front of its dock leaf, opening it from the catalog first if its tab is closed — what a menu entry naming a panel does. Unlike <code>openPanel</code>, this acts visibly when the tab is already open behind another.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id.</li> </ul> <h2 id="shell-ispaneldirty">modules/edui.shell/isPanelDirty</h2> <pre><code class="language-lua">isPanelDirty(id: string): boolean </code></pre> <p>Whether a catalogued panel reports unsaved edits through its spec's <code>dirty</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id passed to addPanel.</li> </ul> <pre><code class="language-lua">if edui.shell.isPanelDirty("code") then end </code></pre> <h2 id="shell-ispanelopen">modules/edui.shell/isPanelOpen</h2> <pre><code class="language-lua">isPanelOpen(id: string): boolean </code></pre> <p>Whether the shell is holding a panel's tab open right now. A panel that arms something while it is on screen reads this to find out whether it still is: the shell can rebuild a panel whose tab has just gone, and the answer here has already moved.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id passed to addPanel.</li> </ul> <pre><code class="language-lua">if edui.shell.isPanelOpen("capture") then end </code></pre> <h2 id="shell-layoutmode">modules/edui.shell/layoutMode</h2> <pre><code class="language-lua">layoutMode(): string </code></pre> <p>The mode of the layout in force: "full" (a panel that registers later opens as a tab) or "simple" (the layout's panel set is fixed: a panel that registers later waits in the Window menu).</p> <h2 id="shell-layoutrevision">modules/edui.shell/layoutRevision</h2> <pre><code class="language-lua">layoutRevision(): number </code></pre> <p>How many times the registered layouts, the slots or the layout in force have changed. The bar compares this between frames to rebuild its layout switch.</p> <h2 id="shell-layoutslots">modules/edui.shell/layoutSlots</h2> <pre><code class="language-lua">layoutSlots(): { a: string?, b: string? } </code></pre> <p>The layout each side of the layout switch stands for: the one a person assigned while it is registered, else the one the library ships for that side.</p> <pre><code class="language-lua">edui.shell.layoutSlots().a </code></pre> <h2 id="shell-layouts">modules/edui.shell/layouts</h2> <pre><code class="language-lua">layouts(): { any } </code></pre> <p>Every layout that can be applied: the built-in library's standard ones (<code>kind = "standard"</code>) in registration order, then the world's own (<code>kind = "saved"</code>), each with the title the layout switch shows (the layout's name when its layout.yaml gives none), the sentence its layout.yaml describes it with, its <code>mode</code> and whether it shows the <code>toolstrip</code>, the side of the switch it declares (<code>defaultSlot</code>), the panel ids it opens in the order it names them, where it places each (<code>placements</code>) and the share each side region takes (<code>fractions</code>). A world layout with a standard layout's name stands in for it and is listed once. A layout holding a captured arrangement rather than a panel list reports an empty <code>panels</code> and the arrangement decides its tabs.</p> <h2 id="shell-listlayouts">modules/edui.shell/listLayouts</h2> <pre><code class="language-lua">listLayouts(): { string } </code></pre> <p>Every layout name that can be applied, in menu order: the standard layouts first, then the world's own.</p> <h2 id="shell-loadlayout">modules/edui.shell/loadLayout</h2> <pre><code class="language-lua">loadLayout(name: string): (boolean, string?) </code></pre> <p><code>applyLayout</code>, under the name the Window menu has always used.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — A name <code>layouts</code> reports.</li> </ul> <h2 id="shell-mount">modules/edui.shell/mount</h2> <pre><code class="language-lua">mount(opts: any?): any </code></pre> <p>Mount the shell (idempotent — returns the existing app if already up). Registers the <code>ui.*</code> screen on the EditorUI layer and does the first build.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ refresh? = <seconds between liveness rebuilds, default 0.5> }</code>.</li> </ul> <pre><code class="language-lua">edui.shell.mount() </code></pre> <h2 id="shell-openpanel">modules/edui.shell/openPanel</h2> <pre><code class="language-lua">openPanel(id: string): boolean </code></pre> <p>Re-open a catalogued panel by id — restores its tab in its preferred dock region. A no-op (returns false) if the id was never registered or is already open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id.</li> </ul> <h2 id="shell-panel">modules/edui.shell/panel</h2> <pre><code class="language-lua">panel(id: string): any? </code></pre> <p>The catalogued panel record for <code>id</code> — the spec table its registration passed to addPanel, extra keys included. This is how one panel reaches another's exported surface: the Files panel opens a file in the Code panel through the <code>openFile</code> its record carries.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id passed to addPanel.</li> </ul> <pre><code class="language-lua">local rec = edui.shell.panel("code"); if rec then rec.openFile(path) end </code></pre> <h2 id="shell-panelssorted">modules/edui.shell/panelsSorted</h2> <pre><code class="language-lua">panelsSorted(): { any } </code></pre> <p>The registered panels sorted by (order, id) — the dock/tab order.</p> <h2 id="shell-refresh">modules/edui.shell/refresh</h2> <pre><code class="language-lua">refresh() </code></pre> <p>Rebuild every active panel now — each re-queries live data. Call after a change the shell can't observe (a scene edit made outside its own handlers). For a change that concerns ONE panel, <code>refreshPanel</code> rebuilds just that panel.</p> <h2 id="shell-refreshchrome">modules/edui.shell/refreshChrome</h2> <pre><code class="language-lua">refreshChrome() </code></pre> <p>Rebuild the dock chrome — the tab titles and what each tab's close does. A panel with a <code>dirty</code> spec calls this when its answer changes.</p> <pre><code class="language-lua">edui.shell.refreshChrome() </code></pre> <h2 id="shell-refreshpanel">modules/edui.shell/refreshPanel</h2> <pre><code class="language-lua">refreshPanel(id: string) </code></pre> <p>Rebuild one panel — its own screen republishes; every other panel and the dock itself are untouched. The scoped path for a data event with a known audience (a selection change concerns the entity tree and the inspector, not the console).</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id to rebuild.</li> </ul> <pre><code class="language-lua">edui.shell.refreshPanel("inspector") </code></pre> <h2 id="shell-registerlayout">modules/edui.shell/registerLayout</h2> <pre><code class="language-lua">registerLayout(record: any): boolean </code></pre> <p>Register a layout with the shell, the way an <code>.editorLayout</code> asset's <code>onRegister</code> does. <code>record</code> is <code>{ name, title?, description?, mode, panels, fractions?, dock?, origin }</code>; <code>title</code> and <code>description</code> are text or <code>i18n.text</code> values, read in the editor's language each time the layouts are listed; <code>origin</code> is "builtin" for the library's or "world" for one authored into the world, and a world layout stands in for a built-in one of the same name. A layout the boot asked for applies as soon as it is registered.</p> <p><strong>Parameters</strong></p> <ul> <li><code>record</code> <code>any</code> <em>(optional)</em> — The layout record.</li> </ul> <h2 id="shell-reloadlayoutslots">modules/edui.shell/reloadLayoutSlots</h2> <pre><code class="language-lua">reloadLayoutSlots() </code></pre> <p>Read the layout assignment this world kept, replacing what the editor holds: what a boot does before it applies anything.</p> <h2 id="shell-removepanel">modules/edui.shell/removePanel</h2> <pre><code class="language-lua">removePanel(id: string): boolean </code></pre> <p>Remove a registered panel by id — closes its tab. The panel stays in the catalog, so the Window menu can re-open it later; a <code>transient</code> panel is forgotten with its tab. A spec that declares <code>close</code> is told: the hook runs as the tab goes, so a panel holding live resources (a render session, a watcher) releases them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id passed to addPanel.</li> </ul> <h2 id="shell-requestclose">modules/edui.shell/requestClose</h2> <pre><code class="language-lua">requestClose(id: string): boolean </code></pre> <p>Close a panel's tab the way its close button does: a panel without unsaved edits closes at once, one with them asks first (<code>confirmClose</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id passed to addPanel.</li> </ul> <pre><code class="language-lua">edui.shell.requestClose("code") </code></pre> <h2 id="shell-resetlayout">modules/edui.shell/resetLayout</h2> <pre><code class="language-lua">resetLayout() </code></pre> <p>Restore the arrangement a fresh editor starts from, the escape hatch for a layout dragged into an unusable state: the layout side A of the layout switch stands for when it is registered, else the mode's own panel set (every catalogued panel in full mode, the viewport alone in simple) rebuilt from each panel's <code>dock</code> region hint.</p> <pre><code class="language-lua">edui.shell.resetLayout() </code></pre> <h2 id="shell-resetlayoutslots">modules/edui.shell/resetLayoutSlots</h2> <pre><code class="language-lua">resetLayoutSlots() </code></pre> <p>Forget the layouts assigned to the two sides, so each stands for the layout the library ships for it, and forget the file this world kept them in.</p> <h2 id="shell-resolvebootlayout">modules/edui.shell/resolveBootLayout</h2> <pre><code class="language-lua">resolveBootLayout(boot: string?, dock: string?, startLayout: string?): any </code></pre> <p>What the editor comes up in, from the three things that can name it. A layout named at boot is an instruction for this boot, so it applies without being recorded and the arrangement the editor was left in does not stand over it. With none named, that saved arrangement comes back; with none saved, the layout this world's editor last applied; and with none of those, side A of the layout switch, whichever layout it stands for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>boot</code> <code>string?</code> <em>(optional)</em> — The layout <code>--layout</code> named for this boot, or nil.</li> <li><code>dock</code> <code>string?</code> <em>(optional)</em> — The arrangement saved for this world, or nil.</li> <li><code>startLayout</code> <code>string?</code> <em>(optional)</em> — The layout this world's editor last applied, or nil.</li> </ul> <pre><code class="language-lua">edui.shell.resolveBootLayout(nil, nil, nil).slot </code></pre> <h2 id="shell-restorelayout">modules/edui.shell/restoreLayout</h2> <pre><code class="language-lua">restoreLayout(json: string): (boolean, string?) </code></pre> <p>Put the dock back into an arrangement <code>exportLayout</code> read: the open set becomes exactly the tabs it names, and the dock restores its tabs and splits on its next render. The counterpart of <code>exportLayout</code> for a caller that read the arrangement, moved it, and hands it back. A string that does not decode to a dock arrangement is refused, and the dock stays as it is.</p> <p><strong>Parameters</strong></p> <ul> <li><code>json</code> <code>string</code> — An arrangement <code>exportLayout</code> returned.</li> </ul> <pre><code class="language-lua">local held = edui.shell.exportLayout(); edui.shell.applyLayout("watch"); edui.shell.restoreLayout(held) </code></pre> <h2 id="shell-samearrangement">modules/edui.shell/sameArrangement</h2> <pre><code class="language-lua">sameArrangement(a: string?, b: string?): boolean </code></pre> <p>Whether two arrangements <code>exportLayout</code> read hold the same tabs in the same splits at the same fractions, whatever size the window drew them at and whichever tab had focus. Two nils are the same arrangement; a nil and an arrangement are not.</p> <p><strong>Parameters</strong></p> <ul> <li><code>a</code> <code>string?</code> <em>(optional)</em> — An arrangement <code>exportLayout</code> returned, or nil.</li> <li><code>b</code> <code>string?</code> <em>(optional)</em> — Another, or nil.</li> </ul> <pre><code class="language-lua">if not edui.shell.sameArrangement(held, edui.shell.exportLayout()) then edui.shell.restoreLayout(held) end </code></pre> <h2 id="shell-savelayout">modules/edui.shell/saveLayout</h2> <pre><code class="language-lua">saveLayout(name: string, opts: any?): (boolean, string?) </code></pre> <p>Save the live dock arrangement as the world's <code>.editorLayout</code> asset named <code>name</code>, at <code><folder>/<name>.editorLayout/</code> (the folder defaults to <code>/zero/source/editor/layouts</code>): <code>layout.yaml</code> points at the <code>dock.json</code> the live dock wrote. The asset registers itself, so the layout is in the Window menu at once, and it is renameable and removable through the Files panel like any other asset.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The layout's name; non-identifier characters fold to <code>_</code>.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ folder?, title?, description? }</code> — the folder under <code>/zero/source</code> to create the asset in, the name the layout switch shows (the layout's name by default), and the sentence the menu shows beside it.</li> </ul> <pre><code class="language-lua">edui.shell.saveLayout("modeling") </code></pre> <h2 id="shell-screenname">modules/edui.shell/screenName</h2> <pre><code class="language-lua">screenName(): string </code></pre> <p>The shell's screen name (for <code>ui.showScreen</code> / capture <code>screen</code>).</p> <h2 id="shell-setlayoutmode">modules/edui.shell/setLayoutMode</h2> <pre><code class="language-lua">setLayoutMode(mode: string, opts: any?): boolean </code></pre> <p>Put the editor in a layout mode without applying a layout: the mode every layout declares, and applying a layout does this for you. "simple" closes the authoring panels — every open panel that opens by default; the Scene viewport and every panel opened on purpose (one registered <code>startClosed</code>, an agent's tab among them) stay open. "full" restores the arrangement the editor held when simple was entered, tabs and splits as they were, or on a boot into simple opens every default panel at its region. The mode in force persists across sessions alongside the saved dock arrangement.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — "full" | "simple".</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ persist? = false }</code> skips the write (the boot restore path).</li> </ul> <pre><code class="language-lua">edui.shell.setLayoutMode("simple") </code></pre> <h2 id="shell-switchlayout">modules/edui.shell/switchLayout</h2> <pre><code class="language-lua">switchLayout(): (boolean, string?) </code></pre> <p>Take the editor to the other side of the layout switch: from A to B, from B to A, and from a layout standing for neither to A. The layout it leaves is remembered as it was left, tabs and splits included, and comes back that way.</p> <pre><code class="language-lua">edui.shell.switchLayout() </code></pre> <h2 id="shell-togglepanel">modules/edui.shell/togglePanel</h2> <pre><code class="language-lua">togglePanel(id: string): boolean </code></pre> <p>Toggle a catalogued panel open/closed — the Window-menu action. Closing goes through <code>requestClose</code>, so a panel holding unsaved edits asks first and stays open while it asks.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id.</li> </ul> <h2 id="shell-unmount">modules/edui.shell/unmount</h2> <pre><code class="language-lua">unmount() </code></pre> <p>Tear the shell down (unregisters the screen + callback env). Panel registrations are kept, so a later <code>mount</code> brings the same set back up.</p> <h2 id="shell-unregisterlayout">modules/edui.shell/unregisterLayout</h2> <pre><code class="language-lua">unregisterLayout(name: string, origin: string?) </code></pre> <p>Remove a registered layout, the way an <code>.editorLayout</code> asset's <code>onUnregister</code> does. With a built-in one of the same name still registered, that one stands again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The layout's name.</li> <li><code>origin</code> <code>string?</code> <em>(optional)</em> — "builtin" | "world"; omitted removes both.</li> </ul> <h2 id="shell-unregisterpanel">modules/edui.shell/unregisterPanel</h2> <pre><code class="language-lua">unregisterPanel(id: string): boolean </code></pre> <p>Drop a panel from the shell altogether: its tab closes the way <code>removePanel</code> closes it, and the catalog forgets it, so the Window menu stops listing it — what an <code>.editorPanel</code> asset's removal does.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The panel id passed to addPanel.</li> </ul> <h2 id="shell-update">modules/edui.shell/update</h2> <pre><code class="language-lua">update(dt: number) </code></pre> <p>Drive the shell's liveness refresh. Call each frame from the editor update loop; every ACTIVE panel rebuilds every <code>refresh</code> seconds so it reflects live scene state without arming its own timer. A tab switch is also caught here: the newly shown panel refreshes on the tick after the switch. The tool strip rebuilds on the frame after a message or chat post is written.</p> <p><strong>Parameters</strong></p> <ul> <li><code>dt</code> <code>number</code> — Seconds since the last call.</li> </ul> <h2 id="shown-readme">modules/edui.shown/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.shown") -- edui.shown </code></pre> <p>Panels opened to put a file in front of the user: each <code>open</code> is its own closable dock tab around an <code>edui.document</code>, beside whatever the user has open. The panels are transient — closing one releases its document, and no saved arrangement records them. A panel holding unsaved edits marks its tab and asks the user before it closes. The <code>show</code> toolbox is the agent surface over this module.</p> <p>Usage: local edui.shown = require("@builtin::editor.edui.shown")</p> <h2 id="shown-available">modules/edui.shown/available</h2> <pre><code class="language-lua">available(): boolean </code></pre> <p>Whether this engine has an editor dock to show into — false in a headless engine and under the runtime profile.</p> <h2 id="shown-close">modules/edui.shown/close</h2> <pre><code class="language-lua">close(id: string): (boolean, string?) </code></pre> <p>Close a shown panel — the same as its tab's close button. A panel holding unsaved edits stays open and the user is asked to save or discard them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code></li> </ul> <pre><code class="language-lua">local closed, pending = edui.shown.close("show:3") </code></pre> <h2 id="shown-get">modules/edui.shown/get</h2> <pre><code class="language-lua">get(id: string): ShownRow? </code></pre> <p>The row for one shown panel, or nil when <code>id</code> is not open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code></li> </ul> <h2 id="shown-list">modules/edui.shown/list</h2> <pre><code class="language-lua">list(): { ShownRow } </code></pre> <p>Every open shown panel, in the order opened. <code>dirty</code> marks unsaved edits the user made in it.</p> <h2 id="shown-open">modules/edui.shown/open</h2> <pre><code class="language-lua">open(path: string, opts: { line: number?, title: string? }?): string? </code></pre> <p>Open <code>path</code> in a new dock tab and bring it to the front. Every call makes its own tab, including for a path already shown.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — A VFS path (<code>edui.shown.resolve</code> turns an asset reference into one).</li> <li><code>opts</code> <code>{ line: number?, title: string? }?</code> <em>(optional)</em> — <code>{ line?, title? }</code> — the line code opens at, and the tab title (the file name when omitted).</li> </ul> <pre><code class="language-lua">local id = edui.shown.open("/zero/source/notes.md", { title = "Notes" }) </code></pre> <h2 id="shown-resolve">modules/edui.shown/resolve</h2> <pre><code class="language-lua">resolve(target: string): string </code></pre> <p>The VFS path <code>target</code> names: itself when something is there, else the source of the asset it names (name, identity or guid). Raises when it names nothing, and raises with the candidates when it names more than one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string</code> — A VFS path or an asset reference.</li> </ul> <pre><code class="language-lua">edui.shown.resolve("hero.bundle") </code></pre> <h2 id="shown-scratchpath">modules/edui.shown/scratchPath</h2> <pre><code class="language-lua">scratchPath(title: string?, ext: string): string </code></pre> <p>An unused scratch path for text with no file behind it, under <code>/zero/source/tmp/show/</code> (never saved, never synced), named from <code>title</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>title</code> <code>string?</code> <em>(optional)</em> — What the text is; slugged into the file name.</li> <li><code>ext</code> <code>string</code> — The file extension, which picks the viewer.</li> </ul> <pre><code class="language-lua">edui.shown.scratchPath("Plan", "md") -- /zero/source/tmp/show/plan-1.md </code></pre> <h2 id="shown-unavailablereason">modules/edui.shown/unavailableReason</h2> <pre><code class="language-lua">unavailableReason(): string? </code></pre> <p>Why this engine has no editor dock to show into right now, or nil when <code>available()</code> is true. Each cause has its own message: a headless boot mounts no editor shell, the runtime profile mounts none either, and a windowed editor still coming up mounts its shell a moment after this module is reachable, so that message tells the caller to call again.</p> <pre><code class="language-lua">if not edui.shown.available() then error(edui.shown.unavailableReason()) end </code></pre> <h2 id="topbar-readme">modules/edui.topbar/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.topbar") -- edui.topbar </code></pre> <p>The editor menu bar on edui — a <code>ui.*</code> ctx-level <code>topPanel</code> above the dock: domain dropdown menus on the left (Scene / Content / Debug), the Window menu that opens/closes edui panels, and the play/pause transport on the right. On a window too narrow for that row (a phone's) the bar folds its menus and view toggles into one overflow menu, and unfolds once the window is wide enough again. Every menu entry names a panel this editor ships and focuses it. It owns one edui app on its own screen, so it composes above the shell's dock and toolstrip.</p> <p>Usage: local edui.topbar = require("@builtin::editor.edui.topbar")</p> <h2 id="topbar-layoutswitch">modules/edui.topbar/layoutSwitch</h2> <pre><code class="language-lua">layoutSwitch(): { label: string, slot: string?, tooltip: string } </code></pre> <p>What the bar's layout switch shows: <code>label</code> is the title of the layout in force, <code>slot</code> the side it stands for ("a", "b", or nil for a layout in neither), and <code>tooltip</code> says what a press does.</p> <pre><code class="language-lua">edui.topbar.layoutSwitch().label </code></pre> <h2 id="topbar-mount">modules/edui.topbar/mount</h2> <pre><code class="language-lua">mount(layerMask: number?): any </code></pre> <p>Mount the menu bar (idempotent). Registers its <code>ui.*</code> screen on the EditorUI layer and does the first build.</p> <p><strong>Parameters</strong></p> <ul> <li><code>layerMask</code> <code>number?</code> <em>(optional)</em> — Optional EditorUI render-layer mask; computed when omitted.</li> </ul> <pre><code class="language-lua">edui.topbar.mount() </code></pre> <h2 id="topbar-openbranchmenu">modules/edui.topbar/openBranchMenu</h2> <pre><code class="language-lua">openBranchMenu() </code></pre> <p>Open the branch menu under the top bar's breadcrumb, reading the world's branches afresh — the route another panel takes to switch branch.</p> <h2 id="topbar-screenname">modules/edui.topbar/screenName</h2> <pre><code class="language-lua">screenName(): string </code></pre> <p>The bar's screen name (for <code>ui.showScreen</code> / capture <code>screen</code>).</p> <h2 id="topbar-unmount">modules/edui.topbar/unmount</h2> <pre><code class="language-lua">unmount() </code></pre> <p>Tear the menu bar down.</p> <h2 id="assetbrowser-readme">modules/edui.widgets.assetBrowser/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.assetBrowser") -- edui.widgets.assetBrowser </code></pre> <p>The generic asset browser — one surface for browsing and for picking. Search (name and type), a scope filter (project / builtin), a type filter built from the live registry, list and grid presentation with adjustable tile size and image previews where the asset is one, over the world's asset registry. Browse mode is the Assets panel's body; picker mode is what a REF field or an Add Component flow opens, filtered to the kinds the target accepts, firing <code>onPick</code> with the chosen record.</p> <p>Usage: local edui.widgets.assetBrowser = require("@builtin::editor.edui.widgets.assetBrowser")</p> <h2 id="assetbrowser-data">modules/edui.widgets.assetBrowser/data</h2> <pre><code class="language-lua">data(): { rows: { AssetRecord }, kinds: { { id: string, count: number } } } </code></pre> <p>The shared registry reading — <code>{ rows, kinds }</code>, fetched on first use and held until <code>invalidate</code>. The capability queries above resolve their per-kind probes against it.</p> <h2 id="assetbrowser-epoch">modules/edui.widgets.assetBrowser/epoch</h2> <pre><code class="language-lua">epoch(): number </code></pre> <p>The browser's view epoch — fold into the host panel's build signature.</p> <h2 id="assetbrowser-instantiablekinds">modules/edui.widgets.assetBrowser/instantiableKinds</h2> <pre><code class="language-lua">instantiableKinds(): { string } </code></pre> <p>The placeable kind ids in the live registry, sorted — each answered by its type's <code>canInstantiate</code> capability. For an accepts list or a label.</p> <h2 id="assetbrowser-invalidate">modules/edui.widgets.assetBrowser/invalidate</h2> <pre><code class="language-lua">invalidate() </code></pre> <p>Drop the shared registry cache; the next build re-reads the registry. Call after authoring/installing content so every open browser sees it.</p> <h2 id="assetbrowser-isinstantiable">modules/edui.widgets.assetBrowser/isInstantiable</h2> <pre><code class="language-lua">isInstantiable(kind: any): boolean </code></pre> <p>Whether assets of <code>kind</code> can be placed into a scene — the asset type's <code>canInstantiate</code> capability, answered once per kind. Drop targets and filters that accept "a placeable asset" resolve the question here, so every surface answers it the same way.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="assetbrowser-make">modules/edui.widgets.assetBrowser/make</h2> <pre><code class="language-lua">make(W: any): (any) -> any </code></pre> <p>Bind the browser builder to the widgets barrel <code>W</code>. The barrel calls this once and exposes the result as <code>edui.assetBrowser</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em> — The <code>edui.widgets</code> table.</li> </ul> <p><strong>Returns</strong> <code>any</code> — <code>assetBrowser(o) -> widget</code>.</p> <h2 id="assetbrowser-notemutation">modules/edui.widgets.assetBrowser/noteMutation</h2> <pre><code class="language-lua">noteMutation(path: string, kind: string) </code></pre> <p>What the browser does with one change under <code>/zero/source/</code>: the callback its <code>vfs.watch</code> runs, for a caller that reports a change itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The changed path.</li> <li><code>kind</code> <code>string</code> — <code>"write"</code> or <code>"remove"</code>.</li> </ul> <h2 id="assetbrowser-previewsrc">modules/edui.widgets.assetBrowser/previewSrc</h2> <pre><code class="language-lua">previewSrc(path: any): string? </code></pre> <p>The image source that pictures an asset — its own file when the path is an image, else the preview/source file inside its folder. Nil when nothing drawable is found. Cached per path; <code>M.invalidate()</code> clears it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="assetbrowser-showsubassets">modules/edui.widgets.assetBrowser/showSubassets</h2> <pre><code class="language-lua">showSubassets(shown: boolean) </code></pre> <p>Show or hide private subassets in every open browser; the registry is re-read the next time a browser builds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>shown</code> <code>boolean</code> — True to list private subassets as well.</li> </ul> <h2 id="assetbrowser-subassetsshown">modules/edui.widgets.assetBrowser/subassetsShown</h2> <pre><code class="language-lua">subassetsShown(): boolean </code></pre> <p>Whether the listing includes private subassets — the interiors of composite assets such as a bundle's meshes and materials.</p> <h2 id="controls-readme">modules/edui.widgets.controls/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.controls") -- edui.widgets.controls </code></pre> <p>edui's OWN interactive controls, drawn on the <code>ui.*</code> canvas substrate (paint commands + pointer/key/scroll events) rather than the generic <code>ui.*</code> widgets — so an editor control looks and behaves like an editor control, independent of general UI, and enforces its type. A number is a drag-scrub field that only ever holds a number (drag to scrub, scroll to nudge, double-click to type digits — letters can't enter); a bool is a toggle switch. <code>Controls.make(W)</code> binds them to the widgets barrel.</p> <p>Usage: local edui.widgets.controls = require("@builtin::editor.edui.widgets.controls")</p> <h2 id="controls-make">modules/edui.widgets.controls/make</h2> <pre><code class="language-lua">make(W: any): any </code></pre> <p>Bind the canvas controls to the widgets barrel <code>W</code>. Returns <code>{ numberField, boolToggle }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-readme">modules/edui.widgets.document/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.document") -- edui.widgets.document </code></pre> <p>One open document in an editor panel: a VFS file or asset shown in the viewer its kind calls for, editable where it is text, and following the file as it changes on disk. The Code panel holds one document; every panel <code>edui.shown</code> opens holds another. A document owns its state and prefixes every element id with its own <code>id</code>, so any number coexist. Viewers are modes: <code>registerMode</code> adds one, and a document opens a path in the highest-priority mode whose <code>match</code> claims it.</p> <p>Usage: local edui.widgets.document = require("@builtin::editor.edui.widgets.document")</p> <h2 id="document-conflictbar">modules/edui.widgets.document/_conflictBar</h2> <pre><code class="language-lua">_conflictBar(self: any, P: any): any? </code></pre> <p>The bar over the body while the open file moved on disk under unsaved edits or was removed, or nil.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>P</code> <code>any</code> <em>(optional)</em> — The palette.</li> </ul> <h2 id="document-watch">modules/edui.widgets.document/_watch</h2> <pre><code class="language-lua">_watch(self: any) </code></pre> <p>Watch the open path for writes and removals.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-build">modules/edui.widgets.document/build</h2> <pre><code class="language-lua">build(self: any, P: any): any </code></pre> <p>The panel content: a toolbar (name, path, status, the mode's items, the owner's extras) over the mode's body; an empty panel while nothing is open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>P</code> <code>any</code> <em>(optional)</em> — The palette (<code>edui.palette()</code>).</li> </ul> <h2 id="document-close">modules/edui.widgets.document/close</h2> <pre><code class="language-lua">close(self: any) </code></pre> <p>Close the open path: stop watching it, release the mode's resources, and empty the document.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-codebody">modules/edui.widgets.document/codeBody</h2> <pre><code class="language-lua">codeBody(doc: any, P: any): { any } </code></pre> <p>The code editor with its diagnostics strip and context menu, for a mode's body: line numbers, highlighting for the document's language, folding and hover inspection for Luau, and a context menu with Go to Definition and Inspect, every element keyed under the document.</p> <p><strong>Parameters</strong></p> <ul> <li><code>doc</code> <code>any</code> <em>(optional)</em> — The document the mode is building.</li> <li><code>P</code> <code>any</code> <em>(optional)</em> — The palette (<code>edui.palette()</code>).</li> </ul> <pre><code class="language-lua">return { edui.document.saveButton(doc) }, edui.document.codeBody(doc, P) </code></pre> <h2 id="document-conflictstate">modules/edui.widgets.document/conflictState</h2> <pre><code class="language-lua">conflictState(self: any): string? return self.conflict end </code></pre> <p>"changed" while the file moved under unsaved edits, "removed" while it is gone from disk, nil otherwise.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-dirty">modules/edui.widgets.document/dirty</h2> <pre><code class="language-lua">dirty(self: any): boolean return self.buf ~= self.savedText end </code></pre> <p>Whether the buffer holds edits the file does not.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-dispose">modules/edui.widgets.document/dispose</h2> <pre><code class="language-lua">dispose(self: any) </code></pre> <p>Release everything the document holds — what a closing panel calls.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-healthepoch">modules/edui.widgets.document/healthEpoch</h2> <pre><code class="language-lua">healthEpoch(): number </code></pre> <p>Moves whenever any path's recorded health changes — fold into a build signature that renders health badges.</p> <h2 id="document-istextbytes">modules/edui.widgets.document/isTextBytes</h2> <pre><code class="language-lua">isTextBytes(bytes: string): boolean </code></pre> <p>Whether <code>bytes</code> are text a document can edit: valid UTF-8 holding no NUL byte. What a file holds decides it, whatever its name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>string</code> — A file's contents.</li> </ul> <pre><code class="language-lua">edui.document.isTextBytes(vfs.read("/zero/source/.zmignore") or "") </code></pre> <h2 id="document-istextfile">modules/edui.widgets.document/isTextFile</h2> <pre><code class="language-lua">isTextFile(path: string): boolean </code></pre> <p>Whether the file at <code>path</code> opens as editable text, read from what it holds (<code>isTextBytes</code>). A directory, or a path holding no file, is not.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — A VFS path.</li> </ul> <pre><code class="language-lua">edui.document.isTextFile("/zero/source/a/init.luau") -- true </code></pre> <h2 id="document-keepmine">modules/edui.widgets.document/keepMine</h2> <pre><code class="language-lua">keepMine(self: any) </code></pre> <p>Keep the unsaved edits over the file's new contents; Save writes them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-key">modules/edui.widgets.document/key</h2> <pre><code class="language-lua">key(self: any, suffix: string): string </code></pre> <p>The element id / callback key <code>suffix</code> under this document.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>suffix</code> <code>string</code> — What the id names within the document.</li> </ul> <h2 id="document-modename">modules/edui.widgets.document/modeName</h2> <pre><code class="language-lua">modeName(self: any): string? return if self.mode ~= nil then self.mode.name else nil end </code></pre> <p>The name of the mode showing the open path, or nil when nothing is open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-new">modules/edui.widgets.document/new</h2> <pre><code class="language-lua">new(opts: DocOpts): any </code></pre> <p>A new, empty document. <code>id</code> prefixes every element id and callback key it builds; <code>panelId</code> is the dock panel it draws in (defaults to <code>id</code>); <code>onChange</code> runs whenever its state moves outside an interaction (a disk change, a preview coming live, a <code>setText</code> that moves <code>dirty()</code>) so the panel can rebuild and mark its tab; <code>startInSource</code> opens markdown as source rather than rendered; <code>toolbarExtras</code> returns items appended to its toolbar.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>DocOpts</code> — <code>{ id, panelId?, onChange?, startInSource?, toolbarExtras? }</code>.</li> </ul> <pre><code class="language-lua">local doc = edui.document.new{ id = "notes" } </code></pre> <h2 id="document-open">modules/edui.widgets.document/open</h2> <pre><code class="language-lua">open(self: any, path: string, line: number?): boolean </code></pre> <p>Open <code>path</code>: text is read into the buffer, the mode is picked, a .luau file is LSP-checked, and the file is watched. With <code>line</code>, code lands on that line.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>path</code> <code>string</code> — A VFS path — a file, or an asset folder.</li> <li><code>line</code> <code>number?</code> <em>(optional)</em> — The source line to reveal.</li> </ul> <h2 id="document-path">modules/edui.widgets.document/path</h2> <pre><code class="language-lua">path(self: any): string? return self.filePath end </code></pre> <p>The open path, or nil when nothing is open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-pathhealth">modules/edui.widgets.document/pathHealth</h2> <pre><code class="language-lua">pathHealth(path: string): number? </code></pre> <p>The error count from <code>path</code>'s last LSP check in any document, or nil when no document has checked it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — A VFS path.</li> </ul> <h2 id="document-registermode">modules/edui.widgets.document/registerMode</h2> <pre><code class="language-lua">registerMode(mode: Mode) </code></pre> <p>Register a viewer mode, or replace the one of the same name. <code>match</code> returns a priority for a path it claims (nil otherwise); a document opens a path in the highest. <code>build(doc, P)</code> returns the toolbar items and the body children. <code>open</code> / <code>dispose</code> bracket the mode's own resources, kept in <code>doc.modeState</code>; a suspended document releases them through <code>dispose</code> and takes them again through <code>open</code> as it resumes. <code>onDisk(doc, path, kind)</code> replaces the text reaction to a watched write or removal.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>Mode</code> — <code>{ name, match, build, open?, dispose?, onDisk? }</code>.</li> </ul> <pre><code class="language-lua">edui.document.registerMode{ name = "csv", match = function(_, i) return i.ext == "csv" and 25 or nil end, build = function(doc, P) return {}, {} end } </code></pre> <h2 id="document-reload">modules/edui.widgets.document/reload</h2> <pre><code class="language-lua">reload(self: any) </code></pre> <p>Drop the unsaved edits and take the file as it is on disk; with the file gone, the buffer goes back to the text last loaded or saved.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-resume">modules/edui.widgets.document/resume</h2> <pre><code class="language-lua">resume(self: any) </code></pre> <p>Pick a suspended document back up: the mode takes its resources again, the file is watched again, and the file is read as it is now — a clean buffer adopts what changed on disk meanwhile, one with unsaved edits keeps them and raises the "changed" conflict, and a file removed meanwhile raises "removed".</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-save">modules/edui.widgets.document/save</h2> <pre><code class="language-lua">save(self: any): (boolean, string?) </code></pre> <p>Write the buffer to the file and re-check it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-savebutton">modules/edui.widgets.document/saveButton</h2> <pre><code class="language-lua">saveButton(doc: any): any </code></pre> <p>The Save button text modes share, for a mode's toolbar items: writes the document's buffer, keyed <code><id>:save</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>doc</code> <code>any</code> <em>(optional)</em> — The document the mode is building.</li> </ul> <pre><code class="language-lua">return { edui.document.saveButton(doc) }, edui.document.codeBody(doc, P) </code></pre> <h2 id="document-settext">modules/edui.widgets.document/setText</h2> <pre><code class="language-lua">setText(self: any, text: string) </code></pre> <p>Replace the buffer — what the editor's onChange does as the user types. A change that moves <code>dirty()</code> tells the owner through <code>onChange</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>text</code> <code>string</code></li> </ul> <h2 id="document-showdiff">modules/edui.widgets.document/showDiff</h2> <pre><code class="language-lua">showDiff(self: any) </code></pre> <p>Show how the unsaved edits differ from the file on disk, in the Code panel's diff view.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-signature">modules/edui.widgets.document/signature</h2> <pre><code class="language-lua">signature(self: any): string </code></pre> <p>What a panel folds into its no-change gate: moves on every state change and every keystroke.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-suspend">modules/edui.widgets.document/suspend</h2> <pre><code class="language-lua">suspend(self: any) </code></pre> <p>Set the document aside while its panel is closed: stop watching the file and release the mode's resources (a live preview), keeping the path, the buffer and the text last loaded or saved. <code>resume</code> picks it back up.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-text">modules/edui.widgets.document/text</h2> <pre><code class="language-lua">text(self: any): string return self.buf end </code></pre> <p>The buffer: the open text with the user's edits.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="document-touch">modules/edui.widgets.document/touch</h2> <pre><code class="language-lua">touch(self: any) </code></pre> <p>Record a state change and tell the owner, which rebuilds the panel.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="entitypicker-readme">modules/edui.widgets.entityPicker/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.entityPicker") -- edui.widgets.entityPicker </code></pre> <p>The entity picker — the Hierarchy's tree shape wherever an entity is chosen. A search field over a windowed, expandable entity tree (collapsed by default, so skeleton bones and other deep noise stay behind their roots until unfolded or matched), firing <code>onPick</code> with the chosen entity's id. What a REF field opens instead of a flat dump of every entity in the world.</p> <p>Usage: local edui.widgets.entityPicker = require("@builtin::editor.edui.widgets.entityPicker")</p> <h2 id="entitypicker-epoch">modules/edui.widgets.entityPicker/epoch</h2> <pre><code class="language-lua">epoch(): number </code></pre> <p>The picker's view epoch — fold into the host panel's build signature.</p> <h2 id="entitypicker-make">modules/edui.widgets.entityPicker/make</h2> <pre><code class="language-lua">make(W: any): (any) -> any </code></pre> <p>Bind the picker builder to the widgets barrel <code>W</code>. The barrel calls this once and exposes the result as <code>edui.entityPicker</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em> — The <code>edui.widgets</code> table.</li> </ul> <p><strong>Returns</strong> <code>any</code> — <code>entityPicker(o) -> widget</code>.</p> <h2 id="fields-readme">modules/edui.widgets.fields/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.fields") -- edui.widgets.fields </code></pre> <p>Inspector field rows for edui — a two-column (label + control) row whose control is chosen by the field's declared TYPE (from the component's backing schema), falling back to the value's shape when no type is given. The registry maps: number → numeric field, bool → checkbox, string → text field, enum → a dropdown of its members, color → a colour picker, <code>{x,y,z}</code> → a vec3 triple, a quaternion → an editable euler (X/Y/Z degrees), an asset slot → a live-asset chip, a nested table → a recursing disclosure. Each control wraps the matching <code>ui.*</code> widget and carries the author's <code>onChange</code> closure, so a field writes straight back to the live component. <code>schemaField</code> builds the row from a Field descriptor instead: its kind and constraint pick the control, a struct or list edits in place, and a vec2 is an axis pair. <code>Fields.make(W)</code> binds these to the widgets barrel; the barrel exposes <code>edui.field</code> / <code>edui.schemaField</code> / <code>edui.section</code>.</p> <p>Usage: local edui.widgets.fields = require("@builtin::editor.edui.widgets.fields")</p> <h2 id="fields-epoch">modules/edui.widgets.fields/epoch</h2> <pre><code class="language-lua">epoch(): number </code></pre> <p>The field layer's interaction epoch — moves on every dropdown / nested-table / picker open or close, and on every view mutation inside an open asset or entity picker. Fold it into a panel's build signature so those interactions repaint through a no-change gate.</p> <h2 id="fields-field">modules/edui.widgets.fields/field</h2> <pre><code class="language-lua">field(o: any): any </code></pre> <p>An inspector field row. <code>o = { label, value, kind?, options?, optionLabels?, accepts?, onChange?, readOnly?, key }</code>. <code>kind</code> (from the component schema) selects the control; without it the control is inferred from the value's shape. An enum shows each of its <code>options</code> as its entry in <code>optionLabels</code> (text or an <code>i18n.text</code> value) when it has one, and <code>onChange</code> receives the option itself. An editable leaf fires <code>onChange(newValue)</code> (vec3/quat fire the full new table).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">edui.field{ label = "kind", value = c.kind, kind = "enum", options = {"a","b"}, onChange = set, key = "k" } </code></pre> <h2 id="fields-make">modules/edui.widgets.fields/make</h2> <pre><code class="language-lua">make(W: any): any </code></pre> <p>Bind the field builders to the widgets barrel <code>W</code>. Returns <code>{ field, section, schemaField, subErrors }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em> — The edui widgets barrel.</li> </ul> <h2 id="fields-openeditorssignature">modules/edui.widgets.fields/openEditorsSignature</h2> <pre><code class="language-lua">openEditorsSignature(): string </code></pre> <p>Text that moves whenever the asset shown in any open inline editor moves (<code>edui.inspector.signature</code> of each). Fold it into a panel's build signature so an editor follows an undo or a peer's write.</p> <pre><code class="language-lua">parts[#parts + 1] = Fields.openEditorsSignature() </code></pre> <h2 id="fields-releaseeditors">modules/edui.widgets.fields/releaseEditors</h2> <pre><code class="language-lua">releaseEditors(prefix: string) </code></pre> <p>Close every inline editor whose field key is <code>prefix</code> or nested under it, and forget what was open or refused inside them — what a panel does for the subject it stops showing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>prefix</code> <code>string</code> — A field key prefix, e.g. a component card's <code>insp:e:<id></code>.</li> </ul> <pre><code class="language-lua">Fields.releaseEditors("insp:e:" .. previousId) </code></pre> <h2 id="fields-section">modules/edui.widgets.fields/section</h2> <pre><code class="language-lua">section(o: any): any </code></pre> <p>A collapsible inspector section, drawn as a CARD — a bordered, rounded container whose clickable header (chevron + optional icon + uppercase title, with an optional trailing widget) sits over a body shown while open. <code>o = { title, open, onToggle, children, key, headerRight?, icon?, muted? }</code>. <code>headerRight</code> is a SIBLING of the clickable header region, so its click isn't swallowed by the header toggle.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">edui.section{ title = "Transform", icon = tfIcon, open = st, onToggle = t, children = rows } </code></pre> <h2 id="inlinemarkdown-readme">modules/edui.widgets.inlineMarkdown/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.inlineMarkdown") -- edui.widgets.inlineMarkdown </code></pre> <p>Inline markdown of one run of text as styled segments — the <code>{ text, color, monospace?, italic?, background? }</code> list a <code>richText</code>, a <code>tooltip</code> and a code editor's <code>hoverText</code> draw. Reads <code>code</code>, <strong>bold</strong>, <em>italic</em> and <a href="target">text</a>; everything else, newlines included, passes through as plain text.</p> <p>Usage: local edui.widgets.inlineMarkdown = require("@builtin::editor.edui.widgets.inlineMarkdown")</p> <h2 id="inlinemarkdown-segments">modules/edui.widgets.inlineMarkdown/segments</h2> <pre><code class="language-lua">segments(text: string, colors: Colors): { any } </code></pre> <p>The segments <code>text</code> reads as. Bold draws in <code>colors.strong</code> (a segment carries no weight), inline code in <code>colors.code</code> on <code>colors.codeBackground</code> in the monospace face, a link's label in <code>colors.link</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — One run of markdown text.</li> <li><code>colors</code> <code>Colors</code> — The colours each kind of run draws in.</li> </ul> <pre><code class="language-lua">local segs = InlineMarkdown.segments("Returns the `Playback`.", colors) </code></pre> <h2 id="livepreview-readme">modules/edui.widgets.livePreview/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.livePreview") -- edui.widgets.livePreview </code></pre> <p>The ONE live preview session, following the asset selection. Every surface that shows the selected asset live — the Preview panel, the Inspector's Preview section — reads this module instead of holding its own rig, so one camera and one subject serve them all. The session starts when the asset selection lands, swaps with it, and tears down when it clears. <code>subscribe</code> tells a consumer the session's state moved (started, came live, failed) so it can rebuild; <code>epoch</code> folds that state into a build signature. <code>play</code> replaces the still session with a stage play of the selected effect or graph (<code>proc.stage</code>, <code>effects.stage</code>): a session with a transport (<code>transport</code>, <code>pause</code>, <code>resume</code>, <code>stop</code>, <code>seek</code>), inputs given while it plays (<code>set</code>), and a profile. <code>setMeasure</code> has the graph editor show that play's cost on each node (<code>numbers</code>); a selection that moves disposes the session, and the numbers of a node opened from the graph editor (<code>openInside</code>) stay with the asset it opened.</p> <p>Usage: local edui.widgets.livePreview = require("@builtin::editor.edui.widgets.livePreview")</p> <h2 id="livepreview-assetid">modules/edui.widgets.livePreview/assetId</h2> <pre><code class="language-lua">assetId(): string? return sessionFor end </code></pre> <p>The guid the session (or attempt) is for, or nil.</p> <h2 id="livepreview-dragorbit">modules/edui.widgets.livePreview/dragOrbit</h2> <pre><code class="language-lua">dragOrbit(d: any) </code></pre> <p>Apply a pointer-drag delta as an orbit — the shared handler every live-preview surface hands its <code>onDrag</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>d</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="livepreview-ensure">modules/edui.widgets.livePreview/ensure</h2> <pre><code class="language-lua">ensure() </code></pre> <p>Bring the session in line with the asset selection: a new primary disposes the old rig and assembles one for it, a cleared selection tears down. Idempotent — consumers call it from their builds; the selection subscription below calls it on every change.</p> <h2 id="livepreview-epoch">modules/edui.widgets.livePreview/epoch</h2> <pre><code class="language-lua">epoch(): number return epoch end </code></pre> <p>The monotonic state counter — fold into a build signature so the session coming live is a change the panel's no-change gate can see.</p> <h2 id="livepreview-isstarting">modules/edui.widgets.livePreview/isStarting</h2> <pre><code class="language-lua">isStarting(): boolean return starting end </code></pre> <p>True while a session is assembling.</p> <h2 id="livepreview-kind">modules/edui.widgets.livePreview/kind</h2> <pre><code class="language-lua">kind(): string return sessionKind end </code></pre> <p>The kind of the selected asset once known, else "".</p> <h2 id="livepreview-measuring">modules/edui.widgets.livePreview/measuring</h2> <pre><code class="language-lua">measuring(): boolean return measuring end </code></pre> <p>Whether the graph editor shows the stage play's cost on each node.</p> <h2 id="livepreview-name">modules/edui.widgets.livePreview/name</h2> <pre><code class="language-lua">name(): string return sessionName end </code></pre> <p>The selected asset's display name once known, else "".</p> <h2 id="livepreview-numbers">modules/edui.widgets.livePreview/numbers</h2> <pre><code class="language-lua">numbers(guid: string, path: string): any </code></pre> <p>The cost of the graph asset <code>guid</code> (at <code>path</code>) as the measurement stands: the branch of its cost tree, whose <code>children</code> hold each node by id with <code>ms</code>, <code>firstMs</code>, <code>againMs</code>, <code>checkMs</code>, <code>dispatches</code>, <code>uploads</code>, <code>readbacks</code>. The played graph is read from the stage play; a graph opened from a node (<code>openInside</code>) from the snapshot taken then, at the node's place in the tree. Nil while nothing is measured for it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>guid</code> <code>string</code> — The asset's guid.</li> <li><code>path</code> <code>string</code> — The asset's VFS path.</li> </ul> <h2 id="livepreview-openinside">modules/edui.widgets.livePreview/openInside</h2> <pre><code class="language-lua">openInside(viewedGuid: string, viewedPath: string, nodeId: string, innerGuid: string) </code></pre> <p>Open the graph asset <code>innerGuid</code>, which node <code>nodeId</code> of the graph being viewed (<code>viewedGuid</code>, at <code>viewedPath</code>) stands for, keeping that node's cost: the numbers the inner graph then shows are that instance's.</p> <p><strong>Parameters</strong></p> <ul> <li><code>viewedGuid</code> <code>string</code> — The guid of the graph the node is in.</li> <li><code>viewedPath</code> <code>string</code> — Its VFS path.</li> <li><code>nodeId</code> <code>string</code> — The node's id.</li> <li><code>innerGuid</code> <code>string</code> — The guid of the asset the node is.</li> </ul> <h2 id="livepreview-orbitsession">modules/edui.widgets.livePreview/orbitSession</h2> <pre><code class="language-lua">orbitSession(s: any, d: any) </code></pre> <p>Apply a pointer-drag delta as an orbit of <code>s</code>, a live preview session.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>any</code> <em>(optional)</em></li> <li><code>d</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="livepreview-pause">modules/edui.widgets.livePreview/pause</h2> <pre><code class="language-lua">pause() stagedCall("pause") end </code></pre> <p>Stop the stage play's clock where it is.</p> <h2 id="livepreview-play">modules/edui.widgets.livePreview/play</h2> <pre><code class="language-lua">play(opts: { [string]: any }?) </code></pre> <p>Replace the still session with a stage play of the selected effect or graph, from its first frame. The play loops and its picture is the session's render target.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ entry?, inputs? }</code>: the entry of an effect, and values for the inputs by name.</li> </ul> <pre><code class="language-lua">livePreview.play({ entry = "burst", inputs = { size = 2 } }) </code></pre> <h2 id="livepreview-playable">modules/edui.widgets.livePreview/playable</h2> <pre><code class="language-lua">playable(): boolean </code></pre> <p>Whether the selected asset is one a stage play can be started for: an effect or a graph.</p> <h2 id="livepreview-reason">modules/edui.widgets.livePreview/reason</h2> <pre><code class="language-lua">reason(): string? return failReason end </code></pre> <p>Why the last attempt produced no live session, or nil.</p> <h2 id="livepreview-resetview">modules/edui.widgets.livePreview/resetView</h2> <pre><code class="language-lua">resetView() </code></pre> <p>Back to the framed opening view.</p> <h2 id="livepreview-restart">modules/edui.widgets.livePreview/restart</h2> <pre><code class="language-lua">restart() stagedCall("play") end </code></pre> <p>Play the stage play again from its first frame.</p> <h2 id="livepreview-resume">modules/edui.widgets.livePreview/resume</h2> <pre><code class="language-lua">resume() stagedCall("resume") end </code></pre> <p>Run the stage play from where it stands.</p> <h2 id="livepreview-scrollzoom">modules/edui.widgets.livePreview/scrollZoom</h2> <pre><code class="language-lua">scrollZoom(d: any) </code></pre> <p>Apply a wheel delta as a zoom — the shared handler every live-preview surface hands its <code>onScroll</code>. Wheel-up moves in.</p> <p><strong>Parameters</strong></p> <ul> <li><code>d</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="livepreview-seek">modules/edui.widgets.livePreview/seek</h2> <pre><code class="language-lua">seek(seconds: number) </code></pre> <p>Move the stage play to <code>seconds</code> and hold it there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>seconds</code> <code>number</code></li> </ul> <h2 id="livepreview-session">modules/edui.widgets.livePreview/session</h2> <pre><code class="language-lua">session(): any return session end </code></pre> <p>The live session for the selected asset, or nil while there is none (nothing selected, still starting, or the type has no live path).</p> <h2 id="livepreview-set">modules/edui.widgets.livePreview/set</h2> <pre><code class="language-lua">set(name: string, value: any) </code></pre> <p>Give the stage play an input's value while it plays.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The input's name.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — A value, or a path over time <code>{ { t, value }, ... }</code>.</li> </ul> <h2 id="livepreview-setentry">modules/edui.widgets.livePreview/setEntry</h2> <pre><code class="language-lua">setEntry(entry: string) </code></pre> <p>Start the stage play again on another entry of the effect, keeping the inputs given.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entry</code> <code>string</code></li> </ul> <h2 id="livepreview-setmeasure">modules/edui.widgets.livePreview/setMeasure</h2> <pre><code class="language-lua">setMeasure(on: boolean) </code></pre> <p>Turn the cost shown on the graph editor's nodes on or off.</p> <p><strong>Parameters</strong></p> <ul> <li><code>on</code> <code>boolean</code></li> </ul> <h2 id="livepreview-staged">modules/edui.widgets.livePreview/staged</h2> <pre><code class="language-lua">staged(): boolean return staged ~= nil end </code></pre> <p>Whether the session is a stage play.</p> <h2 id="livepreview-stop">modules/edui.widgets.livePreview/stop</h2> <pre><code class="language-lua">stop() stagedCall("stop") end </code></pre> <p>Stop the stage play and show nothing; <code>restart</code> plays it again from its first frame.</p> <h2 id="livepreview-subscribe">modules/edui.widgets.livePreview/subscribe</h2> <pre><code class="language-lua">subscribe(fn: () -> ()) </code></pre> <p>Register a consumer's rebuild closure, called whenever the session's state moves. Registration is for the VM lifetime.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> ()</code></li> </ul> <pre><code class="language-lua">livePreview.subscribe(function() edui.shell.refreshPanel("preview") end) </code></pre> <h2 id="livepreview-transport">modules/edui.widgets.livePreview/transport</h2> <pre><code class="language-lua">transport(): any </code></pre> <p>What the stage play stands at: <code>{ playing, stopped, time, frame, frames, fps, ends, entry, inputs }</code>, or nil while there is no stage play. <code>frames</code> is the length of the longest graph that ends, or 1.</p> <h2 id="livepreview-verdicts">modules/edui.widgets.livePreview/verdicts</h2> <pre><code class="language-lua">verdicts(guid: string, path: string): { [string]: { string } }? </code></pre> <p>What the measurement found wrong at the nodes of the graph asset <code>guid</code> (at <code>path</code>): for each node of it that has a verdict, its id and the verdict lines (time paid again with the cause, waits by site). A call node carries the verdicts of the nodes inside it. Nil while nothing is measured for it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>guid</code> <code>string</code> — The asset's guid.</li> <li><code>path</code> <code>string</code> — The asset's VFS path.</li> </ul> <h2 id="livepreview-zoomby">modules/edui.widgets.livePreview/zoomBy</h2> <pre><code class="language-lua">zoomBy(factor: number) </code></pre> <p>Zoom by a plain factor (toolbar buttons).</p> <p><strong>Parameters</strong></p> <ul> <li><code>factor</code> <code>number</code></li> </ul> <h2 id="livepreview-zoomsession">modules/edui.widgets.livePreview/zoomSession</h2> <pre><code class="language-lua">zoomSession(s: any, d: any) </code></pre> <p>Apply a wheel delta as a zoom of <code>s</code>, a live preview session. Wheel-up moves in.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>any</code> <em>(optional)</em></li> <li><code>d</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="newasset-readme">modules/edui.widgets.newAsset/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.newAsset") -- edui.widgets.newAsset </code></pre> <p>What the Files panel's "New asset" flow offers and how it creates it. An asset kind that cannot be created bare, because <code>asset.create</code> needs it bound to another asset (a <code>data</code> asset is a set of values for one <code>dataType</code>), is offered once per asset it can be bound to, so the choice that creation needs is made in the same menu that names the kind. A kind that creates bare is offered as it is.</p> <p>Usage: local edui.widgets.newAsset = require("@builtin::editor.edui.widgets.newAsset")</p> <h2 id="newasset-binding">modules/edui.widgets.newAsset/binding</h2> <pre><code class="language-lua">binding(kind: string): { param: string, from: string }? </code></pre> <p>The binding a kind needs, or nil when it creates bare.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string</code> — An asset kind name.</li> </ul> <pre><code class="language-lua">local b = NewAsset.binding("data") </code></pre> <h2 id="newasset-candidates">modules/edui.widgets.newAsset/candidates</h2> <pre><code class="language-lua">candidates(kind: string): { string } </code></pre> <p>The identities of every asset a kind can be bound to, sorted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string</code> — A kind that has a binding.</li> </ul> <pre><code class="language-lua">for _, id in ipairs(NewAsset.candidates("data")) do print(id) end </code></pre> <h2 id="newasset-choices">modules/edui.widgets.newAsset/choices</h2> <pre><code class="language-lua">choices(kinds: { { key: string, label: string } }): { Choice } </code></pre> <p>The menu rows for a list of creatable kinds: one row for each kind that creates bare, and for a kind needing a binding one row per asset it can be bound to, or a single disabled row saying what is missing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kinds</code> <code>{ { key: string, label: string } }</code> — Array of <code>{ key, label }</code> kind rows.</li> </ul> <pre><code class="language-lua">local rows = NewAsset.choices({ { key = "data", label = "data" } }) </code></pre> <h2 id="newasset-create">modules/edui.widgets.newAsset/create</h2> <pre><code class="language-lua">create(kind: string, name: string, folder: string?, bound: string?): any </code></pre> <p>Create an asset of a kind through <code>asset.create</code>, with the field that binds it to another asset when the kind needs one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string</code> — The asset kind.</li> <li><code>name</code> <code>string</code> — The new asset's name.</li> <li><code>folder</code> <code>string?</code> <em>(optional)</em> — The subfolder of the world's source to create it in, or nil for the root.</li> <li><code>bound</code> <code>string?</code> <em>(optional)</em> — The identity of the asset it is bound to (required when the kind has a binding).</li> </ul> <pre><code class="language-lua">NewAsset.create("data", "goblin", nil, "enemy") </code></pre> <h2 id="newasset-kindsboundto">modules/edui.widgets.newAsset/kindsBoundTo</h2> <pre><code class="language-lua">kindsBoundTo(assetType: string): { string } </code></pre> <p>The kinds that are created from an asset of the given type.</p> <p><strong>Parameters</strong></p> <ul> <li><code>assetType</code> <code>string</code> — The type of the asset the kinds are bound to.</li> </ul> <pre><code class="language-lua">NewAsset.kindsBoundTo("dataType") -- { "data" } </code></pre> <h2 id="nodegraph-readme">modules/edui.widgets.nodeGraph/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.nodeGraph") -- edui.widgets.nodeGraph </code></pre> <p>A node graph: titled nodes with output ports and input ports, a value on every input port that has no wire into it, wires between the ports, and everything done with a pointer. The whole graph is one <code>canvas</code>, painted from the palette's own tokens, so a graph of hundreds of nodes is one widget; it reports where inside itself a pointer is, which is what hit-testing a port, a value, a node or a wire needs. Drag the background to pan, scroll to zoom about the pointer, drag a node to move it, drag from a port to another port to connect, drag a connected input port away to pick its wire up, click a node or a wire to select it, press Delete to remove the selection. Click a value to edit it where it stands: a number or a string takes typing, a number also scrubs when dragged, a switch flips, a choice opens its options. A node given no position is placed by a layered layout over the wires. The widget draws and reports; the owner holds the graph and applies each change it is told of. <code>NodeGraph.make(W)</code> binds it to the widgets barrel; the barrel exposes <code>edui.nodeGraph</code>.</p> <p>Usage: local edui.widgets.nodeGraph = require("@builtin::editor.edui.widgets.nodeGraph")</p> <h2 id="nodegraph-layout">modules/edui.widgets.nodeGraph/layout</h2> <pre><code class="language-lua">layout(nodes: { any }, edges: { any }): { [string]: { x: number, y: number } } </code></pre> <p>A layered layout over the wires: each node sits one column right of the furthest of its producers, and the rows of a column follow the average row of the nodes feeding it, so wires run left to right with few crossings. A wire that closes a cycle is ignored. <code>nodes</code> is <code>{ { id, ... } }</code> and <code>edges</code> is <code>{ { from, to } }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>nodes</code> <code>{ any }</code></li> <li><code>edges</code> <code>{ any }</code></li> </ul> <h2 id="nodegraph-make">modules/edui.widgets.nodeGraph/make</h2> <pre><code class="language-lua">make(W: any): any </code></pre> <p>Bind the node graph to the widgets barrel <code>W</code>. Returns <code>{ nodeGraph, layout, nodeSize, view }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="nodegraph-nodegraph">modules/edui.widgets.nodeGraph/nodeGraph</h2> <pre><code class="language-lua">nodeGraph(o: any): any </code></pre> <p>A node graph.</p> <p><code>o = { key, nodes, edges, selected?, height?, onSelect?, onMove?, onConnect?, onDisconnect?, onDelete?, onOpen?, onDropOnEmpty?, onField?, redraw?, toolbar? }</code>.</p> <ul> <li><code>nodes</code>: <code>{ { id, title?, sub?, heat?, flag?, tone?, outputs = { { name, type? } }, inputs = { { name, type?, field?, extra? } }, fields? = { field }, x?, y?, open? } }</code>. <code>tone</code> is <code>"input"</code>, <code>"output"</code>, <code>"graph"</code>, <code>"asset"</code> or nil; <code>open</code> marks a node a double-click opens. <code>sub</code> is the small text on the right of the header, and <code>heat</code> (0 to 1) is a bar along the foot of the header, longer for a costlier node and green, amber or red as it grows. A node with a <code>flag</code> (a short text such as <code>"!"</code>) carries it in a red mark at the right end of the header.</li> <li>a <code>field</code> is <code>{ name, label?, kind, value, options?, min?, max?, int? }</code>; <code>kind</code> is <code>"number"</code>, <code>"string"</code>, <code>"bool"</code>, <code>"enum"</code>, <code>"vec3"</code>, <code>"color"</code> or <code>"asset"</code>. On an input port it is the value that drives the port while nothing is wired into it; in <code>fields</code> it is a row of the node's own. A port flagged <code>extra</code> is one of the node's unset values, which fold under "more" past the first few.</li> <li><code>edges</code>: <code>{ { from, fromPort, to, toPort } }</code>; an input takes one wire.</li> <li><code>onField(id, name, value)</code> is told of each value a person sets; <code>value</code> is a whole vec3 for a vec3.</li> <li><code>onSelect(id?)</code>, <code>onMove(id, x, y)</code> (once, when a drag ends), <code>onConnect(from, fromPort, to, toPort)</code>, <code>onDisconnect(to, toPort, from, fromPort)</code>, <code>onDelete(id)</code>, <code>onOpen(id)</code>, <code>onDropOnEmpty(from, fromPort, x, y)</code>.</li> <li><code>redraw()</code> is called after a change to the view alone (pan, zoom, a drag in flight), so the owner can rebuild its panel.</li> <li><code>toolbar</code> is extra widgets placed after the built-in view controls.</li> </ul> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>any</code> <em>(optional)</em> — The graph's state and handlers.</li> </ul> <pre><code class="language-lua">edui.nodeGraph{ key = "g:graph", nodes = nodes, edges = edges, selected = id, onSelect = select } </code></pre> <h2 id="nodegraph-nodesize">modules/edui.widgets.nodeGraph/nodeSize</h2> <pre><code class="language-lua">nodeSize(n: any, wired: { [string]: boolean }?, expanded: boolean?): (number, number) </code></pre> <p>The size of a node in graph units. <code>n</code> is <code>{ id, title, sub?, outputs, inputs, fields? }</code>; <code>wired</code> names the input ports that have a wire in (<code>"<id>\1<port>"</code>), and <code>expanded</code> whether the node shows every input.</p> <p><strong>Parameters</strong></p> <ul> <li><code>n</code> <code>any</code> <em>(optional)</em></li> <li><code>wired</code> <code>{ [string]: boolean }?</code> <em>(optional)</em></li> <li><code>expanded</code> <code>boolean?</code> <em>(optional)</em></li> </ul> <h2 id="nodegraph-view">modules/edui.widgets.nodeGraph/view</h2> <pre><code class="language-lua">view(key: string): any </code></pre> <p>Where the view stands for the widget keyed <code>key</code>: <code>{ panX, panY, zoom, positions }</code>. Positions are in graph units.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code></li> </ul> <h2 id="timeline-readme">modules/edui.widgets.timeline/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.timeline") -- edui.widgets.timeline </code></pre> <p>A timeline: a transport, a ruler ticked frame by frame, and a track a playhead runs along, with the played range as a region whose ends are dragged and the frames already cooked filled in behind it. The track is one <code>canvas</code> — the only widget that reports where inside itself a pointer is, which is what click-to-seek and dragging a handle need — painted from the palette's own tokens. <code>Timeline.make(W)</code> binds it to the widgets barrel; the barrel exposes <code>edui.timeline</code>.</p> <p>Usage: local edui.widgets.timeline = require("@builtin::editor.edui.widgets.timeline")</p> <h2 id="timeline-make">modules/edui.widgets.timeline/make</h2> <pre><code class="language-lua">make(W: any): any </code></pre> <p>Bind the timeline to the widgets barrel <code>W</code>. Returns <code>{ timeline }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="timeline-timeline">modules/edui.widgets.timeline/timeline</h2> <pre><code class="language-lua">timeline(o: any): any </code></pre> <p>A timeline for a clock something else owns: a transport, a ruler and a track carrying the played range, what is cooked, and a playhead.</p> <p><code>o = { frame, first, last, frames, fps, cookedThrough?, playing?, held?, loop?, realTime?, wallTime?, time?, key, onSeek?, onRange?, onPlay?, onPause?, onLoop?, onWallTime?, onFps? }</code>. Frames count from zero; <code>first</code>/<code>last</code> are the played range inside <code>frames</code>, and <code>cookedThrough</code> is the last frame already cooked. A handler left out takes its control away.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>any</code> <em>(optional)</em> — The timeline's state and handlers.</li> </ul> <pre><code class="language-lua">edui.timeline{ frame = 42, first = 0, last = 149, frames = 150, fps = 30, key = "gen:tl", onSeek = seek } </code></pre> <h2 id="tree-readme">modules/edui.widgets.tree/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets.tree") -- edui.widgets.tree </code></pre> <p>The editor hierarchy tree primitive — flattens a nested node graph into the rows that are actually visible under the current expand state, and renders them as indented, selectable <code>ui.*</code> rows inside a virtualized scroll area. The flatten pass is pure and separately exported, so row order is testable without a rendered frame.</p> <p>Usage: local edui.widgets.tree = require("@builtin::editor.edui.widgets.tree")</p> <h2 id="tree-flatten">modules/edui.widgets.tree/flatten</h2> <pre><code class="language-lua">flatten(o: { [string]: any }): { Row } </code></pre> <p>Flatten <code>o.roots</code> into the ordered list of rows visible under the current expand state — a node's children are included only while that node is expanded. Pure: no <code>ui.*</code> calls, no app required.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code> — <code>{ roots, childrenOf?, idOf?, expanded?, isExpanded? }</code>. <code>expanded</code> is an <code>{ [id] = true }</code> set; <code>isExpanded(node, id) -> boolean</code> overrides it.</li> </ul> <pre><code class="language-lua">local rows = edui.widgets.flattenTree{ roots = roots, expanded = { root = true } } </code></pre> <h2 id="tree-make">modules/edui.widgets.tree/make</h2> <pre><code class="language-lua">make(W: any): (any) -> any </code></pre> <p>Bind the tree builder to a widgets barrel <code>W</code>. The barrel calls this once and exposes the result as <code>edui.tree</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>W</code> <code>any</code> <em>(optional)</em> — The <code>edui.widgets</code> table (supplies palette/text/svgIcon/cbId/merge).</li> </ul> <p><strong>Returns</strong> <code>any</code> — <code>tree(o) -> widget</code>.</p> <h2 id="tree-windowbounds">modules/edui.widgets.tree/windowBounds</h2> <pre><code class="language-lua">windowBounds(total: number, offset: number, viewport: number, rowH: number, </code></pre> <p>The band of a <code>total</code>-row list that exists as widgets: the rows in view plus an overscan on each side, its start snapped to a multiple of the quantum so a small scroll inside the overscan moves nothing.</p> <pre><code class="language-lua">edui.widgets.windowBounds(4000, 0, 440, 22) --> 0, 48 </code></pre> <h2 id="widgets-readme">modules/edui.widgets/README</h2> <pre><code class="language-lua">require("@builtin::editor.edui.widgets") -- edui.widgets </code></pre> <p>Editor-chrome primitives for edui — styled <code>ui.*</code> subtree builders (box/text/icon/iconButton/button/toolbar/panel/divider/spacer). Each reads the active theme's editor tokens so chrome looks like the editor by default, and interactive builders register their closures with the currently-building <code>edui</code> app (<code>edui.current()</code>), so an author passes a closure, not a callback id.</p> <p>Usage: local edui.widgets = require("@builtin::editor.edui.widgets")</p> <h2 id="widgets-button">modules/edui.widgets/button</h2> <pre><code class="language-lua">button(o: { [string]: any }): any </code></pre> <p>A text button. <code>o.text</code>, <code>o.onClick</code>, <code>o.key</code>, <code>o.primary</code>, <code>o.icon</code> (SVG path), <code>o.tooltip</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.button{ text = "Add", primary = true, onClick = onAdd, key = "add" } </code></pre> <h2 id="widgets-divider">modules/edui.widgets/divider</h2> <pre><code class="language-lua">divider(o: { [string]: any }?): any </code></pre> <p>A 1px divider line (horizontal by default).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }?</code> <em>(optional)</em></li> </ul> <h2 id="widgets-group">modules/edui.widgets/group</h2> <pre><code class="language-lua">group(o: { [string]: any }): any </code></pre> <p>A FLAT collapsible group (chevron + uppercase title + optional count), with none of the section card's border/shadow — for grouping a list inside a panel. <code>o.title</code>, <code>o.count?</code>, <code>o.open</code>, <code>o.onToggle</code>, <code>o.children</code>, <code>o.key</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.group{ title = "@builtin", count = 12, open = o, onToggle = t, children = rows } </code></pre> <h2 id="widgets-hbox">modules/edui.widgets/hbox</h2> <pre><code class="language-lua">hbox(o: { [string]: any }): any </code></pre> <p>A horizontal flex container. Forwards <code>onClick</code>, <code>onDoubleClick</code>, <code>onContextMenu</code>, <code>onDrag</code>, <code>dragPayload</code> (with <code>dragLabel</code>), <code>onDrop</code> and <code>tooltip</code> (a string, or styled segments).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.hbox{ gap = 6, align = "center", children = { ... } } </code></pre> <h2 id="widgets-iconbutton">modules/edui.widgets/iconButton</h2> <pre><code class="language-lua">iconButton(o: { [string]: any }): any </code></pre> <p>A compact icon button — a hoverable, optionally-active square holding an SVG icon. <code>o.d</code> (icon path), <code>o.tooltip</code>, <code>o.active</code>, <code>o.onClick</code>, <code>o.key</code>, <code>o.size</code>, <code>o.color</code>, <code>o.id</code> (widget id, e.g. a popup anchor).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.iconButton{ d = eyeIcon, tooltip = "Visible", active = vis, onClick = toggle, key = "vis:"..id } </code></pre> <h2 id="widgets-input">modules/edui.widgets/input</h2> <pre><code class="language-lua">input(o: { [string]: any }): any </code></pre> <p>A flat text-entry field — a styled container holding an optional leading icon and a borderless input, so it reads as one clean field instead of the raw widget's heavy frame. <code>o.text</code>, <code>o.placeholder</code>, <code>o.onChange</code> (live per-keystroke), <code>o.onSubmit</code> (command-line mode: the full line on Enter, buffer cleared, caret kept), <code>o.key</code>, <code>o.icon</code> (SVG path), <code>o.style</code>, <code>o.bare</code> (no container frame at all — the input alone, for a surface like a terminal prompt that draws its own ground).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.input{ placeholder = "Search…", icon = searchPath, onChange = fn, key = "search" } </code></pre> <h2 id="widgets-menu">modules/edui.widgets/menu</h2> <pre><code class="language-lua">menu(o: { [string]: any }): any? </code></pre> <p>A context/action menu — a <code>ui.*</code> popup pinned below an anchor widget or at a window point, holding clickable action items. <code>o.anchorTo</code> (widget id of the anchor, which must render before this in tree order) or <code>o.at</code> (<code>{ x, y }</code> in the window's points — the value an <code>onContextMenu</code> delivers, so a right-click menu opens at the pointer), <code>o.open</code> (bool), <code>o.items</code> (list of <code>{ label, icon?, onClick, danger?, disabled?, hint?, separator?, key? }</code> — a disabled item dims and takes no click; <code>hint</code> renders right-aligned dim text, where a shortcut goes), <code>o.onDismiss</code> (closure, fired on click-outside / Escape, and after an item's <code>onClick</code> runs — a menu closes once it has acted), <code>o.key</code> (id namespace), <code>o.pivot</code>. Returns nil when closed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.menu{ anchorTo = "rowmenu:"..id, open = open, items = {...}, onDismiss = close } edui.menu{ at = clickPoint, open = clickPoint ~= nil, items = {...}, onDismiss = close } </code></pre> <h2 id="widgets-notice">modules/edui.widgets/notice</h2> <pre><code class="language-lua">notice(o: { [string]: any }): any </code></pre> <p>An inline status banner. <code>o.text</code>, <code>o.tone</code> ("info" | "ok" | "error"). edui has no timer, so a transient toast is the caller gating this on/off; this draws the styled line.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.notice{ text = "Saved", tone = "ok" } </code></pre> <h2 id="widgets-panel">modules/edui.widgets/panel</h2> <pre><code class="language-lua">panel(o: { [string]: any }): any </code></pre> <p>A panel frame — an optional title/toolbar header over a body that fills the remaining height. This is the standard dockable-panel shell. <code>o.title</code> (string) OR <code>o.toolbar</code> (a prebuilt toolbar widget); <code>o.body</code> (widget); <code>o.style</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.panel{ title = "Entities", body = tree } </code></pre> <h2 id="widgets-scroll">modules/edui.widgets/scroll</h2> <pre><code class="language-lua">scroll(o: { [string]: any }): any </code></pre> <p>A first-class vertical scroll container filling its parent. <code>o.children</code>, <code>o.height?</code>, <code>o.maxHeight?</code>, <code>o.style?</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.scroll{ children = rows } </code></pre> <h2 id="widgets-spacer">modules/edui.widgets/spacer</h2> <pre><code class="language-lua">spacer(): any </code></pre> <p>A flexible spacer that eats remaining space in a flex row/column.</p> <h2 id="widgets-stat">modules/edui.widgets/stat</h2> <pre><code class="language-lua">stat(o: { [string]: any }): any </code></pre> <p>A dense read-only telemetry row: <code>label</code> on the left, <code>value</code> on the right (mono by default). <code>good</code>/<code>warn</code> numeric thresholds tint the value (higher = worse: ≥warn is danger, ≥good is warn-hue, else ok); an explicit <code>color</code> overrides. For readouts too light to warrant an editable <code>field</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.stat{ label = "RSS", value = "412 MB", good = 400, warn = 800, mono = true } </code></pre> <h2 id="widgets-svgicon">modules/edui.widgets/svgIcon</h2> <pre><code class="language-lua">svgIcon(o: { [string]: any }): any </code></pre> <p>An inline SVG icon. <code>o.d</code> is an SVG path (or full <code><svg></code>); <code>o.size</code>, <code>o.color</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.svgIcon{ d = "<path d='M6 9l6 6 6-6'/>", size = 14 } </code></pre> <h2 id="widgets-switch">modules/edui.widgets/switch</h2> <pre><code class="language-lua">switch(o: { [string]: any }): any </code></pre> <p>A bare toggle switch (pill track + sliding knob) — the switch control on its own, distinct from a checkbox or a power button. <code>o.value</code> (bool), <code>o.onChange</code>, <code>o.key</code>, <code>o.readOnly</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.switch{ value = enabled, onChange = function(v) ... end, key = "comp:en" } </code></pre> <h2 id="widgets-table">modules/edui.widgets/table</h2> <pre><code class="language-lua">table(o: { [string]: any }): any </code></pre> <p>A multi-column table with a header row, on the real CSS grid. <code>o.columns</code> is <code>{ { key, label?, width?, align?, mono? } }</code> (width defaults to <code>1fr</code>); <code>o.rows</code> is <code>{ [colKey] = value }</code> records. <code>o.key</code> (widget id).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.table{ columns = {{key="name",label="Script"},{key="ms",label="ms",align="right",mono=true}}, rows = rows, key = "vm" } </code></pre> <h2 id="widgets-text">modules/edui.widgets/text</h2> <pre><code class="language-lua">text(o: { [string]: any } | string): any </code></pre> <p>A text label. <code>o.text</code>, <code>o.color</code>, <code>o.dim</code> (muted), <code>o.weight</code>, <code>o.size</code>, <code>o.style</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any } | string</code></li> </ul> <pre><code class="language-lua">edui.text{ text = "Entities", weight = "600" } </code></pre> <h2 id="widgets-toolbar">modules/edui.widgets/toolbar</h2> <pre><code class="language-lua">toolbar(o: { [string]: any }): any </code></pre> <p>A header row — items laid out horizontally with comfortable padding and a hairline rule beneath, on the panel's own surface (no competing fill). <code>o.items</code> (widget list), <code>o.style</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.toolbar{ items = { edui.text{text="Entities"}, edui.spacer(), addBtn } } </code></pre> <h2 id="widgets-typebadge">modules/edui.widgets/typeBadge</h2> <pre><code class="language-lua">typeBadge(kind: string?): any? </code></pre> <p>A tiny type badge — a mono uppercase pill that names a field's kind in its own hue (num/str/bool/enum/rgb/vec3/rot/ref/obj), so a row's type reads at a glance. <code>kind</code> is a field kind (<code>number</code>/<code>string</code>/<code>enum</code>/…).</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string?</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">edui.typeBadge("enum") </code></pre> <h2 id="widgets-vbox">modules/edui.widgets/vbox</h2> <pre><code class="language-lua">vbox(o: { [string]: any }): any </code></pre> <p>A vertical flex container. <code>o.style</code> overrides; <code>o.children</code> (or the positional 1st arg) are the children. Forwards <code>onClick</code>, <code>onDoubleClick</code>, <code>onContextMenu</code> (the right button, with the pointer's <code>{ x, y }</code> — open an <code>edui.menu{ at = ... }</code> there), <code>onDrag</code>, <code>dragPayload</code> (with <code>dragLabel</code>, what the ghost under the pointer reads while it is dragged), <code>onDrop</code> and <code>tooltip</code> (a string, or styled segments).</p> <p><strong>Parameters</strong></p> <ul> <li><code>o</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">edui.vbox{ gap = 4, children = { ... } } </code></pre> <h2 id="modules-egress-readme">modules/egress/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.egress") -- egress (also available as global 'egress') </code></pre> <p>Credential-injecting HTTP for BYO-key world egress (Mechanism B). Public Luau surface over the <code>__egress</code> Internal FFI namespace.</p> <p>Usage: local egress = require("@builtin::modules.api.engine.egress") Also available as global: egress</p> <h2 id="modules-egress-clearcredential">modules/egress/clearCredential</h2> <pre><code class="language-lua">clearCredential(name: string): boolean </code></pre> <p>TRUSTED ONLY. Remove a named credential.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Credential name.</li> </ul> <pre><code class="language-lua">egress.clearCredential("meshy") </code></pre> <h2 id="modules-egress-configureforward">modules/egress/configureForward</h2> <pre><code class="language-lua">configureForward(baseUrl: string, worldGuid: string): boolean </code></pre> <p>TRUSTED ONLY. Route named-secret requests through the bound world's ZeroMind forwarding endpoint.</p> <p><strong>Parameters</strong></p> <ul> <li><code>baseUrl</code> <code>string</code> — ZeroMind service origin.</li> <li><code>worldGuid</code> <code>string</code> — Bound world identity.</li> </ul> <h2 id="modules-egress-credentialnames">modules/egress/credentialNames</h2> <pre><code class="language-lua">credentialNames(): { string } </code></pre> <p>List the names in the local credential store. Names only — secret values are never exposed to Luau.</p> <pre><code class="language-lua">for _, n in ipairs(egress.credentialNames()) do print(n) end </code></pre> <h2 id="modules-egress-fetch">modules/egress/fetch</h2> <pre><code class="language-lua">fetch(name: string, method: string, url: string, </code></pre> <p>Perform an HTTP request using a named credential. A bound world forwards the request to ZeroMind, which resolves its world secret and enforces the allowed destination. Await the returned handle; a refused forwarded request returns the HTTP error JSON with <code>__http_error = true</code>. With a local credential store, an unknown name or a URL outside its allowed prefix returns nil immediately. Credential values stay with the store or forwarding server and never enter world code.</p> <pre><code class="language-lua">local h = egress.fetch("meshy", "POST", url, nil, { prompt = p }) </code></pre> <h2 id="modules-egress-hascredential">modules/egress/hasCredential</h2> <pre><code class="language-lua">hasCredential(name: string): boolean </code></pre> <p>Whether the local credential store holds a name. Returns only a boolean. World secrets are resolved by ZeroMind when a forwarded request runs; this probe answers for the local store.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Credential name.</li> </ul> <pre><code class="language-lua">if not egress.hasCredential("meshy") then error("set MESHY_API_KEY") end </code></pre> <h2 id="modules-egress-setcredential">modules/egress/setCredential</h2> <pre><code class="language-lua">setCredential(name: string, base_url: string, </code></pre> <p>TRUSTED ONLY. Register a named credential whose header is injected into matching <code>egress.fetch</code> calls. The value is held in Rust and never returned to Luau.</p> <pre><code class="language-lua">egress.setCredential("meshy", "https://api.meshy.ai/", "Authorization", "Bearer " .. key) </code></pre> <h2 id="dirty-hot-reload-readme">modules/engine.dirty_hot_reload/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.dirty_hot_reload") -- engine.dirty_hot_reload </code></pre> <p>Per-entity hot reload on <code><scene>.dirty/entities/</code>. Subscribes to the layer's dirty entities directory via the generic <code>vfs.watch(folder, callback)</code> primitive and reapplies changed bodies to live entities — the consumer side of the collaborative-via-VFS-sync edit model. Local edits write a dirty file; VFS sync replicates the bytes to every peer's local VFS; the subscription fires for both Local and Remote writes; the callback reads the replicated file and applies it to the peer's live entity. No polling. Writes made through <code>vfs.*</code> fire watchers synchronously inside the FFI call. Shell writes and peer-driven writes are queued in <code>zero_vfs::write_events</code> and drained by the engine schedule's <code>apply_vfs_mutations</code> system, which fires watchers within a frame of the bytes landing. Cost is O(0) when nothing changes. Mark-suppression contract: Body application happens inside <code>__scene_load.begin()</code> / <code>__scene_load.finish()</code> so the Rust mark sites (<code>mark_entity_dirty</code>, <code>mark_manifest_dirty</code>) early-return — without the gate, applying a peer's edit would re-mark the entity dirty locally, the saver would re-write the same body to disk, VFS sync would re-broadcast back, and the feedback loop would saturate the relay. The same gate the scene loader uses on full-scene loads. Echo-loop suppression: Local writes round-trip through the watcher too (lua_vfs_write fires synchronously). The <code>known</code> per-eid content cache short- circuits re-apply when the file content matches the last-seen string — we just wrote it, no need to re-apply our own write. Lifecycle: <code>layers.M.fireLoad(proxy)</code> calls <code>M.install(proxy)</code> for every scene (additive or not) that just loaded — the install seeds <code>known</code> from current dir contents and registers one <code>vfs.watch</code> on <code><entitiesDir></code> (folder subscription). <code>M.fireUnload(proxy)</code> calls <code>M.uninstall(proxy)</code> which calls <code>vfs.unwatch(id)</code> and drops the per-layer state. Authored content lives in additive overlays too (editor side-panels, side games, custom HUD overlays) — they get the same hot-reload treatment. Scenes that genuinely don't resolve to a dirty subtree (e.g. an engine-internal scaffolding overlay) early-return inside <code>entitiesDirFor</code>.</p> <p>Usage: local engine.dirty_hot_reload = require("@builtin::modules.api.engine.dirty_hot_reload")</p> <h2 id="dirty-hot-reload-canonicalchanged">modules/engine.dirty_hot_reload/canonicalChanged</h2> <pre><code class="language-lua">canonicalChanged(sceneGuid: string, origin: string?): number </code></pre> <p>Take each entity whose record changed in the layer's canonical <code>scene.json</code> to what that record now says, and the loaded scene to the scene-level fields the write changed (<code>player</code>, the setting blocks), and report what that reached. The entry point for a write this module's own subscription does not see: <code>vfs.watch</code> fires for a write made inside the vm and for one replicated from a peer, and NOT for one made through the host (<code>write_file</code> / <code>edit_file</code>), which is the route an author reaches for first. The scene assetType's <code>onChange</code> sees that one and calls this.</p> <p>Guarded by the same content cache the subscription uses, so whichever of the two arrives first applies the write and the other reads an echo. A record whose entity already matches costs nothing either way, and so does a scene-level field the scene already holds as the file states it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sceneGuid</code> <code>string</code> — The layer's guid.</li> <li><code>origin</code> <code>string?</code> <em>(optional)</em> — Who wrote the file: <code>"local"</code> (the default) for this session, <code>"remote"</code> for a peer, whose records are applied as a peer's.</li> </ul> <pre><code class="language-lua">DirtyHotReload.canonicalChanged(layers.active.guid) </code></pre> <h2 id="dirty-hot-reload-debugfire">modules/engine.dirty_hot_reload/debugFire</h2> <pre><code class="language-lua">debugFire(guid, path, kind, origin) </code></pre> <p>Diagnostic: synchronously dispatch a synthetic write event for the given guid + path, as a write <code>origin</code> made (<code>"local"</code> by default, <code>"remote"</code> for a peer's). Used by tests and debugging; production reads come from <code>vfs.watch</code> callbacks.</p> <p><strong>Parameters</strong></p> <ul> <li><code>guid</code> <code>any</code> <em>(optional)</em></li> <li><code>path</code> <code>any</code> <em>(optional)</em></li> <li><code>kind</code> <code>any</code> <em>(optional)</em></li> <li><code>origin</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="dirty-hot-reload-debugstate">modules/engine.dirty_hot_reload/debugState</h2> <pre><code class="language-lua">debugState(guid) </code></pre> <p>Diagnostic: return a shallow copy of the per-layer state for inspection. Returns <code>{ entitiesDir, knownKeys, waiting, active, watcherId, eventCount, peerSettles, peerSettlePending }</code>. <code>peerSettles</code> counts the times the layer was respawned from its files because another session took overlay records out of them and those files then held something other than the live layer; <code>peerSettlePending</code> is true while such a removal waits for its run of removals to end before the files are read. <code>waiting</code> maps the id of each record whose parent no entity provides to the parent reference it states; that record's entity is built when an entity providing the parent is built from the scene's records.</p> <p><strong>Parameters</strong></p> <ul> <li><code>guid</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="dirty-hot-reload-install">modules/engine.dirty_hot_reload/install</h2> <pre><code class="language-lua">install(sceneProxy) </code></pre> <p>Subscribe to the layer's dirty/entities/ directory via <code>vfs.watch</code> (folder subscription — fires for any descendant write/remove, both Local and Remote origins). Called automatically from <code>layers.M.fireLoad</code> for EVERY loaded scene (additive or root) — scenes don't need to install it manually. Scenes that don't resolve to a dirty subtree (engine-internal scaffolding overlays, persistent UI layers with no on-disk authoring path) early-return inside <code>entitiesDirFor</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sceneProxy</code> <code>any</code> <em>(optional)</em> — SceneProxy for the just-loaded layer.</li> </ul> <h2 id="dirty-hot-reload-installed">modules/engine.dirty_hot_reload/installed</h2> <pre><code class="language-lua">installed() </code></pre> <p>Return the set of currently-installed layer guids. Observability hook for tests / debugging.</p> <h2 id="dirty-hot-reload-uninstall">modules/engine.dirty_hot_reload/uninstall</h2> <pre><code class="language-lua">uninstall(sceneProxy) </code></pre> <p>Stop the watcher for the given layer. Called from <code>layers.M.fireUnload</code>. Idempotent — safe to call when no install ran or after a prior uninstall.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sceneProxy</code> <code>any</code> <em>(optional)</em> — SceneProxy for the layer being unloaded.</li> </ul> <h2 id="modules-entity-readme">modules/entity/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.entity") -- entity (also available as global 'entity') </code></pre> <p>Entity spawn / despawn / query / hierarchy surface. Calling the table itself — <code>entity(idOrProxy)</code> — resolves an id or proxy to its live entityRef proxy, and iterating it (<code>for name, member in entity do</code>) lists its members. Public Luau surface over the <code>__entity</code> Internal FFI namespace, composed with the entityRef proxy metatable, the hierarchy swap helper, id/proxy coercion, and the polymorphic batch read/write dispatch.</p> <p>Usage: local entity = require("@builtin::modules.api.engine.entity") Also available as global: entity</p> <h2 id="modules-entity-batchaddcomponent">modules/entity/batchAddComponent</h2> <pre><code class="language-lua">batchAddComponent(targets: { string | entityRef }, type_name: string, data: table?): number </code></pre> <p>Add the same component type to many entities in one call. Returns the count of entities the component was added to — an entity already carrying an unnamed instance of the same type is skipped rather than double-added.</p> <p><strong>Parameters</strong></p> <ul> <li><code>targets</code> <code>{ string | entityRef }</code> — Array of entity ids or entity proxies (e.g. the return of <code>entity.batchSpawn</code> or <code>entity.findAll</code>).</li> <li><code>type_name</code> <code>string</code> — Component type to add to every entity.</li> <li><code>data</code> <code>table?</code> <em>(optional)</em> — Init data: one table applied identically to every entity — the same shape the second arg to <code>entity(id).component.add(type, data)</code> takes — or an array holding one such table per target, in the targets' order, so each entity is added with its own values.</li> </ul> <pre><code class="language-lua">local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 }) entity.batchAddComponent({ a, b }, "Debris", { { lifetime = 2 }, { lifetime = 7 } }) </code></pre> <h2 id="modules-entity-batchdespawn">modules/entity/batchDespawn</h2> <pre><code class="language-lua">batchDespawn(targets: { string | entityRef }): number </code></pre> <p>Despawn many entities in one call. Locked or unresolvable entities are skipped. Returns the count queued for despawn.</p> <p><strong>Parameters</strong></p> <ul> <li><code>targets</code> <code>{ string | entityRef }</code> — Array of entity ids, entity proxies, or display names (e.g. the return of <code>entity.batchSpawn</code> / <code>entity.findAll</code>).</li> </ul> <pre><code class="language-lua">local n = entity.batchDespawn(ids) </code></pre> <h2 id="modules-entity-batchproxy">modules/entity/batchProxy</h2> <pre><code class="language-lua">batchProxy(targets: { string | entityRef }): { entityRef? } </code></pre> <p>Resolve an array of entity ids to proxies in one call. Each output slot is the standard <code>entity(id)</code> proxy; ids missing from the frame cache surface as nil at that index. Use when iterating over a snapshot of entities so per-id lookups don't dominate the hot path.</p> <p><strong>Parameters</strong></p> <ul> <li><code>targets</code> <code>{ string | entityRef }</code> — Array of entity ids or entity proxies.</li> </ul> <pre><code class="language-lua">local proxies = entity.batchProxy(ids) </code></pre> <h2 id="modules-entity-batchread">modules/entity/batchRead</h2> <pre><code class="language-lua">batchRead(target: { string | entityRef } | binding, component: string?, field: string?, sink: TypedBuffer?): { any? } | number </code></pre> <p>Read a component-field across many entities in one call. Polymorphic on the shape of <code>target</code> and <code>sink</code>:</p> <ul> <li><code>entity.batchRead(ids)</code> / <code>(ids, comp)</code> / <code>(ids, comp, field)</code> — returns one value per entity (a whole snapshot, one component table, or one field value). Missing entities/components/fields surface as nil at that slot.</li> <li><code>entity.batchRead(binding, comp, field, buffer)</code> — reads each entity's field directly into a typed CPU substrate buffer (<code>substrate.createBuffer({type="vec3"})</code>, etc.) with no per-entity Lua table allocation. Returns the count of successful reads. <code>Transform.worldMatrix</code> reads each entity's world matrix — its own transform folded with its ancestors', the one <code>entity(id).worldMatrix()</code> answers — as 16 column-major numbers: into a <code>mat4</code> buffer, or as an array of 16 per entity. It is read, never written. <code>target</code> accepts an entity-id array or a <code>ecs.bindEntities(ids)</code> handle. Buffer sinks require a binding — the typed kernel is binding-only.</li> </ul> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>{ string | entityRef } | binding</code> — Array of entity ids or entity proxies, or a binding handle from <code>ecs.bindEntities(ids)</code>.</li> <li><code>component</code> <code>string?</code> <em>(optional)</em> — Component type name (e.g. "Transform").</li> <li><code>field</code> <code>string?</code> <em>(optional)</em> — Field name (e.g. "position").</li> <li><code>sink</code> <code>TypedBuffer?</code> <em>(optional)</em> — Typed CPU buffer from <code>substrate.createBuffer({...})</code> to memcpy field values into. Required when <code>target</code> is a binding.</li> </ul> <pre><code class="language-lua">local snapshot = entity.batchRead(ids) local positions = entity.batchRead(ids, "Transform", "position") entity.batchRead(ecs.bindEntities(ids), "Transform", "worldMatrix", matrices) </code></pre> <h2 id="modules-entity-batchreadtobuffer">modules/entity/batchReadToBuffer</h2> <pre><code class="language-lua">batchReadToBuffer(binding: number, component: string, field: string, buffer: number): number </code></pre> <p>FFI primitive backing <code>entity.batchRead(binding, ..., buffer)</code>. Prefer the unified <code>entity.batchRead</code>, which auto-dispatches by argument shape. Reads each entity's component field directly into a typed CPU substrate buffer, with no per-entity Lua table allocation. After the call, read the buffer via <code>buf:read(0, count*stride)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>binding</code> <code>number</code> — Binding id from <code>ecs.bindEntities(ids).id</code>, live while its handle is held.</li> <li><code>component</code> <code>string</code> — Component type name.</li> <li><code>field</code> <code>string</code> — Field name to read.</li> <li><code>buffer</code> <code>number</code> — Destination buffer id (must be the matching type).</li> </ul> <pre><code class="language-lua">entity.batchReadToBuffer(binding.id, "Transform", "position", buf.id) </code></pre> <h2 id="modules-entity-batchspawn">modules/entity/batchSpawn</h2> <pre><code class="language-lua">batchSpawn(count: number, name_prefix: string?, opts: { temporary: boolean?, parent: (string | entityRef)? }?): { string } </code></pre> <p>Spawn <code>count</code> entities in one call. Returns an array of the new entity ids in spawn order. Each entity is given a display name of <code><name_prefix><i></code> (or <code>entity<i></code> if the prefix is omitted). Prefer this over looping <code>entity.spawn</code> when creating large entity counts.</p> <p><strong>Parameters</strong></p> <ul> <li><code>count</code> <code>number</code> — How many entities to spawn (capped at 1,000,000).</li> <li><code>name_prefix</code> <code>string?</code> <em>(optional)</em> — Display-name prefix appended with the 1-based index. Defaults to "entity".</li> <li><code>opts</code> <code>{ temporary: boolean?, parent: (string | entityRef)? }?</code> <em>(optional)</em> — <code>{ temporary?, parent? }</code> — a temporary batch is one the scene does not save, the same thing <code>entity.spawn({ temporary = true })</code> asks for. The component goes on in one crossing, so a batch of a hundred thousand costs what one does. A batch spawned inside an entrypoint that is itself temporary is temporary whatever this says. <code>parent</code> — an id or an entity — makes every entity in the batch its child as it is made, so the batch stands, moves and goes with it.</li> </ul> <pre><code class="language-lua">local ids = entity.batchSpawn(100, "grass_") local particles = entity.batchSpawn(5000, "spark_", { temporary = true }) local parts = entity.batchSpawn(8, "part_", { temporary = true, parent = owner }) </code></pre> <h2 id="modules-entity-batchwrite">modules/entity/batchWrite</h2> <pre><code class="language-lua">batchWrite(target: { string | entityRef } | binding, component: string, field: string, source: { any? } | TypedBuffer): number </code></pre> <p>Write a single component-field across many entities in one call. Polymorphic on the shape of <code>target</code> and <code>source</code>:</p> <ul> <li><code>entity.batchWrite(ids, comp, field, values)</code> — per-call entity-id resolution; <code>values</code> is an array the same length as <code>ids</code> (nil slots are skipped). Use for one-shot writes.</li> <li><code>entity.batchWrite(binding, comp, field, values)</code> — binding handle from <code>ecs.bindEntities(ids)</code>; skips per-call id resolution. Use for per-frame writes against a stable entity set.</li> <li><code>entity.batchWrite(binding, comp, field, buffer)</code> — typed CPU buffer source (<code>substrate.createBuffer({type="vec3"})</code>, etc.), with no per-entity table allocation. Returns the count of successful writes. Buffer sources require a binding — the typed kernel is binding-only.</li> </ul> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>{ string | entityRef } | binding</code> — Array of entity ids or entity proxies, or a binding handle from <code>ecs.bindEntities(ids)</code>.</li> <li><code>component</code> <code>string</code> — Component type name.</li> <li><code>field</code> <code>string</code> — Field name to write.</li> <li><code>source</code> <code>{ any? } | TypedBuffer</code> — Per-entity values array (nil entries are skipped), or a typed CPU buffer from <code>substrate.createBuffer({...})</code>. A buffer source requires a binding <code>target</code>.</li> </ul> <pre><code class="language-lua">entity.batchWrite(ids, "Transform", "position", positions) </code></pre> <h2 id="modules-entity-batchwritebound">modules/entity/batchWriteBound</h2> <pre><code class="language-lua">batchWriteBound(binding: number, component: string, field: string, values: { any? }): number </code></pre> <p>FFI primitive backing <code>entity.batchWrite(binding, ...)</code> with a per-entity values table. Prefer the unified <code>entity.batchWrite</code>, which auto-dispatches by argument shape; this entry stays for power users / debug code that wants to skip dispatch overhead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>binding</code> <code>number</code> — Binding id from <code>ecs.bindEntities(ids).id</code>, live while its handle is held.</li> <li><code>component</code> <code>string</code> — Component type name.</li> <li><code>field</code> <code>string</code> — Field name to write.</li> <li><code>values</code> <code>{ any? }</code> — Per-entity source values (nil = skip). Length must match the binding's entity count.</li> </ul> <pre><code class="language-lua">entity.batchWriteBound(binding.id, "Transform", "position", values) </code></pre> <h2 id="modules-entity-batchwritefrombuffer">modules/entity/batchWriteFromBuffer</h2> <pre><code class="language-lua">batchWriteFromBuffer(binding: number, component: string, field: string, buffer: number): number </code></pre> <p>FFI primitive backing <code>entity.batchWrite(binding, ..., buffer)</code>. Prefer the unified <code>entity.batchWrite</code>, which auto-dispatches by argument shape. Caller fills a typed substrate buffer (<code>substrate.createBuffer({type="vec3"})</code>) once via <code>buf:write(...)</code>, then this memcpys 12 (vec3) or 16 (quat) bytes per entity into the component field. Buffer count and binding count should match — a mismatch processes the smaller of the two.</p> <p><strong>Parameters</strong></p> <ul> <li><code>binding</code> <code>number</code> — Binding id from <code>ecs.bindEntities(ids).id</code>, live while its handle is held.</li> <li><code>component</code> <code>string</code> — Component type name.</li> <li><code>field</code> <code>string</code> — Field name to write.</li> <li><code>buffer</code> <code>number</code> — Buffer id from <code>substrate.createBuffer({type="vec3", len=N}).id</code>.</li> </ul> <pre><code class="language-lua">entity.batchWriteFromBuffer(binding.id, "Transform", "position", buf.id) </code></pre> <h2 id="modules-entity-capture">modules/entity/capture</h2> <pre><code class="language-lua">capture(builder: () -> ()): ({ string }, any?, { string }, { any }) </code></pre> <p>Run <code>builder</code> inside an entity capture scope and return the entity ids it minted, in creation order, the error it raised (if any), and the ids among them that a component the builder attached minted in its own lifecycle. Every id minted while the builder runs is recorded — through <code>entity.spawn</code>, <code>entity.spawnSynced</code>, <code>entity.batchSpawn</code>, and <code>entity.instantiate</code> alike. Scopes nest: an id minted inside an inner capture is recorded by that capture AND every enclosing one — the innermost capture answers, so a nested build shapes its own entities, not the ones around it. A builder that raises still returns its ids, so the caller can despawn what a failed build left behind; the scope closes either way and never outlives this call. While the builder runs, an operation whose result cannot be composed into a record is refused rather than applied, and so is any operation aimed at an entity the builder did not mint — a builder that returned while something it did was refused comes back with an error naming every refusal.</p> <p><strong>Parameters</strong></p> <ul> <li><code>builder</code> <code>() -> ()</code> — Function run inside the scope; the entities it creates are what comes back.</li> </ul> <pre><code class="language-lua">local ids, err, reproduced, refusals = entity.capture(function() entity.spawn("chair") end) </code></pre> <h2 id="modules-entity-capturing">modules/entity/capturing</h2> <pre><code class="language-lua">capturing(): boolean </code></pre> <p>Whether an <code>entity.capture</code> builder (a scene build) is running on the calling thread. A component a build adds takes the world's Preset Manager rows in every mode, so a scene built in a play session holds what the same build holds in edit mode.</p> <pre><code class="language-lua">if entity.capturing() then return end </code></pre> <h2 id="modules-entity-despawn">modules/entity/despawn</h2> <pre><code class="language-lua">despawn(target: string | entityRef) </code></pre> <p>Despawn an entity and all its components. Pass an id string or an entity proxy to despawn that ONE entity. Pass a name to despawn EVERY entity with that name — names are not unique, so a name argument despawns all matches, not one arbitrary match. A despawned id becomes invalid after this call. Raises if no entity matches; for a bulk name despawn, locked entities are skipped with a logged summary and only raise if every match is locked.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — Entity id, name, or entity proxy. A name despawns all entities sharing that name.</li> </ul> <pre><code class="language-lua">entity.despawn(id) entity.despawn("Enemy") -- despawns every entity named "Enemy" </code></pre> <h2 id="modules-entity-duplicate">modules/entity/duplicate</h2> <pre><code class="language-lua">duplicate(sourceId: string | entityRef, name: string?, opts: table?): string? </code></pre> <p>Duplicate an entity with all its components (transform, script components, attributes, visuals, material) and its descendants. Returns the new entity's id, or nil when <code>sourceId</code> names no live entity. Descendants marked temporary are left out of the copy: they are scaffolding whatever spawned them re-creates, so a component that regenerates its own children rebuilds them on the copy rather than the copy carrying a second set. <code>includeTemporary</code> copies them too, for the hierarchy that IS the temporary thing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sourceId</code> <code>string | entityRef</code> — Entity id or entity proxy of the source entity to clone.</li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Display name for the copy (defaults to source name + " (copy)").</li> <li><code>opts</code> <code>table?</code> <em>(optional)</em> — <code>{ includeTemporary?: boolean, descendants?: boolean, components?: table, name?: string }</code>: <code>descendants = false</code> copies the source entity alone, leaving its children to it; <code>components</code> lays fields over the copy's script components by type (<code>{ SoftBody = { mass = 2 } }</code>), merged over the state copied from the source; <code>name</code> is the same field the <code>name</code> argument sets, and wins when both are given.</li> </ul> <pre><code class="language-lua">local copyId = entity.duplicate(id); if copyId then entity(copyId).position = { 1, 0, 0 } end local copyId = entity.duplicate(id, "Turret", { includeTemporary = true }) local lone = entity.duplicate(id, nil, { descendants = false, components = { SoftBody = { mass = 2 } } }) </code></pre> <h2 id="modules-entity-exists">modules/entity/exists</h2> <pre><code class="language-lua">exists(idOrProxy: string | entityRef): boolean </code></pre> <p>Check whether an entity currently exists in the scene. Accepts an entity-id string or an entity proxy, matched by entity id — so it agrees exactly with <code>entity(id)</code>. A name is a different kind of identifier: a string that misses as an id but names a live entity raises rather than answering false, since false there is indistinguishable from absence. Check by name with <code>entity.find(name) ~= nil</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>idOrProxy</code> <code>string | entityRef</code> — Entity id or an entity proxy.</li> </ul> <pre><code class="language-lua">if entity.exists(id) then ... end </code></pre> <h2 id="modules-entity-find">modules/entity/find</h2> <pre><code class="language-lua">find(nameOrGlob: string): entityRef? </code></pre> <p>Find the first entity matching <code>nameOrGlob</code>. A plain string matches an exact id or Name component; a string containing <code>*</code> (any run of characters) or <code>?</code> (any single character) matches Names as a glob, so <code>entity.find("enemy_*")</code> is the first entity whose name starts with <code>enemy_</code>. A glob addresses Names only, never ids. Same-frame pending spawns are searched too, and anything queued for despawn in the same frame is skipped. Names are NOT unique — use <code>entity.findAll</code> when every match matters; <code>find</code> is the first entity <code>findAll</code> lists. Inside a build (a scene's <code>content()</code>, a module's builder), a name the build's own entities carry resolves to those first, so <code>entity.spawn("door")</code> followed by <code>entity.find("door")</code> is the door the build just made even when the world around it holds another <code>door</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>nameOrGlob</code> <code>string</code> — Exact entity Name or id, or a <code>*</code> / <code>?</code> glob over Names.</li> </ul> <pre><code class="language-lua">local e = entity.find("enemy_*") </code></pre> <h2 id="modules-entity-findall">modules/entity/findAll</h2> <pre><code class="language-lua">findAll(nameOrGlob: string?): { entityRef } </code></pre> <p>Enumerate entity proxies. With a <code>nameOrGlob</code> argument, returns every entity whose Name component or id matches (names are not unique): a plain string matches exactly, while a <code>*</code> / <code>?</code> glob matches Names. With no argument, returns every entity in the current snapshot — <code>findAll("")</code> is the exact-match filter for the empty name, which normally matches nothing. Same-frame pending spawns are included and same-frame despawns filtered out. Elements are entity proxies, not id strings — for ids, wrap the result: <code>entity.ids(entity.findAll(...))</code>. Inside a build (a scene's <code>content()</code>, a module's builder), the entities the build made come first and the world's same-named entities follow.</p> <p><strong>Parameters</strong></p> <ul> <li><code>nameOrGlob</code> <code>string?</code> <em>(optional)</em> — Exact entity Name or id to filter by, or a <code>*</code> / <code>?</code> glob over Names. Omit to enumerate every entity.</li> </ul> <pre><code class="language-lua">for _, e in entity.findAll("enemy_*") do e:despawn() end </code></pre> <h2 id="modules-entity-getchildren">modules/entity/getChildren</h2> <pre><code class="language-lua">getChildren(id: string | entityRef): { entityRef } </code></pre> <p>Get an array of the direct children as entity proxies. Each element carries <code>.name</code>, <code>.id</code>, <code>.position</code>, <code>.component</code>, and the rest of the per-entity surface, the same shape <code>entity.findAll</code> returns. A string that names a live entity rather than identifying it raises, naming <code>entity.find(name)</code>, the lookup that takes a name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string | entityRef</code> — Entity id or entity proxy.</li> </ul> <pre><code class="language-lua">for _, c in entity.getChildren(id) do c.internal = true end </code></pre> <h2 id="modules-entity-getdescendants">modules/entity/getDescendants</h2> <pre><code class="language-lua">getDescendants(id: string | entityRef): { entityRef } </code></pre> <p>Get every descendant (children, grandchildren, and deeper) of the given entity as entity proxies in breadth-first order, excluding the entity itself. Resolves the whole subtree in one linear pass over the entity set, so a large subtree costs proportionally to the entity count rather than to the subtree size times the entity count. A string that names a live entity rather than identifying it raises, naming <code>entity.find(name)</code>, the lookup that takes a name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string | entityRef</code> — Entity id or entity proxy.</li> </ul> <pre><code class="language-lua">local all = entity.getDescendants(id) </code></pre> <h2 id="modules-entity-getparent">modules/entity/getParent</h2> <pre><code class="language-lua">getParent(id: string | entityRef): entityRef? </code></pre> <p>Get the parent entity proxy, or nil if the entity is a root entity. The returned proxy carries <code>.name</code>, <code>.id</code>, <code>.position</code>, <code>.component</code>, and the rest of the per-entity surface — the same shape <code>entity.find</code> returns. A string that names a live entity rather than identifying it raises, naming <code>entity.find(name)</code>, the lookup that takes a name: answering it as an id would read the same as a root entity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string | entityRef</code> — Entity id or entity proxy.</li> </ul> <pre><code class="language-lua">local p = entity.getParent(id) </code></pre> <h2 id="modules-entity-instantiate">modules/entity/instantiate</h2> <pre><code class="language-lua">instantiate(handle: EntityTemplate, count: number, fn: ((number) -> EntityInstantiateOverrides?)?): { string } </code></pre> <p>Spawn <code>count</code> instances of a template registered with <code>entity.template</code>. Each instance gets a fresh entity id; the optional <code>fn(i)</code> callback runs per instance (<code>i</code> in 1..=count) and may return an overrides table. Override keys: <code>name</code>, <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>parent</code>, <code>temporary</code> / <code>active</code> / <code>internal</code>, <code>attributes</code>, <code>components</code> (script components, merged over the template body's data for that type — a type the template lacks is added fresh), and <code>ecs</code> (native components, merged the same way). Each override supersedes the template's shared config for that instance. The whole batch crosses in one call and lands as a single deferred mutation the engine expands into bulk work — per-instance cost drops from a full round trip to one callback plus one mutation. Inside <code>queue()</code> the batch is deferred onto the cross-frame ring; outside, it lands in the next frame's drain. Returns the array of newly-minted entity ids in spawn order.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>EntityTemplate</code> — Template handle from <code>entity.template</code>.</li> <li><code>count</code> <code>number</code> — Number of instances to spawn (capped at 1,000,000).</li> <li><code>fn</code> <code>((number) -> EntityInstantiateOverrides?)?</code> <em>(optional)</em> — Per-instance override callback <code>(i) -> table?</code>.</li> </ul> <pre><code class="language-lua">local ids = entity.instantiate(h, 50, function(i) return { position = { i, 0, 0 } } end) </code></pre> <h2 id="modules-entity-scenewalks">modules/entity/sceneWalks</h2> <pre><code class="language-lua">sceneWalks(): number </code></pre> <p>How many whole-scene walks <code>entity.findAll</code> has made since the module loaded. A walk is priced by the number of entities the scene holds, so a caller that must not scale with the scene — a per-frame push, a write that reaches its target through a record — is read either side of its work against this count, which a walk raises by one.</p> <pre><code class="language-lua">local before = entity.sceneWalks(); entity(id):rename("door"); assert(entity.sceneWalks() == before) </code></pre> <h2 id="modules-entity-spawn">modules/entity/spawn</h2> <pre><code class="language-lua">spawn(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?): entityRef </code></pre> <p>Spawn a new entity and return its PROXY (the same value entity(id) yields) — act on it immediately (<code>entity.spawn(name).component.add(...)</code>, <code>.localPosition = ...</code>) with no second <code>entity(id)</code> round trip. The proxy still exposes <code>.id</code> for the rare site that needs the raw string. An entity with no components carries only a Transform and is invisible; pass <code>components</code> to give it the components that make it visible in the same call — <code>entity.spawn { name = "crate", components = { Model = { model = "cube" } } }</code> — or add them afterwards through the returned proxy. Mirrors <code>entity.find</code> / <code>entity.findAll</code>, which also return proxies. The options table can be passed on its own with the name inside it — <code>entity.spawn { name = "turret", position = { 1, 2, 3 } }</code> is the same call as <code>entity.spawn("turret", { position = { 1, 2, 3 } })</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>nameOrOpts</code> <code>(string | SpawnOpts)?</code> <em>(optional)</em> — Display name for the entity, or the options table itself.</li> <li><code>opts</code> <code>SpawnOpts?</code> <em>(optional)</em> — Options: <code>components</code> = component types to attach to the new entity, keyed by type name with each value the component's init table (attached in sorted type order; a failing add raises), <code>internal</code> = take the entity out of the default entity listings (it still renders — <code>entity(id):hide()</code> stops the draw), <code>parent</code> = parent entity id or proxy, <code>temporary</code> = skip this entity (and descendants) from scene/world saves, <code>position</code> / <code>rotation</code> / <code>scale</code> = place the entity's Transform at spawn, <code>attributes</code> = per-entity attribute values keyed by name (<code>{ role = "guard" }</code>, the same values <code>entity(id).attribute.set(key, value)</code> writes and <code>entity.template</code> / <code>entity.instantiate</code> take), <code>synced</code> = flag the entity multiplayer-synced (what <code>entity.spawnSynced</code> implies), <code>active</code> = false spawns it inactive, <code>scene_owner</code> = the scene layer guid the entity is attributed to (inherited from the spawning scene when unset), <code>id</code> = restore a previously-assigned entity id (scene_loader use; leave unset for a normal spawn). An unrecognised key is rejected loudly.</li> </ul> <pre><code class="language-lua">local e = entity.spawn("crate", { components = { Model = { model = "cube" } } }) local e = entity.spawn { name = "turret", position = { 1, 2, 3 } } local guard = entity.spawn("guard", { attributes = { role = "guard", hp = 3 } }) </code></pre> <h2 id="modules-entity-spawnsynced">modules/entity/spawnSynced</h2> <pre><code class="language-lua">spawnSynced(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?): entityRef </code></pre> <p>Spawn an entity already flagged multiplayer-synced at the root — the explicit form of <code>entity.spawn</code> for SHARED, host-authoritative content. The entity's existence broadcasts to every peer; joiners receive it from the relay snapshot instead of spawning their own copy. Use this (NOT <code>entity.spawn</code>) for anything that must be the SAME object on all clients: enemies, pickups, projectiles, dynamic world props. Call it ONLY where exactly one client runs the code — a scene's <code>onHostLoad</code> (host-only) phase, or behind <code>multiplayer.isHost()</code>. Calling it in all-client code makes every client spawn+sync its own copy — the double-spawn "explosion". Equivalent to <code>entity.spawn(name, { synced = true })</code>; identical in every other respect.</p> <p><strong>Parameters</strong></p> <ul> <li><code>nameOrOpts</code> <code>(string | SpawnOpts)?</code> <em>(optional)</em> — Display name for the entity, or the options table itself.</li> <li><code>opts</code> <code>SpawnOpts?</code> <em>(optional)</em> — Same options as <code>entity.spawn</code> (<code>synced</code> is already implied).</li> </ul> <pre><code class="language-lua">if multiplayer.isHost() then entity.spawnSynced("Goblin") end </code></pre> <h2 id="modules-entity-template">modules/entity/template</h2> <pre><code class="language-lua">template(def: EntityTemplateDef): EntityTemplate </code></pre> <p>Construct a reusable spawn template. Captures a shared entity config ONCE and returns an <code>EntityTemplate</code> handle for <code>entity.instantiate(handle, count, fn?)</code> — one call per batch instead of one per entity. The template lives as long as a script holds the handle: once nothing reaches it, the collector releases the template, and the entities already instantiated from it stay as they are. Keep the handle (a module-level local, a field) for as long as more instances may follow. <code>def</code> keys: <code>components</code> (script components, <code>{ [type] = init-data }</code>), <code>ecs</code> (array of native <code>ecs.X{...}</code> components), <code>temporary</code> (instances skip scene/world saves), <code>active</code> (spawn state), <code>internal</code> (instances are taken out of the default entity listings; they still render), <code>attributes</code> (<code>{ key = value }</code> applied to every instance). Every value is a shared default; a per-instance <code>entity.instantiate</code> override supersedes it. The template body is captured by value — later edits to the source table do not affect templates already created.</p> <p><strong>Parameters</strong></p> <ul> <li><code>def</code> <code>EntityTemplateDef</code> — Template definition: <code>components</code> / <code>ecs</code> / <code>temporary</code> / <code>active</code> / <code>internal</code> / <code>attributes</code>. Per-instance <code>name</code> / <code>position</code> / <code>rotation</code> / <code>scale</code> / <code>parent</code> and any override go through the <code>instantiate</code> callback.</li> </ul> <pre><code class="language-lua">local h = entity.template({ components = { Model = { model = "cube" } } }) </code></pre> <h2 id="modules-entity-tree">modules/entity/tree</h2> <pre><code class="language-lua">tree(opts: { [string]: any }?): { [string]: any } </code></pre> <p>A windowed, lean view over the scene's entity tree, in one crossing. Rows carry id, name, parentId, depth, childCount, active, sceneLayer and componentNames — names only, never component values — so the call costs the rows it answers with rather than the size of the scene. Entities group under scene layers, per-layer roots and children name-sorted; internal entities and their subtrees stay out. <code>expanded</code> names the ids whose children unfold, and a collapsed node still reports its <code>childCount</code>; <code>filter</code> keeps the rows whose name or id contains the needle plus every ancestor on a path to one, auto-unfolded, with the actual matches flagged <code>matched</code>. <code>offset</code> / <code>limit</code> window the flattened rows, <code>layer</code> scopes the window and its <code>total</code> to one layer while <code>layers</code> still reports every layer's row count, and <code>revision</code> echoes <code>getEntitiesRevision("structure")</code>, which moves only on structural change.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ layer?, expanded?, filter?, offset?, limit? }</code>.</li> </ul> <pre><code class="language-lua">local view = entity.tree({ filter = "crate", limit = 50 }) </code></pre> <h2 id="modules-entitymembers-readme">modules/entityMembers/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.entity.members") -- entityMembers </code></pre> <p>The declarative description of every entityRef member. The runtime dispatch, the accepted-member set, the miss diagnostics and the LSP tree all derive from this table, so they cannot disagree.</p> <p>Usage: local entityMembers = require("@builtin::modules.api.engine.entity.members")</p> <h2 id="modules-enum-values-readme">modules/enum_values/README</h2> <pre><code class="language-lua">require("@builtin::modules.enum_values") -- enum_values </code></pre> <p>The <code>enum</code> field-constraint validator: a constrained value must be one of the strings the field declared. A rejection names every member, so the error carries the whole set the caller may choose from. Registers itself with the generic field_constraints registry on load. <code>nil</code> passes, so an enum field may be left unset.</p> <p>Usage: local enum_values = require("@builtin::modules.enum_values")</p> <h2 id="modules-environment-readme">modules/environment/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.environment") -- environment (also available as global 'environment') </code></pre> <p>Environment / reflection capture — bake the scene into reflection-probe cube slots from world positions, persist them as <code>faces6</code> <code>.texture</code> assets, and set per-probe blend data so surfaces reflect the nearest probe(s). Public Luau surface over the <code>__environment</code> Internal FFI namespace. The generic "render the scene into a cubemap from a point" capability the reflection probe system is built on.</p> <p>Usage: local environment = require("@builtin::modules.api.engine.environment") Also available as global: environment</p> <h2 id="modules-environment-capture">modules/environment/capture</h2> <pre><code class="language-lua">capture(x: number, y: number, z: number): boolean </code></pre> <p>Bake the scene into the environment from <code>(x, y, z)</code> as the single global reflection (slot 0 + one full-coverage probe). Every PBR surface reflects it. Queued — takes effect on the next frame. For multiple proximity-blended probes use the reflectionProbe system instead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>x</code> <code>number</code> — World X of the capture position.</li> <li><code>y</code> <code>number</code> — World Y of the capture position.</li> <li><code>z</code> <code>number</code> — World Z of the capture position.</li> </ul> <pre><code class="language-lua">environment.capture(0, 2, 0) </code></pre> <h2 id="modules-environment-capturesky">modules/environment/captureSky</h2> <pre><code class="language-lua">captureSky(x: number?, y: number?, z: number?): boolean </code></pre> <p>Render the SKY alone into the environment's sky slot from <code>(x, y, z)</code> and arm the sky fallback. A reflective surface no probe covers then reflects the sky rather than black, and a partially covered one blends the shortfall against it. The capture holds whatever the scene's sky draws — a gradient, a physical atmosphere, a skybox material — with no geometry in it, so it stays correct wherever the camera goes. Once captured, the slot follows the sky the scene draws: a sky that changes is recaptured from the same position. Queued — takes effect on the next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>x</code> <code>number?</code> <em>(optional)</em> — World X of the capture position. Defaults to 0.</li> <li><code>y</code> <code>number?</code> <em>(optional)</em> — World Y of the capture position — the altitude a height-dependent atmosphere is sampled at. Defaults to 0.</li> <li><code>z</code> <code>number?</code> <em>(optional)</em> — World Z of the capture position. Defaults to 0.</li> </ul> <pre><code class="language-lua">environment.captureSky() </code></pre> <h2 id="modules-environment-captureslot">modules/environment/captureSlot</h2> <pre><code class="language-lua">captureSlot(slot: number, x: number, y: number, z: number): boolean </code></pre> <p>Bake the scene into reflection-probe <code>slot</code> from <code>(x, y, z)</code>. Renders the FULL scene (geometry + sky) six times from that point into that slot. Register the probe's position+radius via <code>setProbes</code> so surfaces blend it by proximity. Queued — takes effect next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>slot</code> <code>number</code> — Reflection-probe slot (0-based).</li> <li><code>x</code> <code>number</code> — World X of the capture position.</li> <li><code>y</code> <code>number</code> — World Y of the capture position.</li> <li><code>z</code> <code>number</code> — World Z of the capture position.</li> </ul> <pre><code class="language-lua">environment.captureSlot(0, 0, 2, 0) </code></pre> <h2 id="modules-environment-captureslottoasset">modules/environment/captureSlotToAsset</h2> <pre><code class="language-lua">captureSlotToAsset(name: string, slot: number, x: number, y: number, z: number, timeoutFrames: number?): (string?, string?) </code></pre> <p>Bake the scene into reflection-probe <code>slot</code> from <code>(x, y, z)</code> AND persist the 6 rendered faces into a <code>faces6</code> <code>.texture</code> cubemap asset at <code>/source/<name>.texture/</code> (px/nx/py/ny/pz/nz PNGs + a <code>cube.yaml</code> sidecar). Survives an engine restart and syncs like any other texture. Yields a few frames while the bake + GPU readback complete; must be called from a task/coroutine context (component hook, <code>task.spawn</code>, or <code>execute</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Destination asset identity (writes <code>/source/<name>.texture/</code>).</li> <li><code>slot</code> <code>number</code> — Reflection-probe slot (0-based).</li> <li><code>x</code> <code>number</code> — World X of the capture position.</li> <li><code>y</code> <code>number</code> — World Y of the capture position.</li> <li><code>z</code> <code>number</code> — World Z of the capture position.</li> <li><code>timeoutFrames</code> <code>number?</code> <em>(optional)</em> — Optional max frames to wait for the readback (default 900, past the frames the engine gives a readback before it gives up on one, so what ends the wait is the readback's own answer).</li> </ul> <pre><code class="language-lua">environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0) </code></pre> <h2 id="modules-environment-capturetoasset">modules/environment/captureToAsset</h2> <pre><code class="language-lua">captureToAsset(name: string, x: number, y: number, z: number): (string?, string?) </code></pre> <p>Bake the single global reflection AND persist it to a <code>faces6</code> <code>.texture</code> asset (slot 0). Yields a few frames; call from a task/coroutine context.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Destination asset identity (writes <code>/source/<name>.texture/</code>).</li> <li><code>x</code> <code>number</code> — World X of the capture position.</li> <li><code>y</code> <code>number</code> — World Y of the capture position.</li> <li><code>z</code> <code>number</code> — World Z of the capture position.</li> </ul> <pre><code class="language-lua">environment.captureToAsset("env_main", 0, 2, 0) </code></pre> <h2 id="modules-environment-ensureskyfallback">modules/environment/ensureSkyFallback</h2> <pre><code class="language-lua">ensureSkyFallback(): boolean </code></pre> <p>Ensure the scene's sky is in the environment's sky slot: a reflective surface no probe covers then reflects the sky rather than black, and a partially covered one blends the shortfall against it. Queues a capture when the sky slot holds none, and re-arms the fallback when a capture is there but switched off. The engine's own state answers both questions, so everything that stands a sky up can call this and one capture is shared between them. Once captured, the slot follows the sky the scene draws on its own.</p> <pre><code class="language-lua">environment.ensureSkyFallback() </code></pre> <h2 id="modules-environment-loadfromasset">modules/environment/loadFromAsset</h2> <pre><code class="language-lua">loadFromAsset(name: string): (boolean, string?) </code></pre> <p>Load a persisted global reflection asset into slot 0 and make it the active single reflection (one full-coverage probe).</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Source asset identity (reads <code>/source/<name>.texture/</code>).</li> </ul> <pre><code class="language-lua">environment.loadFromAsset("env_main") </code></pre> <h2 id="modules-environment-loadslotfromasset">modules/environment/loadSlotFromAsset</h2> <pre><code class="language-lua">loadSlotFromAsset(name: string, slot: number): (boolean, string?) </code></pre> <p>Load a persisted <code>faces6</code> <code>.texture</code> cubemap (written by <code>captureSlotToAsset</code>) into reflection-probe <code>slot</code> WITHOUT re-rendering the scene. Reads the 6 face PNGs from <code>/source/<name>.texture/</code> and uploads them into the slot's cube layers. How a persisted probe restores its baked environment on reload.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Source asset identity (reads <code>/source/<name>.texture/</code>).</li> <li><code>slot</code> <code>number</code> — Reflection-probe slot (0-based).</li> </ul> <pre><code class="language-lua">environment.loadSlotFromAsset("probe_lobby", 0) </code></pre> <h2 id="modules-environment-setprobes">modules/environment/setProbes</h2> <pre><code class="language-lua">setProbes(probes: { any }): boolean </code></pre> <p>Set the active reflection probes' blend data. <code>probes</code> is an array of <code>{ x, y, z, radius }</code> (or <code>{ position = {x,y,z}, radius = r }</code>); index i is probe slot i. Surfaces blend the probe slots by proximity to these positions, gathering the highest <code>priority</code> first — each rank takes the coverage the ranks above it left, so a small interior probe ranked above a large exterior one wins outright wherever it reaches full weight. Coverage left over reflects the sky once <code>captureSky</code> has run. Queued for next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>probes</code> <code>{ any }</code> — Array of <code>{ x, y, z, radius, priority? }</code>, one per active probe slot. <code>priority</code> defaults to 0.</li> </ul> <pre><code class="language-lua">environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } }) </code></pre> <h2 id="modules-environment-setskyfallback">modules/environment/setSkyFallback</h2> <pre><code class="language-lua">setSkyFallback(active: boolean): boolean </code></pre> <p>Arm or disarm the sky fallback against the sky already captured, with no recapture. Disarmed, reflections come from the probes alone. Arming is refused while the sky slot holds no capture (<code>captureSky</code> fills it), since an uncaptured slot reflects black; <code>renderer.reflectionEnvironment()</code> reports whether the fallback ended up armed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>active</code> <code>boolean</code> — Whether reflections fall back to the captured sky.</li> </ul> <pre><code class="language-lua">environment.setSkyFallback(false) </code></pre> <h2 id="modules-environment-statefromasset">modules/environment/stateFromAsset</h2> <pre><code class="language-lua">stateFromAsset(identity: string): (boolean, string?) </code></pre> <p>Make a <code>faces6</code> <code>.texture</code> cubemap the environment that cameras stating it reflect. A camera rendering an isolated environment reflects only what it states — never the world's sky or reflection probes — so this is what a mirror or a pane of glass in its frame shows. The cube is stated under <code>identity</code>, which is the name a camera's <code>environmentMap</code> gives it; stating one already stated, from the same bytes, reads nothing. Called from a task, it waits for faces whose bytes are still being fetched (the web build fetches builtin images); called outside one, a face not yet fetched is answered as missing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>identity</code> <code>string</code> — The cube texture, by the identity <code>asset.resolve</code> takes.</li> </ul> <pre><code class="language-lua">task.spawn(environment.stateFromAsset, "@builtin::textures.studio_environment") </code></pre> <h2 id="modules-exposure-readme">modules/exposure/README</h2> <pre><code class="language-lua">require("@builtin::systems.exposure.exposure") -- exposure </code></pre> <p>Exposure in stops, and eye adaptation — the image settles when the scene gets brighter or darker instead of clipping or going black.</p> <p>Usage: local exposure = require("@builtin::systems.exposure.exposure")</p> <h2 id="modules-exposure-active">modules/exposure/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the exposure passes are currently running.</p> <pre><code class="language-lua">if exposure.active() then print("metering") end </code></pre> <h2 id="modules-exposure-buffers">modules/exposure/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The buffers the exposure passes read: <code>params</code> carries the settings this module packs, <code>state</code> is the adaptation value the GPU owns between frames. The render feature binds what this hands it.</p> <pre><code class="language-lua">local b = exposure.buffers() </code></pre> <h2 id="modules-exposure-clear">modules/exposure/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Turn exposure off and release the passes. The display goes back to the exposure that stood before the metering started. The other settings are kept, so a later <code>set</code> brings back the same behaviour.</p> <pre><code class="language-lua">exposure.clear() </code></pre> <h2 id="modules-exposure-get">modules/exposure/get</h2> <pre><code class="language-lua">get(): ExposureState </code></pre> <p>The exposure settings currently in force.</p> <pre><code class="language-lua">local ev = exposure.get().compensation </code></pre> <h2 id="modules-exposure-set">modules/exposure/set</h2> <pre><code class="language-lua">set(opts: ExposureOpts?): ExposureState </code></pre> <p>Set the scene's exposure. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. With <code>auto</code> off and <code>compensation</code> at 0 nothing is being asked for and the passes are released. Each setting names the interval it means something in, and a value outside that interval is refused naming the field, the value and the interval — the whole call, so the scene stays on the exposure it already had. The settings this returns are therefore the settings the frame is rendered with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>ExposureOpts?</code> <em>(optional)</em> — Exposure settings — see <code>ExposureOpts</code>.</li> </ul> <pre><code class="language-lua">exposure.set({ auto = true, targetGrey = 0.18, meter = "centre" }) </code></pre> <h2 id="modules-exposure-tick">modules/exposure/tick</h2> <pre><code class="language-lua">tick(dt: number) </code></pre> <p>Advance adaptation by <code>dt</code> seconds. Adaptation is a rate, so the passes need the frame's own delta to move at the same speed whatever the frame rate. The <code>AutoExposure</code> component calls this; code driving the module directly calls it once a frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>dt</code> <code>number</code> — Seconds since the last frame.</li> </ul> <pre><code class="language-lua">exposure.tick(dt) </code></pre> <h2 id="modules-field-constraints-readme">modules/field_constraints/README</h2> <pre><code class="language-lua">require("@builtin::modules.field_constraints") -- field_constraints </code></pre> <p>Generic field-constraint dispatch. A Field descriptor may carry an opaque <code>constraint</code> table with a <code>kind</code>; the engine calls <code>_G.__zero_check_field_constraint(value, constraint)</code> on every write to (and default of) a constrained field. This module owns that hook and the validator registry — the engine core carries the constraint verbatim and never interprets it.</p> <p>Usage: local field_constraints = require("@builtin::modules.field_constraints")</p> <h2 id="modules-field-constraints-register">modules/field_constraints/register</h2> <pre><code class="language-lua">register(kind: string, fn: Validator) </code></pre> <p>Register the validator for a constraint kind. A kind belongs to the chunk its validator is defined in. Registering it again from that chunk (its module running again, on a hot reload or after its require cache entry was dropped) replaces the validator; registering it with a validator defined in another chunk is an error naming both.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string</code> — The constraint kind string (matches <code>constraint.kind</code>).</li> <li><code>fn</code> <code>Validator</code> — <code>(value, constraint) -> ok, reason?</code>.</li> </ul> <pre><code class="language-lua">FieldConstraints.register("dataContract", checkDataContract) </code></pre> <h2 id="modules-fluid-readme">modules/fluid/README</h2> <pre><code class="language-lua">require("@builtin::systems.fluidSim.fluid") -- fluid </code></pre> <p>Grid-based GPU fluid simulation — smoke, fire and gas that curls, rolls and is deflected by forces, rather than translating rigidly the way billboard particles do.</p> <p>Usage: local fluid = require("@builtin::systems.fluidSim.fluid")</p> <h2 id="modules-fluid-addforce">modules/fluid/addForce</h2> <pre><code class="language-lua">addForce(self: any, opts: ForceOpts) </code></pre> <p>Place a directional force. Same one-step lifetime as a source, so a sustained wind is re-applied each frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>opts</code> <code>ForceOpts</code> — Force placement and direction — see <code>ForceOpts</code>.</li> </ul> <pre><code class="language-lua">sim:addForce({ position = { 2, 1, 0 }, direction = { -1, 0, 0 }, strength = 5 }) </code></pre> <h2 id="modules-fluid-addsource">modules/fluid/addSource</h2> <pre><code class="language-lua">addSource(self: any, opts: SourceOpts) </code></pre> <p>Place a density/heat source. It emits for one step, so a continuous plume calls this each frame — which is also what lets emission follow a moving object without any separate binding.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>opts</code> <code>SourceOpts</code> — Source placement and emission — see <code>SourceOpts</code>.</li> </ul> <pre><code class="language-lua">sim:addSource({ position = { 0, 1, 0 }, density = 1.0, temperature = 400 }) </code></pre> <h2 id="modules-fluid-create">modules/fluid/create</h2> <pre><code class="language-lua">create(opts: FluidOpts?): any </code></pre> <p>Create a fluid simulation over a world-space box.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>FluidOpts?</code> <em>(optional)</em> — Grid and solver settings — see <code>FluidOpts</code>.</li> </ul> <pre><code class="language-lua">local sim = fluid.create({ resolution = { 64, 64, 64 } }) </code></pre> <h2 id="modules-fluid-destroy">modules/fluid/destroy</h2> <pre><code class="language-lua">destroy(self: any) </code></pre> <p>Release every GPU resource the simulation owns. The handle is unusable afterwards.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">sim:destroy() </code></pre> <h2 id="modules-fluid-step">modules/fluid/step</h2> <pre><code class="language-lua">step(self: any, dt: number) </code></pre> <p>Advance the simulation one step. Emitters added since the last step are consumed here, so they act exactly once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>dt</code> <code>number</code> — Step length in seconds.</li> </ul> <pre><code class="language-lua">sim:step(1 / 60) </code></pre> <h2 id="modules-fluid-velocitytexture">modules/fluid/velocityTexture</h2> <pre><code class="language-lua">velocityTexture(self: any): string </code></pre> <p>The 3D texture currently holding the velocity field. Velocity changes texture each step, so read this when you need it rather than caching it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local vel = sim:velocityTexture() </code></pre> <h2 id="modules-fog-readme">modules/fog/README</h2> <pre><code class="language-lua">require("@builtin::systems.atmosphere.fog") -- fog </code></pre> <p>Scene-wide exponential height fog with directional in-scattering. Density falls off with altitude, so valleys pool fog while hilltops clear, and the medium brightens toward the sun so looking into it is not the same as looking away from it.</p> <p>Usage: local fog = require("@builtin::systems.atmosphere.fog")</p> <h2 id="modules-fog-active">modules/fog/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the fog pass is running this frame.</p> <pre><code class="language-lua">if fog.active() then ... end </code></pre> <h2 id="modules-fog-clear">modules/fog/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Turn fog off and release the pass. The other settings are kept, so a later <code>set({ density = ... })</code> brings back the same look.</p> <pre><code class="language-lua">fog.clear() </code></pre> <h2 id="modules-fog-get">modules/fog/get</h2> <pre><code class="language-lua">get(): FogState </code></pre> <p>The fog settings currently in force.</p> <pre><code class="language-lua">local d = fog.get().density </code></pre> <h2 id="modules-fog-paramsbuffer">modules/fog/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.</p> <pre><code class="language-lua">local p = fog.paramsBuffer() </code></pre> <h2 id="modules-fog-set">modules/fog/set</h2> <pre><code class="language-lua">set(opts: FogOpts?): FogState </code></pre> <p>Set the scene's fog. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. A <code>density</code> of 0 turns fog off and releases the pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>FogOpts?</code> <em>(optional)</em> — Fog settings — see <code>FogOpts</code>.</li> </ul> <pre><code class="language-lua">fog.set({ density = 0.03, heightFalloff = 0.12, heightRef = 0, inscatter = 0.85 }) </code></pre> <h2 id="modules-font-readme">modules/font/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.font") -- font (also available as global 'font') </code></pre> <p>Font primitive — parse a font file ONCE into a baked, vectorized glyph format (<code>ZFNT</code>), then drive every text surface from it. A font is a general CPU resource addressed by name (the CPU counterpart of a renderer GPU resource), so a single registration is usable from UI text, 2D text, and true 3D glyph geometry. Public Luau surface over the <code>__font</code> Internal FFI namespace. Authored fonts are <code>.font</code> assets whose <code>onRegister</code> hook calls <code>font.register</code>.</p> <p>Usage: local font = require("@builtin::modules.api.engine.font") Also available as global: font</p> <h2 id="modules-font-coverage">modules/font/coverage</h2> <pre><code class="language-lua">coverage(bytes: buffer | string): string? </code></pre> <p>The codepoints a font file maps to a glyph, as the coverage text <code>font.declare</code> reads: comma-separated inclusive hexadecimal ranges.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — Raw font-file bytes (TTF / OTF).</li> </ul> <pre><code class="language-lua">local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf"))) </code></pre> <h2 id="modules-font-declare">modules/font/declare</h2> <pre><code class="language-lua">declare(name: string, coverage: string, opts: table): number </code></pre> <p>Declare an on-demand face: a face known by the codepoints it covers, whose bytes are read only when a text surface first meets one of those codepoints with no installed face that draws it. Until then the face costs its coverage and nothing else, so a session that never shows its script never reads its file. When a label, a <code>text.*</code> object, or <code>font.prefetch</code> reaches a covered character, the engine requests the face: its file is read and registered with <code>font.register(name, bytes, opts)</code>, and every text surface draws the character from it from then on. A <code>.font</code> asset whose <code>face.json</code> lists <code>onDemand</code> chunks declares each of them through this call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The name the face is declared and registered under.</li> <li><code>coverage</code> <code>string</code> — The codepoints it covers: comma-separated inclusive hexadecimal ranges (<code>"3040-30ff,4e00"</code>), the text <code>font.coverage(bytes)</code> writes.</li> <li><code>opts</code> <code>table</code> — <code>{ file: string, family: string?, role: string?, aliases: {string}? }</code> naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and the <code>font.register</code> options it installs with.</li> </ul> <pre><code class="language-lua">font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" }) </code></pre> <h2 id="modules-font-glyph">modules/font/glyph</h2> <pre><code class="language-lua">glyph(name: string, codepoint: number): any </code></pre> <p>Read one glyph's vectorized outline from a registered font, in font units (resolution-independent — scale by <code>fontSize / unitsPerEm</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Registered family name.</li> <li><code>codepoint</code> <code>number</code> — Unicode codepoint (e.g. <code>string.byte("A")</code>).</li> </ul> <pre><code class="language-lua">local g = font.glyph("Inter", string.byte("A")) </code></pre> <h2 id="modules-font-list">modules/font/list</h2> <pre><code class="language-lua">list(): { string } </code></pre> <p>List every registered font family name.</p> <pre><code class="language-lua">for _, fam in font.list() do print(fam) end </code></pre> <h2 id="modules-font-observe">modules/font/observe</h2> <pre><code class="language-lua">observe(): { any } </code></pre> <p>What the text system is holding for fonts: one row per family the shaper can resolve, with its face count, the numeric weights those faces carry, whether any of them is slanted, and whether the family arrived through a registration rather than from the platform. <code>weights</code> is what a style's <code>weight</code> can name for that family. The same rows are <code>fonts</code> in <code>text.observe()</code>.</p> <pre><code class="language-lua">for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end </code></pre> <h2 id="modules-font-ondemand">modules/font/onDemand</h2> <pre><code class="language-lua">onDemand(): { any } </code></pre> <p>Every on-demand face <code>font.declare</code> declared, in declaration order, and where each stands: <code>declared</code> (its file never read), <code>requested</code> (a character it covers reached a text surface that could not draw it, and its file is being read), <code>installed</code> (registered on every text surface; <code>bytes</code> is the size of the file it installed from) or <code>failed</code> (<code>error</code> says why). <code>requestedBy</code> is the character whose miss requested it, and <code>family</code> the family group it installs into (<code>opts.family</code> of its <code>font.declare</code>, else its own name); <code>ui.listFonts()</code> lists each face under that family's row.</p> <pre><code class="language-lua">for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end </code></pre> <h2 id="modules-font-parse">modules/font/parse</h2> <pre><code class="language-lua">parse(bytes: buffer | string): string? </code></pre> <p>Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized glyph format (<code>ZFNT</code>): per-glyph vector outlines + metrics + character map, plus the original bytes. Heavy — run at import time (the <code>.font</code> assetType's onCreate / the font importer), then store the result as the asset payload. <code>font.register</code> loads it cheaply.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — Raw font-file bytes (binary-safe) — TTF / OTF.</li> </ul> <pre><code class="language-lua">local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf"))) </code></pre> <h2 id="modules-font-prefetch">modules/font/prefetch</h2> <pre><code class="language-lua">prefetch(text: string): number </code></pre> <p>Request, ahead of drawing it, every on-demand face covering a character of <code>text</code> that no requested or installed face covers: a screen about to show a script starts reading its faces before its first frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — Text whose characters the faces must cover.</li> </ul> <pre><code class="language-lua">font.prefetch("日本語") </code></pre> <h2 id="modules-font-ready">modules/font/ready</h2> <pre><code class="language-lua">ready(text: string): boolean </code></pre> <p>Whether <code>text</code> draws without waiting on an on-demand face: no character of it is covered by a face still <code>declared</code> or <code>requested</code>. Call <code>font.prefetch(text)</code> first, then open the screen once this answers true, and its first frame draws every glyph. True as well for a character no declared face covers (<code>ui.missingGlyphs()</code> names it once drawn) and for a face that failed (<code>font.onDemand()</code> says why).</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — Text to be drawn.</li> </ul> <pre><code class="language-lua">font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end) </code></pre> <h2 id="modules-font-reconcile">modules/font/reconcile</h2> <pre><code class="language-lua">reconcile(): { any } </code></pre> <p>Every family the text shaper can resolve, held against what the shaper does with it. <code>family</code> is the name, <code>faces</code> how many faces of it the font database holds, <code>weights</code> the numeric weights those faces carry, <code>loaded</code> whether it arrived through a registration rather than from the platform, <code>registered</code> whether content registered the name, <code>selectable</code> whether some style naming the family reaches it, <code>matched</code> whether <code>fontFamily = family</code> on its own reaches it — the family name at the default weight over Latin text — <code>weight</code> the weight it needs when the default is not it, <code>shapedWith</code> the family the name selects, spelled the way the font file spells it, which for a registration name is the family declared inside the file rather than the name the asset carries, and <code>reason</code> why when the face that answered is not that family. A family is probed at its own weights and over content from several scripts, so a family reachable only at one weight or covering only one script is reported selectable, with <code>matched</code> false and <code>weight</code> naming what the style must carry. Every probe object is destroyed again, so the live text-object count is where it was.</p> <pre><code class="language-lua">for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end </code></pre> <h2 id="modules-font-register">modules/font/register</h2> <pre><code class="language-lua">register(name: string, zfnt: string, opts: table?): any </code></pre> <p>Register a baked font (<code>ZFNT</code> from <code>font.parse</code>) under <code>name</code>, making it usable on every text surface via <code>fontFamily = "<name>"</code>. Loads the vectorized glyph data into the runtime store (for <code>font.glyph</code> / <code>font.textMesh</code>) and feeds the embedded face to the 2D text and egui UI systems. Raw font bytes (TTF / OTF) register too: their tables are read and the glyph outlines are baked the first time <code>font.glyph</code> / <code>font.textMesh</code> reads one. A name declared with <code>font.declare</code> is installed by this call. Registering a name again replaces what the earlier call installed under it: a name, group or slot the earlier call gave the face and this one does not stops selecting it, and text laid out under a name whose face changed is laid out again. <code>opts</code> groups several weight/style faces under one CSS family and maps web-font names onto it: <code>opts.family</code> is the shared group key, <code>opts.role</code> is <code>"regular" | "bold" | "italic" | "bolditalic"</code>, and <code>opts.aliases</code> is a list of extra selectable names (web fonts + CSS generics like <code>"Arial"</code>, <code>"sans-serif"</code>) that resolve to this group, matched case-insensitively. With a group set, <code>font-weight</code> / <code>font-style</code> on a <code>font-family</code> pick the real metric-compatible face instead of a synthesized one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Family name to register under.</li> <li><code>zfnt</code> <code>string</code> — Baked <code>ZFNT</code> payload from <code>font.parse</code> (binary-safe string).</li> <li><code>opts</code> <code>table?</code> <em>(optional)</em> — <code>{ family: string?, role: string?, aliases: {string}? }</code> — group key, weight/style role, and case-insensitive selectable aliases.</li> </ul> <pre><code class="language-lua">local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf"))))) </code></pre> <h2 id="modules-font-textmesh">modules/font/textMesh</h2> <pre><code class="language-lua">textMesh(name: string, text: string, opts: table?): any </code></pre> <p>Tessellate a string into renderable mesh geometry from a registered font's glyph outlines — true 3D text, laid out left-to-right by advance (newlines drop a line). Hand the result to <code>renderer.mesh.create()</code> (GPU) or <code>asset.create("mesh")</code> (persistable).</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Registered family name.</li> <li><code>text</code> <code>string</code> — String to lay out.</li> <li><code>opts</code> <code>table?</code> <em>(optional)</em> — <code>{ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }</code>.</li> </ul> <pre><code class="language-lua">local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 }) </code></pre> <h2 id="modules-framestream-readme">modules/frameStream/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.frameStream") -- frameStream (also available as global 'frameStream') </code></pre> <p>Carries an image the GPU drew out to another program — the view a camera renders, read back off its render target and written into a byte stream frame after frame, so pixels that sit in GPU memory reach a process outside the engine while the world runs. Public Luau surface over the <code>__framestream</code> Internal FFI namespace.</p> <p>Usage: local frameStream = require("@builtin::modules.api.engine.frameStream") Also available as global: frameStream</p> <h2 id="modules-framestream-attach">modules/frameStream/attach</h2> <pre><code class="language-lua">attach(texture: string, stream: string, opts: AttachOpts?): (string?, string?) </code></pre> <p>Carry an image the GPU drew out to an open byte stream, frame after frame. <code>texture</code> is the guid of the render target it was drawn into — <code>renderer.texture.create({ width = W, height = H })</code> makes one, and a Camera component draws into it as its <code>textureHandle</code>; the session reads that target back when a frame comes due, so what the camera drew last reaches the far end. <code>stream</code> is a handle from <code>stream.open</code>. What reaches the stream is one frame's pixels then the next frame's, with nothing between them: a frame is <code>width * height * bytesPerPixel</code> bytes of tight rows, written in a single call so a consumer reads a whole frame or none of it. Each frame is read back off the render thread, so the stream never holds the renderer up. <code>fps</code> caps how often a frame is taken and defaults to one per rendered frame; <code>format</code> accepts <code>"rgb24"</code> (3 bytes per pixel, the default) or <code>"rgba8"</code> (4) — a call with a format outside those two raises, naming both; <code>flipY</code> writes the last texture row first. Returns the session handle, or nil and the reason an empty texture, a handle naming no open stream, a stream another session already carries, or a non-positive fps was refused with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string</code> — Guid of the render target the image was drawn into (a Camera's textureHandle).</li> <li><code>stream</code> <code>string</code> — Stream handle from stream.open.</li> <li><code>opts</code> <code>AttachOpts?</code> <em>(optional)</em> — Rate, pixel layout and row order (optional).</li> </ul> <pre><code class="language-lua">local session = frameStream.attach(rt.guid, handle, { fps = 30 }) </code></pre> <h2 id="modules-framestream-detach">modules/frameStream/detach</h2> <pre><code class="language-lua">detach(handle: string): boolean </code></pre> <p>End the session and free the staging buffers it read frames back through. The stream stays open — whoever opened it closes it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Session handle from frameStream.attach.</li> </ul> <pre><code class="language-lua">frameStream.detach(session) </code></pre> <h2 id="modules-framestream-list">modules/frameStream/list</h2> <pre><code class="language-lua">list(): { string } </code></pre> <p>Every live session handle, in a stable order.</p> <pre><code class="language-lua">for _, h in frameStream.list() do frameStream.detach(h) end </code></pre> <h2 id="modules-framestream-status">modules/frameStream/status</h2> <pre><code class="language-lua">status(handle: string): FrameStreamStatus? </code></pre> <p>Report what the session has carried and lost. <code>frames</code> counts the frames the stream accepted and <code>bytes</code> the bytes they carried. <code>dropped</code> counts the frames it refused, of which <code>droppedBackpressure</code> is the part refused because the consumer was behind; <code>stalledReadbacks</code> counts the frames that came due while every staging buffer still held a copy on its way from the GPU. <code>achievedFps</code> is the rate the accepted frames arrived at, across the span from the first to the most recent, and reads 0 until two have been accepted — compare it against <code>requestedFps</code> to see a display running slower than it was asked to. <code>lastCopyFrame</code> is the engine frame, in <code>time.frameCount()</code> numbering, the copy of the most recent accepted frame was taken on, and is nil until one is accepted: every copy one engine frame takes of a texture holds the same image, so two sessions on one texture whose <code>lastCopyFrame</code> agree hold the same image as the newest frame of their streams. <code>lastOutcome</code> names what became of the most recent frame offered. nil when handle names no live session.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Session handle from frameStream.attach.</li> </ul> <pre><code class="language-lua">local s = frameStream.status(session); print(s.frames, s.dropped, s.achievedFps) </code></pre> <h2 id="modules-future-readme">modules/future/README</h2> <pre><code class="language-lua">require("@builtin::modules.future") -- future </code></pre> <p>Futures: a promise as a value you hold, pass around, combine and await later. Every promise-returning call (<code>http.get_json</code>, <code>delay</code>, <code>scene.save</code>, ...) already hands back a future, a <code>PromiseHandle</code>; this module makes new ones, runs work as one, chains them (<code>f:andThen</code>, <code>f:catch</code>, <code>f:finally</code>), waits for several at once (<code>all</code>, <code>race</code>, <code>any</code>, <code>allSettled</code>), bounds one in time (<code>f:timeout</code>) and cancels them (<code>f:cancel</code>).</p> <p>A future is a <code>PromiseHandle</code>. Store it in a table, return it from a function, hand it to another task, and await it whenever the value is needed: local future = require("@builtin::modules.future") local pending: { Future<any> } = {} table.insert(pending, http.get_json(urlA)) table.insert(pending, http.get_json(urlB)) -- both requests are in flight; collect them in order for i, f in pending do results[i] = f:await() end Combine them instead of awaiting one after another: -- every result, in order; the first failure fails the lot local both = future.all({ http.get_json(urlA), http.get_json(urlB) }):await() -- whichever settles first; the others are cancelled local first = future.race({ future.spawn(searchNear), future.spawn(searchFar) }):await() -- no answer within 5 seconds rejects, and cancels the load local level = future.spawn(loadLevel, "forest"):timeout(5):await() Members every future answers: f:await() yield until it settles and return its value; one that has already settled returns at once, with no yield. A rejection raises its error at this line (the same call as <code>await(f)</code> and <code>task.await(f)</code>) f:state() "pending", "resolved", "rejected" or "cancelled", read without taking the value f:rejection() why it was rejected, <code>{ message, origin?, stack? }</code>: the reason as text, the <code>chunk:line</code> the rejection came from, and the stack there when Luau rejected it; nil while it is pending or once it resolved f:source() the <code>chunk:line</code> whose call created the promise f.handle the promise's id string; <code>tostring(f)</code> reads the same f:andThen(onResolve, onReject?) a future of what the handler returns f:catch(onReject) a future that recovers from a rejection f:finally(fn) a future settled as <code>f</code> was, once <code>fn</code> has run f:tap(fn) a future of <code>f</code>'s value, once <code>fn</code> has seen it f:timeout(seconds, reason?) a future of <code>f</code>'s value that rejects when <code>seconds</code> of real time pass first f:cancel() end it as cancelled, and the work producing it with it A rejection is kept apart from any value: a future resolved with a table of any shape delivers that table. Catch one with <code>pcall</code>, and read where it came from on the future: local ok, err = pcall(f.await, f) if not ok then local why = f:rejection() log.warn(<code>{why.message} (from {why.origin})</code>) end Every task that awaits a future receives its value: the ones waiting when it settles and any that await it later, while the future is held. Cancellation. <code>f:cancel()</code> ends a pending future as cancelled: awaiting it raises the cancellation <code>task.isCancellation(err)</code> recognises, and <code>f:state()</code> reads "cancelled". The work producing it ends with it: the task <code>future.spawn</code> or a handler runs in is cancelled and unwinds (the cleanup around its wait runs), and so is everything that work started. Cancelling reaches both ways along a chain:</p> <ul> <li>downstream: a future chained from a cancelled one (<code>andThen</code>, <code>catch</code>, <code>tap</code>, <code>timeout</code>, <code>all</code>, <code>race</code>, ...) is cancelled too; <code>finally</code> runs its function first;</li> <li>upstream: a future counts the chains and combinators waiting on it, and one whose last such consumer is cancelled or has settled without it (a <code>race</code> another input won, a <code>timeout</code> that ran out) is cancelled, so work nobody waits for any more stops. A direct <code>await</code> is not a consumer. A future an engine call handed back (<code>http.get_json</code>, <code>delay</code>) runs to its end whatever waits on it, and <code>f:cancel()</code> answers false and the reason for one. Where the work lives: the task running a handler or <code>future.spawn</code> belongs to what started it (the component instance whose code called it, the scene load, an enclosing <code>scopes.run</code>), so the end of that cancels the future too. A handler's work runs in a <code>scopes.run</code>: the future settles once everything the handler started is over, and an error any of it raises rejects the future. Identity: a VM holds one future object per promise. Every call that hands a promise to the VM while its future is alive, <code>future.wrap</code> included, hands back that same object, so <code>future.wrap(f) == f</code>, <code>rawequal(future.wrap(f.handle), f)</code>, and a future is one table key. In type annotations, <code>Future<T></code> is a future whose await delivers a <code>T</code>, and <code>PromiseHandle</code> is a future whose value has no stated type; the two are the same value.</li> </ul> <p>Usage: local future = require("@builtin::modules.future")</p> <h2 id="modules-future-after">modules/future/after</h2> <pre><code class="language-lua">after(seconds: number): Future<boolean> </code></pre> <p>A future that resolves with true after <code>seconds</code> of real time, the engine's own clock that <code>engine.timeScale</code> does not reach. The same promise <code>delay(seconds)</code> hands back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>seconds</code> <code>number</code> — Seconds of real time until it resolves.</li> </ul> <pre><code class="language-lua">local timeout = future.after(5) </code></pre> <h2 id="modules-future-all-t">modules/future/all<T></h2> <pre><code class="language-lua">all<T>(futures: { Future<T> }): Future<{ T }> </code></pre> <p>A future of every value <code>futures</code> resolve with, in the order given, once all have resolved. The first one to reject rejects it with that error, and the first one cancelled cancels it; either way the ones still pending are let go of, and each nothing else waits on is cancelled. An empty array resolves with an empty table.</p> <p><strong>Parameters</strong></p> <ul> <li><code>futures</code> <code>{ Future<T> }</code> — An array of futures.</li> </ul> <pre><code class="language-lua">local maps = future.all({ http.get_json(a), http.get_json(b) }):await() </code></pre> <h2 id="modules-future-allsettled">modules/future/allSettled</h2> <pre><code class="language-lua">allSettled(futures: { Future<any> }): Future<{ Settlement }> </code></pre> <p>A future of how each of <code>futures</code> settled, in the order given, once all have: <code>{ status = "resolved", value = v }</code>, <code>{ status = "rejected", error = e }</code> or <code>{ status = "cancelled" }</code>. It never rejects.</p> <p><strong>Parameters</strong></p> <ul> <li><code>futures</code> <code>{ Future<any> }</code> — An array of futures.</li> </ul> <pre><code class="language-lua">for _, s in future.allSettled(loads):await() do print(s.status) end </code></pre> <h2 id="modules-future-andthen">modules/future/andThen</h2> <pre><code class="language-lua">andThen(self: any, onResolve: ((any) -> any)?, onReject: ((any) -> any)?): any </code></pre> <p>A future of what <code>onResolve</code> returns once this future resolves; a future it returns is awaited and its value delivered. With <code>onReject</code>, a rejection is handed to it and the future is what it returns; without, the rejection passes through. A handler that raises rejects the future with its error. The handler runs in a task (so it may wait) inside a <code>scopes.run</code>. A cancellation passes through without calling either.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>onResolve</code> <code>((any) -> any)?</code> <em>(optional)</em> — Optional: called with the value.</li> <li><code>onReject</code> <code>((any) -> any)?</code> <em>(optional)</em> — Optional: called with the error.</li> </ul> <pre><code class="language-lua">http.get_json(url):andThen(function(doc) return doc.levels end) </code></pre> <h2 id="modules-future-any-t">modules/future/any<T></h2> <pre><code class="language-lua">any<T>(futures: { Future<T> }): Future<T> </code></pre> <p>A future of the first value any of <code>futures</code> resolves with; the rest are let go of, and each nothing else waits on is cancelled. When none resolves it rejects with an AnyRejected, <code>{ message, errors }</code>, holding each one's error in the order given; when every one was cancelled it is cancelled. An empty array raises.</p> <p><strong>Parameters</strong></p> <ul> <li><code>futures</code> <code>{ Future<T> }</code> — A non-empty array of futures.</li> </ul> <pre><code class="language-lua">local mirror = future.any({ http.get_json(primary), http.get_json(backup) }):await() </code></pre> <h2 id="modules-future-cancel">modules/future/cancel</h2> <pre><code class="language-lua">cancel(self: any): (boolean, string?) </code></pre> <p>End this future as cancelled, when it is still pending: every await of it raises the cancellation <code>task.isCancellation</code> recognises, the work producing it is cancelled and unwinds, the futures chained from it are cancelled, and each future it waited on that nothing else waits on is cancelled. Answers true when this call cancelled it, or false and the reason: it had already settled, or an engine call or another VM settles it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local ok = load:cancel() </code></pre> <h2 id="modules-future-catch">modules/future/catch</h2> <pre><code class="language-lua">catch(self: any, onReject: (any) -> any): any </code></pre> <p>A future that recovers from this one's rejection: resolved with this future's value, or with what <code>onReject</code> returns given the error. A cancellation passes through.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>onReject</code> <code>(any) -> any</code> — Called with the error.</li> </ul> <pre><code class="language-lua">local doc = http.get_json(url):catch(function() return defaults end):await() </code></pre> <h2 id="modules-future-finally">modules/future/finally</h2> <pre><code class="language-lua">finally(self: any, fn: () -> ()): any </code></pre> <p>A future that settles the way this one did, once <code>fn()</code> has run after it settled, whichever way, cancelled included. <code>fn</code> raising rejects it with that error instead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>fn</code> <code>() -> ()</code> — Called with no arguments once this future settles.</li> </ul> <pre><code class="language-lua">load:finally(function() hud.hideLoading() end) </code></pre> <h2 id="modules-future-fromevent">modules/future/fromEvent</h2> <pre><code class="language-lua">fromEvent(event: any, predicate: ((...any) -> boolean)?): Future<any> </code></pre> <p>A future of the next fire of <code>event</code> that <code>predicate</code> accepts: it resolves with the first value that fire carried. <code>event</code> is a Signal, a component event (<code>comp.events.onHit</code>), or anything offering <code>:Connect(fn)</code> / <code>:connect(fn)</code>. Its handler is disconnected once the future settles or is cancelled. The listener belongs to what started the call (a component instance, a scene load, a task scope), so the end of that cancels the future. A predicate that raises rejects the future with its error.</p> <p><strong>Parameters</strong></p> <ul> <li><code>event</code> <code>any</code> <em>(optional)</em> — The event to listen to.</li> <li><code>predicate</code> <code>((...any) -> boolean)?</code> <em>(optional)</em> — Optional: called with the fire's values; the future resolves on the first fire it answers true for.</li> </ul> <pre><code class="language-lua">local damage = future.fromEvent(hit, function(amount) return amount > 10 end):await() </code></pre> <h2 id="modules-future-new">modules/future/new</h2> <pre><code class="language-lua">new(onCancel: (() -> ())?): (Future<any>, Resolve, Reject) </code></pre> <p>Make a pending future and the two functions that settle it. <code>resolve(value)</code> delivers <code>value</code> to every task awaiting the future, the value itself in this VM; <code>reject(error)</code> raises <code>error</code> at each of their awaits: a string at the await's line, any other value (an error table) as itself. The future's <code>rejection()</code> reads the message, the line that called <code>reject</code> and the stack there. A future settles once: each function answers true when its call settled it, or false and the reason (it had already settled). Only this VM settles it. <code>onCancel</code> runs when the future is cancelled, by <code>f:cancel()</code> or because the last chain or combinator waiting on it let it go: release what producing its value holds there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>onCancel</code> <code>(() -> ())?</code> <em>(optional)</em> — Optional: called with no arguments when the future is cancelled.</li> </ul> <pre><code class="language-lua">local f, resolve = future.new(); task.delay(2, function() resolve(true) end); f:await() </code></pre> <h2 id="modules-future-race-t">modules/future/race<T></h2> <pre><code class="language-lua">race<T>(futures: { Future<T> }): Future<T> </code></pre> <p>A future that settles the way the first of <code>futures</code> to settle does: resolved with its value, rejected with its error, or cancelled. The rest are let go of, and each nothing else waits on is cancelled. An empty array never settles, so it raises.</p> <p><strong>Parameters</strong></p> <ul> <li><code>futures</code> <code>{ Future<T> }</code> — A non-empty array of futures.</li> </ul> <pre><code class="language-lua">local winner = future.race({ future.spawn(pathA), future.spawn(pathB) }):await() </code></pre> <h2 id="modules-future-rejected">modules/future/rejected</h2> <pre><code class="language-lua">rejected(err: any): Future<any> </code></pre> <p>A future already rejected with <code>err</code>, any value but nil: awaiting it raises <code>err</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>err</code> <code>any</code> <em>(optional)</em> — The error an await of it raises.</li> </ul> <pre><code class="language-lua">return future.rejected("no such level") </code></pre> <h2 id="modules-future-resolved-t">modules/future/resolved<T></h2> <pre><code class="language-lua">resolved<T>(value: T): Future<T> </code></pre> <p>A future already resolved with <code>value</code>: awaiting it returns <code>value</code> at once. For a function whose callers await its result when the answer is already known.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>T</code> — The value an await of it returns.</li> </ul> <pre><code class="language-lua">local cached = future.resolved(cache[key]); return cached </code></pre> <h2 id="modules-future-retry-t">modules/future/retry<T></h2> <pre><code class="language-lua">retry<T>(fn: () -> T, attempts: number, delaySeconds: number?): Future<T> </code></pre> <p>Call <code>fn()</code> until it succeeds, at most <code>attempts</code> times, and hand back the future of the value it succeeded with. <code>fn</code> may wait, and may return a future, which is awaited. After the last failed attempt the future rejects with that attempt's error. <code>delaySeconds</code> of real time pass between attempts. Cancelling the future cancels the attempt in progress.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> T</code> — The function to call; each call is one attempt.</li> <li><code>attempts</code> <code>number</code> — The most attempts to make, at least 1.</li> <li><code>delaySeconds</code> <code>number?</code> <em>(optional)</em> — Optional: real seconds to wait between attempts.</li> </ul> <pre><code class="language-lua">local config = future.retry(function() return http.get_json(url) end, 3, 1):await() </code></pre> <h2 id="modules-future-spawn-t">modules/future/spawn<T></h2> <pre><code class="language-lua">spawn<T>(fn: (...any) -> T, ...: any): Future<T> </code></pre> <p>Run <code>fn(...)</code> as a task and hand back the future of its result: it resolves with what <code>fn</code> returns (a future <code>fn</code> returns is awaited, and its value delivered) and rejects with what <code>fn</code> raises. <code>fn</code> may wait (<code>task.wait</code>, <code>await</code>, ...). Its work runs in a <code>scopes.run</code>: the future settles once everything <code>fn</code> started is over, and an error any of it raises rejects the future. Cancelling the future cancels that task and everything it started, which unwind.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>(...any) -> T</code> — The function to run.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local loaded = future.spawn(loadLevel, "forest") </code></pre> <h2 id="modules-future-tap">modules/future/tap</h2> <pre><code class="language-lua">tap(self: any, fn: (any) -> ()): any </code></pre> <p>A future of this one's value, once <code>fn(value)</code> has seen it; a rejection or cancellation passes through without calling <code>fn</code>. <code>fn</code> raising rejects it with that error.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>fn</code> <code>(any) -> ()</code> — Called with the value.</li> </ul> <pre><code class="language-lua">load:tap(function(level) print("loaded", level.name) end) </code></pre> <h2 id="modules-future-timeout">modules/future/timeout</h2> <pre><code class="language-lua">timeout(self: any, seconds: number, reason: any?): any </code></pre> <p>A future of this one's value that rejects with <code>reason</code> when <code>seconds</code> of real time pass before this one settles; this one is then let go of, and cancelled when nothing else waits on it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>seconds</code> <code>number</code> — Seconds of real time to wait.</li> <li><code>reason</code> <code>any?</code> <em>(optional)</em> — Optional: the error to reject with; a message naming the time by default.</li> </ul> <pre><code class="language-lua">local level = future.spawn(loadLevel):timeout(5):await() </code></pre> <h2 id="modules-future-wrap">modules/future/wrap</h2> <pre><code class="language-lua">wrap(promise: PromiseHandle | string): Future<any> </code></pre> <p>Adopt a promise: the future naming it, from its future or from its id string. A future passed in answers itself, and an id answers the future this VM already holds for that promise, when it holds one. An id naming no promise the engine keeps raises, saying whether it was released or never existed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>promise</code> <code>PromiseHandle | string</code> — A future (<code>PromiseHandle</code>) or a promise's id string.</li> </ul> <pre><code class="language-lua">local f = future.wrap(savedId); local value = f:await() </code></pre> <h2 id="modules-gaussiansplats-readme">modules/gaussianSplats/README</h2> <pre><code class="language-lua">require("@builtin::systems.gaussianSplats.gaussianSplats") -- gaussianSplats </code></pre> <p>Registry of live Gaussian splat clouds — the entity-id-keyed table the <code>gaussianSplats</code> render feature reads each frame. <code>GaussianSplat.component</code> writes the declaration into it; the feature owns every GPU buffer and reports back one thing, its verdict on whether it could build the cloud.</p> <p>Usage: local gaussianSplats = require("@builtin::systems.gaussianSplats.gaussianSplats")</p> <h2 id="modules-gaussiansplats-clear">modules/gaussianSplats/clear</h2> <pre><code class="language-lua">clear(id: string) </code></pre> <p>Drop an entity's cloud, along with the placement recorded for it. The render feature frees its GPU buffers on the next frame that finds the entry gone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> </ul> <pre><code class="language-lua">gaussianSplats.clear(id) </code></pre> <h2 id="modules-gaussiansplats-clearfailure">modules/gaussianSplats/clearFailure</h2> <pre><code class="language-lua">clearFailure(id: string) </code></pre> <p>Drop the failure standing against an entity's cloud. The feature calls this on the frame it builds the cloud.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> </ul> <pre><code class="language-lua">gaussianSplats.clearFailure(id) </code></pre> <h2 id="modules-gaussiansplats-entries">modules/gaussianSplats/entries</h2> <pre><code class="language-lua">entries(): { [string]: SplatEntry } </code></pre> <p>The live registry, keyed by entity id. Read-only for callers other than <code>GaussianSplat.component</code>.</p> <pre><code class="language-lua">for id, entry in pairs(gaussianSplats.entries()) do ... end </code></pre> <h2 id="modules-gaussiansplats-failure">modules/gaussianSplats/failure</h2> <pre><code class="language-lua">failure(id: string): string? </code></pre> <p>Why the feature could not build an entity's cloud, or nil while it has nothing against it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> </ul> <pre><code class="language-lua">local why = gaussianSplats.failure(id) </code></pre> <h2 id="modules-gaussiansplats-get">modules/gaussianSplats/get</h2> <pre><code class="language-lua">get(id: string): SplatEntry? </code></pre> <p>The entry registered for one entity, or nil.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> </ul> <pre><code class="language-lua">local e = gaussianSplats.get(id) </code></pre> <h2 id="modules-gaussiansplats-placement">modules/gaussianSplats/placement</h2> <pre><code class="language-lua">placement(id: string): { number }? </code></pre> <p>The placement an entity's cloud was last culled and drawn at — 16 column-major numbers, translation in elements 13, 14, 15. The cull transforms every splat position by this matrix and the draw conjugates each covariance by its upper-left 3x3, so it is where the cloud was cut as well as where it was drawn. A fresh table each call, so writing to it leaves the record alone. Nil before the feature has drawn the cloud, on any frame it does not draw it, and once the cloud has left the registry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> </ul> <pre><code class="language-lua">local m = gaussianSplats.placement(id) </code></pre> <h2 id="modules-gaussiansplats-recordplacement">modules/gaussianSplats/recordPlacement</h2> <pre><code class="language-lua">recordPlacement(id: string, matrix: { number }?) </code></pre> <p>Record the placement an entity's splats were culled and drawn at, as 16 column-major numbers. The render feature calls this each frame it draws the cloud, with the matrix it hands the cull pass; nil drops the record. The numbers are copied in, so the caller keeps its own matrix to itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> <li><code>matrix</code> <code>{ number }?</code> <em>(optional)</em> — The 16 column-major numbers the cloud was placed by.</li> </ul> <pre><code class="language-lua">gaussianSplats.recordPlacement(id, entity(id).worldMatrix()) </code></pre> <h2 id="modules-gaussiansplats-reportfailure">modules/gaussianSplats/reportFailure</h2> <pre><code class="language-lua">reportFailure(id: string, message: string) </code></pre> <p>Report that the feature could not build an entity's cloud. The message reaches the entry's status listener, which is how it lands on the component's error surface.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> <li><code>message</code> <code>string</code> — What the feature refused the cloud for.</li> </ul> <pre><code class="language-lua">gaussianSplats.reportFailure(id, "no capture at 'captures/room.gaussianSplat'") </code></pre> <h2 id="modules-gaussiansplats-set">modules/gaussianSplats/set</h2> <pre><code class="language-lua">set(id: string, entry: SplatEntry, onStatus: StatusListener?) </code></pre> <p>Register or update the cloud an entity renders. Changing <code>source</code> or <code>convention</code> bumps the entry's generation, which makes the render feature re-decode and re-upload on its next frame, and drops any verdict the feature reached about the capture asked for before.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> <li><code>entry</code> <code>SplatEntry</code> — The cloud's declaration.</li> <li><code>onStatus</code> <code>StatusListener?</code> <em>(optional)</em> — Called with the feature's message when it cannot build the cloud, and with nil once it can. <code>GaussianSplat.component</code> passes the call that puts the message on its own error surface.</li> </ul> <pre><code class="language-lua">gaussianSplats.set(id, { source = "captures/ceramic.spz", opacity = 1 }) </code></pre> <h2 id="modules-gaussiansplats-setvelocity">modules/gaussianSplats/setVelocity</h2> <pre><code class="language-lua">setVelocity(enabled: boolean): boolean </code></pre> <p>Whether a moving cloud writes its screen-space displacement into the frame's velocity buffer, which is what makes motion blur, temporal antialiasing, temporal upsampling and a denoiser's history term treat it as moving. On costs two screen-sized targets and two full-screen passes while any cloud is on screen. On by default.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — Whether the clouds report their motion.</li> </ul> <pre><code class="language-lua">gaussianSplats.setVelocity(false) </code></pre> <h2 id="modules-gaussiansplats-velocity">modules/gaussianSplats/velocity</h2> <pre><code class="language-lua">velocity(): boolean </code></pre> <p>Whether the clouds report their motion into the frame's velocity buffer.</p> <pre><code class="language-lua">if gaussianSplats.velocity() then ... end </code></pre> <h2 id="modules-home-readme">modules/home/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.home") -- home (also available as global 'home') </code></pre> <p>The person's home folder, <code>/zero/home/<username></code>: their own files, carried from world to world in the editor. It is the content of the account's home world, an ordinary private world the account owns, mounted in every world the person opens in their own editor sessions. A write under the folder is saved to the home world as it is made, with no commit or push. World content cannot depend on it: a world script that requires a home module is refused, and a world file that references a home path is refused at push, naming the file. The person's own <code>execute</code> and a home module requiring another in the same folder work.</p> <p>Read and write the folder with <code>vfs</code> like any other path; <code>home.status()</code> says whether it is mounted and, when it is not, why. <code>home.get()</code> answers the guid of the account's home world, or nil when the account has none, and <code>home.ensure()</code> answers it and creates it the first time it is needed. <code>guides { path: "core/home" }</code> covers what to keep there and the rule that keeps it out of worlds.</p> <p>Usage: local home = require("@builtin::modules.api.engine.home") Also available as global: home</p> <h2 id="modules-home-ensure">modules/home/ensure</h2> <pre><code class="language-lua">ensure(): (string, boolean) </code></pre> <p>The guid of the account's home world, created the first time it is needed: an account with no home world gets a new private world, owned by the person the account acts for (an agent account's is its person's), and every later call answers that same world. It is the world whose content <code>home.folder()</code> mounts; a launch with <code>--home-world-guid</code> names it for the session. Yields while ZeroMind answers. Raises, naming the reason, on an engine that booted offline, one signed in to no account, or when ZeroMind cannot be read.</p> <pre><code class="language-lua">local guid, created = home.ensure(); print(guid, created) </code></pre> <h2 id="modules-home-folder">modules/home/folder</h2> <pre><code class="language-lua">folder(): (string?, string?) </code></pre> <p>The home folder mounted in this session, <code>/zero/home/<username></code>, or nil and the reason no home folder is mounted.</p> <pre><code class="language-lua">local folder, why = home.folder() </code></pre> <h2 id="modules-home-get">modules/home/get</h2> <pre><code class="language-lua">get(): string? </code></pre> <p>The guid of the account's home world, or nil when the account has none; it never creates one (<code>home.ensure()</code> does). The same world <code>home.ensure()</code> answers, whose content <code>home.folder()</code> mounts. Yields while ZeroMind answers. Raises, naming the reason, on an engine that booted offline, one signed in to no account, or when ZeroMind cannot be read.</p> <pre><code class="language-lua">local guid = home.get(); if guid == nil then print("no home world yet") end </code></pre> <h2 id="modules-home-status">modules/home/status</h2> <pre><code class="language-lua">status(): Status </code></pre> <p>Where the home folder stands in this session. <code>state</code> is <code>"mounted"</code> (with <code>folder</code>, the path to read and write, <code>owner</code>, the account it belongs to, <code>world</code>, the guid of the home world that holds it, <code>files</code>, how many files the home world held when it was mounted, and <code>from</code>, <code>"launch"</code> or <code>"zeromind"</code>, saying where that world was named), <code>"mounting"</code> while a world load is mounting it, or <code>"unmounted"</code> with <code>reason</code> saying why no home folder is mounted.</p> <pre><code class="language-lua">local s = home.status(); if s.state == "mounted" then vfs.write(s.folder .. "/notes.md", "hi") end </code></pre> <h2 id="modules-http-readme">modules/http/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.http") -- http (also available as global 'http') </code></pre> <p>Async HTTP — GET/POST returning JSON or raw bytes. Public Luau surface over the <code>__http</code> Internal FFI namespace.</p> <p>Usage: local http = require("@builtin::modules.api.engine.http") Also available as global: http</p> <h2 id="modules-http-get-bytes">modules/http/get_bytes</h2> <pre><code class="language-lua">get_bytes(url: string, headers: Headers?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?): PromiseId </code></pre> <p>Async HTTP GET returning raw bytes (binary-safe string). Suitable for piping into <code>vfs.write</code> to download a file.</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — Request URL.</li> <li><code>headers</code> <code>Headers?</code> <em>(optional)</em> — Header key-value pairs (optional).</li> <li><code>timeoutMs</code> <code>TimeoutMs?</code> <em>(optional)</em> — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).</li> <li><code>expectedStatuses</code> <code>ExpectedStatuses?</code> <em>(optional)</em> — HTTP statuses this caller handles as an answer it expected, e.g. <code>{ 404 }</code>: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).</li> </ul> <pre><code class="language-lua">local bytes = task.await(http.get_bytes("https://example.com/sound.ogg")) </code></pre> <h2 id="modules-http-get-json">modules/http/get_json</h2> <pre><code class="language-lua">get_json(url: string, headers: Headers?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?): PromiseId </code></pre> <p>Async HTTP GET returning JSON. Returns a future (a <code>PromiseHandle</code>) at once, with the request in flight: keep it, <code>f:await()</code> (or <code>task.await(f)</code>) when the response is needed, and <code>f:state()</code> to see whether it has arrived without waiting.</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — Request URL.</li> <li><code>headers</code> <code>Headers?</code> <em>(optional)</em> — Header key-value pairs (optional).</li> <li><code>timeoutMs</code> <code>TimeoutMs?</code> <em>(optional)</em> — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).</li> <li><code>expectedStatuses</code> <code>ExpectedStatuses?</code> <em>(optional)</em> — HTTP statuses this caller handles as an answer it expected, e.g. <code>{ 404 }</code>: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).</li> </ul> <pre><code class="language-lua">local data = task.await(http.get_json("https://api.example.com/info")) local record = task.await(http.get_json(recordUrl, nil, nil, { 404 })) -- a missing record is an answer </code></pre> <h2 id="modules-http-post-bytes">modules/http/post_bytes</h2> <pre><code class="language-lua">post_bytes(url: string, headers: Headers?, body: JsonBody?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?): PromiseId </code></pre> <p>Async HTTP POST returning raw bytes — use for APIs that accept JSON input but return binary output (audio, images).</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — Request URL.</li> <li><code>headers</code> <code>Headers?</code> <em>(optional)</em> — Header key-value pairs (optional).</li> <li><code>body</code> <code>JsonBody?</code> <em>(optional)</em> — JSON body (optional).</li> <li><code>timeoutMs</code> <code>TimeoutMs?</code> <em>(optional)</em> — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).</li> <li><code>expectedStatuses</code> <code>ExpectedStatuses?</code> <em>(optional)</em> — HTTP statuses this caller handles as an answer it expected, e.g. <code>{ 404 }</code>: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).</li> </ul> <pre><code class="language-lua">local audio = task.await(http.post_bytes(ttsUrl, nil, { text = "hello" })) </code></pre> <h2 id="modules-http-post-json">modules/http/post_json</h2> <pre><code class="language-lua">post_json(url: string, headers: Headers?, body: JsonBody?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?): PromiseId </code></pre> <p>Async HTTP POST returning JSON. Body is a Luau table; the FFI layer JSON-encodes it before the request goes out.</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — Request URL.</li> <li><code>headers</code> <code>Headers?</code> <em>(optional)</em> — Header key-value pairs (optional).</li> <li><code>body</code> <code>JsonBody?</code> <em>(optional)</em> — JSON body (optional).</li> <li><code>timeoutMs</code> <code>TimeoutMs?</code> <em>(optional)</em> — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).</li> <li><code>expectedStatuses</code> <code>ExpectedStatuses?</code> <em>(optional)</em> — HTTP statuses this caller handles as an answer it expected, e.g. <code>{ 404 }</code>: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).</li> </ul> <pre><code class="language-lua">local r = task.await(http.post_json(url, nil, { name = "Alice" })) </code></pre> <h2 id="modules-http-request">modules/http/request</h2> <pre><code class="language-lua">request(method: string, url: string, headers: Headers?, body: JsonBody?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?): PromiseId </code></pre> <p>Async HTTP request with an arbitrary verb (GET/POST/PUT/PATCH/ DELETE/…) returning JSON. Body is a Luau table; an empty 2xx response resolves to an empty table.</p> <p><strong>Parameters</strong></p> <ul> <li><code>method</code> <code>string</code> — HTTP verb (case-insensitive).</li> <li><code>url</code> <code>string</code> — Request URL.</li> <li><code>headers</code> <code>Headers?</code> <em>(optional)</em> — Header key-value pairs (optional).</li> <li><code>body</code> <code>JsonBody?</code> <em>(optional)</em> — JSON body (optional).</li> <li><code>timeoutMs</code> <code>TimeoutMs?</code> <em>(optional)</em> — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).</li> <li><code>expectedStatuses</code> <code>ExpectedStatuses?</code> <em>(optional)</em> — HTTP statuses this caller handles as an answer it expected, e.g. <code>{ 404 }</code>: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).</li> </ul> <pre><code class="language-lua">local w = task.await(http.request("PATCH", url, hdrs, { description = "hi" })) </code></pre> <h2 id="modules-http-request-raw">modules/http/request_raw</h2> <pre><code class="language-lua">request_raw(method: string, url: string, headers: Headers?, body: buffer | string | nil, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?): PromiseId </code></pre> <p>Async HTTP request with an arbitrary verb and a RAW binary request body (a binary-safe string), for content-addressed blob uploads. The resolved value is the response body text.</p> <p><strong>Parameters</strong></p> <ul> <li><code>method</code> <code>string</code> — HTTP verb (case-insensitive).</li> <li><code>url</code> <code>string</code> — Request URL.</li> <li><code>headers</code> <code>Headers?</code> <em>(optional)</em> — Header key-value pairs (optional).</li> <li><code>body</code> <code>buffer | string | nil</code> <em>(optional)</em> — Raw binary request body (optional).</li> <li><code>timeoutMs</code> <code>TimeoutMs?</code> <em>(optional)</em> — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).</li> <li><code>expectedStatuses</code> <code>ExpectedStatuses?</code> <em>(optional)</em> — HTTP statuses this caller handles as an answer it expected, e.g. <code>{ 404 }</code>: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).</li> </ul> <pre><code class="language-lua">local r = task.await(http.request_raw("POST", blobsUrl, hdrs, pngBytes)) </code></pre> <h2 id="modules-httpserver-readme">modules/httpServer/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.httpServer") -- httpServer (also available as global 'httpServer') </code></pre> <p>Serve HTTP from this engine. A world registers a handler for a method and a path, and the engine's own HTTP server routes matching requests to it — so a browser tab, a curl call or another process on this machine reaches the running world over plain HTTP, with the world deciding what every address answers. The engine's own HTTP server listens on the loopback interface, so a world that asks for nothing is reachable from this machine, and through a forward another program opens to it (<code>adb forward</code>, an SSH tunnel, a reverse proxy). <code>httpServer.listen(target)</code> holds a second address of this world's own choosing: the host in the target is the interface bound and the whole of what decides who can reach it, so <code>"0.0.0.0:8080"</code> answers a phone on the same wifi and <code>"127.0.0.1:8080"</code> answers this machine. <code>httpServer.address(path)</code> is the URL to call, and <code>httpServer.status()</code> reports the host, the port and the <code>reach</code> those routes answer under. Content routes are served under the <code>/app</code> mount: <code>httpServer.route("GET", "/status", h)</code> answers <code>http://<host>:<port>/app/status</code>. The engine's own <code>/engine/*</code> routes are matched first, and every registration lands under <code>/app</code>, so the two trees stay disjoint. A route belongs to the chunk that registered it, and so does an address that chunk opened. When that chunk runs again — a module hot-reload, a cleared require cache — both are released and the new run makes its own, so an edited handler is the one that answers. <code>httpServer.routes()</code> names the owner of every address. A handler runs on the script thread, inside the frame, like any other world code — it may read and write the world, and it holds the tick for as long as it runs. The request waits at most <code>timeoutMs</code> (5 s by default) for its answer; past that the caller is told 504 and the connection closes, while the handler itself carries on to completion.</p> <p>Usage: local httpServer = require("@builtin::modules.api.engine.httpServer") Also available as global: httpServer</p> <h2 id="modules-httpserver-address">modules/httpServer/address</h2> <pre><code class="language-lua">address(path: string): (string?, string?) </code></pre> <p>The URL a path answers on — scheme, host, port and the <code>/app</code> mount, ready to be fetched or printed for someone to open. Takes the same path spelling <code>route</code> does, and reads the interface and port from the socket routes answer on: the address <code>listen</code> opened while one is open, and the engine's own server otherwise.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Path under the <code>/app</code> mount, e.g. "/status".</li> </ul> <pre><code class="language-lua">print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status </code></pre> <h2 id="modules-httpserver-listen">modules/httpServer/listen</h2> <pre><code class="language-lua">listen(target: string): (HttpListener?, string?) </code></pre> <p>Hold an interface and port of this world's own, and answer content routes on it.</p> <p>The host in <code>target</code> is the interface bound, and the whole of what decides who can reach those routes: <code>"127.0.0.1:8080"</code> answers programs on this machine, <code>"0.0.0.0:8080"</code> answers any host that routes to this machine on that port — a phone on the same wifi, and whatever else the network lets through. Bind loopback unless you want that. A port of <code>0</code> asks the operating system for a free one, which the returned record reports, and <code>http://</code> may be spelled out in front.</p> <p>This address serves the routes registered under the <code>/app</code> mount. The engine's own <code>/engine/*</code> tree answers on the loopback server it booted with, whose interface stays what the boot bound.</p> <p>The address belongs to the chunk that opened it and is released when that chunk runs again, so an edited module holds the address its current source names. Asking for the address already held is the same address back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string</code> — Interface and port to hold, e.g. "0.0.0.0:8080".</li> </ul> <pre><code class="language-lua">local l = assert(httpServer.listen("0.0.0.0:8080")) </code></pre> <h2 id="modules-httpserver-route">modules/httpServer/route</h2> <pre><code class="language-lua">route( </code></pre> <p>Serve one method and path from this engine, answering each matching request with <code>handler</code>.</p> <p>The path is relative to the <code>/app</code> mount, and a trailing <code>/*</code> segment matches the rest of the path — <code>"/files/*"</code> answers <code>/app/files/a/b</code>, with <code>"a/b"</code> in <code>request.wildcard</code>. An exact path answers ahead of a wildcard, and among wildcards the longest one wins.</p> <p>One method and path is served by one handler. Registering an address another chunk serves returns nil and a reason naming the handle and the chunk holding it; <code>httpServer.routes()</code> finds that handle and <code>httpServer.unroute</code> frees the address. Registering an address this same chunk already serves takes it back and releases the handler it replaces, so a chunk that runs twice serves the handler it just built.</p> <p>The handler runs on the script thread; a route registered from a worker job keeps its handler in that worker's VM, which runs it once per request as the job that registered it while the request waits for its value and the engine thread waits for nothing. The route lasts until it is unregistered or the world changes or the scripting state resets, whatever became of the job that made it. Raising inside the handler answers 500 and writes the error to the engine log; returning something that is not a response table or a string answers 500 saying what arrived.</p> <pre><code class="language-lua">local h = httpServer.route("GET", "/status", function(req) </code></pre> <h2 id="modules-httpserver-routes">modules/httpServer/routes</h2> <pre><code class="language-lua">routes(): { HttpRoute } </code></pre> <p>Every route this engine currently serves, in registration order — handle, method, registered path, the address it answers on, its full URL, the chunk that registered it, and how long a request for it waits.</p> <pre><code class="language-lua">for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end </code></pre> <h2 id="modules-httpserver-status">modules/httpServer/status</h2> <pre><code class="language-lua">status(): HttpServerStatus </code></pre> <p>Whether this engine serves content routes, on which interface, port and mount, who can reach them, and how many routes and waiting requests it holds. <code>host</code>, <code>port</code>, <code>url</code> and <code>reach</code> are read from the socket routes answer on — the one <code>listen</code> opened while one is open, and the engine's own server otherwise — and <code>listeners</code> carries every address, each with its own reach. When <code>supported</code> is false, <code>reason</code> says why: a browser tab answers HTTP requests and holds no address of its own.</p> <pre><code class="language-lua">local s = httpServer.status(); print(s.url, s.reach) </code></pre> <h2 id="modules-httpserver-unlisten">modules/httpServer/unlisten</h2> <pre><code class="language-lua">unlisten(): boolean </code></pre> <p>Release the address <code>listen</code> opened. Returns once the socket is free, so the same port binds again straight after.</p> <pre><code class="language-lua">httpServer.unlisten() </code></pre> <h2 id="modules-httpserver-unroute">modules/httpServer/unroute</h2> <pre><code class="language-lua">unroute(handle: number): boolean </code></pre> <p>Stop serving a route and release its handler. The address is free for another registration once this returns true.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>number</code> — The handle <code>httpServer.route</code> returned.</li> </ul> <pre><code class="language-lua">httpServer.unroute(h) </code></pre> <h2 id="modules-humanoid-readme">modules/humanoid/README</h2> <pre><code class="language-lua">require("@builtin::avatarTypes.humanoid") -- humanoid </code></pre> <p>The humanoid avatar type: a two-legged body with a spine, a head and one or two arms. <code>qualify(structure)</code> maps a skeleton onto the humanoid roles and chains from its shape, for any bone naming and in any pose the skeleton rests in; names only raise confidence and are checked against the geometry.</p> <p>Usage: local humanoid = require("@builtin::avatarTypes.humanoid")</p> <h2 id="modules-humanoid-frames">modules/humanoid/frames</h2> <pre><code class="language-lua">frames(s: any, roles: { [string]: string }): { [string]: any } </code></pre> <p>Each mapped body role's anatomical frame, from the rig's rest joint positions: X toward the next bone of the role's chain, Y the role's anatomical secondary (the hip line for hips, spine and chest; the shoulder line for upper chest, neck and head; the palm normal for shoulder, arm, forearm, hand and fingers; the toe direction for thigh and shin), Z = X x Y. A foot and its toes take the ground they stand on at rest: X their direction along the ground, Y the stand axis, so a heel's pitch is the rig's own and a standing foot of one rig is a standing foot of another. Bone rolls do not change a frame, and a pose turns it with the bones it turns. The root takes the hips' frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>any</code> <em>(optional)</em> — A rig structure (<code>rigStructure.fromDoc</code>).</li> <li><code>roles</code> <code>{ [string]: string }</code> — The role -> bone name map (<code>qualify(s).mapping.roles</code>, or an effective mapping).</li> </ul> <h2 id="modules-i18n-readme">modules/i18n/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.i18n") -- i18n (also available as global 'i18n') </code></pre> <p>The player's language, and your world's text in it. <code>i18n.t(key, values)</code> reads a string from the world's catalogs (<code>/zero/source/locales/<tag>/<namespace>.json</code>, the format origozero.ai uses: <code>{{ name }}</code> parameters, <code>_one</code> / <code>_other</code> plural forms), falling back through the locale's chain to English; <code>i18n.text(key)</code> is the same text as a value widgets take, which follows the language on screen; <code>i18n.formatNumber</code> / <code>formatBytes</code> / <code>formatDate</code> / <code>formatAgo</code> / <code>formatMoney</code> write values the way the player's locale does; <code>i18n.pseudo(true)</code> shows every catalog string bracketed so text left in code stands out; <code>i18n.check()</code> runs the catalog checks. <code>i18n.pick({"en", "pt", "es"})</code> answers which of the languages your content has text for this player reads; <code>i18n.preferred()</code> is the languages their platform says they read (OS, browser or Android settings), most preferred first; <code>i18n.onChange</code> reports every change while they play; <code>i18n.emulate(tags)</code> stands a visitor's language list in to try content under it. <code>i18n.locale()</code> is the locale the engine's own text is in, one of <code>i18n.offered()</code>; <code>i18n.choose(tag)</code> is the player's own choice, kept for their account. The same surface in every VM. The engine picks the locale from one chain: the player's choice, then the language ZeroMind shows them, then the platform's list, then <code>en</code>, each matched against the offered locales (same tag, then the tag truncated, then the same language). <code>i18n.sources()</code> shows every link and which one decided.</p> <p>Usage: local i18n = require("@builtin::modules.api.engine.i18n") Also available as global: i18n</p> <h2 id="modules-i18n-check">modules/i18n/check</h2> <pre><code class="language-lua">check(root: string?): CheckResult </code></pre> <p>Run the catalog checks origozero.ai runs over every catalog of a root (<code><root>/locales/<tag>/*.json</code>, <code>en</code> the source): files that do not parse, keys a translation has that English lacks, placeholders or markup that differ from English, plural forms the locale does not use, a translation with no current review record (<code><namespace>.review.json</code>; the finding prints the record to write), and the rest of the shared checks. A translated locale's terms are held to its glossary, <code><root>/glossary/<tag>.json</code> (<code>{ "terms": [ { "source": ["coin", "coins"], "target": ["moeda*"] } ], "exceptions": [], "same_as_english": [] }</code>); without one the check warns.</p> <p><strong>Parameters</strong></p> <ul> <li><code>root</code> <code>string?</code> <em>(optional)</em> — The folder holding <code>locales/</code>; the world's <code>/zero/source</code> by default.</li> </ul> <pre><code class="language-lua">local r = i18n.check(); for _, e in r.errors do print(e) end </code></pre> <h2 id="modules-i18n-choice">modules/i18n/choice</h2> <pre><code class="language-lua">choice(): string? </code></pre> <p>The locale the player chose for themselves, or nil when they chose none and the chain decides.</p> <h2 id="modules-i18n-choose">modules/i18n/choose</h2> <pre><code class="language-lua">choose(tag: string?): ChooseResult </code></pre> <p>Choose <code>tag</code> as the player's locale, kept on this device for their ZeroMind account (or for the signed-out player), so it wins the chain from now on and on every later boot. <code>nil</code> clears the choice and the chain decides again. A tag no offered locale matches is refused, naming the offered tags.</p> <p><strong>Parameters</strong></p> <ul> <li><code>tag</code> <code>string?</code> <em>(optional)</em> — A tag from <code>i18n.offered()</code>, or nil to clear the choice.</li> </ul> <pre><code class="language-lua">local r = i18n.choose("pt-BR") if not r.ok then log.warn(r.reason) end </code></pre> <h2 id="modules-i18n-emulate">modules/i18n/emulate</h2> <pre><code class="language-lua">emulate(tags: { string }?): { ok: boolean, locale: string?, platform: { string }?, reason: string? } </code></pre> <p>Stand <code>tags</code> in for the platform's preferred languages, the way a visitor whose device or browser reads them would arrive, and negotiate again: <code>i18n.preferred()</code>, <code>i18n.pick</code>, <code>i18n.locale()</code> and <code>i18n.onChange</code> all answer as they would for that visitor. <code>nil</code> reads the platform's own list again. A report the platform makes afterwards (a language change, a desktop window regaining focus) replaces it. The player's own choice (<code>i18n.choose</code>) still comes first in the chain, so clear it with <code>i18n.choose(nil)</code> to see the list decide.</p> <p><strong>Parameters</strong></p> <ul> <li><code>tags</code> <code>{ string }?</code> <em>(optional)</em> — BCP 47 tags, most preferred first, or nil for the platform's own list.</li> </ul> <pre><code class="language-lua">i18n.emulate({ "pt-BR", "en" }) -- try the world as a Brazilian visitor i18n.emulate(nil) -- back to this machine's own languages </code></pre> <h2 id="modules-i18n-explain">modules/i18n/explain</h2> <pre><code class="language-lua">explain(key: string, values: { [string]: any }?): Explanation </code></pre> <p>Where the text of <code>key</code> comes from for the caller: the text, the locale and file it was found in, and every file searched in order (locale by locale, layer by layer). The first thing to read when a screen shows a key, English in a translated screen, or another layer's wording.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — <code><namespace>.<key></code>.</li> <li><code>values</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Parameter values, as <code>i18n.t</code> takes them.</li> </ul> <pre><code class="language-lua">local e = i18n.explain("hud.score"); print(e.found, e.path, e.locale) </code></pre> <h2 id="modules-i18n-fallback">modules/i18n/fallback</h2> <pre><code class="language-lua">fallback(): { string } </code></pre> <p>The chosen locale's fallback chain: the locales a string missing from its catalog is looked up in, in order.</p> <h2 id="modules-i18n-formatago">modules/i18n/formatAgo</h2> <pre><code class="language-lua">formatAgo(amount: number, unit: string?, locale: string?): string </code></pre> <p>How long ago something happened, as the caller's text locale writes it in CLDR's narrow style: <code>seconds</code> before now in the largest unit that fits (<code>45s ago</code>, <code>3h ago</code>), or with <code>unit</code> (<code>second</code>, <code>minute</code>, <code>hour</code>, <code>day</code>, <code>month</code>, <code>year</code>) that many of that unit (<code>há 5 min.</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>amount</code> <code>number</code> — Seconds before now, or a count of <code>unit</code>.</li> <li><code>unit</code> <code>string?</code> <em>(optional)</em> — The unit <code>amount</code> counts, or nil for seconds in the unit that fits.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-i18n-formatbytes">modules/i18n/formatBytes</h2> <pre><code class="language-lua">formatBytes(bytes: number, locale: string?): string </code></pre> <p>A size in decimal units as the caller's text locale or <code>locale</code> writes it: <code>228 B</code>, <code>16.2 KB</code>, <code>1.5 MB</code> (<code>16,2 KB</code> in Brazilian Portuguese).</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>number</code> — The size in bytes.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-i18n-formatcompact">modules/i18n/formatCompact</h2> <pre><code class="language-lua">formatCompact(n: number, locale: string?): string </code></pre> <p>A count in the short compact form of the caller's text locale or <code>locale</code>: <code>1.2K</code>, <code>1,2 mil</code>. Below a thousand the number is written whole.</p> <p><strong>Parameters</strong></p> <ul> <li><code>n</code> <code>number</code> — A whole number.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-i18n-formatdate">modules/i18n/formatDate</h2> <pre><code class="language-lua">formatDate(date: { year: number, month: number, day: number }, style: string?, locale: string?): string </code></pre> <p>A calendar date as the caller's text locale (<code>i18n.textLocale()</code>) or <code>locale</code> writes it. <code>style</code> is <code>long</code> (<code>October 1, 2026</code>, <code>1 de outubro de 2026</code>; the default), <code>medium</code> (<code>Oct 1, 2026</code>), <code>monthYear</code> (<code>October 2026</code>) or <code>monthDay</code> (<code>Oct 1</code>). Pass the result into a message as text: <code>i18n.t("hud.last_played", { date = i18n.formatDate(d) })</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>date</code> <code>{ year: number, month: number, day: number }</code> — <code>{ year, month, day }</code> (the shape <code>os.date("*t")</code> returns).</li> <li><code>style</code> <code>string?</code> <em>(optional)</em> — The date style.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <pre><code class="language-lua">print(i18n.formatDate(os.date("*t"))) </code></pre> <h2 id="modules-i18n-formatmoney">modules/i18n/formatMoney</h2> <pre><code class="language-lua">formatMoney(amountMinor: number, currency: string, locale: string?): string </code></pre> <p>An amount of money as the caller's text locale or <code>locale</code> writes it, given in the currency's minor unit (cents for <code>USD</code>): <code>$12.50</code>, <code>R$ 12,50</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>amountMinor</code> <code>number</code> — The amount in minor units.</li> <li><code>currency</code> <code>string</code> — An ISO 4217 code such as <code>BRL</code>.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-i18n-formatnumber">modules/i18n/formatNumber</h2> <pre><code class="language-lua">formatNumber(n: number, locale: string?): string </code></pre> <p>A whole number as the caller's text locale (<code>i18n.textLocale()</code>) or <code>locale</code> writes it: <code>1,234,567</code> in English, <code>1.234.567</code> in Brazilian Portuguese.</p> <p><strong>Parameters</strong></p> <ul> <li><code>n</code> <code>number</code> — A whole number.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-i18n-has">modules/i18n/has</h2> <pre><code class="language-lua">has(key: string): boolean </code></pre> <p>Whether a catalog the caller reads holds <code>key</code>, in the player's locale, its fallback chain or <code>en</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — <code><namespace>.<key></code>.</li> </ul> <h2 id="modules-i18n-istext">modules/i18n/isText</h2> <pre><code class="language-lua">isText(value: any): boolean </code></pre> <p>Whether <code>value</code> is an <code>i18n.text</code> value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — Any value.</li> </ul> <h2 id="modules-i18n-locale">modules/i18n/locale</h2> <pre><code class="language-lua">locale(): string </code></pre> <p>The locale the engine chose for the player for its own text, a BCP 47 tag such as <code>en</code> or <code>pt-BR</code>. It is always one of <code>i18n.offered()</code>; <code>en</code>, the engine's source language, until a negotiation states another. To choose among your own content's languages, use <code>i18n.pick</code>.</p> <pre><code class="language-lua">if i18n.locale() == "pt-BR" then label.text = "Olá" end </code></pre> <h2 id="modules-i18n-offchange">modules/i18n/offChange</h2> <pre><code class="language-lua">offChange(id: number): boolean </code></pre> <p>Remove an <code>i18n.onChange</code> subscriber by its watcher id. Returns true when a live watcher carried that id (false for one already removed, or ended with the component that made it).</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>number</code> — Watcher id returned by <code>i18n.onChange</code>.</li> </ul> <h2 id="modules-i18n-offered">modules/i18n/offered</h2> <pre><code class="language-lua">offered(): { Locale } </code></pre> <p>The locales the engine offers for its own text, sorted by tag. Only an offered locale can be chosen or negotiated. Content picks among its own languages with <code>i18n.pick</code>.</p> <h2 id="modules-i18n-onchange">modules/i18n/onChange</h2> <pre><code class="language-lua">onChange(callback: (string, { string }) -> ()): number </code></pre> <p>Run <code>callback</code> each time the player's language changes: the platform's preferred list changes (the player changed their OS, browser or device language while the engine runs, or <code>i18n.emulate</code> stood a list in), the player chooses or clears a choice, the ZeroMind language moves, the offered locales change, the pseudo-locale is switched, or a catalog file this VM read text from is edited. Called as <code>(locale, preferred)</code>, after every screen built from <code>i18n.text</code> values redrew, and after the engine renegotiated, so <code>i18n.pick</code> inside it answers for the new state. A change <code>i18n.emulate</code> or <code>i18n.choose</code> makes reaches this VM's subscribers before that call returns; one the platform reports reaches them within a tenth of a second. A subscription made from a component ends with that component. Returns a watcher id for <code>i18n.offChange</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>callback</code> <code>(string, { string }) -> ()</code> — Function invoked as <code>(locale: string, preferred: {string})</code>.</li> </ul> <pre><code class="language-lua">local id = i18n.onChange(function(locale, preferred) log.info(("locale %s, platform %s"):format(locale, table.concat(preferred, ","))) end) </code></pre> <h2 id="modules-i18n-pick">modules/i18n/pick</h2> <pre><code class="language-lua">pick(available: { string }): string </code></pre> <p>Which of <code>available</code>, the languages your content has text for, this player reads: the tag the engine's chain picks from the player's own choice, the language ZeroMind shows them and their platform's list, matched by the same rules (same tag, the tag truncated, the same language). The first of <code>available</code> when none of them matches, so list your fallback first. Call it again (or from <code>i18n.onChange</code>) to follow a change while they play.</p> <p><strong>Parameters</strong></p> <ul> <li><code>available</code> <code>{ string }</code> — BCP 47 tags your content has text for, fallback first.</li> </ul> <pre><code class="language-lua">local lang = i18n.pick({ "en", "pt", "es" }) label.text = ({ en = "Welcome!", pt = "Bem-vindo!", es = "¡Bienvenido!" })[lang] </code></pre> <h2 id="modules-i18n-plural">modules/i18n/plural</h2> <pre><code class="language-lua">plural(count: number, locale: string?): string </code></pre> <p>The CLDR plural category <code>count</code> takes in the caller's text locale (<code>i18n.textLocale()</code>), or <code>locale</code>: <code>zero</code>, <code>one</code>, <code>two</code>, <code>few</code>, <code>many</code> or <code>other</code>. A catalog's plural message picks its form this way; use it for a choice that is not text.</p> <p><strong>Parameters</strong></p> <ul> <li><code>count</code> <code>number</code> — The number.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-i18n-preferred">modules/i18n/preferred</h2> <pre><code class="language-lua">preferred(): { string } </code></pre> <p>The languages the player's platform says they read, as BCP 47 tags, most preferred first: the OS language list on Linux, Windows and macOS, <code>navigator.languages</code> in a browser, the device (or per-app) language list on Android. Empty when the platform states none.</p> <pre><code class="language-lua">local first = i18n.preferred()[1] </code></pre> <h2 id="modules-i18n-pseudo">modules/i18n/pseudo</h2> <pre><code class="language-lua">pseudo(on: boolean?): boolean </code></pre> <p>Show (<code>true</code>) or stop showing (<code>false</code>) the pseudo-locale <code>qps</code> in every VM: every catalog text renders as accented, bracketed English (<code>⟦Ŝçöŕé⟧</code>), padded by about a third, so text that did not come from a catalog reads as plain English and a label too narrow for a longer language overflows. Screens built from <code>i18n.text</code> values redraw. With no argument it changes nothing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>on</code> <code>boolean?</code> <em>(optional)</em> — Whether to show it; nil reads the current state.</li> </ul> <pre><code class="language-lua">i18n.pseudo(true) -- every English string left on screen is one no catalog holds i18n.pseudo(false) </code></pre> <h2 id="modules-i18n-revision">modules/i18n/revision</h2> <pre><code class="language-lua">revision(): number </code></pre> <p>A number that moves whenever a catalog text may read differently: the locale changed, the pseudo-locale was switched, or a catalog file this VM read was written. A build that caches text compares it.</p> <h2 id="modules-i18n-sources">modules/i18n/sources</h2> <pre><code class="language-lua">sources(): Sources </code></pre> <p>Every link of the chain the locale was chosen from: the player's <code>choice</code>, the <code>zeromind</code> language, the <code>platform</code> list, and which link decided (<code>decidedBy</code>: <code>choice</code>, <code>zeromind</code>, <code>platform</code> or <code>default</code>) with the tag it matched; and where a choice is kept (<code>choiceKept</code>: <code>device</code>, or <code>run</code> in an offline engine, which keeps it for that run alone).</p> <h2 id="modules-i18n-t">modules/i18n/t</h2> <pre><code class="language-lua">t(key: string, values: { [string]: any }?): string </code></pre> <p>The text of <code>key</code> in the player's language, from the catalogs: the world's <code>/zero/source/locales/<tag>/<namespace>.json</code>, then (for code of a package) the package's own <code>locales/</code>, then the built-in library's. A key is <code><namespace>.<key></code>: <code>hud.score_label</code> is <code>score_label</code> in <code>hud.json</code>. A world's (or an installed package's) catalogs are read in whichever of their languages the player reads (the chain <code>i18n.pick</code> runs), then <code>en</code>; the library's in the engine's locale, its fallback chain, then <code>en</code>. The first layer holding the key gives it. <code>values</code> fills the message's <code>{{ name }}</code> parameters: text as written, whole numbers grouped the way the message's language groups them, an <code>i18n.text</code> value as its text, a function as what it returns when the text renders; <code>count</code> also picks the plural form (<code>items_one</code> / <code>items_other</code> in the file). A key no catalog holds reads as the key, said once in the log. While the pseudo-locale is shown (<code>i18n.pseudo(true)</code>) the text is accented, bracketed English.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — <code><namespace>.<key></code>, lowercase words joined by <code>.</code> and <code>_</code>.</li> <li><code>values</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Parameter values by name; <code>count</code> picks the plural form.</li> </ul> <pre><code class="language-lua">-- /zero/source/locales/en/hud.json: { "score": "Score: {{ points }}", "lives_one": "{{ count }} life", "lives_other": "{{ count }} lives" } label.text = i18n.t("hud.score", { points = 1250 }) -- "Score: 1,250"; "Pontos: 1.250" in pt-BR print(i18n.t("hud.lives", { count = 3 })) -- "3 lives" </code></pre> <h2 id="modules-i18n-text">modules/i18n/text</h2> <pre><code class="language-lua">text(key: string, values: { [string]: any }?): Text </code></pre> <p>A value that reads as the text of <code>key</code> (see <code>i18n.t</code>): <code>tostring</code> renders it in the player's language at that moment. Pass it wherever a widget takes text (<code>text</code>, <code>title</code>, <code>header</code>, <code>placeholder</code>, <code>tooltip</code>, a dropdown's <code>options</code>; every edui widget's label), and the screen follows the player's language, the pseudo-locale and edits to the catalog without being built again (<code>ui.refreshLiveText</code>). A value of <code>values</code> may itself be an <code>i18n.text</code> value, or a function called each time the text renders, so a value written in the player's language follows it too: <code>{ date = function() return i18n.formatDate(d) end }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — <code><namespace>.<key></code>.</li> <li><code>values</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Parameter values by name; <code>count</code> picks the plural form.</li> </ul> <pre><code class="language-lua">ui.registerScreen("hud", { type = "label", props = { text = i18n.text("hud.title") } }) edui.button({ label = i18n.text("editor.save"), onClick = save }) i18n.text("hud.last_played", { date = function() return i18n.formatDate(lastPlayed) end }) </code></pre> <h2 id="modules-i18n-textlocale">modules/i18n/textLocale</h2> <pre><code class="language-lua">textLocale(): string </code></pre> <p>The locale the caller's text is shown in: for a world's code, the one of the world's catalog languages the player reads (<code>pt-BR</code> for a Brazilian player of a world with a <code>locales/pt-BR</code>, even while the engine shows its own text in English); for a package's code, its package's; for the engine's own code, <code>i18n.locale()</code>. The formatting helpers write in it by default, so a date or a count reads in the same language as the sentence around it.</p> <pre><code class="language-lua">local lang = i18n.textLocale() -- "pt-BR" for a Brazilian player of a world with pt-BR catalogs </code></pre> <h2 id="modules-i18n-upper">modules/i18n/upper</h2> <pre><code class="language-lua">upper(text: string, locale: string?): string </code></pre> <p><code>text</code> in capitals by the case rules of the caller's text locale or <code>locale</code> (<code>CONEXÃO</code>, Turkish <code>İ</code>). <code>string.upper</code> changes ASCII letters only.</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — The text.</li> <li><code>locale</code> <code>string?</code> <em>(optional)</em> — A locale tag, in place of the caller's text locale.</li> </ul> <h2 id="modules-ibl-readme">modules/ibl/README</h2> <pre><code class="language-lua">require("@builtin::systems.imageBasedLighting.ibl") -- ibl </code></pre> <p>Image-based lighting from the scene's own sky — metal reflects what is actually above it, and matte surfaces pick up the sky's colour.</p> <p>Usage: local ibl = require("@builtin::systems.imageBasedLighting.ibl")</p> <h2 id="modules-ibl-active">modules/ibl/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the environment-lighting passes are currently running.</p> <pre><code class="language-lua">if ibl.active() then print("lit by the sky") end </code></pre> <h2 id="modules-ibl-clear">modules/ibl/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Turn environment lighting off and release the passes. The other settings are kept, so a later <code>set</code> brings back the same look.</p> <pre><code class="language-lua">ibl.clear() </code></pre> <h2 id="modules-ibl-follow">modules/ibl/follow</h2> <pre><code class="language-lua">follow() </code></pre> <p>Go back to reading the sky and sun from the scene, after a call pinned them.</p> <pre><code class="language-lua">ibl.follow() </code></pre> <h2 id="modules-ibl-get">modules/ibl/get</h2> <pre><code class="language-lua">get(): IblState </code></pre> <p>The environment-lighting settings currently in force.</p> <pre><code class="language-lua">local s = ibl.get().specular </code></pre> <h2 id="modules-ibl-paramsbuffer">modules/ibl/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.</p> <pre><code class="language-lua">local p = ibl.paramsBuffer() </code></pre> <h2 id="modules-ibl-refresh">modules/ibl/refresh</h2> <pre><code class="language-lua">refresh() </code></pre> <p>Carry the scene's current sky and sun into the running passes. The scales — <code>diffuse</code>, <code>specular</code>, <code>skyIntensity</code>, <code>horizonSharpness</code> and <code>sunSize</code> — stay as they stand, so a caller ticking this every frame keeps a day/night cycle reaching the reflections without restating them. It reads while the passes are running and the scene is on automatic tracking; a colour or direction pinned through <code>set</code> holds it off until <code>follow</code>.</p> <pre><code class="language-lua">ibl.refresh() </code></pre> <h2 id="modules-ibl-set">modules/ibl/set</h2> <pre><code class="language-lua">set(opts: IblOpts?): IblState </code></pre> <p>Set the scene's environment lighting. Any omitted field keeps its current value. With both <code>diffuse</code> and <code>specular</code> at 0 nothing is being asked for and the passes are released.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>IblOpts?</code> <em>(optional)</em> — Settings — see <code>IblOpts</code>.</li> </ul> <pre><code class="language-lua">ibl.set({ diffuse = 1, specular = 1 }) </code></pre> <h2 id="modules-ies-readme">modules/ies/README</h2> <pre><code class="language-lua">require("@builtin::systems.photometrics.ies") -- ies </code></pre> <p>Photometric light distributions in the IESNA LM-63 format, turned into a texture a spot light projects through its cone.</p> <p>Usage: local ies = require("@builtin::systems.photometrics.ies")</p> <h2 id="modules-ies-parse">modules/ies/parse</h2> <pre><code class="language-lua">parse(src: string): (Profile?, string?) </code></pre> <p>Read an IESNA LM-63 photometric file.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The file's text.</li> </ul> <pre><code class="language-lua">local p = ies.parse(vfs.read("/source/lights/downlight.ies")) </code></pre> <h2 id="modules-ies-tolayer">modules/ies/toLayer</h2> <pre><code class="language-lua">toLayer(profile: Profile, layer: number, coneHalfAngleDeg: number, resolution: number?): (number?, string?) </code></pre> <p>Rasterize a profile into one layer of the shared feature-texture array, ready for a spot light's <code>cookieLayer</code> to project it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>profile</code> <code>Profile</code> — A profile from <code>ies.parse</code>.</li> <li><code>layer</code> <code>number</code> — Which feature-texture layer to fill.</li> <li><code>coneHalfAngleDeg</code> <code>number</code> — The spot's outer half-angle, so the profile's angles land where the cone actually reaches.</li> <li><code>resolution</code> <code>number?</code> <em>(optional)</em> — Layer size in pixels, a multiple of 32. Defaults to 256.</li> </ul> <pre><code class="language-lua">ies.toLayer(profile, 0, spot.angle) </code></pre> <h2 id="modules-launcher-readme">modules/launcher/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.launcher") -- launcher </code></pre> <p>The world chooser: the screen a launch that names no world opens where the platform asks for one (the Android app started from its launcher). It lists the public worlds on ZeroMind, each to play or to edit, the world the person chose last, and a local session, and starts the app again on the one the person picks. Where the platform shows the app's open source license notices, the screen opens them. Installed by the prelude; on a boot that did not open the chooser it draws nothing.</p> <p>Every word the screen draws is read from the library catalog <code>locales/<tag>/world_chooser.json</code> as an <code>i18n.text</code> value, so the screen follows the player's language. The world list, the credential it is read with and the relaunch live in the trusted VM (<code>trusted.call("launcher.*")</code>), which answers only on a chooser boot. Playing a world boots the runtime profile in play mode; editing it boots the editor profile in edit mode.</p> <p>Usage: local launcher = require("@builtin::modules.api.engine.launcher")</p> <h2 id="modules-launcher-buildscreen">modules/launcher/buildScreen</h2> <pre><code class="language-lua">buildScreen(model: Model): { [string]: any } </code></pre> <p>The chooser's widget tree for <code>model</code>: the account line, the world chosen last, the world list (or why it could not be read), a local session, and the app's open source licenses. Buttons call back <code>launcher:play:<guid></code>, <code>launcher:edit:<guid></code>, <code>launcher:continue</code>, <code>launcher:retry</code>, <code>launcher:local</code> and <code>launcher:licenses</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>model</code> <code>Model</code> — <code>{ last?, signedIn, licenses?, status = "loading"|"ready"|"unreachable", worlds?, reason?, opening?, refusal?, scale?, height? }</code>; <code>licenses</code> offers the platform's screen of the app's license notices; <code>scale</code> multiplies every size, 1 when omitted; <code>height</code> is the height the list scrolls within, the screen's.</li> </ul> <h2 id="modules-launcher-install">modules/launcher/install</h2> <pre><code class="language-lua">install() </code></pre> <p>Start the chooser on a system task, so it opens on a boot that opened it. Called once by the prelude.</p> <h2 id="modules-launcher-show">modules/launcher/show</h2> <pre><code class="language-lua">show(): boolean </code></pre> <p>Open the chooser when this boot opened it: ask the trusted VM, then show the screen and read the world list. Draws nothing on any other boot.</p> <h2 id="modules-layeredmaterial-readme">modules/layeredMaterial/README</h2> <pre><code class="language-lua">require("@builtin::systems.layeredMaterial.layeredMaterial") -- layeredMaterial </code></pre> <p>A surface written as a stack of materials that already exist, compiled to one WGSL surface shader. Rust over painted metal, snow over rock, mud over a vehicle, lacquer over paint: each is one material laid over another. The two materials are already authored; what is missing is a way to say "this one, over that one, where the mask says". Written by hand, that means a new <code>fn surface()</code> for every pair, and the pair is what the shader is about — neither half is reusable. A stack names the materials instead, and <code>material()</code> compiles it, writes the shader out as an ordinary <code>.shader</code> asset and hands back a material wearing it. Each layer is read off the material it names — its shading values and the textures it binds — and the emitted shader carries every layer's inputs under its own name, blends them by coverage, and shades <strong>once</strong>. Shading each layer and adding the results would put more energy back out than arrived; interpolating the inputs and shading the result cannot, which is what keeps a half-covered surface from reading brighter than either layer alone. A layer past the first is revealed by the product of three terms, each of which can be left neutral: a mask texture read through one channel and a contrast, a per-material amount, and one lane of the entity's own shader data. All three are ordinary material properties, so the blend is drivable at runtime, and the instance term is what lets two entities wearing one material carry different amounts of wear in the same frame. <code>property(i, name)</code> spells the naming convention out for a caller driving them, <code>inputs()</code> lists what a layer carries, and <code>limits()</code> reports the bounds a stack is compiled against.</p> <p>Usage: local layeredMaterial = require("@builtin::systems.layeredMaterial.layeredMaterial")</p> <h2 id="modules-layeredmaterial-check">modules/layeredMaterial/check</h2> <pre><code class="language-lua">check(stack: Stack): (boolean, any) </code></pre> <p>Compile a stack without raising. The same work <code>compile</code> does, with the error handed back rather than thrown — what an editor wants while the author is still choosing materials.</p> <p><strong>Parameters</strong></p> <ul> <li><code>stack</code> <code>Stack</code> — The stack to check.</li> </ul> <pre><code class="language-lua">local ok, result = layeredMaterial.check(stack) </code></pre> <h2 id="modules-layeredmaterial-compile">modules/layeredMaterial/compile</h2> <pre><code class="language-lua">compile(stack: Stack, name: string?): Compiled </code></pre> <p>Compile a stack of materials into the two files a <code>.shader</code> asset holds. The layers are read off the materials they name, so this reaches the asset graph; it touches no renderer and no GPU, and a layer written as plain <code>values</code> and <code>textures</code> instead of a <code>material</code> reference needs no asset at all. Every reference a layer holds (its <code>material</code>, its <code>textures</code>, its <code>mask</code>) is read where the call was written, so <code>~.textures.x</code> in a package's code names that package's texture.</p> <p><strong>Parameters</strong></p> <ul> <li><code>stack</code> <code>Stack</code> — <code>{ name, layers = { { material, blend, mask, ... }, ... } }</code>, the bottom layer first.</li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Overrides <code>stack.name</code> for the shader this compiles to.</li> </ul> <pre><code class="language-lua">local out = layeredMaterial.compile({ name = "worn", layers = { { material = "@builtin::materials.default" }, { material = "@builtin::materials.gold", blend = 0.5 } } }) print(out.wgsl) </code></pre> <h2 id="modules-layeredmaterial-inputs">modules/layeredMaterial/inputs</h2> <pre><code class="language-lua">inputs(): { </code></pre> <p>What a layer carries: the shading inputs the stack blends, the texture slots a layer may bind, the coverage controls a layer past the first declares, and the properties that belong to the whole surface.</p> <pre><code class="language-lua">for _, decl in ipairs(layeredMaterial.inputs().values) do print(decl.name) end </code></pre> <h2 id="modules-layeredmaterial-install">modules/layeredMaterial/install</h2> <pre><code class="language-lua">install(stack: Stack, opts: { name: string?, into: any? }?): Installed </code></pre> <p>Compile a stack and write it out as a real <code>.shader</code> asset. The result is an ordinary shader asset — it can be read, hand-edited and shipped like any other. Installing the same stack again rewrites the same asset. The shader is compiled at the next frame boundary, the way any shader edit is; <code>asset.ref(name, "shader"):compileStatus()</code> is what reports the outcome.</p> <p><strong>Parameters</strong></p> <ul> <li><code>stack</code> <code>Stack</code> — The stack to install.</li> <li><code>opts</code> <code>{ name: string?, into: any? }?</code> <em>(optional)</em> — <code>{ name = <shader name>, into = { path = <folder> } }</code>. <code>into</code> places the asset as it is created; installing again writes wherever it already is.</li> </ul> <pre><code class="language-lua">local shader = layeredMaterial.install(stack, { name = "worn_barrel" }) local status = asset.ref(shader.name, "shader"):compileStatus() </code></pre> <h2 id="modules-layeredmaterial-limits">modules/layeredMaterial/limits</h2> <pre><code class="language-lua">limits(): { maxLayers: number, textureSlots: number } </code></pre> <p>The bounds a stack is compiled against: how many layers one holds, and how many texture slots the layers may spend between them.</p> <pre><code class="language-lua">print(layeredMaterial.limits().textureSlots) </code></pre> <h2 id="modules-layeredmaterial-material">modules/layeredMaterial/material</h2> <pre><code class="language-lua">material( </code></pre> <p>Install a stack and make a material wearing the shader it compiled to, carrying every layer's values and every texture the layers bind. The result is an ordinary material asset: hand it to a Model's <code>material</code> field, or drive its properties afterwards like any other. Calling it again with the same material name is how a stack is iterated on — the shader is rewritten, the material is brought onto it, and the layers are applied over it, so what the call says is what the material holds when it returns.</p> <pre><code class="language-lua">local mat = layeredMaterial.material(stack, { name = "worn_barrel" }) entity.spawn("barrel").component.add("Model", { model = "cube", material = mat }) </code></pre> <h2 id="modules-layeredmaterial-property">modules/layeredMaterial/property</h2> <pre><code class="language-lua">property(index: number, input: string): string </code></pre> <p>The name the generated shader declares one of a layer's inputs under. The convention is <code>l<index>_<input></code>, and this is what says so — a caller driving a blend at runtime asks for the name rather than spelling it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>index</code> <code>number</code> — Which layer, counting the bottom one as 1.</li> <li><code>input</code> <code>string</code> — The input's name — one of <code>inputs()</code>, or a coverage control. The coverage controls belong to a layer laid over another, so asking for one on the bottom layer raises rather than naming a property no stack declares.</li> </ul> <pre><code class="language-lua">matRef:setProperty(layeredMaterial.property(2, "coverage"), 0.7) </code></pre> <h2 id="modules-layeredmaterial-read">modules/layeredMaterial/read</h2> <pre><code class="language-lua">read(materialRef: any): Read </code></pre> <p>Read a layer's inputs off a material that already exists — the values it carries for everything a stack blends, and the textures it binds for them. This is what <code>compile</code> does with a layer written as <code>{ material = ... }</code>, exposed on its own so a caller can see what a material would contribute before stacking it. A slot the material leaves on a built-in fallback (<code>default:white</code>, <code>default:normal</code>) is read as bound to nothing, since that fallback is what a slot left undeclared samples anyway. The material's name is read where the call was written, so <code>~.materials.x</code> in a package's code names that package's material. A texture the material writes relative to its own file (<code>~.textures.x</code>) is given as the guid it names there, since that is the texture the material binds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>materialRef</code> <code>any</code> <em>(optional)</em> — The material — a name, an identity, or an <code>AssetRef<material></code>.</li> </ul> <pre><code class="language-lua">local layer = layeredMaterial.read("@builtin::materials.gold") print(layer.values.metallic, layer.textures.base_color_texture) </code></pre> <h2 id="modules-layers-readme">modules/layers/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.layers") -- layers (also available as global 'layers') </code></pre> <p>Top-level scene-management namespace + Scene proxy. Owns the public layer-management API surface exposed on <code>_G.layers</code>. Pure Luau composition over internal <code>__layers.*</code> ECS-glue primitives; the LSP discovers the public shape via this <code>--!global layers</code> directive.</p> <p>Usage: local layers = require("@builtin::modules.api.engine.layers") Also available as global: layers</p> <h2 id="modules-layers-cost">modules/layers/cost</h2> <pre><code class="language-lua">cost(): { SceneLayerCost } </code></pre> <p>What each loaded scene's per-frame tick costs, attributed to the layer that owns it — the <code>update</code> / <code>editorUpdate</code> its entrypoint declares, timed where it runs. <code>totalMs</code> is a SUM across the window <code>layers.observe().window</code> reports, so divide by <code>calls</code> (or read <code>avgMs</code>) for the per-tick figure; a tick that runs every frame makes that the per-frame figure. Call <code>layers.resetCostWindow()</code> first to time a particular stretch. A layer whose entrypoint declares no tick is absent.</p> <pre><code class="language-lua">layers.resetCostWindow(); task.wait(1); for _, c in layers.cost() do print(c.name, c.avgMs) end </code></pre> <h2 id="modules-layers-find">modules/layers/find</h2> <pre><code class="language-lua">find(ref: AssetRef<scene> | string): any? </code></pre> <p>The loaded layer for a scene, matched on guid — the canonical identity, since display names can collide and paths drift when assets move. A layer torn down but not yet pumped out of the engine's loaded list reads as gone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>AssetRef<scene> | string</code> — A scene <code>AssetRef</code>, or an identity string resolved through <code>asset.ref</code>.</li> </ul> <pre><code class="language-lua">local layer = layers.find("scenes.arena") </code></pre> <h2 id="modules-layers-firebeforeload">modules/layers/fireBeforeLoad</h2> <pre><code class="language-lua">fireBeforeLoad(proxy: any): nil </code></pre> <p>Announce that a scene layer is about to load: clears any pending unload for that layer slot, marks the proxy loading, and fans out to every <code>layers.onBeforeLoad</code> subscriber. The scene-load pipeline calls this.</p> <p><strong>Parameters</strong></p> <ul> <li><code>proxy</code> <code>any</code> <em>(optional)</em> — The scene proxy about to load.</li> </ul> <pre><code class="language-lua">layers.fireBeforeLoad(sceneProxy) </code></pre> <h2 id="modules-layers-fireload">modules/layers/fireLoad</h2> <pre><code class="language-lua">fireLoad(proxy: any): nil </code></pre> <p>Announce that a scene layer has loaded, fanning out to every <code>layers.onLoad</code> subscriber. The layer is pinned as the active one for the duration of the fan-out, so entities a subscriber spawns are attributed to it rather than landing orphaned. The scene-load pipeline calls this.</p> <p><strong>Parameters</strong></p> <ul> <li><code>proxy</code> <code>any</code> <em>(optional)</em> — The loaded scene proxy.</li> </ul> <pre><code class="language-lua">layers.fireLoad(sceneProxy) </code></pre> <h2 id="modules-layers-fireunload">modules/layers/fireUnload</h2> <pre><code class="language-lua">fireUnload(proxy: any): nil </code></pre> <p>Announce that a scene layer is unloading: fans out to every <code>layers.onUnload</code> subscriber, then drops the layer's cached proxy and per-layer state so the next load of that scene rebuilds from disk. The unload path calls this.</p> <p><strong>Parameters</strong></p> <ul> <li><code>proxy</code> <code>any</code> <em>(optional)</em> — The scene proxy being unloaded.</li> </ul> <pre><code class="language-lua">layers.fireUnload(sceneProxy) </code></pre> <h2 id="modules-layers-install">modules/layers/install</h2> <pre><code class="language-lua">install(): nil </code></pre> <p>Install the <code>layers</code> global. <code>layers.active</code> is exposed as a property whose every read resolves the current root scene, so it tracks scene changes without manual invalidation; other keys resolve against this module. The prelude calls this once at boot.</p> <pre><code class="language-lua">layers.install() </code></pre> <h2 id="modules-layers-inventory">modules/layers/inventory</h2> <pre><code class="language-lua">inventory(): { SceneLayerInventory } </code></pre> <p>What each loaded layer holds: the entities the engine attributes to it, whether it came up whole, and how many failures it carries. <code>unattributed</code> in <code>layers.observe().totals</code> counts what exists in the world that no layer claims.</p> <pre><code class="language-lua">for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end </code></pre> <h2 id="modules-layers-is-loaded">modules/layers/is_loaded</h2> <pre><code class="language-lua">is_loaded(ref: AssetRef<scene> | string): boolean </code></pre> <p>Whether a scene currently has a loaded layer — the boolean form of <code>layers.find</code>. A scene counts as loaded from the frame the engine holds a layer slot for it — the same slot its entities are attributed to — until an unload is issued against that slot. So a gate like <code>if layers.is_loaded(ref) then layers.unload(ref) end</code> sees the layer on the frame its entities exist.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>AssetRef<scene> | string</code> — A scene <code>AssetRef</code>, or an identity string.</li> </ul> <pre><code class="language-lua">if not layers.is_loaded("scenes.hud") then layers.load("scenes.hud", { additive = true }) end </code></pre> <h2 id="modules-layers-lastload">modules/layers/lastLoad</h2> <pre><code class="language-lua">lastLoad(): SceneLoadReport? </code></pre> <p>The most recent load's report: what it loaded, what root it replaced and which overlays went with it, the entity counts on each side, how long each phase took, and every failure it produced. Nil on an engine that has loaded nothing — which is how "nothing has loaded" reads differently from a load that changed nothing.</p> <pre><code class="language-lua">local r = layers.lastLoad(); print(r.name, r.outcome, r.entities.added) </code></pre> <h2 id="modules-layers-lastunload">modules/layers/lastUnload</h2> <pre><code class="language-lua">lastUnload(): SceneUnloadReport? </code></pre> <p>The most recent unload's report: the layer it took down under the name it was loaded with, the overlays it cascaded, and the entities that went with them. A guid no longer resolves to a name once its layer is gone, so this is where that name survives.</p> <pre><code class="language-lua">local u = layers.lastUnload(); print(u.name, u.entities.removed) </code></pre> <h2 id="modules-layers-list">modules/layers/list</h2> <pre><code class="language-lua">list(): { any } </code></pre> <p>Every loaded scene layer as a proxy, root and additive alike, in the order the engine reports them. A proxy carries <code>name</code>, <code>sceneIdentity</code> (the scene asset it was loaded from, e.g. <code>scenes.main</code>), <code>guid</code>, <code>path</code>, <code>additive</code>, <code>persistent</code> and <code>visible</code>. The root one — the open scene — is also <code>layers.active</code>.</p> <pre><code class="language-lua">for _, layer in ipairs(layers.list()) do print(layer.name, layer.sceneIdentity, layer.additive) end print("open scene:", layers.active.sceneIdentity) </code></pre> <h2 id="modules-layers-load">modules/layers/load</h2> <pre><code class="language-lua">load(ref: AssetRef<scene> | string, opts: LoadOpts?): any </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>AssetRef<scene> | string</code></li> <li><code>opts</code> <code>LoadOpts?</code> <em>(optional)</em></li> </ul> <h2 id="modules-layers-loadhistory">modules/layers/loadHistory</h2> <pre><code class="language-lua">loadHistory(): { SceneLoadReport } </code></pre> <p>Every load report the engine still holds, oldest first. Bounded — old reports fall off the front, so a long session's memory does not grow with how many times a scene was swapped.</p> <pre><code class="language-lua">for _, r in layers.loadHistory() do print(r.name, r.durationMs) end </code></pre> <h2 id="modules-layers-loadinflight">modules/layers/loadInFlight</h2> <pre><code class="language-lua">loadInFlight(): number </code></pre> <p>Returns the number of scene loads currently in flight (queued but not yet visible via <code>onLoad</code> dispatch). Returns 0 when the engine is in a stable load state. Used by <code>engine.mode = ...</code> to block flips while a load is mid-air; agents can read this to wait for a load to finish before driving the next operation.</p> <h2 id="modules-layers-observe">modules/layers/observe</h2> <pre><code class="language-lua">observe(): SceneObservation </code></pre> <p>What every scene load did, and what each loaded scene costs. One read covering the last load's report (what it produced, what it replaced, what it failed to produce and why, and how long each phase took), the load and unload history, a per-layer inventory of what the engine attributes to each layer, and the per-frame cost of each layer's entrypoint tick. Answers in edit mode as well as play.</p> <pre><code class="language-lua">local o = layers.observe(); print(o.lastLoad.outcome, o.lastLoad.durationMs) for _, c in layers.observe().cost do print(c.name, c.avgMs) end </code></pre> <h2 id="modules-layers-offbeforeload">modules/layers/offBeforeLoad</h2> <pre><code class="language-lua">offBeforeLoad(h: number): boolean return remove(beforeLoadCbs, h) end </code></pre> <p>Cancel a <code>layers.onBeforeLoad</code> subscription.</p> <p><strong>Parameters</strong></p> <ul> <li><code>h</code> <code>number</code> — The handle <code>layers.onBeforeLoad</code> returned.</li> </ul> <pre><code class="language-lua">layers.offBeforeLoad(h) </code></pre> <h2 id="modules-layers-offentitychanged">modules/layers/offEntityChanged</h2> <pre><code class="language-lua">offEntityChanged(h: number): boolean </code></pre> <p>Remove a subscription made with <code>layers.onEntityChanged</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>h</code> <code>number</code> — The handle returned by <code>layers.onEntityChanged</code>.</li> </ul> <pre><code class="language-lua">layers.offEntityChanged(handle) </code></pre> <h2 id="modules-layers-offload">modules/layers/offLoad</h2> <pre><code class="language-lua">offLoad(h: number): boolean return remove(loadCbs, h) end </code></pre> <p>Cancel a <code>layers.onLoad</code> subscription.</p> <p><strong>Parameters</strong></p> <ul> <li><code>h</code> <code>number</code> — The handle <code>layers.onLoad</code> returned.</li> </ul> <pre><code class="language-lua">layers.offLoad(h) </code></pre> <h2 id="modules-layers-offunload">modules/layers/offUnload</h2> <pre><code class="language-lua">offUnload(h: number): boolean return remove(unloadCbs, h) end </code></pre> <p>Cancel a <code>layers.onUnload</code> subscription.</p> <p><strong>Parameters</strong></p> <ul> <li><code>h</code> <code>number</code> — The handle <code>layers.onUnload</code> returned.</li> </ul> <pre><code class="language-lua">layers.offUnload(h) </code></pre> <h2 id="modules-layers-onbeforeload">modules/layers/onBeforeLoad</h2> <pre><code class="language-lua">onBeforeLoad(cb: (any) -> ()): number return push(beforeLoadCbs, cb) end </code></pre> <p>Run a callback just before a scene layer loads, while the previous layer's entities are still present.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(any) -> ()</code> — Receives the scene proxy about to load.</li> </ul> <pre><code class="language-lua">local h = layers.onBeforeLoad(function(scene) print("loading", scene.name) end) </code></pre> <h2 id="modules-layers-onentitychanged">modules/layers/onEntityChanged</h2> <pre><code class="language-lua">onEntityChanged(cb: (any) -> ()): number </code></pre> <p>Subscribe to authored entity changes. The callback runs once per frame with every entity edited since the previous frame, batched by layer as <code>{ { scene = string, entities = { string } } }</code> — a moved transform, an edited component field, a spawn, or a despawn (the id of a despawned entity arrives with <code>entity.exists</code> already false). Any number of subscribers can watch the same edits.</p> <p>Scope: authored edits in edit mode — what lands in the scene's dirty overlay. Mutations a component makes from its own <code>update</code> are runtime behavior and do not appear, so a subscriber that rebuilds derived data cannot re-trigger itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(any) -> ()</code> — Called with the change batch.</li> </ul> <pre><code class="language-lua">layers.onEntityChanged(function(batch) for _, row in ipairs(batch) do for _, id in ipairs(row.entities) do print("changed", id) end end end) </code></pre> <h2 id="modules-layers-onload">modules/layers/onLoad</h2> <pre><code class="language-lua">onLoad(cb: (any) -> ()): number return push(loadCbs, cb) end </code></pre> <p>Run a callback once a scene layer has loaded — the point where its entities exist and player / camera spawners can attach to them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(any) -> ()</code> — Receives the loaded scene proxy.</li> </ul> <pre><code class="language-lua">local h = layers.onLoad(function(scene) print("loaded", scene.name) end) </code></pre> <h2 id="modules-layers-onunload">modules/layers/onUnload</h2> <pre><code class="language-lua">onUnload(cb: (any) -> ()): number return push(unloadCbs, cb) end </code></pre> <p>Run a callback as a scene layer unloads, while its entities are still addressable — the place to release anything keyed to them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(any) -> ()</code> — Receives the scene proxy being unloaded.</li> </ul> <pre><code class="language-lua">local h = layers.onUnload(function(scene) print("unloaded", scene.name) end) </code></pre> <h2 id="modules-layers-problems">modules/layers/problems</h2> <pre><code class="language-lua">problems(ref: (AssetRef<scene> | string | any)?): { SceneLoadFailure } </code></pre> <p>What a layer failed to produce, and why. Each entry names the phase it happened in, one reason from the closed set, and the engine's own words — plus the entity, component or lifecycle hook it is about when it is about one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>(AssetRef<scene> | string | any)?</code> <em>(optional)</em> — A scene <code>AssetRef</code>, an identity string, or a scene proxy. Omit for the active root layer.</li> </ul> <pre><code class="language-lua">for _, f in layers.problems() do print(f.reason, f.entity, f.message) end </code></pre> <h2 id="modules-layers-rebuildinflight">modules/layers/rebuildInFlight</h2> <pre><code class="language-lua">rebuildInFlight(): boolean </code></pre> <p>Whether the engine is rebuilding the live scene right now: a scene load is carrying entities in, an edit↔play flip's transition is materialising the layer set, or a scene's <code>build.luau</code> build is landing what it produced. A flip unloads the root layer and loads it again for the new mode across many frames, and each mode materialises a different set of entities; a build lands its records and saves the scene over several frames. Either way the live entities are a stage of a scene being built while this reads true. A caller whose answer belongs to the settled scene, a test taking a root or a validator judging the live tree, polls it down to false first.</p> <pre><code class="language-lua">if not layers.rebuildInFlight() then print(#entity.findAll(), "entities settled") end </code></pre> <h2 id="modules-layers-reload">modules/layers/reload</h2> <pre><code class="language-lua">reload(ref: (AssetRef<scene> | string)?): any? </code></pre> <p>Unload and re-load a scene layer in place, so an edited scene asset takes effect without rebuilding the surrounding layer stack. The scene's <code>build.luau</code> runs against what it resolves right now, so a build script whose inputs moved (a component that now exists, an asset that now resolves) produces the scene it describes today. The rebuild saves the scene, as a write to <code>build.luau</code> does, so <code>scene.json</code> holds what the reload built and the next play loads it; unsaved edits in the scene are saved with it. A library scene (under <code>/libs/</code>) is rebuilt in the live scene alone. A reload asked for while a build of the scene is landing (a write to <code>build.luau</code> rebuilds over several frames) waits for that build to land and save first.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>(AssetRef<scene> | string)?</code> <em>(optional)</em> — A scene <code>AssetRef</code>, or an identity string. Omit to reload the active root scene.</li> </ul> <pre><code class="language-lua">layers.reload("scenes.arena") </code></pre> <h2 id="modules-layers-resetcostwindow">modules/layers/resetCostWindow</h2> <pre><code class="language-lua">resetCostWindow(): nil </code></pre> <p>Open a new cost window, discarding what the previous one measured. Call this before timing a stretch of frames; the load history is untouched.</p> <pre><code class="language-lua">layers.resetCostWindow() </code></pre> <h2 id="modules-layers-unload">modules/layers/unload</h2> <pre><code class="language-lua">unload(refOrProxy: (AssetRef<scene> | string | any)?): nil </code></pre> <p>Unload a scene layer. Unloading the root cascades through its additive overlays first, most-recently-loaded first, so none is left as a layer the engine still lists after its entities are gone; persistent additive layers survive the cascade. A scene with no loaded layer is a no-op. An unload made while a load is part way through a scene's records waits for that load to apply the rest.</p> <p><strong>Parameters</strong></p> <ul> <li><code>refOrProxy</code> <code>(AssetRef<scene> | string | any)?</code> <em>(optional)</em> — A scene <code>AssetRef</code>, an identity string, or a scene proxy. Omit to unload the active root scene.</li> </ul> <pre><code class="language-lua">layers.unload("scenes.hud") layers.unload() -- the active root, plus its non-persistent overlays </code></pre> <h2 id="modules-layers-whypartial">modules/layers/whyPartial</h2> <pre><code class="language-lua">whyPartial(ref: (AssetRef<scene> | string | any)?): (string?, string?) </code></pre> <p>Why a layer is not whole. Returns nil when it IS — everything the scene declared was produced — and otherwise the nearest cause from the closed set <code>loaderRaised</code>, <code>entrypointCompileFailed</code>, <code>entrypointBodyRaised</code>, <code>entrypointRaised</code>, <code>buildRaised</code>, <code>entityFailed</code>, <code>parentMissing</code>, <code>parentRefused</code>, <code>parentAbandoned</code>, <code>componentUnresolved</code>, <code>componentRefused</code>, <code>subscriberRaised</code>, <code>updateRaised</code>. A second return carries the engine's own words for that cause.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>(AssetRef<scene> | string | any)?</code> <em>(optional)</em> — A scene <code>AssetRef</code>, an identity string, or a scene proxy. Omit for the active root layer.</li> </ul> <pre><code class="language-lua">local why, detail = layers.whyPartial(); if why then print(why, detail) end </code></pre> <h2 id="modules-lensfx-readme">modules/lensFx/README</h2> <pre><code class="language-lua">require("@builtin::systems.lensFx.lensFx") -- lensFx </code></pre> <p>The lens and the film — flare thrown by bright sources, dirt on the front element, and grain that is redrawn every frame rather than a fixed screen pattern.</p> <p>Usage: local lensFx = require("@builtin::systems.lensFx.lensFx")</p> <h2 id="modules-lensfx-active">modules/lensFx/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the lens passes are running this frame.</p> <pre><code class="language-lua">if lensFx.active() then ... end </code></pre> <h2 id="modules-lensfx-clear">modules/lensFx/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Turn the flare and grain off and release the passes. The other settings are kept, so a later <code>set</code> brings back the same look.</p> <pre><code class="language-lua">lensFx.clear() </code></pre> <h2 id="modules-lensfx-get">modules/lensFx/get</h2> <pre><code class="language-lua">get(): LensFxState </code></pre> <p>The lens settings currently in force.</p> <pre><code class="language-lua">local g = lensFx.get().grainIntensity </code></pre> <h2 id="modules-lensfx-paramsbuffer">modules/lensFx/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.</p> <pre><code class="language-lua">local p = lensFx.paramsBuffer() </code></pre> <h2 id="modules-lensfx-set">modules/lensFx/set</h2> <pre><code class="language-lua">set(opts: LensFxOpts?): LensFxState </code></pre> <p>Set the lens and film. Any omitted field keeps its current value. With both <code>flareIntensity</code> and <code>grainIntensity</code> at 0 the passes are released.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>LensFxOpts?</code> <em>(optional)</em> — Lens settings — see <code>LensFxOpts</code>.</li> </ul> <pre><code class="language-lua">lensFx.set({ flareIntensity = 1.0, grainIntensity = 0.04 }) </code></pre> <h2 id="modules-lensfx-sync">modules/lensFx/sync</h2> <pre><code class="language-lua">sync() </code></pre> <p>Bring the buffer's grain clock to the instant this frame draws at. The render feature calls it once a frame, so the field follows a <code>renderer.temporal.hold</code> the moment one is taken or released, whatever is driving the scene's own clock.</p> <pre><code class="language-lua">lensFx.sync() </code></pre> <h2 id="modules-lensfx-tick">modules/lensFx/tick</h2> <pre><code class="language-lua">tick(dt: number) </code></pre> <p>Advance the clock the grain is drawn against. Grain is a fresh field every frame rather than a fixed screen pattern, so it needs the frame's own time — which cannot arrive through <code>set</code>, since a value that has not changed is not re-pushed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>dt</code> <code>number</code> — Seconds since the previous frame.</li> </ul> <pre><code class="language-lua">function update(dt) lensFx.tick(dt) end </code></pre> <h2 id="modules-library-readme">modules/library/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.library") -- library (also available as global 'library') </code></pre> <p>Discover, check, and import library assets — the <code>@builtin/*</code> tree the engine ships and the <code>@namespace/*</code> trees imported from other worlds.</p> <p>Usage: local library = require("@builtin::modules.api.engine.library") Also available as global: library</p> <h2 id="modules-library-has">modules/library/has</h2> <pre><code class="language-lua">has(path: string): boolean </code></pre> <p>Check if a library asset exists at the given path. Answers from the content the engine holds: an imported library's namespace (<code>"@mylib"</code>) and its assets answer true once its pinned commit is mounted, and false while only its marker is there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Library asset path (e.g. "@builtin/models/Sample/DamagedHelmet").</li> </ul> <pre><code class="language-lua">assert(library.has("@builtin/models/Cube")) </code></pre> <h2 id="modules-library-import">modules/library/import</h2> <pre><code class="language-lua">import(namespace: string, worldRef: string): LibraryImport </code></pre> <p>Import another world as a library under the given namespace. Resolves the world, pins its current commit, and writes the library marker at <code>/source/libs/@<namespace></code>, then waits while the engine fetches the pinned commit from ZeroMind and mounts it. When it returns, the imported tree answers to <code>require("@<namespace>::path")</code> and is listed by <code>library.list()</code>. A library that does not mount raises naming why, and running the import again retries it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>namespace</code> <code>string</code> — Library namespace, with or without the leading <code>@</code> (e.g. <code>"@mylib"</code> or <code>"mylib"</code>).</li> <li><code>worldRef</code> <code>string</code> — The upstream world's guid, or its name as it appears in <code>world.list()</code>.</li> </ul> <pre><code class="language-lua">library.import("@mylib", "my-shared-world") </code></pre> <h2 id="modules-library-list">modules/library/list</h2> <pre><code class="language-lua">list(assetType: string?): { LibraryAsset } </code></pre> <p>List all available library assets. Optionally filter by asset type — call <code>asset.categories()</code> for the live set.</p> <p><strong>Parameters</strong></p> <ul> <li><code>assetType</code> <code>string?</code> <em>(optional)</em> — Asset type filter (optional).</li> </ul> <pre><code class="language-lua">for _, a in ipairs(library.list("model")) do print(a.path) end </code></pre> <h2 id="modules-lightcookies-readme">modules/lightCookies/README</h2> <pre><code class="language-lua">require("@builtin::systems.lightCookies.lightCookies") -- lightCookies </code></pre> <p>Project an authored image through a spot light's cone — a gobo, a window pattern, a headlight cutoff.</p> <p>Usage: local lightCookies = require("@builtin::systems.lightCookies.lightCookies")</p> <h2 id="modules-lightcookies-clear">modules/lightCookies/clear</h2> <pre><code class="language-lua">clear(light: any): boolean </code></pre> <p>Stop projecting a cookie through a light's cone. The layer keeps its contents; the light stops reading it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>light</code> <code>any</code> <em>(optional)</em> — The spot light's entity id or entity proxy.</li> </ul> <pre><code class="language-lua">lightCookies.clear(lampId) </code></pre> <h2 id="modules-lightcookies-configure">modules/lightCookies/configure</h2> <pre><code class="language-lua">configure(opts: { resolution: number, layers: number }): (number?, string?) </code></pre> <p>Size the shared feature-texture array for cookies. Call once, before setting any cookie, with a layer count covering every layer the scene uses — resizing the array zeroes every layer in it, including layers other features own. <code>renderer.featureTexture.state()</code> reports the extent the array carries and the layers holding content, so a cookie that has left <code>filled</code> is one to set again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ resolution: number, layers: number }</code> — <code>{ resolution, layers }</code> — <code>resolution</code> is the square extent of each layer in pixels, a multiple of 32; <code>layers</code> is how many the array holds.</li> </ul> <pre><code class="language-lua">lightCookies.configure({ resolution = 256, layers = 4 }) </code></pre> <h2 id="modules-lightcookies-fill">modules/lightCookies/fill</h2> <pre><code class="language-lua">fill(image: any, opts: Options): (number?, string?) </code></pre> <p>Fill one layer of the shared array with an authored image, ready for a spot light's <code>cookieLayer</code> to project it. The fill lands over the frames after the call: its rasterize is issued on the next frame and its copy into the layer on the frame after that, and it is written once the array reports the copy landed, one fill at a time in the order they were asked for. It lands whatever happens to the code that asked for it. <code>lightCookies.status(layer)</code> reports where the fill stands and, for a fill that could not land, why.</p> <p><strong>Parameters</strong></p> <ul> <li><code>image</code> <code>any</code> <em>(optional)</em> — A texture <code>AssetRef</code>, a <code>TextureHandle</code>, or any string <code>asset.ref</code> resolves to one: a guid, an identity, a name or a source path.</li> <li><code>opts</code> <code>Options</code> — <code>{ layer, gain?, tint? }</code>; <code>layer</code> is below the layer count <code>configure</code> sized the array to.</li> </ul> <pre><code class="language-lua">lightCookies.fill("/textures/cookies/window_blinds.png", { layer = 0 }) </code></pre> <h2 id="modules-lightcookies-layerof">modules/lightCookies/layerOf</h2> <pre><code class="language-lua">layerOf(light: any): number? </code></pre> <p>The layer a light is currently projecting, or nil when it has no cookie.</p> <p><strong>Parameters</strong></p> <ul> <li><code>light</code> <code>any</code> <em>(optional)</em> — The spot light's entity id or entity proxy.</li> </ul> <pre><code class="language-lua">print(lightCookies.layerOf(lampId)) </code></pre> <h2 id="modules-lightcookies-observe">modules/lightCookies/observe</h2> <pre><code class="language-lua">observe(): { [string]: any } </code></pre> <p>What the module is doing: how many fills are waiting, the one in progress, the loop that drains them, and how many times <code>configure</code> has sized the array.</p> <pre><code class="language-lua">print(lightCookies.observe().pending) </code></pre> <h2 id="modules-lightcookies-project">modules/lightCookies/project</h2> <pre><code class="language-lua">project(light: any, image: any, opts: Options): (number?, string?) </code></pre> <p>Project an image through a spot light's cone: fill a layer with it and point the light at that layer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>light</code> <code>any</code> <em>(optional)</em> — The spot light's entity id or entity proxy.</li> <li><code>image</code> <code>any</code> <em>(optional)</em> — A texture asset, in any form <code>lightCookies.fill</code> accepts.</li> <li><code>opts</code> <code>Options</code> — <code>{ layer, gain?, tint? }</code>.</li> </ul> <pre><code class="language-lua">lightCookies.project(lampId, "/textures/cookies/blinds.png", { layer = 0 }) </code></pre> <h2 id="modules-lightcookies-status">modules/lightCookies/status</h2> <pre><code class="language-lua">status(layer: number): Status? </code></pre> <p>Where the last fill of a layer stands. A fill passes through "queued", "rasterizing" and "copying" to "written" over the frames after <code>fill</code> returns, and reads "written" from the reading of the array that first reports its copy landed in the layer; a fill that could not land (the GPU refused its rasterize, a later <code>configure</code> left the array without its layer, or its copy never reached the array) reads "refused" with the reason. <code>filled</code> is the array's own reading of the layer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>layer</code> <code>number</code> — The layer index a <code>fill</code> or <code>project</code> was given.</li> </ul> <pre><code class="language-lua">local s = lightCookies.status(0); print(s.state, s.reason, s.filled) </code></pre> <h2 id="modules-logs-readme">modules/logs/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.logs") -- logs (also available as global 'logs') </code></pre> <p>Read-only query surface over the engine's in-memory log ring buffer. Public Luau surface over the <code>__logs</code> Internal FFI namespace.</p> <p>Usage: local logs = require("@builtin::modules.api.engine.logs") Also available as global: logs</p> <h2 id="modules-logs-clear">modules/logs/clear</h2> <pre><code class="language-lua">clear(): boolean </code></pre> <p>Drop all buffered log entries. Lifetime per-level counts (<code>logs.count</code>) are preserved.</p> <pre><code class="language-lua">logs.clear() </code></pre> <h2 id="modules-logs-count">modules/logs/count</h2> <pre><code class="language-lua">count(opts: LogQueryOpts?): LogCounts </code></pre> <p>Aggregate counters for the log ring. Lifetime counts survive eviction, so <code>errors</code> reflects the total seen even if the lines have scrolled out of the buffer. <code>opts</code> takes the same filter table as <code>logs.query</code>, and <code>matched</code> is how many held entries it selects, counted without materialising them — <code>limit</code> and <code>newest_first</code> bound and order what a query RETURNS, so they leave <code>matched</code> alone. <code>mcp</code> is how many held entries record your own tool traffic; a query leaves those out, so with no <code>opts</code>, <code>matched</code> + <code>mcp</code> is everything held. <code>last_seq</code> is the cursor for incremental polling: read it before an action, then pass it as <code>logs.query({ since = <that> })</code> afterwards to see only what the action logged.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>LogQueryOpts?</code> <em>(optional)</em> — Filter options, as <code>logs.query</code> takes.</li> </ul> <pre><code class="language-lua">print("errors:", logs.count().errors) local before = logs.count().last_seq </code></pre> <h2 id="modules-logs-errors">modules/logs/errors</h2> <pre><code class="language-lua">errors(limit: number?): { LogEntry } </code></pre> <p>Most-recent ERROR-level entries (newest first). <code>limit</code> defaults to 100.</p> <p><strong>Parameters</strong></p> <ul> <li><code>limit</code> <code>number?</code> <em>(optional)</em> — Maximum entries to return.</li> </ul> <pre><code class="language-lua">for _, e in ipairs(logs.errors(20)) do print(e.message) end </code></pre> <h2 id="modules-logs-find">modules/logs/find</h2> <pre><code class="language-lua">find(text: string, limit: number?): { LogEntry } </code></pre> <p>Case-insensitive substring search over log messages. <code>limit</code> defaults to 200 (keeps the most recent matches). Searches what the engine logged, so looking for a marker cannot return the call that looked for it; <code>logs.query({ contains = ..., include_mcp = true })</code> searches your own tool traffic too.</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — Substring to search for.</li> <li><code>limit</code> <code>number?</code> <em>(optional)</em> — Maximum entries to return.</li> </ul> <pre><code class="language-lua">local hits = logs.find("MY_MARKER") </code></pre> <h2 id="modules-logs-query">modules/logs/query</h2> <pre><code class="language-lua">query(opts: LogQueryOpts?): { LogEntry } </code></pre> <p>Query the engine's in-memory log ring — the filtered view of what also reads as plain text at <code>/zero/runtime/logs/engine</code>. Answers about what the engine logged: the MCP record of your own tool traffic is left out, because the call carrying the query is one of those records and an unqualified search would match itself. <code>type = "MCP"</code> selects them; <code>include_mcp = true</code> mixes them in with everything else. On a world several sessions share, <code>origin = "local"</code> narrows the answer to the lines this session's own authoring caused.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>LogQueryOpts?</code> <em>(optional)</em> — Filter options.</li> </ul> <pre><code class="language-lua">logs.query({ entity = "guard-1", limit = 20 }) logs.query({ level = "error", context = 3 }) for _, e in ipairs(logs.query({ level = "warn", limit = 50 })) do print(e.message) end </code></pre> <h2 id="modules-logs-tail">modules/logs/tail</h2> <pre><code class="language-lua">tail(limit: number?): { LogEntry } </code></pre> <p>Most-recent entries of any level in chronological order. <code>limit</code> defaults to 100.</p> <p><strong>Parameters</strong></p> <ul> <li><code>limit</code> <code>number?</code> <em>(optional)</em> — Maximum entries to return.</li> </ul> <pre><code class="language-lua">for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end </code></pre> <h2 id="modules-logs-template">modules/logs/template</h2> <pre><code class="language-lua">template(message: string): string </code></pre> <p>Normalize a message to its template — the same line with the parts that vary between occurrences (numbers, hashes, entity ids) masked out. Two messages that differ only in those parts share a template, which is what turns "this error repeated 400 times" into one row instead of 400. The engine keys its own error retention by the same normalization, so grouping built on this agrees with what survives ring eviction.</p> <p><strong>Parameters</strong></p> <ul> <li><code>message</code> <code>string</code> — Log message to normalize.</li> </ul> <pre><code class="language-lua">local key = logs.template(entry.message) </code></pre> <h2 id="modules-logs-warnings">modules/logs/warnings</h2> <pre><code class="language-lua">warnings(limit: number?): { LogEntry } </code></pre> <p>Most-recent WARN+ entries (newest first). <code>limit</code> defaults to 100.</p> <p><strong>Parameters</strong></p> <ul> <li><code>limit</code> <code>number?</code> <em>(optional)</em> — Maximum entries to return.</li> </ul> <pre><code class="language-lua">print(#logs.warnings(), "warnings") </code></pre> <h2 id="modules-lsp-readme">modules/lsp/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.lsp") -- lsp (also available as global 'lsp') </code></pre> <p>Embedded Luau language server — check / search / inspect Public Luau surface over the <code>__lsp</code> Internal FFI namespace.</p> <p>Usage: local lsp = require("@builtin::modules.api.engine.lsp") Also available as global: lsp</p> <h2 id="modules-lsp-analysisflags">modules/lsp/analysisFlags</h2> <pre><code class="language-lua">analysisFlags(): AnalysisFlags </code></pre> <p>The feature-flag configuration upstream Luau's analysis library (the linter and <code>.luaurc</code> parser behind these checks) runs under, read back from Luau's flag registry. It is upstream's own: every <code>Luau*</code> flag the analysis sources declare is on unless upstream's experimental list names it, and every other flag they declare keeps the value Luau registers it at. The flags the analysis library reads from the parser and the VM are <code>shared</code>: they run at the value the engine's runtime gives them, and one that differs from upstream's carries its <code>deviation</code>. <code>flags</code> lists every flag those sources declare or read, sorted by name; <code>atUpstream</code> counts the linked ones whose value is upstream's. <code>available</code> is false, with <code>reason</code>, on a build that does not link the analysis library.</p> <h2 id="modules-lsp-check">modules/lsp/check</h2> <pre><code class="language-lua">check(path: string, opts: CheckFileOpts?): DiagnosticsResult </code></pre> <p>Validate a single <code>.luau</code> file in the VFS and return its diagnostics. A path the check could not read comes back as one <code>lsp-check-*</code> error naming the path and the reason, and a file past the 2 MiB analysis bound as one <code>source-too-large</code> warning with <code>checked = false</code>, so <code>checked</code> with <code>errors == 0</code> means a code body was analysed and is clean. <code>opts.upstream</code> (true, or the options of <code>lsp.upstreamCheck</code>) also checks the file with upstream Luau's type checker and sets <code>upstream</code> to what <code>lsp.upstreamCheck</code> returns for it, whose <code>modules</code> say how the check of each module it reached ended and how long it took; the diagnostics and counts stay the engine checker's.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path.</li> <li><code>opts</code> <code>CheckFileOpts?</code> <em>(optional)</em> — <code>{ severity?, limit?, context?, upstream? }</code>.</li> </ul> <pre><code class="language-lua">local diags = lsp.check("/zero/source/main.luau") </code></pre> <h2 id="modules-lsp-checkall">modules/lsp/checkAll</h2> <pre><code class="language-lua">checkAll(opts: CheckAllOpts?): CheckAllResult </code></pre> <p>Validate the user's Luau scripts and return an aggregate summary plus diagnostic list. <code>opts.scope = "user"</code> (default) skips library mounts; <code>"all"</code> includes them. The sweep is time-budgeted (ZERO_EXECUTE_INLINE_BUDGET_MS, ~20s by default): if the budget elapses it returns the partial result gathered so far with <code>budgetExceeded = true</code> rather than blocking the engine. <code>opts.budgetMs</code> sets the budget for the call and <code>opts.maxFiles</code> caps the files it checks. <code>opts.upstream</code> (true, or the options of <code>lsp.upstreamCheck</code>) also checks each file with upstream Luau's type checker once the engine's own pass is done, within the same budget, and sets <code>upstream</code> to what that pass found: every module it reached, once, with how its check ended ("completed", "timedOut", "internal", or "notAnalysable" with the reason) and how long it took, <code>reused</code> marking a module checked before the pass reached it and unchanged since. The pass starts no file once the budget is spent, so it ends within the budget plus the check of the one file in progress.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>CheckAllOpts?</code> <em>(optional)</em> — <code>{ scope?, severity?, limit?, budgetMs?, maxFiles?, upstream? }</code>.</li> </ul> <h2 id="modules-lsp-checkcode">modules/lsp/checkCode</h2> <pre><code class="language-lua">checkCode(source: string, opts: CheckOpts?): DiagnosticsResult </code></pre> <p>Validate inline Luau source without a backing file. Useful for checking code before writing it to disk. A source past the 2 MiB analysis bound comes back as one <code>source-too-large</code> warning with <code>checked = false</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>source</code> <code>string</code> — Luau source.</li> <li><code>opts</code> <code>CheckOpts?</code> <em>(optional)</em> — <code>{ severity?, limit?, context? }</code>.</li> </ul> <h2 id="modules-lsp-checkdirty">modules/lsp/checkDirty</h2> <pre><code class="language-lua">checkDirty(): DiagnosticsResult </code></pre> <p>Drain the dirty-file set populated by the hot-reload hook, validate each, and return the combined diagnostic list.</p> <h2 id="modules-lsp-definitions">modules/lsp/definitions</h2> <pre><code class="language-lua">definitions(opts: DefinitionsOpts?): Definitions </code></pre> <p>The engine's API as a Luau definitions source, upstream Luau's <code>.d.luau</code> form (<code>declare</code> statements and <code>export type</code> aliases), generated from the live API registry: the documented members, the <code>--!global-types</code> aliases and the asset-category shapes <code>lsp.check</code> types against, bound under the same names. It declares each global the calling script's globals hold, so a user script, a trusted script and a worker each get the globals they have; a name upstream's builtin environment declares keeps upstream's type and is left out. <code>lsp.upstreamCheck</code> loads the same source into every module it checks, and an external editor running luau-lsp reads it as a definitions file (<code>lsp.exportDefinitions</code> writes it out). It is generated again whenever the registry changes, a library hot reload included. <code>package</code> (<code>@zero</code>) keys each global's documentation symbol (<code>@zero/global/<name></code>, <code>@zero/global/<name>.<member></code>). Each type string the registry holds is declared as written, or listed in <code>unrepresented</code> with the reason and declared <code>any</code>; a declaration upstream refuses to load (an alias that reaches itself through aliases alone) is listed with upstream's reason, so the rest of the source loads. <code>opts.documentation = true</code> also returns <code>documentation</code>, the JSON documentation file luau-lsp reads beside the definitions: each symbol's signature and description. <code>available</code> is false, with <code>reason</code>, on a build that does not link the analysis library.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>DefinitionsOpts?</code> <em>(optional)</em> — <code>{ documentation? }</code>.</li> </ul> <pre><code class="language-lua">local d = lsp.definitions() print(d.bytes, #d.globals, #d.unrepresented) </code></pre> <h2 id="modules-lsp-describe">modules/lsp/describe</h2> <pre><code class="language-lua">describe(path: string, opts: DescribeOpts?): DocEntry? </code></pre> <p>Inspect a single documented entry. Returns the full doc table (signature, args, returns, examples, level), or nil. The path is resolved independently of which root the doc is registered under and of separator style, so the spelling that reads off the API surface (<code>renderer.texture.create</code>) finds the entry registered as <code>globals/renderer/texture/create</code>. A path naming a binding the engine registered internally answers with the entry a Luau module publishes over it where there is one, so the signature is the call content makes; <code>opts.includeInternal</code> answers with the internally registered entry itself. When a path does not resolve, <code>lsp.describePaths</code> says what the registry holds near it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Doc path (e.g. <code>"asset/resolve"</code>, <code>"renderer.texture.create"</code>).</li> <li><code>opts</code> <code>DescribeOpts?</code> <em>(optional)</em> — Optional <code>{ includeInternal? }</code> — default prefers the published entry.</li> </ul> <pre><code class="language-lua">local doc = lsp.describe("renderer.texture.create") </code></pre> <h2 id="modules-lsp-describepaths">modules/lsp/describePaths</h2> <pre><code class="language-lua">describePaths(path: string): { string } </code></pre> <p>List the registered doc paths related to <code>path</code>. A path that names an entry returns every root it is registered under (the first is what <code>lsp.describe</code> resolves to); a path that names a namespace returns the entries registered under it. Empty when the registry holds nothing near the path — so a lookup that returns nil can always be turned into the list of what does exist.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Doc path in any spelling (<code>"renderer.texture"</code>, <code>"ecs/query"</code>).</li> </ul> <pre><code class="language-lua">for _, p in ipairs(lsp.describePaths("renderer.texture")) do print(p) end </code></pre> <h2 id="modules-lsp-describetool">modules/lsp/describeTool</h2> <pre><code class="language-lua">describeTool(path: string): string? </code></pre> <p>Return the full documentation text for a code-mode tool.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Tool path (e.g. <code>"scene/spawnLight"</code>).</li> </ul> <h2 id="modules-lsp-dirtystatus">modules/lsp/dirtyStatus</h2> <pre><code class="language-lua">dirtyStatus(): DirtyStatus </code></pre> <p>How many files wait to be re-validated, whether a drain job holds the set, and how many drains have run in a caller's frame and on a job.</p> <h2 id="modules-lsp-docsbykind">modules/lsp/docsByKind</h2> <pre><code class="language-lua">docsByKind(kind: string): { MethodSummary } </code></pre> <p>List every doc whose registration kind matches <code>kind</code>. Valid: <code>"binding"</code>, <code>"runtime_tool"</code>, <code>"module"</code>, <code>"component"</code>, <code>"library"</code>, <code>"lua_export"</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string</code> — Registration kind.</li> </ul> <h2 id="modules-lsp-draindirty">modules/lsp/drainDirty</h2> <pre><code class="language-lua">drainDirty(): boolean </code></pre> <p>Start re-validating every file marked dirty on a job off the frame, and return whether a job started. The answers land in the cache the play gate reads; <code>lsp.dirtyStatus()</code> reports <code>pending == 0</code> and <code>running == false</code> once none are left.</p> <h2 id="modules-lsp-exportdefinitions">modules/lsp/exportDefinitions</h2> <pre><code class="language-lua">exportDefinitions(dir: string): ExportDefinitionsResult </code></pre> <p>Write the engine's API out for an external editor (VS Code, Neovim, or any editor running luau-lsp), so it types the engine's globals: <code><dir>/zero.d.luau</code> (the definitions <code>lsp.definitions</code> returns), <code><dir>/zero.docs.json</code> (the documentation file naming each global's and member's signature and description) and <code><dir>/.luaurc</code> (nonstrict mode). Point luau-lsp at the first two (its <code>types.definitionFiles</code> and <code>types.documentationFiles</code> settings) and it types the engine's globals: <code>entity.spawn("x")</code> checks against its signature and <code>entity.spwan("x")</code> is an unknown member. The files hold the globals the calling script's globals hold; call it from the tier whose globals the editor should know. The files land in the VFS: <code>userfile.save(path)</code> (the Files panel's "Save to this device") puts each one on the machine of the person at the screen, and over HTTP the engine's <code>GET /engine/vfs/<path></code> reads it. The engine's own checker reads a <code>*.d.luau</code> file the way upstream does, by loading it, so the files can stay in a world's content. Writing again replaces them. <code>globals</code>, <code>types</code> and <code>unrepresented</code> are the lists <code>lsp.definitions</code> returns; the guide <code>topics/external-editor</code> walks through the editor setup.</p> <p><strong>Parameters</strong></p> <ul> <li><code>dir</code> <code>string</code> — VFS directory to write into, such as <code>/zero/source/editor</code>.</li> </ul> <pre><code class="language-lua">local r = lsp.exportDefinitions("/zero/source/editor") print(r.files[1], r.bytes, #r.globals) </code></pre> <h2 id="modules-lsp-getstrictmode">modules/lsp/getStrictMode</h2> <pre><code class="language-lua">getStrictMode(): StrictMode </code></pre> <p>Return the current strict mode.</p> <h2 id="modules-lsp-isstrict">modules/lsp/isStrict</h2> <pre><code class="language-lua">isStrict(): boolean </code></pre> <p>Is the pre-execute LSP gate fully strict? False when off or in soft mode.</p> <h2 id="modules-lsp-lastcheckgen">modules/lsp/lastCheckGen</h2> <pre><code class="language-lua">lastCheckGen(): number </code></pre> <p>Generation counter — bumped each time the cache is rebuilt. UI polls this to know when to redraw.</p> <h2 id="modules-lsp-methods">modules/lsp/methods</h2> <pre><code class="language-lua">methods(namespace: string, opts: MethodsOpts?): { MethodSummary } | { string } </code></pre> <p>List every documented method / entry under a namespace. A broad namespace (<code>ui</code>, <code>renderer</code>) returns a large dump by default, so two options narrow it: <code>opts.filter</code> keeps only methods whose name (or doc path) contains the substring, case-insensitively; <code>opts.namesOnly</code> returns a plain list of method-name strings instead of the full per-method summary tables, which is much smaller and leaves nothing to unwrap. A name, in either form (a summary's <code>name</code> field or a <code>namesOnly</code> string), is the member's path below the namespace, dot-joined (<code>texture.create</code> under <code>renderer</code>, <code>proxy.component.get</code> under <code>entity</code>), so each one names a single member and <code>lsp.describe(namespace .. "." .. name)</code> answers for it. The listing answers with the surface content calls: an entry registered internally is left out where its signature spells the <code>__</code> binding or a Luau module publishes the same member, and <code>opts.includeInternal</code> lists every registered entry instead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>namespace</code> <code>string</code> — Namespace name (e.g. <code>"entity"</code>, <code>"modules/Transform"</code>).</li> <li><code>opts</code> <code>MethodsOpts?</code> <em>(optional)</em> — Optional <code>{ filter?, namesOnly?, includeInternal? }</code>.</li> </ul> <pre><code class="language-lua">for _, name in ipairs(lsp.methods("ui", { namesOnly = true })) do print(name) end lsp.methods("renderer", { filter = "shadow" }) </code></pre> <h2 id="modules-lsp-modules">modules/lsp/modules</h2> <pre><code class="language-lua">modules(): { ModuleEntry } </code></pre> <p>List every Luau library module the engine currently knows about — discovered via <code>--!module</code> headers, library scans, and manually-recorded docs.</p> <h2 id="modules-lsp-namespaces">modules/lsp/namespaces</h2> <pre><code class="language-lua">namespaces(opts: NamespacesOpts?): { NamespaceEntry } </code></pre> <p>List the documentation namespaces reachable from Luau. By default only namespaces exposing at least one PUBLIC method are returned, so the list matches what you can actually call — internal FFI plumbing (e.g. <code>pause</code>, <code>native_entity</code>), whose public surface lives elsewhere (<code>engine.paused</code>, the <code>entity</code> proxy, …), is left out. Pass <code>{ includeInternal = true }</code> to list every namespace, internal ones included.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>NamespacesOpts?</code> <em>(optional)</em> — Optional <code>{ includeInternal? }</code> — default lists public only.</li> </ul> <pre><code class="language-lua">for _, ns in ipairs(lsp.namespaces()) do print(ns.name) end </code></pre> <h2 id="modules-lsp-readdirectives">modules/lsp/readDirectives</h2> <pre><code class="language-lua">readDirectives(source: string): DirectiveBlock </code></pre> <p>Parse the leading <code>--!</code> directive block of a Luau source string. Used by UIs that audit which files have skip directives and what they suppress.</p> <p><strong>Parameters</strong></p> <ul> <li><code>source</code> <code>string</code> — Luau source text.</li> </ul> <h2 id="modules-lsp-search">modules/lsp/search</h2> <pre><code class="language-lua">search(query: string, opts: SearchOpts?): { MethodSummary } </code></pre> <p>Case-insensitive substring search across every registered doc's path, signature, and description. Hits answer with the surface content calls: an entry registered internally is left out where its signature spells the <code>__</code> binding or a Luau module publishes the same member, and <code>opts.includeInternal</code> searches every registered entry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>query</code> <code>string</code> — Substring to search for.</li> <li><code>opts</code> <code>SearchOpts?</code> <em>(optional)</em> — <code>{ limit? = 50, includeInternal? }</code>.</li> </ul> <h2 id="modules-lsp-setstrict">modules/lsp/setStrict</h2> <pre><code class="language-lua">setStrict(enabled: boolean): boolean </code></pre> <p>Toggle the pre-execute LSP gate. Returns true when the change was persisted to <code>.world_settings</code>, false when the play-mode write lock blocked the write.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — True = strict, false = off.</li> </ul> <h2 id="modules-lsp-setstrictmode">modules/lsp/setStrictMode</h2> <pre><code class="language-lua">setStrictMode(mode: StrictMode): boolean </code></pre> <p>Set the pre-execute strict gate's mode. Returns true when the change was persisted to <code>.world_settings</code>, false when the play-mode write lock blocked the write.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>StrictMode</code> — <code>"off"</code> | <code>"soft"</code> | <code>"strict"</code>.</li> </ul> <h2 id="modules-lsp-summary">modules/lsp/summary</h2> <pre><code class="language-lua">summary(): Summary </code></pre> <p>Counts only — does not re-run validation.</p> <h2 id="modules-lsp-symbolat">modules/lsp/symbolAt</h2> <pre><code class="language-lua">symbolAt(path: string, line: number, col: number, name: string, chain: string?): SymbolHit? </code></pre> <p>The identifier <code>name</code> where it stands in <code>path</code> at <code>line</code>:<code>col</code> (1-based, the file's own columns), with the static type it resolves to THERE — a local inside a function body, a parameter, a field, a global — and, for a name a statement in the file introduced, <code>declaredAt = {line, col}</code>. The answer a hover shows and a go-to-definition jumps to. <code>nil</code> when nothing at that position is a reference to <code>name</code>: a word in a comment or a string, a keyword.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path of the file.</li> <li><code>line</code> <code>number</code> — 1-based line.</li> <li><code>col</code> <code>number</code> — 1-based column in the file's own text.</li> <li><code>name</code> <code>string</code> — The identifier at that position.</li> <li><code>chain</code> <code>string?</code> <em>(optional)</em> — The access path the identifier ends — <code>entity.component.add</code> for a pointer on <code>add</code> — which is where its API page is filed.</li> </ul> <pre><code class="language-lua">local hit = lsp.symbolAt("/zero/source/main.luau", 12, 9, "add", "entity.component.add") </code></pre> <h2 id="modules-lsp-tools">modules/lsp/tools</h2> <pre><code class="language-lua">tools(): { ToolEntry } </code></pre> <p>List every code-mode tool registered in the VFS under <code>/zero/docs/tools/<category>/<tool></code>.</p> <h2 id="modules-lsp-typeof">modules/lsp/typeOf</h2> <pre><code class="language-lua">typeOf(expr_source: string, context_path: string?): TypeDescriptor </code></pre> <p>Infer the static type of a Luau expression. When <code>context_path</code> is given, the file is loaded and walked so the inference env contains every local + alias in scope at its end. An expression past the 2 MiB analysis bound answers kind <code>unknown</code>, and a context file past it adds nothing to the env.</p> <p><strong>Parameters</strong></p> <ul> <li><code>expr_source</code> <code>string</code> — Luau expression source (no surrounding chunk).</li> <li><code>context_path</code> <code>string?</code> <em>(optional)</em> — VFS path whose scope should be visible.</li> </ul> <pre><code class="language-lua">local td = lsp.typeOf("Transform.lookAt", "/zero/source/main.luau") </code></pre> <h2 id="modules-lsp-upstreamcheck">modules/lsp/upstreamCheck</h2> <pre><code class="language-lua">upstreamCheck(path: string, opts: UpstreamCheckOpts?): UpstreamCheck </code></pre> <p>Type-check a .luau file with upstream Luau's own type checker (its Frontend), and every module the file requires, and return what upstream reports as data. What <code>lsp.check</code>, the <code>execute</code> gate and the play gate report is unchanged by it. Each <code>require</code> literal resolves to the source file <code>lsp.check</code>'s <code>require-unresolved</code> pass holds it to, which is the file the runtime loads wherever the runtime loads one, in whichever form it is written (<code>@builtin::a.b</code>, <code>@ns::</code>, <code>@root::</code>, a bare dotted <code>a.b</code>, <code>.x</code>, <code>..x</code>, <code>./x</code>, <code>../x</code>, <code>~.x</code>, <code>~pkg.x</code>, an absolute path, a folder asset), and <code>requires</code> lists each literal of the file with the file it reached (<code>module</code>) and the file the runtime's <code>require</code> loads for it from this file (<code>runtimeModule</code>), nil where either reaches none. <code>findings</code> carry upstream's error kind or lint rule, the module they are in and 1-based positions in that module as written; they cover the file, and with <code>opts.includeRequired</code> the type errors of every module it reaches come back as well. Upstream reads every module through its analysis view: the engine's <code>typed</code> declarations read as the plain Luau functions they declare, their generic, parameter and return annotations kept, a return type written after <code>-></code> read after <code>:</code>, and every line and column as in the source; <code>view</code> lists where the view of the file differs from it as <code>{ line, col, written, reads }</code>. A module whose findings come back that upstream cannot parse through its view or past the 2 MiB analysis bound is listed in <code>notAnalysable</code> with the reason, and when that is the file itself <code>analysable</code> is false. The checked modules are kept between calls: <code>checkedModules</code> names the ones this call type-checked, empty when nothing the file reaches changed, and a write to a source re-checks that module and the modules depending on it. The phases of a module's check whose work grows with the types they meet stop at <code>opts.timeLimitMs</code>, and <code>modules</code> says of each module the call reached how its check ended ("completed", "timedOut" or "internal") and how long it took, <code>reused</code> marking one an earlier call checked that is unchanged since. The modules held between calls are bounded: once a call returns their sources sum to at most <code>held.limitBytes</code>, the modules the least recent calls reached going first, and <code>held</code> and <code>evicted</code> report it. A failure inside the analysis library comes back as an "internal" finding with <code>completed</code> false, and the internal errors upstream reports for a module come back as one "internal" finding whose <code>count</code> says how many. <code>opts.typeCheck = false</code> reads the file and every module it reaches and resolves their requires without checking types. <code>available</code> is false, with <code>reason</code>, on a build that does not link the analysis library.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path of a .luau file, a module identity, or a <code>@scope::path</code>.</li> <li><code>opts</code> <code>UpstreamCheckOpts?</code> <em>(optional)</em> — <code>{ solver?, mode?, timeLimitMs?, includeRequired?, typeCheck? }</code>.</li> </ul> <pre><code class="language-lua">local r = lsp.upstreamCheck("@builtin::modules.boids", { mode = "strict" }) </code></pre> <h2 id="modules-luau-coverage-readme">modules/luau_coverage/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.luau_coverage") -- luau_coverage (also available as global 'luau_coverage') </code></pre> <p>Luau source-based coverage dump (LCOV + sources manifest). Public Luau surface over the <code>__luau_coverage</code> Internal FFI namespace.</p> <p>Usage: local luau_coverage = require("@builtin::modules.api.engine.luau_coverage") Also available as global: luau_coverage</p> <h2 id="modules-luau-coverage-lcov">modules/luau_coverage/lcov</h2> <pre><code class="language-lua">lcov(): string </code></pre> <p>Walk every Luau chunk loaded since the VM started (or since the last <code>reset</code>), collect per-line hit counts via Luau's built-in coverage API, and render the aggregate as LCOV text. The report comes back as a string — pair it with <code>vfs.write</code> to put it in the engine's own filesystem. Writing it to the machine's filesystem is the trusted VM's call, which the proxy's <code>luau_coverage_dump</code> tool asks for through <code>trusted.call("coverage.dump")</code> — a call that names no path and supplies no bytes (#13107).</p> <pre><code class="language-lua">vfs.write("/source/tmp/luau.lcov", luau_coverage.lcov()) </code></pre> <h2 id="modules-luau-coverage-level">modules/luau_coverage/level</h2> <pre><code class="language-lua">level(): number </code></pre> <p>The <code>coverageLevel</code> the Luau compiler was configured with (1 = statement coverage, 2 = statement + expression). Returns 0 when coverage is off.</p> <pre><code class="language-lua">if luau_coverage.level() > 0 then ... end </code></pre> <h2 id="modules-luau-coverage-reset">modules/luau_coverage/reset</h2> <pre><code class="language-lua">reset(): boolean </code></pre> <p>Drop every tracked chunk ref and clear the coverage accumulator. The VM stops reporting hits for previously loaded scripts — use before exercising a specific scenario to isolate its coverage.</p> <pre><code class="language-lua">luau_coverage.reset() </code></pre> <h2 id="modules-luau-profile-readme">modules/luau_profile/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.luau_profile") -- luau_profile (also available as global 'luau_profile') </code></pre> <p>Sampling profiler + manual regions for Luau scripts. Public Luau surface over the <code>__luau_profile</code> Internal FFI namespace.</p> <p>Usage: local luau_profile = require("@builtin::modules.api.engine.luau_profile") Also available as global: luau_profile</p> <h2 id="modules-luau-profile-begin">modules/luau_profile/begin</h2> <pre><code class="language-lua">begin(name: string): number </code></pre> <p>Open a named manual region. Returns an opaque integer id; pass it back to <code>end_region(id)</code> to close and record elapsed wall-clock under <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Region name; aggregated across opens.</li> </ul> <pre><code class="language-lua">local id = luau_profile.begin("walk"); ...; luau_profile.end_region(id) </code></pre> <h2 id="modules-luau-profile-dump">modules/luau_profile/dump</h2> <pre><code class="language-lua">dump(path: string): DumpResult </code></pre> <p>TRUSTED ONLY. Write the folded-stack dump to <code>path</code> on the HOST filesystem, one line per stack as <code><ticks> <stack_csv></code> — the format upstream Luau emits and <code>tools/perfgraph.py</code> consumes unchanged. A path naming the engine filesystem (<code>/zero/...</code>, or a bare root such as <code>/source/...</code>) is refused, and says so: <code>luau_profile.folded()</code> with <code>vfs.write</code> puts the dump there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Absolute host filesystem path to write.</li> </ul> <pre><code class="language-lua">local r = luau_profile.dump("/tmp/profile.folded") </code></pre> <h2 id="modules-luau-profile-dump-regions">modules/luau_profile/dump_regions</h2> <pre><code class="language-lua">dump_regions(path: string): DumpRegionsResult </code></pre> <p>TRUSTED ONLY. Write per-region stats to <code>path</code> as JSON, on the HOST filesystem. A path naming the engine filesystem is refused, the same way <code>dump</code> refuses one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Absolute host filesystem path to write.</li> </ul> <pre><code class="language-lua">luau_profile.dump_regions("/tmp/regions.json") </code></pre> <h2 id="modules-luau-profile-end-region">modules/luau_profile/end_region</h2> <pre><code class="language-lua">end_region(id: number) </code></pre> <p>Close a region previously opened by <code>begin(name)</code>. Records elapsed wall-clock under the region's name. Silently no-ops on unknown id (typically a double-close or swapped-out VM).</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>number</code> — Region id returned by <code>begin()</code>.</li> </ul> <pre><code class="language-lua">luau_profile.end_region(id) </code></pre> <h2 id="modules-luau-profile-folded">modules/luau_profile/folded</h2> <pre><code class="language-lua">folded(): string </code></pre> <p>The folded-stack dump as a string, one line per stack as <code><ticks> <stack_csv></code> — the format upstream Luau emits and <code>tools/perfgraph.py</code> consumes unchanged. The same bytes <code>dump</code> writes, handed back instead of written, so the profile can go wherever the caller keeps it: <code>vfs.write</code> puts it in the engine filesystem, where <code>bash</code> and <code>vfs.read</code> reach it.</p> <pre><code class="language-lua">vfs.write("/source/tmp/sample.folded", luau_profile.folded()) </code></pre> <h2 id="modules-luau-profile-is-running">modules/luau_profile/is_running</h2> <pre><code class="language-lua">is_running(): boolean </code></pre> <p>True iff the background sampler is currently running.</p> <pre><code class="language-lua">if luau_profile.is_running() then luau_profile.stop() end </code></pre> <h2 id="modules-luau-profile-reset">modules/luau_profile/reset</h2> <pre><code class="language-lua">reset() </code></pre> <p>Clear every accumulated sample and region stat. The sampler keeps running if it was already on; only the data is wiped.</p> <pre><code class="language-lua">luau_profile.reset() </code></pre> <h2 id="modules-luau-profile-sampling-available">modules/luau_profile/sampling_available</h2> <pre><code class="language-lua">sampling_available(): boolean </code></pre> <p>True on platforms where the background sampler can run (native targets), false on WASM. Manual regions work everywhere — only the sampler is platform-gated.</p> <pre><code class="language-lua">if luau_profile.sampling_available() then luau_profile.start() end </code></pre> <h2 id="modules-luau-profile-snapshot">modules/luau_profile/snapshot</h2> <pre><code class="language-lua">snapshot(top_n: number?): Snapshot </code></pre> <p>Snapshot the current accumulator without touching the filesystem — cheap enough for per-frame UI polling. <code>top_n</code> truncates <code>stacks</code> to the N hottest entries; omitting it returns all stacks sorted descending by <code>self_us</code>. <code>regions</code> is always returned in full (sorted by <code>total_us</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>top_n</code> <code>number?</code> <em>(optional)</em> — Truncate stacks to this many entries; omit for all.</li> </ul> <pre><code class="language-lua">local snap = luau_profile.snapshot(10) </code></pre> <h2 id="modules-luau-profile-span-t">modules/luau_profile/span<T...></h2> <pre><code class="language-lua">span<T...>(name: string, fn: () -> T..., ...): T... </code></pre> <p>Call <code>fn(...)</code> inside a manual region named <code>name</code>. The region is closed even if <code>fn</code> raises (the call goes through pcall internally). Returns whatever <code>fn</code> returned.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Region name.</li> <li><code>fn</code> <code>() -> T...</code> — Function to invoke with the trailing varargs.</li> </ul> <pre><code class="language-lua">local r = luau_profile.span("walk", function() return #entity.findAll() end) </code></pre> <h2 id="modules-luau-profile-start">modules/luau_profile/start</h2> <pre><code class="language-lua">start(hz: number?): StartResult </code></pre> <p>Start the background Luau sampling profiler at <code>hz</code> samples per second (default 1000, clamped to <code>[1, 100000]</code>). Idempotent — calling while already running is a no-op. Returns <code>{ available, hz }</code> — <code>available = false</code> on WASM (no std::thread). Manual regions work regardless.</p> <p><strong>Parameters</strong></p> <ul> <li><code>hz</code> <code>number?</code> <em>(optional)</em> — Sampling rate in Hz.</li> </ul> <pre><code class="language-lua">luau_profile.start(500) </code></pre> <h2 id="modules-luau-profile-stop">modules/luau_profile/stop</h2> <pre><code class="language-lua">stop() </code></pre> <p>Stop the background sampler. Blocks until the sampler thread joins (typically <1ms). Safe when not running. Does not clear the accumulator — call <code>reset()</code> to drop samples.</p> <pre><code class="language-lua">luau_profile.stop() </code></pre> <h2 id="modules-lut-readme">modules/lut/README</h2> <pre><code class="language-lua">require("@builtin::modules.lut") -- lut </code></pre> <p>Colour lookup tables — the table a <code>lut</code> post-process effect reads. Builds one from a grading description, imports one a colourist exported as a <code>.cube</code> file or a strip image, samples it the way the shader does, and installs it as a <code>.texture</code> asset the effect can bind.</p> <p>A LUT is an N-entry colour cube: the answer to "what does this colour become" stored for N^3 input colours, with everything in between read by trilinear interpolation. A grade of any complexity therefore costs the same eight texel reads, which is why colourists deliver a look as a table rather than as a chain of operations. The engine stores a cube as a STRIP: N square tiles in one row, so the image measures <code>N*N</code> wide by <code>N</code> tall. Tile <code>b</code> holds the plane of constant blue index, red runs across a tile and green runs down it. The <code>lut</code> post shader reads N from the image's own height, so one shader serves a 16-, 32- or 64-entry cube with nothing to configure. Apply one through the post chain: local lut = require("@builtin::modules.lut") local ref = lut.install("warm_evening", lut.fromGrade({ exposure = 0.3, temperature = 0.25, saturation = 1.1, })) tools.use("pp", "add", "lut", "lut", { texture = ref.guid, amount = 1.0 }) A <code>lut</code> effect with no table bound passes the frame through untouched, so adding the effect before its table exists changes nothing.</p> <p>Usage: local lut = require("@builtin::modules.lut")</p> <h2 id="modules-lut-build">modules/lut/build</h2> <pre><code class="language-lua">build(size: number, fn: (number, number, number) -> (number, number, number)): Lut </code></pre> <p>Build a table by asking <code>fn</code> what each of its N^3 lattice colours becomes. The input is the lattice colour in 0..1, and so is the answer, which is clamped to that range and stored as it stands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>size</code> <code>number</code> — Cube edge — how many entries each axis holds. 2..90.</li> <li><code>fn</code> <code>(number, number, number) -> (number, number, number)</code> — <code>(r, g, b) -> r, g, b</code>, called once per lattice entry.</li> </ul> <pre><code class="language-lua">local invert = lut.build(16, function(r, g, b) return 1 - r, 1 - g, 1 - b end) </code></pre> <h2 id="modules-lut-fromcube">modules/lut/fromCube</h2> <pre><code class="language-lua">fromCube(text: string): Lut </code></pre> <p>Read a table out of <code>.cube</code> text — the interchange format DaVinci Resolve, Photoshop and every other grading tool writes. <code>LUT_3D_SIZE</code> gives the cube edge and the rows that follow are its entries with red varying fastest, which is the order this reader expects. The table is addressed over 0..1, so a file declaring any other <code>DOMAIN_MIN</code> / <code>DOMAIN_MAX</code> is refused by name rather than read as if it were.</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — The file's contents.</li> </ul> <pre><code class="language-lua">local look = lut.fromCube(vfs.read("/source/looks/kodak2383.cube")) </code></pre> <h2 id="modules-lut-fromgrade">modules/lut/fromGrade</h2> <pre><code class="language-lua">fromGrade(spec: GradeSpec?, size: number?): Lut </code></pre> <p>Bake a grade into a table. The same look as calling <code>lut.grade</code>'s function per pixel, at the cost of eight texel reads however involved the grade is.</p> <p><strong>Parameters</strong></p> <ul> <li><code>spec</code> <code>GradeSpec?</code> <em>(optional)</em> — What the grade does — see <code>GradeSpec</code>.</li> <li><code>size</code> <code>number?</code> <em>(optional)</em> — Cube edge. Defaults to 16.</li> </ul> <pre><code class="language-lua">local look = lut.fromGrade({ contrast = 1.2, saturation = 0.85, lift = { 0.02, 0.02, 0.05 } }) </code></pre> <h2 id="modules-lut-fromimage">modules/lut/fromImage</h2> <pre><code class="language-lua">fromImage(bytes: buffer | string): Lut </code></pre> <p>Read a table out of a strip image laid out as <code>N*N</code> by <code>N</code> — what a grading tool exports as a "2D LUT". Takes either source image bytes (PNG, JPEG, WebP) or the <code>.texture</code> blob <code>lut.install</code> wrote, so a table can be read back out of the asset it was installed as.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — The encoded image, or a <code>.texture</code> blob.</li> </ul> <pre><code class="language-lua">local look = lut.fromImage(assert(vfs.read("/source/looks/teal_orange.png"))) </code></pre> <h2 id="modules-lut-grade">modules/lut/grade</h2> <pre><code class="language-lua">grade(spec: GradeSpec?): (number, number, number) -> (number, number, number) </code></pre> <p>The colour transform a <code>GradeSpec</code> describes, as a plain function. Useful on its own — to grade a single colour, to compose two looks, or to hand to <code>lut.build</code> — and it is what <code>lut.fromGrade</code> bakes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>spec</code> <code>GradeSpec?</code> <em>(optional)</em> — What the grade does. Every field is optional.</li> </ul> <p><strong>Returns</strong> <code>(number, number, number)</code> — <code>(r, g, b) -> r, g, b</code> over 0..1.</p> <pre><code class="language-lua">local warm = lut.grade({ temperature = 0.3 }); local r, g, b = warm(0.5, 0.5, 0.5) </code></pre> <h2 id="modules-lut-identity">modules/lut/identity</h2> <pre><code class="language-lua">identity(size: number?): Lut </code></pre> <p>The table that changes nothing — every entry answers with the colour that indexed it. The starting point for a hand-authored look, and the control an A/B measures against.</p> <p><strong>Parameters</strong></p> <ul> <li><code>size</code> <code>number?</code> <em>(optional)</em> — Cube edge. Defaults to 16.</li> </ul> <pre><code class="language-lua">local base = lut.identity(32) </code></pre> <h2 id="modules-lut-install">modules/lut/install</h2> <pre><code class="language-lua">install(name: string, table_: Lut, opts: { [string]: any }?): any </code></pre> <p>Write the table as a <code>.texture</code> asset the <code>lut</code> post effect can bind, and upload it to the GPU under that asset's own guid. Stored as a float raster, unfiltered and with no mip chain: a table's entries are addresses, so they reach the shader as they were written, with no transfer function between and no mip average to land on a colour the table holds nowhere. The upload is what makes the returned guid bindable from the next frame, and what puts a re-baked look in front of the camera under the name it already had.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The asset's name.</li> <li><code>table_</code> <code>Lut</code> — The <code>Lut</code> to write.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ dest = "<vfs path>", folder = "<vfs folder>", overwrite = true }</code> — placement, forwarded to <code>asset.create</code>. <code>overwrite</code> defaults to true, so installing under a name that exists rewrites that asset and keeps its guid.</li> </ul> <pre><code class="language-lua">local ref = lut.install("dusk", lut.fromGrade({ temperature = -0.2 })) </code></pre> <h2 id="modules-lut-sample">modules/lut/sample</h2> <pre><code class="language-lua">sample(table_: Lut, r: number, g: number, b: number): (number, number, number) </code></pre> <p>What the table answers for a colour — the same trilinear read the shader performs, so a table can be checked without rendering a frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>table_</code> <code>Lut</code> — The <code>Lut</code> to read.</li> <li><code>r</code> <code>number</code> — Red, 0..1. Values outside are clamped, as they are on the GPU.</li> <li><code>g</code> <code>number</code> — Green, 0..1.</li> <li><code>b</code> <code>number</code> — Blue, 0..1.</li> </ul> <pre><code class="language-lua">local r, g, b = lut.sample(look, 0.5, 0.5, 0.5) </code></pre> <h2 id="modules-lut-strip">modules/lut/strip</h2> <pre><code class="language-lua">strip(table_: Lut): ({ number }, number, number) </code></pre> <p>The table's strip raster — the channel values, its width and its height. What <code>renderer.texture.encode</code> and <code>asset.create("texture", ...)</code> take at <code>rgba32f</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>table_</code> <code>Lut</code> — The <code>Lut</code> to read.</li> </ul> <pre><code class="language-lua">local px, w, h = lut.strip(look); print(w, h) </code></pre> <h2 id="modules-lut-tocube">modules/lut/toCube</h2> <pre><code class="language-lua">toCube(table_: Lut, title: string?): string </code></pre> <p>Write a table out as <code>.cube</code> text, so a look built here can be opened in a grading tool or handed to another pipeline.</p> <p><strong>Parameters</strong></p> <ul> <li><code>table_</code> <code>Lut</code> — The <code>Lut</code> to write.</li> <li><code>title</code> <code>string?</code> <em>(optional)</em> — What the file calls itself. Defaults to "zero".</li> </ul> <pre><code class="language-lua">vfs.write("/source/looks/mine.cube", lut.toCube(look, "mine")) </code></pre> <h2 id="modules-materialgraph-readme">modules/materialGraph/README</h2> <pre><code class="language-lua">require("@builtin::systems.materialGraph.materialGraph") -- materialGraph </code></pre> <p>Usage: local materialGraph = require("@builtin::systems.materialGraph.materialGraph")</p> <h2 id="modules-materialgraph-check">modules/materialGraph/check</h2> <pre><code class="language-lua">check(graph: Graph): (boolean, any) </code></pre> <p>Compile a graph without raising. The same work <code>compile</code> does, with the error handed back rather than thrown — what an editor wants while the author is still typing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>Graph</code> — The graph to check.</li> </ul> <pre><code class="language-lua">local ok, result = materialGraph.check(graph) </code></pre> <h2 id="modules-materialgraph-compile">modules/materialGraph/compile</h2> <pre><code class="language-lua">compile(graph: Graph, name: string?): Compiled </code></pre> <p>Compile a graph into the two files a <code>.shader</code> asset holds. Pure text generation: it touches no asset, no renderer and no GPU, so a graph can be checked, compared and tested with nothing installed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>Graph</code> — The graph — <code>nodes</code>, <code>surface</code>, and optionally <code>name</code> + <code>properties</code>.</li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Overrides <code>graph.name</code> for the shader this compiles to.</li> </ul> <pre><code class="language-lua">local out = materialGraph.compile(graph) print(out.wgsl) </code></pre> <h2 id="modules-materialgraph-install">modules/materialGraph/install</h2> <pre><code class="language-lua">install(graph: Graph, opts: { name: string?, into: any? }?): Installed </code></pre> <p>Compile a graph and write it out as a real <code>.shader</code> asset. The result is an ordinary shader asset — nothing keeps a link back to the graph, so it can be read, hand-edited and shipped like any other. Installing the same graph again rewrites the same asset. The shader itself is compiled at the next frame boundary, the way any shader edit is; <code>asset.ref(name, "shader"):compileStatus()</code> is what reports the outcome.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>Graph</code> — The graph to install.</li> <li><code>opts</code> <code>{ name: string?, into: any? }?</code> <em>(optional)</em> — <code>{ name = <shader name>, into = { path = <folder> } }</code>. <code>into</code> places the asset as it is created; installing again writes wherever it already is, and an <code>into</code> naming somewhere else is refused rather than ignored.</li> </ul> <pre><code class="language-lua">local shader = materialGraph.install(graph, { name = "rusty_metal" }) local status = asset.ref(shader.name, "shader"):compileStatus() </code></pre> <h2 id="modules-materialgraph-material">modules/materialGraph/material</h2> <pre><code class="language-lua">material(graph: Graph, opts: { name: string?, shader: string?, into: any?, values: any? }?): string </code></pre> <p>Install a graph and make a material wearing the shader it compiled to. The material is an ordinary material asset: hand it to a Model's <code>material</code> field, or edit its values afterwards like any other. Calling it again with the same material name is how an author iterates: the shader is rewritten from the graph, the material is brought onto that shader's property list, and <code>values</code> is applied over it — so what the call says is what the material holds when it returns.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>Graph</code> — The graph to install.</li> <li><code>opts</code> <code>{ name: string?, shader: string?, into: any?, values: any? }?</code> <em>(optional)</em> — <code>{ name = <material name>, shader = <shader name>, into = ..., values = { <property> = <value> } }</code>.</li> </ul> <pre><code class="language-lua">local mat = materialGraph.material(graph, { name = "rusty", values = { wear = 0.8 } }) </code></pre> <h2 id="modules-materialgraph-nodetypes">modules/materialGraph/nodeTypes</h2> <pre><code class="language-lua">nodeTypes(): { [string]: { ports: { string }, required: { string }, result: string, doc: string } } </code></pre> <p>Every node operation this compiler knows: the inputs a node of that op is written with, which of them it cannot be written without, the type it produces and a line on what it does. A graph written from this listing compiles.</p> <pre><code class="language-lua">for op, info in pairs(materialGraph.nodeTypes()) do print(op, info.doc) end print(table.concat(materialGraph.nodeTypes().sampleTexture.required, ", ")) </code></pre> <h2 id="modules-materialgraph-surfacechannels">modules/materialGraph/surfaceChannels</h2> <pre><code class="language-lua">surfaceChannels(): { [string]: { field: string, ty: string } } </code></pre> <p>The channels the <code>surface</code> terminal accepts, and the shading-model field each one drives.</p> <pre><code class="language-lua">print(materialGraph.surfaceChannels().roughness.field) </code></pre> <h2 id="modules-mathx-readme">modules/mathx/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.mathx") -- mathx (also available as global 'mathx') </code></pre> <p>Batch math kernels over buffer slices (damp, lerp/slerp, transform). Public Luau surface over the <code>__mathx</code> Internal FFI namespace.</p> <p>Usage: local mathx = require("@builtin::modules.api.engine.mathx") Also available as global: mathx</p> <h2 id="modules-mathx-addscaledvec3">modules/mathx/addScaledVec3</h2> <pre><code class="language-lua">addScaledVec3(dstBuffer: Substrate.TypedBuffer, srcBuffer: Substrate.TypedBuffer, </code></pre> <p><code>dst[i] += src[i] * scale</code> for <code>count</code> vec3 elements. Both buffers must hold at least <code>count * 3</code> floats. Useful for particle integration (position += velocity * dt) and accumulator passes.</p> <pre><code class="language-lua">mathx.addScaledVec3(positions, velocities, n, dt) </code></pre> <h2 id="modules-mathx-dampscalar">modules/mathx/dampScalar</h2> <pre><code class="language-lua">dampScalar(buffer: Substrate.TypedBuffer, offset: number, count: number, </code></pre> <p>Critically-damped exponential approach toward <code>target</code> for <code>count</code> scalars at <code>buffer[offset .. offset+count]</code>. <code>smoothTime</code> is the time constant (~ 0.16 ⇒ ~63% per frame at 60 Hz). Pass <code>smoothTime <= 0</code> to snap to the target.</p> <pre><code class="language-lua">mathx.dampScalar(buf, 0, 16, 0.0, 0.16, dt) </code></pre> <h2 id="modules-mathx-lerpvec3">modules/mathx/lerpVec3</h2> <pre><code class="language-lua">lerpVec3(buffer: Substrate.TypedBuffer, offset: number, count: number, </code></pre> <p>Element-wise linear blend of <code>count</code> vec3s in <code>buffer[offset .. offset+count*3]</code> toward <code>(tx, ty, tz)</code> by <code>t</code>.</p> <pre><code class="language-lua">mathx.lerpVec3(buf, 0, n, 0, 1, 0, 0.5) </code></pre> <h2 id="modules-mathx-normalizequat">modules/mathx/normalizeQuat</h2> <pre><code class="language-lua">normalizeQuat(buffer: Substrate.TypedBuffer, offset: number, count: number): boolean </code></pre> <p>Re-normalise <code>count</code> quaternions in place. Zero-length quats become identity (0, 0, 0, 1) so downstream code never sees NaN.</p> <p><strong>Parameters</strong></p> <ul> <li><code>buffer</code> <code>Substrate.TypedBuffer</code> — The buffer to operate on.</li> <li><code>offset</code> <code>number</code> — Starting f32 index.</li> <li><code>count</code> <code>number</code> — Number of quaternions.</li> </ul> <pre><code class="language-lua">mathx.normalizeQuat(buf, 0, n) </code></pre> <h2 id="modules-mathx-slerpquat">modules/mathx/slerpQuat</h2> <pre><code class="language-lua">slerpQuat(buffer: Substrate.TypedBuffer, offset: number, count: number, </code></pre> <p>Slerp <code>count</code> quaternions (xyzw) at <code>buffer[offset..]</code> toward <code>(tx, ty, tz, tw)</code> by <code>t</code>. Falls back to nlerp+normalize for very-close quats. Always picks the shortest-arc path.</p> <pre><code class="language-lua">mathx.slerpQuat(buf, 0, n, 0, 0, 0, 1, 0.25) </code></pre> <h2 id="modules-mathx-transformvec3">modules/mathx/transformVec3</h2> <pre><code class="language-lua">transformVec3(buffer: Substrate.TypedBuffer, offset: number, </code></pre> <p>Treat each vec3 in <code>buffer[offset..]</code> as a position (w = 1), multiply by the 4x4 column-major matrix <code>mat16</code> (16-element array), write <code>.xyz</code> of the result back. Layout matches glam, wgpu, and GLSL conventions.</p> <pre><code class="language-lua">mathx.transformVec3(positions, 0, n, worldMatrix) </code></pre> <h2 id="modules-mcplog-readme">modules/mcpLog/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.mcpLog") -- mcpLog (also available as global 'mcpLog') </code></pre> <p>Read-only MCP tool-call log ring buffer. Public Luau surface over the <code>__mcpLog</code> Internal FFI namespace.</p> <p>Usage: local mcpLog = require("@builtin::modules.api.engine.mcpLog") Also available as global: mcpLog</p> <h2 id="modules-mcplog-clear">modules/mcpLog/clear</h2> <pre><code class="language-lua">clear(): boolean </code></pre> <p>Clear all entries from the engine's MCP log ring buffer.</p> <pre><code class="language-lua">mcpLog.clear() </code></pre> <h2 id="modules-mcplog-query">modules/mcpLog/query</h2> <pre><code class="language-lua">query(limit: number?): { McpLogEntry } </code></pre> <p>Return the most-recent MCP tool-call entries from the engine's MCP log ring buffer (newest last). Pass <code>limit</code> to cap how many entries are returned — omit for the full ring (up to 500 entries).</p> <p><strong>Parameters</strong></p> <ul> <li><code>limit</code> <code>number?</code> <em>(optional)</em> — Maximum number of entries to return.</li> </ul> <pre><code class="language-lua">for _, e in ipairs(mcpLog.query(50)) do print(e.tool_name, e.status) end </code></pre> <h2 id="modules-mediatransmittance-readme">modules/mediaTransmittance/README</h2> <pre><code class="language-lua">require("@builtin::systems.volumetrics.mediaTransmittance") -- mediaTransmittance </code></pre> <p>Light attenuation through participating media, in a form both media and surfaces can sample. A fog bank dims what is behind it, and overlapping volumes stack.</p> <p>Usage: local mediaTransmittance = require("@builtin::systems.volumetrics.mediaTransmittance")</p> <h2 id="modules-mediatransmittance-active">modules/mediaTransmittance/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the transmittance passes are running this frame.</p> <pre><code class="language-lua">if mediaTransmittance.active() then ... end </code></pre> <h2 id="modules-mediatransmittance-buffers">modules/mediaTransmittance/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The buffer(s) this system's passes read. A pass binds what this hands it, so it has the values this module packed.</p> <pre><code class="language-lua">local b = mediaTransmittance.buffers() </code></pre> <h2 id="modules-mediatransmittance-clear">modules/mediaTransmittance/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Remove every medium and release the passes. The settings are kept.</p> <pre><code class="language-lua">mediaTransmittance.clear() </code></pre> <h2 id="modules-mediatransmittance-get">modules/mediaTransmittance/get</h2> <pre><code class="language-lua">get(): State </code></pre> <p>The transmittance settings currently in force.</p> <pre><code class="language-lua">local s = mediaTransmittance.get().strength </code></pre> <h2 id="modules-mediatransmittance-removemedium">modules/mediaTransmittance/removeMedium</h2> <pre><code class="language-lua">removeMedium(key: string): number </code></pre> <p>Remove a registered medium.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The id it was registered under.</li> </ul> <pre><code class="language-lua">mediaTransmittance.removeMedium("bank") </code></pre> <h2 id="modules-mediatransmittance-set">modules/mediaTransmittance/set</h2> <pre><code class="language-lua">set(opts: TransmittanceOpts?): State </code></pre> <p>Set the scene-wide transmittance settings. Any omitted field keeps its current value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>TransmittanceOpts?</code> <em>(optional)</em> — Settings — see <code>TransmittanceOpts</code>.</li> </ul> <pre><code class="language-lua">mediaTransmittance.set({ sunDirection = { 0.4, 0.8, 0.2 }, steps = 32 }) </code></pre> <h2 id="modules-mediatransmittance-setmedium">modules/mediaTransmittance/setMedium</h2> <pre><code class="language-lua">setMedium(key: string, opts: MediumOpts): number </code></pre> <p>Register (or move) a medium. Pushing the same key again replaces it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — A stable id for this medium.</li> <li><code>opts</code> <code>MediumOpts</code> — Placement and density — see <code>MediumOpts</code>.</li> </ul> <pre><code class="language-lua">mediaTransmittance.setMedium("bank", { position = { 0, 8, 0 }, size = { 20, 6, 20 }, density = 0.12 }) </code></pre> <h2 id="modules-microphone-readme">modules/microphone/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.microphone") -- microphone (also available as global 'microphone') </code></pre> <p>Capture from an input device and read what is arriving: a loudness, a magnitude spectrum, and the raw PCM. Public Luau surface over the <code>__microphone</code> Internal FFI namespace.</p> <p>Usage: local microphone = require("@builtin::modules.api.engine.microphone") Also available as global: microphone</p> <h2 id="modules-microphone-awaitrunning">modules/microphone/awaitRunning</h2> <pre><code class="language-lua">awaitRunning(timeout: number?): (MicState, string?) </code></pre> <p>Wait until the capture settles out of <code>starting</code> and <code>permissionPending</code>, and report where it landed. Returns as soon as the state settles, or when <code>timeout</code> seconds have passed, whichever comes first — a browser permission prompt nobody answers never settles, so the wait is always bounded.</p> <p><strong>Parameters</strong></p> <ul> <li><code>timeout</code> <code>number?</code> <em>(optional)</em> — Seconds to wait at most. Defaults to 10.</li> </ul> <pre><code class="language-lua">microphone.start(); local state, why = microphone.awaitRunning() </code></pre> <h2 id="modules-microphone-devices">modules/microphone/devices</h2> <pre><code class="language-lua">devices(): { MicDevice } </code></pre> <p>Every input device the platform offers. <code>id</code> is what <code>microphone.start</code> takes to select one and is stable across reboots where the platform provides a stable identifier; <code>name</code> is the label a person recognises.</p> <p>An empty list is a legitimate answer, not a failure: a machine with no input hardware offers none, and a browser names none until microphone access has been granted at least once — the labels are part of what the permission protects.</p> <pre><code class="language-lua">for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end </code></pre> <h2 id="modules-microphone-frequencies">modules/microphone/frequencies</h2> <pre><code class="language-lua">frequencies(): { number } </code></pre> <p>The frequency each spectrum bin is centred on, in Hz, as an array parallel to <code>microphone.spectrum()</code>. Derived from the capture's rate and transform size, so it changes only when a capture is started with different ones. Empty while no capture is running.</p> <pre><code class="language-lua">local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency </code></pre> <h2 id="modules-microphone-level">modules/microphone/level</h2> <pre><code class="language-lua">level(): number </code></pre> <p>Loudness of the most recent analysis window, as an RMS amplitude in 0..1. A full-scale sine reads about 0.707 and silence reads 0.</p> <p>Measured over only the samples that have arrived, so a capture that has just started reports the loudness of what it holds rather than a level diluted by a window it has not filled yet. 0 while no capture is running.</p> <pre><code class="language-lua">if microphone.level() > 0.05 then print("someone is talking") end </code></pre> <h2 id="modules-microphone-peak">modules/microphone/peak</h2> <pre><code class="language-lua">peak(): { [string]: number }? </code></pre> <p>The bin carrying the most energy and what it says: the frequency it is centred on, its amplitude, and the loudness of the whole window. A capture reading silence answers with amplitude 0 at bin 1.</p> <pre><code class="language-lua">local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end </code></pre> <h2 id="modules-microphone-samples">modules/microphone/samples</h2> <pre><code class="language-lua">samples(max: number?): buffer? </code></pre> <p>Captured mono PCM no caller has taken yet, oldest sample first, as a buffer of little-endian f32 read with <code>buffer.readf32</code>. The samples are removed, so successive calls walk forward through the capture and a caller doing its own analysis sees every frame once.</p> <p>nil while no capture is running, and a zero-length buffer when the capture is running and nothing new has arrived. Samples nobody takes are discarded once the queue fills, and <code>status().overruns</code> counts every one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>max</code> <code>number?</code> <em>(optional)</em> — How many samples to take at most. Omitted, everything held comes back.</li> </ul> <pre><code class="language-lua">local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end </code></pre> <h2 id="modules-microphone-spectrum">modules/microphone/spectrum</h2> <pre><code class="language-lua">spectrum(): { number } </code></pre> <p>Amplitude per frequency bin over the most recent analysis window: <code>fftSize / 2 + 1</code> numbers, DC at index 1 through the Nyquist frequency at the last. Bin <code>i</code> covers <code>(i - 1) * status().binHz</code> Hz.</p> <p>Each value is an amplitude estimate rather than a raw transform magnitude, so a full-scale tone sitting on a bin centre reads about 1.0 and the numbers stay comparable across transform sizes.</p> <p>The window is multiplied by a <strong>Hann</strong> taper before the transform. An untapered window ends abruptly at both edges and the transform reads that as energy spread across every bin, smearing one tone into a skirt that buries quieter tones beside it. Hann trades a slightly wider main lobe — a tone occupies about three bins rather than one — for sidelobes that fall away steeply, which is what lets neighbouring tones be told apart. Read a peak as "a tone near here", not "a tone exactly here".</p> <p>Reading this takes no samples away from <code>microphone.samples()</code>. Empty while no capture is running.</p> <pre><code class="language-lua">local bins = microphone.spectrum(); print(#bins, bins[1]) </code></pre> <h2 id="modules-microphone-start">modules/microphone/start</h2> <pre><code class="language-lua">start(opts: MicOpts?): (MicState?, string?) </code></pre> <p>Open an input device and begin capturing. Returns the state the capture reached — <code>"running"</code> once a device is delivering, or <code>"permissionPending"</code> where the platform must ask for access first, which is the browser's normal path. Poll <code>microphone.status()</code> from there, or use <code>microphone.awaitRunning()</code>. Where access is refused without asking (a native engine's fake microphone presented as <code>deny</code>), it returns <code>"denied"</code>.</p> <p>A request that cannot be made at all returns nil and the reason: an <code>fftSize</code> that is not a whole power of two between 64 and 16384, a device no machine here offers, a rate the device does not capture at, or a capture that is already running.</p> <p>Omitting <code>device</code> opens the platform default. Omitting <code>sampleRate</code> takes the device's own rate, which is what avoids a resample. <code>fftSize</code> is how many samples one analysis window covers and defaults to 1024 — at 48 kHz that spans ~21 ms and resolves ~47 Hz per bin.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>MicOpts?</code> <em>(optional)</em> — <code>{ device, sampleRate, fftSize }</code>.</li> </ul> <pre><code class="language-lua">local state, why = microphone.start({ fftSize = 2048 }) </code></pre> <h2 id="modules-microphone-status">modules/microphone/status</h2> <pre><code class="language-lua">status(): MicStatus </code></pre> <p>Where the capture stands.</p> <p><code>reason</code> carries the platform's own message: the refusal for <code>denied</code>, the device's message for <code>failed</code>, what is being waited on for <code>permissionPending</code>. <code>binHz</code> is the width of one spectrum bin and <code>bins</code> how many <code>microphone.spectrum()</code> returns.</p> <p><code>framesCaptured</code> counts every mono frame the device delivered whether or not anything drained it, so a silent room reads differently from a stalled device. <code>overruns</code> counts samples discarded because a consumer did not keep up — it standing still is what says the readings are continuous, and it climbing is why a caller sees gaps.</p> <pre><code class="language-lua">local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns) </code></pre> <h2 id="modules-microphone-stop">modules/microphone/stop</h2> <pre><code class="language-lua">stop(): boolean </code></pre> <p>Stop the capture and release the device. True when a capture was open or being opened at call time. The device is let go before this returns, so a stop followed by a start opens it again rather than finding it held.</p> <pre><code class="language-lua">microphone.stop() </code></pre> <h2 id="modules-modelimport-readme">modules/modelImport/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.modelImport") -- modelImport (also available as global 'modelImport') </code></pre> <p>The model-import pipeline: decompose any assimp-supported 3D model (fbx, obj, dae, gltf, glb, stl, ply, 3ds, …) into meshes, materials, textures, animation clips and a node graph; extract a single clip to its <code>.zanim</code> payload; and derive a <code>.rig</code> from a skinned mesh or from an animation-only file's driven skeleton. Public Luau surface over the <code>__model</code> and <code>__rig</code> Internal FFI namespaces.</p> <p>Usage: local modelImport = require("@builtin::modules.api.engine.modelImport") Also available as global: modelImport</p> <h2 id="modules-modelimport-decompose">modules/modelImport/decompose</h2> <pre><code class="language-lua">decompose(bytes: buffer | string, format: string): PromiseHandle </code></pre> <p>Parse raw model bytes on a background thread. <code>format</code> is the real source extension (<code>"fbx"</code>, <code>"obj"</code>, <code>"dae"</code>, <code>"gltf"</code>, <code>"glb"</code>, <code>"stl"</code>, <code>"ply"</code>, <code>"3ds"</code>, …), forwarded to assimp as the format hint. Returns a promise handle: <code>task.await</code> it, then read the data with <code>result(handle)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — Raw model file bytes (from <code>vfs.readAsync</code>).</li> <li><code>format</code> <code>string</code> — The source file extension (lowercase, no dot).</li> </ul> <pre><code class="language-lua">local h = modelImport.decompose(bytes, "obj"); task.await(h) </code></pre> <h2 id="modules-modelimport-decomposefiles">modules/modelImport/decomposeFiles</h2> <pre><code class="language-lua">decomposeFiles(files: { ModelFile }, mainName: string): PromiseHandle </code></pre> <p>Parse a model plus its companion files on a background thread, so assimp resolves the model's external references (a <code>.gltf</code>'s external <code>.bin</code> and image files, an <code>.obj</code>'s <code>.mtl</code> colors/textures, MD5's <code>.md5anim</code>, …). <code>files</code> is an array of <code>{ name = basename, bytes = <bytes> }</code> that MUST include the model file itself; <code>mainName</code> is that file's basename. Returns a promise handle: <code>task.await</code> it, then read the data with <code>result(handle)</code> — the same shape <code>decompose</code> produces.</p> <p><strong>Parameters</strong></p> <ul> <li><code>files</code> <code>{ ModelFile }</code> — Array of <code>{ name, bytes }</code>: the model file plus its companions.</li> <li><code>mainName</code> <code>string</code> — Basename of the model file to import (one of <code>files</code>' names).</li> </ul> <pre><code class="language-lua">local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h) </code></pre> <h2 id="modules-modelimport-extractanimation">modules/modelImport/extractAnimation</h2> <pre><code class="language-lua">extractAnimation(sourcePath: string, clipName: string): PromiseHandle </code></pre> <p>Read a model source file and extract one animation clip to its <code>.zanim</code> payload, stashed for retrieval. The source extension decides the parser, so this is format-agnostic. Returns a promise handle: <code>task.await</code> it, then <code>extractAnimationResult(handle)</code> returns the bytes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sourcePath</code> <code>string</code> — VFS path to the source model file.</li> <li><code>clipName</code> <code>string</code> — Clip name as returned by <code>result(handle).animations[i].name</code>.</li> </ul> <h2 id="modules-modelimport-extractanimationresult">modules/modelImport/extractAnimationResult</h2> <pre><code class="language-lua">extractAnimationResult(handle: PromiseHandle | string): string? </code></pre> <p>After awaiting an <code>extractAnimation</code> handle, return the extracted <code>.zanim</code> bytes (binary-safe), consuming them. The bytes to hand to <code>asset.create("animation", name, { bytes })</code>. Returns nil on failure or if already taken.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>PromiseHandle | string</code> — Promise handle from <code>extractAnimation</code>.</li> </ul> <h2 id="modules-modelimport-ishandle">modules/modelImport/isHandle</h2> <pre><code class="language-lua">isHandle(handle: any): boolean </code></pre> <p>Call <code>makeHandle</code>, which returns a promise handle (or its id string), or a falsy value on a transient failure (e.g. a source read that raced a pending write during a parallel import), up to <code>retries + 1</code> times, yielding via <code>yield</code> between attempts so a pending write can land before the next try. Returns the handle once one is produced, or nil when every attempt failed. Callers <code>task.await</code> the result only when it is non-nil, so a transient miss never reaches <code>task.await</code> as something other than a handle.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-modelimport-result">modules/modelImport/result</h2> <pre><code class="language-lua">result(handle: PromiseHandle | string): any? </code></pre> <p>Read the decomposed model after <code>decompose</code>'s handle has been awaited. Every format decomposes into the same shape, so this is format-agnostic. Runs on the main thread; consumes the stored result.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>PromiseHandle | string</code> — Promise handle from <code>decompose</code>.</li> </ul> <h2 id="modules-modelimport-retryhandle">modules/modelImport/retryHandle</h2> <pre><code class="language-lua">retryHandle(makeHandle: () -> any, retries: number?, yield: (() -> ())?): (PromiseHandle | string)? </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>makeHandle</code> <code>() -> any</code></li> <li><code>retries</code> <code>number?</code> <em>(optional)</em></li> <li><code>yield</code> <code>(() -> ())?</code> <em>(optional)</em></li> </ul> <h2 id="modules-modelimport-rigfrommeshskin">modules/modelImport/rigFromMeshSkin</h2> <pre><code class="language-lua">rigFromMeshSkin(meshBytes: buffer | string): string? </code></pre> <p>Lift the skeleton out of a skinned <code>.mesh</code> (ZMSH) payload and return it as a <code>.rig</code> JSON document: bones (hierarchy, rest pose, inverse-bind). The source rig a skinned mesh's clips retarget through. Returns nil when the bytes are not a mesh or carry no skin.</p> <p><strong>Parameters</strong></p> <ul> <li><code>meshBytes</code> <code>buffer | string</code> — Raw ZMSH mesh bytes carrying a skin.</li> </ul> <h2 id="modules-modelimport-rigfromskeleton">modules/modelImport/rigFromSkeleton</h2> <pre><code class="language-lua">rigFromSkeleton(skeleton: AnimationSkeleton): string? </code></pre> <p>Build a <code>.rig</code> JSON document from the skeleton an animation-only file was authored on — <code>result(handle).skeleton</code>, the bones its clips drive with their local rest transforms. Forward kinematics over the locals resolves globals + inverse-bind. The source rig a standalone clip retargets through. Returns nil on malformed input.</p> <p><strong>Parameters</strong></p> <ul> <li><code>skeleton</code> <code>AnimationSkeleton</code> — <code>{ names, parents, locals }</code> (a <code>decompose</code> result's <code>skeleton</code>).</li> </ul> <pre><code class="language-lua">local rigJson = modelImport.rigFromSkeleton(data.skeleton) </code></pre> <h2 id="modules-motionblur-readme">modules/motionBlur/README</h2> <pre><code class="language-lua">require("@builtin::systems.motionBlur.motionBlur") -- motionBlur </code></pre> <p>Camera-shutter motion blur. Blur length comes from a shutter angle, and the reconstruction lets a moving object smear past its own silhouette instead of stopping dead at its edge.</p> <p>Usage: local motionBlur = require("@builtin::systems.motionBlur.motionBlur")</p> <h2 id="modules-motionblur-active">modules/motionBlur/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the motion-blur passes are running this frame.</p> <pre><code class="language-lua">if motionBlur.active() then ... end </code></pre> <h2 id="modules-motionblur-buffers">modules/motionBlur/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The parameter buffer the motion-blur passes read, carrying the settings this module packs. The render feature binds what this hands it.</p> <pre><code class="language-lua">local b = motionBlur.buffers() </code></pre> <h2 id="modules-motionblur-clear">modules/motionBlur/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Close the shutter and release the passes. The other settings are kept, so a later <code>set({ shutterAngle = ... })</code> brings back the same look.</p> <pre><code class="language-lua">motionBlur.clear() </code></pre> <h2 id="modules-motionblur-get">modules/motionBlur/get</h2> <pre><code class="language-lua">get(): MotionBlurState </code></pre> <p>The shutter settings currently in force.</p> <pre><code class="language-lua">local a = motionBlur.get().shutterAngle </code></pre> <h2 id="modules-motionblur-set">modules/motionBlur/set</h2> <pre><code class="language-lua">set(opts: MotionBlurOpts?): MotionBlurState </code></pre> <p>Set the camera's shutter. Any omitted field keeps its current value. A <code>shutterAngle</code> of 0 closes the shutter and releases the passes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>MotionBlurOpts?</code> <em>(optional)</em> — Shutter settings — see <code>MotionBlurOpts</code>.</li> </ul> <pre><code class="language-lua">motionBlur.set({ shutterAngle = 180, samples = 16 }) </code></pre> <h2 id="modules-multiplayer-readme">modules/multiplayer/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.multiplayer") -- multiplayer (also available as global 'multiplayer') </code></pre> <p>Multiplayer sync state and operations — connection, peers, ownership, rooms, undo/redo. Public Luau surface over the <code>__multiplayer</code> Internal FFI namespace.</p> <p>Usage: local multiplayer = require("@builtin::modules.api.engine.multiplayer") Also available as global: multiplayer</p> <h2 id="modules-multiplayer-beginoperation">modules/multiplayer/beginOperation</h2> <pre><code class="language-lua">beginOperation(description: string) </code></pre> <p>Begin recording an undoable operation. All mutations until <code>commitOperation()</code> are grouped into one undo entry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>description</code> <code>string</code> — Human-readable label.</li> </ul> <pre><code class="language-lua">multiplayer.beginOperation("move cube") </code></pre> <h2 id="modules-multiplayer-canredo">modules/multiplayer/canRedo</h2> <pre><code class="language-lua">canRedo(): boolean </code></pre> <p>Check if this client has any redoable operations.</p> <h2 id="modules-multiplayer-canundo">modules/multiplayer/canUndo</h2> <pre><code class="language-lua">canUndo(): boolean </code></pre> <p>Check if this client has any undoable operations.</p> <h2 id="modules-multiplayer-canceloperation">modules/multiplayer/cancelOperation</h2> <pre><code class="language-lua">cancelOperation() </code></pre> <p>Cancel the current operation and restore all properties to their values at begin time.</p> <pre><code class="language-lua">multiplayer.cancelOperation() </code></pre> <h2 id="modules-multiplayer-claimownership">modules/multiplayer/claimOwnership</h2> <pre><code class="language-lua">claimOwnership(entityId: (string | entityRef)?): boolean </code></pre> <p>Request ownership of an entity. The relay grants a claim on an entity no peer owns (never claimed, or released by its owner) and tells every peer; an entity another peer owns stays theirs, and the refusal is counted in <code>getDiagnostics().refusals</code> and named by <code>lastRefusal</code>. The owner hands an entity on by releasing it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)?</code> <em>(optional)</em> — Entity id or proxy to claim.</li> </ul> <h2 id="modules-multiplayer-clearhistory">modules/multiplayer/clearHistory</h2> <pre><code class="language-lua">clearHistory() </code></pre> <p>Drop this client's whole undo/redo history — for boundaries where old edits stop being meaningful (a scene load, a test rig reset).</p> <pre><code class="language-lua">multiplayer.clearHistory() </code></pre> <h2 id="modules-multiplayer-commitoperation">modules/multiplayer/commitOperation</h2> <pre><code class="language-lua">commitOperation() </code></pre> <p>Finalize the current operation and push it onto the undo stack. Only changes that actually differ from the start state are recorded.</p> <pre><code class="language-lua">multiplayer.commitOperation() </code></pre> <h2 id="modules-multiplayer-connect">modules/multiplayer/connect</h2> <pre><code class="language-lua">connect(relayUrl: string) </code></pre> <p>Connect to a multiplayer relay server for the current world. Uses the loaded world's <code>world_id</code> as the room prefix for scene isolation. A world must be loaded before connecting.</p> <p><strong>Parameters</strong></p> <ul> <li><code>relayUrl</code> <code>string</code> — Relay server URL.</li> </ul> <pre><code class="language-lua">multiplayer.connect("https://relay.example.com") </code></pre> <h2 id="modules-multiplayer-disconnect">modules/multiplayer/disconnect</h2> <pre><code class="language-lua">disconnect() </code></pre> <p>Disconnect from the multiplayer relay server.</p> <pre><code class="language-lua">multiplayer.disconnect() </code></pre> <h2 id="modules-multiplayer-explain">modules/multiplayer/explain</h2> <pre><code class="language-lua">explain( </code></pre> <p>Why a synced property is not reaching the peers this client shares its entity's room with. Answers from the engine's own registry, so a name the component never registered is reported as such instead of inferred from a second client's silence.</p> <pre><code class="language-lua">local v = multiplayer.explain(player, "Health", "hp") if not v.arriving then print(v.reason) end </code></pre> <h2 id="modules-multiplayer-getdiagnostics">modules/multiplayer/getDiagnostics</h2> <pre><code class="language-lua">getDiagnostics(): SyncDiagnostics </code></pre> <p>Get sync diagnostics — traffic counts, bandwidth, link quality, peer count. The counts — <code>bytesSent/Received</code>, <code>datagramsSent/Received</code>, <code>rpcsSent/Received</code>, <code>ownershipChanges</code>, <code>refusals</code> — are running totals for the session, so a sparse event stays readable long after it happened; subtract two samples for the rate over the interval between them. <code>refusals</code> counts the messages of this session's the relay refused (a claim on an entity another peer owns, a change to one, a room outside the world the connection was admitted to), and <code>lastRefusal</code> names the latest in the relay's words once there is one. <code>bytesSentPerSec</code> / <code>bytesReceivedPerSec</code> are averages over the last completed ~1 second window. <code>rttMs</code> is the smoothed round-trip time to the relay and <code>packetLoss</code> the fraction (0..1) of packets lost over the last 5 seconds; both read 0 until the transport has sampled a live connection. <code>messagesAwaitingEntity</code> counts the sync messages this peer is holding for an entity it has not received yet — each waits for the spawn that names it, applies the moment it arrives, and is released once its wait runs out. <code>reliableInFlight</code> counts the reliable messages this peer has sent that the relay has not yet taken: it reads near 0 while the link keeps up with what this peer sends, and a count that grows names a link carrying less than that, every reliable change sent behind it (a Sync field, a spawn) waiting with it.</p> <pre><code class="language-lua">local d = multiplayer.getDiagnostics() if d.packetLoss > 0.05 then log.warn(("unstable connection, %d ms round trip"):format(d.rttMs)) end print(d.messagesAwaitingEntity .. " message(s) waiting for their entity") </code></pre> <h2 id="modules-multiplayer-getpeerid">modules/multiplayer/getPeerId</h2> <pre><code class="language-lua">getPeerId(): number? </code></pre> <p>Get the peer ID the relay issued this client's connection.</p> <h2 id="modules-multiplayer-getpeers">modules/multiplayer/getPeers</h2> <pre><code class="language-lua">getPeers(): { PeerInfo } </code></pre> <p>Get a list of all connected peers in the current session.</p> <pre><code class="language-lua">for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end </code></pre> <h2 id="modules-multiplayer-getroompeers">modules/multiplayer/getRoomPeers</h2> <pre><code class="language-lua">getRoomPeers(roomKey: string): { PeerInfo } </code></pre> <p>Get the peers this client shares the given room with, ordered by peer id. <code>getPeers</code> answers for the whole session — the union of every room this client is in — while this answers for one room, so a peer that leaves this room while staying in another disappears from here and remains in <code>getPeers</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>roomKey</code> <code>string</code> — Fully-qualified room key (<code>{worldGuid}/{profile}/{mode}/{sceneGuid}</code>).</li> </ul> <pre><code class="language-lua">for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end </code></pre> <h2 id="modules-multiplayer-getrooms">modules/multiplayer/getRooms</h2> <pre><code class="language-lua">getRooms(): { string } </code></pre> <p>The relay rooms this client has joined, sorted. A broadcast reaches only the peers that share one of these. They are the <code>rooms</code> of <code>observe()</code> as of the last frame, read from the rooms the session publishes every frame, so a component can ask every frame.</p> <pre><code class="language-lua">for _, key in ipairs(multiplayer.getRooms()) do print(key) end </code></pre> <h2 id="modules-multiplayer-gettickrate">modules/multiplayer/getTickRate</h2> <pre><code class="language-lua">getTickRate(): number </code></pre> <p>Get the current sync tick rate (network updates per second).</p> <h2 id="modules-multiplayer-heldmessages">modules/multiplayer/heldMessages</h2> <pre><code class="language-lua">heldMessages(): { HeldMessage } </code></pre> <p>The sync messages this peer is holding for entities it has not received — what <code>getDiagnostics().messagesAwaitingEntity</code> counts, one entry each, with the entity sync id it names, its age and the grace it is held against.</p> <pre><code class="language-lua">for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end </code></pre> <h2 id="modules-multiplayer-isconnected">modules/multiplayer/isConnected</h2> <pre><code class="language-lua">isConnected(): boolean </code></pre> <p>Check if a multiplayer session is connected to a relay and the relay has named this client. True exactly when <code>getPeerId()</code> answers an id.</p> <h2 id="modules-multiplayer-ishost">modules/multiplayer/isHost</h2> <pre><code class="language-lua">isHost(): boolean </code></pre> <p>Whether THIS client is the host (authoritative owner) of the current scene's play room — the relay room CREATOR, or offline / single-player. Host code spawns the shared synced world (via <code>entity.spawnSynced</code> or a scene's <code>onHostLoad</code>) and runs authoritative simulation; a non-host (JOINER) receives that content from the relay snapshot and must NOT re-create it. Gate ANY code that spawns synced entities or owns shared state with this so it runs on exactly one client — running it on every peer is the double-spawn 'explosion'.</p> <pre><code class="language-lua">if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end </code></pre> <h2 id="modules-multiplayer-isoperationopen">modules/multiplayer/isOperationOpen</h2> <pre><code class="language-lua">isOperationOpen(): boolean </code></pre> <p>Whether an operation is open: true between <code>beginOperation</code> and its <code>commitOperation</code> or <code>cancelOperation</code>. A caller that records file writes reads the file's prior contents only while one is open.</p> <pre><code class="language-lua">if multiplayer.isOperationOpen() then before = vfs.read(p) end </code></pre> <h2 id="modules-multiplayer-isowner">modules/multiplayer/isOwner</h2> <pre><code class="language-lua">isOwner(entityId: (string | entityRef)?): boolean </code></pre> <p>Check if the local client owns the given entity (or the current entity if called from a component). Only the owner can modify synced properties directly.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)?</code> <em>(optional)</em> — Entity id or proxy to check (defaults to <code>self.entityId</code> in component context).</li> </ul> <h2 id="modules-multiplayer-isroomcreator">modules/multiplayer/isRoomCreator</h2> <pre><code class="language-lua">isRoomCreator(roomKey: string): boolean? </code></pre> <p>Whether this client created the given room — it was the FIRST peer to join it (race-free; the relay assigns it on join). In play mode the creator instantiates the scene's entities (synced) and every other joiner receives them from the relay snapshot, so the scene is never double-instantiated.</p> <p><strong>Parameters</strong></p> <ul> <li><code>roomKey</code> <code>string</code> — Fully-qualified room key (<code>{worldGuid}/{profile}/{mode}/{sceneGuid}</code>).</li> </ul> <pre><code class="language-lua">if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end </code></pre> <h2 id="modules-multiplayer-joinroom">modules/multiplayer/joinRoom</h2> <pre><code class="language-lua">joinRoom(roomKey: string) </code></pre> <p>Join a relay room. Room keys are built as <code>{worldGuid}/{profile}/{mode}/{sceneGuid}</code> — four segments, the <code>{profile}</code> one keeping a runtime peer (published content) and an editor peer (live content) in separate rooms even when both are in play mode. Rooms partition the relay's fan-out: only peers in the same room receive each other's broadcasts. <code>getRooms()</code> reports the keys this client is already in and <code>roomFor(entity)</code> the one an entity broadcasts into, so a key can be read rather than rebuilt. A relay that verifies its peers holds each connection to the world it was admitted to: a key whose first segment names another world is refused, and <code>getDiagnostics().refusals</code> counts the refusal and <code>lastRefusal</code> names it. No-op when not connected or already joined.</p> <p><strong>Parameters</strong></p> <ul> <li><code>roomKey</code> <code>string</code> — Fully-qualified room key.</li> </ul> <pre><code class="language-lua">multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid) </code></pre> <h2 id="modules-multiplayer-leaveroom">modules/multiplayer/leaveRoom</h2> <pre><code class="language-lua">leaveRoom(roomKey: string) </code></pre> <p>Leave a relay room. The key is reported under <code>observe().withdrawnRooms</code> until <code>joinRoom</code> names it again. No-op when not connected or not joined.</p> <p><strong>Parameters</strong></p> <ul> <li><code>roomKey</code> <code>string</code> — Fully-qualified room key.</li> </ul> <h2 id="modules-multiplayer-loopback">modules/multiplayer/loopback</h2> <pre><code class="language-lua">loopback(): { [string]: any } </code></pre> <p>Loopback testing harness. Returns a table with <code>enable()</code>, <code>disable()</code>, <code>flush()</code>, <code>receive()</code> methods for testing sync without a relay server.</p> <h2 id="modules-multiplayer-observe">modules/multiplayer/observe</h2> <pre><code class="language-lua">observe(): ReplicationObservation </code></pre> <p>Report what this peer is replicating and why a property is not arriving. Carries the rooms this client joined, one record per entity with a sync id — its owner, the room it broadcasts into, how many other peers share that room, and every REGISTERED synced component with its declared property names, wire indices, public/private table and dirty bits — the messages held for entities that have not arrived, and the registry's totals. Every property carries <code>notArriving</code>: one name from <code>reasons</code>, or nil when it is on its way. Answers in edit mode as well as play mode, for what the relay carries in each: in edit mode scene content is left out of the sync-id pass, so its changes travel to the other clients with the source they are written into and it reads <code>entityNotSynced</code> here. The document describes the session as of the last frame: when no frame has assembled one since the surface was last read, the call waits for the next frame to assemble it, and a caller that cannot yield receives the one published last.</p> <pre><code class="language-lua">local o = multiplayer.observe() print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties") </code></pre> <h2 id="modules-multiplayer-observecomponent">modules/multiplayer/observeComponent</h2> <pre><code class="language-lua">observeComponent( </code></pre> <p>The registered synced component of the named type on an entity's record. Matches a fully-qualified type (<code>@builtin::components.Model</code>) and the leaf name it ends in (<code>Model</code>) alike.</p> <pre><code class="language-lua">local c = multiplayer.observeComponent(player, "Health") print(#c.properties .. " declared properties") </code></pre> <h2 id="modules-multiplayer-observeentity">modules/multiplayer/observeEntity</h2> <pre><code class="language-lua">observeEntity(entityId: (string | entityRef)): EntityReplication? </code></pre> <p>The replication record for one entity — its sync id, owner, room, and the synced components registered on it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)</code> — Entity id or proxy.</li> </ul> <pre><code class="language-lua">local r = multiplayer.observeEntity(e) for _, c in ipairs(r.components) do print(c.componentType) end </code></pre> <h2 id="modules-multiplayer-on">modules/multiplayer/on</h2> <pre><code class="language-lua">on(channel: string, callback: (number, ...any) -> ()) </code></pre> <p>Subscribe to a custom message channel. The callback runs as <code>callback(fromPeerId, ...args)</code> whenever another peer calls <code>multiplayer.send(channel, ...)</code>. Multiple callbacks per channel fire in registration order.</p> <p><strong>Parameters</strong></p> <ul> <li><code>channel</code> <code>string</code> — Channel name to listen on.</li> <li><code>callback</code> <code>(number, ...any) -> ()</code> — <code>function(fromPeerId: number, ...)</code> — the sender's peer id then the sent args.</li> </ul> <h2 id="modules-multiplayer-recordfilewrite">modules/multiplayer/recordFileWrite</h2> <pre><code class="language-lua">recordFileWrite(path: string, before: string?, after: string?): boolean </code></pre> <p>Record a file write into the open operation. Undoing the operation writes <code>before</code> back through the VFS (removing the file when <code>before</code> is nil), which runs the owning asset type's <code>onChange</code> as the original write did; redoing writes <code>after</code>. The path is resolved and held to the rules <code>vfs.write</code> applies, so a sidecar (<code>.meta</code>, <code>.refs</code>), an install's own entry or an asset root no script may write raises. A step's files are put back all together or not at all: each file is checked (its asset exists, and it holds what the step being reversed left in it) before any is written and again immediately before it is written; when a check or a write fails partway, the files the step already wrote go back to what they held, and a notice names each file of the step and what happened to it. Several writes of one path in an operation undo to the first <code>before</code>. Records nothing and returns false when no operation is open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path of the written file.</li> <li><code>before</code> <code>string?</code> <em>(optional)</em> — The file's contents before the write, nil when it did not exist.</li> <li><code>after</code> <code>string?</code> <em>(optional)</em> — The file's contents after the write, nil when the write removed it.</li> </ul> <pre><code class="language-lua">local before = vfs.read(p); vfs.write(p, body); multiplayer.recordFileWrite(p, before, body) </code></pre> <h2 id="modules-multiplayer-recordspawn">modules/multiplayer/recordSpawn</h2> <pre><code class="language-lua">recordSpawn(entityId: string) </code></pre> <p>Adopt an existing entity into the open operation as its spawn — for flows that create an entity before the operation opens (a drag preview adopted on drop). Undoing the operation despawns it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string</code> — Entity id to record as spawned by this operation.</li> </ul> <pre><code class="language-lua">multiplayer.recordSpawn(id) </code></pre> <h2 id="modules-multiplayer-redo">modules/multiplayer/redo</h2> <pre><code class="language-lua">redo(): boolean </code></pre> <p>Redo this client's last undone operation.</p> <h2 id="modules-multiplayer-releaseownership">modules/multiplayer/releaseOwnership</h2> <pre><code class="language-lua">releaseOwnership(entityId: (string | entityRef)?): boolean </code></pre> <p>Release ownership of an entity, leaving it unowned so another peer's claim takes it. Only the owner releases an entity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)?</code> <em>(optional)</em> — Entity id or proxy to release.</li> </ul> <h2 id="modules-multiplayer-roomfor">modules/multiplayer/roomFor</h2> <pre><code class="language-lua">roomFor(entityId: (string | entityRef)): string? </code></pre> <p>The room key an entity's spawns and property deltas broadcast into: the <code>room</code> of its <code>observeEntity</code> record as of the last frame, read from the rooms the session publishes every frame, so a component can ask every frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)</code> — Entity id or proxy.</li> </ul> <pre><code class="language-lua">local key = multiplayer.roomFor(player) if key then multiplayer.joinRoom(key) end </code></pre> <h2 id="modules-multiplayer-send">modules/multiplayer/send</h2> <pre><code class="language-lua">send(channel: string, ...: any) </code></pre> <p>Broadcast a message on a named channel to every OTHER peer in the room. The relay forwards it transparently; peers receive it via <code>multiplayer.on</code>. The arguments are delivered to listeners in order, each one arriving as what it is, at the top level and at every depth of a table:</p> <ul> <li><code>nil</code>, a boolean, a number and a string arrive as themselves.</li> <li>A <code>vector</code> arrives as a <code>vector</code>.</li> <li>The proxy of a synced entity arrives as the receiving peer's proxy for that entity; the proxy of an entity no peer shares, as its id string.</li> <li>A synced component (its proxy, or the <code>componentRef</code> envelope naming it) arrives as that component on the receiving peer; the proxy of a component that holds no sync id, as the envelope <code>{ __ref, entity, componentType }</code> naming it.</li> <li>A transform vector an entity hands out (<code>e.position</code>, <code>e.rotation</code>, <code>e.localScale</code>, ...) arrives as the table of its components, <code>{ x, y, z }</code> or <code>{ x, y, z, w }</code>.</li> <li>Every other table is data and arrives as a new table holding the same keys, each key and each value carried by these same rules: <code>{ x = 5, kind = "coin" }</code> arrives as <code>{ x = 5, kind = "coin" }</code>, <code>{ who = e, n = 1 }</code> with <code>t.who</code> the receiver's proxy for <code>e</code>, <code>{ [5] = "a" }</code> with <code>t[5] == "a"</code>, <code>{ 1, 2, label = "m" }</code> with its list and its <code>label</code>, <code>{ [true] = "yes" }</code> with <code>t[true] == "yes"</code>, and a list with a <code>nil</code> in it with every entry after the <code>nil</code> at the index it was written under. A number inside a table arrives as that number, <code>math.huge</code> and NaN included.</li> <li>A value no peer can hold (a function, a thread, a buffer) arrives as nil, and so does a table entry whose key is one; the sender logs a warning naming the key's type. The same rules carry the arguments of a <code>declare.syncedFunctions</code> method, the payload of a <code>Sync</code> event and a synced table-valued field. No-op when not connected.</li> </ul> <p><strong>Parameters</strong></p> <ul> <li><code>channel</code> <code>string</code> — Channel name listeners subscribe to via <code>multiplayer.on</code>.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-multiplayer-setsmoothing">modules/multiplayer/setSmoothing</h2> <pre><code class="language-lua">setSmoothing(entityId: (string | entityRef), values: SmoothingSettings?): boolean </code></pre> <p>Set how this peer draws one synced entity another peer simulates, in place of the world's settings: any of <code>enabled</code> (false draws the entity at the newest pose received), <code>delay</code> and <code>extrapolation</code> (seconds, 0 to 2) and <code>teleportSpeed</code> (metres per second). A value left out is the world's, and nil or an empty table hands the entity back to them. The setting is this peer's own and lasts while the entity exists. <code>delay = 0</code> draws the newest pose delivered, which trails the owner by the network transit; see <code>smoothing</code> for what is drawn and when. <code>smoothing(entityId)</code> reads it back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)</code> — Synced entity to set.</li> <li><code>values</code> <code>SmoothingSettings?</code> <em>(optional)</em> — The values to use for this entity; nil hands it back to the world's.</li> </ul> <pre><code class="language-lua">multiplayer.setSmoothing(projectile, { delay = 0.05, teleportSpeed = 600 }) multiplayer.setSmoothing(car, nil) -- back to the world's settings </code></pre> <h2 id="modules-multiplayer-smoothing">modules/multiplayer/smoothing</h2> <pre><code class="language-lua">smoothing(entityId: (string | entityRef)?): Smoothing </code></pre> <p>How this peer draws the remote entities other peers simulate. A body another peer simulates is drawn a short delay behind the newest pose received for it, interpolated between the poses either side of that moment, so it moves smoothly whatever the frame rate on either side and the jitter between them, and a pose that arrives after a newer one never moves it back. When poses stop arriving it carries on along its last motion for up to <code>extrapolation</code> seconds, then holds. A step faster than <code>teleportSpeed</code> (metres per second) is a teleport and is drawn as a cut, and a stream whose sender changes (an ownership handover) starts over from its first pose. The owner's own view is untouched. The moment drawn is the owner's now less the network transit less <code>delay</code>; the transit stays in, so <code>delay = 0</code> draws the newest pose delivered and still trails the owner by it. A body that stops dead is carried past its stop until the pose it rests at arrives, about 0.1 s after it stops, when <code>delay</code> is shorter than that.</p> <p>The settings come from the world's settings <code>sync.interpolation</code> (boolean), <code>sync.interpolation_delay</code>, <code>sync.extrapolation_limit</code> (seconds) and <code>sync.teleport_speed</code>, with this peer's <code>setSmoothing</code> values for the entity in their place (<code>overridden</code>). For an entity whose poses this peer receives, <code>stream</code> holds the poses buffered, how far the newest stands ahead of the moment drawn (<code>lag</code>, seconds, negative while extrapolating), whether the sender stamps its poses with its clock, the <code>moment</code> last drawn on the sender's clock and this peer's clock it was drawn at (<code>drawnAt</code>), both in seconds so two reads subtract into how far the drawn moment moved over how much time, and the running counts of poses received, dropped as <code>stale</code>, stream <code>restarts</code> and <code>teleports</code>. Reading asks the coming frames to publish the streams, so the first read of one answers on the next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)?</code> <em>(optional)</em> — Entity to read; omit for the world's settings.</li> </ul> <pre><code class="language-lua">local s = multiplayer.smoothing(car); print(s.delay, s.stream and s.stream.lag) settings.set("sync.interpolation_delay", "0.15") -- every remote entity, world-wide </code></pre> <h2 id="modules-multiplayer-synctotals">modules/multiplayer/syncTotals</h2> <pre><code class="language-lua">syncTotals(): SyncTotals </code></pre> <p>What the sync registry holds across every entity: entities with a registered synced component, component instances, declared properties, declared functions, and the component instances holding a dirty property this tick.</p> <pre><code class="language-lua">print(multiplayer.syncTotals().properties .. " synced properties registered") </code></pre> <h2 id="modules-multiplayer-undo">modules/multiplayer/undo</h2> <pre><code class="language-lua">undo(): boolean </code></pre> <p>Undo this client's last edit-mode operation.</p> <h2 id="modules-names-readme">modules/names/README</h2> <pre><code class="language-lua">require("@builtin::avatarTypes.humanoid.names") -- names </code></pre> <p>Name hints for humanoid qualification. A bone's name never decides its role or its side; it raises the confidence of an assignment the structure already made, and a side word that disagrees with the geometry is reported. Covers the common conventions (Mixamo, Unity, VRM, CATS, Unreal, Synty, Blender).</p> <p>Usage: local names = require("@builtin::avatarTypes.humanoid.names")</p> <h2 id="modules-names-score">modules/names/score</h2> <pre><code class="language-lua">score(role: string, name: string): number </code></pre> <p>How well a bone name fits a role, 0..1. <code>role</code> is a humanoid role; its side and finger segment number are ignored (<code>lefthandindex2</code> is scored as <code>index</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>role</code> <code>string</code></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="modules-names-side">modules/names/side</h2> <pre><code class="language-lua">side(name: string): string? </code></pre> <p>The side a name states, <code>"left"</code>, <code>"right"</code>, or nil when it states none.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code></li> </ul> <h2 id="modules-names-tokens">modules/names/tokens</h2> <pre><code class="language-lua">tokens(name: string): { string } </code></pre> <p>The lowercase words of a bone name: a namespace prefix (<code>mixamorig:</code>) is dropped, and the name is split at separators, at lower-to-upper case changes and at letter/digit boundaries. <code>LeftForeArm</code> -> { "left", "fore", "arm" }; <code>J_Bip_L_UpperArm</code> -> { "j", "bip", "l", "upper", "arm" }.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code></li> </ul> <h2 id="modules-notices-readme">modules/notices/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.notices") -- notices (also available as global 'notices') </code></pre> <p>Post an event record that surfaces to the callers it is addressed to on the response of the tool call that posted it, or on their next tool call. Public Luau surface over the <code>__notices_post</code> Internal FFI global.</p> <p>Usage: local notices = require("@builtin::modules.api.engine.notices") Also available as global: notices</p> <h2 id="modules-notices-post">modules/notices/post</h2> <pre><code class="language-lua">post(template: string, params: { [string]: any }?, opts: NoticeOpts?) </code></pre> <p>Post a notice. <code>template</code> is a fixed sentence used to collapse repeats; put varying values in <code>params</code>. A param holding a picture — an image value from <code>mcp.image</code> or a capture tool — is delivered as a picture beside the notice's line rather than as text. <code>opts.severity</code> defaults to "info"; <code>opts.includeLocation</code> attaches the emitting call site. <code>opts.actionable</code> says whether the callers a notice is addressed to are expected to act on it, rather than take it in as background; an agent's event receiver starts an idle agent's turn for an actionable notice and hands the rest to its context. <code>opts.message</code> is the catalog key (<code><namespace>.<key></code>, see <code>i18n.t</code>) of the sentence a person reads the notice as, in their language: the editor's notification bell shows it (<code>notices.text</code>), read from the world's catalogs (<code>/zero/source/locales/<tag>/<namespace>.json</code>) and then the built-in library's, while an agent reads the template. A notice with no message reads as its template everywhere.</p> <p><strong>Parameters</strong></p> <ul> <li><code>template</code> <code>string</code> — Fixed sentence identifying the notice.</li> <li><code>params</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Optional table of named values rendered alongside the template; a picture in it arrives as a picture.</li> <li><code>opts</code> <code>NoticeOpts?</code> <em>(optional)</em> — Optional table: message (a catalog key), severity ("info" | "warn" | "error"), includeLocation (boolean), audience ("<em>" | { string }?). Who sees the notice: "</em>" addresses every caller; a list addresses those callers only, each entry an agent's name (as the <code>agent</code> toolbox's <code>roster</code> lists it) or a caller key <code><kind>:<id></code> such as "agent:<name>". Defaults to the caller whose code is running, or every caller when none is. A notice reaches the agents on this engine: an entry naming no agent on this world's roster, an entry naming an agent the roster lists on another engine of the world, or an empty list, raises an error. <code>actionable</code> (boolean) states whether the callers it is addressed to are expected to act on it; omitted, a notice addressed to particular callers (the default audience included) is actionable, and one for every caller is background.</li> </ul> <pre><code class="language-lua">notices.post("wave complete", { wave = 3 }) notices.post("save slot corrupted, using defaults", { slot = id }, { severity = "warn" }) notices.post("bake finished", { shot = mcp.image("/source/tmp/bake.png") }) notices.post("boss defeated", nil, { audience = "*" }) notices.post("build ready for review", nil, { audience = { "terrain" }, actionable = true }) notices.post("wave complete", { wave = 3 }, { message = "hud.wave_complete" }) -- /zero/source/locales/<tag>/hud.json </code></pre> <h2 id="modules-notices-text">modules/notices/text</h2> <pre><code class="language-lua">text(row: { template: string, message: string? }): any </code></pre> <p>The sentence a stored notice reads as for a person: its <code>message</code> in the reader's language, read from the world's catalogs and then the built-in library's, or its template when it names none. <code>row</code> is a notice as the <code>notices</code> toolbox's <code>list</code> tool states it (<code>template</code>, <code>message</code>). The value follows the language on screen, like <code>i18n.text</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>row</code> <code>{ template: string, message: string? }</code> — A notice row: <code>{ template, message? }</code>.</li> </ul> <pre><code class="language-lua">for _, row in tools.use("notices", "list").pending do print(tostring(notices.text(row))) end </code></pre> <h2 id="modules-notices-withdraw">modules/notices/withdraw</h2> <pre><code class="language-lua">withdraw(template: string): number </code></pre> <p>Withdraw the notices this code posted under <code>template</code> that are still stored, whoever they are addressed to, because what they state has stopped being true: a caller that has not read one yet would be told something that no longer holds. A notice is keyed by its template and by the code that posted it, so a call reaches only notices posted from the same place. A later <code>notices.post</code> under the template queues again, and reaches the callers that muted it too: their mute acknowledged what the withdrawn notices stated.</p> <p><strong>Parameters</strong></p> <ul> <li><code>template</code> <code>string</code> — The template the notices were posted under.</li> </ul> <pre><code class="language-lua">notices.withdraw("save slot corrupted, using defaults") </code></pre> <h2 id="modules-onunload">modules/onUnload</h2> <pre><code class="language-lua">modules.onUnload(callback) -> () </code></pre> <p>Register a teardown for the calling module's current run. It runs once, at the moment this run ends — immediately before the next run of that chunk replaces it, and when the module leaves the require cache — while this run's module-level locals are still in scope, so the run that spawned the entities, created the particle systems or subscribed to the event bus is the run that releases them. Each run registers its own, and the registration goes with the run that made it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>callback</code> <code>function</code> — Called with no arguments just before this run of the module is replaced</li> </ul> <h2 id="modules-packages-readme">modules/packages/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.packages") -- packages (also available as global 'packages') </code></pre> <p>Discover and inspect registered packages (folders marked with <code>package.yaml</code>). Public Luau surface over the <code>__packages</code> Internal FFI namespace.</p> <p>Usage: local packages = require("@builtin::modules.api.engine.packages") Also available as global: packages</p> <h2 id="modules-packages-list">modules/packages/list</h2> <pre><code class="language-lua">list(): { PackageEntry } </code></pre> <p>List every registered package across scopes.</p> <pre><code class="language-lua">for _, p in ipairs(packages.list()) do print(p.name, p.scope) end </code></pre> <h2 id="modules-packages-lookup">modules/packages/lookup</h2> <pre><code class="language-lua">lookup(name_or_scope: string, name: string?): PackageEntry? </code></pre> <p>Look up a single package by name (any scope) or by exact <code>(scope, name)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name_or_scope</code> <code>string</code> — Package name, or scope if a second arg is given.</li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Package name when the first arg is a scope.</li> </ul> <pre><code class="language-lua">local p = packages.lookup("@builtin", "audio") </code></pre> <h2 id="geometry-readme">modules/pathNetwork.geometry/README</h2> <pre><code class="language-lua">require("@builtin::modules.pathNetwork.geometry") -- pathNetwork.geometry </code></pre> <p>The shape of a path network: the centreline of each edge through its control points, the cross-sections along it (position, heading, bank, the surface's right and up), and the meshes built from them (an edge's surface with side walls and end caps, a junction's surface joining the edges that meet at a node). Pure functions over plain tables; the network in <code>pathNetwork</code> owns the entities they are drawn on.</p> <p>Usage: local pathNetwork.geometry = require("@builtin::modules.pathNetwork.geometry")</p> <h2 id="geometry-at">modules/pathNetwork.geometry/at</h2> <pre><code class="language-lua">at(line: any, d: number): (number, number, number) </code></pre> <p>The point a distance along a sampled centreline, interpolated between its samples, held at either end.</p> <p><strong>Parameters</strong></p> <ul> <li><code>line</code> <code>any</code> <em>(optional)</em> — The table <code>centreline</code> returned.</li> <li><code>d</code> <code>number</code> — Metres from its start.</li> </ul> <h2 id="geometry-centreline">modules/pathNetwork.geometry/centreline</h2> <pre><code class="language-lua">centreline(control: { Point }, before: Point, after: Point, step: number) </code></pre> <p>Sample a centreline through <code>control</code> (two points or more) finely enough to measure it: a Catmull-Rom curve through every control point, with <code>before</code> and <code>after</code> standing in for the neighbours past each end (the next path's control point across a node, so two paths meeting there share one tangent).</p> <p><strong>Parameters</strong></p> <ul> <li><code>control</code> <code>{ Point }</code> — The control points, first to last.</li> <li><code>before</code> <code>Point</code> — The point standing before the first.</li> <li><code>after</code> <code>Point</code> — The point standing after the last.</li> <li><code>step</code> <code>number</code> — Metres between samples.</li> </ul> <h2 id="geometry-edgemesh">modules/pathNetwork.geometry/edgeMesh</h2> <pre><code class="language-lua">edgeMesh(sections: { Section }, opts: any): any </code></pre> <p>The mesh of one edge: its surface across the cross-sections, a wall <code>skirt</code> metres deep down each side, and a cap at each end <code>caps</code> names.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sections</code> <code>{ Section }</code> — The edge's cross-sections.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ skirt, uvScale, capStart, capEnd }</code>.</li> </ul> <h2 id="geometry-fandistance">modules/pathNetwork.geometry/fanDistance</h2> <pre><code class="language-lua">fanDistance(centre: Point, ring: { Point }, x: number, z: number): number </code></pre> <p>How far a point stands outside a junction's surface, seen from above: 0 inside the fan (the centre and its corners in order), else the distance to the nearest side of its outline.</p> <p><strong>Parameters</strong></p> <ul> <li><code>centre</code> <code>Point</code> — The junction's centre.</li> <li><code>ring</code> <code>{ Point }</code> — Its corners in order round the centre.</li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> </ul> <h2 id="geometry-fanheight">modules/pathNetwork.geometry/fanHeight</h2> <pre><code class="language-lua">fanHeight(centre: Point, ring: { Point }, x: number, z: number): number </code></pre> <p>The height of a junction's surface over a point: the plane of the fan triangle (centre and two neighbouring corners) whose wedge the point stands in, seen from the centre.</p> <p><strong>Parameters</strong></p> <ul> <li><code>centre</code> <code>Point</code> — The junction's centre.</li> <li><code>ring</code> <code>{ Point }</code> — Its corners in order round the centre.</li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> </ul> <h2 id="geometry-junctionmesh">modules/pathNetwork.geometry/junctionMesh</h2> <pre><code class="language-lua">junctionMesh(centre: Point, ends: { any }, opts: any): any </code></pre> <p>The mesh of a junction: a surface fanned from the node's centre out to the end corners of every edge that meets there, so each edge's end and the junction share their vertices, with a wall down the open sides between edges.</p> <p><strong>Parameters</strong></p> <ul> <li><code>centre</code> <code>Point</code> — The node's centre.</li> <li><code>ends</code> <code>{ any }</code> — One <code>{ left, right }</code> pair of corner points per edge end at the node.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ skirt, uvScale }</code>.</li> </ul> <h2 id="geometry-project">modules/pathNetwork.geometry/project</h2> <pre><code class="language-lua">project(a: Section, b: Section, x: number, z: number): (number, number, number) </code></pre> <p>Where a point lies against one segment of cross-sections, seen from above: the fraction along it, the signed lateral distance from the centreline (positive to the right), and how far past either end it falls.</p> <p><strong>Parameters</strong></p> <ul> <li><code>a</code> <code>Section</code> — The segment's first section.</li> <li><code>b</code> <code>Section</code> — Its second.</li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> </ul> <h2 id="geometry-sections">modules/pathNetwork.geometry/sections</h2> <pre><code class="language-lua">sections(line: any, spec: any): { Section } </code></pre> <p>Cross-sections along an edge between two distances on its centreline, about <code>spacing</code> apart, each carrying the surface's frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>line</code> <code>any</code> <em>(optional)</em> — The sampled centreline.</li> <li><code>spec</code> <code>any</code> <em>(optional)</em> — <code>{ from, to, spacing, half, bank, heights?, startHeading?, endHeading?, startPlane?, endPlane?, planeReach?, maxTwist?, tolerance? }</code>: the trimmed range, the half width, the bank (degrees, one number or one per control point), the elevation of each section when it is not the centreline's own (a function of <code>(x, z, index, count)</code>, called for the sections <code>spacing</code> apart), the headings an end must take (a unit <code>{x, z}</code>), so two edges meeting at a node join without a crease, and the plane an end lies across (<code>{ gx, gz }</code>, its rise per metre along X and Z): the end's cross-section tips to lie on it, easing back to the bank over <code>planeReach</code> metres, and further where the cross slope (the rise across the surface per metre) would otherwise turn by more than <code>maxTwist</code> per metre, as far as the edge reaches, so the edges meeting at a junction share its plane. <code>tolerance</code> is the most, in metres, the flat triangles drawn between two sections may stand off the surface the sections sweep where it twists: between two sections whose cross slope turns by more than that allows, sections are added, the centreline's height running straight between the two where <code>heights</code> states it.</li> </ul> <h2 id="geometry-smooth">modules/pathNetwork.geometry/smooth</h2> <pre><code class="language-lua">smooth(ys: { number }, window: number, opts: { [string]: any }?): { number } </code></pre> <p>Smooth an elevation profile with a moving average <code>window</code> samples wide each side, and hold its ends. <code>ys</code> may carry <code>lead</code> samples before the profile's first and <code>trail</code> after its last (the ground past each end), which the average reads and the answer leaves out, so the window stays full and centred right up to the ends. A held end meets its height (<code>first</code>, <code>last</code>) and, where one is given, its grade (<code>firstSlope</code>, <code>lastSlope</code>: rise per metre from the first sample toward the last) through a cubic ease that leaves the smoothed profile with the profile's own height and grade. The ease reaches far enough that it adds no more than <code>maxGrade</code> to the grade to make up a height and bends the grade by no more than <code>maxBend</code> per metre to meet one, so two paths meeting at a node with the same height and grade join without a crease.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ys</code> <code>{ number }</code> — The heights, <code>spacing</code> metres apart, <code>lead</code> and <code>trail</code> included.</li> <li><code>window</code> <code>number</code> — Samples each side.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ spacing, lead?, trail?, first?, last?, firstSlope?, lastSlope?, maxGrade?, maxBend? }</code>; an end with no height given keeps its smoothed one.</li> </ul> <h2 id="geometry-surfaceheight">modules/pathNetwork.geometry/surfaceHeight</h2> <pre><code class="language-lua">surfaceHeight(a: Section, b: Section, t: number, lateral: number): number </code></pre> <p>The surface's height at a lateral distance across a segment, at fraction <code>t</code> along it, on the two triangles <code>edgeMesh</code> draws the segment with (split from the first section's left corner to the second's right), so the height is the drawn and colliding surface's, a banked or twisting stretch included.</p> <p><strong>Parameters</strong></p> <ul> <li><code>a</code> <code>Section</code> — The segment's first section.</li> <li><code>b</code> <code>Section</code> — Its second.</li> <li><code>t</code> <code>number</code> — Fraction along the segment.</li> <li><code>lateral</code> <code>number</code> — Signed metres right of the centreline, held to the surface's edge.</li> </ul> <h2 id="ground-readme">modules/pathNetwork.ground/README</h2> <pre><code class="language-lua">require("@builtin::modules.pathNetwork.ground") -- pathNetwork.ground </code></pre> <p>The ground a path network stands on: the <code>Terrain</code> entities under it, the height of the ground as it was before any path changed it, and the conforming of that ground to the paths, so a path's bed is cut and filled into the slope rather than floating over it or buried in it. Every texel a path changes keeps the height it had, so a path moved or removed gives the ground back.</p> <p>Usage: local pathNetwork.ground = require("@builtin::modules.pathNetwork.ground")</p> <h2 id="ground-baseheight">modules/pathNetwork.ground/baseHeight</h2> <pre><code class="language-lua">baseHeight(state: any, x: number, z: number): number? </code></pre> <p>The ground's height at a point as it stood before the network changed it: the world Y where the world's vertical through the point meets the ground, wherever its terrain's entity puts the landscape, with the texel cell there read the way the terrain reads it (<code>terrainHeightfield.cellHeight</code>) from each texel's kept height where a path has changed it. Nil where no terrain is under the point.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> </ul> <h2 id="ground-conform">modules/pathNetwork.ground/conform</h2> <pre><code class="language-lua">conform(state: any, changed: { Footprint }, all: { Footprint }): number </code></pre> <p>Conform the ground to the network's footprints over the area the <code>changed</code> footprints reach (the ones a build replaced and the ones it made). Each texel there takes the height of the footprint holding it most strongly, blended with its own over the falloff; a texel no footprint holds any more is given back its own height.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> <li><code>changed</code> <code>{ Footprint }</code> — The footprints whose area is conformed again.</li> <li><code>all</code> <code>{ Footprint }</code> — Every footprint the network has; the ones reaching the changed area are what a texel is judged against.</li> </ul> <h2 id="ground-discfootprint">modules/pathNetwork.ground/discFootprint</h2> <pre><code class="language-lua">discFootprint(cx: number, cz: number, radius: number, y: number, ring: { any }, opts: any): Footprint </code></pre> <p>A footprint for a junction: a disc reaching the surface's farthest corner, at the height of the surface's fan over each point.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cx</code> <code>number</code> — Centre X.</li> <li><code>cz</code> <code>number</code> — Centre Z.</li> <li><code>radius</code> <code>number</code> — The surface's radius.</li> <li><code>y</code> <code>number</code> — The surface's height at the centre.</li> <li><code>ring</code> <code>{ any }</code> — The surface's corners in order round the centre.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ shoulder, falloff, bed }</code>.</li> </ul> <h2 id="ground-edgefootprint">modules/pathNetwork.ground/edgeFootprint</h2> <pre><code class="language-lua">edgeFootprint(sections: { any }, opts: any): Footprint </code></pre> <p>A footprint for an edge's cross-sections.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sections</code> <code>{ any }</code> — The edge's cross-sections.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ shoulder, falloff, bed }</code>.</li> </ul> <h2 id="ground-height">modules/pathNetwork.ground/height</h2> <pre><code class="language-lua">height(state: any, x: number, z: number): number? </code></pre> <p>The height the ground stands at now, the network's changes included: the world Y where the world's vertical through the point meets it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> </ul> <h2 id="ground-open">modules/pathNetwork.ground/open</h2> <pre><code class="language-lua">open(terrains: any): any </code></pre> <p>Open the ground under a network: the <code>Terrain</code> entities named, or every one in the scene when none is named.</p> <p><strong>Parameters</strong></p> <ul> <li><code>terrains</code> <code>any</code> <em>(optional)</em> — A terrain entity id, a list of them, <code>false</code> for no ground, or nil for every <code>Terrain</code> in the scene.</li> </ul> <h2 id="ground-present">modules/pathNetwork.ground/present</h2> <pre><code class="language-lua">present(state: any): boolean </code></pre> <p>Whether any ground is under the network.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> </ul> <h2 id="ground-restore">modules/pathNetwork.ground/restore</h2> <pre><code class="language-lua">restore(state: any): number </code></pre> <p>Give the ground back every height the network changed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> </ul> <h2 id="ground-settle">modules/pathNetwork.ground/settle</h2> <pre><code class="language-lua">settle(state: any) </code></pre> <p>Record the ground as it stands now as the ground the network is conformed to, so <code>stale</code> answers false until a terrain is read again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> </ul> <h2 id="ground-stale">modules/pathNetwork.ground/stale</h2> <pre><code class="language-lua">stale(state: any): boolean </code></pre> <p>Whether a terrain under the network holds ground the network has not conformed: one that loaded after the network was built, or one read again from its asset since (a re-bake, a reload, a first read that raced the asset's write), which holds none of the network's cut and fill.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — The ground state.</li> </ul> <h2 id="lanes-readme">modules/pathNetwork.lanes/README</h2> <pre><code class="language-lua">require("@builtin::modules.pathNetwork.lanes") -- pathNetwork.lanes </code></pre> <p>The directed lane graph of a path network and the questions asked of it: which lanes each edge carries and which way they run, how lanes join through each node, where a lane stops for a junction, the nearest lane to a point, a route between two places, and the point a set distance ahead along a lane and the lanes after it. Pure functions over the built network's tables.</p> <p>Usage: local pathNetwork.lanes = require("@builtin::modules.pathNetwork.lanes")</p> <h2 id="lanes-connect">modules/pathNetwork.lanes/connect</h2> <pre><code class="language-lua">connect(lanes: { [string]: Lane }, degree: { [any]: number }) </code></pre> <p>Join the lanes through every node: each lane arriving at a node goes on to the lanes leaving it along the other edges there, keeping its lane index where the next edge has that many lanes. A node at the end of one edge turns its lanes back along it. A lane arriving at a junction (three edges or more) stops at its end, which is its <code>stopLine</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>lanes</code> <code>{ [string]: Lane }</code> — Every lane, by id.</li> <li><code>degree</code> <code>{ [any]: number }</code> — The number of edges at each node, by node id.</li> </ul> <h2 id="lanes-connector">modules/pathNetwork.lanes/connector</h2> <pre><code class="language-lua">connector(from: Lane, to: Lane): any </code></pre> <p>The way from the end of one lane to the start of the next, through a node: a cubic curve leaving along the first lane's heading and arriving along the second's. Lanes that meet end to start join in a single point. Worked out once per pair of lanes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>from</code> <code>Lane</code> — The lane being left.</li> <li><code>to</code> <code>Lane</code> — The lane being entered.</li> </ul> <h2 id="lanes-foredge">modules/pathNetwork.lanes/forEdge</h2> <pre><code class="language-lua">forEdge(edge: any, drive: string): { Lane } </code></pre> <p>The lanes one built edge carries, start to end in the direction each runs. With <code>drive = "right"</code>, lanes running from the edge's first node to its last stand right of its centreline; <code>"left"</code> mirrors it. Lane index 1 is the one beside the centreline.</p> <p><strong>Parameters</strong></p> <ul> <li><code>edge</code> <code>any</code> <em>(optional)</em> — The edge record, with its built <code>sections</code>.</li> <li><code>drive</code> <code>string</code> — <code>"right"</code> or <code>"left"</code>.</li> </ul> <h2 id="lanes-index">modules/pathNetwork.lanes/index</h2> <pre><code class="language-lua">index(edges: { [any]: any }): any </code></pre> <p>An index of every edge's cross-section segments by cell, for the nearest-point query.</p> <p><strong>Parameters</strong></p> <ul> <li><code>edges</code> <code>{ [any]: any }</code> — The built edges.</li> </ul> <h2 id="lanes-lanedistanceat">modules/pathNetwork.lanes/laneDistanceAt</h2> <pre><code class="language-lua">laneDistanceAt(lane: Lane, s: number): number </code></pre> <p>The distance along a lane nearest a distance along its edge.</p> <p><strong>Parameters</strong></p> <ul> <li><code>lane</code> <code>Lane</code> — The lane.</li> <li><code>s</code> <code>number</code> — The distance along the edge's centreline.</li> </ul> <h2 id="lanes-makeconnector">modules/pathNetwork.lanes/makeConnector</h2> <pre><code class="language-lua">makeConnector(from: Lane, to: Lane): any </code></pre> <p>The connector curve between two lanes, worked out afresh.</p> <p><strong>Parameters</strong></p> <ul> <li><code>from</code> <code>Lane</code> — The lane being left.</li> <li><code>to</code> <code>Lane</code> — The lane being entered.</li> </ul> <h2 id="lanes-nearestsegment">modules/pathNetwork.lanes/nearestSegment</h2> <pre><code class="language-lua">nearestSegment(index: any, x: number, z: number, maxDistance: number?, edges: { [any]: boolean }?): any? </code></pre> <p>The nearest point on any edge's centreline to a world point, seen from above, searching outward ring by ring through the index.</p> <p><strong>Parameters</strong></p> <ul> <li><code>index</code> <code>any</code> <em>(optional)</em> — The index <code>index</code> built.</li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> <li><code>maxDistance</code> <code>number?</code> <em>(optional)</em> — How far to look, in metres; nil looks across the whole network.</li> <li><code>edges</code> <code>{ [any]: boolean }?</code> <em>(optional)</em> — Optional set of edge ids (<code>{ [id] = true }</code>) to look on; nil looks on every edge.</li> </ul> <h2 id="lanes-point">modules/pathNetwork.lanes/point</h2> <pre><code class="language-lua">point(lane: Lane, d: number): ({ x: number, y: number, z: number }, { x: number, y: number, z: number }) </code></pre> <p>The point and travel direction a distance along a lane.</p> <p><strong>Parameters</strong></p> <ul> <li><code>lane</code> <code>Lane</code> — The lane.</li> <li><code>d</code> <code>number</code> — Metres from its start, held to its length.</li> </ul> <h2 id="lanes-route">modules/pathNetwork.lanes/route</h2> <pre><code class="language-lua">route(lanes: { [string]: Lane }, start: any, goal: any, by: string): ({ string }?, number?) </code></pre> <p>The cheapest way through the lane graph from one place on a lane to another, by length or by time at each lane's speed limit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>lanes</code> <code>{ [string]: Lane }</code> — Every lane, by id.</li> <li><code>start</code> <code>any</code> <em>(optional)</em> — <code>{ lane, distance }</code>.</li> <li><code>goal</code> <code>any</code> <em>(optional)</em> — <code>{ lane, distance }</code>.</li> <li><code>by</code> <code>string</code> — <code>"distance"</code> or <code>"time"</code>.</li> </ul> <h2 id="procedural-readme">modules/pathNetwork.procedural/README</h2> <pre><code class="language-lua">require("@builtin::modules.pathNetwork.procedural") -- pathNetwork.procedural </code></pre> <p>Rules that lay out a path network: a network joining a set of places (a spanning tree with extra links for loops), a grid of streets, and a closed circuit. Each answers a plan, <code>{ nodes, edges }</code>, which <code>pathNetwork.fromPlan</code> (or <code>net:addPlan</code>) turns into a network. The same seed lays out the same network.</p> <p>Usage: local pathNetwork.procedural = require("@builtin::modules.pathNetwork.procedural")</p> <h2 id="procedural-circuit">modules/pathNetwork.procedural/circuit</h2> <pre><code class="language-lua">circuit(opts: any?): Plan </code></pre> <p>A closed circuit: <code>count</code> nodes round an ellipse whose radius wanders by up to <code>wander</code> of itself, each joined to the next and the last to the first, with <code>pointsPerEdge</code> control points between them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ origin?, radius?, radiusZ?, count?, wander?, pointsPerEdge?, seed? }</code>: the centre, the radius along X in metres (default 200) and along Z (default the same), the nodes (default 4), the share of the radius it wanders by, from 0 to 0.9 (default 0), the control points between two nodes (default 2), and the seed.</li> </ul> <h2 id="procedural-connect">modules/pathNetwork.procedural/connect</h2> <pre><code class="language-lua">connect(places: { any }, opts: any?): Plan </code></pre> <p>A network joining a set of places: the shortest tree that reaches every one of them, plus <code>loops</code> extra links between near neighbours that cross no other link, so there is more than one way round.</p> <p><strong>Parameters</strong></p> <ul> <li><code>places</code> <code>{ any }</code> — The points to join, <code>{x, y, z}</code> each.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ loops?, meander?, spacing?, seed? }</code>: extra links (default a third of the places), how far the links wander sideways in metres (default 0), metres between their control points (default 60), and the seed.</li> </ul> <h2 id="procedural-grid">modules/pathNetwork.procedural/grid</h2> <pre><code class="language-lua">grid(opts: any?): Plan </code></pre> <p>A grid of streets: <code>cols</code> by <code>rows</code> crossings <code>spacing</code> metres apart, each joined to its neighbours, centred on <code>origin</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ cols?, rows?, spacing?, origin?, jitter?, seed? }</code>: crossings along X (default 4) and Z (default 4), metres between them (default 80), the centre, and how far each crossing is moved at random (default 0).</li> </ul> <h2 id="modules-pathnetwork-readme">modules/pathNetwork/README</h2> <pre><code class="language-lua">require("@builtin::modules.pathNetwork") -- pathNetwork </code></pre> <p>Path networks built at runtime from Luau: roads, tracks, rails, footpaths, rivers' courses, racing lines, patrol and delivery routes. A network is NODES (places, junctions, ends) joined by EDGES (paths through control points), each edge carrying directed LANES. <code>build</code> turns it into drivable surfaces with static colliders and a material, conforms the terrain under them, and joins the lanes into a graph that answers the nearest lane to a point, a route between two places, and the point a set distance ahead. Editing a node or an edge and building again rebuilds the edges and junctions that edit reaches and nothing else.</p> <p><code>local paths = require("@builtin::modules.pathNetwork")</code>, then <code>local net = paths.new({ ... })</code>, <code>net:addNode(...)</code>, <code>net:addEdge(...)</code>, <code>net:build()</code>. The README has the whole surface; <code>paths.procedural</code> lays out networks by rule.</p> <p>Usage: local pathNetwork = require("@builtin::modules.pathNetwork")</p> <h2 id="modules-pathnetwork-addedge">modules/pathNetwork/addEdge</h2> <pre><code class="language-lua">addEdge(self: Network, a: any, b: any, fields: any?): string </code></pre> <p>Add an edge: a path from node <code>a</code> to node <code>b</code> through <code>points</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>a</code> <code>any</code> <em>(optional)</em> — The first node's id.</li> <li><code>b</code> <code>any</code> <em>(optional)</em> — The last node's id.</li> <li><code>fields</code> <code>any?</code> <em>(optional)</em> — Optional: <code>points</code> (the control points between the two nodes, in order), <code>id</code>, and any edge setting the network takes (<code>lanes</code>, <code>laneWidth</code>, <code>width</code>, <code>shoulder</code>, <code>speedLimit</code>, <code>bank</code>, <code>elevation</code>, <code>conform</code>, <code>material</code>, ...).</li> </ul> <pre><code class="language-lua">local e = net:addEdge(a, b, { points = { { 40, 0, 10 } }, bank = 4 }) </code></pre> <h2 id="modules-pathnetwork-addnode">modules/pathNetwork/addNode</h2> <pre><code class="language-lua">addNode(self: Network, position: any, fields: any?): string </code></pre> <p>Add a node: a place two or more edges meet at, or an end.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>position</code> <code>any</code> <em>(optional)</em> — <code>{ x, y, z }</code>. With <code>elevation = "terrain"</code> (the default) the node stands on the ground and <code>y</code> is read only where there is none.</li> <li><code>fields</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ id, elevation, junctionRadius }</code>: an id of your own (a string), <code>"terrain"</code> or <code>"points"</code>, and the radius in plan of the junction surface built where three edges or more meet (worked out from the widths and angles of the edges when unset).</li> </ul> <pre><code class="language-lua">local a = net:addNode({ 0, 0, 0 }) </code></pre> <h2 id="modules-pathnetwork-addplan">modules/pathNetwork/addPlan</h2> <pre><code class="language-lua">addPlan(self: Network, plan: any, edgeFields: any?): { [string]: string } </code></pre> <p>Add every node and edge of a plan (<code>pathNetwork.procedural</code>'s, or one written by hand: <code>{ nodes = { { id, x, y, z } }, edges = { { a, b, points } } }</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>plan</code> <code>any</code> <em>(optional)</em> — The plan.</li> <li><code>edgeFields</code> <code>any?</code> <em>(optional)</em> — Optional settings every edge of the plan takes.</li> </ul> <pre><code class="language-lua">net:addPlan(paths.procedural.grid({ cols = 3, rows = 3 })) </code></pre> <h2 id="modules-pathnetwork-apply">modules/pathNetwork/apply</h2> <pre><code class="language-lua">apply(self: Network, spec: any) </code></pre> <p>Bring the network to the state a spec describes: nodes and edges are matched by id, those that differ are changed, those missing are removed and new ones added, so the next <code>build</code> rebuilds only what changed. An edge in the spec without an id is given <code><a>-<b></code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>spec</code> <code>any</code> <em>(optional)</em> — <code>{ nodes = { { id, x, y, z, ... } }, edges = { { id?, a, b, points?, ... } } }</code>.</li> </ul> <h2 id="modules-pathnetwork-build">modules/pathNetwork/build</h2> <pre><code class="language-lua">build(self: Network, opts: any?): any </code></pre> <p>Build what has changed since the last build: the surfaces of every edge added, edited, or at a node that moved (and its neighbours across a node of two edges, which share its tangent), the junctions at their ends, the ground under all of them, and the lane graph. The first build builds everything, and so does a build after the surfaces were taken down from outside the network (the scene reloaded, or play ended and took the temporary entities they stand on).</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ all = true }</code> to rebuild every edge and junction.</li> </ul> <pre><code class="language-lua">local stats = net:build() </code></pre> <h2 id="modules-pathnetwork-destroy">modules/pathNetwork/destroy</h2> <pre><code class="language-lua">destroy(self: Network) </code></pre> <p>Take down every surface the network built and give the ground back. The network's nodes and edges stay; <code>build</code> stands it up again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> </ul> <h2 id="modules-pathnetwork-edge">modules/pathNetwork/edge</h2> <pre><code class="language-lua">edge(self: Network, id: any): any </code></pre> <p>An edge as data: its ends, control points, settings, and once built its <code>length</code> (of the full centreline), <code>width</code>, lane ids and the entity its surface stands on.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The edge's id.</li> </ul> <h2 id="modules-pathnetwork-edgeids">modules/pathNetwork/edgeIds</h2> <pre><code class="language-lua">edgeIds(self: Network): { string } </code></pre> <p>The ids of every edge, sorted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> </ul> <h2 id="modules-pathnetwork-entity">modules/pathNetwork/entity</h2> <pre><code class="language-lua">entity(self: Network): string? </code></pre> <p>The entity every surface of the network stands under, once built.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> </ul> <h2 id="modules-pathnetwork-fromplan">modules/pathNetwork/fromPlan</h2> <pre><code class="language-lua">fromPlan(plan: any, opts: any?): Network </code></pre> <p>A network from a plan, with the network settings <code>opts</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plan</code> <code>any</code> <em>(optional)</em> — The plan.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — Network settings, as <code>new</code> takes.</li> </ul> <pre><code class="language-lua">local net = paths.fromPlan(paths.procedural.circuit({ radius = 300 })) </code></pre> <h2 id="modules-pathnetwork-groundheight">modules/pathNetwork/groundHeight</h2> <pre><code class="language-lua">groundHeight(self: Network, x: number, z: number): number? </code></pre> <p>The ground's height at a point now, the network's cut and fill included; nil where no terrain is under the network.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>x</code> <code>number</code> — World X.</li> <li><code>z</code> <code>number</code> — World Z.</li> </ul> <h2 id="modules-pathnetwork-lane">modules/pathNetwork/lane</h2> <pre><code class="language-lua">lane(self: Network, id: any): any </code></pre> <p>A lane as data: its edge, which way it runs (<code>direction</code> 1 from the edge's first node, -1 toward it), its index from the centreline, lateral <code>offset</code> and <code>width</code>, <code>speedLimit</code>, <code>length</code>, the nodes it runs between, the lanes it leads on to (<code>next</code>) and comes from (<code>prev</code>), and <code>stopLine</code>, the distance along it where it meets a junction.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The lane's id.</li> </ul> <h2 id="modules-pathnetwork-laneids">modules/pathNetwork/laneIds</h2> <pre><code class="language-lua">laneIds(self: Network): { string } </code></pre> <p>The ids of every lane, sorted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> </ul> <h2 id="modules-pathnetwork-lanepath">modules/pathNetwork/lanePath</h2> <pre><code class="language-lua">lanePath(self: Network, ids: { any }, opts: any?): { any } </code></pre> <p>The polyline of a run of lanes joined through their nodes, start to end: a lane loop for a follower to circle, a patrol, a racing line.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>ids</code> <code>{ any }</code> — Lane ids in order, each leading on to the next.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ closed = true }</code> to join the last lane back to the first, the line then ending on the point it starts at.</li> </ul> <pre><code class="language-lua">local loop = net:lanePath({ "e1:+1", "e2:+1", "e3:+1" }, { closed = true }) </code></pre> <h2 id="modules-pathnetwork-lanepoint">modules/pathNetwork/lanePoint</h2> <pre><code class="language-lua">lanePoint(self: Network, id: any, distance: number): (any, any) </code></pre> <p>The point and travel direction a distance along a lane.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The lane's id.</li> <li><code>distance</code> <code>number</code> — Metres from the lane's start, held to its length.</li> </ul> <pre><code class="language-lua">local p, dir = net:lanePoint("e1:+1", 25) </code></pre> <h2 id="modules-pathnetwork-lookahead">modules/pathNetwork/lookAhead</h2> <pre><code class="language-lua">lookAhead(self: Network, from: any, ahead: number, opts: any?): any </code></pre> <p>The point <code>ahead</code> metres further along from a place on a lane, carrying on into the lanes after it through each node: along <code>route</code> (a list of lane ids, as <code>route</code> answers) where it names the next lane, else the lane <code>choose(lane, nextIds)</code> picks, else the first of them. What an AI driver steers toward and a follower looks ahead with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>from</code> <code>any</code> <em>(optional)</em> — <code>{ lane, distance }</code>, or a world point (its nearest lane).</li> <li><code>ahead</code> <code>number</code> — Metres to look ahead.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ route, choose }</code>.</li> </ul> <pre><code class="language-lua">local target = net:lookAhead({ lane = here.lane, distance = here.laneDistance }, 12) </code></pre> <h2 id="modules-pathnetwork-movenode">modules/pathNetwork/moveNode</h2> <pre><code class="language-lua">moveNode(self: Network, id: any, position: any) </code></pre> <p>Move a node. The edges at it, and the junctions at their other ends, are rebuilt by the next <code>build</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The node's id.</li> <li><code>position</code> <code>any</code> <em>(optional)</em> — The new <code>{ x, y, z }</code>.</li> </ul> <pre><code class="language-lua">net:moveNode(a, { 10, 0, -4 }) </code></pre> <h2 id="modules-pathnetwork-nearest">modules/pathNetwork/nearest</h2> <pre><code class="language-lua">nearest(self: Network, position: any, opts: any?): any? </code></pre> <p>Where a world point stands against the network, seen from above: the nearest edge and the distance along it, how far right of its centreline the point is, whether it is on the surface, and the lane under it (the one whose centre is nearest, preferring lanes running along <code>heading</code> when one is given) with the distance along that lane and its direction. With <code>lanes</code>, only those lanes and their edges are looked at: a driver keeping to its route through a junction, where another edge stands nearer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>position</code> <code>any</code> <em>(optional)</em> — The world point.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ heading = {x, y, z}, maxDistance = metres, lanes = { laneId, ... } }</code>.</li> </ul> <pre><code class="language-lua">local here = net:nearest(car.position, { heading = car.forward }) </code></pre> <h2 id="modules-pathnetwork-new">modules/pathNetwork/new</h2> <pre><code class="language-lua">new(opts: { [string]: any }?): Network </code></pre> <p>A new, empty network. Every edge reads its settings from the edge, then from here, then from <code>pathNetwork.DEFAULTS</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Network settings: any edge setting (<code>laneWidth</code>, <code>lanes</code>, <code>shoulder</code>, <code>speedLimit</code>, <code>bank</code>, <code>elevation</code>, <code>smoothing</code>, <code>lift</code>, <code>conform</code>, <code>bed</code>, <code>falloff</code>, <code>skirt</code>, <code>resolution</code>, <code>uvScale</code>, <code>material</code>, <code>tint</code>, <code>collider</code>, <code>width</code>), plus <code>name</code> (the parent entity's name), <code>drive</code> (<code>"right"</code> or <code>"left"</code>: which side of an edge its forward lanes keep to), <code>terrain</code> (a Terrain entity or a list of them, <code>false</code> for none; nil uses every Terrain in the scene) and <code>parent</code> (an entity the built surfaces go under).</li> </ul> <pre><code class="language-lua">local net = paths.new({ lanes = { forward = 2, backward = 2 }, speedLimit = 22 }) </code></pre> <h2 id="modules-pathnetwork-node">modules/pathNetwork/node</h2> <pre><code class="language-lua">node(self: Network, id: any): any </code></pre> <p>A node as data: <code>{ id, x, y, z, height, edges }</code>, <code>height</code> being the height its surface was built at.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The node's id.</li> </ul> <h2 id="modules-pathnetwork-nodeids">modules/pathNetwork/nodeIds</h2> <pre><code class="language-lua">nodeIds(self: Network): { string } </code></pre> <p>The ids of every node, sorted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> </ul> <h2 id="modules-pathnetwork-removeedge">modules/pathNetwork/removeEdge</h2> <pre><code class="language-lua">removeEdge(self: Network, id: any) </code></pre> <p>Remove an edge. Its surface goes and the ground under it comes back on the next <code>build</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The edge's id.</li> </ul> <h2 id="modules-pathnetwork-removenode">modules/pathNetwork/removeNode</h2> <pre><code class="language-lua">removeNode(self: Network, id: any) </code></pre> <p>Remove a node and every edge at it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The node's id.</li> </ul> <h2 id="modules-pathnetwork-route">modules/pathNetwork/route</h2> <pre><code class="language-lua">route(self: Network, from: any, to: any, opts: any?): any? </code></pre> <p>The way along the lanes from one place to another: the lanes in order, the cost, and the polyline a follower drives (lane centres joined through each node by connector curves).</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>from</code> <code>any</code> <em>(optional)</em> — A world point (its nearest lane) or <code>{ lane, distance }</code>.</li> <li><code>to</code> <code>any</code> <em>(optional)</em> — A world point or <code>{ lane, distance }</code>.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ by = "distance" | "time" }</code>: the shortest way, or the quickest at each lane's speed limit.</li> </ul> <pre><code class="language-lua">local r = net:route(start, goal); follower:follow(r.points) </code></pre> <h2 id="modules-pathnetwork-sample">modules/pathNetwork/sample</h2> <pre><code class="language-lua">sample(self: Network, id: any, s: number): any </code></pre> <p>The surface an edge was built with, at a distance along its centreline: the position, the heading, the right and up of the banked surface.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The edge's id.</li> <li><code>s</code> <code>number</code> — Metres from the edge's first node, held to the built surface.</li> </ul> <h2 id="modules-pathnetwork-setedge">modules/pathNetwork/setEdge</h2> <pre><code class="language-lua">setEdge(self: Network, id: any, fields: any) </code></pre> <p>Change an edge: its control points (<code>points</code>, replaced whole) or any edge setting. <code>clear = { "bank", ... }</code> drops settings the edge stated, so it reads the network's again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The edge's id.</li> <li><code>fields</code> <code>any</code> <em>(optional)</em> — The fields to change.</li> </ul> <pre><code class="language-lua">net:setEdge(e, { points = { { 20, 0, 30 } }, lanes = { forward = 2, backward = 0 } }) </code></pre> <h2 id="modules-pathnetwork-setnode">modules/pathNetwork/setNode</h2> <pre><code class="language-lua">setNode(self: Network, id: any, fields: any) </code></pre> <p>Change a node's fields (<code>elevation</code>, <code>junctionRadius</code>, <code>x</code>, <code>y</code>, <code>z</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> <li><code>id</code> <code>any</code> <em>(optional)</em> — The node's id.</li> <li><code>fields</code> <code>any</code> <em>(optional)</em> — The fields to change.</li> </ul> <h2 id="modules-pathnetwork-spec">modules/pathNetwork/spec</h2> <pre><code class="language-lua">spec(self: Network): any </code></pre> <p>The network as plain data: every node and edge with the fields they state, the form <code>apply</code> takes and a component can store.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Network</code></li> </ul> <h2 id="modules-persist-readme">modules/persist/README</h2> <pre><code class="language-lua">require("@builtin::modules.persist") -- persist </code></pre> <p>Usage: local persist = require("@builtin::modules.persist")</p> <h2 id="modules-persist-origin-readme">modules/persist_origin/README</h2> <pre><code class="language-lua">require("@builtin::modules.persist.origin") -- persist_origin </code></pre> <p>Provenance / origin-context layer for the persist (create-flow) system. origin kinds: "execute" — an agent call (the engine's authoring window is open) "scene" — scene loader / entrypoint onLoad (<code>__scene_load.inProgress()</code>) "component" — a script-component lifecycle callback (awake/update/...) "engine" — engine-driven code that is none of the above (an assetType behavior composing an avatar, a bundle exploding its hierarchy). The DEFAULT: a surface is authored only while the authoring window is open, so a surface added later classifies as code without being enumerated anywhere.</p> <p>Usage: local persist_origin = require("@builtin::modules.persist.origin")</p> <h2 id="modules-persist-player-camera-readme">modules/persist_player_camera/README</h2> <pre><code class="language-lua">require("@builtin::modules.persist.player_camera") -- persist_player_camera </code></pre> <p>Freeze the LIVE player + camera into a scene's player/camera config. The persist north star is "what you see is what you get": the freeze PRESERVES the exact live state, it never synthesizes. PLAYER. The live player body is <code>layers.active.players.localPlayer.avatar</code> — whatever entity is in that slot right now, however it was built (procedurally spawned, instantiated, hand-assembled). The scene CONFIG (<code>player.avatar_<mode></code>) is a BUNDLE ref the spawner instantiates on the next load. So the freeze CONVERTS the live avatar entity-tree into a bundle and points the config at it: * If the live avatar is an UNMODIFIED instance of an existing bundle (its <code>bundleProvenance</code> carries exactly one source bundle with no added components/entities), reference THAT bundle — don't duplicate. * Otherwise compose the live tree into a bundle (new on first freeze, re-composed in place on re-freeze) and reference it. The entity-tree -> bundle work goes through the bundle assetType's own composeTemplate path: first freeze = one-step <code>asset.create("bundle", name, { entity = ref })</code> (onCreate composes the live tree), re-freeze = <code>bundleRef:update(entityId)</code> in place. Both capture LIVE component state via serialized component snapshots — exactly what's on screen. CAMERA. The live primary camera's behavior is a COMPONENT (the agent authors one to drive the camera); the freeze references it as <code>camera.behavior_<mode></code>. Camera parameters (fov/near/far/follow) are baked into the live Camera component and preserved with it. No synthesis. Bundle CREATION is side-effecting, so it runs from <code>confirm()</code>, never <code>plan()</code> (a cancelled plan must leave no orphan bundle in /source).</p> <p>Usage: local persist_player_camera = require("@builtin::modules.persist.player_camera")</p> <h2 id="modules-persist-serializer-readme">modules/persist_serializer/README</h2> <pre><code class="language-lua">require("@builtin::modules.persist.serializer") -- persist_serializer </code></pre> <p>Live-state serializer driver for persist. The missing half of the scene-save story: scene_saver.serializeEntity already turns a LIVE entity into the v6 per-entity body, but it is only ever fed ids from the EDIT-mode dirty-mark queue (empty in play). This driver supplies the play-mode side — it ENUMERATES the active layer's live entities, filters them by PROVENANCE (execute-origin only, via persist.origin) + authored-ness (scene_saver's spawner-managed / temporary filter), and assembles a loadable v6 scene body. Lighting stays ENTITY-PRIMARY: lighting comes from Light components on entities, which serialize as normal entities — so we deliberately do NOT emit scene_saver's global <code>lighting</code> manifest block (it would compete with / override the entity lights). Only the player + camera config blocks are carried over.</p> <p>Usage: local persist_serializer = require("@builtin::modules.persist.serializer")</p> <h2 id="modules-playersetupvalidation-readme">modules/playerSetupValidation/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.playerSetupValidation") -- playerSetupValidation </code></pre> <p>Agent-facing validation for authored player setups. A player-spawn scene is built from PlayerSpawn entities (each naming a PlayerPrototype subtree to instantiate on join) and PlayerPrototype roots (marked PrototypeOnly, with a <code>body</code> and a <code>camera</code> field each naming a DESCENDANT of the prototype — the entity adopted as the avatar and the entity carrying the player's Camera). The prototype is cloned as a self-contained subtree, so both refs must resolve inside it and it may hold no camera other than the one <code>camera</code> names. checkEntity inspects one such entity and returns human-readable messages that name what is wrong and what to do about it; checkScene validates the scene's player intent against its PlayerSpawn / Camera counts. checkActiveScene runs the full rule set across the live active-layer scene, resolving each spawn's prototype wherever the materialisation holds it — the live entity in edit, the captured authored template in play; checkSceneJson runs it statically over a scene.json document — the path the scene assetType's <code>validate</code> hook uses so <code>asset.validate</code>, <code>worldValidation</code>, and the <code>world.push</code> gate all catch a broken player setup at authoring time.</p> <p>Usage: local playerSetupValidation = require("@builtin::modules.api.engine.playerSetupValidation")</p> <h2 id="modules-playersetupvalidation-checkactivescene">modules/playerSetupValidation/checkActiveScene</h2> <pre><code class="language-lua">checkActiveScene(): { { entity: string, message: string } } </code></pre> <p>Gather every player-setup validation message for the live active-layer scene: the per-entity rules across all PlayerSpawn / PlayerPrototype entities, the competing-camera rule, and the scene-intent rule. Returns a flat list an agent can read to see what to fix. The verdict belongs to the settled scene, so the call holds while a scene load or a mode-flip transition is rebuilding the live tree, and judges what the rebuild lands on.</p> <h2 id="modules-playersetupvalidation-checkentity">modules/playerSetupValidation/checkEntity</h2> <pre><code class="language-lua">checkEntity(entityId: string): { string } </code></pre> <p>Validate a single PlayerSpawn or PlayerPrototype entity, returning agent-facing messages naming what is wrong and what to do. An entity carrying neither component (or one that does not exist) yields no messages.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string</code> — The entity to inspect.</li> </ul> <h2 id="modules-playersetupvalidation-checkscene">modules/playerSetupValidation/checkScene</h2> <pre><code class="language-lua">checkScene(opts: { playerIntent: string, spawnCount: number, cameraCount: number }): { string } </code></pre> <p>Validate a scene's player intent against its PlayerSpawn / Camera counts, returning agent-facing messages. A "spawns" scene with no PlayerSpawn, or a "none" scene with no Camera, yields a message; every other combination is clean.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ playerIntent: string, spawnCount: number, cameraCount: number }</code> — <code>{ playerIntent: string, spawnCount: number, cameraCount: number }</code>.</li> </ul> <h2 id="modules-playersetupvalidation-checkscenejson">modules/playerSetupValidation/checkSceneJson</h2> <pre><code class="language-lua">checkSceneJson(sceneJson: { [string]: any }): { { code: string, severity: string, message: string } } </code></pre> <p>Validate a decoded scene.json document statically: the full player-setup rule set (per-entity, competing-camera, scene-intent) run over the scene's authored entity tree without loading it. This is what the scene assetType's <code>validate</code> hook calls, so <code>asset.validate</code> / <code>worldValidation</code> / the <code>world.push</code> gate all report a broken player setup at authoring time.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sceneJson</code> <code>{ [string]: any }</code> — The decoded scene.json table (<code>{ player, version, entities }</code>).</li> </ul> <h2 id="modules-playersetupvalidation-playreadinessproblems">modules/playerSetupValidation/playReadinessProblems</h2> <pre><code class="language-lua">playReadinessProblems(): { { entity: string, message: string } } </code></pre> <p>The player-setup problems that must block a flip into play: the per-entity spawn/prototype rules (body + camera refs set, resolving to descendants, a single referenced camera) and the scene-intent rule, over the LIVE active scene. A "spawns" scene with none of these problems is ready to play. The competing-camera rule is deliberately excluded — the editor's own free-fly camera is a live viewport camera outside every prototype, so running it here would false-positive on every edit session; that rule stays a static / publish-time concern. Empty for a non-"spawns" scene (no player requirement), and empty while the active scene is still being materialised — the verdict belongs to the settled scene, so it waits for the layer to finish loading and for any mode-flip transition to converge.</p> <h2 id="modules-player-lifecycle-readme">modules/player_lifecycle/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.player_lifecycle") -- player_lifecycle </code></pre> <p>Bridges the UserIdentity component's <code>awake / onDestroy</code> into the per-scene players registry. Installed by the prelude. The UserIdentity component requires this module and calls <code>playerJoined(eid)</code> in awake, <code>playerLeft(eid)</code> in onDestroy, and <code>localAvatarBound(eid, av)</code> when the local avatar slot fills. Each routes into <code>layers.active.players._addPlayer / _removePlayer / _onLocalAvatarBound</code>, which update the per-scene set AND fire <code>onPlayerJoined / onPlayerLeft</code> in one atomic step. onLocalReady is fired separately when the joining entity matches <code>world.connectedUsers.localUser.entity</code>. <code>install()</code> also registers <code>__layers_*</code> dispatch channels the player_spawner uses.</p> <p>Usage: local player_lifecycle = require("@builtin::modules.api.engine.player_lifecycle")</p> <h2 id="modules-player-prototype-spawn-readme">modules/player_prototype_spawn/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.player_prototype_spawn") -- player_prototype_spawn </code></pre> <p>The deterministic spawn core for authored player prototypes. A PlayerSpawn entity names a PlayerPrototype subtree to instantiate; spawnFor clones that subtree, activates and reveals the clone, prunes clone-subtree nodes whose networkScope excludes the caller's owner/authority role, marks the clone RuntimeOnly, and turns the clone ROOT into the joining user's internal identity: the root spawns under the name every user identity carries ("user"), leaves the PlayerPrototype / PlayerSpawn markers behind, and takes the UserIdentity component, so the root plugs into the existing players registry / join-leave / ownership / sync as a first-class local player. The clone root's authored children are detached to standalone world entities — nothing is parented to the identity. The joining user's avatar is the body named by the root's <code>PlayerPrototype.body</code> ref; it is positioned at the PlayerSpawn's world transform (placement <code>at_spawn_transform</code>) and bound by assigning the identity's <code>avatar</code>, which links it back through <code>PlayerAvatar.owner</code>. The clone's Camera node follows the bound body. Runtime provenance attributes (source prototype, owner user, owner player) are stamped on the root and read back with runtimeSpawnedInfo. The attributes are runtime-only: they live on the live entity and are not serialized into the world. installJoinHook wires spawnFor to a scene's user-join event so a joining user is placed from a PlayerSpawn automatically.</p> <p>Usage: local player_prototype_spawn = require("@builtin::modules.api.engine.player_prototype_spawn")</p> <h2 id="modules-player-prototype-spawn-applynetworkscope">modules/player_prototype_spawn/applyNetworkScope</h2> <pre><code class="language-lua">applyNetworkScope(cloneRootId: string, isOwner: boolean, isAuthority: boolean) </code></pre> <p>Prune a clone subtree by each node's networkScope against the caller's role. Walks the subtree from <code>cloneRootId</code>; a node scoped OwnerOnly is despawned when the caller is not the owner, AuthorityOnly when the caller is not the authority, and Replicated (or any other value) is kept.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cloneRootId</code> <code>string</code> — The clone's root entity id.</li> <li><code>isOwner</code> <code>boolean</code> — Whether the caller owns this clone.</li> <li><code>isAuthority</code> <code>boolean</code> — Whether the caller is the simulation authority for this clone.</li> </ul> <h2 id="modules-player-prototype-spawn-authordefault">modules/player_prototype_spawn/authorDefault</h2> <pre><code class="language-lua">authorDefault(): { [string]: string } </code></pre> <p>Author the canonical default player setup into the active scene — the same shape the default world and the static_player canonical scene ship: a PrototypeOnly prototype whose body adopts the humanoid avatar and whose OwnerOnly camera rig runs the orbital follow behavior, plus a spawn at the origin. Returns the authored entity ids. This is the single builder the scene.player tool set to "spawns" and the "player" scene template both resolve to, so a joining user's avatar always replaces the same authored body.</p> <h2 id="modules-player-prototype-spawn-capturetemplatesfromentities">modules/player_prototype_spawn/captureTemplatesFromEntities</h2> <pre><code class="language-lua">captureTemplatesFromEntities(entities: { any }) </code></pre> <p>Build the prototype-template registry from a scene's authored entity records (the parsed scene data, not live entities). This is the primary capture path: it is independent of scene-load order and runtime composition, so it captures the clean authored subtree (no composed avatar) and works in the runtime profile, which boots straight to play. Called by the scene loader for v7 scenes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entities</code> <code>{ any }</code> — The scene's authored entity records (each <code>{ id, name, parent, networkScope, renderLayer, transform, components }</code>).</li> </ul> <h2 id="modules-player-prototype-spawn-capturedtemplate">modules/player_prototype_spawn/capturedTemplate</h2> <pre><code class="language-lua">capturedTemplate(prototypeId: string): any </code></pre> <p>The captured authored subtree for a PlayerPrototype — the clone source <code>spawnFor</code> instantiates for each joining player, keyed by the prototype's authored entity id (the id a PlayerSpawn's <code>prototype</code> field carries). Each node is <code>{ id, name, participation, networkScope, renderLayer, position, rotation, scale, components = { [type] = data }, namedComponents?, children }</code>: <code>components</code> holds each type's anonymous instance, and <code>namedComponents = { [type] = { [instanceName] = data } }</code> the instances added under a name. Where the materialisation keeps authored prototype subtrees out of the live scene — play — this template is the authored prototype, and it is the subtree <code>spawnFor</code> clones for each joining player.</p> <p><strong>Parameters</strong></p> <ul> <li><code>prototypeId</code> <code>string</code> — The PlayerPrototype root's authored entity id.</li> </ul> <pre><code class="language-lua">local proto = player_prototype_spawn.capturedTemplate(spawn.prototype.id) </code></pre> <h2 id="modules-player-prototype-spawn-choosespawn">modules/player_prototype_spawn/chooseSpawn</h2> <pre><code class="language-lua">chooseSpawn(ctx): (string?, { [string]: any }?, string?) </code></pre> <p>Pick the PlayerSpawn-carrying entity to spawn from. Enumerates entities carrying the PlayerSpawn component in the joining user's ROOT scene, skipping any that live in an additive overlay layer (editor UI, HUD scenes). When the root scene resolves (<code>ctx.rootSceneGuid</code>, else <code>layers.active.guid</code>), only spawns in that scene's layer are considered; otherwise every non-overlay spawn is eligible. Spawns with no layer attribution yet belong to the world root and stay eligible either way. Honors an optional <code>ctx.spawnId</code> override (used for deterministic selection), otherwise returns the first matching spawn.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ctx</code> <code>any</code> <em>(optional)</em> — A table; <code>ctx.spawnId</code> optionally names the spawn entity to select, <code>ctx.rootSceneGuid</code> optionally names the scene layer to scope the search to.</li> </ul> <h2 id="modules-player-prototype-spawn-clearjoinhook">modules/player_prototype_spawn/clearJoinHook</h2> <pre><code class="language-lua">clearJoinHook(guid: string) </code></pre> <p>Clear a scene's join-hook flag. Called when a "spawns" scene unloads so the once-registered connect / play-entry handlers stand down (they no-op while no wired scene remains). Idempotent for an unknown guid.</p> <p><strong>Parameters</strong></p> <ul> <li><code>guid</code> <code>string</code> — The scene guid passed to installJoinHook.</li> </ul> <h2 id="modules-player-prototype-spawn-heldastemplate">modules/player_prototype_spawn/heldAsTemplate</h2> <pre><code class="language-lua">heldAsTemplate(entityId: string): (any, string?) </code></pre> <p>The captured template node holding an authored entity the live scene leaves out. Where the materialisation keeps authored PlayerPrototype subtrees out of the scene (play), every entity of such a subtree is held by its prototype's template instead, carrying the authored state the scene's records give it, unsaved edits included. Answers that node and the prototype's id; nil for an entity no template holds, and wherever prototypes materialise as live entities (edit).</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string</code> — The authored entity's id.</li> </ul> <pre><code class="language-lua">local node, prototypeId = player_prototype_spawn.heldAsTemplate(row.id) </code></pre> <h2 id="modules-player-prototype-spawn-installjoinhook">modules/player_prototype_spawn/installJoinHook</h2> <pre><code class="language-lua">installJoinHook(sceneProxy, opts: { materialisedForPlay: boolean? }?): boolean </code></pre> <p>Wire spawnFor to the world's connected-user join event. When a user connects, the hook picks a PlayerSpawn and instantiates that user's prototype instance (internal identity + avatar + camera-follow) via spawnFor. The trigger is <code>world.connectedUsers.onConnect</code> — the WORLD-level "a user joined the session" event — not the room players registry, so the internal identity the clone becomes (which folds into that registry) does not re-trigger a spawn. Entering play spawns every already-connected user (their onConnect fired in edit, ignored then). A scene wired while ALREADY in play — the runtime profile boots straight into play, or a scene swapped in mid-play — gets that same sweep immediately, since no play flip follows to trigger it. Every spawn path is per-user idempotent: a user who already owns a live clone is skipped, so overlapping paths and re-flips never produce a second player. A scene whose load placed its prototypes for edit (<code>opts.materialisedForPlay = false</code>, a flip to play that landed part-way through its load) takes no sweep: the flip rebuilds that scene for play, and the rebuilt scene's hook places every connected user. Idempotent per scene proxy: a second call for the same scene installs nothing further.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sceneProxy</code> <code>any</code> <em>(optional)</em> — A non-additive Scene proxy.</li> <li><code>opts</code> <code>{ materialisedForPlay: boolean? }?</code> <em>(optional)</em> — Optional <code>{ materialisedForPlay: boolean? }</code>: whether the scene's load placed its prototypes as play's clone sources. Absent reads as true.</li> </ul> <h2 id="modules-player-prototype-spawn-runtimespawnedinfo">modules/player_prototype_spawn/runtimeSpawnedInfo</h2> <pre><code class="language-lua">runtimeSpawnedInfo(id: string): { [string]: any }? </code></pre> <p>Read back the runtime provenance stamped on a clone root by spawnFor.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The clone root entity id.</li> </ul> <h2 id="modules-player-prototype-spawn-spawnfor">modules/player_prototype_spawn/spawnFor</h2> <pre><code class="language-lua">spawnFor(ctx): string? </code></pre> <p>Spawn a player instance for a joining user from the chosen PlayerSpawn's prototype. Chooses a spawn (honoring <code>ctx.spawnId</code>), resolves and validates its prototype, clones the prototype subtree, activates and reveals the clone, prunes it by networkScope against the caller's owner/authority role (both default true), marks the clone RuntimeOnly, stamps provenance attributes, places the clone at the spawn's world transform, and registers it with the prototype lifecycle so it is despawned on the return to edit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ctx</code> <code>any</code> <em>(optional)</em> — <code>{ userId, playerEntityId?, spawnId?, isOwner?, isAuthority? }</code>.</li> </ul> <h2 id="modules-player-prototype-spawn-storeprototypetemplate">modules/player_prototype_spawn/storePrototypeTemplate</h2> <pre><code class="language-lua">storePrototypeTemplate(prototypeId: string) </code></pre> <p>Capture a PlayerPrototype's authored subtree into the template registry. Called by PlayerPrototype.awake (before its Asset composes and before it deactivates) so spawnFor can instantiate the authored structure per player.</p> <p><strong>Parameters</strong></p> <ul> <li><code>prototypeId</code> <code>string</code> — The PlayerPrototype root entity id.</li> </ul> <h2 id="modules-player-prototype-spawn-userhasspawnedplayer">modules/player_prototype_spawn/userHasSpawnedPlayer</h2> <pre><code class="language-lua">userHasSpawnedPlayer(userId: string?): boolean </code></pre> <p>Whether a live clone spawned by spawnFor already carries this user's owner provenance. Scans the live entities for a root whose <code>ownerUserId</code> attribute matches. The idempotency guard the auto-spawn paths use so a user who already has a spawned player never gets a second one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>userId</code> <code>string?</code> <em>(optional)</em> — The joining user's account id.</li> </ul> <h2 id="modules-player-spawner-readme">modules/player_spawner/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.player_spawner") -- player_spawner </code></pre> <p>DEPRECATED: v7 scenes place players through the PlayerSpawn / PlayerPrototype flow. This engine-default-Player avatar path serves legacy v6 scenes only; M.ensure stands down (returns early) for any scene carrying a string <code>playerIntent</code>.</p> <p>Binds the per-mode avatar bundle to the engine-default identity entity on non-additive <code>layers.onLoad</code>, for legacy v6 scenes. The avatar ref resolves as: per-scene <code>settings.player.avatar_<mode></code> when set, else the world default <code>world.avatar_default_<mode></code>. Missing-but-required is <code>log.error</code> + skip — no hardcoded fallback bundle. A single <code>avatar</code> slot combines visual + controller in one bundle. It spawns a fresh body entity from that bundle, then binds it by assigning the identity's <code>avatar</code> field, which marks the body synced + PlayerOwned and attaches its <code>PlayerAvatar</code> link. Scene-level <code>avatar_<mode> = ""</code> is the explicit opt-out — the scene builds its own body in <code>entrypoint.luau::onLocalReady</code>.</p> <p>Usage: local player_spawner = require("@builtin::modules.api.engine.player_spawner")</p> <h2 id="modules-players-readme">modules/players/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.players") -- players </code></pre> <p>Per-scene players registry. Tied to a non-additive Scene proxy. Reached as <code>layers.active.players</code> or <code>layers.find(name).players</code>. Returns curated player handles — never entity or component proxies — so the caller reads/writes player data (<code>.userId</code>, <code>.displayName</code>) and reaches the body via <code>player.avatar</code> (an entity ref) uniformly across all consumers. Both colon (<code>p:onJoin(cb)</code>) and dot (<code>p.onJoin(cb)</code>) call styles are supported on every method; the registry is a namespace surface, not an object, so neither style is canonical. Additive layers do NOT carry a players surface today (multiplayer support for players living inside additive overlays is a follow-up — see the layers.module additive-scene comment). <code>layers.find(<additive>).players</code> returns nil; this module is only instantiated on the root scene proxy.</p> <p>Usage: local players = require("@builtin::modules.api.engine.players")</p> <h2 id="modules-plugins-readme">modules/plugins/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.plugins") -- plugins (also available as global 'plugins') </code></pre> <p>What the plugin host loaded, and the one way to call a plugin. Public Luau surface over the <code>__plugins</code> Internal FFI namespace.</p> <p>Usage: local plugins = require("@builtin::modules.api.engine.plugins") Also available as global: plugins</p> <h2 id="modules-plugins-awaitingperson">modules/plugins/awaitingPerson</h2> <pre><code class="language-lua">awaitingPerson(plugin: string): boolean </code></pre> <p>Whether a call to this plugin is waiting on a person to answer the engine's consent dialog.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plugin</code> <code>string</code> — The plugin asset's library identity, written as a LITERAL.</li> </ul> <pre><code class="language-lua">if plugins.awaitingPerson(PLUGIN) then print("waiting for you") end </code></pre> <h2 id="modules-plugins-call">modules/plugins/call</h2> <pre><code class="language-lua">call(plugin: string, method: string, ...: any): any </code></pre> <p>Call a method the plugin at <code>plugin</code> exposes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plugin</code> <code>string</code> — The plugin asset's identity, written as a LITERAL: the same string <code>asset.identity</code> answers for it (<code>motion</code> for a plugin in this world, <code>@publisher::path.name</code> for one in a library), or its path. A computed identity is refused: runtime code has no dynamic requires and a plugin is no exception, because a name the reference deriver cannot read at authoring time is a dependency that never enters the graph.</li> <li><code>method</code> <code>string</code> — A method the plugin's <code>plugin.yaml</code> exposes. One it does not is not callable, whatever the component carries.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local agents = await(plugins.call("@builtin/plugins/dotbot.plugin", "agents")) </code></pre> <h2 id="modules-plugins-isloaded">modules/plugins/isLoaded</h2> <pre><code class="language-lua">isLoaded(plugin: string): boolean </code></pre> <p>Whether the plugin is loaded and answering calls.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plugin</code> <code>string</code> — The plugin asset's identity, written as a LITERAL.</li> </ul> <pre><code class="language-lua">while not plugins.isLoaded("motion") do task.wait(0.1) end </code></pre> <h2 id="modules-plugins-off">modules/plugins/off</h2> <pre><code class="language-lua">off(plugin: string, id: number): boolean </code></pre> <p>Stop a listener <code>plugins.on</code> started.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plugin</code> <code>string</code> — The same identity the listener was started with.</li> <li><code>id</code> <code>number</code> — The id <code>plugins.on</code> answered.</li> </ul> <h2 id="modules-plugins-on">modules/plugins/on</h2> <pre><code class="language-lua">on(plugin: string, listener: (string, any) -> ()): number </code></pre> <p>Listen to the events a plugin raises.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plugin</code> <code>string</code> — The plugin asset's library identity, written as a LITERAL.</li> <li><code>listener</code> <code>(string, any) -> ()</code> — <code>function(eventType, payload)</code> — <code>payload</code> is the event's decoded JSON.</li> </ul> <pre><code class="language-lua">local id = plugins.on("@builtin/plugins/dotbot.plugin", function(kind, body) end) </code></pre> <h2 id="modules-plugins-openurl">modules/plugins/openUrl</h2> <pre><code class="language-lua">openUrl(plugin: string, url: string): any </code></pre> <p>Ask the person to open one of the plugin's own pages.</p> <p><strong>Parameters</strong></p> <ul> <li><code>plugin</code> <code>string</code> — The plugin asset's library identity, written as a LITERAL.</li> <li><code>url</code> <code>string</code> — An https URL whose host the plugin declared under <code>network.hosts</code>. Anywhere else is refused — otherwise this would be an unbounded way to put an address in front of somebody.</li> </ul> <pre><code class="language-lua">await(plugins.openUrl("@builtin/plugins/dotbot.plugin", "https://claude.ai/login")) </code></pre> <h2 id="modules-plugins-status">modules/plugins/status</h2> <pre><code class="language-lua">status(): { [string]: any } </code></pre> <p>What the plugin host loaded, and what refused to load.</p> <pre><code class="language-lua">local report = plugins.status() print(report.started, #report.plugins) </code></pre> <h2 id="modules-postprocess-readme">modules/postprocess/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.postprocess") -- postprocess (also available as global 'postprocess') </code></pre> <p>Fullscreen post-process effects — register / remove / toggle / named-property updates / list the chain / read one effect's declared property schema and the value each property holds. Public Luau surface over the <code>__postprocess</code> Internal FFI namespace.</p> <p>Usage: local postprocess = require("@builtin::modules.api.engine.postprocess") Also available as global: postprocess</p> <h2 id="modules-postprocess-add">modules/postprocess/add</h2> <pre><code class="language-lua">add(name: string, shader: string | AssetRef, opts: PostprocessOpts?): (boolean, string?) </code></pre> <p>Register a fullscreen post-process effect. This call is what puts a pass into the frame — a <code>post-process</code> <code>.shader</code> asset defines an effect, and renders only once registered here. The chain applies the registration on the caller's own stack and the returned boolean is its answer, so a <code>setProperty</code> or <code>setTexture</code> naming the effect in the same call finds it. <code>shader</code> is a <code>.shader</code> asset reference whose <code>shader.wgsl</code> provides <code>fn fragment(in: PostInput) -> vec4<f32></code> and whose <code>properties.yaml</code> declares the effect's properties; the engine generates the group(0) framework + schema-driven group(1) from that schema. Editing that shader afterwards recompiles this effect in place, keeping its enabled state, priority, layer and tuned property values. WGSL text is accepted in place of a shader reference, and then <code>opts.properties</code> is the whole schema — this form is deprecated and will be removed, author the effect as a <code>.shader</code> asset instead. Effects run in priority order (lower first, default 100). A registered effect runs over the live viewport's frame AND over every offscreen one — a capture from a world-space station, one orbiting an entity, one of a named camera, a render-to-texture camera. In each of those the effect's <code>engine.view_proj</code> / <code>engine.prev_view_proj</code> / <code>engine.inv_view_proj</code> are the camera THAT render was drawn from and <code>engine.resolution</code> is that target's own size, so a pass reconstructing world space from <code>zero_scene_depth(uv)</code> reconstructs against the station and lens the capture asked for. An offscreen capture is therefore an oracle for an authored grade: it photographs a chosen station without taking the on-screen camera from whoever else is driving the scene, and a capture's <code>postProcessing = false</code> is the one control that takes the chain off the frame it returns. An offscreen render keeps no view history of its own, so <code>engine.prev_view_proj</code> there holds that same matrix rather than the frame before it, and a pass taking camera motion from the two reads none.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Unique effect name.</li> <li><code>shader</code> <code>string | AssetRef</code> — A resolved <code>shader</code> asset reference, or author WGSL (<code>fn fragment(in: PostInput)</code> only).</li> <li><code>opts</code> <code>PostprocessOpts?</code> <em>(optional)</em> — <code>{ priority = 100, enabled = true, layer = "scene"|"all" (omitted: follows space), space = "light"|"display", properties = {{name, type, default?, min?, max?, textureDefault?}}, passes = {{name, entry, scale?, inputs?}} }</code> — with a shader asset, <code>properties</code> layers over the asset's own schema. <code>layer</code> picks which composited image the effect grades: <code>"scene"</code> runs it before the UI is drawn, over the scene's own light in a float target, so it grades the rendered picture and leaves every widget on screen as authored, and <code>"all"</code> runs it after the UI has landed, over the picture the display transform fitted to the screen, so the interface is graded along with the picture. A call that names no layer takes the one its <code>space</code> belongs to: a <code>"light"</code> effect grades <code>"scene"</code> and a <code>"display"</code> effect grades <code>"all"</code>, so a bloom reads the scene's highlights where they still stand above 1.0 and a lookup table reads code values. Naming <code>"all"</code> is what an effect that must grade the interface says — and on a world several authors share, it grades everyone else's screens too. <code>space</code> names what <code>fragment()</code> reads and returns: <code>"light"</code> (the default) for a threshold/blur/grade on the scene, <code>"display"</code> for a lookup table, dither ladder or authored overlay working on code values. With a shader asset, defaults to what that asset's <code>getSpace()</code> declares. <code>passes</code> names the passes the effect draws, each an entry in the shader, a fraction of the view it draws at, and what it reads. With a shader asset, defaults to what that asset's <code>getPasses()</code> declares; a single-pass effect (the common case) declares none. <code>textureDefault</code> is what a <code>type = "texture"</code> property samples while nothing is bound to it: <code>"white"</code> (1,1,1,1 — the default), <code>"black"</code> (0,0,0,1), <code>"normal"</code> (0.5,0.5,1,1) or <code>"transparent"</code> (0,0,0,0). An effect that lays its texture over the scene wants <code>"transparent"</code>, so the frame is untouched until <code>setTexture</code> binds a texture that exists.</li> </ul> <pre><code class="language-lua">postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader")) postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} }) </code></pre> <h2 id="modules-postprocess-describe">modules/postprocess/describe</h2> <pre><code class="language-lua">describe(name: string): PostprocessDescription? </code></pre> <p>One effect by name, read in full: the chain state <code>postprocess.status()</code> lists for it, and on top of that <code>properties</code> — the schema the effect declared, each entry <code>{ name, type, default?, min?, max?, textureDefault? }</code> in the shape <code>add</code> takes — and <code>values</code>, what each of those properties currently holds. A property's value is the one the last <code>setProperty</code> wrote, or the schema's own default where nothing has written one, and it comes back as a number for a scalar and as the array for a wider value, which is what <code>setProperty</code> takes, so a property read here is written straight back.</p> <p>This is the read-back for a property write. <code>setProperty</code> answers whether the uniform took the value; this answers what the effect holds now, which is the reading a pass that writes its properties every frame needs and the one that tells a mistyped property name from an effect that is not grading. The schema and the values are the engine's own record of the effect — the schema it was registered with and every write the chain accepted into its uniform, the same record <code>/runtime/fx/<name>/meta.json</code> is serialized from. A write the chain refused is not in it, and neither is one made against a property the schema does not declare.</p> <p>Before an effect is registered its schema lives on the <code>.shader</code> asset it will render: <code>asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties()</code> names what that shader declares.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Effect name.</li> </ul> <pre><code class="language-lua">local e = postprocess.describe("vignette") print(if e then e.values.intensity else "not registered") for _, p in ipairs(postprocess.describe("bloom").properties) do print(p.name, p.type, p.min, p.max) end </code></pre> <h2 id="modules-postprocess-list">modules/postprocess/list</h2> <pre><code class="language-lua">list(): { string } </code></pre> <p>List all registered post-process effect names in renderer priority order (lower priority runs first).</p> <pre><code class="language-lua">for _, n in ipairs(postprocess.list()) do print(n) end </code></pre> <h2 id="modules-postprocess-remove">modules/postprocess/remove</h2> <pre><code class="language-lua">remove(name: string): boolean </code></pre> <p>Queue removal of a post-process effect. Takes effect on the next frame. Removing a name that isn't registered is a silent no-op.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Effect name to remove.</li> </ul> <pre><code class="language-lua">postprocess.remove("vignette") </code></pre> <h2 id="modules-postprocess-setenabled">modules/postprocess/setEnabled</h2> <pre><code class="language-lua">setEnabled(name: string, enabled: boolean): boolean </code></pre> <p>Queue an enable/disable toggle on a registered post-process effect. Targeting an unknown name is a silent no-op.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Effect name.</li> <li><code>enabled</code> <code>boolean</code> — True to enable, false to disable.</li> </ul> <pre><code class="language-lua">postprocess.setEnabled("bloom", false) </code></pre> <h2 id="modules-postprocess-setproperty">modules/postprocess/setProperty</h2> <pre><code class="language-lua">setProperty(name: string, prop: string, value: (number | { number })): boolean </code></pre> <p>Set a named material property on a registered post-process effect. The property must be declared in the effect's <code>properties</code> schema; read in WGSL as <code>material.<prop></code>. <code>value</code> is a number or a number array (vec/color).</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Effect name.</li> <li><code>prop</code> <code>string</code> — Declared property name.</li> <li><code>value</code> <code>(number | { number })</code> — Number or array of numbers.</li> </ul> <pre><code class="language-lua">postprocess.setProperty("vignette", "intensity", 0.6) </code></pre> <h2 id="modules-postprocess-setsampler">modules/postprocess/setSampler</h2> <pre><code class="language-lua">setSampler(name: string, opts: { [string]: any }): boolean </code></pre> <p>Configure the per-effect user sampler shared by the effect's declared texture properties. opts.filter = "linear" (default) or "nearest". opts.wrap (alias .address) = "clamp" (default), "repeat", or "mirror" — applied to all axes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Effect name.</li> <li><code>opts</code> <code>{ [string]: any }</code> — <code>{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }</code>.</li> </ul> <pre><code class="language-lua">postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" }) </code></pre> <h2 id="modules-postprocess-settexture">modules/postprocess/setTexture</h2> <pre><code class="language-lua">setTexture(name: string, prop: string, path: string): boolean </code></pre> <p>Bind a texture to one of an effect's declared <code>texture</code> properties. Declare it in <code>properties</code> (<code>{ name = "noise", type = "texture" }</code>) and sample in WGSL as <code>textureSample(noise, noise_sampler, in.uv)</code>. <code>path</code> is any TextureCache-resolvable spec (<code>@builtin::textures.foo</code>, <code>color:1,0,0</code>, <code>default:white</code>, a render-target name, ...). A path whose texture has not reached the GPU yet — one this same script created — is held and bound as soon as it does; <code>postprocess.status()</code> reports it under <code>pendingTextures</code> until then.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Effect name.</li> <li><code>prop</code> <code>string</code> — Declared texture-property name.</li> <li><code>path</code> <code>string</code> — Texture path / spec.</li> </ul> <pre><code class="language-lua">postprocess.setTexture("__vsky_composite", "cloud", "cloud_target") </code></pre> <h2 id="modules-postprocess-status">modules/postprocess/status</h2> <pre><code class="language-lua">status(): { PostprocessStatus } </code></pre> <p>Every registered effect in chain order with the state that decides whether it reaches the frame — enabled flag, priority, layer, the shader's compile error when it has one, the <code>.shader</code> asset it renders when it was registered from one, the texture each declared slot is bound to (<code>textures</code>) and the bindings still waiting for their texture (<code>pendingTextures</code>). This is what the renderer draws with, so a survey of the chain answers "is this one affecting the picture right now?" without capturing a frame and reading pixels.</p> <p>An effect this script has just registered is listed with <code>pending = true</code> until the renderer publishes it, since a registration is queued for the next frame.</p> <pre><code class="language-lua">for _, e in ipairs(postprocess.status()) do print(e.name, e.enabled, e.layer, e.error) end </code></pre> <h2 id="derive-readme">modules/preview.derive/README</h2> <pre><code class="language-lua">require("@builtin::modules.preview.derive") -- preview.derive </code></pre> <p>The worker entries of the <code><type>.preview</code> derive hooks: the fingerprint of the content a preview pictures. The picture itself renders on the engine thread, through <code>preview.renderForReservation</code>.</p> <p>Usage: local preview.derive = require("@builtin::modules.preview.derive")</p> <h2 id="derive-compute">modules/preview.derive/compute</h2> <pre><code class="language-lua">compute(_args: any): any </code></pre> <p>Raises: a preview renders on the engine thread.</p> <p><strong>Parameters</strong></p> <ul> <li><code>_args</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="derive-fingerprint">modules/preview.derive/fingerprint</h2> <pre><code class="language-lua">fingerprint(args: any): string </code></pre> <p>The fingerprint of the content <code>args.snapshot</code> holds: the digest of its fingerprint lines, the value <code>preview.inputsFingerprint</code> states for the same content.</p> <p><strong>Parameters</strong></p> <ul> <li><code>args</code> <code>any</code> <em>(optional)</em> — <code>{ snapshot }</code>.</li> </ul> <h2 id="code-readme">modules/proc.code/README</h2> <pre><code class="language-lua">require("@builtin::systems.procgen.proc.code") -- proc.code </code></pre> <p>A graph's source as code to edit. A graph and a node are the code of their <code>init.luau</code> (<code>proc.define(function(g) ... end)</code>); this module reads that code as statements and values and makes the edits the procgen tools offer on it: lift a run of statements into a node of its own, write a node's body out in place of a call to it, call an existing node in place of a run of statements, set and unset the arguments of a call, add, remove, rename and repoint calls, and declare inputs and outputs. Each edit answers with new source text and leaves the writing to its caller; <code>build</code> builds a source text the way loading it would, without writing it.</p> <p>A SECTION is a run of whole statements of the builder function's body, named by the lines it spans. The values it READS are the locals declared before it in the body that its code names, and the values it MAKES are the locals it declares that the code after it names. Lifting the section makes the first the new node's inputs and the second its outputs, and replaces the section with one call of the node, so the graph is built from the same calls as before. A CALL is a statement <code>local x = g.<name>(...)</code> or <code>g.<name>(...)</code>, addressed by the local it declares or by a line it spans. Its arguments are a table naming inputs (<code>{ count = 8 }</code>) or values in the node's input order.</p> <p>Usage: local proc.code = require("@builtin::systems.procgen.proc.code")</p> <h2 id="code-addcall">modules/proc.code/addCall</h2> <pre><code class="language-lua">addCall(src: string, name: string, as: string?, args: { [string]: any }?, after: any?): string </code></pre> <p>Add a call statement <code>local <as> = <param>.<name>(<args>)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>name</code> <code>string</code> — The node to call.</li> <li><code>as</code> <code>string?</code> <em>(optional)</em> — The local the call declares (nil for a call whose outputs nothing reads).</li> <li><code>args</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Input name to value (<code>{ expr = "..." }</code> for code).</li> <li><code>after</code> <code>any?</code> <em>(optional)</em> — The call or line the new one follows (default: after the last statement before the outputs).</li> </ul> <h2 id="code-awaitregistered">modules/proc.code/awaitRegistered</h2> <pre><code class="language-lua">awaitRegistered(path: string) </code></pre> <p>Wait until the node written at <code>path</code> is registered, so a graph calling it cooks: the node type registers a node from its source in the frames after the write.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The <code>.procNode</code> folder.</li> </ul> <h2 id="code-build">modules/proc.code/build</h2> <pre><code class="language-lua">build(src: string, path: string): (any?, string?) </code></pre> <p>Build the graph a source text describes, as loading it from <code>path</code> would, without writing it: the static check a load holds a source to, then the build. Calls by <code>~</code> resolve from <code>path</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The source text.</li> <li><code>path</code> <code>string</code> — The folder the source is (or will be) the entry of.</li> </ul> <h2 id="code-callname">modules/proc.code/callName</h2> <pre><code class="language-lua">callName(graphPath: string, nodePath: string): string </code></pre> <p>The name a graph at <code>graphPath</code> calls the node at <code>nodePath</code> by: <code><box>.<name></code> for a node in a nodebox, <code>~/<path></code> when both sit under one root, the node's path otherwise.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graphPath</code> <code>string</code> — The calling graph's folder.</li> <li><code>nodePath</code> <code>string</code> — The node's folder.</li> </ul> <h2 id="code-calleetext">modules/proc.code/calleeText</h2> <pre><code class="language-lua">calleeText(param: string, name: string): string </code></pre> <p>The text a graph calls a node by: <code>g.points.disc</code> for a name, <code>g["~/parts/crack"]</code> for a path.</p> <p><strong>Parameters</strong></p> <ul> <li><code>param</code> <code>string</code> — The builder function's parameter (<code>g</code>).</li> <li><code>name</code> <code>string</code> — The name, <code>~</code> path, path, identity or guid.</li> </ul> <h2 id="code-calls">modules/proc.code/calls</h2> <pre><code class="language-lua">calls(src: string): { { line: number, target: { string }, callee: string } } </code></pre> <p>Every call statement of a graph's body, at any depth: <code>{ line, target, callee }</code> per call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> </ul> <h2 id="code-inline">modules/proc.code/inline</h2> <pre><code class="language-lua">inline(src: string, address: any, nodeSrc: string, nodePath: string, hostPath: string?): Inlined </code></pre> <p>Write the body of the node a call names out in place of the call: its inputs become locals holding what the call passed (or the defaults it declares), its outputs the locals the call declared, all in a <code>do ... end</code> block so the node's own locals stay its own.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>address</code> <code>any</code> <em>(optional)</em> — The local the call declares, or a line it spans.</li> <li><code>nodeSrc</code> <code>string</code> — The node's source.</li> <li><code>nodePath</code> <code>string</code> — The node's folder.</li> <li><code>hostPath</code> <code>string?</code> <em>(optional)</em> — The graph's folder.</li> </ul> <h2 id="code-lift">modules/proc.code/lift</h2> <pre><code class="language-lua">lift(src: string, path: string, from: number, to: number, opts: { call: string, description: string }): Lifted </code></pre> <p>Lift the statements on lines <code>from</code>-<code>to</code> of a graph's body into a node of their own. The node's inputs are the locals declared before the section that it reads (a graph input's declaration is carried over as it is written), its outputs are the locals it declares that the code after it reads, and a local function it calls is copied into the node with it. The section in the graph is replaced by one call of the node, <code>call</code> naming it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>path</code> <code>string</code> — The graph's folder (what <code>~</code> in its calls is relative to).</li> <li><code>from</code> <code>number</code> — First line of the section.</li> <li><code>to</code> <code>number</code> — Last line of the section.</li> <li><code>opts</code> <code>{ call: string, description: string }</code> — <code>{ call, description }</code>: the name the graph calls the node by (<code>~/parts/rim</code>, <code>points.rim</code>), and the sentence the node describes itself with.</li> </ul> <h2 id="code-literal">modules/proc.code/literal</h2> <pre><code class="language-lua">literal(value: any): string </code></pre> <p>The Luau text of a plain value: a number, string, boolean, vector, or a table of them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — The value.</li> </ul> <h2 id="code-parse">modules/proc.code/parse</h2> <pre><code class="language-lua">parse(src: string): Parsed </code></pre> <p>Read a graph or node source: its tokens, the builder function <code>proc.define</code> is given, and the statements of that function's body and of the file around it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The text of an <code>init.luau</code>.</li> </ul> <h2 id="code-ports">modules/proc.code/ports</h2> <pre><code class="language-lua">ports(graph: any): ({ Ported }, { Ported }) </code></pre> <p>A built graph's inputs and outputs in the order a call takes and gives them: each input with its type, default, range, options and description, each output with the type of the value it gives.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em> — A graph built in the call form.</li> </ul> <h2 id="code-promote">modules/proc.code/promote</h2> <pre><code class="language-lua">promote(partPath: string, opts: { name: string?, into: string?, description: string?, tags: { string }? }?): { [string]: any } </code></pre> <p>Make a part (a <code>.procGraph</code> a graph calls by its path) a node: write a <code>.procNode</code> that declares the part's inputs as its own, calls the part with them and gives the part's outputs. The part keeps its code and its callers; the node puts it among the nodes <code>procgen.searchNodes</code> finds and a graph calls by name, and stays the part's: editing the part changes what the node makes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>partPath</code> <code>string</code> — The part's folder.</li> <li><code>opts</code> <code>{ name: string?, into: string?, description: string?, tags: { string }? }?</code> <em>(optional)</em> — <code>{ name, into, description, tags }</code>: the node's name (default: the part's), the folder it goes in (default: the part's folder's parent), what it does and the tags search finds it by (default: the part's own <code>.metadata</code>).</li> </ul> <h2 id="code-promotesource">modules/proc.code/promoteSource</h2> <pre><code class="language-lua">promoteSource(part: any, call: string, description: string): string </code></pre> <p>The source of a node standing for a part (a <code>.procGraph</code> called by its path): it declares the part's inputs as its own, calls the part with them and gives the part's outputs as its own, so the part keeps its code and its callers and the node puts it among the nodes a graph calls by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>part</code> <code>any</code> <em>(optional)</em> — The part's graph.</li> <li><code>call</code> <code>string</code> — The name the node calls the part by.</li> <li><code>description</code> <code>string</code> — What the node does.</li> </ul> <h2 id="code-reftype">modules/proc.code/refType</h2> <pre><code class="language-lua">refType(graph: any, ref: any, depth: number?): string? </code></pre> <p>The type of the value a ref of <code>graph</code> stands for: a graph input's declared type, or the type of the output a node gives, followed into a node's own graph where the node states no narrower type than Any.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em> — The graph the ref belongs to.</li> <li><code>ref</code> <code>any</code> <em>(optional)</em> — An InputRef (<code>graph_input</code> or <code>node_output</code>).</li> <li><code>depth</code> <code>number?</code> <em>(optional)</em></li> </ul> <h2 id="code-reference">modules/proc.code/reference</h2> <pre><code class="language-lua">reference(src: string, from: number, to: number, call: string, node: any, inputs: { [string]: string }?, </code></pre> <p>Replace the statements on lines <code>from</code>-<code>to</code> with a call of an existing node. Each input of the node takes the local the section reads under the same name unless <code>inputs</code> says otherwise; each local the section makes that the code after it reads is taken from the node's output of the same name unless <code>outputs</code> says otherwise.</p> <h2 id="code-removecall">modules/proc.code/removeCall</h2> <pre><code class="language-lua">removeCall(src: string, address: any): string </code></pre> <p>Remove one call statement. Refused while the code after it still names a local it declares.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>address</code> <code>any</code> <em>(optional)</em> — The local the call declares, or a line it spans.</li> </ul> <h2 id="code-rename">modules/proc.code/rename</h2> <pre><code class="language-lua">rename(src: string, from: string, to: string): string </code></pre> <p>Rename a local of the graph's body everywhere the body names it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>from</code> <code>string</code> — The local's name.</li> <li><code>to</code> <code>string</code> — Its new name.</li> </ul> <h2 id="code-repoint">modules/proc.code/repoint</h2> <pre><code class="language-lua">repoint(src: string, address: any, name: string): string </code></pre> <p>Point a call at another node, keeping its arguments.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>address</code> <code>any</code> <em>(optional)</em> — The local the call declares, or a line it spans.</li> <li><code>name</code> <code>string</code> — The node to call (<code>points.disc</code>, <code>~/parts/rim</code>, a path).</li> </ul> <h2 id="code-resolvecall">modules/proc.code/resolveCall</h2> <pre><code class="language-lua">resolveCall(graphPath: string, name: string): string </code></pre> <p>The folder of the node or part a graph at <code>graphPath</code> calls by <code>name</code>, resolved the way the call resolves it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graphPath</code> <code>string</code> — The calling graph's folder.</li> <li><code>name</code> <code>string</code> — The name the call gives (<code>points.disc</code>, <code>~/parts/rim</code>, a path, an identity or a guid).</li> </ul> <h2 id="code-rootof">modules/proc.code/rootOf</h2> <pre><code class="language-lua">rootOf(path: string): string? </code></pre> <p>The nearest folder a <code>~</code> name is relative to, for an asset at <code>path</code>: the nearest package, effect, or the world's <code>source</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — An asset's folder.</li> </ul> <h2 id="code-setargs">modules/proc.code/setArgs</h2> <pre><code class="language-lua">setArgs(src: string, address: any, set: { [string]: any }?, unset: { string }?, path: string?): string </code></pre> <p>Set and unset the arguments of one call. <code>set</code> maps an input name to a value (<code>{ expr = "..." }</code> for code, anything else a plain value); <code>unset</code> lists inputs to leave to the node's defaults.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>address</code> <code>any</code> <em>(optional)</em> — The local the call declares, or a line it spans.</li> <li><code>set</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Input name to value.</li> <li><code>unset</code> <code>{ string }?</code> <em>(optional)</em> — Input names.</li> <li><code>path</code> <code>string?</code> <em>(optional)</em> — The graph's folder, for a call by <code>~</code>.</li> </ul> <h2 id="code-setinput">modules/proc.code/setInput</h2> <pre><code class="language-lua">setInput(src: string, name: string, default: any, opts: { [string]: any }?): string </code></pre> <p>Declare one of the graph's inputs, or change the declaration of one it has: <code>local <name> = g:input("<name>", <default>, <opts>)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>name</code> <code>string</code> — The input's name.</li> <li><code>default</code> <code>any</code> <em>(optional)</em> — Its default (nil for none: then <code>opts.type</code> is needed).</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ type, min, max, options, description, optional }</code>.</li> </ul> <h2 id="code-setoutput">modules/proc.code/setOutput</h2> <pre><code class="language-lua">setOutput(src: string, name: string, value: any?): string </code></pre> <p>Declare, change or remove one of the graph's outputs. <code>value</code> is code (<code>{ expr = "..." }</code> or the name of a local as a string); nil removes the output.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — The graph's source.</li> <li><code>name</code> <code>string</code> — The output's name.</li> <li><code>value</code> <code>any?</code> <em>(optional)</em> — What the output gives, or nil to remove it.</li> </ul> <h2 id="code-writenode">modules/proc.code/writeNode</h2> <pre><code class="language-lua">writeNode(path: string, source: string, meta: { description: string, tags: { string }, call: string }): any </code></pre> <p>Write a node asset: its <code>.metadata</code> (description and tags), a README stating what it does, what it takes and gives and how a graph calls it, and its source, in that order so the node registers from its own source with its description already beside it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The <code>.procNode</code> folder.</li> <li><code>source</code> <code>string</code> — The node's source.</li> <li><code>meta</code> <code>{ description: string, tags: { string }, call: string }</code> — <code>{ description, tags, call }</code>: what it does, the tags search finds it by, and the name a graph calls it by.</li> </ul> <h2 id="diskcache-readme">modules/proc.diskcache/README</h2> <pre><code class="language-lua">require("@builtin::systems.procgen.proc.diskcache") -- proc.diskcache </code></pre> <p>A graph output's cooked value on disk, so a Generator loads what was cooked instead of cooking it again.</p> <p>Usage: local proc.diskcache = require("@builtin::systems.procgen.proc.diskcache")</p> <h2 id="diskcache-complete">modules/proc.diskcache/complete</h2> <pre><code class="language-lua">complete(index: any?, first: number, last: number): boolean </code></pre> <p>Whether an index holds every frame from <code>first</code> to <code>last</code> it will ever hold: a graph time cannot change holds them all in one, and a write its budget stopped holds what fit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>index</code> <code>any?</code> <em>(optional)</em> — An index from <code>readIndex</code>, or nil.</li> <li><code>first</code> <code>number</code> — The first frame of the range.</li> <li><code>last</code> <code>number</code> — The last frame of the range.</li> </ul> <h2 id="diskcache-contentfor">modules/proc.diskcache/contentFor</h2> <pre><code class="language-lua">contentFor(index: any, frame: number): string? </code></pre> <p>The content file a frame of an index names, or nil. A graph time cannot change names one file for every frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>index</code> <code>any</code> <em>(optional)</em> — An index from <code>readIndex</code>.</li> <li><code>frame</code> <code>number</code> — The timeline frame.</li> </ul> <h2 id="diskcache-decode">modules/proc.diskcache/decode</h2> <pre><code class="language-lua">decode(bytes: string): any </code></pre> <p>The value <code>bytes</code> hold, rebuilt: columns in CPU storage, assets and handles resolved again, a population's draws registered again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>string</code> — What <code>encode</code> made.</li> </ul> <pre><code class="language-lua">local geometry = proc.diskcache.decode(bytes) </code></pre> <h2 id="diskcache-encode">modules/proc.diskcache/encode</h2> <pre><code class="language-lua">encode(value: any): string </code></pre> <p>The bytes a cooked value is stored as. A value a dispatch left on the device is read back first; anything with no disk form — a function, a field with no elements to be read over — raises, naming where it sits.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — A cooked value.</li> </ul> <pre><code class="language-lua">local bytes = proc.diskcache.encode(geometry) </code></pre> <h2 id="diskcache-graphdigest">modules/proc.diskcache/graphDigest</h2> <pre><code class="language-lua">graphDigest(graph: any): string </code></pre> <p>The digest of a graph's document: what an index records it was cooked from, so an index made for a version of the graph that no longer stands is known and dropped.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em> — The Graph.</li> </ul> <pre><code class="language-lua">local digest = proc.diskcache.graphDigest(g) </code></pre> <h2 id="diskcache-indexkey">modules/proc.diskcache/indexKey</h2> <pre><code class="language-lua">indexKey(graph: any, outputName: string, params: any, timeline: any, timelessKey: number): string </code></pre> <p>The name of the index for one way of cooking a graph: the SHA-256 over everything that decides what it cooks — the graph's document, the params, its timeline's frame rate and range, the output's time-free key (which carries the versions of every op and node it uses, and the live state it reads) and the cache format.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em> — The Graph.</li> <li><code>outputName</code> <code>string</code> — The output.</li> <li><code>params</code> <code>any</code> <em>(optional)</em> — The params the graph is cooked with.</li> <li><code>timeline</code> <code>any</code> <em>(optional)</em> — <code>{ fps, first, last }</code>: the rate the timeline plays at and the frames its range runs between, which a graph's time expressions read.</li> <li><code>timelessKey</code> <code>number</code> — The output's time-free key (<code>proc.historyKey</code>).</li> </ul> <pre><code class="language-lua">local key = proc.diskcache.indexKey(g, "mesh", {}, { fps = 30, first = 0, last = 149 }, k) </code></pre> <h2 id="diskcache-load">modules/proc.diskcache/load</h2> <pre><code class="language-lua">load(graphGuid: string, hash: string): any? </code></pre> <p>The value stored under content hash <code>hash</code> of graph <code>graphGuid</code>, decoded, or nil when there is no such file.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graphGuid</code> <code>string</code> — The graph's guid.</li> <li><code>hash</code> <code>string</code> — A content hash.</li> </ul> <h2 id="diskcache-provide">modules/proc.diskcache/provide</h2> <pre><code class="language-lua">provide(id: string, provider: () -> { any }) </code></pre> <p>Register what a generator stores when the world is committed: <code>provider()</code> answers <code>{ { graphGuid, key, static, frames, valueAt, graphDigest, written? } }</code>, one entry per output that caches and whose index is not complete — <code>frames</code> and <code>valueAt</code> as <code>write</code> takes them, <code>graphDigest</code> the graph as it stands, and <code>written(result)</code> told what the write did. Registering the same id again replaces it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The generator's id.</li> <li><code>provider</code> <code>() -> { any }</code> — The function answering what to store.</li> </ul> <pre><code class="language-lua">proc.diskcache.provide(entityId, function() return {} end) </code></pre> <h2 id="diskcache-prune">modules/proc.diskcache/prune</h2> <pre><code class="language-lua">prune(graphGuid: string, graphDigest: string?): { string } </code></pre> <p>Delete what graph <code>graphGuid</code>'s store holds for nothing: every index cooked from a version of the graph other than <code>graphDigest</code>, and every content file no remaining index names — what a frame's output used to be, before the graph or its params changed. Answers the paths it removed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graphGuid</code> <code>string</code> — The graph's guid.</li> <li><code>graphDigest</code> <code>string?</code> <em>(optional)</em> — The <code>graphDigest</code> of the graph as it stands; an index recording another is dropped. Nil keeps every index.</li> </ul> <pre><code class="language-lua">local removed = proc.diskcache.prune(guid, proc.diskcache.graphDigest(g)) </code></pre> <h2 id="diskcache-readindex">modules/proc.diskcache/readIndex</h2> <pre><code class="language-lua">readIndex(graphGuid: string, key: string): any? </code></pre> <p>What the index <code>key</code> of graph <code>graphGuid</code> holds: <code>{ frames = { [frame] = contentHash }, static, stoppedAt? }</code>, or nil when it has none. <code>stoppedAt</code> is the first frame the write's budget left out.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graphGuid</code> <code>string</code> — The graph's guid.</li> <li><code>key</code> <code>string</code> — An index name from <code>indexKey</code>.</li> </ul> <h2 id="diskcache-store">modules/proc.diskcache/store</h2> <pre><code class="language-lua">store(): any </code></pre> <p>Write what every registered generator stores — the frames of each output whose index is not complete — and delete the content no index of those graphs names any longer. Committing the world does this first and stages every path it touched into the commit.</p> <pre><code class="language-lua">local touched = proc.diskcache.store() </code></pre> <h2 id="diskcache-withdraw">modules/proc.diskcache/withdraw</h2> <pre><code class="language-lua">withdraw(id: string) </code></pre> <p>Stop storing anything for generator <code>id</code> at commit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The generator's id.</li> </ul> <h2 id="diskcache-write">modules/proc.diskcache/write</h2> <pre><code class="language-lua">write(graphGuid: string, key: string, frames: { number }, valueAt: (number) -> any, </code></pre> <p>Write the frames of one way of cooking a graph: each value's content once, named by its hash, and the index mapping every frame to the content it produced. A frame whose output is the same as another's shares its file. Frames are asked for one at a time and encoded as they come, so a long timeline never holds more than one frame's bytes.</p> <pre><code class="language-lua">proc.diskcache.write(guid, key, { 0 }, function() return geo end, true) </code></pre> <h2 id="edit-readme">modules/proc.edit/README</h2> <pre><code class="language-lua">require("@builtin::systems.procgen.proc.edit") -- proc.edit </code></pre> <p>Editing a procedural graph as nodes and wires: the picture of a Graph a node editor draws (<code>view</code>), the catalogue of ops a node can be added from (<code>catalogue</code>), and every change a person makes to it (<code>addNode</code>, <code>removeNode</code>, <code>connect</code>, <code>disconnect</code>, <code>setParam</code>, the graph's inputs and outputs). Each change edits the Graph in memory and answers <code>(ok, why?)</code>; writing the result into the asset is <code>proc.saveSource</code>. Positions are the editor's own and live beside the asset in <code>layout.json</code>. Ports named <code>in:<name></code> and <code>out:<name></code> stand for the graph's own inputs and outputs, so they wire like any node.</p> <p>Usage: local proc.edit = require("@builtin::systems.procgen.proc.edit")</p> <h2 id="edit-addnode">modules/proc.edit/addNode</h2> <pre><code class="language-lua">addNode(graph: any, op: any, id: string?): (string?, string?) </code></pre> <p>Add a node running <code>op</code> (an op id, or a <code>.procNode</code> / <code>.procGraph</code> asset ref). Its id comes from the op's name unless <code>id</code> is given.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>op</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>string?</code> <em>(optional)</em></li> </ul> <h2 id="edit-catalogue">modules/proc.edit/catalogue</h2> <pre><code class="language-lua">catalogue(): { any } </code></pre> <p>Every op a node can be added from: <code>{ id, pack, description, context }</code>, sorted by id. Rebuilt when the registry changes.</p> <h2 id="edit-connect">modules/proc.edit/connect</h2> <pre><code class="language-lua">connect(graph: any, fromId: string, fromPort: string, toId: string, toPort: string): (boolean, string?) </code></pre> <p>Wire the output port <code>fromPort</code> of <code>fromId</code> into the input port <code>toPort</code> of <code>toId</code>. An input takes one wire (a socket typed <code>T[]</code> takes several, and this adds one); a graph output (<code>out:<name></code>) is bound to what it reads. A wire is refused when it would close a cycle or carry a type the port does not accept.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>fromId</code> <code>string</code></li> <li><code>fromPort</code> <code>string</code></li> <li><code>toId</code> <code>string</code></li> <li><code>toPort</code> <code>string</code></li> </ul> <h2 id="edit-disconnect">modules/proc.edit/disconnect</h2> <pre><code class="language-lua">disconnect(graph: any, toId: string, toPort: string, fromId: string?, fromPort: string?): (boolean, string?) </code></pre> <p>Take away what feeds the input port <code>toPort</code> of <code>toId</code>: every wire on it, or with <code>fromId</code> and <code>fromPort</code> just the one that comes from there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>toId</code> <code>string</code></li> <li><code>toPort</code> <code>string</code></li> <li><code>fromId</code> <code>string?</code> <em>(optional)</em></li> <li><code>fromPort</code> <code>string?</code> <em>(optional)</em></li> </ul> <h2 id="edit-inputtypes">modules/proc.edit/inputTypes</h2> <pre><code class="language-lua">inputTypes(): { string } </code></pre> <p>The types a graph input can be declared as.</p> <h2 id="edit-loadlayout">modules/proc.edit/loadLayout</h2> <pre><code class="language-lua">loadLayout(folder: string): { [string]: any } </code></pre> <p>The positions kept beside the graph in <code>layout.json</code>: <code>{ [id] = { x, y } }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>folder</code> <code>string</code></li> </ul> <h2 id="edit-params">modules/proc.edit/params</h2> <pre><code class="language-lua">params(graph: any, id: string): { any } </code></pre> <p>The params of node <code>id</code> as editable rows: <code>{ name, type, value, default, description, min, max, options, wired }</code>, sorted by name. <code>value</code> is the constant the node holds, nil while it takes the default or a wire.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>string</code></li> </ul> <h2 id="edit-removeinput">modules/proc.edit/removeInput</h2> <pre><code class="language-lua">removeInput(graph: any, name: string): (boolean, string?) </code></pre> <p>Remove the graph input <code>name</code> and every wire that read it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="edit-removenode">modules/proc.edit/removeNode</h2> <pre><code class="language-lua">removeNode(graph: any, id: string): (boolean, string?) </code></pre> <p>Remove a node (or composite) and every wire that read it. An output the node alone fed is removed with it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>string</code></li> </ul> <h2 id="edit-removeoutput">modules/proc.edit/removeOutput</h2> <pre><code class="language-lua">removeOutput(graph: any, name: string): (boolean, string?) </code></pre> <p>Remove the graph output <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="edit-renameinput">modules/proc.edit/renameInput</h2> <pre><code class="language-lua">renameInput(graph: any, old: string, new: string): (boolean, string?) </code></pre> <p>Rename the graph input <code>old</code> to <code>new</code>, keeping its place in the order and every wire that reads it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>old</code> <code>string</code></li> <li><code>new</code> <code>string</code></li> </ul> <h2 id="edit-renameoutput">modules/proc.edit/renameOutput</h2> <pre><code class="language-lua">renameOutput(graph: any, old: string, new: string): (boolean, string?) </code></pre> <p>Rename the graph output <code>old</code> to <code>new</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>old</code> <code>string</code></li> <li><code>new</code> <code>string</code></li> </ul> <h2 id="edit-savelayout">modules/proc.edit/saveLayout</h2> <pre><code class="language-lua">saveLayout(folder: string, positions: { [string]: any }, keep: { [string]: boolean }?) </code></pre> <p>Keep <code>positions</code> beside the graph in <code>layout.json</code>, dropping the ids <code>keep</code> (a set) does not hold. Written only when it changes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>folder</code> <code>string</code></li> <li><code>positions</code> <code>{ [string]: any }</code></li> <li><code>keep</code> <code>{ [string]: boolean }?</code> <em>(optional)</em></li> </ul> <h2 id="edit-search">modules/proc.edit/search</h2> <pre><code class="language-lua">search(query: string, limit: number?): { any } </code></pre> <p>The ops whose id, pack or description holds every word of <code>query</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>query</code> <code>string</code> — Space-separated words; empty answers the whole catalogue.</li> <li><code>limit</code> <code>number?</code> <em>(optional)</em> — Most rows to answer (default 60).</li> </ul> <h2 id="edit-setdescription">modules/proc.edit/setDescription</h2> <pre><code class="language-lua">setDescription(graph: any, text: string): (boolean, string?) </code></pre> <p>Say what the graph makes: the sentence search finds it by and the publish gate asks for. An empty sentence takes it away.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>text</code> <code>string</code></li> </ul> <h2 id="edit-setdomain">modules/proc.edit/setDomain</h2> <pre><code class="language-lua">setDomain(graph: any, name: string, domain: string?): (boolean, string?) </code></pre> <p>Say which domain the graph output <code>name</code> stands as: <code>"point"</code>, <code>"primitive"</code>, <code>"mesh"</code>, or nil for none.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> <li><code>domain</code> <code>string?</code> <em>(optional)</em></li> </ul> <h2 id="edit-setinput">modules/proc.edit/setInput</h2> <pre><code class="language-lua">setInput(graph: any, name: string, typ: string, opts: any?): (boolean, string?) </code></pre> <p>Declare (or redeclare) the graph input <code>name</code>: <code>{ type, default, min, max, description }</code>. The order inputs were declared in is kept.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> <li><code>typ</code> <code>string</code></li> <li><code>opts</code> <code>any?</code> <em>(optional)</em></li> </ul> <h2 id="edit-setoutput">modules/proc.edit/setOutput</h2> <pre><code class="language-lua">setOutput(graph: any, name: string, fromId: string, fromPort: string): (boolean, string?) </code></pre> <p>Declare the graph output <code>name</code>, reading <code>fromPort</code> of <code>fromId</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> <li><code>fromId</code> <code>string</code></li> <li><code>fromPort</code> <code>string</code></li> </ul> <h2 id="edit-setparam">modules/proc.edit/setParam</h2> <pre><code class="language-lua">setParam(graph: any, id: string, name: string, value: any): (boolean, string?) </code></pre> <p>Set the constant param <code>name</code> of node <code>id</code> to <code>value</code>; nil takes the param back to its default. A wire on the param is replaced by the constant.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>string</code></li> <li><code>name</code> <code>string</code></li> <li><code>value</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="edit-uniqueid">modules/proc.edit/uniqueId</h2> <pre><code class="language-lua">uniqueId(graph: any, base: string): string </code></pre> <p>An id no node of <code>graph</code> holds, derived from <code>base</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>base</code> <code>string</code></li> </ul> <h2 id="edit-updateinput">modules/proc.edit/updateInput</h2> <pre><code class="language-lua">updateInput(graph: any, name: string, patch: { [string]: any }): (boolean, string?) </code></pre> <p>Change fields of the graph input <code>name</code>: <code>patch</code> holds any of <code>type</code>, <code>default</code>, <code>min</code>, <code>max</code>, <code>description</code>. A change of type takes the default the new type starts from when the old default does not fit it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> <li><code>patch</code> <code>{ [string]: any }</code></li> </ul> <h2 id="edit-view">modules/proc.edit/view</h2> <pre><code class="language-lua">view(graph: any, positions: { [string]: any }?): any </code></pre> <p>The picture of <code>graph</code> a node editor draws: <code>{ nodes, edges }</code> in the shape <code>edui.nodeGraph</code> takes. Every graph node, composite, input and output is a node; each producer a node or output reads is an edge. An op's node carries a port for each socket and each declared param, a param with the value that drives it while no wire does; a graph input's node carries its name, type, default and range as fields. The words the editor draws on a node (its fields' labels, the subtitle of an input, an output or a composite) read from the <code>procgen</code> catalog in the editor's language; a field's <code>name</code> is the one code finds it by.</p> <p><strong>Parameters</strong></p> <ul> <li><code>graph</code> <code>any</code> <em>(optional)</em> — A Graph.</li> <li><code>positions</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Optional <code>{ [id] = { x, y } }</code> stored positions.</li> </ul> <h2 id="polyline-readme">modules/proc.polyline/README</h2> <pre><code class="language-lua">require("@builtin::systems.procgen.proc.polyline") -- proc.polyline </code></pre> <p>Arc-length measure along a polyline held as flat positions: its length, and the position and direction a given share of the way along it. The curves a geometry holds are read out with <code>Geometry.curvePositions</code>, and the ops that sample, sweep or grow along them measure them here.</p> <p>Usage: local proc.polyline = require("@builtin::systems.procgen.proc.polyline")</p> <h2 id="polyline-length">modules/proc.polyline/length</h2> <pre><code class="language-lua">length(pos: { number }, closed: boolean): number </code></pre> <p>The polyline's length, the closing span included when it closes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>pos</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> <li><code>closed</code> <code>boolean</code> — Whether the last point joins back to the first.</li> </ul> <pre><code class="language-lua">proc.polyline.length({ 0,0,0, 3,4,0 }, false) --> 5 </code></pre> <h2 id="polyline-locate">modules/proc.polyline/locate</h2> <pre><code class="language-lua">locate(pos: { number }, closed: boolean, t: number): (number, number) </code></pre> <p>Which span <code>t</code> of the way along the polyline falls in, and how far along that span: the span runs from point <code>span</code> to the next (to point 0 on the closing span).</p> <p><strong>Parameters</strong></p> <ul> <li><code>pos</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> <li><code>closed</code> <code>boolean</code> — Whether the last point joins back to the first.</li> <li><code>t</code> <code>number</code> — The share of the length.</li> </ul> <h2 id="polyline-sample">modules/proc.polyline/sample</h2> <pre><code class="language-lua">sample(pos: { number }, closed: boolean, t: number): (number, number, number) </code></pre> <p>The position <code>t</code> of the way along the polyline by arc length, <code>t</code> in [0, 1].</p> <p><strong>Parameters</strong></p> <ul> <li><code>pos</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> <li><code>closed</code> <code>boolean</code> — Whether the last point joins back to the first.</li> <li><code>t</code> <code>number</code> — The share of the length.</li> </ul> <pre><code class="language-lua">local x, y, z = proc.polyline.sample(pos, false, 0.5) </code></pre> <h2 id="polyline-tangent">modules/proc.polyline/tangent</h2> <pre><code class="language-lua">tangent(pos: { number }, closed: boolean, t: number): (number, number, number) </code></pre> <p>The unit direction the polyline runs in <code>t</code> of the way along it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>pos</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> <li><code>closed</code> <code>boolean</code> — Whether the last point joins back to the first.</li> <li><code>t</code> <code>number</code> — The share of the length.</li> </ul> <h2 id="substrate-readme">modules/proc.substrate/README</h2> <pre><code class="language-lua">require("@builtin::systems.procgen.proc.substrate") -- proc.substrate </code></pre> <p>The substrate a graph carries: a geometry, its four domains, and the named columns over them.</p> <p>Usage: local proc.substrate = require("@builtin::systems.procgen.proc.substrate")</p> <h2 id="substrate-addcurve">modules/proc.substrate/addCurve</h2> <pre><code class="language-lua">addCurve(self, pointIndices: { number }, closed: boolean?): number </code></pre> <p>Add one curve: a primitive running through the points <code>pointIndices</code> names, in order. A curve is a line rather than a surface — a mesh built from the geometry draws its faces and not its curves, and the ops that sample, sweep or grow along lines read its curves — while its points are points like any other, so every point and field op moves them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>pointIndices</code> <code>{ number }</code> — 0-based point indices, in the order the curve runs.</li> <li><code>closed</code> <code>boolean?</code> <em>(optional)</em> — Whether the curve runs from its last point back to its first.</li> </ul> <pre><code class="language-lua">g:addCurve({ 0, 1, 2, 3 }, false) </code></pre> <h2 id="substrate-addpoints">modules/proc.substrate/addPoints</h2> <pre><code class="language-lua">addPoints(self, positions: { number }): any </code></pre> <p>Add <code>count</code> points at <code>positions</code> (a flat <code>count * 3</code> array), each with an id nothing else in this geometry carries. Ids are identity: a filter renumbers indices and every id it keeps stays what it was.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>positions</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> </ul> <pre><code class="language-lua">g:addPoints({ 0,0,0, 1,0,0, 2,0,0 }) </code></pre> <h2 id="substrate-addprimitive">modules/proc.substrate/addPrimitive</h2> <pre><code class="language-lua">addPrimitive(self, pointIndices: { number }): number </code></pre> <p>Add one primitive made of the points <code>pointIndices</code> names, in order. It creates one vertex per entry, each naming the point it reads, and the primitive's run over them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>pointIndices</code> <code>{ number }</code> — 0-based point indices, in the order the primitive uses them.</li> </ul> <pre><code class="language-lua">g:addPrimitive({ 0, 1, 2 }) </code></pre> <h2 id="substrate-adopt">modules/proc.substrate/adopt</h2> <pre><code class="language-lua">adopt(self, owner: string) </code></pre> <p>Hold this column's device runs for <code>owner</code> from now on (<code>substrate.setOwner</code>): its own run, the mirrors it keeps of a run the CPU holds, the indices an interned column stands on, the runs it keeps alive with it (<code>Column.keep</code>), and the ones it keeps later. What something keeping the column longer than the owner that made it — a cache several makers read — calls when it takes the column in.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>owner</code> <code>string</code> — The owner to hold the runs for.</li> </ul> <pre><code class="language-lua">col:adopt(proc.sharedCacheOwner(key)) </code></pre> <h2 id="substrate-anylive">modules/proc.substrate/anyLive</h2> <pre><code class="language-lua">anyLive(): boolean </code></pre> <p>Whether any run is live: some registration rewrites a run in place as the time it is a function of moves. A cook with no live run to follow asks nothing of the columns it handles.</p> <pre><code class="language-lua">if proc.substrate.anyLive() then print("a graph run is still on the device") end </code></pre> <h2 id="substrate-atcapacity">modules/proc.substrate/atCapacity</h2> <pre><code class="language-lua">atCapacity(fn: () -> any): any </code></pre> <p>Run <code>fn</code> with every live count (the one-word column an appended output or a device-made count holds) sampling as the capacity it is bounded by, and answer what <code>fn</code> returns. A Field built over live counts, sampled inside it, answers the largest value it can take: the capacity a count derived on the device is allocated for. The device count itself is never read.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> any</code> — The function to run.</li> </ul> <pre><code class="language-lua">local most = proc.substrate.atCapacity(function() return field:sample(0) end) </code></pre> <h2 id="substrate-attributeat">modules/proc.substrate/attributeAt</h2> <pre><code class="language-lua">attributeAt(self, name: string, id: number): ...any </code></pre> <p>The point attribute <code>name</code> at point <code>id</code>, as its components — what <code>g:getAttribute("point", name):sample(id)</code> answers, in one call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code> — The attribute's name.</li> <li><code>id</code> <code>number</code> — 0-based point index.</li> </ul> <h2 id="substrate-attributenames">modules/proc.substrate/attributeNames</h2> <pre><code class="language-lua">attributeNames(self, class: string): { string } </code></pre> <p>The attribute names in a class — every column except the intrinsics that hold the geometry itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>class</code> <code>string</code> — "point", "vertex", "edge", "primitive" or "detail".</li> </ul> <h2 id="substrate-awaitprefetch">modules/proc.substrate/awaitPrefetch</h2> <pre><code class="language-lua">awaitPrefetch(self, asBytes: boolean?): boolean </code></pre> <p>Wait until this column's run is on the CPU, as <code>prefetch</code> answers it: the read is asked for if it is not in flight and the task parks on the read itself, so it resumes in the frame the device answered where the engine can wait on the device. A read of a run the device rewrites frame after frame answers the run as the device held it when the read was taken, and the next <code>read</code> or <code>readBytes</code> takes that answer. A read the engine gave up raises with its reason.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>asBytes</code> <code>boolean?</code> <em>(optional)</em> — Whether the read wanted is <code>readBytes</code>'s rather than <code>read</code>'s.</li> </ul> <pre><code class="language-lua">col:awaitPrefetch(true); print(#col:readBytes(), "bytes") </code></pre> <h2 id="substrate-bounds">modules/proc.substrate/bounds</h2> <pre><code class="language-lua">bounds(self): { min: { x: number, y: number, z: number }, max: { x: number, y: number, z: number } } </code></pre> <p>Axis-aligned bounds over the live points, as <code>{ min = {x,y,z}, max = {x,y,z} }</code>: the least and greatest of every lane, folded on the device and read back as those six numbers. A geometry with no live point is bounded by the origin.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-buffer">modules/proc.substrate/buffer</h2> <pre><code class="language-lua">buffer(self): any </code></pre> <p>The substrate buffer behind a numeric column — the handle <code>entity.batchWrite</code>, <code>nx</code> and a compute dispatch all take. A constant column materializes here.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">entity.batchWrite(binding, "Transform", "position", col:buffer()) </code></pre> <h2 id="substrate-bufferlayout">modules/proc.substrate/bufferLayout</h2> <pre><code class="language-lua">bufferLayout(t: string): (string?, number?) </code></pre> <p>The substrate buffer element type a column of <code>t</code> is built from, and how many of them one element occupies. A type the buffer primitive expresses directly answers <code>per = 1</code>, and its buffer is one a batch write takes as it stands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — A procgen type tag.</li> </ul> <pre><code class="language-lua">local kind, per = proc.substrate.bufferLayout("Vec3") --> "vec3", 1 </code></pre> <h2 id="substrate-buffername">modules/proc.substrate/bufferName</h2> <pre><code class="language-lua">bufferName(self): string? </code></pre> <p>The name this column's run lives under in the substrate, or nil for a column whose backing is not a named device buffer.</p> <p>A device binding names its source buffer rather than holding a handle, so this is what lets a run already on the device be bound as the source of something else — placing a domain's elements from the columns that already hold their transform, rather than reading the run back to write it per element.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local name = column:bufferName() </code></pre> <h2 id="substrate-capacityof">modules/proc.substrate/capacityOf</h2> <pre><code class="language-lua">capacityOf(col: any): number? </code></pre> <p>The capacity a live count is bounded by: the one-word column an appended output or a count made on the device holds, or a clone of one (a clone shares the run, and the capacity is the run's).</p> <p><strong>Parameters</strong></p> <ul> <li><code>col</code> <code>any</code> <em>(optional)</em> — A Column.</li> </ul> <pre><code class="language-lua">local most = proc.substrate.capacityOf(kept.indices_count) </code></pre> <h2 id="substrate-carry">modules/proc.substrate/carry</h2> <pre><code class="language-lua">carry(src: any, dst: any, by: { [string]: any }?): any </code></pre> <p>Carry <code>src</code>'s attributes onto <code>dst</code>. A domain <code>by</code> names an index list for is GATHERED by it — element k of the result reads source element <code>by[k + 1]</code>, which is the one operation a filter, a split, a weld and a remap all come down to, and it runs where the column lives. A domain with no list carries its columns as they stand, and one named <code>false</code> is left alone — that is the domain whose values the op computes rather than reads, an average across a weld among them. The intrinsics are never carried: the op that changed the topology wrote those itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>any</code> <em>(optional)</em> — The geometry the attributes come from.</li> <li><code>dst</code> <code>any</code> <em>(optional)</em> — The geometry they land on.</li> <li><code>by</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ point = { ... } | false, vertex = ..., primitive = ... }</code>, each list an array of 0-based source indices.</li> </ul> <pre><code class="language-lua">proc.substrate.carry(g, out, { point = srcPoint, primitive = srcFace }) </code></pre> <h2 id="substrate-carrybetween">modules/proc.substrate/carryBetween</h2> <pre><code class="language-lua">carryBetween(src: any, dst: any, a: { number }, b: { number }, w: { number }): any </code></pre> <p>Carry <code>src</code>'s point attributes onto <code>dst</code>'s points, each of which lies between two of <code>src</code>'s: point k of <code>dst</code> is <code>w[k]</code> of the way from point <code>a[k]</code> to point <code>b[k]</code>. Numbers are blended between the two; a value that cannot be blended is taken from the nearer. <code>P</code> and <code>id</code> are the destination's own.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>any</code> <em>(optional)</em> — The geometry the attributes come from.</li> <li><code>dst</code> <code>any</code> <em>(optional)</em> — The geometry whose points they land on, one per entry of <code>a</code>.</li> <li><code>a</code> <code>{ number }</code> — 0-based source point each destination point starts from.</li> <li><code>b</code> <code>{ number }</code> — 0-based source point each destination point runs toward.</li> <li><code>w</code> <code>{ number }</code> — How far from <code>a[k]</code> to <code>b[k]</code>, in [0, 1].</li> </ul> <pre><code class="language-lua">proc.substrate.carryBetween(curve, samples, { 0, 0 }, { 1, 1 }, { 0.25, 0.75 }) </code></pre> <h2 id="substrate-carrycount">modules/proc.substrate/carryCount</h2> <pre><code class="language-lua">carryCount(col: any, counter: any, capacity: number): any </code></pre> <p>Make <code>col</code> a column of <code>capacity</code> slots of which the first <code>counter</code> words are live.</p> <p><strong>Parameters</strong></p> <ul> <li><code>col</code> <code>any</code> <em>(optional)</em> — A Column the caller owns.</li> <li><code>counter</code> <code>any</code> <em>(optional)</em> — The one-word count the column carries; it goes with the column.</li> <li><code>capacity</code> <code>number</code> — The slots the column is allocated for.</li> </ul> <pre><code class="language-lua">proc.substrate.carryCount(indices, counter, 64) </code></pre> <h2 id="substrate-clone">modules/proc.substrate/clone</h2> <pre><code class="language-lua">clone(self): any </code></pre> <p>A copy with its own backing. A constant column copies as a constant one, so sharing survives the copy at the cost of one value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-closelivewindow">modules/proc.substrate/closeLiveWindow</h2> <pre><code class="language-lua">closeLiveWindow(window: any): boolean </code></pre> <p>Close <code>window</code> (<code>openLiveWindow</code>) and every window opened after it on this thread, and answer whether the work inside it read a live run, or a live value made before it opened.</p> <p><strong>Parameters</strong></p> <ul> <li><code>window</code> <code>any</code> <em>(optional)</em> — The window.</li> </ul> <pre><code class="language-lua">local readLive = proc.substrate.closeLiveWindow(window) </code></pre> <h2 id="substrate-column">modules/proc.substrate/column</h2> <pre><code class="language-lua">column(self, name: string): any </code></pre> <p>The column named <code>name</code>, or nil.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="substrate-columnmoves">modules/proc.substrate/columnMoves</h2> <pre><code class="language-lua">columnMoves(): number </code></pre> <p>How many times a column that already stood has been let go of its run or given another, or a domain has had a column put in or taken out. A holder that read which runs a value's columns stand on at one count knows they stand there still while the count is the same: a column made afterwards changes no column that stood before.</p> <pre><code class="language-lua">local at = proc.substrate.columnMoves() </code></pre> <h2 id="substrate-concat">modules/proc.substrate/concat</h2> <pre><code class="language-lua">concat(t: string, parts: { any }, counts: { number }, absent: { number }?): any </code></pre> <p>One column holding <code>parts</code> end to end, each covering the element count <code>counts</code> gives it. A part that is nil covers its elements with <code>absent</code>, or zeros without one (for a column carried as values, with nothing). Runs the device holds are laid end to end on the device; runs the CPU holds are copied there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type of the result.</li> <li><code>parts</code> <code>{ any }</code> — One Column or nil per part.</li> <li><code>counts</code> <code>{ number }</code> — The element count of each part.</li> <li><code>absent</code> <code>{ number }?</code> <em>(optional)</em> — The value, one number per lane, a nil part's elements hold.</li> </ul> <pre><code class="language-lua">local P = proc.substrate.concat("Vec3", { a:column("point", "P"), b:column("point", "P") }, { 4, 9 }) </code></pre> <h2 id="substrate-constant">modules/proc.substrate/constant</h2> <pre><code class="language-lua">constant(t: string, value: any, count: number): any </code></pre> <p>A column holding <code>value</code> at every element. It costs one value however many elements it stands for, and a write to any one of them materializes the run.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type tag.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — One element's value — <code>lanes</code> numbers as an array for a numeric type, the value itself for a type carried as values.</li> <li><code>count</code> <code>number</code> — How many elements it stands for.</li> </ul> <pre><code class="language-lua">proc.substrate.Column.constant("Vec3", { 0, 1, 0 }, 1000000) </code></pre> <h2 id="substrate-copytopoints">modules/proc.substrate/copyToPoints</h2> <pre><code class="language-lua">copyToPoints(sources: { any }, points: any, opts: { variant: string?, skip: { [string]: boolean }? }?): any </code></pre> <p>A copy of a source stamped at every point, all in one geometry, on the device. Each point's copy is its source scaled by the point's <code>scale</code>, turned by its <code>orient</code> (a quaternion x, y, z, w) and moved to its <code>P</code>; a point without one stands at no scale or turn. With several sources each point's <code>variant</code> attribute names the one it takes (0 is the first); a point naming none of them takes no copy. The copies lie in point order, in every domain: their corners name their own copy's points, their faces their own copy's corners, and their normals turn with them. A source point's own <code>orient</code> is turned by its copy's and its Vec3 <code>scale</code> multiplied by it, so instances copied onto points keep their own turn and size inside the copy's. Every other point attribute stands on each point of its copy, over a source's own attribute of that name; one carried as values (text, records) is carried as its indices into the values it holds (<code>Column.intern</code>). The copies' ids and slots are their own places.</p> <p>The whole runs where the points are: nothing is read back, the points' live count bounds every pass, and how many copies there are in all is a count on the device, so a population a solver moves copies on every frame without a pause. A geometry whose counts the CPU cannot know — several sources, or points counted on the device — carries its live counts there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sources</code> <code>{ any }</code> — The geometries to copy, in the order <code>variant</code> names them.</li> <li><code>points</code> <code>any</code> <em>(optional)</em> — A geometry whose points the copies stand at.</li> <li><code>opts</code> <code>{ variant: string?, skip: { [string]: boolean }? }?</code> <em>(optional)</em> — <code>{ variant = name?, skip = { [name] = true }? }</code>: the point attribute that names each point's source, and point attributes the copies take nothing from.</li> </ul> <pre><code class="language-lua">local rocks = proc.substrate.Geometry.copyToPoints({ rock }, scattered) </code></pre> <h2 id="substrate-count">modules/proc.substrate/count</h2> <pre><code class="language-lua">count(self): number </code></pre> <p>Element count.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-counttraffic">modules/proc.substrate/countTraffic</h2> <pre><code class="language-lua">countTraffic(handle: any, opts: { type: string, len: number }): any </code></pre> <p>Count what a device buffer made outside this module moves between the CPU and the device — a buffer a shader owns (<code>shaderRef:createBuffer</code>) — into <code>proc.substrate.traffic</code>. Every buffer <code>createBuffer</code> makes is counted already.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — The buffer handle.</li> <li><code>opts</code> <code>{ type: string, len: number }</code> — The <code>{ type, len }</code> it was created with.</li> </ul> <pre><code class="language-lua">local dst = proc.substrate.countTraffic(shaderRef:createBuffer("dst", opts), opts) </code></pre> <h2 id="substrate-countedreads">modules/proc.substrate/countedReads</h2> <pre><code class="language-lua">countedReads(fn: () -> any): any </code></pre> <p>Run <code>fn</code>, counting the reads it makes in <code>traffic().countReads</code> rather than in <code>readbacks</code>: a read the caller asks again itself, the way a count read where a number is needed is asked again of a recorded cook.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> any</code> — The function to run.</li> </ul> <pre><code class="language-lua">local same = proc.substrate.countedReads(function() return a:readBytes() == b:readBytes() end) </code></pre> <h2 id="substrate-cpubytes">modules/proc.substrate/cpuBytes</h2> <pre><code class="language-lua">cpuBytes(self): number </code></pre> <p>The bytes this column holds on the CPU: its words when its buffer lives there, a slot per element for a column carried as values. A column in GPU storage holds none here — its run is counted by the owner it is held for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-createbuffer">modules/proc.substrate/createBuffer</h2> <pre><code class="language-lua">createBuffer(opts: { [string]: any }): any </code></pre> <p>A substrate buffer, as <code>substrate.createBuffer</code> makes it, whose traffic between the CPU and the device — when it lives on the device — is counted into <code>proc.substrate.traffic</code>. The one way procedural code makes a buffer, so what every node moves is known without the node saying.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }</code> — <code>substrate.createBuffer</code>'s options.</li> </ul> <pre><code class="language-lua">local tile = proc.substrate.createBuffer({ name = "t.tile", type = "f32", len = 256, kind = "gpu" }) </code></pre> <h2 id="substrate-curvecount">modules/proc.substrate/curveCount</h2> <pre><code class="language-lua">curveCount(self): number </code></pre> <p>How many of the geometry's primitives are curves.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-curvepositions">modules/proc.substrate/curvePositions</h2> <pre><code class="language-lua">curvePositions(self, curve: any): { number } </code></pre> <p>The positions a curve runs through, flat, in its order.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>curve</code> <code>any</code> <em>(optional)</em> — One entry of <code>curves()</code>.</li> </ul> <h2 id="substrate-curves">modules/proc.substrate/curves</h2> <pre><code class="language-lua">curves(self): { any } </code></pre> <p>Every curve the geometry holds, in primitive order: the primitive, the point indices it runs through and whether it closes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">for _, c in ipairs(g:curves()) do print(#c.points) end </code></pre> <h2 id="substrate-deferreleases">modules/proc.substrate/deferReleases</h2> <pre><code class="language-lua">deferReleases(): any </code></pre> <p>Hold every column let go of from now on until <code>releaseDeferred</code>: the buffers a recorded cook's dispatches use stay on the device after the nodes that made them let go.</p> <pre><code class="language-lua">local held = proc.substrate.deferReleases() </code></pre> <h2 id="substrate-deferredreads">modules/proc.substrate/deferredReads</h2> <pre><code class="language-lua">deferredReads(fn: () -> any): any </code></pre> <p>Run <code>fn</code>, counting the reads it asks for in <code>traffic().deferredReads</code> rather than in <code>readbacks</code>: reads whose answers land in a later frame and that nothing waits for, such as the words a replay's guards are checked by.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> any</code> — The function to run.</li> </ul> <pre><code class="language-lua">local later = proc.substrate.deferredReads(function() return proc.substrate.readLater({ flag = flag }) end) </code></pre> <h2 id="substrate-detailnames">modules/proc.substrate/detailNames</h2> <pre><code class="language-lua">detailNames(self): { string } </code></pre> <p>The names of the values covering the whole geometry, sorted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-devicewords">modules/proc.substrate/deviceWords</h2> <pre><code class="language-lua">deviceWords(self): any? </code></pre> <p>The run in GPU storage that holds this column's own 32-bit words (<code>element()</code>): a <code>kernel</code> binding of <code>i32</code> or <code>u32</code> reads an <code>Int</code> or <code>UInt</code> column through it. A column already in GPU storage is itself; a CPU column keeps a mirror of its words that goes with it, as <code>gpuMirror</code>'s does.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local run = col:deviceWords() </code></pre> <h2 id="substrate-distinct">modules/proc.substrate/distinct</h2> <pre><code class="language-lua">distinct(self): { any } </code></pre> <p>The distinct values an interned column indexes into, 1-based.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-domain">modules/proc.substrate/domain</h2> <pre><code class="language-lua">domain(self, name: string): any </code></pre> <p>The domain named <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code> — "point", "vertex", "primitive" or "detail".</li> </ul> <h2 id="substrate-drain">modules/proc.substrate/drain</h2> <pre><code class="language-lua">drain(self): any </code></pre> <p>Bring every column the device holds back to the CPU, so the flat arrays derived from them can be read. This is what a mesh algorithm written over <code>positions</code> and <code>indices</code> pays to run on the CPU, paid ONCE and where it can be seen, rather than per column inside a read that cannot wait. A domain whose live elements are counted on the device (<code>Domain.liveCount</code>) is read as exactly those: its count comes back with its runs, and what is answered is a copy holding that many elements, since a CPU reader walks <code>count()</code> of them. A geometry a column carries as a value (the <code>mesh</code> of a packed piece's <code>Mesh</code> record) is drained with it, and a column carrying one that drains to a copy is answered in a copy carrying the drained one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">g = g:drain() </code></pre> <h2 id="substrate-drainall">modules/proc.substrate/drainAll</h2> <pre><code class="language-lua">drainAll(value: any): any </code></pre> <p>Bring every geometry <code>value</code> holds back to the CPU (<code>Geometry.drain</code>): a geometry itself, the sources and points of an instance set, and every element of an array. What a CPU reader is handed, so the flat arrays it walks can be read; a value holding nothing on the device costs a walk of its columns and no readback. A geometry counted on the device is answered as the copy <code>Geometry.drain</code> makes, and a value holding one as a copy holding that, so what <code>value</code> itself holds is left as it stands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — Any value an op or a realize step is about to read.</li> </ul> <pre><code class="language-lua">local g = proc.substrate.drainAll(inputs.geometry) </code></pre> <h2 id="substrate-drawversion">modules/proc.substrate/drawVersion</h2> <pre><code class="language-lua">drawVersion(self): string </code></pre> <p>Which version of its shape the geometry draws as: the versions of every column a mesh is built from, as one string. The same string exactly while none of them has been written, so a mesh built from the geometry stands for as long as this answers what it did when the mesh was made. A mesh is built with normals, and a geometry carrying none is given them here, the way <code>toMeshArgs</code> would, so the answer is the same before its mesh is made as after.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">if g:drawVersion() ~= held.version then print("the graph draws a new version") end </code></pre> <h2 id="substrate-dropreads">modules/proc.substrate/dropReads</h2> <pre><code class="language-lua">dropReads(self) </code></pre> <p>Let go of the flat arrays this column holds from being read: the run a read brought back to the CPU and the bytes a byte read brought back. The buffer the column stands on is untouched, and the next read takes them again. A reader that read a column once and keeps the column calls it so the column holds its buffer alone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-edges">modules/proc.substrate/edges</h2> <pre><code class="language-lua">edges(self): any </code></pre> <p>The geometry's edges on the device — each unordered point pair a side of its fan triangles joins, numbered as <code>proc.ops.topology</code> numbers them (see <code>proc.device.edges</code>). Derived on the first ask and held until the faces they come from change; the geometry lets them go with its own runs.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local edges = g:edges(); print(edges.capacity) </code></pre> <h2 id="substrate-element">modules/proc.substrate/element</h2> <pre><code class="language-lua">element(self): string </code></pre> <p>The 32-bit word form the column's run holds: <code>"i32"</code>, <code>"u32"</code> or <code>"f32"</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">if col:element() == "i32" then ... end </code></pre> <h2 id="substrate-elementcount">modules/proc.substrate/elementCount</h2> <pre><code class="language-lua">elementCount(self, class: string): number </code></pre> <p>How many elements an attribute class has. An edge is the unordered point pair a primitive uses rather than a row of its own, so its count is derived — <code>proc.ops.topology.elementCount(g, "edge")</code> answers it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>class</code> <code>string</code></li> </ul> <h2 id="substrate-elementof">modules/proc.substrate/elementOf</h2> <pre><code class="language-lua">elementOf(t: string): string </code></pre> <p>The 32-bit word form a column of type <code>t</code> holds: <code>"i32"</code> for <code>Int</code>, <code>"u32"</code> for <code>UInt</code>, <code>"f32"</code> for every other type.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — A procgen type tag.</li> </ul> <pre><code class="language-lua">proc.substrate.elementOf("Int") --> "i32" </code></pre> <h2 id="substrate-enddeferral">modules/proc.substrate/endDeferral</h2> <pre><code class="language-lua">endDeferral(held: any) </code></pre> <p>Stop holding the columns let go of: the ones held so far stay with <code>held</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>held</code> <code>any</code> <em>(optional)</em> — What <code>deferReleases</code> answered.</li> </ul> <pre><code class="language-lua">proc.substrate.endDeferral(held) </code></pre> <h2 id="substrate-following">modules/proc.substrate/following</h2> <pre><code class="language-lua">following(fn: (...any) -> ...any, ...: any): ...any </code></pre> <p>Run <code>fn</code> for a reader that follows the live runs it reads: it is presented again at every rewrite of what it read, so what it reads holds no moment's value, and those reads are not counted (<code>liveReads</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>(...any) -> ...any</code> — The function that reads.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local bytes = proc.substrate.following(column.readBytes, column) </code></pre> <h2 id="substrate-forgetdevice">modules/proc.substrate/forgetDevice</h2> <pre><code class="language-lua">forgetDevice() </code></pre> <p>Let go of every mirror after the render device is rebuilt. What each held stood on the device that went, so a run that reads one of those columns on the device puts it there again.</p> <pre><code class="language-lua">proc.substrate.forgetDevice() </code></pre> <h2 id="substrate-fromarrays">modules/proc.substrate/fromArrays</h2> <pre><code class="language-lua">fromArrays(data: { positions: { number }?, indices: { number }?, </code></pre> <p>A geometry holding the flat arrays a mesh algorithm produces: one point per position, one triangle per index triple, and <code>normals</code> / <code>uvs</code> as point columns. Every column is filled once, so building a mesh of any size costs one write per column rather than one per element.</p> <pre><code class="language-lua">proc.substrate.fromArrays({ positions = pos, indices = idx }) </code></pre> <h2 id="substrate-fromcurves">modules/proc.substrate/fromCurves</h2> <pre><code class="language-lua">fromCurves(positions: { number }, curves: { any }): any </code></pre> <p>A geometry of curves over one set of points: <code>positions</code> are the points, and each entry of <code>curves</code> is one curve through them — the point indices it runs through, in order, and whether it closes. Curves may share points, the way a branch starts where its parent passes. Every column is written once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>positions</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> <li><code>curves</code> <code>{ any }</code> — <code>{ { points = { 0-based point indices }, closed = boolean? }, ... }</code>.</li> </ul> <pre><code class="language-lua">proc.substrate.fromCurves({ 0,0,0, 1,0,0, 1,1,0 }, { { points = { 0, 1, 2 } } }) </code></pre> <h2 id="substrate-frompointcloud">modules/proc.substrate/fromPointCloud</h2> <pre><code class="language-lua">fromPointCloud(pc: any): any </code></pre> <p>A geometry holding the points of a <code>PointCloud</code> value: its positions as the point run, each attribute on the point domain, each detail as a detail value. What a socket declared as anything but <code>PointCloud</code> is handed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>pc</code> <code>any</code> <em>(optional)</em> — A PointCloud (<code>proc.PointCloud.new</code>, or one read from a graph document).</li> </ul> <pre><code class="language-lua">local g = proc.substrate.fromPointCloud(proc.PointCloud.new({ 0,0,0 })) </code></pre> <h2 id="substrate-frompositions">modules/proc.substrate/fromPositions</h2> <pre><code class="language-lua">fromPositions(positions: { number }): any </code></pre> <p>A geometry holding <code>positions</code> as its points and nothing else — what a source op makes. One buffer write, no growth pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>positions</code> <code>{ number }</code> — Flat <code>{ x0,y0,z0, ... }</code>.</li> </ul> <pre><code class="language-lua">proc.substrate.fromPositions({ 0,0,0, 1,0,0 }) </code></pre> <h2 id="substrate-gather">modules/proc.substrate/gather</h2> <pre><code class="language-lua">gather(self, keep: { number }): any </code></pre> <p>A column of the same type holding the elements <code>keep</code> names, in that order — what a filter, a sort and a delete all come down to.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>keep</code> <code>{ number }</code> — An array of 0-based element indices.</li> </ul> <pre><code class="language-lua">local kept = col:gather({ 0, 3, 4 }) </code></pre> <h2 id="substrate-get">modules/proc.substrate/get</h2> <pre><code class="language-lua">get(self, id: number): ...any </code></pre> <p>The value at element <code>id</code> (0-based): <code>lanes</code> numbers for a numeric column, the value itself for one carried as values. Per-element reads are for a small column or a probe; a column of any size is handed to a kernel or a batch write whole.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>number</code> — 0-based element index.</li> </ul> <pre><code class="language-lua">local x, y, z = col:get(7) </code></pre> <h2 id="substrate-getattribute">modules/proc.substrate/getAttribute</h2> <pre><code class="language-lua">getAttribute(self, class: string, name: string): any </code></pre> <p>Read a named attribute. An element class answers the column, which is what a field is asked for too; detail answers the value it was set with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>class</code> <code>string</code></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="substrate-getdetail">modules/proc.substrate/getDetail</h2> <pre><code class="language-lua">getDetail(self, name: string): any </code></pre> <p>A value covering the whole geometry, or nil.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="substrate-gpu">modules/proc.substrate/gpu</h2> <pre><code class="language-lua">gpu(t: string, count: number, usage: BufferUsage?): any </code></pre> <p>A numeric column whose buffer lives in GPU storage — the same handle a compute dispatch binds. Reading one back on the CPU is a readback, so a column meant to be read asks for it here.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type tag.</li> <li><code>count</code> <code>number</code> — Element count.</li> <li><code>usage</code> <code>BufferUsage?</code> <em>(optional)</em> — Extra buffer usage, e.g. <code>{ "readback" }</code>.</li> </ul> <pre><code class="language-lua">proc.substrate.Column.gpu("Vec3", 30000, { "readback" }) </code></pre> <h2 id="substrate-gpumirror">modules/proc.substrate/gpuMirror</h2> <pre><code class="language-lua">gpuMirror(self): any? </code></pre> <p>This column's run in GPU storage — itself when it already lives there, else a mirror the column keeps.</p> <p>A compute dispatch binds GPU storage, so a run the CPU holds is uploaded once and the mirror is what a dispatch reads. The mirror belongs to the column: it is dropped the moment anything takes the right to write the buffer under it, and freed when the column is released. So a run that does not change is uploaded once however many dispatches read it, and a run that does not outlive its column does not outlive it on the device either.</p> <p>A column something keeps longer than the owner that made it (<code>Column.adopt</code>) keeps its mirror for that keeper, whenever the mirror is made.</p> <p>Answers nil for a column carried as values, which has no run to mirror, and for one the device refused storage for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local run = col:gpuMirror() </code></pre> <h2 id="substrate-holdcarried">modules/proc.substrate/holdCarried</h2> <pre><code class="language-lua">holdCarried(holder: any, value: any) </code></pre> <p>Make <code>holder</code> hold <code>value</code>, a value it carries (a geometry a record of its columns holds) that the cook which made it lets go of: the value stands while any holder of it stands (a copy of a holder holds it too), and is let go of with the last of them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>holder</code> <code>any</code> <em>(optional)</em> — A Geometry, or a Column carried as values, whose columns carry <code>value</code>.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — A value with a release hook (a Geometry or a Column).</li> </ul> <pre><code class="language-lua">proc.substrate.holdCarried(pieces, mesh) </code></pre> <h2 id="substrate-holdfor">modules/proc.substrate/holdFor</h2> <pre><code class="language-lua">holdFor(owner: any) </code></pre> <p>Hold every device run a column makes on this thread, for the rest of the thread's life, for <code>owner</code> — a name, or a function answering it when a run is made. What a worker thread doing one owner's work calls once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>owner</code> <code>any</code> <em>(optional)</em> — The owner, or a function answering it.</li> </ul> <pre><code class="language-lua">proc.substrate.holdFor(function() return cacheOwner end) </code></pre> <h2 id="substrate-holdsids">modules/proc.substrate/holdsIds</h2> <pre><code class="language-lua">holdsIds(self, name: string): boolean </code></pre> <p>Whether the column named <code>name</code> is marked as holding point <code>id</code>s.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code> — The attribute's name.</li> </ul> <h2 id="substrate-holdsstandin">modules/proc.substrate/holdsStandIn</h2> <pre><code class="language-lua">holdsStandIn(value: any): boolean </code></pre> <p>Whether a value an op answered holds a stand-in for a column on the device (<code>structureOf</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — What was answered.</li> </ul> <h2 id="substrate-idcolumns">modules/proc.substrate/idColumns</h2> <pre><code class="language-lua">idColumns(self, class: string): { string } </code></pre> <p>The names of the columns of <code>class</code> that are marked as holding point <code>id</code>s, sorted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>class</code> <code>string</code> — "point", "vertex" or "primitive".</li> </ul> <h2 id="substrate-index">modules/proc.substrate/index</h2> <pre><code class="language-lua">index(self): any </code></pre> <p>The run of indices an interned column holds: one Float per element, each an index into its distinct values.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local run = col:index():gpuMirror() </code></pre> <h2 id="substrate-intern">modules/proc.substrate/intern</h2> <pre><code class="language-lua">intern(self): any </code></pre> <p>This column as indices into its distinct values (<code>Column.interned</code>), which is how a column carried as values is placed on the device: it keeps what it is when it already is one, and a column carried as values is interned once — its distinct values in the order they first appear — and remembers the answer. A text is distinct by its characters, any other value by its identity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local idx = col:intern():index() </code></pre> <h2 id="substrate-interned">modules/proc.substrate/interned</h2> <pre><code class="language-lua">interned(t: string, values: { any }, index: any): any </code></pre> <p>A column carried as values whose elements are placed on the device as numbers: each element is an index into <code>values</code>, the distinct values the column holds, and <code>index</code> is that run of indices — a Float column, on the CPU or the device. A pass over elements moves the indices and carries the same <code>values</code>; a reader on the CPU gets each element's value exactly as it gets one from <code>Column.values</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type tag.</li> <li><code>values</code> <code>{ any }</code> — The distinct values, 1-based; index 0 names the first.</li> <li><code>index</code> <code>any</code> <em>(optional)</em> — A Float column of 0-based indices into <code>values</code>, one per element.</li> </ul> <pre><code class="language-lua">local labels = proc.substrate.Column.interned("String", { "oak", "pine" }, idx) </code></pre> <h2 id="substrate-isconstant">modules/proc.substrate/isConstant</h2> <pre><code class="language-lua">isConstant(self): boolean </code></pre> <p>Whether the column is one value standing for every element, which holds no run.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-iscountcolumn">modules/proc.substrate/isCountColumn</h2> <pre><code class="language-lua">isCountColumn(v: any): boolean </code></pre> <p>Whether a value is a count a Column holds as one word: what a domain's <code>.count</code> answers while its count is on the device.</p> <p><strong>Parameters</strong></p> <ul> <li><code>v</code> <code>any</code> <em>(optional)</em> — Any value.</li> </ul> <h2 id="substrate-iscountvalue">modules/proc.substrate/isCountValue</h2> <pre><code class="language-lua">isCountValue(v: any): boolean </code></pre> <p>Whether a value stands for one number held on the device: a count Column, or a Field made only of counts and constants (arithmetic over a count). <code>readCount</code> reads either.</p> <p><strong>Parameters</strong></p> <ul> <li><code>v</code> <code>any</code> <em>(optional)</em> — Any value.</li> </ul> <h2 id="substrate-iscurve">modules/proc.substrate/isCurve</h2> <pre><code class="language-lua">isCurve(self, prim: number): boolean </code></pre> <p>Whether primitive <code>prim</code> is a curve rather than a face.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>prim</code> <code>number</code> — 0-based primitive index.</li> </ul> <h2 id="substrate-isgpu">modules/proc.substrate/isGpu</h2> <pre><code class="language-lua">isGpu(self): boolean </code></pre> <p>Whether the column's buffer lives in GPU storage.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-isnumeric">modules/proc.substrate/isNumeric</h2> <pre><code class="language-lua">isNumeric(t: string): boolean </code></pre> <p>Whether a column of <code>t</code> is backed by a substrate buffer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — A procgen type tag.</li> </ul> <h2 id="substrate-isshared">modules/proc.substrate/isShared</h2> <pre><code class="language-lua">isShared(self): boolean </code></pre> <p>Whether every element holds one shared value — true for a column still on its constant backing. What a reader gets is the same either way.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-join">modules/proc.substrate/join</h2> <pre><code class="language-lua">join(self: any, col: any) </code></pre> <p>Add the run of <code>col</code> to the runs this counter counts writes of, from its next note on. A run already in it, and a column holding no run on the device, are left as they are.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>col</code> <code>any</code> <em>(optional)</em> — The Column.</li> </ul> <pre><code class="language-lua">counter:join(col) </code></pre> <h2 id="substrate-keep">modules/proc.substrate/keep</h2> <pre><code class="language-lua">keep(self, run: any) </code></pre> <p>Keep <code>run</code> alive with this column: it is released with the column, and held for whoever keeps the column (<code>Column.adopt</code>), now or later.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>run</code> <code>any</code> <em>(optional)</em> — A Column whose run this column reads from.</li> </ul> <pre><code class="language-lua">col:keep(converted) </code></pre> <h2 id="substrate-lanes">modules/proc.substrate/lanes</h2> <pre><code class="language-lua">lanes(self): number </code></pre> <p>Numbers per element, 0 for a column carried as values.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-lanesof">modules/proc.substrate/lanesOf</h2> <pre><code class="language-lua">lanesOf(t: string): number </code></pre> <p>How many numbers one element of <code>t</code> occupies, or 0 when the type is carried as values rather than as numbers.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — A procgen type tag ("Vec3", "String", "Mesh", ...).</li> </ul> <pre><code class="language-lua">proc.substrate.lanesOf("Vec3") --> 3 </code></pre> <h2 id="substrate-live">modules/proc.substrate/live</h2> <pre><code class="language-lua">live(self): any? </code></pre> <p>The registration that rewrites this run in place, or nil for a run nothing rewrites.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local reg = col:live() </code></pre> <h2 id="substrate-livecount">modules/proc.substrate/liveCount</h2> <pre><code class="language-lua">liveCount(self): any? </code></pre> <p>How many of the domain's elements are live, as a count on the device — or nil when every element counted by <code>count()</code> is. A domain whose membership is decided on the device (the particles a solver keeps, the points a compaction kept) holds its runs at a capacity <code>count()</code> answers and its live count here, so no pass reads it back to know how many there are: a kernel over the domain reads the count where it is and leaves every element past it alone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-livemint">modules/proc.substrate/liveMint</h2> <pre><code class="language-lua">liveMint(): number </code></pre> <p>A number for a live value being made (<code>proc.fieldc.live</code>), to be handed to <code>noteLiveRead</code> when it is read: a window opened before the value was made sees a read of it as one of its own time.</p> <pre><code class="language-lua">local mint = proc.substrate.liveMint() </code></pre> <h2 id="substrate-livereads">modules/proc.substrate/liveReads</h2> <pre><code class="language-lua">liveReads(): number </code></pre> <p>How many times the calling thread has read a live run — or a live value — on the CPU. Two equal answers mean it took no snapshot of one in between, whatever other threads read meanwhile.</p> <pre><code class="language-lua">local before = proc.substrate.liveReads() </code></pre> <h2 id="substrate-livesize">modules/proc.substrate/liveSize</h2> <pre><code class="language-lua">liveSize(count: any): (number?, any?) </code></pre> <p>The room and the one-word counter of a count that sizes work: a count Column carrying its capacity, or arithmetic over counts that only grows with them (the capacity is what it comes to with every count full, the counter is that arithmetic evaluated on the device).</p> <p><strong>Parameters</strong></p> <ul> <li><code>count</code> <code>any</code> <em>(optional)</em> — A count Column or a Field made of counts and constants.</li> </ul> <pre><code class="language-lua">local room, counter = proc.substrate.liveSize(geometry.point.count * 3) </code></pre> <h2 id="substrate-marklive">modules/proc.substrate/markLive</h2> <pre><code class="language-lua">markLive(self, registration: any) </code></pre> <p>Declare this GPU run to be rewritten in place by <code>registration</code>, which is what <code>live</code> answers for it and for every clone of it. A registration names the live registrations of the runs it reads in <code>deps</code>, and either the compiled kernel that rewrites it (<code>kernel</code>, <code>buffers</code>, <code>count</code>, <code>paramsRun</code>, <code>values</code>, <code>live</code>) or a <code>rewrite(clock)</code> that brings it to a new time. A rewrite that queues the same dispatches over the same runs whatever the clock says carries <code>record = true</code>, and is recorded on its first rewrite and replayed after (<code>proc.device.record</code>); what holds the rewritten runs lets the recording go with <code>proc.fieldc.releaseRecording(registration)</code>. One registration may stand for several runs one rewrite writes together.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>registration</code> <code>any</code> <em>(optional)</em> — What rewrites the run.</li> </ul> <pre><code class="language-lua">out:markLive({ deps = {}, rewrite = function(clock) end }) </code></pre> <h2 id="substrate-materialize">modules/proc.substrate/materialize</h2> <pre><code class="language-lua">materialize(self, count: number?): { number } </code></pre> <p>The flat run of <code>count</code> elements, under the name a field answers to. A column already holds it, so this is the read rather than a walk of one sample per element.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>count</code> <code>number?</code> <em>(optional)</em> — How many elements to answer for.</li> </ul> <h2 id="substrate-merge">modules/proc.substrate/merge</h2> <pre><code class="language-lua">merge(parts: { any }, absent: ((string) -> { number }?)?): any </code></pre> <p>Every part laid end to end in one geometry: each domain's elements in part order, every column any part carries (a part without one fills its elements with the value <code>absent</code> names for the column, or zeros), the vertices re-pointed at their own part's points and the primitives at their own part's vertices. Ids stay identity: an element's id is its own id times the part count plus its part's index, so every element of a part keeps one id however the other parts change. The detail values are the first part's. Parts that count any domain's elements on the device, or hold runs there, are merged there: in every domain each part's live elements after the live elements before it, the whole counted on the device, the indices into another domain moved past that domain's elements before them, a point's <code>slot</code> moved past the capacities of the parts before it, and an attribute indexing the labels a part's detail names moved past the labels before it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>parts</code> <code>{ any }</code> — The geometries, in order.</li> <li><code>absent</code> <code>((string) -> { number }?)?</code> <em>(optional)</em> — The value, one number per lane, the elements of a part that carries no column named so hold: <code>proc.translate.absentValue</code> gives every element the rest its components stand at.</li> </ul> <pre><code class="language-lua">local both = proc.substrate.Geometry.merge({ a, b }) </code></pre> <h2 id="substrate-mintname">modules/proc.substrate/mintName</h2> <pre><code class="language-lua">mintName(prefix: string): string </code></pre> <p><code>prefix</code> followed by a number no VM of the engine was handed before (<code>substrate.mintName</code>): a name for something procgen files by name, unique across every VM a cook runs in.</p> <p><strong>Parameters</strong></p> <ul> <li><code>prefix</code> <code>string</code> — What the name starts with.</li> </ul> <pre><code class="language-lua">local key = proc.substrate.mintName("proc.translate.") </code></pre> <h2 id="substrate-mintowner">modules/proc.substrate/mintOwner</h2> <pre><code class="language-lua">mintOwner(kind: string): string </code></pre> <p>A fresh owner name, unique across every VM of the engine, for what makes runs and lets go of them together (<code>proc.substrate.withOwner</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>kind</code> <code>string</code> — What the owner is, the name's prefix.</li> </ul> <pre><code class="language-lua">local owner = proc.substrate.mintOwner("cache") </code></pre> <h2 id="substrate-movedat">modules/proc.substrate/movedAt</h2> <pre><code class="language-lua">movedAt(v: any): number </code></pre> <p>The count (<code>columnMoves</code>) at which a column was last let go of its run or given another, or a domain last had a column put in or taken out; 0 for one that has not moved. A holder that read a value's columns at one stamp knows they stand while the stamps of the columns and domains it read are the same, whatever moved elsewhere.</p> <p><strong>Parameters</strong></p> <ul> <li><code>v</code> <code>any</code> <em>(optional)</em> — A Column or a Domain.</li> </ul> <pre><code class="language-lua">local at = proc.substrate.movedAt(geometry:domain("point")) </code></pre> <h2 id="substrate-name">modules/proc.substrate/name</h2> <pre><code class="language-lua">name(self): string </code></pre> <p>The domain's name: "point", "vertex", "primitive" or "detail".</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-names">modules/proc.substrate/names</h2> <pre><code class="language-lua">names(self): { string } </code></pre> <p>Every attribute name on the domain, sorted, so iteration is the same on every run and a cook key over them is stable.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-new">modules/proc.substrate/new</h2> <pre><code class="language-lua">new(): any </code></pre> <p>An empty geometry: no points, no vertices, no primitives, one detail element.</p> <pre><code class="language-lua">local g = proc.substrate.Geometry.new() </code></pre> <h2 id="substrate-note">modules/proc.substrate/note</h2> <pre><code class="language-lua">note(self: any) </code></pre> <p>Count one write of every run in this counter: every read of any of them taken before stops being an answer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">counter:note() </code></pre> <h2 id="substrate-noteliveread">modules/proc.substrate/noteLiveRead</h2> <pre><code class="language-lua">noteLiveRead(mint: number?) </code></pre> <p>Count one CPU read of a live value — what a value that stands for a live run's input, rather than a run itself, reports when it is sampled. <code>mint</code> is the number the value was made under (<code>liveMint</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>mint</code> <code>number?</code> <em>(optional)</em> — The value's number, or nil for one made before any window.</li> </ul> <pre><code class="language-lua">proc.substrate.noteLiveRead(mint) </code></pre> <h2 id="substrate-noterewrite">modules/proc.substrate/noteRewrite</h2> <pre><code class="language-lua">noteRewrite() </code></pre> <p>Say that live runs were rewritten on the device: a read of one taken before this no longer holds, and the next read of it comes back again.</p> <pre><code class="language-lua">proc.substrate.noteRewrite() </code></pre> <h2 id="substrate-notewritten">modules/proc.substrate/noteWritten</h2> <pre><code class="language-lua">noteWritten(self) </code></pre> <p>Note that a kernel wrote this column's run in place: every read of the run taken before, by this column or any other holder of the run, stops being an answer. What a pass writing into a run that already stands calls for each run it writes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">col:noteWritten() </code></pre> <h2 id="substrate-numbers">modules/proc.substrate/numbers</h2> <pre><code class="language-lua">numbers(t: string, count: number, values: { number }?): any </code></pre> <p>A numeric column of <code>count</code> elements, backed by a substrate buffer. <code>values</code>, when given, is the flat <code>count * lanes</code> run written into it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type tag, one that occupies lanes.</li> <li><code>count</code> <code>number</code> — Element count.</li> <li><code>values</code> <code>{ number }?</code> <em>(optional)</em> — Optional flat array to write.</li> </ul> <pre><code class="language-lua">proc.substrate.Column.numbers("Vec3", 2, { 0,0,0, 1,0,0 }) </code></pre> <h2 id="substrate-oncountread">modules/proc.substrate/onCountRead</h2> <pre><code class="language-lua">onCountRead(hook: ((any, number) -> ())?) </code></pre> <p>Say what to do with every count read where a number is needed: the evaluator records the read of a cook being recorded as a list, so a replay asks it again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>hook</code> <code>((any, number) -> ())?</code> <em>(optional)</em> — <code>hook(column, number)</code>, or nil.</li> </ul> <h2 id="substrate-ondevice">modules/proc.substrate/onDevice</h2> <pre><code class="language-lua">onDevice(self): boolean </code></pre> <p>Whether any column the geometry draws from is held on the device: its positions, its corners, its faces or its live counts. A geometry that is becomes a mesh with <code>toDeviceMesh</code>, which leaves it there.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em> — Geometry.</li> </ul> <pre><code class="language-lua">if g:onDevice() then local m = g:toDeviceMesh("rock") end </code></pre> <h2 id="substrate-openlivewindow">modules/proc.substrate/openLiveWindow</h2> <pre><code class="language-lua">openLiveWindow(): any </code></pre> <p>Open a window over the work that follows on this thread, closed with <code>closeLiveWindow</code>: whether that work read on the CPU a live run, or a live value made before the window opened, and so holds a moment's value.</p> <pre><code class="language-lua">local window = proc.substrate.openLiveWindow() </code></pre> <h2 id="substrate-owner">modules/proc.substrate/owner</h2> <pre><code class="language-lua">owner(): string? </code></pre> <p>The owner device runs made on this thread now are held for, or nil.</p> <h2 id="substrate-position">modules/proc.substrate/position</h2> <pre><code class="language-lua">position(self, id: number): (number, number, number) </code></pre> <p>Where point <code>id</code> is, as <code>x, y, z</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>number</code> — 0-based point index.</li> </ul> <pre><code class="language-lua">local x, y, z = g:position(0) </code></pre> <h2 id="substrate-prefetch">modules/proc.substrate/prefetch</h2> <pre><code class="language-lua">prefetch(self, asBytes: boolean?): boolean </code></pre> <p>Ask for this column's run to come back to the CPU without waiting for it, and answer whether it has: true once a read that still holds has arrived, after which <code>read</code> answers without waiting. A column the CPU already holds answers true at once. What a reader that must not wait — a frame's tick — polls before it reads.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>asBytes</code> <code>boolean?</code> <em>(optional)</em> — Whether the read wanted is <code>readBytes</code>'s rather than <code>read</code>'s.</li> </ul> <pre><code class="language-lua">if col:prefetch() then print(col:read()) end </code></pre> <h2 id="substrate-primitivepoints">modules/proc.substrate/primitivePoints</h2> <pre><code class="language-lua">primitivePoints(self, prim: number): { number } </code></pre> <p>The point indices a primitive uses, in order.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>prim</code> <code>number</code> — 0-based primitive index.</li> </ul> <h2 id="substrate-read">modules/proc.substrate/read</h2> <pre><code class="language-lua">read(self): { number } </code></pre> <p>The numbers of a column. A column that carries a live count (an appended output, a column of a geometry whose class counts its live elements on the device) answers the live elements only: the slots past the count are never in the answer, and the count is read back once for it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-readbytes">modules/proc.substrate/readBytes</h2> <pre><code class="language-lua">readBytes(self): buffer </code></pre> <p>The run this column holds, AS BYTES — the same memory the buffer has, without a number per element passing through Luau on the way.</p> <p>That is the difference between a copy and a walk: <code>read</code> builds one Luau value per component, and a run of five thousand vec3s is fifteen thousand of them made, held and collected every time it is read. This answers the block, which is what another buffer's <code>writeBytes</code> takes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">dst:buffer():writeBytes(src:readBytes()) </code></pre> <h2 id="substrate-readcount">modules/proc.substrate/readCount</h2> <pre><code class="language-lua">readCount(col: any): number </code></pre> <p>The number a count Column holds, read back at the point that needs a number. The read is one read-back, counted in <code>traffic().countReads</code> apart from the reads a node makes of its own, and reported to the hook of <code>onCountRead</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>col</code> <code>any</code> <em>(optional)</em> — A count Column (<code>isCountColumn</code>).</li> </ul> <pre><code class="language-lua">local n = proc.substrate.readCount(geometry.point.count) </code></pre> <h2 id="substrate-readlater">modules/proc.substrate/readLater</h2> <pre><code class="language-lua">readLater(columns: { [string]: any }): any </code></pre> <p>Read columns back to the CPU without waiting for them, each answer about the frame its read was asked in. <code>poll()</code> answers the numbers of every column, by name (<code>lanes</code> per slot, as <code>read</code> answers them), once the reads asked for together have all landed, and nil until they have; whenever no read is in flight it asks for the columns as they stand now. A read the engine gave up is asked for again. A run the device rewrites frame after frame is read here as the value of one frame, and takes no live read: what takes the answer holds it as that frame's. <code>release()</code> lets go of the columns, which the reader holds while it reads them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>columns</code> <code>{ [string]: any }</code> — The columns, by name.</li> </ul> <pre><code class="language-lua">local later = proc.substrate.readLater({ total = count }); local landed = later.poll() </code></pre> <h2 id="substrate-readslots">modules/proc.substrate/readSlots</h2> <pre><code class="language-lua">readSlots(col: any): { number } </code></pre> <p>Every slot of a column's run, the ones past a live count included: what a mirror of the run holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>col</code> <code>any</code> <em>(optional)</em> — A Column.</li> </ul> <h2 id="substrate-recalcnormals">modules/proc.substrate/recalcNormals</h2> <pre><code class="language-lua">recalcNormals(self): any </code></pre> <p>Recompute the per-point normal column <code>N</code> from the primitives: each face's unnormalized cross product, which carries twice its area, is added to every point it touches and the sum is normalized — so a large face weighs on a shared point in proportion to how much of it there is. A point no primitive gives a direction gets (0, 1, 0).</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-relaxpoints">modules/proc.substrate/relaxPoints</h2> <pre><code class="language-lua">relaxPoints(column: any, iterations: number, radius: number, strength: number): any </code></pre> <p>Push every point off its neighbours inside <code>radius</code>, <code>iterations</code> times, on the device, finding them through a spatial grid. What a scatter runs to even itself out.</p> <p><strong>Parameters</strong></p> <ul> <li><code>column</code> <code>any</code> <em>(optional)</em> — The point positions, a Vec3 column.</li> <li><code>iterations</code> <code>number</code> — How many passes.</li> <li><code>radius</code> <code>number</code> — The neighbourhood each point pushes within.</li> <li><code>strength</code> <code>number</code> — How far a point moves per pass, as a fraction of <code>radius</code>.</li> </ul> <pre><code class="language-lua">local relaxed = proc.substrate.relaxPoints(P, 5, 0.4, 0.5) </code></pre> <h2 id="substrate-release">modules/proc.substrate/release</h2> <pre><code class="language-lua">release(self: any) </code></pre> <p>Take every run out of this counter, each keeping the writes it was counted as its own; later notes count nothing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">counter:release() </code></pre> <h2 id="substrate-releasedeferred">modules/proc.substrate/releaseDeferred</h2> <pre><code class="language-lua">releaseDeferred(held: any) </code></pre> <p>Let go of every column <code>held</code> holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>held</code> <code>any</code> <em>(optional)</em> — What <code>deferReleases</code> answered.</li> </ul> <pre><code class="language-lua">proc.substrate.releaseDeferred(held) </code></pre> <h2 id="substrate-removeattribute">modules/proc.substrate/removeAttribute</h2> <pre><code class="language-lua">removeAttribute(self, class: string, name: string): any </code></pre> <p>Drop a named attribute, freeing what it held.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>class</code> <code>string</code></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="substrate-removecolumn">modules/proc.substrate/removeColumn</h2> <pre><code class="language-lua">removeColumn(self, name: string): any </code></pre> <p>Drop the column named <code>name</code>, freeing its buffer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> </ul> <h2 id="substrate-restoreowner">modules/proc.substrate/restoreOwner</h2> <pre><code class="language-lua">restoreOwner(slot: any, previous: any, ok: boolean, ...: any): ...any </code></pre> <p>Call <code>fn(...)</code> with every device run a column makes during it held for <code>owner</code>, on this thread, and answer what <code>fn</code> answers. <code>owner</code> is a name, or a function answering the name when a run is made. A run's owner lets go of it with <code>substrate.releaseOwner(owner)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>slot</code> <code>any</code> <em>(optional)</em></li> <li><code>previous</code> <code>any</code> <em>(optional)</em></li> <li><code>ok</code> <code>boolean</code></li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">proc.substrate.withOwner(owner, function() return proc.eval(graph) end) </code></pre> <h2 id="substrate-sample">modules/proc.substrate/sample</h2> <pre><code class="language-lua">sample(self, id: number): ...any </code></pre> <p>The value at element <code>id</code>, under the name a field answers to.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>number</code> — 0-based element index.</li> </ul> <h2 id="substrate-scratch">modules/proc.substrate/scratch</h2> <pre><code class="language-lua">scratch(t: string, count: number): any </code></pre> <p>A numeric column in GPU storage whose run may have held another column's: its elements hold nothing the caller did not write. It is for a run a dispatch writes — every element, or the elements a count bounds. Its run is a range of the device arenas' pages, which a range released a few frames ago is handed out again as, so making one costs no device allocation.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type tag.</li> <li><code>count</code> <code>number</code> — Element count.</li> </ul> <pre><code class="language-lua">local out = proc.substrate.Column.scratch("Vec3", 4096) </code></pre> <h2 id="substrate-set">modules/proc.substrate/set</h2> <pre><code class="language-lua">set(self, id: number, ...: any): any </code></pre> <p>Write element <code>id</code>. A constant column materializes here, and no reader of either form can tell which it was given.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>id</code> <code>number</code> — 0-based element index.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">col:set(7, 1, 2, 3) </code></pre> <h2 id="substrate-setattribute">modules/proc.substrate/setAttribute</h2> <pre><code class="language-lua">setAttribute(self, class: string, name: string, value: any): any </code></pre> <p>Write a named attribute on a domain class. A column is stored as it stands; a field is read onto one, which is where a chain behind it is spent; a detail value is held as itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>class</code> <code>string</code> — "point", "vertex", "primitive" or "detail".</li> <li><code>name</code> <code>string</code> — The attribute's name.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — A Column, a Field, or — on detail — any value.</li> </ul> <h2 id="substrate-setcolumn">modules/proc.substrate/setColumn</h2> <pre><code class="language-lua">setColumn(self, name: string, column: any): any </code></pre> <p>Attach <code>column</code> under <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code> — The attribute's name, which means nothing to the substrate.</li> <li><code>column</code> <code>any</code> <em>(optional)</em> — A Column.</li> </ul> <h2 id="substrate-setcount">modules/proc.substrate/setCount</h2> <pre><code class="language-lua">setCount(self, n: number): any </code></pre> <p>Set the domain's element count. A constant column covers the new count at no cost; a written one keeps the buffer it has.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>n</code> <code>number</code></li> </ul> <h2 id="substrate-setdetail">modules/proc.substrate/setDetail</h2> <pre><code class="language-lua">setDetail(self, name: string, value: any): any </code></pre> <p>Write a value that covers the whole geometry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code></li> <li><code>value</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-setholdsids">modules/proc.substrate/setHoldsIds</h2> <pre><code class="language-lua">setHoldsIds(self, name: string, flag: boolean): any </code></pre> <p>Say whether the column named <code>name</code> holds the <code>id</code>s of this geometry's points: a join of geometries moves such a column's values with the ids they name, and a selection leaves no value naming a point it removed. The mark belongs to the attribute, so a node that rewrites the column keeps it, and it goes when the column is removed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string</code> — The attribute's name.</li> <li><code>flag</code> <code>boolean</code> — True to mark the column, false to clear the mark.</li> </ul> <h2 id="substrate-setlivecount">modules/proc.substrate/setLiveCount</h2> <pre><code class="language-lua">setLiveCount(self, count: any?): any </code></pre> <p>Set the live count (see <code>liveCount</code>), or clear it with nil. The domain holds the count it is given.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>count</code> <code>any?</code> <em>(optional)</em> — A one-word Column holding a u32, or nil.</li> </ul> <h2 id="substrate-setownergroup">modules/proc.substrate/setOwnerGroup</h2> <pre><code class="language-lua">setOwnerGroup(owner: string, key: string?) </code></pre> <p>Place every device run made for <code>owner</code> from now on in the pages the group <code>key</code> owns, wherever the work runs, as <code>withPageGroup</code> does for one call: work that makes the same runs in the same order stands in the same pages each time, so the kernels that bind them are the same programs. A thread's own <code>withPageGroup</code> stands over it. Nil puts the owner's runs back in the pages every run shares.</p> <p><strong>Parameters</strong></p> <ul> <li><code>owner</code> <code>string</code> — The owner (<code>mintOwner</code>).</li> <li><code>key</code> <code>string?</code> <em>(optional)</em> — The group's name, or nil.</li> </ul> <pre><code class="language-lua">proc.substrate.setOwnerGroup(owner, "generator:" .. contentSignature) </code></pre> <h2 id="substrate-setpointnumbers">modules/proc.substrate/setPointNumbers</h2> <pre><code class="language-lua">setPointNumbers(g: any, name: string, t: string, values: { number }): any </code></pre> <p>Attach a point column under <code>name</code> — the shorthand a source op writing an attribute alongside its positions takes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>g</code> <code>any</code> <em>(optional)</em> — A Geometry.</li> <li><code>name</code> <code>string</code> — The attribute's name.</li> <li><code>t</code> <code>string</code> — The element type tag.</li> <li><code>values</code> <code>{ number }</code> — Flat <code>count * lanes</code> numbers.</li> </ul> <pre><code class="language-lua">proc.substrate.setPointNumbers(g, "normal", "Vec3", nrm) </code></pre> <h2 id="substrate-stride">modules/proc.substrate/stride</h2> <pre><code class="language-lua">stride(self): number </code></pre> <p>How many numbers one element holds — what a field calls the same question. A column IS a field whose values have already been read, so an op that samples a field samples a column without knowing which it was given.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-structureof">modules/proc.substrate/structureOf</h2> <pre><code class="language-lua">structureOf(value: any): (any, { touched: boolean }?) </code></pre> <p>What <code>value</code> looks like to a reader that asks only for structure: every geometry it holds with its columns on the device standing in as columns that answer their type, width, length and run and nothing else. Asking for the numbers of one, or for the count of a class counted on the device, sets <code>state.touched</code>; the reader is then handed <code>drainAll</code> instead. A part, or a stand-in left in what the reader answers, has no such view.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — Any value an op is about to read.</li> </ul> <pre><code class="language-lua">local view, state = proc.substrate.structureOf(inputs.geometry) </code></pre> <h2 id="substrate-tagforstride">modules/proc.substrate/tagForStride</h2> <pre><code class="language-lua">tagForStride(t: string, stride: number): string </code></pre> <p>The type tag to hold a run of <code>stride</code> numbers an element under. <code>t</code> is kept when its lanes agree with the stride — so an <code>Int</code> or a <code>Color</code> stays what it is — and otherwise the tag for that many lanes answers, which is what a run known only by its width is.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The tag the run came with.</li> <li><code>stride</code> <code>number</code> — Numbers per element.</li> </ul> <pre><code class="language-lua">proc.substrate.tagForStride("Float", 3) --> "Vec3" </code></pre> <h2 id="substrate-todevicemesh">modules/proc.substrate/toDeviceMesh</h2> <pre><code class="language-lua">toDeviceMesh(self, name: string?, opts: { measure: boolean? }?): any? </code></pre> <p>The geometry as the renderer takes it, built on the device: a vertex buffer and an index buffer the renderer draws from where they are, the way <code>toMeshArgs</code> lays them out — one GPU vertex per point when no corner carries a normal or uv of its own, one per corner otherwise, and every face fanned from its first corner. Normals come from the corners, else the points, else the faces. Two numbers come back to size the draw and frame it: how many triangles there are, and the positions' extent, both asked for before either is waited on. With <code>measure = false</code> neither is waited on and the mesh is ready the moment this returns: the draw covers every index the index run holds, those past the last face being empty triangles that draw nothing. The extent is still asked for, and comes back as <code>extent</code>, two runs whose reads are on their way: the caller states it on the mesh (<code>renderer.mesh.update</code> with <code>aabbMin</code> / <code>aabbMax</code>) once they arrive, since the extent is what the mesh is culled against and framed by.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em> — Geometry.</li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Debug name carried on the resulting mesh.</li> <li><code>opts</code> <code>{ measure: boolean? }?</code> <em>(optional)</em> — <code>{ measure: boolean? }</code> — false skips the wait for the triangle count and the extent.</li> </ul> <pre><code class="language-lua">local built = g:toDeviceMesh("rock"); local mesh = renderer.mesh.create(built.args) local card = g:toDeviceMesh("card", { measure = false }) </code></pre> <h2 id="substrate-tomeshargs">modules/proc.substrate/toMeshArgs</h2> <pre><code class="language-lua">toMeshArgs(self, name: string?): { name: string?, positions: { number }, </code></pre> <p>The geometry as the renderer takes it: one GPU vertex per VERTEX, and triangles over them.</p> <p>A GPU vertex carries one position, one normal and one UV, so a point read by two primitives that disagree about any of those is two GPU vertices. That is what the vertex domain is: the split lives there, and this is where it is spent. A point every primitive agrees about still costs one vertex each, which is the price of a format that carries no sharing.</p> <p><code>normal</code> and <code>uv</code> are read from the vertex domain first, then the point domain, so a mesh that splits its normals says so per vertex and one that does not says so once per point. With neither, the normals are computed from the faces. A primitive of more than three vertices is fanned.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Debug name carried on the resulting mesh.</li> </ul> <pre><code class="language-lua">renderer.mesh.create(g:toMeshArgs("rock")) </code></pre> <h2 id="substrate-topointcloud">modules/proc.substrate/toPointCloud</h2> <pre><code class="language-lua">toPointCloud(g: any): any </code></pre> <p>The <code>PointCloud</code> value a geometry's points make: their positions, each point attribute as the column holding it, each detail value. What a socket declared as <code>PointCloud</code> is handed. Columns the device holds are brought back first, since the value reads its positions as an array.</p> <p><strong>Parameters</strong></p> <ul> <li><code>g</code> <code>any</code> <em>(optional)</em> — A Geometry.</li> </ul> <pre><code class="language-lua">local pc = proc.substrate.toPointCloud(g) </code></pre> <h2 id="substrate-traffic">modules/proc.substrate/traffic</h2> <pre><code class="language-lua">traffic(): { uploads: number, uploadBytes: number, readbacks: number, readbackBytes: number, allocations: number, </code></pre> <p>What the calling thread has moved between the CPU and the device through procedural columns since it began: <code>{ uploads, uploadBytes, readbacks, readbackBytes }</code>. Two readings taken around a piece of work on the same thread give what that work moved, whatever other threads moved meanwhile.</p> <pre><code class="language-lua">local before = proc.substrate.traffic() </code></pre> <h2 id="substrate-trafficcounts">modules/proc.substrate/trafficCounts</h2> <pre><code class="language-lua">trafficCounts(): (number, number, number) </code></pre> <p>The calling thread's <code>traffic()</code> counts a step's statistics read, as numbers rather than a table: uploads, read-backs and device buffers made.</p> <pre><code class="language-lua">local uploads, readbacks, allocations = proc.substrate.trafficCounts() </code></pre> <h2 id="substrate-trianglecount">modules/proc.substrate/triangleCount</h2> <pre><code class="language-lua">triangleCount(self): number </code></pre> <p>How many triangles the geometry holds — the elements <code>indices</code> runs over, counting an n-gon as the fan it draws as.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-tryread">modules/proc.substrate/tryRead</h2> <pre><code class="language-lua">tryRead(self): { number }? </code></pre> <p>The run this column holds IF it can be answered without waiting — a CPU buffer always can, and a GPU one only once its readback has landed. nil means the device still has it.</p> <p>This is what a read that CANNOT wait uses. A readback takes frames, and waiting for one means yielding; a metamethod cannot yield, so the flat-array view a geometry presents is built on this and says what to do instead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local run = col:tryRead() </code></pre> <h2 id="substrate-type">modules/proc.substrate/type</h2> <pre><code class="language-lua">type(self): string </code></pre> <p>The column's element type tag.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-valuearray">modules/proc.substrate/valueArray</h2> <pre><code class="language-lua">valueArray(self): { any } </code></pre> <p>The array of values behind a column carried as values, materializing a constant backing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-values">modules/proc.substrate/values</h2> <pre><code class="language-lua">values(t: string, arr: { any }, count: number?): any </code></pre> <p>A column backed by an array of values — the backing a type the buffer primitive cannot express takes: text, a reference, a handle, a record.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — The element type tag.</li> <li><code>arr</code> <code>{ any }</code> — The array of values, one per element.</li> <li><code>count</code> <code>number?</code> <em>(optional)</em> — How many elements, when <code>arr</code> holds nil for some of them — an element with no value, such as an entity without the component a record column carries. Left out, it is the array's length.</li> </ul> <pre><code class="language-lua">proc.substrate.Column.values("String", { "a", "b" }) proc.substrate.Column.values("Record", records, #ids) </code></pre> <h2 id="substrate-version">modules/proc.substrate/version</h2> <pre><code class="language-lua">version(self): (number, number) </code></pre> <p>What identifies this run, and what it is on: a serial unique to the column for the life of the VM, and a revision its writes bump. Two calls answering the same pair are the same values; a different pair is a run that has changed. Nothing is read to answer it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local s, r = col:version() </code></pre> <h2 id="substrate-vertexcount">modules/proc.substrate/vertexCount</h2> <pre><code class="language-lua">vertexCount(self): number </code></pre> <p>How many points the geometry holds — the elements <code>positions</code> runs over.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="substrate-vertexprimitive">modules/proc.substrate/vertexPrimitive</h2> <pre><code class="language-lua">vertexPrimitive(self, v: number): number? </code></pre> <p>Which primitive uses vertex <code>v</code>, derived from the primitive runs.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>v</code> <code>number</code> — 0-based vertex index.</li> </ul> <h2 id="substrate-wgslof">modules/proc.substrate/wgslOf</h2> <pre><code class="language-lua">wgslOf(t: string): string? </code></pre> <p>The WGSL type one element of a column of type <code>t</code> is read as: <code>f32</code>, <code>i32</code> or <code>u32</code> for a column of one lane, <code>vec2<f32></code>, <code>vec3<f32></code> or <code>vec4<f32></code> for more (<code>Color</code> and <code>Quat</code> are four lanes). The one mapping from procgen types to WGSL types: a specialization that names a column's type takes this.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — A procgen type tag.</li> </ul> <pre><code class="language-lua">proc.substrate.wgslOf("Vec3") --> "vec3<f32>" </code></pre> <h2 id="substrate-withindex">modules/proc.substrate/withIndex</h2> <pre><code class="language-lua">withIndex(self, index: any): any </code></pre> <p>The same distinct values over a new run of indices — what a pass that moved an interned column's indices hands on. The column holds <code>index</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>index</code> <code>any</code> <em>(optional)</em> — A Float column of indices into this column's distinct values.</li> </ul> <pre><code class="language-lua">local kept = col:withIndex(proc.device.gather(col:index():gpuMirror(), keep, n, cap)) </code></pre> <h2 id="substrate-withpagegroup">modules/proc.substrate/withPageGroup</h2> <pre><code class="language-lua">withPageGroup(key: string, fn: (...any) -> ...any, ...: any): ...any </code></pre> <p>Call <code>fn(...)</code> with every device run a column makes during it, on this thread, placed in the pages the group <code>key</code> owns, and answer what <code>fn</code> answers. A run in a group stands in a page no other group's run shares, in the first free room of the group's pages in the order they were made, so work that asks for the same shapes in the same order is handed the same pages in the same order each time it runs: a kernel that binds pages and addresses its columns by offset is then the same program, over the same bind groups, every play.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The group's name.</li> <li><code>fn</code> <code>(...any) -> ...any</code> — The work.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">proc.substrate.withPageGroup("step:" .. stepId, function() return proc.eval(graph) end) </code></pre> <h2 id="substrate-writecount">modules/proc.substrate/writeCount</h2> <pre><code class="language-lua">writeCount(self): number </code></pre> <p>How many times this column's run has been written in place, by any holder of it: what a read of the run taken before is checked against. It only grows.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local before = col:writeCount() </code></pre> <h2 id="substrate-writecounter">modules/proc.substrate/writeCounter</h2> <pre><code class="language-lua">writeCounter(): any </code></pre> <p>A write counter: one count a set of runs share, so a pass that writes all of them (a recorded list replayed) notes the write once instead of once per run. <code>counter:join(col)</code> adds a column's run to the set, <code>counter:note()</code> counts one write of every run in it, and <code>counter:release()</code> takes every run out, each keeping the writes it was counted. A run's <code>writeCount()</code> is its own noted writes and the notes of every counter it is in, so a run written outside the set is counted as before.</p> <pre><code class="language-lua">local counter = proc.substrate.writeCounter(); counter:join(col); counter:note() </code></pre> <h2 id="substrate-writerun">modules/proc.substrate/writeRun</h2> <pre><code class="language-lua">writeRun(self, first: number, values: { number }): any </code></pre> <p>Write <code>values</code>, a flat run of whole elements, into this column from element <code>first</code> on. This is how the pass that owns a GPU-resident column writes it — a solver placing the particles it spawned into the slots it holds — and the write lands behind every dispatch already queued over the column. A CPU column takes it too.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>first</code> <code>number</code> — 0-based index of the first element written.</li> <li><code>values</code> <code>{ number }</code> — <code>lanes</code> numbers per element written.</li> </ul> <pre><code class="language-lua">col:writeRun(12, { 0, 1, 0, 0, 2, 0 }) </code></pre> <h2 id="translate-readme">modules/proc.translate/README</h2> <pre><code class="language-lua">require("@builtin::systems.procgen.proc.translate") -- proc.translate </code></pre> <p>Turning a geometry into scene things, and keeping them that way.</p> <p>Usage: local proc.translate = require("@builtin::systems.procgen.proc.translate")</p> <h2 id="translate-absentvalue">modules/proc.translate/absentValue</h2> <pre><code class="language-lua">absentValue(attribute: string): { number }? </code></pre> <p>What an element carrying no <code>attribute</code> stands at: the value the component field the attribute feeds rests at when nothing sets it, one number per lane. An element with no <code>orient</code> stands unrotated and one with no <code>scale</code> at full size, so parts laid together fill the attribute a part lacks with these rather than zeros.</p> <p><strong>Parameters</strong></p> <ul> <li><code>attribute</code> <code>string</code> — An attribute name.</li> </ul> <pre><code class="language-lua">proc.translate.absentValue("orient") --> { 0, 0, 0, 1 } </code></pre> <h2 id="translate-apply">modules/proc.translate/apply</h2> <pre><code class="language-lua">apply(state: any, cooked: any, namePrefix: string?, spawnOpts: any?, interval: number?): any </code></pre> <p>Present a cooked frame: spawn and despawn exactly the difference in the element list, then write only the fields whose digest moved.</p> <p>A frame in which nothing changed writes nothing and touches no entity. A frame in which only the transforms moved writes one buffer. Membership changing is what rebuilds the binding, and nothing else does.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state from <code>newState</code>.</li> <li><code>cooked</code> <code>any</code> <em>(optional)</em> — A value from <code>cook</code>.</li> <li><code>namePrefix</code> <code>string?</code> <em>(optional)</em> — What to name the entities this makes.</li> <li><code>spawnOpts</code> <code>any?</code> <em>(optional)</em> — What the entities this makes are spawned as — the options <code>entity.batchSpawn</code> takes. They are born temporary: a cook's elements are what code produced, so they stay out of the scene's authored content, and a scene saved while they stand does not record them. <code>temporary = false</code> is the explicit ask for elements the scene keeps.</li> <li><code>interval</code> <code>number?</code> <em>(optional)</em> — Seconds between this domain's presents. Given, the elements placed on the device are drawn every frame between the last two presents — position, rotation, scale and shader data lanes blended — so they move continuously whatever rate the domain presents at. Omitted, each present is drawn as it lands.</li> </ul> <pre><code class="language-lua">proc.translate.apply(state, cooked, "fx") proc.translate.apply(state, cooked, "kept", { temporary = false }) proc.translate.apply(state, cooked, "sparks", nil, 1 / 30) </code></pre> <h2 id="translate-bindframes">modules/proc.translate/bindFrames</h2> <pre><code class="language-lua">bindFrames(held: any, key: string, states: { [string]: any }, spawnOpts: any?, interval: number?): (boolean, string?) </code></pre> <p>Bind a play to frames <code>defineFrames</code> held: each output's state stands as one instanced draw reserving the most elements any frame holds, and <code>showFrame</code> then places it from any frame by index.</p> <p><strong>Parameters</strong></p> <ul> <li><code>held</code> <code>any</code> <em>(optional)</em> — A value from <code>defineFrames</code>.</li> <li><code>key</code> <code>string</code> — The play's name, unique to it.</li> <li><code>states</code> <code>{ [string]: any }</code> — The state of each output held, by output; a missing one is made.</li> <li><code>spawnOpts</code> <code>any?</code> <em>(optional)</em> — What the draw is registered under — its <code>parent</code>.</li> <li><code>interval</code> <code>number?</code> <em>(optional)</em> — Seconds between the frames' presents; given, each element is drawn between the last two frames shown.</li> </ul> <pre><code class="language-lua">proc.translate.bindFrames(held, "fx#12", states, { parent = id }, 1 / 30) </code></pre> <h2 id="translate-bounds">modules/proc.translate/bounds</h2> <pre><code class="language-lua">bounds(state: any): any? </code></pre> <p>The box a state's instanced population reaches, in the space of the entity it stands under: <code>{ min = {x,y,z}, max = {x,y,z} }</code>, the latest the device has answered, or nil before any has. Every slot's position, widened by the drawn mesh's reach at the largest scale any slot holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state <code>apply</code> has written into.</li> </ul> <pre><code class="language-lua">local box = proc.translate.bounds(state) </code></pre> <h2 id="translate-clear">modules/proc.translate/clear</h2> <pre><code class="language-lua">clear(state: any) </code></pre> <p>Release everything an applied translation is holding: the entities it spawned and the binding over them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state from <code>newState</code>.</li> </ul> <h2 id="translate-componentof">modules/proc.translate/componentOf</h2> <pre><code class="language-lua">componentOf(name: string): (string?, string?) </code></pre> <p>Which component an attribute named after one holds: a native component of the <code>ecs</code> catalog, or a script component declared as a <code>.component</code>. An attribute that merely feeds a component's field answers nil, since the component is not the attribute.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — An attribute name.</li> </ul> <pre><code class="language-lua">proc.translate.componentOf("Mesh") --> "native" </code></pre> <h2 id="translate-defineframes">modules/proc.translate/defineFrames</h2> <pre><code class="language-lua">defineFrames(substrateMod: any, frames: { { [string]: any } }): (any?, string?) </code></pre> <p>Hold a timeline's frames on the engine, so plays show each one by index with no script running per frame. <code>frames[f]</code> maps each output to the <code>{ value, domain }</code> it realizes at frame f; every output that changes from frame to frame must stand as one instanced draw, with one mesh and one material for the whole timeline. An output that holds the same value on every frame is left to stand as it was realized.</p> <p><strong>Parameters</strong></p> <ul> <li><code>substrateMod</code> <code>any</code> <em>(optional)</em> — The substrate module columns are built from.</li> <li><code>frames</code> <code>{ { [string]: any } }</code> — The outputs at each frame, in order.</li> </ul> <pre><code class="language-lua">local held, why = proc.translate.defineFrames(proc.substrate, frames) </code></pre> <h2 id="translate-drawn">modules/proc.translate/drawn</h2> <pre><code class="language-lua">drawn(state: any): any? </code></pre> <p>How the state's elements are drawn: <code>{ as, count, because? }</code>. <code>as</code> is <code>"instances"</code> for a domain drawn as one instanced draw and <code>"entities"</code> for one standing on an entity per element; <code>because</code> names what a domain of one mesh and one material carries that made it entities.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state <code>apply</code> has written into.</li> </ul> <pre><code class="language-lua">print(proc.translate.drawn(state).as) </code></pre> <h2 id="translate-drawnas">modules/proc.translate/drawnAs</h2> <pre><code class="language-lua">drawnAs(geometry: any): string? </code></pre> <p>Cook a geometry's domain into its scene-ready form.</p> <p>The result holds the element ids and one block per component the domain's attributes carry, in the order they are applied: a native component's whole records, then the native fields fed by their own attributes, then the script components. A native field laid out as a contiguous run comes out as a column whose buffer a bulk write copies; the rest come out as a values array. A script component's block holds the columns that feed it, read into each element's data when the block is applied.</p> <p>A domain whose live elements are counted on the device, carrying only the components the device draws with (Transform, Mesh, Material, ShaderData), is cooked as the entities it stands on: its capacity of them, each column scattered to the entity each element's <code>slot</code> names on the device. One carrying any other component — a light, a script — is read back and cooked as exactly its live elements, since the scene reads those on the CPU.</p> <p>Nothing here is written to the scene — that is <code>apply</code>. Which of a geometry's domains its elements stand in the scene as: the point or primitive domain carrying a component — a Mesh, a Material, a Light, a script component — is the one whose elements the author made into scene things. Nil when neither carries one, which is a geometry that is drawn whole, as one mesh.</p> <p><strong>Parameters</strong></p> <ul> <li><code>geometry</code> <code>any</code> <em>(optional)</em> — A Geometry.</li> </ul> <pre><code class="language-lua">local cooked = proc.translate.cook(proc.substrate, points, "point") local domain = proc.translate.drawnAs(part) </code></pre> <h2 id="translate-iscomponent">modules/proc.translate/isComponent</h2> <pre><code class="language-lua">isComponent(name: string): boolean </code></pre> <p>Whether <code>name</code> names a component translation carries: a native component in the <code>ecs</code> catalog, a script component declared as a <code>.component</code>, or an attribute a component declares as feeding one of its fields.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — An attribute name.</li> </ul> <pre><code class="language-lua">proc.translate.isComponent("Mesh") --> true proc.translate.isComponent("P") --> true, it feeds Transform.position </code></pre> <h2 id="translate-materialkeys">modules/proc.translate/materialKeys</h2> <pre><code class="language-lua">materialKeys(state: any): { string } </code></pre> <p>The keys every material this state filed is under — the ones its elements and its shared fields draw with — so whoever presents the state can write what those materials animate on every frame it draws them. The list is the state's own and frozen, the same list until a material is filed or released, so a caller can keep it and compare it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state from <code>newState</code>.</li> </ul> <pre><code class="language-lua">for _, key in ipairs(proc.translate.materialKeys(state)) do print(key) end </code></pre> <h2 id="translate-newstate">modules/proc.translate/newState</h2> <pre><code class="language-lua">newState(): any </code></pre> <p>The state an applied translation keeps between frames: which entities stand for which elements, the binding over them, and the digest each field was last written at.</p> <pre><code class="language-lua">local state = proc.translate.newState() </code></pre> <h2 id="translate-piecesof">modules/proc.translate/piecesOf</h2> <pre><code class="language-lua">piecesOf(value: any): { any }? </code></pre> <p>The copies of a population of several pieces as one population per piece: each holds the copies whose <code>Mesh</code> indexes that piece (the records the detail's <code>labels.Mesh</code> lists) and carries that piece's record and material, so each stands as one instanced draw. Nil for a value that is not such a population.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — A Geometry.</li> </ul> <pre><code class="language-lua">local pieces = proc.translate.piecesOf(population) </code></pre> <h2 id="translate-refreshbounds">modules/proc.translate/refreshBounds</h2> <pre><code class="language-lua">refreshBounds(state: any) </code></pre> <p>Ask the device again for the box a state's population reaches, over the runs it was last placed from: a present on the device moves those runs without placing anything anew, so the box follows them only when asked. Asks nothing while an earlier question is on its way; <code>M.bounds</code> answers once the reads arrive.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state <code>apply</code> has written into.</li> </ul> <pre><code class="language-lua">proc.translate.refreshBounds(state) </code></pre> <h2 id="translate-releasecooked">modules/proc.translate/releaseCooked</h2> <pre><code class="language-lua">releaseCooked(cooked: any) </code></pre> <p>Release a cooked frame <code>share</code> kept: the runs it stands on.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cooked</code> <code>any</code> <em>(optional)</em> — A value from <code>cook</code>.</li> </ul> <pre><code class="language-lua">proc.translate.releaseCooked(cooked) </code></pre> <h2 id="translate-releaseframes">modules/proc.translate/releaseFrames</h2> <pre><code class="language-lua">releaseFrames(held: any) </code></pre> <p>Let go of frames <code>defineFrames</code> held: the engine forgets them and the runs they are placed from are released. Every play bound to them is unbound first (<code>unbindFrames</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>held</code> <code>any</code> <em>(optional)</em> — A value from <code>defineFrames</code>.</li> </ul> <pre><code class="language-lua">proc.translate.releaseFrames(held) </code></pre> <h2 id="translate-share">modules/proc.translate/share</h2> <pre><code class="language-lua">share(cooked: any, source: any) </code></pre> <p>Keep a cooked frame for every state that presents it: <code>apply</code> then leaves the runs it stands on in place, and whoever shares it releases it once with <code>releaseCooked</code>. Plays of one effect presenting one cooked frame cook it once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cooked</code> <code>any</code> <em>(optional)</em> — A value from <code>cook</code>.</li> <li><code>source</code> <code>any</code> <em>(optional)</em> — The geometry it was cooked from, which <code>source</code> names after.</li> </ul> <pre><code class="language-lua">proc.translate.share(cooked, frameValue) </code></pre> <h2 id="translate-showframe">modules/proc.translate/showFrame</h2> <pre><code class="language-lua">showFrame(key: string, frame: number) </code></pre> <p>Show frame <code>frame</code> (from 1) of the frames the play <code>key</code> is bound to.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The play's name.</li> <li><code>frame</code> <code>number</code> — The frame, from 1.</li> </ul> <pre><code class="language-lua">proc.translate.showFrame("fx#12", 4) </code></pre> <h2 id="translate-standing">modules/proc.translate/standing</h2> <pre><code class="language-lua">standing(state: any): { any } </code></pre> <p>The runs the state's elements stand on and are placed from on the device — what a present rewriting the elements' own runs has to rewrite too.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state <code>apply</code> has written into.</li> </ul> <pre><code class="language-lua">local runs = proc.fieldc.liveRuns(proc.translate.standing(state)) </code></pre> <h2 id="translate-unbindframes">modules/proc.translate/unbindFrames</h2> <pre><code class="language-lua">unbindFrames(key: string) </code></pre> <p>Stop showing frames for the play <code>key</code>; what it placed stands as the last frame shown left it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The play's name.</li> </ul> <pre><code class="language-lua">proc.translate.unbindFrames("fx#12") </code></pre> <h2 id="translate-writelive">modules/proc.translate/writeLive</h2> <pre><code class="language-lua">writeLive(state: any, clock: any) </code></pre> <p>Write every animated field of the records the state's elements share, as each curve stands at <code>clock</code>: a record holding a curve — a light's flicker, a colour's pulse — is followed on every frame drawn rather than at the graph's presents. Only the fields a curve feeds are written, and a field whose value has not moved is left alone. The clock is also what the next record written whole reads its curves at. A field standing on a run the device rewrites (a light placed at a point that moves) is read again for <code>clock</code>, and lands on the entities a frame or more later.</p> <p><strong>Parameters</strong></p> <ul> <li><code>state</code> <code>any</code> <em>(optional)</em> — A state <code>apply</code> has written into.</li> <li><code>clock</code> <code>any</code> <em>(optional)</em> — <code>{ time, frame, fps, duration }</code>.</li> </ul> <pre><code class="language-lua">proc.translate.writeLive(state, clock) </code></pre> <h2 id="modules-profiler-readme">modules/profiler/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.profiler") -- profiler (also available as global 'profiler') </code></pre> <p>Frame-level profiler capture, EMA stats, and named profiling blocks. Public Luau surface over the <code>__profiler</code> Internal FFI namespace.</p> <p>Usage: local profiler = require("@builtin::modules.api.engine.profiler") Also available as global: profiler</p> <h2 id="modules-profiler-begin">modules/profiler/begin</h2> <pre><code class="language-lua">begin(name: string) </code></pre> <p>Start a named profiling block. Call <code>profiler.finish(name)</code> to record the duration. Blocks appear in <code>profiler.stats()</code> under <code>"script.<name>"</code> and inside captures.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Block name (e.g. "MyComponent.update").</li> </ul> <pre><code class="language-lua">profiler.begin("MyComponent.update"); ...; profiler.finish() </code></pre> <h2 id="modules-profiler-counters">modules/profiler/counters</h2> <pre><code class="language-lua">counters(pattern: string?): { ProfilerCount } </code></pre> <p>Get the profiler's counts: unitless per-frame tallies (for example the waits the engine thread made on the process-wide lock, under <code>system.schedule.frame.process_local_waits</code>), kept apart from the millisecond timings of <code>stats</code>. <code>frame</code> is the tally of the frame in progress, <code>prev_frame</code> the last complete frame's, <code>max_frame</code> the highest complete-frame tally and <code>total</code> the tally since the profiler started. Optional glob pattern filters by name (supports <code>*</code> and <code>?</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>pattern</code> <code>string?</code> <em>(optional)</em> — Filter pattern (e.g. "system.schedule.frame.*").</li> </ul> <pre><code class="language-lua">for _, c in ipairs(profiler.counters("system.schedule.frame.*")) do print(c.name, c.prev_frame) end </code></pre> <h2 id="modules-profiler-disablering">modules/profiler/disableRing</h2> <pre><code class="language-lua">disableRing() </code></pre> <p>Disable the ring buffer and clear its history.</p> <pre><code class="language-lua">profiler.disableRing() </code></pre> <h2 id="modules-profiler-enablering">modules/profiler/enableRing</h2> <pre><code class="language-lua">enableRing(seconds: number?): boolean </code></pre> <p>Enable the always-recording ring buffer, retaining the last <code>seconds</code> of per-frame data (default 20). Query it AFTER the fact with <code>profiler.retro()</code> — latency-immune, since the data is historical. Editor profile only: returns false in the runtime profile. The enable is the gate; the ring costs nothing until on.</p> <p><strong>Parameters</strong></p> <ul> <li><code>seconds</code> <code>number?</code> <em>(optional)</em> — Seconds of history to retain (default 20).</li> </ul> <pre><code class="language-lua">if profiler.enableRing(30) then ... end </code></pre> <h2 id="modules-profiler-finish">modules/profiler/finish</h2> <pre><code class="language-lua">finish(name: string?): number? </code></pre> <p>Finish a profiling block and record the elapsed duration as <code>"script.<name>"</code>. Without an argument, closes the most-recently- begun block (LIFO stack). With a name, closes the most recent block whose name matches — useful when blocks of different names are nested.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string?</code> <em>(optional)</em> — Block name to finish. Omit to pop the top of the stack.</li> </ul> <pre><code class="language-lua">local ms = profiler.finish("MyComponent.update") </code></pre> <h2 id="modules-profiler-gpuframe">modules/profiler/gpuFrame</h2> <pre><code class="language-lua">gpuFrame(): GpuFrameReport </code></pre> <p>Label-aggregated GPU pass timings over the last <code>window_frames</code> resolved frames, measured with GPU timestamp queries. <code>supported</code> is false when the device lacks timestamp queries — <code>spans</code> stays empty. Each span covers every render/compute pass recorded under one label — <code>compute.<shader></code> per compute dispatch, <code>scene.*</code> for the scene passes, <code>post.<effect></code> per post-process effect, <code>feature.*</code> for render-feature passes: <code>ms</code> is the median of its per-frame totals, <code>min_ms</code>/<code>max_ms</code> the range that median sits in, <code>count</code> the passes per frame and <code>frames</code> how much of the window carried it. <code>at_floor</code> marks a label whose every sample landed within a few ticks of the device's timestamp counter (<code>tick_ms</code>) — those passes ran and the device resolved no duration for them, which is not the same as a measured zero. <code>ran</code> is whether the label recorded a measured pass in the newest resolved frame, and <code>last_frame</code> the newest frame that did. The window outlives the work it describes, so a pass that stops being recorded leaves a row standing for up to <code>window_frames</code> frames carrying the median of the frames it did run in: read <code>ran</code> to answer whether a pass is running, <code>frame - last_frame</code> for how many resolved frames ago it last did, and <code>ms</code> as the cost of the frames it ran in. <code>frame_span_ms</code> (first pass begin to last pass end) and <code>total_ms</code> are medians too, so rows do not sum to <code>total_ms</code>, and the GPU may overlap passes so <code>total_ms</code> can exceed <code>frame_span_ms</code>. The readback is asynchronous: the window lags the live frame by a few frames.</p> <pre><code class="language-lua">local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label) </code></pre> <h2 id="modules-profiler-hits">modules/profiler/hits</h2> <pre><code class="language-lua">hits(label: string?): string? </code></pre> <p>Drain the watchdog's recorded hit frames into a capture stored under <code>label</code> (default <code>"watch_hits"</code>) and clear the buffer. Returns the capture JSON (same shape as <code>stopCapture</code>), or nil if there were no hits.</p> <p><strong>Parameters</strong></p> <ul> <li><code>label</code> <code>string?</code> <em>(optional)</em> — Capture label to store under (default "watch_hits").</li> </ul> <pre><code class="language-lua">local json = profiler.hits() </code></pre> <h2 id="modules-profiler-iscapturing">modules/profiler/isCapturing</h2> <pre><code class="language-lua">isCapturing(): boolean </code></pre> <p>Check if a profiler capture is currently active.</p> <pre><code class="language-lua">if profiler.isCapturing() then ... end </code></pre> <h2 id="modules-profiler-lastcapture">modules/profiler/lastCapture</h2> <pre><code class="language-lua">lastCapture(): string? </code></pre> <p>Get the most recent completed capture result as a JSON string. Same shape as <code>profiler.stopCapture()</code>. Returns nil if no capture has been completed yet.</p> <pre><code class="language-lua">local last = profiler.lastCapture() </code></pre> <h2 id="modules-profiler-measure-t">modules/profiler/measure<T...></h2> <pre><code class="language-lua">measure<T...>(name: string, fn: () -> T...): T... </code></pre> <p>Run a function inside a profiling block. Equivalent to a begin/finish pair but handles errors correctly. Returns the function's return values.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Block name.</li> <li><code>fn</code> <code>() -> T...</code> — Function to profile.</li> </ul> <pre><code class="language-lua">local count = profiler.measure("walk", function() return #entity.findAll() end) </code></pre> <h2 id="modules-profiler-perfline">modules/profiler/perfLine</h2> <pre><code class="language-lua">perfLine(): string? </code></pre> <p>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. A DIFFERENT measurement from everything below: this is the published smoothed frame, while <code>stats</code> and the profiler toolbox report the effective frame time (wall clock minus the cost of the call doing the asking), so a line composed from those disagrees with what other callers are told about the same engine. Reading it does not move the edit-to-edit baseline the delta is measured against.</p> <pre><code class="language-lua">local line = profiler.perfLine() </code></pre> <h2 id="modules-profiler-recordinghold">modules/profiler/recordingHold</h2> <pre><code class="language-lua">recordingHold(): string? </code></pre> <p>Who holds the one windowed recording, as a JSON string <code>{ label, owner, started_at, held_seconds }</code>, or nil when nothing is recording. <code>owner</code> is the caller key of whoever called <code>startCapture</code>, the same value <code>vfs.currentAuthor().id</code> reads for the calling caller, so a caller refused the slot tells its own earlier recording from a sibling session's; it is absent when that call attested no caller. Several agent sessions share one engine and one recording, which is why a refusal is answered with this.</p> <pre><code class="language-lua">local held = profiler.recordingHold() </code></pre> <h2 id="modules-profiler-retro">modules/profiler/retro</h2> <pre><code class="language-lua">retro(seconds: number?, label: string?): { [string]: any }? </code></pre> <p>Retroactively aggregate the last <code>seconds</code> of the ring (default: the whole ring). The full per-frame capture is retained under <code>label</code> (default <code>"retro"</code>) for in-engine drill-down (the <code>frame</code> / <code>hotspots</code> tools); this RETURNS a compact structured aggregate table (frame-time distribution</p> <ul> <li>per-system summary), never the raw per-frame array — bounded, so it is safe over the ZeroMind bridge. Code-facing primitive; the <code>retro</code> tool renders the agent-facing report. Latency-immune: the data is historical.</li> </ul> <p><strong>Parameters</strong></p> <ul> <li><code>seconds</code> <code>number?</code> <em>(optional)</em> — How many seconds back to include (default: whole ring).</li> <li><code>label</code> <code>string?</code> <em>(optional)</em> — Capture label to store under (default "retro").</li> </ul> <pre><code class="language-lua">local agg = profiler.retro(8, "collapse") </code></pre> <h2 id="modules-profiler-ringstatus">modules/profiler/ringStatus</h2> <pre><code class="language-lua">ringStatus(): string </code></pre> <p>Ring buffer status as a JSON string: <code>{ enabled, frames, capacity, span_seconds }</code>.</p> <pre><code class="language-lua">local s = profiler.ringStatus() </code></pre> <h2 id="modules-profiler-startcapture">modules/profiler/startCapture</h2> <pre><code class="language-lua">startCapture(label: string?): boolean </code></pre> <p>Start recording per-frame profiler data, held by the calling caller. Each frame's system timings are captured until <code>stopCapture()</code> is called. Results are accessible via <code>profiler.lastCapture()</code> and VFS at <code>/zero/runtime/profiler/<label>.json</code>. The recording is one engine-wide slot, so a start while another recording holds it returns false and <code>profiler.recordingHold()</code> names the label and the caller holding it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>label</code> <code>string?</code> <em>(optional)</em> — Capture label (default <code>"capture"</code>).</li> </ul> <pre><code class="language-lua">if profiler.startCapture("frame-spike") then ... end </code></pre> <h2 id="modules-profiler-stats">modules/profiler/stats</h2> <pre><code class="language-lua">stats(pattern: string?): { ProfilerStat } </code></pre> <p>Get current EMA profiling statistics from the SystemProfiler. Optional glob pattern filters by metric name (supports <code>*</code> and <code>?</code> wildcards). Each entry carries two averages: <code>avg_ms</code> averages one RUN of the block and is folded when the block runs, so a block that has stopped running keeps the last value it saw; <code>avg_frame_ms</code> averages one FRAME and is folded every frame, including the frames the block did not run in, so it is the block's share of the current frame and falls back to zero once the block stops running.</p> <p><strong>Parameters</strong></p> <ul> <li><code>pattern</code> <code>string?</code> <em>(optional)</em> — Filter pattern (e.g. "schedule.<em>", "system.schedule.render.</em>").</li> </ul> <pre><code class="language-lua">for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end </code></pre> <h2 id="modules-profiler-stopcapture">modules/profiler/stopCapture</h2> <pre><code class="language-lua">stopCapture(): string? </code></pre> <p>Stop the active profiler capture and return its result as a JSON string. The capture is also saved to VFS at <code>/zero/runtime/profiler/<label>.json</code>. Top-level fields: <code>label</code>, <code>frame_count</code>, <code>started_at</code>, <code>ended_at</code>, <code>frames</code>, <code>summary</code>, <code>callers</code>. Compute duration as <code>ended_at - started_at</code>. <code>callers</code> holds <code>{ caller, total_ms, max_ms, frames }</code> per caller whose queued Luau ran in the window, heaviest first, and each frame carries its own split as <code>agent_by_caller</code>.</p> <pre><code class="language-lua">local json = profiler.stopCapture() </code></pre> <h2 id="modules-profiler-unwatch">modules/profiler/unwatch</h2> <pre><code class="language-lua">unwatch() </code></pre> <p>Disarm the watchdog. Recorded hits are kept for a final <code>profiler.hits()</code>.</p> <pre><code class="language-lua">profiler.unwatch() </code></pre> <h2 id="modules-profiler-watch">modules/profiler/watch</h2> <pre><code class="language-lua">watch(ceilingMs: number, mode: string?, excludeAgent: boolean?, maxHits: number?): boolean </code></pre> <p>Arm the frame-time watchdog. When a frame's EFFECTIVE time (total minus agent-injected <code>execute</code> cost) crosses <code>ceilingMs</code>, mode <code>"record"</code> logs every offending frame (read with <code>profiler.hits()</code>), and mode <code>"pause"</code> pauses gameplay ONCE to freeze the bad state, then disarms. Editor profile only: returns false in the runtime profile. <code>excludeAgent</code> (default true) keeps the agent's own calls from tripping it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ceilingMs</code> <code>number</code> — Effective frame-time ceiling in ms.</li> <li><code>mode</code> <code>string?</code> <em>(optional)</em> — "record" (default) or "pause".</li> <li><code>excludeAgent</code> <code>boolean?</code> <em>(optional)</em> — Subtract agent cost before comparing (default true).</li> <li><code>maxHits</code> <code>number?</code> <em>(optional)</em> — Max frames retained in record mode (default 240).</li> </ul> <pre><code class="language-lua">if profiler.watch(50, "pause") then ... end </code></pre> <h2 id="modules-profiler-watchstatus">modules/profiler/watchStatus</h2> <pre><code class="language-lua">watchStatus(): string </code></pre> <p>Watchdog status as a JSON string: <code>{ armed, ceiling_ms, mode, exclude_agent, hits, dropped_hits, tripped }</code>.</p> <pre><code class="language-lua">local s = profiler.watchStatus() </code></pre> <h2 id="modules-prototype-lifecycle-readme">modules/prototype_lifecycle/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.prototype_lifecycle") -- prototype_lifecycle </code></pre> <p>The play-mode invariant for authored player prototypes and EditorOnly entities. In play a PlayerPrototype subtree is the clone source, not a live scene entity: the scene loader spawns it only in edit and the spawn-on-join flow instantiates a clone per joining user in play. This module deactivates and hides any prototype still live when play begins (the fallback when the loader spawned one) and every EditorOnly entity, so neither is simulated nor rendered in play. In edit both are fully live. Runtime clones are tracked here and despawned on the return to edit. Deactivating and hiding a root cascades to its subtree through the hierarchy, so operating on the root covers its children.</p> <p>Usage: local prototype_lifecycle = require("@builtin::modules.api.engine.prototype_lifecycle")</p> <h2 id="modules-prototype-lifecycle-activateprototypes">modules/prototype_lifecycle/activatePrototypes</h2> <pre><code class="language-lua">activatePrototypes() </code></pre> <p>Reactivate and unhide every prototype root and EditorOnly entity.</p> <h2 id="modules-prototype-lifecycle-applycurrentmode">modules/prototype_lifecycle/applyCurrentMode</h2> <pre><code class="language-lua">applyCurrentMode() </code></pre> <p>Hold what is live now to the invariant of the mode the engine is in. A scene load that lands while play runs (a root scene, an additive overlay such as the editor UI layer, or a load a slow boot finishes after the flip) brings in prototypes and EditorOnly entities after <code>enterPlay</code> ran. In play this deactivates and hides every prototype root, and holds the EditorOnly authoring surface to the state the session's pause gave it: stood down while play runs, live while a pause has it shown over the frozen world. It tracks that surface either way, so pause and resume reach it. In edit both are already live, and it changes nothing.</p> <pre><code class="language-lua">PrototypeLifecycle.applyCurrentMode() </code></pre> <h2 id="modules-prototype-lifecycle-deactivateprototypes">modules/prototype_lifecycle/deactivatePrototypes</h2> <pre><code class="language-lua">deactivatePrototypes() </code></pre> <p>Deactivate and hide every prototype root and EditorOnly entity so play mode neither simulates nor renders them.</p> <h2 id="modules-prototype-lifecycle-despawnclones">modules/prototype_lifecycle/despawnClones</h2> <pre><code class="language-lua">despawnClones() </code></pre> <p>Despawn every still-existing tracked clone and clear the tracking list.</p> <h2 id="modules-prototype-lifecycle-enteredit">modules/prototype_lifecycle/enterEdit</h2> <pre><code class="language-lua">enterEdit() </code></pre> <p>Enter edit mode: despawn runtime clones, then reactivate prototypes and EditorOnly entities.</p> <h2 id="modules-prototype-lifecycle-enterplay">modules/prototype_lifecycle/enterPlay</h2> <pre><code class="language-lua">enterPlay() </code></pre> <p>Enter play mode: deactivate and hide prototypes and EditorOnly entities.</p> <h2 id="modules-prototype-lifecycle-hideeditorsurface">modules/prototype_lifecycle/hideEditorSurface</h2> <pre><code class="language-lua">hideEditorSurface() </code></pre> <p>Deactivate and hide the EditorOnly authoring surface without touching player-prototype roots. Used when play mode resumes so the surface disappears and gameplay cameras take the viewport back.</p> <h2 id="modules-prototype-lifecycle-install">modules/prototype_lifecycle/install</h2> <pre><code class="language-lua">install() </code></pre> <p>Keep the play-mode invariant applied for every flip, in every world. <code>enterPlay</code> / <code>enterEdit</code> are what make <code>PrototypeOnly</code> and <code>EditorOnly</code> mean something at runtime, and until something calls them on the flip a template stays live: its camera competes for the viewport with the camera of the player cloned from it, carries no follow target, and holds the shot at the spawn point; its body answers the same input as a second character.</p> <p>Registered from the prelude beside the other engine installs rather than from a scene-load path — a load that does not run leaves the invariant unapplied with nothing reporting it, and a flip that reloads no scene never reaches a loader hook at all. Idempotent: a second call registers nothing, and the current mode is applied once on install so a world opened straight into play does not start with its templates live.</p> <pre><code class="language-lua">PrototypeLifecycle.install() </code></pre> <h2 id="modules-prototype-lifecycle-prototyperoots">modules/prototype_lifecycle/prototypeRoots</h2> <pre><code class="language-lua">prototypeRoots(): { string } </code></pre> <p>Ids of every active-layer entity carrying the PlayerPrototype component.</p> <h2 id="modules-prototype-lifecycle-registerclone">modules/prototype_lifecycle/registerClone</h2> <pre><code class="language-lua">registerClone(id: string) </code></pre> <p>Track a runtime clone root so it can be despawned on the return to edit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The clone's root entity id.</li> </ul> <h2 id="modules-prototype-lifecycle-showeditorsurface">modules/prototype_lifecycle/showEditorSurface</h2> <pre><code class="language-lua">showEditorSurface() </code></pre> <p>Reactivate and unhide the EditorOnly authoring surface (free camera + editor-only visualizers) without touching player-prototype roots. Used when play mode is paused so the editor camera returns over the frozen world.</p> <h2 id="modules-proxyocclusion-readme">modules/proxyOcclusion/README</h2> <pre><code class="language-lua">require("@builtin::systems.proxyOcclusion.proxyOcclusion") -- proxyOcclusion </code></pre> <p>Analytic occlusion from coarse proxy shapes — grounding shadow for subjects a shadow map does not reach, at a cost that scales with the number of proxies rather than with scene geometry.</p> <p>Usage: local proxyOcclusion = require("@builtin::systems.proxyOcclusion.proxyOcclusion")</p> <h2 id="modules-proxyocclusion-active">modules/proxyOcclusion/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the occlusion pass is currently running.</p> <pre><code class="language-lua">if proxyOcclusion.active() then print("occluding") end </code></pre> <h2 id="modules-proxyocclusion-buffers">modules/proxyOcclusion/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The buffer(s) this system's passes read. A pass binds what this hands it, so it has the values this module packed.</p> <pre><code class="language-lua">local b = proxyOcclusion.buffers() </code></pre> <h2 id="modules-proxyocclusion-clear">modules/proxyOcclusion/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Drop every proxy and release the pass. The settings are kept.</p> <pre><code class="language-lua">proxyOcclusion.clear() </code></pre> <h2 id="modules-proxyocclusion-configure">modules/proxyOcclusion/configure</h2> <pre><code class="language-lua">configure(opts: ProxyOcclusionOpts?): ProxyOcclusionState </code></pre> <p>Adjust how the occlusion is applied. Any omitted field keeps its current value. An <code>intensity</code> of 0 releases the pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>ProxyOcclusionOpts?</code> <em>(optional)</em> — Settings — see <code>ProxyOcclusionOpts</code>.</li> </ul> <pre><code class="language-lua">proxyOcclusion.configure({ intensity = 0.8, minDistance = 40 }) </code></pre> <h2 id="modules-proxyocclusion-count">modules/proxyOcclusion/count</h2> <pre><code class="language-lua">count(): number </code></pre> <p>How many proxies are registered.</p> <pre><code class="language-lua">print(proxyOcclusion.count()) </code></pre> <h2 id="modules-proxyocclusion-remove">modules/proxyOcclusion/remove</h2> <pre><code class="language-lua">remove(key: string): boolean </code></pre> <p>Remove the proxy registered under <code>key</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The identifier the proxy was registered with.</li> </ul> <pre><code class="language-lua">proxyOcclusion.remove("boulder") </code></pre> <h2 id="modules-proxyocclusion-set">modules/proxyOcclusion/set</h2> <pre><code class="language-lua">set(key: string, shape: ProxyShape): number </code></pre> <p>Add or replace a proxy under <code>key</code>. Re-submitting the same key moves that proxy rather than adding another, which is what lets a component push its shape every frame as its entity moves.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Stable identifier for this proxy — an entity id works well.</li> <li><code>shape</code> <code>ProxyShape</code> — The capsule — see <code>ProxyShape</code>.</li> </ul> <pre><code class="language-lua">proxyOcclusion.set("boulder", { a = { 0, 1, 0 }, radius = 2 }) </code></pre> <h2 id="modules-proxyocclusion-settings">modules/proxyOcclusion/settings</h2> <pre><code class="language-lua">settings(): ProxyOcclusionState </code></pre> <p>The settings currently in force.</p> <pre><code class="language-lua">local i = proxyOcclusion.settings().intensity </code></pre> <h2 id="modules-radiancecache-readme">modules/radianceCache/README</h2> <pre><code class="language-lua">require("@builtin::systems.radianceCache.radianceCache") -- radianceCache </code></pre> <p>A world-space store of lighting results that survives the frame that produced it. A lighting technique that gathers per pixel throws its answer away when the frame ends and pays for it again the next one; a technique that writes into a cache pays once per patch of world and reuses the answer for every later frame and every pixel standing on that patch. The cache is a fixed table of slots addressed by hashing a world-space cell, so it covers an unbounded world in bounded memory: <code>capacity</code> slots at 68 bytes each, and nothing about the size of the scene changes that. Cells coarsen with distance from the camera, and a slot no pixel has asked for in <code>maxAge</code> frames is reclaimed, so what the table holds tracks the view rather than accumulating everything ever seen. Each cache is created by name and owns its own table, so two techniques caching in the same frame do not share slots. Either drive the built-in screen-space producer with <code>run</code>, or enqueue your own producing pass over <code>bindings()</code> and let this own the table, the addressing and the eviction.</p> <p>Usage: local radianceCache = require("@builtin::systems.radianceCache.radianceCache")</p> <h2 id="modules-radiancecache-allocate">modules/radianceCache/allocate</h2> <pre><code class="language-lua">allocate(self: Cache, ctx: any, opts: { phase: string?, order: number? }?) </code></pre> <p>Claim a slot for every patch of world the frame is looking at, and keep the ones already held alive. Every technique using the cache runs this first: it is what decides what the table holds, and a producing pass can only fill patches this has stamped.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> <li><code>ctx</code> <code>any</code> <em>(optional)</em> — The render context the calling feature received.</li> <li><code>opts</code> <code>{ phase: string?, order: number? }?</code> <em>(optional)</em> — <code>{ phase, order }</code> — where the pass runs.</li> </ul> <pre><code class="language-lua">cache:allocate(ctx, { phase = "afterLighting", order = 40 }) </code></pre> <h2 id="modules-radiancecache-bindings">modules/radianceCache/bindings</h2> <pre><code class="language-lua">bindings(self: Cache): { [string]: any } </code></pre> <p>The buffers a pass binds to reach this cache. Hand them to <code>ctx.enqueue</code>'s <code>buffers</code> and <code>#include "@builtin::systems.radianceCache.radiance_cache"</code> in the shader — that is the whole contract for a technique that wants to produce into this cache or read out of it with its own pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> </ul> <pre><code class="language-lua">local function render(ctx) ctx.enqueue { kind = "compute", program = mine, buffers = cache:bindings() } end </code></pre> <h2 id="modules-radiancecache-create">modules/radianceCache/create</h2> <pre><code class="language-lua">create(name: string, opts: CacheOpts?): Cache </code></pre> <p>Create a cache that owns its own table. <code>name</code> keys its buffers and its resolve target, so two techniques caching in the same frame each pass their own name and never share slots.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Identifies this cache's resources. Unique per technique.</li> <li><code>opts</code> <code>CacheOpts?</code> <em>(optional)</em> — How the cache is sized and how it fills — see <code>CacheOpts</code>.</li> </ul> <pre><code class="language-lua">local c = radianceCache.create("indirect", { capacity = 65536, stride = 8 }) </code></pre> <h2 id="modules-radiancecache-destroy">modules/radianceCache/destroy</h2> <pre><code class="language-lua">destroy(self: Cache) </code></pre> <p>Release the table, the read-back and the resolve target. The cache rebuilds — empty — on its next use.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> </ul> <pre><code class="language-lua">cache:destroy() </code></pre> <h2 id="modules-radiancecache-gather">modules/radianceCache/gather</h2> <pre><code class="language-lua">gather(self: Cache, ctx: any, opts: { phase: string?, order: number? }?) </code></pre> <p>Gather light into the slots this frame is scheduled to visit, from the scene that was just drawn. The built-in producer: it needs no acceleration structure and no bake, so it runs wherever the deferred path does. A technique with its own way of computing radiance enqueues that instead and skips this.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> <li><code>ctx</code> <code>any</code> <em>(optional)</em> — The render context the calling feature received.</li> <li><code>opts</code> <code>{ phase: string?, order: number? }?</code> <em>(optional)</em> — <code>{ phase, order }</code> — where the pass runs.</li> </ul> <pre><code class="language-lua">cache:gather(ctx, { phase = "afterLighting", order = 41 }) </code></pre> <h2 id="modules-radiancecache-get">modules/radianceCache/get</h2> <pre><code class="language-lua">get(self: Cache): CacheState </code></pre> <p>This cache's settings.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> </ul> <pre><code class="language-lua">local n = cache:get().capacity </code></pre> <h2 id="modules-radiancecache-invalidate">modules/radianceCache/invalidate</h2> <pre><code class="language-lua">invalidate(self: Cache) </code></pre> <p>Declare that the lighting which produced what is in the cache is gone. Every slot takes its next gather whole instead of averaging it into light that no longer exists, so the cache is rebuilt within <code>stride</code> frames rather than over <code>history * stride</code>. Call it after moving, recolouring or switching off a light.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> </ul> <pre><code class="language-lua">cache:invalidate() </code></pre> <h2 id="modules-radiancecache-memorybytes">modules/radianceCache/memoryBytes</h2> <pre><code class="language-lua">memoryBytes(self: Cache): number </code></pre> <p>What this cache's TABLE costs, in bytes: <code>capacity</code> slots of payload plus one key word each. Fixed at creation and independent of the scene — the resolve target the cache also owns is screen-sized and scales with the viewport instead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> </ul> <pre><code class="language-lua">print(cache:memoryBytes() // 1024, "KiB") </code></pre> <h2 id="modules-radiancecache-resolve">modules/radianceCache/resolve</h2> <pre><code class="language-lua">resolve(self: Cache, ctx: any, opts: { phase: string?, order: number? }?): string </code></pre> <p>Read the cache back out per pixel and answer the guid of the texture holding it. The light in it arrived over however many frames have visited the patches on screen, so a pixel drawn for the first time still gets the converged answer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> <li><code>ctx</code> <code>any</code> <em>(optional)</em> — The render context the calling feature received.</li> <li><code>opts</code> <code>{ phase: string?, order: number? }?</code> <em>(optional)</em> — <code>{ phase, order }</code> — where the pass runs.</li> </ul> <pre><code class="language-lua">local gi = cache:resolve(ctx, { phase = "afterLighting", order = 42 }) </code></pre> <h2 id="modules-radiancecache-run">modules/radianceCache/run</h2> <pre><code class="language-lua">run(self: Cache, ctx: any, opts: { phase: string?, order: number? }?): string </code></pre> <p>Allocate, gather and resolve in three consecutive slots — the whole cache for a technique that wants the built-in screen-space producer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> <li><code>ctx</code> <code>any</code> <em>(optional)</em> — The render context the calling feature received.</li> <li><code>opts</code> <code>{ phase: string?, order: number? }?</code> <em>(optional)</em> — <code>{ phase, order }</code> — <code>order</code> is the first of three consecutive slots.</li> </ul> <pre><code class="language-lua">local gi = cache:run(ctx, { phase = "afterLighting", order = 40 }) </code></pre> <h2 id="modules-radiancecache-set">modules/radianceCache/set</h2> <pre><code class="language-lua">set(self: Cache, opts: CacheOpts?): CacheState </code></pre> <p>Change this cache's settings. Any omitted field keeps its current value. <code>capacity</code> is fixed at creation — a table cannot be resized under the patches already in it — so changing it is refused rather than silently ignored.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> <li><code>opts</code> <code>CacheOpts?</code> <em>(optional)</em> — The settings to change — see <code>CacheOpts</code>.</li> </ul> <pre><code class="language-lua">cache:set({ stride = 4, samples = 16 }) </code></pre> <h2 id="modules-radiancecache-stats">modules/radianceCache/stats</h2> <pre><code class="language-lua">stats(self: Cache): CacheStats? </code></pre> <p>What the cache did on the most recent frame a read-back has landed for, or nil before the first one arrives. Counted on the GPU by the passes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Cache</code></li> </ul> <pre><code class="language-lua">local s = cache:stats(); print(s.live, s.dropped) </code></pre> <h2 id="modules-range-bounds-readme">modules/range_bounds/README</h2> <pre><code class="language-lua">require("@builtin::modules.range_bounds") -- range_bounds </code></pre> <p>The <code>range</code> field-constraint validator: a constrained value must be a finite number inside the interval the field declared. A rejection names the interval, so the error carries the range the caller may write in. A constraint carrying <code>whole = true</code> (<code>Field.integer</code>) takes whole numbers alone. Registers itself with the generic field_constraints registry on load. <code>nil</code> passes, so a ranged field may be left unset.</p> <p>Usage: local range_bounds = require("@builtin::modules.range_bounds")</p> <h2 id="modules-referenceview-readme">modules/referenceView/README</h2> <pre><code class="language-lua">require("@builtin::systems.pathTracing.referenceView") -- referenceView </code></pre> <p>A progressive path-traced view of the scene through the active camera, accumulating samples while the view holds still, so the real-time image has a converged reference to be judged against.</p> <p>Usage: local referenceView = require("@builtin::systems.pathTracing.referenceView")</p> <h2 id="modules-referenceview-frame">modules/referenceView/__frame</h2> <pre><code class="language-lua">__frame(): { [string]: any }? </code></pre> <p>Advance one frame of accumulation: read the camera, restart if it has moved, and write the parameter block the trace pass reads. Called by the <code>pathTrace</code> render feature once per frame.</p> <h2 id="modules-referenceview-active">modules/referenceView/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the reference view is tracing.</p> <pre><code class="language-lua">if referenceView.active() then ... end </code></pre> <h2 id="modules-referenceview-disable">modules/referenceView/disable</h2> <pre><code class="language-lua">disable() </code></pre> <p>Stop tracing and release the accumulation buffer and scene snapshot. The render feature's own targets are released with it.</p> <pre><code class="language-lua">referenceView.disable() </code></pre> <h2 id="modules-referenceview-enable">modules/referenceView/enable</h2> <pre><code class="language-lua">enable(opts: ViewOpts?): ({ [string]: any }?, string?) </code></pre> <p>Start path tracing the active camera. Allocates the accumulation buffer and snapshots the scene's geometry and lights; the image begins converging on the next frame the camera holds still for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>ViewOpts?</code> <em>(optional)</em> — Trace settings — see the fields below. All are optional.</li> </ul> <pre><code class="language-lua">referenceView.enable({ samplesPerFrame = 4, bounces = 5 }) </code></pre> <h2 id="modules-referenceview-refresh">modules/referenceView/refresh</h2> <pre><code class="language-lua">refresh(): (boolean, string?) </code></pre> <p>Retake the scene snapshot — geometry and lights as they now stand — and begin converging again. What to call after moving, adding or removing anything the tracer must see.</p> <pre><code class="language-lua">referenceView.refresh() </code></pre> <h2 id="modules-referenceview-reset">modules/referenceView/reset</h2> <pre><code class="language-lua">reset() </code></pre> <p>Throw away every sample gathered so far and begin converging again from the current camera. The camera is watched automatically — this is for a change the view cannot see, such as a light being retuned.</p> <pre><code class="language-lua">referenceView.reset() </code></pre> <h2 id="modules-referenceview-settings">modules/referenceView/settings</h2> <pre><code class="language-lua">settings(): { [string]: any }? </code></pre> <p>The settings in force, the size of the scene snapshot, and how far the image has converged — <code>samples</code> is the count folded into the displayed image, and <code>converged</code> is true once it has reached <code>maxSamples</code>.</p> <pre><code class="language-lua">local s = referenceView.settings(); print(s.samples, s.converged) </code></pre> <h2 id="modules-reference-names-readme">modules/reference_names/README</h2> <pre><code class="language-lua">require("@builtin::modules.reference_names") -- reference_names </code></pre> <p>The world sources that spell a reference by a name its target no longer has, the ones among them the world's author owns, and the reconcile that writes the current names into those.</p> <p>A source records, beside every reference name it uses, the guid of the asset the name reached. When that asset later sits at another path (it was moved, or the engine's library files it somewhere else), the reference keeps resolving through the guid, while the text keeps the old name. Nothing rewrites a source for this on its own. Engines of different versions can hold one world open together, and their libraries can file the same asset at different paths: a source rewritten to the layout of whichever engine read it would be rewritten back by the other, and every rewrite is a change to the world that runs the asset's change hooks (shader recompiles, preview renders) with nobody having asked for it. So the old name stays until someone reconciles it. Until then, each time the world becomes ready, a notice names every owned source that still spells an old name, and <code>assets.reconcileReferenceNames</code> writes the current ones. Reconciling changes no behaviour: every reference already resolves to the asset its guid names, and afterwards the text names it the same way. A source is the author's when the world authors it and it is not inside an installed asset still as it was published. The engine library (<code>/source/libs/</code>) and pulled dependencies (<code>/source/deps/</code>) keep the text they were published with. An installed asset the author has modified since installing it is theirs, and is reconciled like their own files.</p> <p>Usage: local reference_names = require("@builtin::modules.reference_names")</p> <h2 id="modules-reference-names-owned">modules/reference_names/__owned</h2> <pre><code class="language-lua">__owned(source: string, held: { InstallOwnership }): boolean </code></pre> <p>Whether <code>source</code> is the author's, given the installs the world holds. The innermost install holding the source decides: an install the author has changed or deleted anything in is theirs, the files they left as published included; one they have left as published is still the publisher's text. A source no install holds is the author's, and a pulled dependency (<code>/source/deps/</code>) never is.</p> <p><strong>Parameters</strong></p> <ul> <li><code>source</code> <code>string</code> — The source's path.</li> <li><code>held</code> <code>{ InstallOwnership }</code> — The installs, as <code>world.installs</code> lists them.</li> </ul> <pre><code class="language-lua">reference_names.__owned("/zero/source/a.material/mat.yaml", world.installs()) </code></pre> <h2 id="modules-reference-names-install">modules/reference_names/install</h2> <pre><code class="language-lua">install() </code></pre> <p>Check, each time a world becomes ready, for owned sources spelling a moved reference's old name, and post a notice for each. Installed by the prelude.</p> <h2 id="modules-reference-names-pending">modules/reference_names/pending</h2> <pre><code class="language-lua">pending(paths: { string }?): { MovedSource } </code></pre> <p>The owned world sources that spell a reference by a name its target no longer has. Owned: authored by this world, outside <code>/source/deps/</code>, and not inside an installed asset still as it was published. Reads only.</p> <p><strong>Parameters</strong></p> <ul> <li><code>paths</code> <code>{ string }?</code> <em>(optional)</em> — Sources to ask about; every owned source when absent.</li> </ul> <pre><code class="language-lua">for _, s in ipairs(reference_names.pending()) do print(s.source, #s.names) end </code></pre> <h2 id="modules-reference-names-reconcile">modules/reference_names/reconcile</h2> <pre><code class="language-lua">reconcile(opts: { paths: { string }?, dryRun: boolean? }?): ReconcileReport </code></pre> <p>Write the current name of every moved reference into the owned sources that still spell an old one. The write is the source's own change: it syncs to the world and runs the asset's change hooks like any edit. What each reference resolves to stays as it was; only its spelling changes, and the source drops out of <code>pending</code>. The notices already posted about moved names are withdrawn and posted again for the owned sources still holding one, so a caller that has not read them yet is told only what is still true.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ paths: { string }?, dryRun: boolean? }?</code> <em>(optional)</em> — <code>{ paths?: { string }, dryRun?: boolean }</code>: <code>paths</code> narrows to those sources; <code>dryRun</code> reports what would be written and writes nothing.</li> </ul> <pre><code class="language-lua">reference_names.reconcile({ dryRun = true }) </code></pre> <h2 id="modules-reflectionprobe-readme">modules/reflectionProbe/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.reflectionProbe") -- reflectionProbe (also available as global 'reflectionProbe') </code></pre> <p>Reflection-probe system — multiple proximity-blended reflection probes in a scene. Each probe bakes the scene into its own cube slot from its position; surfaces reflect the probes covering them, gathered highest <code>priority</code> first and blended by proximity within a rank (the renderer's per-fragment probe blend), with the scene's sky under whatever coverage the probes leave. A probe reflects once it holds a cube (baked, or loaded from its persisted bake); until then it is registered and listed with <code>baked = false</code> and adds nothing to the frame. One-liner authoring (<code>reflectionProbe.add</code>) and a one-call bake-everything (<code>reflectionProbe.bakeAll</code>).</p> <p>Usage: local reflectionProbe = require("@builtin::modules.api.engine.reflectionProbe") Also available as global: reflectionProbe</p> <h2 id="modules-reflectionprobe-add">modules/reflectionProbe/add</h2> <pre><code class="language-lua">add(x: number, y: number, z: number, opts: { [string]: any }?): string </code></pre> <p>Add a reflection probe at <code>(x, y, z)</code> in one call: spawns a probe entity carrying a ReflectionProbe component, which registers it and loads its persisted bake when one exists. A probe with no bake reflects nothing until it is baked (<code>bakeAll()</code>, or <code>bake(id)</code>). The probe is an editor gizmo, invisible in play mode. Returns the probe entity id.</p> <p><strong>Parameters</strong></p> <ul> <li><code>x</code> <code>number</code> — World X.</li> <li><code>y</code> <code>number</code> — World Y.</li> <li><code>z</code> <code>number</code> — World Z.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — Optional <code>{ radius = 12, probeId = "...", name = "..." }</code>. <code>probeId</code> is the STABLE asset identity (so a re-created probe reloads the same baked cube); defaults to the entity id. The probe does NOT bake on add — call <code>bakeAll()</code> once the scene is built (baking is an authoring step).</li> </ul> <pre><code class="language-lua">reflectionProbe.add(0, 3, 0, { radius = 15, probeId = "lobby" }) </code></pre> <h2 id="modules-reflectionprobe-apply">modules/reflectionProbe/apply</h2> <pre><code class="language-lua">apply(): number </code></pre> <p>Push the current active-probe blend data (live positions + radii) to the renderer. Builds a dense slot array so each probe's data lands at its cube slot; freed/missing slots, and the slot of a probe that holds no cube yet, become inert placeholders that reach no surface. Called automatically by add / bake / remove; call it directly after moving a probe entity.</p> <h2 id="modules-reflectionprobe-bake">modules/reflectionProbe/bake</h2> <pre><code class="language-lua">bake(id: string): (string?, string?) </code></pre> <p>Bake the scene into probe <code>id</code>'s cube slot from its current position AND persist it to a <code>faces6</code> <code>.texture</code> asset (so it survives reload + syncs), then re-apply the probe set. Yields a few frames; call from a task/coroutine context (component hook via task.spawn, <code>bakeAll</code>, or <code>execute</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> </ul> <h2 id="modules-reflectionprobe-bakeall">modules/reflectionProbe/bakeAll</h2> <pre><code class="language-lua">bakeAll(): { baked: number, failed: number, errors: { string } } </code></pre> <p>Bake EVERY registered probe in the active layers, in one call. Captures the sky into the fallback slot, then each probe's scene from its position into its slot, persists it, and applies the full probe set. The agent/editor one-liner. Yields; call from a task/coroutine context (<code>execute</code>, a tool, or <code>task.spawn</code>).</p> <pre><code class="language-lua">reflectionProbe.bakeAll() </code></pre> <h2 id="modules-reflectionprobe-baking">modules/reflectionProbe/baking</h2> <pre><code class="language-lua">baking(id: string?): boolean </code></pre> <p>Whether a bake under way will reach probe <code>id</code>: its own <code>bake</code>, or a <code>bakeAll</code> that has it still to reach. With no <code>id</code>, whether any bake is under way.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string?</code> <em>(optional)</em> — Optional probe entity id.</li> </ul> <pre><code class="language-lua">reflectionProbe.baking(id) </code></pre> <h2 id="modules-reflectionprobe-count">modules/reflectionProbe/count</h2> <pre><code class="language-lua">count(): number </code></pre> <p>Number of registered probes.</p> <h2 id="modules-reflectionprobe-ensureskyfallback">modules/reflectionProbe/ensureSkyFallback</h2> <pre><code class="language-lua">ensureSkyFallback(): boolean </code></pre> <p>Ensure the scene's sky is in the environment's sky fallback: a reflective surface no probe covers then reflects the sky rather than black, and a partially covered one blends the shortfall against it. Queues a capture when the sky slot holds none, and re-arms the fallback when a capture is there but switched off. The engine's own state answers both questions, so calling this on every probe that comes up costs one capture between them, and a scene that lost its fallback gets it back. Once captured, the fallback follows the sky the scene draws on its own.</p> <pre><code class="language-lua">reflectionProbe.ensureSkyFallback() </code></pre> <h2 id="modules-reflectionprobe-isbaked">modules/reflectionProbe/isBaked</h2> <pre><code class="language-lua">isBaked(id: string): boolean </code></pre> <p>Whether probe <code>id</code> holds a cube it reflects: baked by <code>bake</code> / <code>bakeAll</code>, or loaded from its persisted bake by <code>loadBaked</code>. An unbaked probe adds nothing to the frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> </ul> <pre><code class="language-lua">reflectionProbe.isBaked(id) </code></pre> <h2 id="modules-reflectionprobe-list">modules/reflectionProbe/list</h2> <pre><code class="language-lua">list(): { any } </code></pre> <p>List every registered probe: <code>{ { id, slot, radius, priority, baked, asset, position }, ... }</code>. <code>baked</code> is whether the probe holds a cube it reflects; an unbaked probe adds nothing to the frame. <code>asset</code> names the persisted bake once there is one.</p> <h2 id="modules-reflectionprobe-loadbaked">modules/reflectionProbe/loadBaked</h2> <pre><code class="language-lua">loadBaked(id: string): boolean </code></pre> <p>Load probe <code>id</code>'s PERSISTED baked cube (<code>probe_<key>.texture</code>) into its slot WITHOUT re-rendering the scene — the runtime path. A probe bakes once at authoring time and loads the asset on every subsequent scene load. Returns false (not an error) when no baked asset exists yet.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> </ul> <h2 id="modules-reflectionprobe-register">modules/reflectionProbe/register</h2> <pre><code class="language-lua">register(id: string, radius: number, key: string?): number? </code></pre> <p>Register a reflection probe for entity <code>id</code> with influence <code>radius</code>. Assigns a free cube slot and applies the updated probe set. Idempotent — a re-register keeps the same slot and just updates the radius. Called by the ReflectionProbe component's awake; rarely called directly.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> <li><code>radius</code> <code>number</code> — Influence radius (world units) — surfaces within blend it.</li> <li><code>key</code> <code>string?</code> <em>(optional)</em> — Optional STABLE asset identity (the probe's probeId). Defaults to <code>id</code>. The baked cube persists at <code>probe_<key>.texture</code> so an authored probe keeps the same asset across reloads even though its runtime entity id changes.</li> </ul> <h2 id="modules-reflectionprobe-setpriority">modules/reflectionProbe/setPriority</h2> <pre><code class="language-lua">setPriority(id: string, priority: number) </code></pre> <p>Set a probe's blend rank against the probes it overlaps, and re-apply. Probes are gathered highest rank first and each rank takes the coverage the ranks above it left, so a small interior probe ranked above the large exterior one it sits inside wins outright wherever it reaches full weight, while probes of equal rank crossfade by proximity as before.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> <li><code>priority</code> <code>number</code> — Blend rank. Defaults to 0 on every probe.</li> </ul> <pre><code class="language-lua">reflectionProbe.setPriority(interiorId, 1) </code></pre> <h2 id="modules-reflectionprobe-setproxy">modules/reflectionProbe/setProxy</h2> <pre><code class="language-lua">setProxy(id: string, kind: string, x: number, y: number, z: number) </code></pre> <p>Anchor a probe's reflections to a proxy volume and re-apply. A cube records the environment from one point, so sampling it along the raw reflection vector puts everything it recorded at infinity and the reflection slides across a surface as the camera moves. Sizing a proxy to the geometry the probe recorded — a room's walls, say — keeps the reflection anchored to what it depicts.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> <li><code>kind</code> <code>string</code> — "box" (sized by all three half-extents), "sphere" (sized by <code>x</code>), or "none" to sample along the raw reflection vector.</li> <li><code>x</code> <code>number</code> — Half-extent along X, in world units — the sphere radius for "sphere".</li> <li><code>y</code> <code>number</code> — Half-extent along Y.</li> <li><code>z</code> <code>number</code> — Half-extent along Z.</li> </ul> <pre><code class="language-lua">reflectionProbe.setProxy(id, "box", 5, 3, 4) -- a 10x6x8 room </code></pre> <h2 id="modules-reflectionprobe-setradius">modules/reflectionProbe/setRadius</h2> <pre><code class="language-lua">setRadius(id: string, radius: number) </code></pre> <p>Update a probe's influence radius and re-apply.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> <li><code>radius</code> <code>number</code> — New influence radius.</li> </ul> <h2 id="modules-reflectionprobe-unregister">modules/reflectionProbe/unregister</h2> <pre><code class="language-lua">unregister(id: string) </code></pre> <p>Unregister entity <code>id</code>'s probe, freeing its cube slot, and re-apply.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Probe entity id.</li> </ul> <h2 id="refs-readme">modules/renderer.refs/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.renderer.refs") -- renderer.refs </code></pre> <p>How <code>renderer.*</code> names a live resource: the guid a runtime resource is minted and checked under, and the one coercion per resource kind from whatever a caller holds (a key or guid, a handle, an AssetRef) to the key the renderer's stores index it by, with the rejection that names the accepted forms.</p> <p>Usage: local renderer.refs = require("@builtin::modules.api.engine.renderer.refs")</p> <h2 id="shading-readme">modules/renderer.shading/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.renderer.shading") -- renderer.shading </code></pre> <p>The readings <code>renderer.shadingOf</code> builds out of the document the renderer publishes every frame: the subject a question names, the row that answers for it, and the fault reading a row carries.</p> <p>Usage: local renderer.shading = require("@builtin::modules.api.engine.renderer.shading")</p> <h2 id="sources-readme">modules/renderer.sources/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.renderer.sources") -- renderer.sources </code></pre> <p>The <code>src</code> shapes <code>renderer.mesh.create</code> / <code>update</code> and <code>renderer.texture.create</code> / <code>update</code> take: which shape a src is, the per-vertex record form a mesh src may be written in, the index-range fault a geometry src is refused for, and the bounds and counts raw geometry implies.</p> <p>Usage: local renderer.sources = require("@builtin::modules.api.engine.renderer.sources")</p> <h2 id="modules-renderer-readme">modules/renderer/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.renderer") -- renderer (also available as global 'renderer') </code></pre> <p>GPU-resource factory + CPU codec/store wrappers — the GPU/CPU half of the asset↔resource split. This module (under <code>modules/api/</code>) is the SOLE caller of the internal <code>__mesh</code> / <code>__meshcpu</code> / <code>__meshgpu</code> / <code>__splat</code> / <code>__texture</code> / <code>__texturecpu</code> / <code>__texturegpu</code> / <code>__instancedata</code> FFI; assetType behaviours, components, and every other module call <code>renderer.*</code>, never the <code>__</code> internals.</p> <p>Usage: local renderer = require("@builtin::modules.api.engine.renderer") Also available as global: renderer</p> <h2 id="modules-renderer-anisotropy">modules/renderer/anisotropy</h2> <pre><code class="language-lua">anisotropy(): number </code></pre> <p>The maximum anisotropy material textures are sampled with right now — the requested level clamped to what this device honours.</p> <pre><code class="language-lua">if renderer.anisotropy() < 4 then ... end </code></pre> <h2 id="held">modules/renderer/atmospherics.held</h2> <pre><code class="language-lua">atmospherics.held(): boolean </code></pre> <p>Whether a hold is standing on the air right now.</p> <pre><code class="language-lua">if renderer.atmospherics.held() then print("clear air") end </code></pre> <h2 id="hold">modules/renderer/atmospherics.hold</h2> <pre><code class="language-lua">atmospherics.hold(share: number?, options: AtmosphericsHoldOptions?): () -> () </code></pre> <p>Hold the air between the camera and every surface at a stated share of what the scene authored, and return the release. At the default 0 the media contribute nothing and a surface renders in its own colour, which is what lets a reader judge an albedo, a tint or a material while another slice of a shared world drives the weather. The share reaches aerial perspective, height fog and volumetric light scattering; the sky, the sun and the light they put on a surface are untouched, because those are what the surface's colour is made of. Holds nest: the innermost names the share, and the authored air is back once the last release is called. Each release ends its own hold whatever order the releases come in, so two callers holding at once each end their own. <code>exclusive</code> takes the air for the <code>owner</code> key the call states: while that hold stands, a hold is admitted only when it states the same key, and every other one is refused with an error naming the key and the share holding it. That is what lets one caller clear the air to read a surface and keep it clear while another agent drives the same engine. The key is what an owner presents to take a nested hold of its own, and what <code>renderer.atmospherics.release</code> hands the air back by.</p> <p><strong>Parameters</strong></p> <ul> <li><code>share</code> <code>number?</code> <em>(optional)</em> — How much of the authored air reaches the image, in [0, 1]. Defaults to 0 -- no air at all.</li> <li><code>options</code> <code>AtmosphericsHoldOptions?</code> <em>(optional)</em> — <code>owner</code> is the key this hold is taken under, and an exclusive hold states one. A hold that states no key is labelled with the caller the call is attributed to, which names the calling session where the transport attested one and the ZeroMind account otherwise. <code>exclusive</code> takes the air for the stated key until the hold is released.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that releases this hold. Calling it twice releases once.</p> <pre><code class="language-lua">local release = renderer.atmospherics.hold() ; local picture = tools.use("capture", "fromPosition", { position = { 0, 2, 8 } }) ; release() local release = renderer.atmospherics.hold(0, { exclusive = true, owner = "albedo-read" }) </code></pre> <h2 id="onchange">modules/renderer/atmospherics.onChange</h2> <pre><code class="language-lua">atmospherics.onChange(listener: (number) -> ()): () -> () </code></pre> <p>Register a listener called with the share now in force whenever it changes — a hold taken, a hold released — and return the unsubscribe. A system that packs a medium into a GPU buffer registers here and re-packs what it has already pushed, so the buffer carries the share before the frame the hold was taken on is drawn rather than a frame later.</p> <p><strong>Parameters</strong></p> <ul> <li><code>listener</code> <code>(number) -> ()</code> — Called with the share now in force, in [0, 1].</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that removes this listener.</p> <pre><code class="language-lua">local stop = renderer.atmospherics.onChange(function(share) print("atmosphere share", share) end) </code></pre> <h2 id="owner">modules/renderer/atmospherics.owner</h2> <pre><code class="language-lua">atmospherics.owner(): { id: string?, name: string?, share: number, exclusive: boolean }? </code></pre> <p>The hold naming the share of the authored air that reaches the image right now: who took it, what share it asked for, and whether it took the air exclusively. Several agents drive one engine at once and a hold any of them takes moves the air every surface in every other agent's frame is rendered through, so this is how a caller sees that another agent holds it before its own share is quietly replaced -- and, when <code>exclusive</code> is true, <code>id</code> is the key a hold of its own states to be admitted, and the key <code>renderer.atmospherics.release</code> hands the air back by. <code>id</code> and <code>name</code> are nil for a hold that stated no key and that the engine attributes to no agent.</p> <pre><code class="language-lua">local who = renderer.atmospherics.owner() if who ~= nil and who.exclusive then print(who.name, "holds the air at", who.share) end </code></pre> <h2 id="release">modules/renderer/atmospherics.release</h2> <pre><code class="language-lua">atmospherics.release(owner: string): number </code></pre> <p>Hand the air back by the key its holds were taken under, and report how many came off. A hold stands until its release is called, and the release is a closure the call that took the hold holds: a caller that takes a hold in one call and comes back in another, and a task that ends between the two, both leave the air pinned with nobody holding a release for it. Naming the key is how the authored air comes back, and how a caller refused by an exclusive hold takes one over.</p> <p><strong>Parameters</strong></p> <ul> <li><code>owner</code> <code>string</code> — The key the holds to release were taken under -- what <code>owner</code> stated when they were taken, which <code>renderer.atmospherics.owner</code> reports.</li> </ul> <pre><code class="language-lua">renderer.atmospherics.release("albedo-read") </code></pre> <h2 id="share">modules/renderer/atmospherics.share</h2> <pre><code class="language-lua">atmospherics.share(): number </code></pre> <p>The share of the authored air that reaches the image: the innermost hold's share while one stands, and 1 otherwise. A system that packs a medium multiplies its extinction — <code>aerial</code>, a fog <code>density</code> — by this, and a hold then reaches that medium however it is being driven.</p> <pre><code class="language-lua">local density = state.density * renderer.atmospherics.share() </code></pre> <h2 id="modules-renderer-blendedbatching">modules/renderer/blendedBatching</h2> <pre><code class="language-lua">blendedBatching(): boolean </code></pre> <p>Whether blended neighbours sharing a draw key draw together.</p> <h2 id="clear">modules/renderer/bounds.clear</h2> <pre><code class="language-lua">bounds.clear(id: string): boolean </code></pre> <p>Withdraw the box an entity published, so it stops contributing to the entity's reported extent.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> </ul> <pre><code class="language-lua">renderer.bounds.clear(id) </code></pre> <h2 id="set">modules/renderer/bounds.set</h2> <pre><code class="language-lua">bounds.set(id: string, min: any, max: any): boolean </code></pre> <p>Publish the local-space box an entity's content-drawn geometry occupies. <code>entity:bounds()</code> and <code>entity:hierarchyBounds()</code> union it with whatever mesh geometry the entity has, each carried out of its own local space, so framing a camera on the entity frames what a feature actually draws.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Entity id.</li> <li><code>min</code> <code>any</code> <em>(optional)</em> — Local-space minimum corner — a Vec3 table or a 3-element array.</li> <li><code>max</code> <code>any</code> <em>(optional)</em> — Local-space maximum corner, same shape as <code>min</code>.</li> </ul> <pre><code class="language-lua">renderer.bounds.set(id, cloud.boundsMin, cloud.boundsMax) </code></pre> <h2 id="channelid">modules/renderer/captureView.channelId</h2> <pre><code class="language-lua">captureView.channelId(name: string): number? </code></pre> <p>The debug channel a registered view draws on — what a feature passes as its pass <code>debugChannel</code>. Nil when no view is registered under <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The view name.</li> </ul> <pre><code class="language-lua">local debugChannel = renderer.captureView.channelId("lightmap") -- a render feature passes it as an enqueued pass's `debugChannel` </code></pre> <h2 id="list">modules/renderer/captureView.list</h2> <pre><code class="language-lua">captureView.list(): { any } </code></pre> <p>Every registered capture view as <code>{ name, channel, description }</code> records — what backs the discoverability of <code>capture pass=<name></code> and the unknown-view error's suggestion list.</p> <pre><code class="language-lua">for _, v in ipairs(renderer.captureView.list()) do ... end </code></pre> <h2 id="ready">modules/renderer/captureView.ready</h2> <pre><code class="language-lua">captureView.ready(name: string): boolean </code></pre> <p>Whether a registered view can draw yet. A view's passes are enqueued from the moment its render feature first runs, but they are skipped while the materials they name have no pipeline — their shader is still compiling — so for the first frames of a session a camera bound to the view renders the ORDINARY view into its target, and the image gives no sign of it. This reports the difference, and reports it before any camera is on the view, so it is answerable for the first camera bound to one. Asking builds the view's parts if they are not yet there. A view that registered a <code>ready</code> of its own is also held to it, so a view whose feature is not running, or whose draw has not landed, reads false. False for an unregistered name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The view name.</li> </ul> <pre><code class="language-lua">repeat task.wait() until renderer.captureView.ready("zfighting") </code></pre> <h2 id="register">modules/renderer/captureView.register</h2> <pre><code class="language-lua">captureView.register(name: string, config: any): number </code></pre> <p>Register (or update) a content capture view under <code>name</code> and return the debug CHANNEL number assigned to it. A render feature gates its pass to this channel (<code>debugChannel = channel</code>) so the pass draws only when a capture selects the view. Idempotent: re-registering the same name keeps its channel.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The view name, selected via <code>capture pass=<name></code>.</li> <li><code>config</code> <code>any</code> <em>(optional)</em> — <code>{ description?, ensure?, ready?, warmup?, renderLayers? }</code>. <code>ensure</code> is called before a capture of this view so the feature that draws it is live (e.g. create it on demand). <code>ready</code> is the view's own statement of whether what it draws is in place: a <code>function(): (boolean, string?)</code> answering true, or false and the reason. A capture of the view waits on it after <code>ensure</code> and before it renders, so a view whose draw lands over the frames after <code>ensure</code> (an entity it spawns, a feature that reads the state <code>ensure</code> built on its next frame, a material still compiling) is drawn rather than captured empty, and a view that stays unready is refused with that reason. <code>warmup</code> is how many present frames a capture lets the view accumulate, once it is ready, before it reads: set it when the feature retains prior-frame state (a temporal diff) so the first capture reads a warm result. <code>renderLayers</code> is the layer spec a capture of this view uses when the caller named none. A view that draws its own geometry and wants the scene's kept out of the frame (and out of the depth buffer it tests against) names only its own layer.</li> </ul> <pre><code class="language-lua">local ch = renderer.captureView.register("lightmap", { description = "...", ensure = fn }) </code></pre> <h2 id="resolve">modules/renderer/captureView.resolve</h2> <pre><code class="language-lua">captureView.resolve(name: string): any </code></pre> <p>Resolve a capture view by name to its <code>{ channel, ensure, ready, description, warmup, renderLayers }</code> record, or nil when no view is registered under <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The view name.</li> </ul> <pre><code class="language-lua">local v = renderer.captureView.resolve("lightmap") </code></pre> <h2 id="unregister">modules/renderer/captureView.unregister</h2> <pre><code class="language-lua">captureView.unregister(name: string): boolean </code></pre> <p>Withdraw a capture view. A subsequent <code>capture pass=<name></code> no longer resolves to it (falls through to the unknown-view error).</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The view name.</li> </ul> <pre><code class="language-lua">renderer.captureView.unregister("lightmap") </code></pre> <h2 id="modules-renderer-clearshadowhero">modules/renderer/clearShadowHero</h2> <pre><code class="language-lua">clearShadowHero(): boolean </code></pre> <p>Release the hero caster, so every receiver samples the level its footprint selects again.</p> <pre><code class="language-lua">renderer.clearShadowHero() </code></pre> <h2 id="modules-renderer-clearshadowproxy">modules/renderer/clearShadowProxy</h2> <pre><code class="language-lua">clearShadowProxy(mesh: string?): number </code></pre> <p>Stop proxying <code>mesh</code>, so it rasterizes its own geometry into shadow pages again. Called with no argument, drops every registration.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string?</code> <em>(optional)</em> — The mesh to stop proxying. Omit to clear all of them.</li> </ul> <pre><code class="language-lua">renderer.clearShadowProxy(statueMesh) print(renderer.clearShadowProxy(), "proxies dropped") </code></pre> <h2 id="modules-renderer-clustererrorpx">modules/renderer/clusterErrorPx</h2> <pre><code class="language-lua">clusterErrorPx(px: number?): number </code></pre> <p>The cluster cut's error threshold, in pixels. Every static mesh draws the coarsest clusters whose simplification error projects to at most this many pixels in the view drawing it — measured against that view's own pixel height and projection, so a small render target draws coarser geometry than the full-screen view does from the same place. Lower draws finer geometry at more cost; higher draws coarser geometry. A client graphics setting. Default 1.</p> <p><strong>Parameters</strong></p> <ul> <li><code>px</code> <code>number?</code> <em>(optional)</em> — A finite number above 0. Omit to read the threshold.</li> </ul> <pre><code class="language-lua">renderer.clusterErrorPx(0.5) -- finer geometry on this machine </code></pre> <h2 id="modules-renderer-collect">modules/renderer/collect</h2> <pre><code class="language-lua">collect(): RuntimeCollection </code></pre> <p>Release every runtime texture, material, mesh and render feature nothing holds: no handle a script still reaches, no live owner, no reference from live engine state, no asset backing it, no hold. A root scene load runs this once the new scene stands, so what the previous scene's content created and nothing still wears goes with that scene; calling it directly collects at any other moment. A session material's handle counts as reached while the entity it was keyed for stands, and stops counting once that entity is gone. It reaches the GPU textures the device holds beside the registry's own: a texture the cache loaded for an asset goes once nothing live names it and is read back from that asset the next time something asks for it, while one no asset answers for stays, there being nothing to read it back from — a render pass's own target, a colour swatch, an atlas the engine built. A texture the ASSET path uploaded and whose asset has since been removed has nothing to come back from either, and the collection decides about it from its holders the way it does about every other resource: a handle a script still reaches, a live owner, a reference from live engine state, a hold. Features go first, then materials, then meshes, then textures, so a texture only a released material named goes with the material. Runs a full garbage collection first, so a handle nothing reaches counts as let go, and yields for the frame the census runs on. A handle the calling function still has in a variable — or in a temporary it has not overwritten — is one a script reaches, so a resource created in the function that collects is let go by the next collection rather than this one. A runtime mesh needs no call: once the garbage collector has taken its last handle, the engine judges it against the same holders, again whenever what displays it changes or its hold is released, and every few hundred frames while something holds it, and releases it once nothing does. Returns once what it released has left the device: the GPU objects of the textures and meshes it let go are freed over the frames after the call, and it waits for those frames (up to 60 drawn frames), so <code>renderer.texture.list</code>, <code>renderer.mesh.list</code>, <code>renderer.material.list</code>, <code>renderer.textureMemory()</code> and <code>renderer.gpuMemory()</code> read after it in the same script no longer count anything it reports released. <code>landed</code> says whether every release left the device within that wait; false names a texture or mesh something put back on the device before it could be seen gone, which <code>renderer.references</code> explains. Code that cannot yield (a metamethod, <code>onDestroy</code>, a module's top level while <code>require</code> runs it) is answered at once, without the wait: there <code>landed</code> is false while a release is still on its way, and the listings and totals stop counting it over the drawn frames that follow.</p> <pre><code class="language-lua">local c = renderer.collect() print(c.released.texture, c.kept) local c = renderer.collect() print(c.landed, renderer.gpuMemory().meshes) </code></pre> <h2 id="modules-renderer-compiledshaders">modules/renderer/compiledShaders</h2> <pre><code class="language-lua">compiledShaders(): { string } </code></pre> <p>Every key <code>renderer.compiledSource</code> answers for — one per shader key (<code>guid@checksum</code>) or variant program a compile has run under, whether it succeeded or failed, for as long as the shader that compiled under it stands: a removed <code>.shader</code> takes its keys with it. What makes the composed-source surface enumerable rather than something to guess a key for.</p> <pre><code class="language-lua">for _, name in renderer.compiledShaders() do print(name) end </code></pre> <h2 id="modules-renderer-compiledsource">modules/renderer/compiledSource</h2> <pre><code class="language-lua">compiledSource(shader: string): string? </code></pre> <p>The WGSL the shader compiler received under one name, exactly as it received it — the composed module, which is what a compile error's line numbers and handle indices are positions in. Answers for a shader key (<code>guid@checksum</code>), a <code>program</code> from <code>renderer.shaderVariants()</code>, or any reference to a shader asset (identity, path, guid), which reaches the key of the version that reference resolves to now; for a shader that declares no features, and for a shader whose compile FAILED, which is the case it exists for: a message about a function body carries a position and nothing else, and the text that position is in is this. The failed text stands for as long as <code>shaderRef:compileStatus()</code> reports that failure.</p> <p><strong>Parameters</strong></p> <ul> <li><code>shader</code> <code>string</code> — A shader key, a <code>shaderVariants()</code> program name, or a reference to a shader asset.</li> </ul> <pre><code class="language-lua">local status = asset.resolve("myShader", "shader"):compileStatus() if status.status == "failed" then print(status.error) print(renderer.compiledSource("myShader")) end </code></pre> <h2 id="modules-renderer-compositesize">modules/renderer/compositeSize</h2> <pre><code class="language-lua">compositeSize(): { width: number, height: number } </code></pre> <p>The size of the image the post-scene phases worked on in the last presented frame — the target the UI composites onto, which every pass after the scene reads as <code>@scene.color</code> and writes into, and which a <code>screenSpace = "composite"</code> render target follows. While the renderer presents the viewport itself that is the display's own size, whatever fraction of it the scene rasterized at; while a UI viewport panel owns the presentation it is the size the scene rasterized at, since the panel draws the scene target at its own rect and nothing upscales before the composite. Both read <code>0</code> before a frame has drawn.</p> <pre><code class="language-lua">local c = renderer.compositeSize() </code></pre> <h2 id="modules-renderer-cullstats">modules/renderer/cullStats</h2> <pre><code class="language-lua">cullStats(): { </code></pre> <p>What the last completed frame decided to draw. <code>total</code> renderables went into the frustum test, <code>culled</code> fell outside it and <code>visible</code> survived. Of those, occlusion culling measured <code>occlusionTested</code> against the depth pyramid and proved <code>occlusionCulled</code> were entirely behind other geometry — both 0 while <code>renderer.occlusionCulling()</code> is false. A renderable the pyramid has no say over — one that laid no depth in the visibility raster, one whose bounds were never recorded, one straddling the near plane — is measured against nothing and counted in neither, so the gap between <code>visible</code> and <code>occlusionTested</code> reads how much of the frame the test could speak for.</p> <p>This answers for the main camera. What the shadow page cull did with the frame's casters for each light is on that light's row in <code>renderer.shadowViews()</code>.</p> <p><code>views</code> is the engine cull's own count, one row per camera view that rendered: <code>view</code> is <code>"main"</code> for the main camera and the camera's entity id for an offscreen one, <code>kind</code> says which, and <code>frame</code> is the frame the counts describe. The main view's row is first. <code>drawn</code> is whether a render drew the view in that frame: a frame the renderer declined still ran the cull's first test, so its row carries that test's counts and no second one, for a picture nothing rasterized. The counts are read back from the GPU and handed out oldest first, one frame per render, so a script reading once a frame reads the frames in the order they ran. A frame's counts wait at most two renders to be handed out: a row describes a frame as many frames back as the read took and at most two more, and when more frames' counts come back at once than that lets wait, the oldest of them are the frames a script does not read. <code>instancesTested</code> renderables were tested; <code>instancesCulled</code> breaks the rejections down by reason (<code>hidden</code>, <code>layer</code>, <code>range</code>, <code>frustum</code>, <code>screenSize</code>, and <code>occludedPass1</code>/<code>occludedPass2</code> for occlusion against the depth of the frame that last drew the view and then this frame's); <code>instancesRecovered</code> came back in the second test. Then the clusters of the survivors: <code>clustersTested</code>, rejected by the level-of-detail cut (<code>clustersLodRejected</code>), the frustum (<code>clustersFrustumCulled</code>), facing away (<code>clustersConeCulled</code>) or occlusion (<code>clustersOccludedPass1</code>, of which <code>clustersRecovered</code> came back), and <code>clustersDrawn</code> / <code>trianglesDrawn</code>. <code>binsPending</code> counts material pipelines still compiling, whose clusters wait a frame; <code>overflow</code> counts clusters that did not fit the view's lists. <code>total</code>/<code>culled</code>/<code>visible</code> and <code>occlusionTested</code>/<code>occlusionCulled</code> above are the main view's count for the renderables drawn outside the cluster lists.</p> <pre><code class="language-lua">local s = renderer.cullStats(); print(s.visible - s.occlusionCulled, "drawn") for _, v in renderer.cullStats().views do if v.view == "main" then print(v.clustersDrawn, "clusters,", v.instancesCulled.frustum, "outside the frustum") end end </code></pre> <h2 id="builtins">modules/renderer/debugPass.builtins</h2> <pre><code class="language-lua">debugPass.builtins(): { string } </code></pre> <p>The built-in debug-pass names, one per channel in channel order — the engine's built-in pass vocabulary (final, albedo, normal, depth, …).</p> <pre><code class="language-lua">for _, n in ipairs(renderer.debugPass.builtins()) do ... end </code></pre> <h2 id="channel">modules/renderer/debugPass.channel</h2> <pre><code class="language-lua">debugPass.channel(name: string): number? </code></pre> <p>The channel a debug-pass NAME renders on: a built-in pass, else a content capture view registered via <code>renderer.captureView</code>. Nil when the name is neither — the signal a selector uses to reject an unknown pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — A debug-pass name (e.g. "normal", "depth", "lightmap").</li> </ul> <pre><code class="language-lua">local ch = renderer.debugPass.channel("normal") -- 7 </code></pre> <h2 id="list">modules/renderer/debugPass.list</h2> <pre><code class="language-lua">debugPass.list(): { string } </code></pre> <p>Every selectable debug-pass name: the built-in passes plus every registered content capture view. What a debug-pass selector offers.</p> <pre><code class="language-lua">local passes = renderer.debugPass.list() </code></pre> <h2 id="name">modules/renderer/debugPass.name</h2> <pre><code class="language-lua">debugPass.name(channel: number): string? </code></pre> <p>The canonical NAME for a debug channel: a built-in pass name for a built-in channel, else a registered capture view's name. Channel 0 is "final" (the lit image). Nil when no pass owns the channel.</p> <p><strong>Parameters</strong></p> <ul> <li><code>channel</code> <code>number</code> — The channel number.</li> </ul> <pre><code class="language-lua">local name = renderer.debugPass.name(7) -- "normal" </code></pre> <h2 id="modules-renderer-destroy">modules/renderer/destroy</h2> <pre><code class="language-lua">destroy(handleOrKind: any, id: string?): boolean </code></pre> <p>Free the GPU resource a renderer resource holds (the GPU-destroy verb). Takes any of the forms that name it: the handle a create returned, routed by its <code>category</code> so one call releases a mixed set of handles; the id a listing hands out, whose kind is read back off what the renderer holds under it — the runtime registry, the material definitions, the live features, and the device itself for an asset's own texture or mesh; or the kind with the id beside it, the shape <code>renderer.hold</code> and <code>renderer.references</code> take, which is what names the kind for an id two of them answer to. An id nothing holds anything under releases nothing and answers false. The on-disk asset, if any, is untouched. A CPU handle's <code>:unload()</code> frees the CPU copy separately.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrKind</code> <code>any</code> <em>(optional)</em> — A <code>MeshHandle</code>, <code>TextureHandle</code>, <code>MaterialHandle</code> or feature handle; the id itself; or the kind (<code>"texture"</code>, <code>"material"</code>, <code>"mesh"</code>, <code>"feature"</code>) with the id as the second argument.</li> <li><code>id</code> <code>string?</code> <em>(optional)</em> — The guid or registry key, when the first argument is a kind.</li> </ul> <pre><code class="language-lua">renderer.destroy(meshHandle); renderer.destroy(materialHandle) renderer.destroy(renderer.texture.list()[1].guid) renderer.destroy("mesh", guid) </code></pre> <h2 id="modules-renderer-devicegeneration">modules/renderer/deviceGeneration</h2> <pre><code class="language-lua">deviceGeneration(): number </code></pre> <p>Which render device this process is on, counted from the first.</p> <p>A render device is lost when a driver resets, when the GPU is taken away, or when a browser reclaims a WebGPU context. The engine answers by building another device and re-deriving this session's resources onto it, and this number moves by one each time it does. Anything held across frames that was built from a GPU resource records this beside it and remakes it when the two differ; <code>engine.onDeviceRebuilt</code> is the hook that fires when it moves.</p> <pre><code class="language-lua">local generation = renderer.deviceGeneration() engine.onDeviceRebuilt(function(g) print("device " .. g) end) </code></pre> <h2 id="modules-renderer-devicestate">modules/renderer/deviceState</h2> <pre><code class="language-lua">deviceState(): string </code></pre> <p>Whether the render device this process draws through is the one it is using, one it is replacing, or one the platform has refused every quick attempt to replace.</p> <p><code>"ready"</code> is a live device. <code>"rebuilding"</code> is the window between a device reporting itself lost and another being in place: every GPU resource built from the old one is invalid, the frames in that window draw nothing, and anything reaching the GPU refuses. <code>"abandoned"</code> is after every quick attempt at another device was refused (a browser whose GPU process crashed twice answers no adapter for minutes): nothing is drawn, the engine log names the platform's last answer, and the engine keeps asking at a slower pace, so the state returns to <code>"ready"</code> on its own once the platform hands a device over. Restarting the engine (reloading the page on the web) builds one from scratch.</p> <p>Work that spans the device — build a render target, draw into it, read it back — reads this to tell an operation that failed because the device went out from under it, which is worth doing again once <code>renderer.deviceGeneration()</code> moves, from one that failed on its own terms. The loss is reported before the next device exists, so the two readings answer different halves: this one says a replacement is coming, the generation says it arrived.</p> <pre><code class="language-lua">if renderer.deviceState() == "rebuilding" then return end </code></pre> <h2 id="modules-renderer-drawdiagnostics">modules/renderer/drawDiagnostics</h2> <pre><code class="language-lua">drawDiagnostics(): { DrawDiagnostic } </code></pre> <p>Every renderable that is NOT drawing what its material says — the one call for "why does this surface look wrong". Three states land here: a surface rendering as the magenta placeholder (<code>substituted</code>), one the renderer could bind nothing for at all (<code>outcome = "skipped"</code>), and one drawing a program whose most recent compile FAILED (<code>stale</code>), which is what a shader edited into brokenness looks like — the pipeline its last good compile built keeps drawing, so the picture is intact and answers to none of the edits since. Each row names the entity, the program asked for, the program bound, <code>programStatus</code> — the compile gate's word about the program the material NAMED — and the one cause from <code>shaderCompileFailed</code> / <code>shaderNotRegistered</code> / <code>shaderNotCompiledYet</code> / <code>noGbufferEntry</code> / <code>renderStateKeyNotBuilt</code> / <code>noPipelineForTarget</code> / <code>unshaded</code>, with the compiler's own message in <code>detail</code> or <code>programError</code>. Covers every renderable the renderer holds, whether or not a camera reached it: a row with <code>observed = false</code> and <code>outcome = "notDrawn"</code> carries the renderer's own resolution for one this frame drew nowhere, so a broken surface off-screen is reported the same as one in frame. An empty result means every renderable the renderer holds is drawing the program its material named and that program compiles. Answers on the deferred path as well as forward, and in edit mode as well as play.</p> <pre><code class="language-lua">for _, d in renderer.drawDiagnostics() do print(d.entity, d.reason, d.detail) end if #renderer.drawDiagnostics() == 0 then print("every surface is drawing what it says") end </code></pre> <h2 id="modules-renderer-drawstats">modules/renderer/drawStats</h2> <pre><code class="language-lua">drawStats(): { </code></pre> <p>What the last completed frame actually submitted. <code>draws</code> counts every geometry draw call the frame issued — the camera's passes, each shadow view a shadow-casting light adds, and whatever a render feature draws — and <code>instances</code> counts the instances those draws covered. The pair is what separates one draw carrying five hundred instances from five hundred draws carrying one each, so it reads how well the scene batches rather than how many objects are in it.</p> <p><code>compacted</code> is how many of those instances the frame planned through draws whose instance count the GPU decides: the culler's own per-object answers packed into a dense run, so an object it rejects is absent from the draw instead of collapsing to nothing in the vertex stage. <code>compactedDrawn</code> is how many of them survived, counted on the GPU as it packed them — a pass that then skips a whole draw over its own layer or visibility answer leaves that draw's instances in both numbers.</p> <p>The plan is made over the populations the frame draws, and the tests answer which of their instances the packing keeps. That packing runs before any pass has resolved the depth occlusion culling is tested against, so on its own it reads the frustum and screen-size answers alone. With <code>setOcclusionCulling</code> armed the frame packs the same plan a second time once the test has answered, and <code>compactedDrawn</code> then counts what came through occlusion as well.</p> <p><code>compactedDrawn</code> comes back from the buffer the GPU wrote, so it describes a frame that has finished while <code>compacted</code> describes the most recent plan, and it holds the last count the GPU wrote until another arrives — a frame that compacts nothing reads <code>compacted</code> 0 beside the count from the last frame that did. In a scene standing still the gap between the two is the front-end work culling removed.</p> <p><code>materialBinds</code> is how many times the frame's geometry passes set a <strong>material group's</strong> textures (group 1), and <code>materialBindsElided</code> how many times a pass reached that decision and found the group already bound. On the bindless backend every material group binds the one texture group of the frame, so a pass binds it once; on buckets a pass binds once per group it draws (see <code>renderer.materialTable().groups</code>). Their sum is how many times the decision was reached — once per unit of geometry submitted, which sits at or below <code>draws</code>, since a mesh of several primitives draws once per primitive under one set of binds. <code>materialExtraBinds</code> and <code>materialExtraBindsElided</code> are the same pair for group 2, the storage buffers a shader declares, bound per distinct buffer set, which only the shaders that declare them ever bind.</p> <p><code>pipelineBinds</code> and <code>pipelineBindsElided</code> are the same pair for the pipeline itself: how many times the frame's geometry passes set one, and how many times a pass reached that decision and found the pipeline it wanted already bound. Which pipeline a unit needs follows its shader, its material's render state and its mesh's vertex layout together, so a scene whose units share all three costs one set for the run of them, while units differing in any one of the three each pay their own. Their sum is how many units reached the pipeline decision, which sits at or above what the material pair reports: a unit the pass settles a pipeline for and then abandons — one whose material group resolved to nothing — counts here and never reaches the material decision.</p> <p>Every figure here is the whole frame's. <code>renderer.shadowViews()</code> carries the main camera's own share of <code>compacted</code> and <code>compactedDrawn</code>.</p> <p><code>clusters</code> and <code>clusterTriangles</code> are the clusters and triangles the camera views drew from the cluster lists, from the GPU's own count.</p> <p><code>drawsByPass</code> splits <code>draws</code> by the part of the frame that submitted them — <code>visibility</code> (the id raster: one draw per raster bin per Hi-Z phase in each camera view, submitted whether or not the GPU cull left anything in the bin), <code>resolve</code> (one draw per resolve draw in each camera view: one per shader, and on the array-bucket backend one per shader and material group), <code>legacy</code> (geometry drawn outside cluster storage: skinned, morphed and compute-written meshes, line topology, shaders that write their own vertex stage), <code>blended</code>, <code>exception</code> (opaque materials whose depth state the visibility buffer cannot express), <code>featureGeometry</code>, <code>featureDraw</code>, <code>shadow</code> and <code>splat</code> (one draw per splat buffer a render feature draws) — and the nine sum to <code>draws</code>.</p> <p><code>instances</code> counts instances named by draws whose size the CPU decided; the visibility raster's work is <code>clusters</code> and <code>clusterTriangles</code>, which the GPU decided. <code>pixelsResolved</code> is how many pixels the resolve shaded, summed over camera views, and <code>pixelsByShader</code> the same split by shader. <code>clusters</code>, <code>clusterTriangles</code>, <code>pixelsResolved</code> and <code>pixelsByShader</code> come back from the GPU and describe the frame numbered <code>frame</code>, which is earlier than the frame <code>draws</code> describes. <code>frame</code> is the renderer's own frame number, counted from 0 when the renderer was built (again after a device loss), a count of its own that compares only with another <code>drawStats().frame</code>.</p> <pre><code class="language-lua">local d = renderer.drawStats(); print(d.instances / math.max(d.draws, 1), "instances per draw") print(renderer.drawStats().compactedDrawn, "of", renderer.drawStats().compacted, "survived the cull") local d = renderer.drawStats(); print(d.materialBinds, "material group binds over", d.materialBinds + d.materialBindsElided, "units") local d = renderer.drawStats(); print(d.pipelineBinds, "pipeline sets over", d.pipelineBinds + d.pipelineBindsElided, "units") local d = renderer.drawStats(); print(d.clusters, "clusters,", d.clusterTriangles, "triangles from the cluster lists") local d = renderer.drawStats(); print(d.drawsByPass.visibility, "id-raster draws,", d.drawsByPass.resolve, "resolve draws") local d = renderer.drawStats(); for shader, px in d.pixelsByShader do print(shader, px) end </code></pre> <h2 id="modules-renderer-editorchromeonwindow">modules/renderer/editorChromeOnWindow</h2> <pre><code class="language-lua">editorChromeOnWindow(): boolean </code></pre> <p>Read back the frames the engine presents, exactly as presented: no camera is spawned and nothing is drawn for the read, so the frames carry the viewport's real temporal history (TAA and every accumulating effect), its camera motion (motion blur), its auto exposure and its render scale and upscale. From the first frame presented after the call, every <code>interval</code>-th frame is copied out as it is finished, until <code>frames</code> are taken; the call yields until all of them have landed and returns them in the order they were presented. Each frame's pixels are its <code>cpu</code> field, a <code>TextureCpuHandle</code> (<code>readPixel</code>, <code>tone</code>, <code>rgba</code>, <code>encodePng</code>, <code>unload</code>).</p> <p><code>scope = "viewport"</code> (the default) is the game view as finished for the player: after the post chain, the upscale, the display transform, the content UI and every post-scene pass, at the size the frame was composed at (a viewport panel's own texture where a panel shows it). <code>scope = "window"</code> is the whole presented surface (the window, the browser canvas or the headless framebuffer) with every window-level UI pass on it, the editor's chrome included. Where no panel shows the viewport the two are the same image, and while the editor's chrome is on the window each such viewport frame carries a <code>note</code> saying the chrome stands over the scene in it and how to show the Scene panel.</p> <p>Each frame states what it was drawn with: <code>frame</code> (consecutive frames carry consecutive numbers), the raster and composite sizes, <code>renderScale</code> and <code>scaleMode</code>, the projection <code>jitter</code>, the display <code>exposure</code>, and <code>historyReset</code>. Call <code>frame.cpu:unload()</code> when done with the pixels.</p> <p><code>velocity = true</code> also takes the viewport's velocity buffer with each frame, copied at the same moment as its pixels, as <code>frame.velocity</code> (a <code>VelocityField</code>): each pixel's travel between the previous frame and this one in pixels per frame of the scene's raster, x right and y down, signed and unclamped, measured against the projection without its jitter. It is the field motion blur, temporal antialiasing and every other temporal effect read, camera motion included; <code>field:at(x, y)</code>, <code>field:row(y)</code> and <code>field:region(rect)</code> read it.</p> <pre><code class="language-lua">local f = renderer.presentedFrames({ frames = 8 }); for _, fr in f do print(fr.frame, fr.renderScale, fr.cpu:tone().mean); fr.cpu:unload() end local fr = renderer.presentedFrames({ velocity = true })[1]; local v = fr.velocity :: any; local c = v:at(v.width // 2, v.height // 2); print(fr.frame, c.x, c.y); fr.cpu:unload() </code></pre> <h2 id="configure">modules/renderer/feature.configure</h2> <pre><code class="language-lua">feature.configure(handleOrRef: any, settings: { [string]: any }): { [string]: any } </code></pre> <p>Lay <code>settings</code> over a live feature's knobs. Any omitted knob keeps its value; a knob the feature never declared, or a value of the wrong shape, is refused by name. The feature reads the result as <code>ctx.settings</code> from its next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrRef</code> <code>any</code> <em>(optional)</em> — A <code>RenderFeatureHandle</code>, its guid, or the feature's ref.</li> <li><code>settings</code> <code>{ [string]: any }</code> — The knob values to apply.</li> </ul> <pre><code class="language-lua">renderer.feature.configure(h, { intensity = 0.5, quality = "medium" }) </code></pre> <h2 id="create">modules/renderer/feature.create</h2> <pre><code class="language-lua">feature.create(ref: any, opts: (string | { guid: string?, settings: { [string]: any }? })?): any </code></pre> <p>Instantiate a render feature so the engine calls its <code>render(ctx)</code> hook every frame. <code>ref</code> is an <code>AssetRef<renderFeature></code> whose <code>init.luau</code> returns <code>{ setup?, render, teardown? }</code>. Returns a live <code>RenderFeatureHandle</code> (its <code>guid</code> is the stable id, same as mesh/texture handles); tear it down with <code>renderer:destroy(handle)</code>. Pass <code>guid</code> to assign a specific id.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>any</code> <em>(optional)</em> — An <code>AssetRef<renderFeature></code>, or a string identity/guid resolved via <code>asset.resolve(ref, "renderFeature")</code>.</li> <li><code>opts</code> <code>(string | { guid: string?, settings: { [string]: any }? })?</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local h = renderer.feature.create(asset.resolve("acrylic", "renderFeature")) local h = renderer.feature.create("acrylic") local h = renderer.feature.create("@builtin::systems.reflections.ssr", { settings = { intensity = 0.8 } }) </code></pre> <h2 id="destroy">modules/renderer/feature.destroy</h2> <pre><code class="language-lua">feature.destroy(handleOrGuid: any): boolean </code></pre> <p>Tear down a live render feature by its <code>RenderFeatureHandle</code> OR its guid string — the by-id path for when the handle was lost (e.g. across <code>execute</code> calls). Same effect as <code>renderer.destroy(handle)</code>. Returns true if a feature was live under that id.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrGuid</code> <code>any</code> <em>(optional)</em> — A <code>RenderFeatureHandle</code> or its <code>guid</code> string.</li> </ul> <pre><code class="language-lua">renderer.feature.destroy("eb623975-d8bd-47a1-a0a4-5c0e56238e2f") </code></pre> <h2 id="find">modules/renderer/feature.find</h2> <pre><code class="language-lua">feature.find(ref: any): any </code></pre> <p>The live handle of the feature running <code>ref</code> — an AssetRef<renderFeature> or a renderFeature identity — or nil when none is. The check a caller makes before creating one, so a feature already live is configured rather than stacked.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>any</code> <em>(optional)</em> — An AssetRef<renderFeature>, a renderFeature identity, or a guid.</li> </ul> <pre><code class="language-lua">local h = renderer.feature.find("@builtin::systems.reflections.ssr") </code></pre> <h2 id="list">modules/renderer/feature.list</h2> <pre><code class="language-lua">feature.list(): { { guid: string, identity: string, settings: { [string]: any } } } </code></pre> <p>List every render feature currently live (running its <code>render(ctx)</code> each frame). Each entry is <code>{ guid, identity, settings }</code> — the <code>guid</code> is the same id a <code>RenderFeatureHandle</code> carries, so you can tear a feature down by guid even after losing its handle (e.g. across separate <code>execute</code> calls), and <code>settings</code> is every knob the feature declares at the value in force.</p> <pre><code class="language-lua">for _, f in renderer.feature.list() do print(f.identity, f.guid) end </code></pre> <h2 id="schema">modules/renderer/feature.schema</h2> <pre><code class="language-lua">feature.schema(ref: any): { [string]: any } </code></pre> <p>The settings a <code>.renderFeature</code> declares, with each knob's descriptor, read without running it: what a caller may pass to <code>create</code> / <code>configure</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>any</code> <em>(optional)</em> — An AssetRef<renderFeature> or a renderFeature identity.</li> </ul> <pre><code class="language-lua">for name, desc in pairs(renderer.feature.schema("@builtin::systems.reflections.ssr")) do print(name, desc.default) end </code></pre> <h2 id="settings">modules/renderer/feature.settings</h2> <pre><code class="language-lua">feature.settings(handleOrRef: any): { [string]: any }? </code></pre> <p>A live feature's settings as they stand: every declared knob at its default or at what <code>create</code> / <code>configure</code> set. A copy — writing to it changes nothing; <code>configure</code> does.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrRef</code> <code>any</code> <em>(optional)</em> — A <code>RenderFeatureHandle</code>, its guid, or the feature's ref.</li> </ul> <pre><code class="language-lua">local s = renderer.feature.settings(h); print(s.intensity) </code></pre> <h2 id="shaded">modules/renderer/feature.shaded</h2> <pre><code class="language-lua">feature.shaded(): { [string]: number } </code></pre> <p>How many pixels each fragment pass a render feature enqueued shaded on the last drawn frame, keyed by the pass's shader/effect name. A fragment pass draws one triangle over its target, so it shades the whole screen whatever its effect actually reaches — unless it declares <code>bounds</code> on the pass spec, the world-space box its effect stays inside, in which case it shades the rectangle that box projects into for the camera drawing it and is skipped for a camera that cannot see the box at all. This is the reading that says which of the two a pass is: it moves when the effect moves, and a pass absent from it shaded nothing. Summed over every camera the frame drew.</p> <pre><code class="language-lua">local px = renderer.feature.shaded(); for name, n in pairs(px) do print(name, n) end </code></pre> <h2 id="configure">modules/renderer/featureTexture.configure</h2> <pre><code class="language-lua">featureTexture.configure(width: number, height: number, layers: number) </code></pre> <p>Size the shared feature-texture array — the layers a surface shader reads through <code>zero_feature_texture(uv, layer)</code>, and the layers a <code>SpotLight</code> projects through its cone via <code>cookieLayer</code>. Layers are <code>rgba16f</code>. A call for the size the array already has is left alone. One that changes the size reallocates, and the replacement is zeroed — so it empties every layer in the array, including the layers other features and other cookies own. <code>renderer.featureTexture.state()</code> reports the extent and the layers holding content, which is how a feature re-fills the layer a resize took from it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>width</code> <code>number</code> — Layer width in pixels.</li> <li><code>height</code> <code>number</code> — Layer height in pixels.</li> <li><code>layers</code> <code>number</code> — How many layers the array holds.</li> </ul> <pre><code class="language-lua">renderer.featureTexture.configure(512, 512, 4) </code></pre> <h2 id="setlayer">modules/renderer/featureTexture.setLayer</h2> <pre><code class="language-lua">featureTexture.setLayer(layer: number, textureKey: string, x: number, y: number) </code></pre> <p>Copy a texture already on the GPU into one layer of the shared array, its top-left corner at <code>(x, y)</code> — GPU to GPU, with no readback. Several small images pack into one layer by calling this once per image at different offsets. The source must be <code>rgba16f</code> and fit at that offset.</p> <p><strong>Parameters</strong></p> <ul> <li><code>layer</code> <code>number</code> — Which layer of the array to write into.</li> <li><code>textureKey</code> <code>string</code> — The source texture's name — the one it was created under. A <code>compute.createStorageTexture2D</code> target, a <code>compute.createTextureHistory</code> pair (its current side), and a texture a <code>compute.copyBufferToTexture</code> wrote all answer to the name they were given.</li> <li><code>x</code> <code>number</code> — Left edge of the destination rectangle, in pixels.</li> <li><code>y</code> <code>number</code> — Top edge of the destination rectangle, in pixels.</li> </ul> <pre><code class="language-lua">compute.createStorageTexture2D("gobo", { width = 256, height = 256, format = "rgba16f" }) renderer.featureTexture.setLayer(0, "gobo", 0, 0) </code></pre> <h2 id="state">modules/renderer/featureTexture.state</h2> <pre><code class="language-lua">featureTexture.state(): { </code></pre> <p>What the shared feature-texture array is right now: the extent every layer carries, and <code>filled</code>, the ascending 0-based indices of the layers a <code>setLayer</code> has landed in since the array was last sized. One array is shared by every feature and every light cookie in the scene, and it has no allocator, so this is the call that tells a feature whether the array it sized and filled is still the array it is writing into — a <code>configure</code> that changed the size reallocates and zeroes every layer, and the layer it emptied leaves <code>filled</code> without it. Measured off the renderer at the end of the last rendered frame, so a <code>configure</code> or <code>setLayer</code> issued this frame reads back on a later one.</p> <p>What this describes is the array a shader samples. The source texture a <code>setLayer</code> copied FROM is a GPU resource of its own and keeps the bytes it was written with for as long as it lives, so <code>filled</code> is the reading that answers whether the layer behind a <code>cookieLayer</code> is live right now.</p> <p><code>fills</code> counts, per layer, the <code>setLayer</code>s that have landed in it since the array was last sized: <code>fills[layer + 1]</code> for a 0-based <code>layer</code>. A fill of a layer that already holds content leaves <code>filled</code> as it was, so the count moving is how a feature re-filling its own layer reads its fill arriving.</p> <pre><code class="language-lua">local ft = renderer.featureTexture.state() print(("feature textures: %dx%d over %d layers"):format(ft.width, ft.height, ft.layers)) -- Re-fill the cookie layer this module owns if anything emptied it. if ft.width ~= myWidth or table.find(ft.filled, myLayer) == nil then print("refill the cookie layer", myLayer) end </code></pre> <h2 id="modules-renderer-framepacing">modules/renderer/framePacing</h2> <pre><code class="language-lua">framePacing(): FramePacing? </code></pre> <p>How far the CPU is allowed to run ahead of the GPU, and what holding it there cost the frame just finished. Submitting work to the GPU returns before the GPU has done it, and everything that submission holds — its staging allocations, its bind groups, its command buffer — stays alive until it completes. A frame that asks for more work than the GPU finishes in a frame's time therefore leaves that behind it, and unbounded that is memory growth rather than a lower frame rate.</p> <p><code>framesInFlight</code> is how many submitted frames have not reported done through the queue's completion signal, held under <code>maxFramesInFlight</code>: a device that keeps up reads under the bound, one that is behind reads at it. It counts submissions, which is its own quantity — how many presented images the swapchain permits in flight is a separate setting. <code>mechanism</code> names how that bound is enforced here: <code>submission-wait</code> waits for the frame that many frames back and reports the wait in <code>waitMs</code>, so a paced frame costs latency and still draws; <code>submitted-work-done</code> counts outstanding frames off the queue's completion signal and declines to start a frame while the bound is met, counting those in <code>pacedFrames</code> and leaving the last presented image up. <code>submittedFrames</code> counts the frames that were admitted and submitted, so it rises for as long as the renderer is producing frames — which is what tells a renderer running slowly under a tight bound from one that has stopped. <code>stalled</code> reads true while the pacer has given the bound up rather than hold the image indefinitely, on either mechanism: the counting one stands down when the completion signal goes quiet across a whole run of declined frames, and the waiting one when a wait on a frame reaches its ceiling with the GPU still holding it. Both clear on the first word the GPU sends after that. A signal that is merely late — a GPU finishing frames more slowly than the loop asks for them — keeps the bound, so <code>stalled</code> answers for a GPU that went silent. <code>standDowns</code> counts those over the session, which is how a session that lost the signal and got it back (<code>stalled = false</code> with <code>standDowns</code> above zero) reads apart from one that never lost it.</p> <p><code>producing</code> is whether the renderer drew the last frame. A headless renderer draws into an offscreen framebuffer that nothing presents, so its image reaches a reader only through something that copies it out: it draws while a consumer is asking — an MCP call in flight, a queued texture readback, a recording, a frame-egress session — and declines the frames between two asks. A renderer presenting to a display draws while what is on screen changes, and for a short settling run after the last change, and declines the frames of a still picture, which stays on screen; whatever else moves (the gameplay clock, a task owed the next frame, held input, a tool call) keeps running at the display's rate meanwhile, and an engine with nothing moving waits instead (<code>engineStill</code>, below). Both count the frames they declined in <code>idleSkippedFrames</code>. Every other renderer stat answers with the last frame that drew, so <code>producing</code> is what separates a live reading from a frozen one.</p> <p><code>drawPolicy</code> names the rule the last frame was decided under: <code>consumers</code> (headless), <code>changes</code> (presenting to a display) or <code>every_frame</code> (content asked for every frame with <code>renderer.setDrawEveryFrame(true)</code>, also read as <code>drawEveryFrame</code>). <code>pictureChanges</code> counts, per source, the frames on which something changed what the next frame draws: <code>ui</code> (a new UI layout or UI texture), <code>camera</code> (the viewport camera, the viewport, a frame setting or a renderer setting a script states: the projection offset, the shadow configuration, the render path), <code>scene</code> (a renderable added, removed, moved, shown or re-described), <code>material</code> (a material created, removed or given new properties, textures or a new shader), <code>lighting</code> (a light, the ambient term or the sky), <code>surface</code> (its size, format or present mode), <code>arrivals</code> (a pipeline, texture or material row still landing), <code>world</code> (a component written, an entity spawned or removed), <code>clock</code> (something drawn with a shader that reads the frame clock, while the clock runs), <code>compute</code> (a compute buffer written or dispatched over), <code>window</code> (the platform asked for the window to be drawn again), <code>texture</code> (a texture's contents replaced from the CPU, a video frame among them) and <code>content</code> (a script's command reached the engine: a post-process effect, a sky, an environment, a render feature). Every renderer counts them, a headless one included. <code>lastPictureChange</code> names the source of the latest, and <code>lastPictureChangeDetail</code> what within it changed where the source names it, and <code>pictureChangeDetails</code> holds, per source, what its latest named change named: the component a <code>world</code> change wrote, the command a <code>content</code> change applied, the program a <code>clock</code> change draws with. Read them on a renderer that keeps drawing a picture that looks still to find what is moving.</p> <p><code>presentMode</code> is what the surface presents with and <code>presentModes</code> what it offers; both are empty of meaning on a headless renderer, which never presents. A desktop presents <code>mailbox</code> unless told otherwise, and Android presents <code>fifo</code>, which shows each frame on a vsync of the panel and makes no frame the panel never shows.</p> <p>The rest describe the rate frames run at. <code>displayRefreshHz</code> is the refresh rate of the display the frames are shown on, where the platform reads it: the phone's panel, the monitor a desktop window is on, the cadence of a browser's animation frames. A headless engine presents to no display and states none. <code>frameRateCap</code> is the cap <code>renderer.setFrameRateCap</code> set, absent while none is. <code>frameRateMode</code> is how the rate is chosen (<code>renderer.setFrameRateMode</code>), <code>chosenFps</code> the rate the frame loop chose and <code>frameRateReason</code> why: <code>cap</code> (the highest rate the cap delivers), <code>display</code> (the display's highest rate, with no cap), <code>frame_time</code> (a lower even rate, because frames did not hold the faster one) or <code>uncapped</code> (no target: a surface that shows frames on any refresh, or no display, with no cap). <code>targetFps</code> is the rate the chosen one delivers on the mode the display runs in now and <code>refreshesPerFrame</code> the refreshes each frame stays on screen there; these and <code>chosenFps</code> are absent while no target is in force. <code>frameWorkMs</code> is the 90th percentile of the time recent drawn frames took, the time the rate is chosen from. <code>lateFrames</code> counts the drawn frames the loop came back to later than the vsync each was due for, <code>missedPresents</code> those the compositor showed later than one interval after the frame before them (a judder; 0 where the surface reports no present times), and <code>rateStepsDown</code> / <code>rateRaises</code> the times the chosen rate went down and up, all since the rates to choose from last changed (a new cap, mode or display). Each change is also one <code>[frame rate]</code> line in the engine log naming what decided it. <code>displayVote</code> names the platform call that tells the display the rate (<code>none</code> where there is none, and frames are held to the display by pacing alone) and <code>displayVoteFps</code> the display mode it last asked for. <code>deliveredFps</code> is the rate the frame loop has run frames at over about the last twenty frames.</p> <p>The last describe whether the engine runs frames at all. An engine with nothing to draw, its gameplay clock stopped, no deferred task or settled promise to resume, no commands waiting to be applied, no tool call in flight, no streamed sound playing and no input held is still, and its loop runs a frame at once when something reaches the engine, else when the earliest wait a task holds comes due, with frames at least a quarter of a second and at most a second apart. <code>engineStill</code> is whether the engine stated itself still at the end of its last frame (a reading taken inside a tool call reads false, since the call is in flight) and <code>movingBecause</code> names what kept it moving otherwise: <code>picture</code>, <code>gameplay</code>, <code>boot</code>, <code>tasks</code>, <code>input</code>, <code>recording</code>, <code>calls</code>, <code>audio</code> or <code>replay</code> (a physics replay plays). <code>stillWaits</code> counts the frames the loop held back for a still engine, <code>framesAfterStill</code> the frames run after such a wait, <code>wokenFrames</code> those of them an arrival started, and <code>stillSeconds</code> the seconds spent waiting; <code>stillFrames</code> counts the frames after which the engine stated itself still and <code>movingFrames</code> those after which it stated itself moving, per reason, so the reason a still scene keeps running frames is the count that grows between two readings.</p> <pre><code class="language-lua">local p = renderer.framePacing() print(("%d of %d frames in flight, paced by %s"):format(p.framesInFlight, p.maxFramesInFlight, p.mechanism)) </code></pre> <h2 id="modules-renderer-getraytrace">modules/renderer/getRaytrace</h2> <pre><code class="language-lua">getRaytrace(): boolean </code></pre> <p>Whether ray tracing is currently enabled.</p> <h2 id="modules-renderer-gpumemory">modules/renderer/gpuMemory</h2> <pre><code class="language-lua">gpuMemory(): GpuMemory </code></pre> <p>Where the renderer's GPU memory went at the last completed frame — the call to reach for when something is holding memory and you do not know what.</p> <p>Three figures answer three different questions, and they are meant to be read against each other:</p> <ul> <li>The categories — <code>shadow</code>, <code>textures</code>, <code>meshes</code>, <code>instances</code>, <code>compute</code>, summing to <code>categorised</code> — are the renderer's own accounting of what it asked for on purpose. Always present, on every backend. <code>textures</code> is what the resident textures occupy, and on the texture-array backend also the slack its shared arrays hold beyond them (<code>renderer.materialTable().bucketSlackBytes</code>).</li> <li><code>allocator</code> is the device allocator's ledger, with a row per creation label largest first, which is what names an allocation no category claims. A render target's colour texture is reported under what holds it now: the target drawing into it, or <code>released_scratch[<width>x<height>].color</code> while it waits for the next target of its size. It exceeds <code>categorised</code> by the per-frame render targets and the scratch nothing categorises, and on the texture-array backend, for the frame after an array grows or is repacked, by the array it replaced. The allocator hands memory out from blocks it reserves whole from the device and returns a block only once nothing is left in it, so <code>reservedBytes</code> runs above <code>allocatedBytes</code> by what those blocks hold unused; <code>blocks</code> lists them emptiest first with the labels that keep each one alive, and <code>emptyBytes</code> plus <code>slackBytes</code> is that distance exactly — the pool held in empty blocks, and the room pinned inside blocks something still sits in.</li> <li><code>driver.deviceLocalBytes</code> is what the graphics driver charges this process, out of the kernel's own accounting. It is the biggest of the three and the one that fills a card, because it also holds the swapchain, the images the driver keeps on the renderer's behalf, and the rounding to whole pages and heap blocks that neither figure above sees. Read it when the question is how much of the machine's GPU this engine is using; read the two above when the question is what the engine spent it on. A platform with no per-process accounting reports <code>available = false</code> and the reason.</li> <li><code>driver.outsideAllocatorBytes</code> is that charge less everything the allocator reserved — what the driver holds on its own account, and the one figure here nothing releases: a dropped pipeline, another scene and <code>renderer.collect()</code> all leave it where it is, and it falls when the device is destroyed. Read it when a session's device memory has grown and no ledger row accounts for the growth.</li> </ul> <p><code>compute</code> is what the compute subsystem holds; <code>compute.observe()</code> names each of those resources and what it costs. <code>arenas</code> states the part of it the pages runs stand in hold, and what those pages' bytes are spent on. <code>renderTargets</code> counts the offscreen render targets the renderer holds at that frame, which is what says a <code>renderer.destroy</code> has been applied rather than queued. <code>textureBudgetBytes</code> is the texture budget in force and <code>pressure</code> the memory pressure level that shapes it: under <code>"critical"</code> the budget reads half the device's, and <code>renderer.memoryPressure()</code> says what the newest report released.</p> <pre><code class="language-lua">local m = renderer.gpuMemory() print(("shadows %.1f MiB"):format(m.shadow.total / 1024 / 1024)) -- how much of the card this engine is holding: if m.driver.available then print(("driver %.1f MiB"):format(m.driver.deviceLocalBytes / 1024 / 1024)) else print("no driver figure: " .. m.driver.reason) end -- the biggest allocation in the engine: local top = m.allocator and m.allocator.byLabel[1] print(top and top.label, top and top.bytes) -- the block held for the least, and what pins it: local block = m.allocator and m.allocator.blocks[1] if block and block.allocationCount > 0 then print(block.size, block.allocatedBytes, block.byLabel[1].label) end </code></pre> <h2 id="modules-renderer-hold">modules/renderer/hold</h2> <pre><code class="language-lua">hold(handleOrKind: any, id: string?): boolean </code></pre> <p>Pin a runtime resource for the session. A held texture, material, mesh or render feature survives every collection — the one a root scene load runs and a direct <code>renderer.collect()</code> alike — until <code>renderer.release</code> lets it go or its destroy frees it. It is the way to keep an ad-hoc resource across the scenes that come and go under it. A hold keeps the resource in the registry; a mesh's GPU buffers are governed by what draws it, parked as a CPU definition when the last instance naming it goes and brought back when one names it again, so <code>renderer.mesh.isResident(guid)</code> is the separate question about the buffers.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrKind</code> <code>any</code> <em>(optional)</em> — The resource's handle, or its kind (<code>"texture"</code>, <code>"material"</code>, <code>"mesh"</code>, <code>"feature"</code>) with the guid or key as the second argument.</li> <li><code>id</code> <code>string?</code> <em>(optional)</em> — The guid or key, when the first argument is a kind.</li> </ul> <pre><code class="language-lua">renderer.hold(tex) renderer.hold("material", "swatch") </code></pre> <h2 id="clear">modules/renderer/instanceData.clear</h2> <pre><code class="language-lua">instanceData.clear(target: string | entityRef) </code></pre> <p>Drop every lane of an entity's per-instance shader data, so its draws read zero again — how a feature releases a subject it is still holding. Despawning an entity releases its block too, so this is for a subject that stays. It takes an entity that has already gone, which is when a feature releasing its subjects often runs, and does nothing for an entity holding no block.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity — a proxy from <code>entity(...)</code> / <code>entity.spawn(...)</code>, or an entity-id string.</li> </ul> <pre><code class="language-lua">renderer.instanceData.clear(subject) </code></pre> <h2 id="lanecount">modules/renderer/instanceData.laneCount</h2> <pre><code class="language-lua">instanceData.laneCount(): number </code></pre> <p>How many <code>vec4</code> lanes each entity's per-instance block holds, so a lane index runs <code>0 .. laneCount() - 1</code>. The same count a surface shader indexes <code>input.shader_data</code> against.</p> <pre><code class="language-lua">for lane = 0, renderer.instanceData.laneCount() - 1 do renderer.instanceData.set(subject, lane, 0) end </code></pre> <h2 id="set">modules/renderer/instanceData.set</h2> <pre><code class="language-lua">instanceData.set( </code></pre> <p>Write one <code>vec4</code> lane of an entity's per-instance shader data — the channel that lets ONE material serve many entities that differ in a value. A surface shader reads the lane back as <code>input.shader_data[lane]</code>, so a dissolve at its own progress per subject, an effect at its own age per firing, or a per-entity mask costs one material rather than one material per entity.</p> <p>The engine attaches no meaning to a lane: a feature picks the lane indices it owns and packs whatever its shader agrees they carry. Name those indices in the module that writes them, so the writer and the shader read the block the same way.</p> <p>A per-instance colour needs no shader of its own: the built-in <code>@builtin::materials.instance_color</code> (lit) and <code>instance_color_unlit</code> read lane 2 as <code>(r, g, b, coverage)</code>, and <code>instance_color_lane</code> names the lane on any <code>@builtin::shaders.pbr</code> or <code>unlit</code> material, so a stress colour, a heat map or a damage flash recoloured every frame is one <code>set</code> per entity.</p> <p>The write reaches the block where it is called, so the entity it names is the one holding that id at that point in the tick, and the value is on the draw from the next frame. It is held until the lane is written again, the entity's block is cleared, or the entity is despawned — a despawned entity releases its whole block. A lane an entity was never given reads zero.</p> <pre><code class="language-lua">-- One dissolve material, each subject at its own progress. local DISSOLVE_LANE = 0 for _, subject in ipairs(dying) do renderer.instanceData.set(subject.entity, DISSOLVE_LANE, subject.progress) end </code></pre> <h2 id="modules-renderer-lighting">modules/renderer/lighting</h2> <pre><code class="language-lua">lighting(): boolean </code></pre> <p>Whether the scene renders lit.</p> <h2 id="modules-renderer-losedevice">modules/renderer/loseDevice</h2> <pre><code class="language-lua">loseDevice() </code></pre> <p>Destroy the render device on the next frame, so the engine meets a real device loss.</p> <p>This is the one loss that can be caused on purpose, and it travels the same path a driver reset does: frames draw nothing until the rebuild lands, <code>GET /engine/status</code> reports the renderer as <code>recovering</code> while it does, <code>engine.onDeviceRebuilt</code> fires afterwards, and <code>renderer.deviceGeneration()</code> moves. Use it to prove that a world's content survives a device loss — anything it holds only on the GPU has to be remade from the rebuild hook, and this is how you find out whether it is.</p> <pre><code class="language-lua">renderer.loseDevice() -- some frames later: print(renderer.deviceGeneration()) -- one higher than before </code></pre> <h2 id="modules-renderer-maincameraview">modules/renderer/mainCameraView</h2> <pre><code class="language-lua">mainCameraView(): { number }? </code></pre> <p>The main camera's inverse view-projection (column-major, 16 numbers) followed by its world position (3 numbers) — <code>{m0..m15, px,py,pz}</code> — for reconstructing world positions from the depth buffer in a ray-tracing pass. Nil before the first render.</p> <h2 id="modules-renderer-markbaseline">modules/renderer/markBaseline</h2> <pre><code class="language-lua">markBaseline(): number </code></pre> <p>Record which render features are live and how the feature-texture array is configured, and make that the point <code>renderer.resetToBaseline</code> returns to.</p> <p>A caller running work in an engine it shares — a sweep of test suites, a tool driving a session — leaves the renderer holding whatever that work set up: a feature a module created on first use, an array reconfigured to the size one pass wanted. This is how such a caller says where its own changes start. mark above the marks before it.</p> <pre><code class="language-lua">local mark = renderer.markBaseline() -- then run, then renderer.resetToBaseline(mark) </code></pre> <h2 id="animatedtexture">modules/renderer/material.animatedTexture</h2> <pre><code class="language-lua">material.animatedTexture( </code></pre> <p>Build a material that PLAYS a layered texture: its layers bound as the frames, its timing bound beside them, and the engine's <code>animatedTexture</code> shader turning the clock into the layer showing now. One call from an imported animated image to a material an entity can wear.</p> <p>The layer showing is resolved per pixel against the texture's own schedule, so frames of unequal length are shown for the lengths they were authored with, and the sequence loops. <code>speed</code> scales the clock (2 plays twice as fast, 0 holds the frame <code>startTime</code> lands in) and <code>startTime</code> offsets into the sequence, so two surfaces sharing one texture can run out of phase.</p> <p>The clock is the engine's, and it runs in edit mode as much as in play and through a pause, so two screenshots of one surface taken moments apart are two different frames of it. <code>speed = 0</code> holds one frame for as long as it is set, which is the state to compare two screenshots in.</p> <p>The returned handle is what a surface wears — <code>Model:applySessionMaterial</code> takes it, and so does a Model's <code>material</code> field. The handle's <code>guid</code> is this material's REGISTRY KEY, the currency of <code>setProperty</code>, <code>describe</code> and <code>destroy</code>; a component field resolves an asset, so a bare key in one leaves the component waiting for an asset to register under that name.</p> <p>The builtin <code>plane</code> mesh emits <code>uv = (u, v)</code> with <code>v</code> along its own +Z, so a quad pitched +90° about X (<code>Transform.eulerToQuat(0, math.pi / 2)</code>) shows the image upright to a camera on +Z, and -90° shows it first-row-last.</p> <p>A texture whose layers carry no timing is rejected — there is nothing to play. <code>renderer.texture.info(bytes).animated</code> is the test.</p> <pre><code class="language-lua">local mat = renderer.material.animatedTexture("banner.texture") local id = entity.spawn("billboard", { rotation = { Transform.eulerToQuat(0, math.pi / 2) } }) entity(id).component.add("Model", { model = "plane" }) entity(id).component.get("Model"):applySessionMaterial(mat) renderer.material.setProperty(mat.guid, "speed", 2) </code></pre> <h2 id="create">modules/renderer/material.create</h2> <pre><code class="language-lua">material.create(content: MaterialContent, key: string): MaterialHandle </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>content</code> <code>MaterialContent</code></li> <li><code>key</code> <code>string</code></li> </ul> <h2 id="describe">modules/renderer/material.describe</h2> <pre><code class="language-lua">material.describe(key: string | { [string]: any } | AssetRef): any </code></pre> <p>The recoverable definition (<code>{ shader, properties, textures, name, render, features, bindings }</code>) this module registered under <code>key</code> via <code>renderer.material.create</code>, or nil for keys registered elsewhere (e.g. material assets resolved by the assetType). <code>properties</code> and <code>textures</code> carry the material's current values: each <code>setProperty</code> / <code>setTexture</code> write lands on this record, a texture slot under the GPU key the slot binds by — these are the WRITES, held here whether or not the renderer took them up. <code>renderer</code> beside them is what the renderer holds for the same key: the program its prepared bind group was built against, the texture each slot of its row names (<code>renderer.textures</code>, the writes the renderer took up), the render state its draws are looked up under, whether a pipeline exists for that key, and the draws, instances and binds the last published frame counted for it. A frame counts those only while something observes the renderer (<code>renderer.materialCost()</code>, <code>renderer.observe()</code> and <code>renderer.drawDiagnostics()</code> each keep it observing for the next 120 drawn frames, and <code>describe</code> does not), so <code>renderer.observed</code> states whether they were counted: <code>true</code> with the counts, <code>false</code> with <code>draws</code>, <code>instances</code>, <code>placeholderDraws</code>, <code>binds</code> and <code>bindsElided</code> all nil. <code>renderer</code> is nil when the renderer holds no material under this key at all, and <code>resident</code> states the same fact as a boolean. Writes reach the screen through both halves: <code>resident = false</code> says the renderer holds nothing to put them in, and <code>renderer.draws = 0</code> on a resident material says it holds them and no renderable is drawing with it. For a material that is resident AND drawn and still looks wrong, <code>renderer.drawDiagnostics()</code> names the renderable and the cause. A material that is not resident carries <code>shaderStatus</code> and <code>reason</code>, where its shader stands: <code>"absent"</code> when the shader it names resolves to no shader asset (nothing will compile it, so it stays unprepared until one arrives), <code>"pending"</code> while that shader has not compiled, <code>"failed"</code> with the compiler's message, or <code>"compiled"</code> when the shader is ready and the material has not been prepared yet. A resident material carries neither.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key, the <code>MaterialHandle</code> from <code>renderer.material.create</code>, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> </ul> <h2 id="destroy">modules/renderer/material.destroy</h2> <pre><code class="language-lua">material.destroy(key: string | { [string]: any } | AssetRef): boolean </code></pre> <p>Drop a runtime material registered via <code>renderer.material.create</code>: clears its recoverable definition, unregisters its runtime-resource stamp so it is no longer swept into the material freeze/save flow, and frees the GPU record. Use for transient materials (e.g. a preview swatch) that must not outlive their use. The on-disk asset, if any, is untouched.</p> <p>The reach is the registry: after this, <code>describe</code> and <code>list</code> stop answering for the key. A surface already wearing the handle goes on drawing what it was given — <code>Model:restoreSessionMaterial</code> is what puts a Model back on its authored material.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key (the one passed to <code>create</code>), the <code>MaterialHandle</code> <code>create</code> returned, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> </ul> <pre><code class="language-lua">renderer.material.destroy("__preview_swatch_" .. texGuid) </code></pre> <h2 id="list">modules/renderer/material.list</h2> <pre><code class="language-lua">material.list(): { any } </code></pre> <p>Every runtime material currently registered, ordered by registry key. Each entry carries the key, where it came from, the shader it binds, and for a key a <code>.material</code> asset filed (<code>guid@checksum</code>) the asset it is. <code>renderer.references("material", key)</code> says what is still holding a row, and <code>renderer.collect()</code> releases the rows nothing holds.</p> <pre><code class="language-lua">for _, m in ipairs(renderer.material.list()) do print(m.asset or m.guid, m.shader) end </code></pre> <h2 id="renderstate">modules/renderer/material.renderState</h2> <pre><code class="language-lua">material.renderState(key: string | { [string]: any } | AssetRef): MaterialObservation? </code></pre> <p>What the renderer holds for a material, which is a different document from the values written to it. <code>index</code> is the material index its row in the material table is filed under, which an edit rewrites in place, and <code>textures</code> the texture each slot of that row names, by slot name, under the key the texture is resident as; a slot drawing its shader's declared default is absent, so a <code>setTexture</code> the renderer has taken up reads here as the key it bound. <code>shader</code> is the program its prepared bind group was built against, <code>renderState</code> the blend / cull / topology / queue / depth key its draws are looked up under, <code>keyBuilt</code> whether a pipeline exists for that key, and <code>draws</code> / <code>instances</code> / <code>placeholderDraws</code> / <code>binds</code> / <code>bindsElided</code> what it cost in the frame the renderer last published. A frame counts those only while something observes the renderer, which <code>renderer.materialCost()</code> and <code>renderer.drawDiagnostics()</code> start, so <code>observed</code> states whether they were counted: <code>true</code> with the five counts, <code>false</code> with all five nil. <code>nil</code> means the renderer holds no material under this key at all — the writes landed on a record nothing is drawing with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key, the <code>MaterialHandle</code> from <code>renderer.material.create</code>, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> </ul> <pre><code class="language-lua">local r = renderer.material.renderState("water"); print(r.renderState.blend, r.keyBuilt) </code></pre> <h2 id="sessionkeyfor">modules/renderer/material.sessionKeyFor</h2> <pre><code class="language-lua">material.sessionKeyFor(entityId: string): string </code></pre> <p>The canonical registry key for an entity's SESSION material — the runtime material a system (e.g. GI baking) shows on an entity in place of its authored material for the lifetime of the engine session. One session material per entity: create it under this key, hand the handle to <code>Model:applySessionMaterial</code>, and the component re-adopts it across VM reloads by probing this key with <code>describe</code>. The key names the entity for as long as the entity stands: once it is gone the session store lets the handle go, and a collection releases the material and whatever its bindings were the last to hold.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string</code> — The entity carrying the material.</li> </ul> <pre><code class="language-lua">local key = renderer.material.sessionKeyFor(entityId) </code></pre> <h2 id="setclock">modules/renderer/material.setClock</h2> <pre><code class="language-lua">material.setClock(key: string | { [string]: any } | AssetRef, seconds: number?, rate: number?): () </code></pre> <p>Drive a material's surfaces from a clock of its own: the <code>time</code> its shader's <code>vertex()</code>, <code>vertex_clip()</code> and <code>fragment()</code> read. The clock reads <code>seconds</code> now and runs on by itself, <code>rate</code> of its seconds for every second of the world's gameplay time, so it is stated again only when that changes — a pause is a rate of 0, a seek is a new <code>seconds</code>, slow motion is a rate below 1. Every surface drawing the material follows it, motion vectors included. <code>seconds = nil</code> hands the material back to the world's time. A procedural graph's Generator drives the materials it realizes from its playhead this way, so an effect's shaders pause, slow and scrub with it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key, the <code>MaterialHandle</code> from <code>renderer.material.create</code>, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> <li><code>seconds</code> <code>number?</code> <em>(optional)</em> — The time the material's surfaces read now, or nil to release it.</li> <li><code>rate</code> <code>number?</code> <em>(optional)</em> — Seconds of the clock per second of gameplay time (default 1).</li> </ul> <pre><code class="language-lua">renderer.material.setClock(asset.resolve("frost_ice", "material"), 12.5, 0) </code></pre> <h2 id="setproperty">modules/renderer/material.setProperty</h2> <pre><code class="language-lua">material.setProperty(key: string | { [string]: any } | AssetRef, name: string, value: any): () </code></pre> <p>Push one changed uniform property to a registered material's GPU record (frame-fast incremental update; no re-register). Keyed by the material's registry key. The value written becomes the material's current one: it is what <code>describe</code> reports, and — for a property the material's shader declares, which is what the uniform buffer is packed by — what a material <code>AssetRef</code> reads back through <code>getProperty</code> / <code>getProperties</code> and what the surface is drawn with. A write under any other name reaches the record <code>describe</code> reports, which is where it reads back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key, the <code>MaterialHandle</code> from <code>renderer.material.create</code>, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> <li><code>name</code> <code>string</code> — Property name.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — New value.</li> </ul> <pre><code class="language-lua">renderer.material.setProperty(asset.resolve("wall", "material"), "roughness", 0.2) </code></pre> <h2 id="settexture">modules/renderer/material.setTexture</h2> <pre><code class="language-lua">material.setTexture(key: string | { [string]: any } | AssetRef, slot: string, ref: string | { [string]: any } | AssetRef): boolean </code></pre> <p>Push one changed texture slot to a registered material's GPU record. Keyed by the material's registry key. The slot is one the material's shader declares as a texture: a slot it does not declare is refused with a warning naming the texture slots it does declare, and the material is left as it was. While the shader's vocabulary has not been read (a custom shader that has not loaded) every slot is taken.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key, the <code>MaterialHandle</code> from <code>renderer.material.create</code>, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> <li><code>slot</code> <code>string</code> — Texture slot name (<code>"base_color_texture"</code>, …).</li> <li><code>ref</code> <code>string | { [string]: any } | AssetRef</code> — Texture reference — a <code>.texture</code> guid / identity / name / path, the image path it was imported from, a <code>color:</code> / <code>default:</code> form, a live GPU handle, or a texture <code>AssetRef</code> carrying one. An asset reference is materialised (Disk→CPU→GPU) and bound by the key the upload lands under.</li> </ul> <pre><code class="language-lua">renderer.material.setTexture("sky", "sky_texture", "panorama.texture") </code></pre> <h2 id="vocabulary">modules/renderer/material.vocabulary</h2> <pre><code class="language-lua">material.vocabulary(key: string | { [string]: any } | AssetRef): MaterialVocabulary? </code></pre> <p>A material's property names under <code>props</code> and its shader-declared and assigned texture slots under <code>textureSlots</code>, each a <code>{ [name] = true }</code> set — with <code>byRole</code> beside them, the index a key spelled in another engine's convention resolves to a declared name through. This is what a write's key is routed against on the record, in the shape <code>MaterialSchema.route</code> takes. It is read off the definition this module holds and touches nothing the renderer holds, so a call costs a table lookup and belongs on a per-frame write path; <code>describe</code> is the read that carries the renderer's own state. One frozen table per record, shared by every key that reaches it, rebuilt on the first read after a write puts a new name on the record.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string | { [string]: any } | AssetRef</code> — The material's registry key, the <code>MaterialHandle</code> from <code>renderer.material.create</code>, or an <code>AssetRef</code> from <code>asset.resolve</code>.</li> </ul> <pre><code class="language-lua">local MaterialSchema = require("@builtin::modules.material_schema") local route, name = MaterialSchema.route(assert(renderer.material.vocabulary(handle)), "color", { 1, 0, 0, 1 }) </code></pre> <h2 id="modules-renderer-materialcost">modules/renderer/materialCost</h2> <pre><code class="language-lua">materialCost(): { MaterialObservation } </code></pre> <p>What each material cost the frame the renderer last drew, and the state it holds each one under. One row per material the renderer holds a prepared bind group for — a material an author wrote and the renderer never prepared is absent, which is itself the answer to "why is nothing I set reaching the screen". <code>draws</code> and <code>instances</code> cover that one frame; <code>placeholderDraws</code> is how many of those draws bound the magenta placeholder instead of this material's own program. Those three count what this material drew itself: a batch drawing several materials of one group adds to each material the draws and instances that wore it.</p> <p><code>group</code> is the material group the material draws in — the unit a pass binds a material's textures (group 1) and its shader's storage buffers (group 2) by — or nil while the material resolves no group, which leaves its draws nothing to bind. <code>binds</code> is how many group-1 and group-2 binds the frame's passes issued for draws of this material's shader in that group, and <code>bindsElided</code> how many times such a draw found the group already bound, which is what draw-key sorting buys. Those binds belong to the group, so every material of one group reports the same pair; a material with no group reports 0 for both, and a draw that fell back to the placeholder counts in <code>placeholderDraws</code> and in neither bind count.</p> <p><code>uniformBytes</code> is the size of the property block in the material's row of the material table: the shader's declared properties, packed at the offsets the shader reads them from and padded to a whole 16 bytes. The row also carries a 64-byte header and a 4-byte handle per texture slot, padded to a whole 16 bytes. <code>renderer.materialTable().rowBytes</code> is the space the table reserves for all rows — its row capacity, free rows included — plus its directories. <code>renderer.drawDiagnostics()</code> names WHICH renderable is not drawing what its material says, and why. <code>material</code> is the key the renderer holds the material under; <code>asset</code> names the <code>.material</code> asset a <code>guid@checksum</code> key is a version of.</p> <pre><code class="language-lua">for _, m in renderer.materialCost() do print(m.asset or m.material, m.draws, m.binds, m.bindsElided) end </code></pre> <h2 id="modules-renderer-materialidentity">modules/renderer/materialIdentity</h2> <pre><code class="language-lua">materialIdentity(): MaterialIdentity </code></pre> <p>Which material each renderable draws with, as a number a shader can carry. A material is authored and bound by name, and no shader can read a string — so every renderable's per-instance record holds a material index instead. <code>slots</code> is the name → index table those indices are drawn from: an index is assigned the first time the renderer draws with that material and does not move while the renderer holds the material, so two renderables that differ only in material read different indices, and one renderable reads the same index frame after frame. A destroyed material's name leaves the table once nothing draws with it, and the next material takes its index, so the table holds a row per name the renderer holds a material under or something draws with. <code>renderables</code> is a row per renderable that owns a GPU slot — the entity it belongs to, that slot, and the index the record at it carries; <code>populations</code> is the same for an instanced draw, whose whole reserved run of slots carries the one material its registration named. That index is what a shader reads as <code>zero_instance(slot).material_index</code>, and the row a ray hit resolves through <code>zeroMaterial()</code>. A renderable draws with the material its entity references, so one whose entity names none carries index 0.</p> <pre><code class="language-lua">local id = renderer.materialIdentity() for _, r in id.renderables do print(r.entity, r.slot, r.index, r.material) end </code></pre> <h2 id="modules-renderer-materialindex">modules/renderer/materialIndex</h2> <pre><code class="language-lua">materialIndex(name: string): number? </code></pre> <p>The index standing for a material, or <code>nil</code> for a name the renderer holds no index for: one it has not drawn with yet, or a destroyed material nothing draws with any more. Pass it to a shader (or compare it against what a shader read out of <code>zero_instance(slot).material_index</code>) to tell which material a drawing instance carries.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — <code>string</code> Material name, as <code>renderer.material.create</code> filed it.</li> </ul> <pre><code class="language-lua">local red = renderer.materialIndex("brick_red") </code></pre> <h2 id="modules-renderer-materialtable">modules/renderer/materialTable</h2> <pre><code class="language-lua">materialTable(): MaterialTable </code></pre> <p>How this device resolves material textures, and what that storage costs. <code>backend</code> is <code>"bindless"</code> (a descriptor array indexed by each material's texture handles) or <code>"buckets"</code> (exact-size texture arrays, one per size, format and mip count, bound per material group). Bindless is taken on a Vulkan adapter that has texture binding arrays with non-uniform indexing and partially bound arrays, and room for descriptor arrays of at least 4096 textures; every other device — the browser build, Android, Metal, DirectX 12, and Vulkan without those — takes buckets.</p> <p><code>materials</code> is how many materials have a row, <code>rowBytes</code> the space the table reserves in the scene buffer (its row capacity, free rows included, plus the material and texture directories), <code>textures</code> how many distinct textures the rows name, <code>descriptorCapacity</code> the length of each bindless array (0 on buckets), and <code>groups</code> how many material groups the materials form. A group is the unit a pass binds a material's textures (group 1) and a shader's own storage buffers (group 2) by, and every material of a group draws under the same binds. On bindless every group binds the one texture group of the frame, so groups differ only by the set of storage buffers they bind. On buckets a group also names, per texture slot, the bucket its members' textures live in and the sampler those textures sample through: materials of one shader split into one group per distinct combination of (bucket and sampler per texture slot, storage buffers), and each group is its own draw.</p> <p><code>buckets</code> lists every texture array: its format, size, mip count, the layers textures occupy, its capacity, what it costs, the slack it holds beyond its live layers, whether it is frozen (a layer's view is held by something outside the material system — a UI image, a post-process input — or a copy out of a layer is taken this frame, such as a readback, so the array neither grows nor compacts), which shard of its key it is, and whether it is external (a one-layer bucket over a texture the GPU writes — a render target, a video frame, a compute output — which its producer owns). <code>bucketBytes</code> is what the shared buckets occupy together, and <code>bucketSlackBytes</code> the slack they hold together, which <code>renderer.setTextureBucketSlack</code> bounds; an external bucket counts in neither. <code>slackBudgetBytes</code> is the budget the buckets bound growth by. On bindless <code>buckets</code> is empty, both totals are 0, and <code>slackBudgetBytes</code> is the budget recorded, which bounds nothing.</p> <pre><code class="language-lua">local t = renderer.materialTable(); print(t.backend, t.groups, "groups over", t.materials, "materials") for _, b in renderer.materialTable().buckets do print(b.format, b.width, b.height, b.layers, "/", b.capacity) end </code></pre> <h2 id="modules-renderer-maxanisotropy">modules/renderer/maxAnisotropy</h2> <pre><code class="language-lua">maxAnisotropy(): number </code></pre> <p>The highest anisotropy this device honours: 16 on hardware that filters anisotropically, 1 on hardware that does not, where a higher request would be downgraded to trilinear regardless. Read it to report quality honestly — <code>renderer.setAnisotropy</code> clamps for you, so a request never needs guarding.</p> <pre><code class="language-lua">local best = renderer.maxAnisotropy() </code></pre> <h2 id="modules-renderer-maxviewportextent">modules/renderer/maxViewportExtent</h2> <pre><code class="language-lua">maxViewportExtent(): number </code></pre> <p>The largest number of pixels this device draws a surface at, per axis — the bound <code>renderer.setViewportSize</code> refuses past. <code>0</code> before the first frame has drawn.</p> <pre><code class="language-lua">local bound = renderer.maxViewportExtent() </code></pre> <h2 id="modules-renderer-memorypressure">modules/renderer/memoryPressure</h2> <pre><code class="language-lua">memoryPressure(): MemoryPressureReport </code></pre> <p>What memory pressure is doing to the engine. <code>level</code> is the level in force: <code>"normal"</code>, <code>"moderate"</code>, <code>"low"</code> or <code>"critical"</code>. A report puts its level in force, whatever stood before, for <code>holdSeconds</code> after the last report, and then the level returns to normal. Reports come from the system (on Android, <code>onTrimMemory</code>: RUNNING_MODERATE is moderate, RUNNING_LOW and UI_HIDDEN low, RUNNING_CRITICAL, BACKGROUND, MODERATE and COMPLETE critical; Android 14 and later report only UI_HIDDEN, when the app leaves the screen, and BACKGROUND) and from <code>renderer.signalMemoryPressure</code>. <code>signals</code> counts the reports taken; <code>lastLevel</code>, <code>lastSource</code> and <code>lastAgeSeconds</code> describe the newest (absent before the first).</p> <p>Each report releases at once what its level releases, and <code>released</code> says what the newest one freed: the <code>textures</code> taken to the level the frame samples and the <code>textureBytes</code> they gave up, and the render <code>targets</code> whose storage was freed. Moderate frees the mip levels the frame does not sample; low also frees render-target storage the last frame did not draw into; critical also halves the texture budget. While a level stands the residency plan keeps the textures there, and <code>textureBudgetBytes</code> is the budget the level leaves in force. <code>engine.onMemoryPressure(cb)</code> fires with each report once its release is done.</p> <pre><code class="language-lua">local mp = renderer.memoryPressure() print(mp.level, mp.released.textureBytes / 1048576, "MiB freed") </code></pre> <h2 id="bounds">modules/renderer/mesh.bounds</h2> <pre><code class="language-lua">mesh.bounds(mesh: string | { [string]: any } | AssetRef): Aabb? </code></pre> <p>The mesh's extent in its own space: the box its vertices span, or the one its author stated (<code>aabbMin</code> / <code>aabbMax</code>) for a mesh whose vertices live only on the GPU. It is what the mesh is culled against and what framing reads; place it in the world through whatever draws the mesh — an entity's transform, or <code>instanceBounds</code> for an instanced population.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local box = renderer.mesh.bounds(mesh) </code></pre> <h2 id="boundssource">modules/renderer/mesh.boundsSource</h2> <pre><code class="language-lua">mesh.boundsSource(mesh: string | { [string]: any } | AssetRef): string </code></pre> <p>Where this mesh's culling bounds come from. <code>"compute"</code> once a compute pass has written its vertices: the engine reduces those vertices to an AABB every frame, so the mesh is culled against the geometry the pass produced wherever it puts it. <code>"geometry"</code> otherwise: the AABB of the geometry the mesh was created with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">print(renderer.mesh.boundsSource(mesh)) </code></pre> <h2 id="buildclusters">modules/renderer/mesh.buildClusters</h2> <pre><code class="language-lua">mesh.buildClusters(mesh: string | { [string]: any } | AssetRef): string? </code></pre> <p>Build a cluster-LOD DAG (Nanite-style virtualized geometry) for the static CPU mesh held under <code>guid</code> and return its serialized <code>data.clusters</code> bytes. Returns nil when the mesh is degenerate, and on an engine whose <code>renderer.mesh.canBuildClusters</code> reports false. Pair with <code>renderer.mesh.uploadClusters</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — A mesh loaded into the CPU store (<code>renderer.mesh.loadCpu</code>) — the <code>MeshCpuHandle</code>, a <code>MeshHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local cb = renderer.mesh.buildClusters(cpu) </code></pre> <h2 id="canbuildclusters">modules/renderer/mesh.canBuildClusters</h2> <pre><code class="language-lua">mesh.canBuildClusters(): boolean </code></pre> <p>Whether this engine bakes cluster-LOD hierarchies. It reads the binding the running engine registered: every target the engine ships on carries the builder, so a mesh loaded in a browser bakes its own clusters the same way one loaded natively does, and an engine built without it reports false and answers nil from <code>renderer.mesh.buildClusters</code>.</p> <pre><code class="language-lua">if renderer.mesh.canBuildClusters() then ... end </code></pre> <h2 id="clusterbakebudget">modules/renderer/mesh.clusterBakeBudget</h2> <pre><code class="language-lua">mesh.clusterBakeBudget(ms: number?): number </code></pre> <p>The wall time one frame may spend advancing scheduled cluster bakes, in milliseconds — set first when <code>ms</code> is given. A slice always runs at least one unit of the build, so the budget bounds what a frame spends by choice and the largest single unit a mesh imposes sets the floor under it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ms</code> <code>number?</code> <em>(optional)</em> — New per-frame budget in milliseconds, capped at 1000. A value that is not a positive, finite number raises.</li> </ul> <pre><code class="language-lua">renderer.mesh.clusterBakeBudget(2) </code></pre> <h2 id="clusterbakes">modules/renderer/mesh.clusterBakes</h2> <pre><code class="language-lua">mesh.clusterBakes(): { [string]: any } </code></pre> <p>What the scheduled cluster bakes are costing. <code>budgetMs</code> is the slice a frame may spend, <code>pending</code> how many bakes are queued, <code>completed</code> how many have finished since the engine started, <code>dropped</code> how many left the queue because the geometry they were scheduled over stopped being readable or cannot be drawn as clusters, and <code>heldBytes</code> the source geometry the queue is holding across all of them — the vertex pool and index run the bake at the head is reading. <code>inFlight</code> is one row per queued bake — <code>{ guid, cpuMs, frames, slices, bytes, state, levels }</code>: the wall time spent advancing it, the frames it has been queued for, the slices it has been advanced by, the geometry it is holding, <code>"baking"</code> for the one being advanced against <code>"queued"</code> for the ones waiting their turn, and <code>levels</code> the depth the bake builds, <code>"dag"</code> or <code>"single"</code>.</p> <pre><code class="language-lua">print(renderer.mesh.clusterBakes().heldBytes) </code></pre> <h2 id="clustercomponents">modules/renderer/mesh.clusterComponents</h2> <pre><code class="language-lua">mesh.clusterComponents(clusterBytes: buffer | string): (ClusterComponents?, string?) </code></pre> <p>Split a cluster blob (from <code>renderer.mesh.buildClusters</code>) into its GPU-ready component byte pools — the cluster vertex pool, the geometry-addressing pool (every cluster's local→global vertex map, then every cluster's triangle bytes), and the per-cluster record array — plus their counts. A cluster's triangles address positions inside its own vertex map one byte at a time, and a record's <code>vertexOffset</code> indexes the geometry pool in <code>u32</code> elements while its <code>indexOffset</code> indexes it in bytes, so ONE binding resolves a corner. A pure decode (no GPU work): upload the pools into buffers a compute shader owns (<code>shaderRef:createBuffer</code> + <code>buf:writeBytes</code>) to drive a cluster draw from Luau.</p> <p><strong>Parameters</strong></p> <ul> <li><code>clusterBytes</code> <code>buffer | string</code> — Serialized cluster bytes (binary-safe).</li> </ul> <pre><code class="language-lua">local c = renderer.mesh.clusterComponents(cb) </code></pre> <h2 id="clusterstore">modules/renderer/mesh.clusterStore</h2> <pre><code class="language-lua">mesh.clusterStore(): { </code></pre> <p>What the geometry heap holds for cluster drawing. <code>heapCapacity</code> and <code>heapLive</code> are the heap's size and the bytes in use. The heap grows to place what does not fit, and gives memory back as meshes leave it: once what it holds has packed into a quarter of its size for 60 frames in a row, it shrinks to double that, never below the size it started at. A mesh destroyed and created again inside those frames finds the heap at the size it left it. <code>vertexBytes</code> and <code>lightmapBytes</code> are the vertex pools and lightmap-UV runs of the meshes drawn through clusters. <code>bakedBytes</code> is the cluster sets of meshes drawing a baked set; <code>unbakedBytes</code> is the sets of meshes still drawing <code>lod0</code> plus those meshes' vertex pools — geometry drawn at full detail while its bake is outstanding. <code>meshes</code> counts meshes drawing any set, <code>unbakedMeshes</code> those on <code>lod0</code>. <code>scriptSetBytes</code> is CPU memory beside the heap: the sets scripts installed with <code>renderer.mesh.uploadClusters</code>, held so each mesh draws its set again after it returns to the device.</p> <pre><code class="language-lua">local s = renderer.mesh.clusterStore(); print(s.unbakedBytes / 1048576, "MiB drawn at full detail while bakes are outstanding") </code></pre> <h2 id="clusters">modules/renderer/mesh.clusters</h2> <pre><code class="language-lua">mesh.clusters(mesh: string | { [string]: any } | AssetRef): { [string]: any }? </code></pre> <p>The cluster set a mesh draws. Every static mesh the geometry heap holds draws through clusters: <code>kind</code> is <code>"lod0"</code> while it draws the full-detail set built from its geometry as it uploaded, <code>"single"</code> for one baked level (<code>clusterLod = false</code>), and <code>"dag"</code> for the baked LOD hierarchy. <code>clusterCount</code>, <code>levelCount</code> and <code>triangleCount</code> describe that set, <code>nodes</code> how many node transforms its clusters lie in, and <code>bytes</code> its size in the heap. A mesh that is not drawn through clusters reads <code>resident = false</code> with <code>reason</code>: <code>"heapFull"</code> (the heap cannot grow to hold it), <code>"spansNodes"</code> (a triangle joins two independently moving parts) or <code>"deformable"</code> (a pass deforms its vertices). Nil for a mesh the renderer holds no GPU geometry for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh to read — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local c = renderer.mesh.clusters(gpu) ; if c and c.kind == "lod0" then print("still baking") end </code></pre> <h2 id="create">modules/renderer/mesh.create</h2> <pre><code class="language-lua">mesh.create(src: any, guid: string?): MeshHandle </code></pre> <p>Create (or fetch) a GPU mesh resource and return its <code>MeshHandle</code>. <code>src</code>: a <code>MeshCpuHandle</code> from <code>meshRef:load()</code> (CPU→GPU upload under the asset's guid, idempotent — returns the resident handle if already uploaded); raw geometry <code>{positions, indices, normals?, uvs?, colors?, uvs1?, unwrapUvs?, tangents?, skinning?, skins?}</code> (a new runtime mesh — <code>uvs1</code> is the lightmap UV set, <code>unwrapUvs</code> generates one, <code>skinning</code>/<code>skins</code> bind a skeleton); GPU compute buffers <code>{vertexBuffer, indexBuffer, vertexCount, indexCount, aabbMin?, aabbMax?, prevVertexBuffer?}</code> (size the vertex buffer at <code>vertexCount * engine.vertexStride</code> bytes, the engine's standard Vertex layout); or a <code>MeshHandle</code> (returned as-is). NEVER takes an AssetRef — load the CPU first.</p> <p><code>prevVertexBuffer</code> is a second buffer of the same size and layout holding those vertices as they stood on the previous frame. Naming it is what makes geometry a compute pass moves report a motion vector: the surface differences the two streams, so every consumer of screen-space velocity — motion blur, temporal reprojection — sees the movement. The engine fills it from the current vertices once per frame, ahead of that frame's compute dispatches, so a frame in which the pass does not run leaves the two streams equal and the geometry reports standing still.</p> <p><code>morphTargets</code> are the shapes the mesh can blend towards: a list of <code>{ name?, positions, normals? }</code> records, each holding one offset per vertex from the base geometry, in the mesh's own vertex order. An entity blends them with <code>ecs.MorphWeights</code>, weight <code>i</code> scaling target <code>i</code>. A <code>name</code> makes the shape addressable as itself — <code>renderer.mesh.morphTargets</code> reads the names back and <code>renderer.mesh.morphWeights</code> drives them by name.</p> <p>Raw geometry is read against the mesh type's conventions: <code>indices</code> count vertices from 0, and a triangle's FRONT face is the one whose vertices turn counter-clockwise as the viewer sees them — <code>cross(v1 - v0, v2 - v0)</code> points out of it. A material culls its back faces by default, so a triangle wound the other way draws nothing where it stands; reverse the index triple, or give the material <code>render = { cull = "none" }</code>, to draw that side. <code>normals</code> give the surface its outward direction and shade the face; the side that draws comes from the index order alone. <code>uvs</code> sample <code>(0,0)</code> at the image's top-left. Model space carries the world's basis: +X right, +Y up, -Z the direction <code>transform.forward</code> points. <code>guides { path = "types/mesh" }</code> has the whole table. A geometry src carrying <code>keepCpu = true</code> also keeps its geometry in the guid-keyed CPU store, so <code>renderer.mesh.getVertices</code> reads it and <code>renderer.mesh.setVertices</code> rewrites its positions in place — the per-frame deformation path, which sends positions alone where <code>renderer.mesh.update</code> re-sends the whole geometry. <code>renderer.mesh.unloadCpu(mesh)</code> releases that copy. Without it the geometry lives on the GPU alone and <code>renderer.mesh.readback(mesh)</code> is what brings it back.</p> <p>A mesh this call builds lives while the returned handle, or another handle over the same guid, is reachable from a script, or while something else holds it (an entity or instanced draw displaying it, its owner, a <code>renderer.hold</code>). Once nothing holds it and the garbage collector has taken its last handle, the engine releases it mid-session: its definition, its buffers and its <code>renderer.mesh.list</code> row go, and its guid names nothing. A script that displays a mesh again later keeps its handle (or holds it) for as long as it means to.</p> <p>A handle a script keeps names its mesh for the rest of the session: a module table or <code>_SCRATCH</code> holding it carries it across play/edit flips, scene loads and hot reloads of the components that use it, and a <code>Model</code> handed it after any of them draws the mesh. While nothing displays the mesh, the engine holds it off the device (<code>renderer.mesh.heldOffDevice</code>) and the next display puts it back. Only a destroy ends a mesh a handle still reaches (<code>renderer.destroy</code>, <code>renderer.mesh.destroy</code>): every handle a create returned over it then carries <code>released</code>, the verb that released it, and a point of use (<code>ecs.Mesh</code>, a <code>Model</code>'s <code>model</code> field) refuses such a handle by name until a mesh stands under its guid again, which the handle then names and holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>any</code> <em>(optional)</em> — A MeshCpuHandle, geometry, compute buffers, or a MeshHandle.</li> <li><code>guid</code> <code>string?</code> <em>(optional)</em> — Optional v4 guid for a NEW runtime mesh (minted Luau-side when absent). Ignored for the CPU-handle path (the asset's guid is used).</li> </ul> <pre><code class="language-lua">local gpu = renderer.mesh.create(meshRef:load()) local gpu = renderer.mesh.create({ positions = {...}, indices = {...} }) -- a runtime mesh whose positions are rewritten in place each frame local m = renderer.mesh.create({ positions = P, indices = I, keepCpu = true }) renderer.mesh.setVertices(m, P) -- a runtime mesh carrying a lightmap UV set (unwrapped at creation) local gpu = renderer.mesh.create({ positions = {...}, indices = {...}, unwrapUvs = true }) -- a mesh with one shape to blend towards, driven by ecs.MorphWeights local gpu = renderer.mesh.create({ positions = P, indices = I, morphTargets = { { positions = D } } }) </code></pre> <h2 id="decode">modules/renderer/mesh.decode</h2> <pre><code class="language-lua">mesh.decode(zmsh: buffer | string): (MeshGeometry?, string?) </code></pre> <p>Decode engine-native <code>ZMSH</code> bytes back into a <code>MeshGeometry</code>. Inverse of <code>renderer.mesh.encode</code>; each optional stream is present only when the blob carries it. Takes the bytes themselves — the geometry of a mesh the engine is holding comes from <code>renderer.mesh.geometry(mesh)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>zmsh</code> <code>buffer | string</code> — Engine-native ZMSH bytes (binary-safe).</li> </ul> <pre><code class="language-lua">local geom = renderer.mesh.decode(meshRef:getBytes()) </code></pre> <h2 id="destroy">modules/renderer/mesh.destroy</h2> <pre><code class="language-lua">mesh.destroy(mesh: string | { [string]: any } | AssetRef): boolean </code></pre> <p>Release the GPU mesh <code>mesh</code> names, the release that pairs with <code>renderer.mesh.create</code>. Takes every form that names a mesh — the <code>MeshHandle</code> <code>create</code> returned, the guid <code>renderer.mesh.list</code> hands out, a <code>MeshCpuHandle</code> or a mesh <code>AssetRef</code> — and routes through <code>renderer.destroy</code>, the verb that releases any renderer resource by its kind. The CPU copy, if one was loaded, is freed separately by the CPU handle's <code>:unload()</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh to release — a <code>MeshHandle</code>, a guid, a <code>MeshCpuHandle</code> or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local m = renderer.mesh.create(cpu); renderer.mesh.destroy(m) renderer.mesh.destroy(renderer.mesh.list()[1].guid) </code></pre> <h2 id="drawinstanced">modules/renderer/mesh.drawInstanced</h2> <pre><code class="language-lua">mesh.drawInstanced(mesh: string | { [string]: any } | AssetRef, opts: any): InstancedDraw </code></pre> <p>Draw one mesh <code>instanceCount</code> times in a single call, each copy placed by a world matrix read from a GPU buffer. The population is a renderable in its own right — it goes through the mesh's ordinary pipeline and the material's ordinary bind groups, so it appears in the deferred pass, the forward passes and the shadow maps exactly as an entity-backed draw of that mesh does.</p> <p>The buffer holds <code>instanceCount</code> <strong>column-major</strong> 4x4 matrices, 64 bytes each, tightly packed — the layout a vertex shader reads as <code>array<mat4x4<f32>></code>, which puts each matrix's translation in its LAST four floats (Lua indices 13/14/15 for x/y/z). Packing row-major transposes every instance.</p> <p>The matrices are COPIED into the engine's transform slots, which is what buys that full-pass parity. Rewrite the buffer between frames and the instances move, with no re-registration and no re-upload. The copy is made on the frames that follow a write of the buffer (a <code>:write</code>, a compute pass whose shader binds it <code>read_write</code>, and every frame a material whose shader binds it <code>read_write</code> is live), so a population whose buffer nothing writes costs no copy per frame however many instances it holds; <code>renderer.uploadStats()</code> counts the copies a frame made.</p> <p><code>material</code> is what the population draws with, and it is required: a <code>MaterialHandle</code> (<code>matRef:handle()</code>), an <code>AssetRef</code>, or a registry key.</p> <p><code>instanceDataBuffer</code> names a second buffer, holding 64 bytes per instance — four <code>vec4</code> lanes, tightly packed, in instance order. Those lanes arrive in the fragment stage as <code>zero_object_data(in.instance_id, lane)</code>, the same read a per-entity <code>__instancedata</code> block answers, so the members of one population can differ in whatever their material's shader agrees the lanes carry. Copied like the transforms, on the frames that follow a write of the buffer, from a buffer a compute pass writes: the values never touch the CPU. Omit it and the lanes read zero. <code>@builtin::materials.instance_color</code> (and <code>instance_color_unlit</code>) colour each copy from lane 2 of its block, <code>(r, g, b, coverage)</code>, given as its <code>AssetRef</code>: <code>material = asset.resolve("@builtin::materials.instance_color", "material")</code>.</p> <p><code>reserveCount</code> sizes the reservation above <code>instanceCount</code> so <code>renderer.mesh.setInstanceCount</code> can raise the drawn count later without re-registering; both buffers must back the reservation, not just the count.</p> <p><code>mobility</code> states whether the copies stand still — <code>"static"</code>, or <code>"movable"</code> when it is left out. It is what a scene gather collecting geometry for precomputed lighting admits a population on, the same declaration <code>Model.mobility</code> makes for an entity: the transforms live in a buffer anything may rewrite between frames, so a population that says nothing is taken as one that moves.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh the population draws — a <code>MeshHandle</code>, the guid <code>renderer.mesh.list</code> hands out, a <code>MeshCpuHandle</code> or a mesh <code>AssetRef</code>. A registration holds the mesh on the device for as long as it lives, and takes a mesh that is currently held off the device — one nothing displays — back onto it.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ transformBuffer, instanceCount, material, instanceDataBuffer?, reserveCount?, renderLayer?, castsShadows?, receivesShadows?, mobility?, parent? }</code>. <code>castsShadows</code> and <code>receivesShadows</code>, both true when left out, state whether the copies cast shadows and whether shadows darken them. <code>parent</code> stands the copies in an entity's space, as <code>setInstanceParent</code> does.</li> </ul> <pre><code class="language-lua">local m = renderer.mesh.create({ positions = ..., indices = ... }) local buf = substrate.createBuffer({ name = "crowd.xf", type = "mat4", len = 64, kind = "gpu", }) -- Column-major: translation lives at indices 13/14/15. local xf = {} for i = 0, 63 do local m4 = { 1,0,0,0, 0,1,0,0, 0,0,1,0, i * 2, 0, 0, 1 } for _, v in ipairs(m4) do xf[#xf + 1] = v end end buf:write(xf) local rock = asset.resolve("rock", "material"):handle() local draw = renderer.mesh.drawInstanced(m, { transformBuffer = "crowd.xf", instanceCount = 64, material = rock }) </code></pre> <h2 id="dropclusters">modules/renderer/mesh.dropClusters</h2> <pre><code class="language-lua">mesh.dropClusters(mesh: string | { [string]: any } | AssetRef): boolean </code></pre> <p>Return a mesh to its <code>lod0</code> set and cancel a bake still in flight for it. The inverse of <code>renderer.mesh.uploadClusters</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh to return — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">renderer.mesh.dropClusters(gpu) </code></pre> <h2 id="dropinstanced">modules/renderer/mesh.dropInstanced</h2> <pre><code class="language-lua">mesh.dropInstanced(draw: InstancedDraw): boolean </code></pre> <p>Release an instanced-draw registration and the transform slots it reserved. The mesh and the transform buffer outlive it — destroy those through <code>renderer.destroy</code> and the buffer handle's <code>:destroy()</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to release.</li> </ul> <pre><code class="language-lua">renderer.mesh.dropInstanced(draw) </code></pre> <h2 id="encode">modules/renderer/mesh.encode</h2> <pre><code class="language-lua">mesh.encode(geom: MeshGeometry): (string?, string?) </code></pre> <p>Encode raw geometry into engine-native <code>ZMSH</code> bytes (the on-disk mesh payload). The CPU codec behind the mesh assetType's <code>onCreate</code>. Every stream the format carries — including tangents, per-vertex skinning, and the skeleton — round-trips back through <code>renderer.mesh.decode</code>. This pair moves DATA the caller is holding; the geometry of a mesh the ENGINE is holding comes from <code>renderer.mesh.geometry(mesh)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>geom</code> <code>MeshGeometry</code> — <code>MeshGeometry</code> — flat per-vertex float / u32 arrays plus optional <code>skinning</code> and <code>skins</code>.</li> </ul> <pre><code class="language-lua">local bytes = renderer.mesh.encode({ positions = {...}, indices = {...} }) local bytes = renderer.mesh.encode(renderer.mesh.geometry(meshHandle)) </code></pre> <h2 id="encodecpu">modules/renderer/mesh.encodeCpu</h2> <pre><code class="language-lua">mesh.encodeCpu(mesh: string | { [string]: any } | AssetRef): string </code></pre> <p>Encode a mesh's resident CPU copy into <code>ZMSH</code> bytes. Reads the ONE guid-keyed CPU store — <code>meshRef:load()</code> populates it for assets, and <code>renderer.mesh.readback(mesh)</code> populates it for a runtime mesh. Errors loudly when the mesh has no resident CPU copy.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local bytes = renderer.mesh.encodeCpu(handle) </code></pre> <h2 id="geometry">modules/renderer/mesh.geometry</h2> <pre><code class="language-lua">mesh.geometry(mesh: string | { [string]: any } | AssetRef): MeshGeometry </code></pre> <p>The complete geometry of a mesh the engine is holding, as a <code>MeshGeometry</code> — the same shape <code>renderer.mesh.create</code> and <code>renderer.mesh.encode</code> take, carrying every stream the mesh has (<code>positions</code>, <code>indices</code>, and whichever of <code>normals</code>, <code>uvs</code>, <code>colors</code>, <code>uvs1</code>, <code>tangents</code>, <code>skinning</code>, <code>skins</code> it was built with). The read that pairs with <code>create</code>: hand it the <code>MeshHandle</code> <code>create</code> returned and get the vertex data back. Reads the resident CPU copy when there is one; for a runtime mesh that lives only on the GPU it reads the geometry back off the GPU first (yielding a frame or two) and leaves CPU residency as it found it. An optional stream is present only when the mesh carries one, so <code>uvs1 == nil</code> is the answer to whether it has a second UV set. The drawable mesh the renderer holds carries the tangent basis its positions, uvs and normals determine — supplied by the caller, or derived at the ingest that made it drawable — and that is what the GPU read gives back. The CPU store answers with the streams the bytes it decoded hold, so a <code>.mesh</code> written without a tangent stream reads back <code>tangents == nil</code> for as long as a CPU copy of it is resident.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local geom = renderer.mesh.geometry(renderer.mesh.create({ positions = P, indices = I })) local tangents = renderer.mesh.geometry(handle).tangents </code></pre> <h2 id="getvertices">modules/renderer/mesh.getVertices</h2> <pre><code class="language-lua">mesh.getVertices(mesh: string | { [string]: any } | AssetRef): { any } </code></pre> <p>Read the vertices of a mesh's resident CPU copy — one entry per vertex, <code>{ pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }</code>. Reads the resident CPU store directly (no re-decode). Errors when the mesh has no resident CPU copy — <code>renderer.mesh.geometry(mesh)</code> is the read that works wherever the mesh lives, and returns the tangent, colour and skinning streams too.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <h2 id="heldoffdevice">modules/renderer/mesh.heldOffDevice</h2> <pre><code class="language-lua">mesh.heldOffDevice(mesh: string | { [string]: any } | AssetRef): boolean </code></pre> <p>True while the engine holds this mesh off the device: nothing displays it, so its GPU buffers were given back and the engine keeps what rebuilds them. The next <code>Model</code> or instanced draw that names the mesh puts it back on the device, where <code>renderer.mesh.isResident</code> reads true again from the frame that bind lands in. A mesh is one of three things: resident, held off the device, or gone; a mesh neither this nor <code>isResident</code> answers true for names nothing the engine can draw.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh: a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local named = renderer.mesh.isResident(h) or renderer.mesh.heldOffDevice(h) </code></pre> <h2 id="instancebounds">modules/renderer/mesh.instanceBounds</h2> <pre><code class="language-lua">mesh.instanceBounds(draw: InstancedDraw, opts: { keep: number? }?): (Aabb?, string?) </code></pre> <p>The world box a registration's drawn copies occupy: every copy of the mesh's extent (<code>renderer.mesh.bounds</code>) where the engine composed it for the last frame drawn, carried through the entity the copies stand in when <code>setInstanceParent</code> named one. It reads the copies back from the GPU, so it waits a few drawn frames. A copy scaled to nothing draws nothing and is left out. A mesh whose extent nobody knows answers with the box around the copies' origins, and says so.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to measure.</li> <li><code>opts</code> <code>{ keep: number? }?</code> <em>(optional)</em> — <code>{ keep? }</code> — the share of the copies the box holds along each axis (default 1, all of them). Below 1, the copies reaching furthest out on each side are left out: <code>keep = 0.9</code> is the box around where the body of a spray stands, without the few copies thrown far from it.</li> </ul> <pre><code class="language-lua">local box, note = renderer.mesh.instanceBounds(draw) local body = renderer.mesh.instanceBounds(draw, { keep = 0.9 }) </code></pre> <h2 id="instanceinfo">modules/renderer/mesh.instanceInfo</h2> <pre><code class="language-lua">mesh.instanceInfo(draw: InstancedDraw): InstancedDrawInfo? </code></pre> <p>What a live instanced-draw registration is drawing: which mesh, which transform buffer, which per-instance data buffer if it named one, how many instances, and how many slots it reserved. Returns nil once the registration has been dropped.</p> <p><code>status</code> is what the renderer did with it. The fields above it are the request, made a stage before the renderer sees it; <code>status</code> is the answer: <code>"drawing"</code> for a registration the renderer is drawing, <code>"hidden"</code> for one it holds but does not draw while its parent entity, or an ancestor of it, is deactivated or gone, <code>"refused"</code> for one it turned away — <code>error</code> carries its reason — and <code>"pending"</code> for the frame between the call and the renderer answering. So a registration whose copies are not being drawn says so here.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to report on.</li> </ul> <pre><code class="language-lua">print(renderer.mesh.instanceInfo(draw).instanceCount) local info = renderer.mesh.instanceInfo(draw) if info.status == "refused" then error(info.error) end </code></pre> <h2 id="instancetransforms">modules/renderer/mesh.instanceTransforms</h2> <pre><code class="language-lua">mesh.instanceTransforms(draw: InstancedDraw): any </code></pre> <p>Read back the world matrices a registration's drawn copies are placed by: <code>instanceCount</code> matrices of 16 floats, column-major and tightly packed, in the layout the transform buffer holds them. The read is of the buffer as it stands when it runs, so a population a compute pass rewrites every frame answers with the placement of the frame the read lands in.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> whose copies to locate.</li> </ul> <pre><code class="language-lua">local pending = renderer.mesh.instanceTransforms(draw) while not pending:ready() do task.wait() end local floats = pending:result() </code></pre> <h2 id="instanceworlds">modules/renderer/mesh.instanceWorlds</h2> <pre><code class="language-lua">mesh.instanceWorlds(draw: InstancedDraw): any </code></pre> <p>Read back where a registration's drawn copies stand in the world, as the engine composed them for the last frame drawn: each copy's matrix from the transform buffer, carried through the world matrix of the entity the copies stand in when they have one (<code>setInstanceParent</code>). <code>instanceCount</code> column-major 4x4 matrices, 16 floats each. Async — poll the handle it returns the same way as <code>instanceTransforms</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to read.</li> </ul> <pre><code class="language-lua">local pending = renderer.mesh.instanceWorlds(draw) while not pending:ready() do task.wait() end local x = pending:result()[13] </code></pre> <h2 id="iscpuresident">modules/renderer/mesh.isCpuResident</h2> <pre><code class="language-lua">mesh.isCpuResident(mesh: string | { [string]: any } | AssetRef): boolean </code></pre> <p>True if this mesh has a resident CPU copy in the guid-keyed CPU store.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">if renderer.mesh.isCpuResident(handle) then ... end </code></pre> <h2 id="isresident">modules/renderer/mesh.isResident</h2> <pre><code class="language-lua">mesh.isResident(mesh: string | { [string]: any } | AssetRef): boolean </code></pre> <p>True if a GPU mesh is resident under this mesh's guid — the device holds its buffers, or the upload queued this frame is still going to hand them over. This is the store the draw paths are gated on, so a mesh this reports resident is one <code>renderer.mesh.drawInstanced</code> and a <code>Model</code> can draw — from the frame after the upload commits: a render asked for in the same tick the upload was queued does not carry the mesh yet. <code>renderer.mesh.onDevice</code> is the answer that waits for that commit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">print(renderer.mesh.isResident(handle)) </code></pre> <h2 id="list">modules/renderer/mesh.list</h2> <pre><code class="language-lua">mesh.list(): { any } </code></pre> <p>Every mesh currently registered, ordered by guid — the ones a script created and the ones that reached the device through an asset alike. Answers "which mesh is this?" when all that is known is a size: each entry carries the guid, the vertex/index counts it was created with, where it came from (<code>origin</code> is <code>"asset"</code> for a mesh the asset path uploaded), whether the GPU still holds it, and where its culling bounds come from (<code>boundsFrom</code> is <code>"compute"</code> for a mesh a compute pass writes). A resident entry also carries the bytes its buffers cost. <code>bytes</code> is the mesh's whole VRAM footprint and is the sum of the THREE buffer columns beside it — <code>vertexBytes + vertexStorageBytes + indexBytes</code>, where the storage column is the same vertices bound as a storage buffer for the passes that read them that way. Summing only the vertex and index columns understates a mesh by its vertex size. The <code>bytes</code> column is what sums to the <code>meshes</code> category of <code>renderer.gpuMemory()</code>. <code>renderer.references("mesh", guid)</code> says what is still holding a row, and <code>renderer.collect()</code> releases the rows nothing holds.</p> <pre><code class="language-lua">for _, m in ipairs(renderer.mesh.list()) do print(m.guid, m.bytes) end </code></pre> <h2 id="listinstanced">modules/renderer/mesh.listInstanced</h2> <pre><code class="language-lua">mesh.listInstanced(): { InstancedDrawInfo } </code></pre> <p>Every instanced-draw registration this engine is drawing, in registration order. Each record is what <code>instanceInfo</code> answers with, and carries a <code>draw</code> handle of its own — so a population whose handle its caller no longer holds is reached here and released, resized or read like any other.</p> <pre><code class="language-lua">for _, pop in ipairs(renderer.mesh.listInstanced()) do renderer.mesh.dropInstanced(pop.draw) end </code></pre> <h2 id="loadcpu">modules/renderer/mesh.loadCpu</h2> <pre><code class="language-lua">mesh.loadCpu(ref: string | AssetRef): MeshCpuHandle </code></pre> <p>Load a <code>.mesh</code> asset's geometry into the ONE guid-keyed CPU store (the Disk→CPU step) and return a CPU handle. The handle holds NO geometry — only the guid plus counts and the per-handle read/encode/unload ops (which read the Rust-side store). Called by <code>meshRef:load()</code>. DEFAULT lifecycle: upload to the GPU then <code>handle:unload()</code>; the store is populated only by this call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>string | AssetRef</code> — A mesh <code>AssetRef</code> (carries <code>.guid</code> and reads its primary via getBytes), or any string <code>asset.ref</code> resolves to one — the guid <code>encodeCpu</code> takes, an identity, a name or a source path.</li> </ul> <pre><code class="language-lua">local cpu = meshRef:load(); local gpu = renderer.mesh.create(cpu); cpu:unload() local cpu = renderer.mesh.loadCpu(gpuMesh.guid) </code></pre> <h2 id="morphtargets">modules/renderer/mesh.morphTargets</h2> <pre><code class="language-lua">mesh.morphTargets(mesh: string | { [string]: any } | AssetRef): { string } </code></pre> <p>The names of the shapes this mesh blends towards, in the order an entity's <code>ecs.MorphWeights</code> addresses them — weight <code>i</code> drives the target named at <code>i</code>. An imported model carries the names its source file gave its blend shapes, so content drives a face by the shape it means rather than by the ordinal that shape happened to import at (which moves when the model is re-exported). A target the source never named reads as an empty string.</p> <p>Empty for a mesh with no morph targets. Errors when the mesh is neither GPU- nor CPU-resident — materialise it first (<code>meshRef:handle()</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">for i, name in renderer.mesh.morphTargets(meshRef) do print(i, name) end </code></pre> <h2 id="morphweights">modules/renderer/mesh.morphWeights</h2> <pre><code class="language-lua">mesh.morphWeights( </code></pre> <p>Turn weights named by shape into the ordered weight array <code>ecs.MorphWeights</code> takes — the drive-a-face-by-name call. Every target the mesh carries gets a slot; the ones <code>weights</code> names take their value and the rest are 0, so the returned array always describes the whole mesh and a shape left out is a shape at rest.</p> <p>A name the mesh does not carry is an error listing the names it does: a mistyped viseme that silently moved nothing would be indistinguishable from a rig that never had it.</p> <pre><code class="language-lua">local w = renderer.mesh.morphWeights(meshRef, { Eyes_Blink = 1, Mouth_Smile = 0.4 }) ecs.set(face, ecs.MorphWeights { weights = w }) </code></pre> <h2 id="ondevice">modules/renderer/mesh.onDevice</h2> <pre><code class="language-lua">mesh.onDevice(mesh: string | { [string]: any } | AssetRef): boolean </code></pre> <p>True once the device holds this mesh's buffers — the upload queued for it has committed — so the next render asked for carries the mesh. <code>renderer.mesh.isResident</code> answers true one frame earlier, while the upload is still queued; a reader about to render and read the picture back waits on this one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">while not renderer.mesh.onDevice(guid) do task.wait() end </code></pre> <h2 id="readback">modules/renderer/mesh.readback</h2> <pre><code class="language-lua">mesh.readback(mesh: string | { [string]: any } | AssetRef, into: string?): MeshCpuHandle </code></pre> <p>Read a runtime GPU mesh's geometry back to CPU and return a <code>MeshCpuHandle</code> for it — the GPU→CPU half of the runtime-mesh freeze path. A mesh made with <code>renderer.mesh.create</code> keeps no CPU copy, so persisting it (<code>:encode()</code> → <code>asset.create("mesh", …)</code>) reads it back here first. Yields until the readback completes (a frame or two). After it returns the geometry is resident in the guid-keyed CPU store: <code>:getTriangles</code>, <code>:getVertices</code>, <code>:getBounds</code>, <code>:geometry</code>, <code>:encode</code>, <code>:unload</code> all work. Errors if the mesh never becomes resident in the vertex pool.</p> <p>The CPU store is keyed by guid and shared by everything that reads the mesh, and whatever owns the mesh can retire the copy under its own guid: <code>renderer.mesh.update</code> from compute buffers unloads it, which an ocean surface does every frame it steps. <code>into</code> lands the copy under a key of the caller's own instead: the handle answers for that key, and it stays resident until the caller unloads it, whatever happens to the mesh.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — the <code>MeshHandle</code> <code>renderer.mesh.create</code> returned, a guid, or a mesh <code>AssetRef</code>.</li> <li><code>into</code> <code>string?</code> <em>(optional)</em> — The CPU-store key the geometry lands under. Defaults to the mesh's own guid.</li> </ul> <pre><code class="language-lua">local cpu = renderer.mesh.readback(handle); local zmsh = cpu:encode(); cpu:unload() local snap = renderer.mesh.readback(handle, "bake:" .. handle.guid); local tris = snap:getTriangles(); snap:unload() </code></pre> <h2 id="readbackposed">modules/renderer/mesh.readbackPosed</h2> <pre><code class="language-lua">mesh.readbackPosed(requests: { { entity: string, mesh: any } }): { [string]: MeshCpuHandle } </code></pre> <p>Read the POSED geometry of skinned entities back to CPU: for each request, the vertices the skinning pass wrote for that entity this frame, joined by the indices of the mesh it is posed from. A skinned surface's world-space triangles are produced on the GPU from the entity's joint matrices, so the mesh asset holds the bind pose and only this reads where the surface actually is. The posed vertices are in model space, so the entity's own world transform still places them — the same transform the raster draw uses.</p> <p>Takes a LIST and answers a map, because the readbacks are queued together and polled together: a scene's worth of characters costs the frames of one readback rather than one entity's after another. Each posed mesh lands in the CPU store under a guid of its own, derived from the entity, so <code>compute.buildBvh</code>, <code>meshcpu.*</code> and every other guid-keyed reader takes it like any other mesh. Call <code>handle:unload()</code> when done with it.</p> <p>An entity the map omits holds no live pose — nothing skinned it this frame, which is also what makes its draws read the source mesh, so its bind-pose geometry is what stands for it.</p> <p>The vertices read are ones the skinning pass wrote for the pose the entity is bound to. An entity bound to a pose that pass has not written yet — the frame it was posed, before the pass reached the queue — is asked again on the next frame rather than read from a slice still holding what stood there before.</p> <p><strong>Parameters</strong></p> <ul> <li><code>requests</code> <code>{ { entity: string, mesh: any } }</code> — <code>{ { entity = <id>, mesh = <mesh> } }</code> — the entity to read, and the mesh it is posed from (a guid, <code>MeshHandle</code> or mesh <code>AssetRef</code>).</li> </ul> <pre><code class="language-lua">local posed = renderer.mesh.readbackPosed({ { entity = id, mesh = ecs.get(id, ecs.Mesh).mesh } }) local tris = posed[id]:getTriangles() </code></pre> <h2 id="scheduleclusters">modules/renderer/mesh.scheduleClusters</h2> <pre><code class="language-lua">mesh.scheduleClusters(mesh: string | { [string]: any } | AssetRef, opts: { levels: string? }?): boolean </code></pre> <p>Queue a cluster bake over the mesh's geometry, off the frame loop. The mesh keeps drawing its current cluster set while the bake runs, and the set the bake produces replaces it when it lands. <code>opts.levels</code> is <code>"dag"</code> for the LOD hierarchy or <code>"single"</code> for one level of clusters at full detail; when omitted, a mesh <code>AssetRef</code> bakes the depth its <code>clusterLod</code> setting asks for and any other argument bakes <code>"dag"</code>. The CPU mesh may be unloaded on the very next line. Geometry with a triangle joining two independently moving parts leaves the queue unbaked with a warning; a mesh <code>renderer.mesh.clusters</code> reports as <code>"deformable"</code> or <code>"heapFull"</code> leaves it unbaked without one.</p> <p>One bake is advanced per frame — the one at the head of the queue — and the geometry is read once a bake gets there and the mesh's GPU upload has landed, from the definition the engine holds for the mesh. The queue therefore holds one mesh's geometry rather than one per mesh, whatever its depth, and the set installs only on the geometry it was read from: a mesh whose geometry is replaced before its set lands keeps the set of its new geometry. <code>renderer.mesh.clusterBakes().heldBytes</code> reports what the queue is holding, and its <code>inFlight</code> rows report which bakes it is holding for. This is what the <code>.mesh</code> assetType materialisation path uses; reach for <code>renderer.mesh.buildClusters</code> when you want the bytes in hand instead. Scheduling the same mesh again replaces the bake already in flight for it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh to bake — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>, loaded into the CPU store (<code>renderer.mesh.loadCpu</code>).</li> <li><code>opts</code> <code>{ levels: string? }?</code> <em>(optional)</em> — <code>{ levels: "dag" | "single" }?</code></li> </ul> <pre><code class="language-lua">renderer.mesh.scheduleClusters(cpu, { levels = "single" }) ; cpu:unload() </code></pre> <h2 id="setinstancecount">modules/renderer/mesh.setInstanceCount</h2> <pre><code class="language-lua">mesh.setInstanceCount(draw: InstancedDraw, count: number): InstancedDraw </code></pre> <p>Change how many of a registration's instances draw. Constant time — the reservation, the transform buffer and the pipeline all stay put, so this is the verb for a population whose size changes per frame. The new count must fit the reservation <code>drawInstanced</code> was given.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to reconfigure.</li> <li><code>count</code> <code>number</code> — Instances to draw, at least 1 and within the reservation.</li> </ul> <pre><code class="language-lua">renderer.mesh.setInstanceCount(draw, visibleCount) </code></pre> <h2 id="setinstanceparent">modules/renderer/mesh.setInstanceParent</h2> <pre><code class="language-lua">mesh.setInstanceParent(draw: InstancedDraw, parent: any?): InstancedDraw </code></pre> <p>Stand a registration's copies in an entity's space: every copy's matrix is taken relative to the entity, so the population moves, turns and scales with it from the next frame on, with its transform buffer untouched. <code>nil</code> puts the copies back in world space. It is the verb for a population that belongs to something — a generator's output, an asset stood on an entity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to reconfigure.</li> <li><code>parent</code> <code>any?</code> <em>(optional)</em> — The entity the copies stand in (a proxy or its id), or nil for world space.</li> </ul> <pre><code class="language-lua">renderer.mesh.setInstanceParent(draw, entity.find("Forest")) </code></pre> <h2 id="setinstancerenderlayer">modules/renderer/mesh.setInstanceRenderLayer</h2> <pre><code class="language-lua">mesh.setInstanceRenderLayer(draw: InstancedDraw, renderLayer: number): InstancedDraw </code></pre> <p>Change which render layers a registration's copies belong to. Constant time — the reservation, the transform buffer and the pipeline all stay put, and the next frame drawn tests the copies against the new membership. It is the verb for a population that follows something whose membership moves: a camera or a capture including the layer draws the copies, one excluding it does not.</p> <p><strong>Parameters</strong></p> <ul> <li><code>draw</code> <code>InstancedDraw</code> — The <code>InstancedDraw</code> to reconfigure.</li> <li><code>renderLayer</code> <code>number</code> — The membership bitmask, the same value <code>drawInstanced</code> takes as <code>renderLayer</code>. At least one bit must be set.</li> </ul> <pre><code class="language-lua">renderer.mesh.setInstanceRenderLayer(draw, mask) </code></pre> <h2 id="setvertices">modules/renderer/mesh.setVertices</h2> <pre><code class="language-lua">mesh.setVertices(mesh: string | { [string]: any } | AssetRef, positions: { number }) </code></pre> <p>Replace a mesh's resident CPU vertex positions (flat <code>{ x,y,z, ... }</code>) IN PLACE — indices, normals/uvs, and skinning are preserved, the AABB recomputes, and the GPU re-fetches the new geometry so it shows on screen. The positions alone travel, so this is the per-frame deformation path where <code>renderer.mesh.update</code> re-sends the whole geometry. The mesh must be CPU-resident: <code>renderer.mesh.create({ ..., keepCpu = true })</code> keeps a copy from the start, <code>renderer.mesh.readback(mesh)</code> recovers one from the GPU, and <code>meshRef:load()</code> loads one for a <code>.mesh</code> asset. Errors with the reason otherwise, or when the vertex count doesn't match.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> <li><code>positions</code> <code>{ number }</code> — Flat <code>{ x,y,z, ... }</code> — one xyz per vertex; count must match the mesh.</li> </ul> <pre><code class="language-lua">local m = renderer.mesh.create({ positions = P, indices = I, keepCpu = true }) renderer.mesh.setVertices(m, P) -- P mutated in place each frame </code></pre> <h2 id="unloadcpu">modules/renderer/mesh.unloadCpu</h2> <pre><code class="language-lua">mesh.unloadCpu(mesh: string | { [string]: any } | AssetRef) </code></pre> <p>Drop a mesh's resident CPU copy from the guid-keyed CPU store. The explicit release for a runtime geometry mesh's recoverable definition.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> </ul> <h2 id="update">modules/renderer/mesh.update</h2> <pre><code class="language-lua">mesh.update(mesh: string | { [string]: any } | AssetRef, src: any): MeshHandle </code></pre> <p>Overwrite the GPU resource <code>mesh</code> names IN PLACE, under the same guid, from new geometry or compute buffers. Never writes a <code>.mesh</code> file — the play-mode mutate path. A Model bound to the guid reflects the change with no re-bind. Takes every form that names a mesh — the <code>MeshHandle</code> <code>create</code> returned, the guid <code>renderer.mesh.list</code> hands out, a <code>MeshCpuHandle</code> or a mesh <code>AssetRef</code>. Returns a handle carrying the bounds the new geometry has: the handle it was given, refreshed, and a handle over the guid otherwise.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh to update — a <code>MeshHandle</code>, a guid, a <code>MeshCpuHandle</code> or a mesh <code>AssetRef</code>.</li> <li><code>src</code> <code>any</code> <em>(optional)</em> — New geometry <code>{positions, indices, ...}</code> or compute buffers <code>{vertexBuffer, indexBuffer, vertexCount, indexCount, prevVertexBuffer?}</code>.</li> </ul> <h2 id="uploadclusters">modules/renderer/mesh.uploadClusters</h2> <pre><code class="language-lua">mesh.uploadClusters(mesh: string | { [string]: any } | AssetRef, clusters: string): boolean </code></pre> <p>Replace the cluster set a mesh draws with a serialized set (bytes from <code>renderer.mesh.buildClusters</code>), built from the geometry the mesh holds as the call is made. A set whose vertex count, vertex maps or nodes do not describe the mesh, or one whose mesh's geometry is replaced before it lands, is refused with a warning, and the mesh keeps its current set and the set it was asked for. Once the set is installed, a bake still in flight for the mesh is cancelled, and the engine keeps the bytes on the CPU (<code>renderer.mesh.clusterStore().scriptSetBytes</code>) so the mesh draws this set again when it returns to the device after nothing displayed it, or after the device is rebuilt. The bytes go when the mesh's geometry is redefined, when <code>renderer.mesh.dropClusters</code> returns it to <code>lod0</code>, and when the mesh is destroyed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string | { [string]: any } | AssetRef</code> — The mesh the clusters belong to — a <code>MeshHandle</code>, a <code>MeshCpuHandle</code>, a guid, or a mesh <code>AssetRef</code>.</li> <li><code>clusters</code> <code>string</code> — Serialized cluster bytes (binary-safe).</li> </ul> <pre><code class="language-lua">renderer.mesh.uploadClusters(gpu, cb) </code></pre> <h2 id="modules-renderer-minscreensize">modules/renderer/minScreenSize</h2> <pre><code class="language-lua">minScreenSize(): number </code></pre> <p>The on-screen radius, in pixels, an object must reach to be drawn. <code>0</code> while the cutoff is off.</p> <pre><code class="language-lua">local px = renderer.minScreenSize() </code></pre> <h2 id="modules-renderer-morphstats">modules/renderer/morphStats</h2> <pre><code class="language-lua">morphStats(): { </code></pre> <p>The morph state the last frame drew with. A mesh carries the shapes it can blend towards and an entity carries how strongly each is blended (<code>ecs.MorphWeights</code>); where both are present, the vertex stage adds the weighted deltas to the base geometry.</p> <p><code>instances</code> is how many render slots that happened at, and <code>blends</code> how many single-target blends those slots carry between them: a slot contributes one per target its weights move, or that they moved the frame before, so the number of targets a mesh can be given is bounded by the buffer the blends live in. <code>meshes</code> is how many meshes hold a delta block and <code>targets</code> how many targets those blocks cover between them; <code>deltaBytes</code> is what the shared buffer they are appended into holds, and <code>deltaBufferBytes</code> the size of that buffer, which the vertex stage binds whole.</p> <p><code>deltaBudgetBytes</code> is the most that buffer may hold on this device: the largest storage buffer the device binds at once (128 MiB on many mobile browsers). A block costs 32 bytes per vertex per target, over every vertex of the mesh. A mesh whose block would take the resident blocks past the budget is refused by name in the engine log and draws in its base shape, and <code>refusedMeshes</code> counts the meshes refused that way; the refusal holds until the mesh is uploaded again. A morph-target mesh whose weights are all zero reads a <code>meshes</code> above zero beside an <code>instances</code> and <code>blends</code> of zero.</p> <pre><code class="language-lua">local m = renderer.morphStats() print(("%d instances carrying %d blends over %d meshes"):format(m.instances, m.blends, m.meshes)) </code></pre> <h2 id="modules-renderer-observe">modules/renderer/observe</h2> <pre><code class="language-lua">observe(): RenderObservation </code></pre> <p>Everything the renderer knows about the frame it last drew: what program it bound for each renderable, the render state it holds each material under, and what each program has cost in pipeline builds. <code>renderables</code> is one row per renderable in the renderer's draw list, carrying the program its material named (<code>requestedProgram</code>) beside the one that was bound (<code>boundProgram</code>) — <code>__error__</code> wherever the lookup missed and the draw went ahead on the magenta placeholder — plus <code>substituted</code>, the <code>outcome</code> (<code>drew</code> / <code>drewPlaceholder</code> / <code>skipped</code> / <code>notDrawn</code>), the <code>reason</code> that forced it and the compiler's own <code>detail</code> for a failed compile. <code>observed</code> says which of two answers a row is: <code>true</code> for a resolution a geometry pass took as it drew, <code>false</code> for the renderer's own resolution of a renderable this frame drew nowhere, which is what a renderable outside every camera's frustum or layer mask reports. <code>materials</code> is one row per material the renderer holds a prepared bind group for; <code>shaders</code> is one row per program pipelines have been built for. <code>frame</code> names the frame every per-frame count covers; <code>retainedFrames</code> how many frames a resolution a pass took is kept for after the last frame that drew it; <code>window</code> and <code>costWindow</code> state both in the document itself. Recording is armed by the first read, so this waits for the frame that first records rather than answering empty.</p> <pre><code class="language-lua">local o = renderer.observe(); print(o.placeholderDraws, "renderables drew the placeholder in frame", o.frame) </code></pre> <h2 id="modules-renderer-occlusionculling">modules/renderer/occlusionCulling</h2> <pre><code class="language-lua">occlusionCulling(): boolean </code></pre> <p>Whether occlusion culling is currently enabled.</p> <h2 id="modules-renderer-passschedule">modules/renderer/passSchedule</h2> <pre><code class="language-lua">passSchedule(): { </code></pre> <p>The schedule check over this frame's enqueued render passes. Passes declare what they read (<code>inputs</code>) and what they write (<code>output</code> / <code>outputs</code> / <code>storage</code>), and the frame runs them in phase order and, inside a phase, in <code>order</code> order. <code>violations</code> holds every input bound to a resource the frame produces LATER: that read samples the resource as it stands ahead of that pass, which is the previous frame's contents for a render target that persists, an empty target for one just created, and the scene draw's own output for a <code>@scene.*</code> buffer — and the pass renders either way. The frame's own buffers are checked on the same terms as a render target: bind <code>@scene.motion</code> at a phase ahead of the pass that writes it and the read is reported, naming the buffer and its writer. A pass reading a resource ahead of that write on purpose declares that slot in its enqueue's <code>readsPrevious</code> and drops out of the list; <code>unboundPrevious</code> holds declared slots the pass binds no such resource to, which cover nothing. A resource no queued pass writes is not reported — a camera rendering to texture and <code>compute.dispatch</code> both fill targets outside the pass queue, and the scene draw fills the <code>@scene.*</code> buffers every frame. A read the frame has only one order for is not reported either: where the writing pass consumes something the reading pass produces, the reader runs first or the writer has nothing to write, which is what a pass reading a buffer into a target of its own and a second pass copying that target back over the buffer forms. <code>unreachable</code> holds passes at a phase that does not run their kind: every phase drains its fragment and compute passes, while <code>afterLighting</code> is the one that draws geometry, draw and splat passes, so one of those enqueued elsewhere sits in the queue and never runs. Each finding is also stated in the engine log the first time it appears.</p> <pre><code class="language-lua">local s = renderer.passSchedule() for _, v in s.violations do print(v.message) end </code></pre> <h2 id="modules-renderer-pendingwork">modules/renderer/pendingWork</h2> <pre><code class="language-lua">pendingWork(): PendingWork? </code></pre> <p>What the renderer still owes the picture — the work it has taken on and has not yet put in the frame.</p> <p>A frame draws what the renderer holds at the moment it runs, and several of the things it holds arrive over more than one frame. A pipeline is built off the frame thread and bound the frame its result is taken in; a texture is uploaded under a per-frame time budget; a released texture's GPU object is freed on the frame the release queue is drained; a streamable texture climbs toward the level its screen size asks for one level at a time. <code>pipelineBuilds</code>, <code>textureUploads</code>, <code>textureReleases</code> and <code>textureResidency</code> are those four. <code>textureTransfers</code> counts the textures the texture cache is still moving over several frames: a large upload written a band at a time, a promotion waiting on its finer levels to be read or decoded, a demotion waiting on its levels to be read back. Each lands as a new GPU texture on a frame of its own, and the frames between two landings replace nothing, so <code>textureResidency</code> alone reads zero on them. <code>outstanding</code> is the sum of every count.</p> <p><code>settled</code> is true when <code>outstanding</code> is zero, and says the renderer owes this picture nothing: the frames after this one draw the same picture unless something changes the scene. That is the renderer's own word for what a test measuring a picture holds for before it reads one.</p> <p><code>epoch</code> counts the times the renderer has gone from owing nothing to owing something since the process started. It is what a pair of readings needs: a run of work that begins AND ends between two samples leaves both of them reading <code>settled</code> and leaves <code>epoch</code> higher, so <code>settled</code> at both ends with the same <code>epoch</code> at both ends is the statement that nothing arrived across the whole span, rather than at its two endpoints alone. <code>Test.settleReading</code> is built on exactly that pair of conditions.</p> <p><code>frame</code> names the drawn frame the reading was published on. Every count is published from the frame thread on every frame the renderer runs, so a headless engine declining frames answers as readily as a windowed one. <code>deviceRebuild</code> is one while the engine is between render devices: the picture is owed the device itself, every frame in that window draws nothing, and what content puts back on the new device lands on the frames after it. <code>computeCommands</code> counts the compute commands a drain held over: a dispatch waiting on the program it names and every command over the buffers it would write queued behind it, each of which runs or drops on a later drain. <code>clusterSets</code> counts the meshes whose cluster set is still being built, or is owed again after a device rebuild: each lands as the set the mesh draws, and the geometry a cut of it draws changes on the frame it does. <code>materialRows</code> counts the materials the frame does not yet draw as they ask: one whose raster or resolve pipeline is still building draws nothing until it lands, one whose shader's resolve gate has not reached a verdict resolves through the error shader until it does, and one whose routing changed since the frame last published it is still drawn by the previous routing. Each changes the picture on the frame it lands. <code>skippedDraws</code> counts the renderables the last drawn frame left undrawn because a pipeline or a material group they draw with was not built yet; each appears on the frame what it waits on lands. <code>shaderCompiles</code> counts the shader compiles running off the frame thread that no frame has landed: each lands as the program its keys name, and the materials drawing with those keys change on the frame it does.</p> <pre><code class="language-lua">if renderer.pendingWork().settled then print("the picture is what it is going to be") end local w = renderer.pendingWork(); print(w.outstanding, "arrivals still owed, over", w.epoch, "runs of work") </code></pre> <h2 id="modules-renderer-pipelinecache">modules/renderer/pipelineCache</h2> <pre><code class="language-lua">pipelineCache(): PipelineCache? </code></pre> <p>What the driver's compiled-pipeline store held, built, and wrote back. A pipeline is machine code the GPU driver compiles from the shader bound into it, and that compile is what a launch pays before the first frame drawing with each pipeline can appear. The store keeps that compiled code across runs, so a launch whose shaders have not changed reads back what the previous one compiled.</p> <p><code>restoredBytes</code> is what a previous run left for this GPU and this launch read; <code>pipelinesBuilt</code> counts the pipelines built since startup and <code>buildMs</code> is what they cost together, which is the number the store lowers. <code>saves</code> and <code>savedBytes</code> describe writing it back — deferred until a burst of builds settles, so one launch is one write — and <code>dirty</code> is true while pipelines have been built that the file does not hold, including after a write that failed, which <code>lastError</code> then names. <code>path</code> is the file, named after the GPU it belongs to.</p> <p><code>supported</code> is false where the platform holds no store a program can carry: a browser keeps its own and hands none out, and an adapter can lack the capability. <code>reason</code> says which, and the build count and timing still read true there. <code>lastError</code> names a read or write failure; a failed store costs the saved compile and never the frame, since every pipeline is built from its source either way.</p> <p>A pipeline a material, the sky, a depth view, a post-process pass or a compute program a script registers needs after boot is built off the frame thread — the shader module behind that compute program with it — on <code>buildWorkers</code> worker threads on native, behind the browser's asynchronous entry point on the web, where <code>buildWorkers</code> reads 0. So a burst of new materials costs a frame nothing beyond drawing the placeholder until each pipeline lands, and a burst of effects costs it nothing beyond each effect starting on the frame its pipeline does. <code>offFrameBuilt</code> and <code>offFrameBuildMs</code> are the pipelines and the milliseconds that never held a frame; <code>pipelinesBuilt - offFrameBuilt</code> is what the frame thread built, which is the renderer's own — the set it stands up before its first frame, and the few it stands up on the frame a pass first asks for them. <code>buildsSubmitted</code>, <code>buildsFinished</code>, <code>buildsFailed</code> and <code>buildsInstalled</code> count build requests (shader modules and pipelines) made, completed, refused and taken back by a frame. <code>buildsAbandoned</code> counts the requests a renderer still held when a device rebuild let it go, which no frame will take back, and <code>buildsPending</code> is what the renderer drawing now has yet to take back: asked for and not yet in use, a finished result included until a frame installs it. So every request made is installed, abandoned or pending, and a reader waiting on <code>buildsPending</code> to reach zero reads <code>pipelinesBuilt</code> with every build it waited for counted, and can tell a queue draining from one that is stuck. <code>modulesBuilt</code> and <code>moduleBuildMs</code> are the shader modules compiled off the frame thread, parse and validation included.</p> <p><code>pipelinesBuilt</code> and <code>buildMs</code> are engine-wide totals; <code>renderer.shaderCost()</code> is the same cost broken down per program, with each one's permutation count.</p> <pre><code class="language-lua">local c = renderer.pipelineCache() print(("%d pipelines in %.1f ms, %d bytes restored"):format(c.pipelinesBuilt, c.buildMs, c.restoredBytes)) </code></pre> <h2 id="modules-renderer-presentedframes">modules/renderer/presentedFrames</h2> <pre><code class="language-lua">presentedFrames(opts: { scope: string?, frames: number?, interval: number?, velocity: boolean? }?): { PresentedFrame } </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ scope: string?, frames: number?, interval: number?, velocity: boolean? }?</code> <em>(optional)</em></li> </ul> <h2 id="modules-renderer-projectionjitter">modules/renderer/projectionJitter</h2> <pre><code class="language-lua">projectionJitter(): { { number } } </code></pre> <p>The offsets <code>renderer.setProjectionJitter</code> stated, each <code>{x, y}</code> in pixels from the pixel centre.</p> <pre><code class="language-lua">local offsets = renderer.projectionJitter() </code></pre> <h2 id="modules-renderer-projectionoffset">modules/renderer/projectionOffset</h2> <pre><code class="language-lua">projectionOffset(): (number, number) </code></pre> <p>The sub-pixel projection offset in force for the main camera, in NDC.</p> <pre><code class="language-lua">local ox, oy = renderer.projectionOffset() </code></pre> <h2 id="modules-renderer-qualitytier">modules/renderer/qualityTier</h2> <pre><code class="language-lua">qualityTier(): QualityTier </code></pre> <p>The device's quality tier and the renderer defaults it chose. The engine assigns every device one of <code>"low"</code>, <code>"medium"</code> and <code>"high"</code> at boot, from its GPU family and limits, its memory and its display, so a world authored on a desktop plays on a phone at the phone's defaults without knowing it runs on one. A desktop, and any device whose GPU is outside the handheld families, is <code>"high"</code>: the renderer as a desktop runs it, one scene pixel per display pixel. A phone renders the scene below a dense display's resolution with UI and text at full resolution, and spends less on shadows, textures and filtering the lower its tier. A lower tier chosen on a desktop renders the scene within that tier's pixel budget the same way, so <code>renderer.setQualityTier</code> alone is a whole graphics setting on every platform.</p> <p>Every default the tier states stands wherever the world's settings name no value of their own (<code>render.anisotropy</code>), and the matching renderer call overrides any one of them: <code>renderer.setResolutionScale</code>, <code>renderer.setTextureBudget</code>, <code>renderer.setAnisotropy</code>. <code>renderer.setQualityTier</code> chooses another tier altogether.</p> <pre><code class="language-lua">local q = renderer.qualityTier() print(q.tier, q.reason, q.defaults.resolutionScale) </code></pre> <h2 id="modules-renderer-raycast">modules/renderer/raycast</h2> <pre><code class="language-lua">raycast( </code></pre> <p>Cast a ray against the geometry the renderer DRAWS and return the nearest surface it meets. Every visible mesh answers, whether or not anything gave it a rigid body — so a terrain, a procedurally generated mesh, or any plain <code>Model</code> reports the surface at a point, which is what a camera station, a prop, a sound source or a scatter standing on the ground needs to know. The answer is the nearest triangle of the mesh, so a sloped or terraced surface reports its height where it was asked rather than the extent of its bounding box.</p> <p><code>distance</code> is measured from <code>origin</code> along the direction given, so it is a world-space distance whenever that direction is a unit vector, and it is directly comparable to a <code>physics.raycast</code> distance along the same ray. <code>normal</code> is a unit vector turned to face back along the ray. <code>exact</code> is true when the answer is a triangle and false when it is the object's bounding box, which is what a mesh whose vertices live only in GPU buffers answers with. The triangles are the mesh's own, placed by the entity's transform and by the mesh's bind pose, so a surface a skinning or morph pass deforms on the GPU answers as the geometry the mesh holds.</p> <p>EVERYTHING drawn is in scope — the ground you meant, and equally a character standing on it, a prop, a placeholder floor. The hit names its entity in <code>entityId</code>, <code>exclude</code> steps over the ones you do not want, and <code>renderer.raycastAll</code> hands back the whole column so you can pick the surface yourself. A height you did not expect is usually a nearer surface you did not mean to ask about, so read <code>entityId</code> before trusting a number.</p> <pre><code class="language-lua">local hit = renderer.raycast({ x, 100, z }, { 0, -1, 0 }, 200) if hit then entity(cameraId).position = { x, hit.point.y + 1.7, z } end </code></pre> <h2 id="modules-renderer-raycastall">modules/renderer/raycastAll</h2> <pre><code class="language-lua">raycastAll( </code></pre> <p>Cast a ray against the geometry the renderer draws and return every surface along it, nearest first. One entry per renderable the ray crosses — the nearest intersection with each — so a stack of surfaces reads as the order they stand in, and a caller after one particular surface finds it by <code>entityId</code> rather than hoping it is the nearest. Each entry carries the fields <code>renderer.raycast</code> returns.</p> <pre><code class="language-lua">for _, hit in renderer.raycastAll(eye, look, 500) do print(hit.entityId, hit.distance) end </code></pre> <h2 id="modules-renderer-raytracecapability">modules/renderer/raytraceCapability</h2> <pre><code class="language-lua">raytraceCapability(): string </code></pre> <p>The active ray-tracing backend: <code>"hardware"</code> (GPU ray query) or <code>"compute"</code> (software traversal — the path on devices without hardware ray query, e.g. the web). The same ray-tracing features work on both.</p> <pre><code class="language-lua">if renderer.raytraceCapability() == "hardware" then ... end </code></pre> <h2 id="modules-renderer-raytracestats">modules/renderer/raytraceStats</h2> <pre><code class="language-lua">raytraceStats(): { [string]: any } </code></pre> <p>What the ray-tracing acceleration structure holds, and what this session's frames have spent building it. A ray walks a structure built over the scene's geometry, and keeping it current is work a frame pays before it traces anything. On the <code>"compute"</code> backend geometry that has stood still long enough is filed under a static partition the frames after it leave alone: <code>staticTriangles</code> + <code>dynamicTriangles</code> = <code>triangles</code>, <code>nodes</code> is the hierarchy over them, <code>fullRebuilds</code> / <code>partialRebuilds</code> / <code>reusedFrames</code> count what the session's frames did, and <code>trianglesRebuilt</code> is what those rebuilds re-emitted, summed. On the <code>"hardware"</code> backend <code>blas</code> is the bottom-level structures cached, <code>blasBuilt</code> how many the last frame built, and <code>tlasInstances</code> what the top-level structure names. The counters are cumulative — sample, run the scene, sample again.</p> <pre><code class="language-lua">local before = renderer.raytraceStats().trianglesRebuilt </code></pre> <h2 id="modules-renderer-references">modules/renderer/references</h2> <pre><code class="language-lua">references(handleOrKind: any, id: string?): RuntimeResourceStatus? </code></pre> <p>What holds a runtime resource right now — the answer a root scene load reads before releasing it. <code>references</code> names each live consumer the engine found: <code>{ by = "entity", id }</code> for an entity wearing the material or mesh, <code>"material"</code> for a material whose slot names the texture (or names a texture array the device has not assembled from it yet), <code>"instancedDraw"</code>, <code>"camera"</code>, <code>"sky"</code>, <code>"lightmap"</code>, <code>"ui"</code> (a screen drawing it) and <code>"postProcess"</code> (an effect sampling it). <code>handleHeld</code> says whether a script still reaches a handle to it, <code>assetBacked</code> whether an asset stands behind it, <code>ownerLive</code> whether the component instance, scene load or feature that created it still stands, and <code>held</code> whether a hold pins it. <code>origin</code> reads <code>"device"</code> for a GPU texture the device holds that no script created — the one the cache loaded for an asset, the atlas the engine built — whose holders are the references, a handle and the asset. Runs a full garbage collection first, the same one <code>renderer.collect</code> runs, so a handle nothing reaches counts as let go and the row says what the next collection does with the resource. Yields for the frame the census runs on.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrKind</code> <code>any</code> <em>(optional)</em> — The resource's handle, or its kind with the id second.</li> <li><code>id</code> <code>string?</code> <em>(optional)</em> — The guid or key, when the first argument is a kind.</li> </ul> <pre><code class="language-lua">local s = renderer.references(mat) for _, r in s.references do print(r.by, r.id) end </code></pre> <h2 id="modules-renderer-reflectionenvironment">modules/renderer/reflectionEnvironment</h2> <pre><code class="language-lua">reflectionEnvironment(): { </code></pre> <p>What a reflective surface is reflecting. <code>probes</code> is how many reflection probes the shading blends; they are gathered highest <code>priority</code> first, each rank taking the coverage the ranks above it left, so a small interior probe ranked above the large exterior one it sits inside wins outright wherever it reaches full weight. <code>ranks</code> is the priority each of those probe slots was published with, in slot order. <code>sky</code> is whether the sky fallback is armed: with it, coverage no probe claims reflects the captured sky, and without it a surface outside every probe's radius falls back to the nearest probe alone. <code>skyCaptured</code> is whether the sky slot holds a capture — arming is refused until it does, since an uncaptured slot reflects black. <code>skyStale</code> is whether the sky changed after that capture, so the slot still depicts the sky from before the change until the engine's recapture, or an explicit <code>environment.captureSky()</code>, retakes it. The engine retakes a sky swapped for another material or turned off on the first frame the new sky can be drawn, and a sky whose look moved once a pacing window has passed since the last capture. <code>skyPosition</code> is where the capture was taken from, absent before one. <code>skyCaptures</code> counts the sky captures that have landed since the renderer was built, so one retaken from the same position reads as new, and <code>skyMaterial</code> names the material the sky pass draws from, absent when no sky is drawn. <code>slots</code> is how many cube slots the environment array holds right now: the sky's alone, at index <code>skySlot</code>, until a probe is captured into it, then that one, the slot a camera's stated environment is held in, and one per probe. <code>maxProbes</code> is how many of them probes may take, and <code>resident</code> whether the array has grown past the sky's single slot. Capture the sky with <code>environment.captureSky()</code>.</p> <pre><code class="language-lua">local env = renderer.reflectionEnvironment() print(("%d probes, sky fallback %s"):format(env.probes, tostring(env.sky))) </code></pre> <h2 id="modules-renderer-registeredcopy">modules/renderer/registeredCopy</h2> <pre><code class="language-lua">registeredCopy(content: { [string]: any }): { [string]: any } </code></pre> <p>Destroy every render feature created since <code>renderer.markBaseline</code>, withdraw every capture view registered since, and put the feature-texture array back to the size it recorded.</p> <p>A feature the mark recorded is known by its identity: one destroyed and created again under the same identity since the mark stands, under whatever guid it now carries. A capture view is known by its name.</p> <p>A feature is usually created by the module that needs it, on first use, and that module holds its own record of having done so. Drop the module's cache alongside this (<code>engine.resetScriptingState</code>) or the module will go on believing it has a feature this destroyed, and nothing will rebuild it.</p> <p>A hold on the clock (<code>renderer.temporal.hold</code>) or on the air (<code>renderer.atmospherics.hold</code>) taken since the mark comes off; one the mark recorded stands until the caller that took it releases it.</p> <p>What this asks for lands on the renderer's next frame, so a caller that must see the renderer already back — one handing the engine to somebody else — waits a frame when the answer is above zero. recorded size, from the next frame on.</p> <p><strong>Parameters</strong></p> <ul> <li><code>content</code> <code>{ [string]: any }</code></li> </ul> <pre><code class="language-lua">if renderer.resetToBaseline(mark) > 0 then task.waitFrames(1) end </code></pre> <h2 id="modules-renderer-release">modules/renderer/release</h2> <pre><code class="language-lua">release(handleOrKind: any, id: string?): boolean </code></pre> <p>Let go of the hold <code>renderer.hold</code> placed. The resource stays until nothing else holds it and a collection releases it — the one a root scene load runs, or a direct <code>renderer.collect()</code>. A runtime mesh no script reaches a handle to is judged again once its hold goes, and released with no collection when nothing else holds it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handleOrKind</code> <code>any</code> <em>(optional)</em> — The resource's handle, or its kind with the id second.</li> <li><code>id</code> <code>string?</code> <em>(optional)</em> — The guid or key, when the first argument is a kind.</li> </ul> <pre><code class="language-lua">renderer.release(tex) </code></pre> <h2 id="modules-renderer-releasebaseline">modules/renderer/releaseBaseline</h2> <pre><code class="language-lua">releaseBaseline(generation: number): boolean </code></pre> <p>Drop the mark <code>generation</code> names, and every mark taken after it, once the caller that took it is done resetting to it. It touches the marks alone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>generation</code> <code>number</code> — The mark to drop, as <code>renderer.markBaseline</code> returned it.</li> </ul> <pre><code class="language-lua">renderer.releaseBaseline(mark) -- the run is over </code></pre> <h2 id="modules-renderer-rendertargetlimits">modules/renderer/renderTargetLimits</h2> <pre><code class="language-lua">renderTargetLimits(): { </code></pre> <p>The size a render target may be on this device. <code>maxDimension</code> is the device's own maximum 2D texture dimension — the largest either side of a render target may take. <code>maxPixels</code> is how many pixels one render target may hold, so the RGBA8 image it reads back as fits in a single buffer on every platform the engine runs on, and <code>maxSquare</code> is the largest square that budget buys. A capture, a <code>renderer.texture.create({ width, height })</code> or a render-to-texture camera past either bound is refused at the call with the reason, so ask here for the size to request.</p> <pre><code class="language-lua">local lim = renderer.renderTargetLimits() local w = math.min(want, lim.maxSquare) </code></pre> <h2 id="modules-renderer-rendertargets">modules/renderer/renderTargets</h2> <pre><code class="language-lua">renderTargets(): { </code></pre> <p>Every render target the renderer owns and what each one costs, measured from the texture that is allocated. One row per target, each carrying its <code>name</code>, whether it is <code>resident</code>, the <code>bytes</code> it holds while it is, its <code>width</code>/<code>height</code>/<code>layers</code>/<code>mipLevels</code>, and <code>onDemand</code>. An <code>onDemand</code> target exists only while something needs it: a target nothing writes into reads <code>resident = false</code> and <code>bytes = 0</code> and appears again the frame something writes it, and one sized by content (the reflection-probe cube array) holds the slots content asked for. The scene's occlusion and reflection channels and its lit colour history (<code>scene_occlusion</code>, <code>scene_reflection</code>, <code>scene_color_history</code>) are resident at the scene's extent while a render-feature pass writes or reads them, and <code>shadow_page_pool</code> while a light casts into the virtual shadow map, with <code>shadow_transmittance_pool</code> beside it while a translucent caster casts into it. The scratch the draws into a render target have needed is reported as <code>camera[<handle>].*</code> rows: depth and motion vectors under any rasterized pass, and the occlusion, reflection and colour-history channels, the scene copies and the G-buffer over them under a camera's scene render. A draw builds what it needs, or takes the scratch a released target of the same size left. A target releases its set to the next target of its size when no live camera names it and when it is destroyed or resized, and frees it once sixty frames pass without a draw, so a target nothing draws into carries no such row. Released scratch waits as <code>released_scratch[<width>x<height>].*</code> rows for the next target of that size, held newest first within 64 MiB and freed once sixty frames pass without a target taking it. The colour image drawn into belongs to the texture cache while its target stands; a destroyed or resized target's colour texture waits on the same terms as a <code>released_scratch[<width>x<height>].color</code> row for the next target made at that size and format, which starts from it cleared to transparent black. <code>totalBytes</code> is what the resident targets hold together. <code>allocations</code> counts the render-target allocations the renderer has made since it was built, the colour textures of the live targets included, each on the first frame whose report holds it: a target taken back at a size it held before adds nothing, so the difference between two readings is what the frames between them allocated. Measured at the end of the last rendered frame.</p> <pre><code class="language-lua">local rt = renderer.renderTargets() print(("render targets: %.1f MiB over %d resident"):format(rt.totalBytes / 1048576, rt.residentCount)) for _, t in rt.targets do if t.onDemand then print(t.name, t.resident, t.bytes) end end </code></pre> <h2 id="modules-renderer-resettobaseline">modules/renderer/resetToBaseline</h2> <pre><code class="language-lua">resetToBaseline(generation: number?): number </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>generation</code> <code>number?</code> <em>(optional)</em></li> </ul> <h2 id="modules-renderer-resolutionscale">modules/renderer/resolutionScale</h2> <pre><code class="language-lua">resolutionScale(): number </code></pre> <p>The fraction of the display resolution the scene is currently rendered at. Until something sets it, the scale the device's quality tier chose for its display (<code>renderer.qualityTier().defaults.resolutionScale</code>): <code>1</code> on a desktop at the high tier, below <code>1</code> on a dense handheld display and on a desktop at a lower tier.</p> <pre><code class="language-lua">local s = renderer.resolutionScale() </code></pre> <h2 id="modules-renderer-resolutionscalemode">modules/renderer/resolutionScaleMode</h2> <pre><code class="language-lua">resolutionScaleMode(): string </code></pre> <p>Where the render scale resolves: <code>"present"</code> or <code>"reconstruct"</code>. <code>"present"</code> until something sets it.</p> <pre><code class="language-lua">local m = renderer.resolutionScaleMode() </code></pre> <h2 id="modules-renderer-setanisotropy">modules/renderer/setAnisotropy</h2> <pre><code class="language-lua">setAnisotropy(level: number): number </code></pre> <p>Set the maximum anisotropy material textures are sampled with. Takes effect on the next frame for content already on screen — no reload, no texture re-upload. 1 is plain trilinear.</p> <p><strong>Parameters</strong></p> <ul> <li><code>level</code> <code>number</code> — One of 1, 2, 4, 8, 16. Any other value is an error.</li> </ul> <pre><code class="language-lua">renderer.setAnisotropy(16) </code></pre> <h2 id="modules-renderer-setblendedbatching">modules/renderer/setBlendedBatching</h2> <pre><code class="language-lua">setBlendedBatching(enabled: boolean): () </code></pre> <p>Whether neighbours in a view's back-to-front blended order draw together. On by default: alpha-blended geometry is submitted farthest-first, and a stretch of neighbours in that order sharing a mesh, a material, a shader and a pose is submitted as one instanced draw over those neighbours, which puts the same members on screen in the same order out of a single submission. A run stops wherever a differently-drawn renderable sorts between two of its members, and a mesh of several primitives keeps a draw per renderable — both would otherwise move fragments through each other. Off, every blended renderable draws on its own at its own slot, so a transparent crowd costs a draw per member. The image is the same either way, which is what makes this the comparison a frame suspected of being formed by the batching is made against; <code>renderer.drawStats().draws</code> counts the difference.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setBlendedBatching(false) -- a draw per blended renderable </code></pre> <h2 id="modules-renderer-setdraweveryframe">modules/renderer/setDrawEveryFrame</h2> <pre><code class="language-lua">setDrawEveryFrame(everyFrame: boolean): boolean </code></pre> <p>Draw every frame continuously (<code>true</code>), or redraw only while there is something new to show (<code>false</code>, the default), so an idle screen whose picture stands still is not redrawn.</p> <p>By default a renderer presenting to a display draws while what is on screen changes (the UI, the camera, a renderable, a material, a light, an animation, a texture, the window) and for a short settling run after the last change, then declines the frames of a still picture, which stays on screen. While anything else moves (the gameplay clock, a task owed the next frame, held input, a tool call in flight) the frames keep running at the display's rate while it declines, and an engine that is still waits for the next arrival or timer instead (<code>renderer.framePacing().engineStill</code>); the next change is drawn on the frame after it. On a phone that is the GPU work, the heat and the battery of every frame an idle menu or a paused editor would otherwise redraw. A headless renderer draws while something reads its frames.</p> <p><code>true</code> draws every frame on every renderer, for content whose picture changes in a way the engine does not see, or that is measured frame by frame. <code>renderer.framePacing()</code> reports the rule in force (<code>drawPolicy</code>), whether the last frame was drawn (<code>producing</code>), the frames declined (<code>idleSkippedFrames</code>) and what woke the renderer (<code>pictureChanges</code>, <code>lastPictureChange</code>). Takes effect on the next frame. <code>draw_every_frame</code> under <code>[render]</code> in the world's <code>.world_settings</code> seeds it, so a world states it once; a call here stays in force until that value changes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>everyFrame</code> <code>boolean</code> — boolean true to draw every frame, false to draw only frames that show something new.</li> </ul> <pre><code class="language-lua">renderer.setDrawEveryFrame(true) -- a benchmark measures every frame print(renderer.framePacing().drawPolicy) --> every_frame renderer.setDrawEveryFrame(false) -- back to drawing changes only </code></pre> <h2 id="modules-renderer-setframeratecap">modules/renderer/setFrameRateCap</h2> <pre><code class="language-lua">setFrameRateCap(fps: number?): number? </code></pre> <p>Hold the engine to at most <code>fps</code> frames per second, or lift the cap with 0 or nil. The whole frame waits for its turn: scripts, physics, audio mixing and networking run once a frame as the drawing does, so a game capped at 30 runs at 30, and a phone spends no battery on frames past it.</p> <p>Frames come due on an even grid. Where the display's refresh rate is known (a phone's panel, a desktop window's monitor, a browser's animation frames), the grid is a whole number of refreshes, the fewest whose rate stays inside the cap, so every frame stays on screen for the same number of vsyncs: on a 120 Hz display a cap of 30 shows each frame for 4 refreshes and 60 for 2, and a cap the refresh rate does not divide evenly delivers the next rate below it (50 on a 120 Hz display delivers 40). On Android the system compositor is told the rate as well, so a panel with more than one refresh rate can run at one the cap divides. On a display that shows frames on its vsync the cap is the highest rate the engine chooses (<code>renderer.setFrameRateMode</code>): frames that cannot hold it run at the next even rate below.</p> <p>Read <code>renderer.framePacing()</code> for the effect: <code>targetFps</code> is the rate the cap delivers, <code>refreshesPerFrame</code> the refreshes each frame holds, <code>displayRefreshHz</code> the display it was worked out against, and <code>deliveredFps</code> the rate frames actually ran at. Takes effect on the next frame. <code>frame_rate_cap</code> under <code>[render]</code> in the world's <code>.world_settings</code> seeds it (0 for no cap), so a world states its cap once and every session opens under it; a call here stays in force until that value changes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fps</code> <code>number?</code> <em>(optional)</em> — number? Frames per second, 1 through 1000; 0 or nil lifts the cap.</li> </ul> <pre><code class="language-lua">renderer.setFrameRateCap(30) -- a steady 30 on any display 30 divides print(renderer.framePacing().targetFps, "fps delivered under the cap") renderer.setFrameRateCap(nil) -- every frame the display shows </code></pre> <h2 id="modules-renderer-setframeratemode">modules/renderer/setFrameRateMode</h2> <pre><code class="language-lua">setFrameRateMode(mode: string): string </code></pre> <p>Choose how the engine picks the frame rate it runs at on a display that shows each frame on a vsync (<code>presentMode</code> fifo, fifo_relaxed or auto_vsync: a phone, a browser tab, a desktop window set to fifo).</p> <p><code>adaptive</code>, the default, runs at the highest rate the display shows evenly that the frames sustain, at most the cap (<code>renderer.setFrameRateCap</code>; the display's highest refresh rate without one). A frame that cannot hold 120 on a 120 Hz panel runs at an even 60 rather than at 60 to 67, one that cannot hold 60 at 40, never below 30, and the rate goes back up once the frames hold a faster one again (in one step after a load). <code>fixed</code> holds the cap's rate (the display's highest rate without a cap) whatever the frames take, for content that needs one set rate.</p> <p>In both modes every frame is due on its own vsync, so the engine makes no frame the display has no refresh to show. On Android the display is asked for the lowest of its modes the chosen rate divides into whole refreshes, so a panel whose frames come at 60 can run at 60 Hz and spend less power; a desktop window and a browser hold frames to their display by pacing alone. On a surface that shows a frame on whatever refresh it lands on (<code>mailbox</code>, <code>immediate</code>), or with no display, the cap alone holds the loop. <code>renderer.framePacing()</code> reports the mode (<code>frameRateMode</code>), the rate chosen (<code>chosenFps</code>), why (<code>frameRateReason</code>), the time frames take (<code>frameWorkMs</code>) and what the display was told (<code>displayVote</code>, <code>displayVoteFps</code>). Takes effect on the next frame. <code>frame_rate_mode</code> under <code>[render]</code> in the world's <code>.world_settings</code> seeds it; a call here stays in force until that value changes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — string "adaptive" or "fixed".</li> </ul> <pre><code class="language-lua">renderer.setFrameRateCap(120) -- a game that wants 120 where frames hold it renderer.setFrameRateMode("adaptive") print(renderer.framePacing().chosenFps, renderer.framePacing().frameRateReason) renderer.setFrameRateMode("fixed") -- hold the cap's rate whatever frames take </code></pre> <h2 id="modules-renderer-setgpumemorytracking">modules/renderer/setGpuMemoryTracking</h2> <pre><code class="language-lua">setGpuMemoryTracking(frames: number?): number </code></pre> <p>Set how often the GPU allocator sampler reads — one reading every <code>frames</code> frames — or turn it off with 0. It starts at 60, a reading a second at 60 Hz, so <code>renderer.gpuMemory().allocator</code> answers without anything arming it. Building the ledger walks every live allocation, which is why it is sampled rather than read every frame; the category figures cost nothing either way, and a reader between samples sees the most recent ledger, so a slow interval still answers.</p> <p>Called with no argument it reports the interval in force and changes nothing, which is how something that retimes the sampler puts it back afterwards instead of restoring a number it assumed was the default.</p> <p><strong>Parameters</strong></p> <ul> <li><code>frames</code> <code>number?</code> <em>(optional)</em> — <code>number?</code> Frames between readings; 0 turns the sampler off. Omit to read the interval without changing it.</li> </ul> <pre><code class="language-lua">renderer.setGpuMemoryTracking(60) local was = renderer.setGpuMemoryTracking() renderer.setGpuMemoryTracking(1) -- ... take a tight reading ... renderer.setGpuMemoryTracking(was) </code></pre> <h2 id="modules-renderer-setlighting">modules/renderer/setLighting</h2> <pre><code class="language-lua">setLighting(enabled: boolean): () </code></pre> <p>Turn the scene's lighting on or off. Off, every lit surface renders as its own colour with no light on it, emissive included: the editor's lighting toggle, for reading a scene's materials without its lights. With no light applied there is no shadow either, so the shadow maps draw nothing while it is off, and the shadow switches apply again once lighting is back on. The sky and post-processing stay as they are.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>true</code> renders the scene lit, <code>false</code> unlit.</li> </ul> <pre><code class="language-lua">renderer.setLighting(false); print(renderer.lighting()) </code></pre> <h2 id="modules-renderer-setmaxframesinflight">modules/renderer/setMaxFramesInFlight</h2> <pre><code class="language-lua">setMaxFramesInFlight(frames: number): number </code></pre> <p>Set how many frames of GPU work may be outstanding before the renderer stops running ahead. One is the least overlap this can express — a frame's work is waited for as soon as the next frame has been submitted — which is the lowest latency and the lowest throughput; higher values let a slow frame build a longer backlog, and that backlog is memory. Takes effect on the next frame.</p> <p>Answers the bound after clamping to [1, 8], so asking for more than the renderer honours reports what you actually got.</p> <p><strong>Parameters</strong></p> <ul> <li><code>frames</code> <code>number</code> — number Frames of GPU work that may be outstanding, 1 through 8.</li> </ul> <pre><code class="language-lua">renderer.setMaxFramesInFlight(1) -- lowest latency print(renderer.setMaxFramesInFlight(99), "was clamped") </code></pre> <h2 id="modules-renderer-setmaxtexturesize">modules/renderer/setMaxTextureSize</h2> <pre><code class="language-lua">setMaxTextureSize(px: number?): { px: number, devicePx: number } </code></pre> <p>The longest side a texture loaded from an asset may be resident at on this client, in texels. Such a texture larger than the cap is served from the first level of its mip chain that fits, so a 4096² texture under a 1024 cap holds its 1024² level and the levels below it, and the texture budget never takes it finer than that level. A changed cap moves the resident textures over the next frames; a raised one reads their finer levels back from their assets. A texture made from pixels with no asset behind it keeps every level it was made with, because the device holds its only copy. The device's own maximum 2D texture dimension bounds the cap from above. Call with no argument to lift the cap to the device's limit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>px</code> <code>number?</code> <em>(optional)</em> — <code>number?</code> — texels, a whole number of at least 1; nil for the device's own limit</li> </ul> <pre><code class="language-lua">renderer.setMaxTextureSize(2048) renderer.setMaxTextureSize() -- the device's own limit </code></pre> <h2 id="modules-renderer-setminscreensize">modules/renderer/setMinScreenSize</h2> <pre><code class="language-lua">setMinScreenSize(pixels: number): () </code></pre> <p>Stop drawing an object once its on-screen radius falls below this many pixels. A few pixels across, an object carries no detail a viewer can resolve while still costing a full vertex and submission pass, and the cutoff drops it from the camera's draws entirely — <code>0</code>, the default, keeps every object however small it lands. Measured from the object's own bounds against the camera's projection, so the same threshold means the same apparent size at any distance or field of view. Shadow casters have their own threshold in <code>renderer.setShadowCasterCutoff</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>pixels</code> <code>number</code> — <code>number</code> — smallest on-screen radius still drawn; 0 disables.</li> </ul> <pre><code class="language-lua">renderer.setMinScreenSize(4) -- drop anything under 4 px of radius print(renderer.drawStats().compactedDrawn, "instances survived it") </code></pre> <h2 id="modules-renderer-setocclusionculling">modules/renderer/setOcclusionCulling</h2> <pre><code class="language-lua">setOcclusionCulling(enabled: boolean): () </code></pre> <p>Enable or disable occlusion culling. While enabled the renderer reduces the depth the visibility raster lays into a pyramid each frame and tests every renderable that cleared the frustum against it, dropping the ones another surface entirely covers before their geometry is submitted. The pyramid describes the frame being drawn, so an object that becomes visible this frame is never held back a frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setOcclusionCulling(true); print(renderer.cullStats().occlusionCulled) </code></pre> <h2 id="modules-renderer-setpresentmode">modules/renderer/setPresentMode</h2> <pre><code class="language-lua">setPresentMode(mode: string): string </code></pre> <p>Set how a presented frame reaches the display. <code>fifo</code> queues every frame and shows it on a vertical blank, which never tears and never drops one; <code>mailbox</code> replaces the queued frame with the newest, which does not tear and does not hold the renderer to the refresh rate; <code>immediate</code> presents as soon as a frame is ready and can tear; <code>fifo_relaxed</code> is <code>fifo</code> that tears rather than stall when a frame misses its blank; <code>auto_vsync</code> and <code>auto_no_vsync</code> leave the choice to the backend.</p> <p>A surface that does not offer the mode presents <code>fifo</code> instead, so read <code>renderer.framePacing().presentMode</code> for what took effect and <code>.presentModes</code> for what this surface offers. Takes effect on the next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — string One of "fifo", "fifo_relaxed", "mailbox", "immediate", "auto_vsync", "auto_no_vsync".</li> </ul> <pre><code class="language-lua">renderer.setPresentMode("mailbox") print(renderer.framePacing().presentMode, "is what the surface took") </code></pre> <h2 id="modules-renderer-setprojectionjitter">modules/renderer/setProjectionJitter</h2> <pre><code class="language-lua">setProjectionJitter(offsets: { { number } }) </code></pre> <p>State where inside the pixel consecutive renders of a view sample, for the frames until it is stated again: a list of <code>{x, y}</code> offsets in pixels from the pixel centre, each within half a pixel. Every view the renderer draws (the viewport, each render-to-texture camera, each capture) walks the list by its own count of renders in a row, so a camera drawn beside the viewport takes nothing out of the viewport's sequence: a view's first render, and its first after a cut, samples the pixel centre, and its next renders take the offsets in order, wrapping. Each offset is applied in the view's own pixels. Velocity (<code>@scene.motion</code>) is measured against the offset-free projection, and a history target's <code>state</code> buffer carries the offset each render took. <code>renderer.setProjectionOffset</code>, while it is not <code>(0, 0)</code>, and an upscale backend's own jitter both stand over the list on the viewport. An empty list samples pixel centres. An offset outside half a pixel is refused, naming it, and leaves the list in force.</p> <p><strong>Parameters</strong></p> <ul> <li><code>offsets</code> <code>{ { number } }</code> — The offsets, each <code>{x, y}</code> in pixels from the pixel centre.</li> </ul> <pre><code class="language-lua">renderer.setProjectionJitter({ { 0.25, -0.25 }, { -0.25, 0.25 } }) </code></pre> <h2 id="modules-renderer-setprojectionoffset">modules/renderer/setProjectionOffset</h2> <pre><code class="language-lua">setProjectionOffset(x: number, y: number) </code></pre> <p>Offset the main camera's projection by a sub-pixel amount, in NDC, for the frames until it is set again. The offset is in NDC because that is the space it is constant in: one pixel is <code>2.0 / width</code> across, so half a pixel is <code>1.0 / width</code>. Velocity (<code>@scene.motion</code>) is measured against the offset-free projection, so a still scene reports no motion however the samples are placed — and picking resolves a click to the same ray either way. <code>(0, 0)</code> samples pixel centres.</p> <p><strong>Parameters</strong></p> <ul> <li><code>x</code> <code>number</code> — Horizontal offset in NDC. One pixel is <code>2.0 / width</code>.</li> <li><code>y</code> <code>number</code> — Vertical offset in NDC. One pixel is <code>2.0 / height</code>.</li> </ul> <pre><code class="language-lua">renderer.setProjectionOffset(0.5 * 2 / w, -0.25 * 2 / h) </code></pre> <h2 id="modules-renderer-setqualitytier">modules/renderer/setQualityTier</h2> <pre><code class="language-lua">setQualityTier(tier: string?): QualityTier </code></pre> <p>Render at another quality tier than the one the device was assigned: <code>"low"</code>, <code>"medium"</code> or <code>"high"</code>, or <code>nil</code> / <code>"default"</code> for the device's own. A change of tier stands the new tier's defaults at once wherever the world's settings state none, replacing what an earlier renderer call set for the render scale, the shadow budgets and the anisotropy; choosing the tier already in force changes no setting. A call made after it, in the same script or later, overrides that one default again. A budget <code>renderer.setTextureBudget</code> names stands over the tier's texture cap until it is returned to the device's own. The choice holds for the process, across scene loads and restarts of the scene, until it is set again. A settings screen offering the player a quality choice is this call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>tier</code> <code>string?</code> <em>(optional)</em> — <code>string?</code>: <code>"low"</code>, <code>"medium"</code>, <code>"high"</code>, or <code>nil</code> / <code>"default"</code> for the device's own.</li> </ul> <pre><code class="language-lua">renderer.setQualityTier("low") renderer.setQualityTier() -- back to the device's own </code></pre> <h2 id="modules-renderer-setraytrace">modules/renderer/setRaytrace</h2> <pre><code class="language-lua">setRaytrace(enabled: boolean): () </code></pre> <p>Enable or disable GPU ray tracing. While enabled the engine builds the scene acceleration structure each frame so ray-tracing render features can trace against it; disabling stops the build (so it costs nothing until a ray-traced effect is active). Required before any ray-traced shadows / AO / reflections render.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setRaytrace(true); renderer.feature.create("rt_shadows") </code></pre> <h2 id="modules-renderer-setresolutionscale">modules/renderer/setResolutionScale</h2> <pre><code class="language-lua">setResolutionScale(scale: number): number </code></pre> <p>Render the scene at a fraction of the display's resolution. Shading cost scales with pixel count and with nothing else, so this trades sharpness for frame time without taking anything out of the scene: at <code>0.5</code> the scene rasterizes a quarter of the pixels. What the smaller raster MEANS for everything composed on top of it — the post chain, the UI, every post-scene pass — is <code>renderer.setResolutionScaleMode</code>. The scene rows in <code>profiler.gpuFrame()</code> are what move.</p> <p><strong>Parameters</strong></p> <ul> <li><code>scale</code> <code>number</code> — <code>number</code> — fraction of the display resolution, clamped to [0.25, 1].</li> </ul> <pre><code class="language-lua">renderer.setResolutionScale(0.7) </code></pre> <h2 id="modules-renderer-setresolutionscalemode">modules/renderer/setResolutionScaleMode</h2> <pre><code class="language-lua">setResolutionScaleMode(mode: string): string </code></pre> <p>Where in the frame the render scale is resolved — what a smaller raster MEANS. <code>"present"</code> (the default) composes the whole frame at the raster: the scene, every post-process layer, the UI and every post-scene pass, carried up to the size it is shown once it is finished. A smaller raster is then a smaller picture, so an effect whose output is a function of pixel size — a dither ladder, a posterize, a pixel grid — resolves at the pitch it was authored at, and the frame carries one composite resolution throughout. <code>"reconstruct"</code> carries the scene up to the size it is shown BEFORE the post-scene phases and composes everything from there, which is what a temporal upsampler needs: it refines the upscale it is handed and has nowhere to put the detail it recovers unless the image it accumulates into is the display's. Either reading is honoured identically whether a viewport panel or the swapchain shows the frame, so what an editor draws and what a shipped runtime draws are the same picture of the same scene.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — <code>string</code> — <code>"present"</code> or <code>"reconstruct"</code>.</li> </ul> <pre><code class="language-lua">renderer.setResolutionScaleMode("present") </code></pre> <h2 id="modules-renderer-setshadowcaching">modules/renderer/setShadowCaching</h2> <pre><code class="language-lua">setShadowCaching(enabled: boolean): () </code></pre> <p>Whether a shadow page that nothing changed is kept rather than drawn again. On by default: every light's pages — the sun's, and each spot, area and point light's — are drawn again only where a caster that changed reaches them. Off, every requested page is invalidated and drawn on every pass, which is what a shadow suspected of holding a stale image is compared against.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setShadowCaching(false) -- draw every requested shadow page, every frame </code></pre> <h2 id="modules-renderer-setshadowcastercutoff">modules/renderer/setShadowCasterCutoff</h2> <pre><code class="language-lua">setShadowCasterCutoff(cfg: { </code></pre> <p>Set the shadow-caster cutoff. An omitted field keeps its current value, so a call can adjust one threshold without restating the other. Both are measured against the camera the frame draws from rather than against each light, so one setting covers every light's pages, and a caster that stops casting is one whose shadow the viewer could not have resolved. <code>maxDistance</code> is measured to the near side of the caster's bounding sphere, so a large object keeps casting while any part of it is in range. 0 releases a threshold; releasing both draws the casters the frame drew before either was set.</p> <pre><code class="language-lua">renderer.setShadowCasterCutoff({ minRadiusPx = 2 }) renderer.setShadowCasterCutoff({ minRadiusPx = 1.5, maxDistance = 120 }) </code></pre> <h2 id="modules-renderer-setshadowconfig">modules/renderer/setShadowConfig</h2> <pre><code class="language-lua">setShadowConfig(cfg: { </code></pre> <p>Set the sun's shadow settings. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. Values are clamped: distance >= 0, fadeFraction [0, 1], softness [0, 1]. A <code>distance</code> of 0 shadows out to the camera's far plane, and a positive one caps it, which is what a scene bounding its shadow cost states; the sun's clipmap holds the levels that reach it. <code>punctualMipFloor</code> (a whole number, 0 to 5) caps how fine a spot, area or point light's map renders; it is refused outside that range, and every punctual light's map is drawn again under a new floor. <code>detailBias</code> ([-8, 8] levels) trades shadow detail for pages: each level coarser asks a quarter as many. <code>budgetMegabytes</code> (>= 1) bounds the depth the pages hold. Each takes <code>"default"</code> to return to the device's bias or the quality tier's budget, and a change to either sizes the pool again, which draws every page again.</p> <pre><code class="language-lua">renderer.setShadowConfig({ softness = 0.65, fadeFraction = 0.28 }) renderer.setShadowConfig({ distance = 240 }) renderer.setShadowConfig({ detailBias = 1, budgetMegabytes = 8 }) renderer.setShadowConfig({ detailBias = "default", budgetMegabytes = "default" }) </code></pre> <h2 id="modules-renderer-setshadowhero">modules/renderer/setShadowHero</h2> <pre><code class="language-lua">setShadowHero(entity: string, padding: number?): () </code></pre> <p>Give one caster a finer sun shadow, in a box around its world bounds.</p> <p>The sun's virtual shadow map resolves every receiver at the level its screen footprint selects, so a character standing in the view gets the texel density every surface at its distance gets. A registered hero widens its bounds by <code>padding</code> into a box, and every receiver inside it requests and samples a level <code>renderer.shadowHero().levelBias</code> levels finer — each level halving the world a texel covers — where that level's window holds it, crossing back to its footprint's level at the box's edge. Nothing else about the shadow changes: the same casters reach it through the same filter.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entity</code> <code>string</code> — The entity whose renderables the box is made around.</li> <li><code>padding</code> <code>number?</code> <em>(optional)</em> — How much room the box leaves around those bounds — for a pose that leaves the bind-pose box and for the filter that samples outside a silhouette. 1.0 holds them exactly.</li> </ul> <pre><code class="language-lua">local before = time.drawnFrameCount() renderer.setShadowHero(player.id) local h = renderer.shadowHero() while h.frame <= before do task.waitDrawnFrames(1) h = renderer.shadowHero() end print(("hero shadow: %d levels finer over %.1f units"):format(h.levelBias, h.worldExtent)) </code></pre> <h2 id="modules-renderer-setshadowproxy">modules/renderer/setShadowProxy</h2> <pre><code class="language-lua">setShadowProxy(mesh: string, proxy: string): () </code></pre> <p>Rasterize <code>proxy</code> in place of <code>mesh</code> in every shadow page. A shadow is a silhouette resolved at the resolution of a shadow map, so the triangles that carry a mesh's close-up detail write depth no reader can resolve — a decimated version of the shape, a level of its own LOD chain, or a hand-built hull casts the same shadow for a fraction of the geometry.</p> <p>The registration is keyed by MESH, so one call covers every instance of it — entities and GPU-driven populations alike — and a crowd sharing that mesh stays one draw. The proxy is placed by whatever places the caster, its instance's own transforms, so it stands where the caster stands, at the caster's scale.</p> <p>An entity caster keeps its own geometry where a stand-in could not be placed or deformed correctly: it is skinned (it rasterizes the post-skinned vertices written for its own mesh), it blends morph targets (whose deltas describe its own mesh and are read by vertex id), or its proxy would be placed by a different node of its model than the source mesh is. Either caster keeps it where the renderer holds no geometry under the proxy's guid. Each of those is counted in <code>renderer.shadowProxies()</code>.</p> <p>Nothing else in the scene draws a proxy, so this call is what brings it onto the GPU, and it raises where it cannot. A proxy already resident there is registered as it stands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mesh</code> <code>string</code> — The mesh a caster draws, as a guid or any mesh reference.</li> <li><code>proxy</code> <code>string</code> — The mesh it rasterizes into shadow pages instead.</li> </ul> <pre><code class="language-lua">renderer.setShadowProxy(statueMesh, statueHullMesh) print(renderer.shadowProxies().triangles, "vs", renderer.shadowProxies().sourceTriangles) </code></pre> <h2 id="modules-renderer-setskinnedbatching">modules/renderer/setSkinnedBatching</h2> <pre><code class="language-lua">setSkinnedBatching(enabled: boolean): () </code></pre> <p>Whether skinned instances holding one pose draw together. On by default: instances of one mesh wearing one material and posed alike read the same post-skinned vertices, so the camera's colour passes submit them as a single instanced draw. Off, each skinned instance draws on its own at its own slot in every pass that rasterizes it. The image is the same either way, which is what makes this the comparison a frame suspected of being formed by the batching is made against — <code>renderer.drawStats().draws</code> counts the difference and <code>renderer.skinningStats().poses</code> says how many distinct poses it holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setSkinnedBatching(false) -- a draw per skinned instance </code></pre> <h2 id="modules-renderer-setskinningposehold">modules/renderer/setSkinningPoseHold</h2> <pre><code class="language-lua">setSkinningPoseHold(enabled: boolean): () </code></pre> <p>Whether a pose the skinning pass already wrote is read as it stands. On by default: the pass produces an instance's vertices from its joint matrices, its node transforms, its blend weight and its blend model, so the slice holding a pose already holds what running the pass over those same inputs would write. A frame binding a pose whose slice still holds it reads the slice and dispatches nothing, and skinning costs what the frame's poses CHANGED — a paused clip, a skeleton nothing drives, a cast standing in one pose, each cost compute the frame the pose arrived and nothing after it. Off, every pose a frame binds is dispatched again, which is the comparison a frame suspected of reading a slice that no longer holds its pose is made against; the image is the same either way and <code>renderer.skinningStats()</code> counts the difference as <code>dispatches</code> against <code>held</code>. A mesh whose vertices a compute pass writes is dispatched every frame however this stands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setSkinningPoseHold(false) -- dispatch every pose, every frame </code></pre> <h2 id="modules-renderer-settexturebucketslack">modules/renderer/setTextureBucketSlack</h2> <pre><code class="language-lua">setTextureBucketSlack(bytes: number?): TextureBucketSlack </code></pre> <p>Bound the GPU memory texture-array buckets may hold beyond the textures in them. A bucket without room doubles while the total slack stays under this budget, and past it grows to the largest capacity whose slack the budget still holds, at least by what the arrival needs. A growth copies every layer the bucket held, so a large one that would gain few layers for what it copies opens another bucket of the same size instead. The default follows the device (a sixty-fourth of the system's RAM where the GPU draws from it, otherwise a sixteenth of the memory scale it reports; between 32 MiB and 512 MiB either way); <code>nil</code> restores it. <code>bytes</code> is a positive number of bytes, 1 or more — 0, a negative number or a non-finite one raises an error naming that range. A budget bounds the next growth; slack a bucket already holds stays until a compaction takes it. On the bindless backend no bucket exists, so the budget is recorded and reads back but bounds nothing. The textures themselves are bounded by <code>renderer.setTextureBudget</code>; read what the buckets hold with <code>renderer.materialTable()</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>number?</code> <em>(optional)</em> — <code>number?</code> — the slack budget in bytes, or nil for the device's</li> </ul> <pre><code class="language-lua">renderer.setTextureBucketSlack(16 * 1024 * 1024) renderer.setTextureBucketSlack(nil) -- the device's budget </code></pre> <h2 id="modules-renderer-settexturebudget">modules/renderer/setTextureBudget</h2> <pre><code class="language-lua">setTextureBudget(opts: TextureBudgetOpts?): TextureBudget </code></pre> <p>Bound the VRAM textures occupy, by keeping only the mip levels the frame is actually sampling once the textures outgrow the budget. Every device starts at its own budget and a bias of 0. Where the GPU draws from the system's RAM (every Android phone, an integrated GPU, a software rasterizer) and the system states how much, the budget is an eighth of that RAM; otherwise it is half the memory scale the adapter reports; between 256 MiB and 2 GiB either way, and held to 768 MiB on a medium-tier and 384 MiB on a low-tier device (<code>renderer.qualityTier()</code>). <code>renderer.textureStreaming()</code> names the rule (<code>budgetRule</code>) and the RAM it read (<code>deviceMemoryBytes</code>), and memory pressure narrows what the budget keeps (<code>renderer.memoryPressure()</code>). Pass <code>{ megabytes = 256 }</code> to override the budget for the process; <code>megabytes</code> is a positive number, 1 to 65536, and 0 or a negative number is an error. A field set to the string <code>"default"</code> returns that one value to the device's own, a field the table leaves out keeps the value it has, and a field holding anything else — a boolean, another string — is an error naming what it holds. <code>nil</code> restores the device's budget and bias together. The budget belongs to the device the world is played on.</p> <p>Below the budget every texture keeps its whole mip chain, whatever its surfaces cover. Mips stream down only while the whole chains of the streamable textures exceed the budget. Then each frame measures how many screen pixels ONE traversal of a texture's coordinate range covers on the surface that spans it widest, and asks for the mip level that serves that span one texel per pixel — the level the GPU picks from the fragment's own derivatives. A material with <code>uvScale = 8</code> lays eight copies of its texture across a surface, so each copy spans an eighth of the surface and asks for three levels coarser than the surface's own size would. A shader that declares <code>// @uv_space: world</code> advances its coordinate over world units rather than over the mesh's UVs, so how many copies a surface carries follows how large that surface is. The textures whose surfaces cover the fewest pixels give up levels until the set fits. Detail climbs one level per frame, from the image already on screen, so a surface the camera approaches sharpens rather than popping, and no texture is taken below the level whose longest side is 64 texels.</p> <p><code>bias</code> shifts every measurement by whole mip levels either way — negative for finer than the sampling implies, positive for coarser — over a look that wants a different trade than one texel per pixel. It is clamped to [-8, 8].</p> <p>The plan moves a texture whose demand the frame can measure: one at least 256 texels on its narrowest side, worn by a surface an entity draws. A texture a UI image, a post-process property or a render feature holds a view of stays whole, because nothing measures how much of the screen those cover.</p> <p>Which textures the budget governs follows the surfaces the frame draws. A texture whose asset still holds its bytes is enrolled the frame a measured surface wears it, whenever it loaded, because a level change reads the levels it needs back from the asset; when the last such surface goes it leaves the set whole, at the level it uploaded at, and a surface reaching it again takes it back up. A texture a script uploaded has its pixels on the device alone, so it holds no copy while it is at its whole chain; the first time the budget takes it coarser its chain is read back off the device before any level is freed, and it holds that copy (<code>renderer.textureMemory().streamSourceBytes</code>) while it stays coarser. Once a surface has worn it and gone it leaves the set whole and stays out for the rest of the process. <code>renderer.textureMemory().pinnedTextures</code> counts those, together with the textures whose asset could not be read back and the ones a UI image, a post-process property or a render feature holds a view of.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>TextureBudgetOpts?</code> <em>(optional)</em> — <code>{ megabytes: (number | "default")?, bias: (number | "default")? }?</code> — nil for the device's budget and bias</li> </ul> <pre><code class="language-lua">renderer.setTextureBudget({ megabytes = 128 }) renderer.setTextureBudget({ megabytes = 128, bias = -1 }) -- one level finer everywhere renderer.setTextureBudget({ megabytes = "default" }) -- the device's budget, the bias as it stands renderer.setTextureBudget() -- back to the device's budget and bias </code></pre> <h2 id="modules-renderer-settransmissionshadows">modules/renderer/setTransmissionShadows</h2> <pre><code class="language-lua">setTransmissionShadows(enabled: boolean): () </code></pre> <p>Let translucent casters tint the sunlight they block instead of blocking it outright. A shadow map holds one depth per texel and is compared as a yes-or-no test, so stained glass, water and thin fabric all project the same black silhouette a wall does. With this on, a caster whose material declares <code>transmission</code> draws into a light-space transmittance map instead, and the colour it lets through multiplies into the directional light reaching whatever stands behind it. Stacked casters compose. A surface whose material blends writes no depth shadow at all and throws its shadow through this map alone: a transparent or premultiplied blend lets <code>1 - alpha</code> of the light through untouched and its <code>transmission</code> of the rest tinted by its base colour, so red stained glass throws red light; a multiply blend lets its base colour through; an additive one blocks nothing. Opaque casters are unaffected, and a scene with no translucent caster allocates nothing and records no pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>enabled</code> <code>boolean</code> — <code>boolean</code></li> </ul> <pre><code class="language-lua">renderer.setTransmissionShadows(true) -- stained glass tints the floor </code></pre> <h2 id="modules-renderer-setupscalebackend">modules/renderer/setUpscaleBackend</h2> <pre><code class="language-lua">setUpscaleBackend(backend: string, quality: string?): string </code></pre> <p>Request a backend for the upscale seam — the step that brings a scene rendered below display resolution back up to it. <code>"blit"</code> stretches; <code>"dlss"</code> reconstructs from motion and history through NGX DLSS Super Resolution on an adapter that has it. The renderer resolves the request against its own capability and reports the outcome through <code>renderer.upscaleBackend()</code> — a request for <code>"dlss"</code> on an adapter <code>renderer.upscaleCapabilities()</code> omits it from resolves to <code>"blit"</code> there, answered truthfully rather than echoed back here. <code>quality</code> names the reconstruction quality mode a reconstructing backend derives its render scale from; the blit keeps whatever <code>renderer.setResolutionScale()</code> set and reads no value from <code>quality</code>. <code>@builtin::systems.temporalUpscale.temporalUpscale</code> is the content-facing surface built on this primitive — it carries the policy this raw setter carries none of: refusing <code>"dlss"</code> where unavailable, refusing <code>quality</code> alongside an explicit render scale, and keeping its own reconstruction from stacking with a reconstructing backend.</p> <p><strong>Parameters</strong></p> <ul> <li><code>backend</code> <code>string</code> — <code>string</code> — <code>"blit"</code> or <code>"dlss"</code>.</li> <li><code>quality</code> <code>string?</code> <em>(optional)</em> — <code>string?</code> — <code>"quality" | "balanced" | "performance" | "ultraPerformance"</code>, read only by a reconstructing backend.</li> </ul> <pre><code class="language-lua">renderer.setUpscaleBackend("dlss", "balanced") </code></pre> <h2 id="modules-renderer-setviewportsize">modules/renderer/setViewportSize</h2> <pre><code class="language-lua">setViewportSize(width: number, height: number): { width: number, height: number } </code></pre> <p>Draw at this many pixels for the rest of the task that asks. The engine's drawing surface is resized to it, and everything measured against that surface follows within a frame: <code>getViewportSize()</code>, <code>ui.screenSize()</code>, the layout every UI screen rebuilds from it, each camera's aspect, and what a capture taken in that task encodes. This is how one session checks a responsive layout at a second shape: a HUD written against <code>ui.screenSize()</code> is re-laid-out at the new size, so an anchored element is drawn where that shape puts it rather than scaled from where the boot size put it.</p> <p>The size is held for the task that asked for it (an <code>execute</code> call is one task), and when that task ends, however it ends, the surface goes back to the size it had before. So a check reads what it needs at the new shape inside the call that set it, and the window or browser canvas a person is looking at is handed back as it was. While several tasks hold a size the surface draws at the one asked for last, and when that task ends it draws at the size the task before it holds. Asked for outside any task, from a component's lifecycle, the size stands until another is asked for.</p> <p>Who honours the size depends on who owns the surface. A headless engine and a browser canvas own theirs and are resized exactly; a browser page that lays its canvas out at a new size meanwhile ends every hold, and the page's size stands. Where an OS window owns it, the window manager is asked and has the last word: a tiled or maximized window keeps the size it has. Read <code>renderer.surfaceSize()</code> on a later frame for what was realized.</p> <p>The new size is in force from the NEXT frame, so wait a frame before reading it back: <code>renderer.surfaceSize()</code> read straight after the call still reports the size the surface had.</p> <p>The logical UI space is normalised to about 1280 points wide, so a resize to a size of the same aspect moves <code>ui.pixelRatio()</code> and leaves <code>ui.screenSize()</code> where it was, while a resize that changes the aspect changes that shape too. Both resize the surface.</p> <p>A size larger than the device draws is refused naming the bound, which <code>renderer.maxViewportExtent()</code> reports.</p> <p><strong>Parameters</strong></p> <ul> <li><code>width</code> <code>number</code> — <code>number</code> — width in pixels, at least 1.</li> <li><code>height</code> <code>number</code> — <code>number</code> — height in pixels, at least 1.</li> </ul> <pre><code class="language-lua">renderer.setViewportSize(390, 844); task.waitFrames(2); print(ui.screenSize()) -- the phone shape, then the surface goes back when the call ends </code></pre> <h2 id="modules-renderer-shadercache">modules/renderer/shaderCache</h2> <pre><code class="language-lua">shaderCache(): ShaderCache? </code></pre> <p>What the shader compile gate's store of baked WGSL held, answered and wrote back. Compiling a <code>.shader</code> wraps the author's body in its framework, expands every <code>#include</code>, and hands the result to naga to parse and validate — work that is a pure function of the text going in, and that a launch would otherwise repeat for every shader it draws with. The store keeps that baked text across launches.</p> <p><code>restoredEntries</code> and <code>restoredBytes</code> are what a previous launch left that this one read back. <code>hits</code> counts the compiles answered out of the store and <code>misses</code> those that ran in full; <code>savedMs</code> sums what each hit's own recorded compile had cost, against <code>compileMs</code>, what the misses spent. <code>stale</code> counts the misses whose key was held but whose <code>#include</code>d modules had changed underneath — an entry records every module its expansion consumed, so editing a module invalidates exactly the shaders that included it and leaves the rest. The store also keeps each resolve gate pass a surface shader's resolve module earned, so a launch does not validate the same resolve text again: <code>verdictHits</code> counts the verdicts answered out of it and <code>verdictMisses</code> the ones whose gate ran, apart from the compile counts above.</p> <p><code>entries</code> and <code>bytes</code> are what the store now holds, <code>evictions</code> how many a write dropped to stay inside its bounds, and <code>saves</code> / <code>savedBytes</code> / <code>dirty</code> describe writing it back, deferred until a burst of compiles settles. <code>persistent</code> is false where a launch has nowhere to keep artifacts and <code>reason</code> says why; <code>location</code> is the file, or the browser store, they are kept in. <code>restoreState</code> is how the read of what a previous launch left has gone — <code>pending</code> while it is still out (a browser answers through a promise, so a launch reaches its first frames before it lands), <code>restored</code> once entries came back, <code>empty</code> when there were none to come back, <code>failed</code> when what was there could not be read, and <code>none</code> where a launch keeps nothing. A cold, missing or corrupt store leaves every shader compiling from source with identical output, and <code>lastError</code> then names what went wrong.</p> <pre><code class="language-lua">local c = renderer.shaderCache() print(("%d hits, %d misses, %.1f ms saved"):format(c.hits, c.misses, c.savedMs)) </code></pre> <h2 id="modules-renderer-shadercost">modules/renderer/shaderCost</h2> <pre><code class="language-lua">shaderCost(): { ShaderCost } </code></pre> <p>What each program has cost in pipeline builds, beside the compile gate's most recent word about it. <code>variants</code> is how many pipelines this engine has built for it — one per (target format, vertex layout, render-state key) permutation reached — and <code>buildMs</code> what those builds cost, both summed since engine start. A pipeline the driver's own store restored is not built and so is not counted, so a second launch on the same adapter reports less than the first. <code>status</code> is <code>compiled</code>, <code>failed</code> or <code>pending</code>, and <code>error</code> carries the compiler's message for a failure. Ordered by cost, most expensive first. <code>shader</code> is the key the program is registered under; <code>asset</code> names the <code>.shader</code> asset a <code>guid@checksum</code> key is a version of.</p> <pre><code class="language-lua">local top = renderer.shaderCost()[1]; print(top.asset or top.shader, top.variants, top.buildMs) </code></pre> <h2 id="modules-renderer-shadervariants">modules/renderer/shaderVariants</h2> <pre><code class="language-lua">shaderVariants(): { ShaderVariants } </code></pre> <p>Every shader that declares optional features, and the programs its materials have made it compile. Each row carries the features the shader declares, the base program it ships as, and one entry per variant with the features that variant holds — so the permutation count a scene's materials are spending is a number to read rather than something to infer from compile time. A shader whose variants reach <code>budget</code> compiles no more; the materials asking for further feature sets draw with the base program.</p> <pre><code class="language-lua">for _, s in renderer.shaderVariants() do print(s.shader, #s.variants, s.budget) end </code></pre> <h2 id="modules-renderer-shadingof">modules/renderer/shadingOf</h2> <pre><code class="language-lua">shadingOf(subject: string | { [string]: any }): ShadingReading </code></pre> <p>What the renderer is shading ONE subject with, taken from the document the renderer publishes — the call a system holding a handle makes to find out whether what reaches the screen is its own material or the magenta placeholder standing in for it, without reading the engine log. <code>subject</code> is an entity that draws or the registry key of a material. <code>state</code> reads <code>itsMaterial</code> where the renderer bound the program the material names, <code>errorMaterial</code> where it bound the placeholder instead, <code>stalePipeline</code> where the pipeline drawing it was built before that program's most recent compile, <code>nothingBound</code> where the renderer resolved no pipeline for it, <code>pending</code> where this call is the one that armed per-draw recording and the frame after it publishes, and <code>unknown</code> where the renderer holds a resolution under no such subject. A fault state carries the renderer's own <code>reason</code> from the closed set <code>renderer.drawDiagnostics()</code> names — plus <code>materialNotPrepared</code>, which a material subject reads where the renderer prepared nothing under that key — the compiler's <code>detail</code>, the <code>program</code> the material asked for and the <code>bound</code> one; <code>means</code> states the reading in a sentence. A material subject answers from the renderables drawing with it, and from the renderer's record for the material itself where a draw registered against the material carries no row of its own; a subject that several renderables draw answers with a refused one wherever there is one. The reading follows the renderer, so a program that compiles on a later edit puts the subject back on <code>itsMaterial</code> from the frame the renderer draws it with again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>subject</code> <code>string | { [string]: any }</code> — The entity — a proxy from <code>entity(...)</code> or an entity-id string — or the material, as its registry key or the <code>MaterialHandle</code> <code>renderer.material.create</code> returned.</li> </ul> <pre><code class="language-lua">local s = renderer.shadingOf(emitter); if s.state ~= "itsMaterial" then print(s.means) end </code></pre> <h2 id="modules-renderer-shadowcachestats">modules/renderer/shadowCacheStats</h2> <pre><code class="language-lua">shadowCacheStats(): { </code></pre> <p>What the main camera's newest counted run did with the shadow pages it asked for, in pages, by the kind of light that owns them. A page is drawn again only when something it holds changed: its light moved, a caster over it moved or appeared or vanished, a caster's geometry or material changed, or the caster changed pose or moved the nodes its parts are placed by. Anything else keeps the depth already in the pool, so a scene that stops moving reads <code>rendered</code> 0 while its pages stay <code>cached</code>. A caster moving on one side of a point light dirties only the pages of the faces it reaches. A mesh whose vertices a compute pass writes — a population, or a mesh built from a compute buffer — re-renders the pages it stands over every frame. <code>spot</code> counts spot and area lights.</p> <p>These are totals over every light of a kind. <code>renderer.shadowViews()</code> is the same run one light at a time.</p> <pre><code class="language-lua">local s = renderer.shadowCacheStats() print(("shadow pages: %d drawn, %d cached"):format(s.rendered, s.cached)) </code></pre> <h2 id="modules-renderer-shadowcaching">modules/renderer/shadowCaching</h2> <pre><code class="language-lua">shadowCaching(): boolean </code></pre> <p>Whether a shadow page may keep the depth it already holds.</p> <h2 id="modules-renderer-shadowcastercutoff">modules/renderer/shadowCasterCutoff</h2> <pre><code class="language-lua">shadowCasterCutoff(): ShadowCasterCutoff </code></pre> <p>How small, and how far away, a caster may get before it stops writing depth into any shadow page. A shadow page rasterizes a caster's whole triangle count whatever the shadow it produces ends up covering, so an object the viewer resolves a fraction of a pixel of, and one past the range the scene cares about, each cost a full depth draw per page it reaches for detail nothing reads. Both thresholds are 0 — released — until something sets them.</p> <pre><code class="language-lua">local c = renderer.shadowCasterCutoff(); print(c.minRadiusPx, c.maxDistance) </code></pre> <h2 id="modules-renderer-shadowconfig">modules/renderer/shadowConfig</h2> <pre><code class="language-lua">shadowConfig(): ShadowConfig </code></pre> <p>The sun's shadow settings now in force. <code>distance</code> bounds how far from the camera the sun shadows (the world's <code>render.shadow_distance</code> until <code>renderer.setShadowConfig</code> sets it); <code>fadeFraction</code> and <code>softness</code> shape how the shadow is sampled. The sun's virtual shadow map follows each receiver's screen footprint, so its resolution follows the view. <code>punctualMipFloor</code> is the finest mip a spot, area or point light's map renders. <code>detailBias</code> and <code>budgetMegabytes</code> are what a caller stated, or <code>"default"</code>; what the renderer sized the pages with is <code>renderer.shadowSizing()</code>. The table is one <code>renderer.setShadowConfig</code> takes back.</p> <pre><code class="language-lua">print(renderer.shadowConfig().distance) </code></pre> <h2 id="modules-renderer-shadowhero">modules/renderer/shadowHero</h2> <pre><code class="language-lua">shadowHero(): ShadowHeroReport </code></pre> <p>The registered hero caster and what the last drawn frame's box was. A frame that made no box says why in <code>decline</code>.</p> <p><code>entity</code> is the live registration; everything else is the box of the drawn frame <code>frame</code> names, and stands until the renderer draws another. So a reading taken straight after <code>setShadowHero</code> or <code>clearShadowHero</code> describes the registration before the call: read the drawn frame count as the change is made, and take the report once its <code>frame</code> is past it.</p> <pre><code class="language-lua">local h = renderer.shadowHero() print(h.active, h.levelBias, h.decline) local before = time.drawnFrameCount() renderer.setShadowHero(player.id) task.waitDrawnFrames(1) print(renderer.shadowHero().frame > before) </code></pre> <h2 id="modules-renderer-shadowmemory">modules/renderer/shadowMemory</h2> <pre><code class="language-lua">shadowMemory(): { </code></pre> <p>How much GPU memory the virtual shadow map's pools hold right now, in bytes. One pool serves every light — the sun, and every spot, area and point light that casts. <code>pool</code> is the depth page pool with the sun's static cache laid out as it, held while any light casts; <code>transmittance</code> is the transmittance pool, held while a translucent caster casts into it (<code>renderer.renderTargets()</code> names them <code>shadow_page_pool</code>, <code>shadow_static_cache</code> and <code>shadow_transmittance_pool</code>). Both read 0 while released.</p> <pre><code class="language-lua">local m = renderer.shadowMemory() print(("shadows: %.1f MiB"):format(m.total / 1024 / 1024)) </code></pre> <h2 id="modules-renderer-shadowpagebreakdown">modules/renderer/shadowPageBreakdown</h2> <pre><code class="language-lua">shadowPageBreakdown(): { { [string]: any } } </code></pre> <p>The breakdowns <code>renderer.watchShadowPages</code> gathered since the last call, oldest first, one per run of the main camera. <code>levels</code> has a row per clipmap level: <code>texelWorld</code> (world units per texel), the pages it <code>requested</code>, held (<code>resident</code>), drew (<code>rendered</code>) and had dirtied by the frame's caster changes (<code>invalidated</code>), the pages those changes reach in its window (<code>reached</code>) and the smallest size a change projects to there (<code>smallestRecordTexels</code>). <code>records</code> lists each caster change with its light-space <code>sizeWorld</code> and, per level, the <code>texels</code> it covers and the <code>pages</code> it reaches. <code>cull</code> is what the page cull walked (<code>slotsWalked</code> caster slots, <code>ncMembers</code> casters drawn from their own vertex buffers) and kept (<code>pairs</code>, casters <code>tested</code>, <code>admitted</code> and <code>cutOff</code>, <code>chunks</code>, <code>visible</code> clusters, <code>ncPairs</code>, <code>batchPages</code>). <code>pages</code> has a row per batch page: its <code>level</code>, the <code>casters</code> it drew and how many changed this frame (<code>dynamicCasters</code>), its <code>chunks</code>, <code>clusters</code> and <code>triangles</code> with the changed casters' shares, and its pairs of casters drawn from their own vertex buffers (<code>ncPairs</code>, <code>dynamicNcPairs</code>).</p> <pre><code class="language-lua">renderer.watchShadowPages(30) task.wait(1) for _, run in renderer.shadowPageBreakdown() do print(run.frame, #run.pages) end </code></pre> <h2 id="modules-renderer-shadowpagestats">modules/renderer/shadowPageStats</h2> <pre><code class="language-lua">shadowPageStats(): { </code></pre> <p>What the sun's virtual shadow map did on the main camera's newest run the GPU counted. The sun's shadow lives in pages around the camera; a run marks the pages the receivers on screen ask for and draws the ones that are missing or that a moved caster dirtied. A still camera over a still scene runs nothing, so <code>ran</code> reads false and <code>runs</code> stands still while <code>frames</code> climbs. The counts come back from the GPU a few frames after the run, so <code>frame</code> names the frame they describe. <code>proxyPairs</code> counts the (caster, page) pairs a shadow proxy stood in for, <code>proxyTriangles</code> the triangles those pairs drew from the proxy and <code>sourceTriangles</code> what the casters' own meshes would have drawn there. <code>castersDropped</code> counts the casters drawn outside the cluster lists (a deforming mesh, a population whose mesh has no cluster set) that the newest frame could not list, which cast no shadow that frame.</p> <pre><code class="language-lua">local s = renderer.shadowPageStats() if s then print(("%d pages asked for, %d drawn"):format(s.requested, s.rendered)) end </code></pre> <h2 id="modules-renderer-shadowproxies">modules/renderer/shadowProxies</h2> <pre><code class="language-lua">shadowProxies(): ShadowProxyReport </code></pre> <p>The shadow proxies in force and what the shadow pages did with them. <code>triangles</code> and <code>sourceTriangles</code> are what the main camera's newest counted run submitted through proxies and what the same (caster, page) pairs would have submitted from the source meshes — the before/after of every registration, equal while nothing is proxied.</p> <pre><code class="language-lua">local p = renderer.shadowProxies() print(("shadow triangles: %d of %d"):format(p.triangles, p.sourceTriangles)) </code></pre> <h2 id="modules-renderer-shadowsizing">modules/renderer/shadowSizing</h2> <pre><code class="language-lua">shadowSizing(): ShadowSizing </code></pre> <p>What the sun's virtual shadow map was sized with. Its pool of pages holds about twice the pages a frame at the render resolution asks for, within the depth budget (the quality tier's: 64, 12 and 8 MiB at high, medium and low, or the one <code>renderer.setShadowConfig</code> states). It is sized again only when the render resolution, the detail bias or the budget changes. A run that finds no slot for a page it asked for raises <code>overflowBias</code> by a level, up to 4, until the pool is sized again.</p> <pre><code class="language-lua">print(renderer.shadowSizing().depthMegabytes) </code></pre> <h2 id="modules-renderer-shadowviews">modules/renderer/shadowViews</h2> <pre><code class="language-lua">shadowViews(): ShadowViewReport? </code></pre> <p>Every shadow-casting spot, area and point light's virtual shadow map on the main camera's newest counted run, one row each.</p> <p><code>renderer.shadowCacheStats()</code> and <code>renderer.shadowPageStats()</code> sum these counts over every light; this is the same run one light at a time. The sun's pages are reported by <code>renderer.shadowPageStats()</code>.</p> <p>Each row names its light and says what its pages did: how many its receivers asked for, how many it holds, how many were drawn, dirtied, starved or left for a later run, and what the page cull did with the casters it walked for the light. <code>camera</code> carries the main camera's share of the instanced-compaction counters.</p> <p>The counts come back from the GPU a few frames after the run, and <code>frame</code> names the frame they belong to. A light that stops casting leaves <code>views</code>.</p> <p>The frame names its lights only while something is reading them, so this call asks the frames after it to publish theirs and waits out the first one. Nil on an engine that renders no frame at all.</p> <pre><code class="language-lua">local report = renderer.shadowViews() for _, view in report.views do print(("%s %d (%s): %d of %d pages resident, %d drawn"):format( view.role, view.index, view.light, view.pagesResident, view.pagesRequested, view.pagesRendered)) end </code></pre> <h2 id="modules-renderer-signalmemorypressure">modules/renderer/signalMemoryPressure</h2> <pre><code class="language-lua">signalMemoryPressure(level: string): () </code></pre> <p>Report memory pressure the way the system does: the engine releases what <code>level</code> releases on its next frame, and <code>engine.onMemoryPressure</code> subscribers hear it with source <code>"script"</code>. Content about to need a lot of memory (a large scene load, a big decode) frees room ahead of it with this, and <code>"normal"</code> lifts a level a script put in force. <code>level</code> is <code>"normal"</code>, <code>"moderate"</code>, <code>"low"</code> or <code>"critical"</code>; anything else is an error naming the four. Read the outcome with <code>renderer.memoryPressure()</code> once a frame has passed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>level</code> <code>string</code> — <code>"normal"</code>, <code>"moderate"</code>, <code>"low"</code> or <code>"critical"</code></li> </ul> <pre><code class="language-lua">renderer.signalMemoryPressure("moderate") -- free unsampled mips before a big load </code></pre> <h2 id="modules-renderer-skinnedbatching">modules/renderer/skinnedBatching</h2> <pre><code class="language-lua">skinnedBatching(): boolean </code></pre> <p>Whether skinned instances holding one pose draw together.</p> <h2 id="modules-renderer-skinningposehold">modules/renderer/skinningPoseHold</h2> <pre><code class="language-lua">skinningPoseHold(): boolean </code></pre> <p>Whether a pose already written into its slice skips its dispatch.</p> <h2 id="modules-renderer-skinningstats">modules/renderer/skinningStats</h2> <pre><code class="language-lua">skinningStats(): { </code></pre> <p>What the last frame's skinned instances cost. A skinned instance is posed by a compute pass that writes its vertices into a shared pool, and instances holding the same pose read one slice of that pool and the single dispatch that fills it. <code>instances</code> is how many were posed, <code>poses</code> how many distinct poses they held, and <code>dispatches</code> how many dispatches those poses cost this frame — so a crowd whose members move together costs what its poses cost rather than what its head count does, while members at different animation times each hold their own pose and pay for it.</p> <p><code>held</code> is how many of the frame's poses cost no dispatch at all. The pass produces a slice from what the pose is made of, so a slice an earlier frame filled already holds what running it again would write, and a pose still wearing that slice is read as it stands. Skinning is paid for by the poses that CHANGED: a cast standing still reads <code>dispatches</code> 0 beside a <code>held</code> equal to its <code>poses</code>, and the two add up to <code>poses</code> in any frame. A slice counts as filled once a render carrying its dispatch has been drawn: the frame's own view, or a render of the scene into a target, such as a camera drawing into its render texture. A frame whose view the renderer declines (a headless one nothing is consuming, a browser behind its GPU) and that renders the scene into no target records nothing, so a pose that arrives over such frames is counted in <code>dispatches</code> on each of them and reads as <code>held</code> from the frame after the first one that records it. A wait for a crowd to settle is therefore counted in drawn frames (<code>task.waitDrawnFrames</code>).</p> <p><code>reusedSlices</code> is how many of the frame's poses took a slice the pool already held — one a retired pose gave back, or one a pose nothing has asked for this frame was holding — rather than one cut from pool the engine had never used. A scene whose poses keep changing reads a non-zero count beside a <code>poolBytes</code> that stays where it was.</p> <p><code>liveBytes</code> is what the slices holding this frame's poses occupy, against <code>unsharedBytes</code> — what the same instances would occupy with a slice each. <code>poolBytes</code> is what the pool holds; a previous-position buffer of the same size rides alongside it so skinned deformation reaches motion vectors.</p> <p>The same pass draws a soft body's model through the body's elements (its cage). <code>cages</code> is how many soft bodies it draws that way, <code>cageBytes</code> what their buffers occupy, and <code>cageUploads</code> how many of them wrote their elements' maps to the GPU this frame: a soft body at rest writes nothing, and a removed one takes its cage with it.</p> <pre><code class="language-lua">local s = renderer.skinningStats() print(("%d instances over %d poses"):format(s.instances, s.poses)) print(("%d poses dispatched, %d read as they stood"):format(s.dispatches, s.held)) print(("skinned vertex storage: %d B of an unshared %d B"):format(s.liveBytes, s.unsharedBytes)) </code></pre> <h2 id="components">modules/renderer/splat.components</h2> <pre><code class="language-lua">splat.components(bytes: any, convention: string?): (SplatComponents?, string?) </code></pre> <p>Decode a Gaussian splat capture — a Niantic <code>.spz</code> (gzipped or raw) or a 3DGS <code>.ply</code> — into the GPU-ready byte pools a render feature uploads. <code>records</code> is the packed splat array at <code>recordBytes</code> per splat (position, log scale, quaternion, DC colour + opacity); <code>sh</code> is the quantized higher-order spherical-harmonics pool at <code>shStrideWords</code> u32 words per splat, empty at degree 0. A pure decode (no GPU work): upload the pools with <code>shaderRef:createBuffer</code> + <code>buf:writeBytes</code> and draw them with a <code>kind = "splat"</code>, <code>channel = "gaussian"</code> pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>any</code> <em>(optional)</em> — Capture bytes — <code>.spz</code> or <code>.ply</code>, as a <code>buffer</code> or a binary string.</li> <li><code>convention</code> <code>string?</code> <em>(optional)</em> — Source axis convention: <code>"rightDownFront"</code> (the default, what COLMAP-trained captures use) or <code>"engineNative"</code> for a capture already in engine space.</li> </ul> <pre><code class="language-lua">local c = renderer.splat.components(vfs.read("/source/captures/ceramic.spz")) </code></pre> <h2 id="modules-renderer-surfacesize">modules/renderer/surfaceSize</h2> <pre><code class="language-lua">surfaceSize(): { width: number, height: number } </code></pre> <p>The whole drawing surface in pixels — the window, the browser canvas or the headless framebuffer. This is the size <code>renderer.setViewportSize</code> sets and the size every UI screen is laid out over, so it is what says whether a resize was realized, on every layout including an editor one whose viewport panel holds a smaller rect than the window. <code>{ width = 0, height = 0 }</code> before the first frame has drawn.</p> <pre><code class="language-lua">local s = renderer.surfaceSize() </code></pre> <h2 id="held">modules/renderer/temporal.held</h2> <pre><code class="language-lua">temporal.held(): boolean </code></pre> <p>Whether a hold is pinning the per-frame clock right now.</p> <pre><code class="language-lua">if renderer.temporal.held() then print("frame is pinned") end </code></pre> <h2 id="hold">modules/renderer/temporal.hold</h2> <pre><code class="language-lua">temporal.hold(at: number?, options: TemporalHoldOptions?): () -> () </code></pre> <p>Pin the clock every per-frame effect draws itself against, and return the release. While the hold stands, <code>renderer.temporal.now</code> answers <code>at</code> instead of the running clock, so film grain and every other field redrawn each frame is redrawn as the same field. Two renders taken under holds at the same instant therefore agree pixel for pixel wherever the scene itself has not moved, which is what makes one frame comparable with another. Holds nest: the innermost names the instant, and the clock runs again once the last release is called. Each release takes its own hold off the stack whatever order the releases come in, so two callers holding at once — two captures in flight together — each end their own hold and the clock runs again when both have. <code>exclusive</code> takes the clock for the <code>owner</code> key the call states: while that hold stands, a hold is admitted only when it states the same key, and every other one is refused with an error naming the key and the instant holding it. That is what lets one caller wind the clock to the second it means to photograph and keep it there while another agent drives the same engine. The key is what an owner presents to take a nested hold of its own, and what <code>renderer.temporal.release</code> hands the clock back by. A capture taken while the hold stands renders at the held instant, and a <code>deterministic</code> capture states that key as its own <code>owner</code> option to be admitted under the hold and photograph the instant it pinned.</p> <p><strong>Parameters</strong></p> <ul> <li><code>at</code> <code>number?</code> <em>(optional)</em> — The instant to pin the clock at, in seconds. Two holds that state the same instant produce the same field; the default 0 is that shared instant.</li> <li><code>options</code> <code>TemporalHoldOptions?</code> <em>(optional)</em> — <code>owner</code> is the key this hold is taken under, and an exclusive hold states one. A hold that states no key is labelled with the caller the call is attributed to, which names the calling session where the transport attested one and the ZeroMind account otherwise. <code>exclusive</code> takes the clock for the stated key until the hold is released.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that releases this hold. Calling it twice releases once.</p> <pre><code class="language-lua">local release = renderer.temporal.hold() ; local picture = tools.use("capture", "fromPosition", { position = { 0, 2, 8 } }) ; release() local release = renderer.temporal.hold(46.0, { exclusive = true, owner = "stage-air" }) </code></pre> <h2 id="now">modules/renderer/temporal.now</h2> <pre><code class="language-lua">temporal.now(): number </code></pre> <p>The instant a per-frame effect should draw itself at: the innermost hold's instant while one stands, and seconds since boot otherwise. A system that redraws a field every frame reads this rather than the running clock, and a capture asking for a repeatable frame then gets one.</p> <pre><code class="language-lua">local params = { grainTime = renderer.temporal.now() } </code></pre> <h2 id="onchange">modules/renderer/temporal.onChange</h2> <pre><code class="language-lua">temporal.onChange(listener: (number) -> ()): () -> () </code></pre> <p>Register a listener called with the pinned instant whenever it changes — a hold taken, a hold released — and return the unsubscribe. A system whose shader reads the clock out of a GPU buffer registers here, so the buffer carries the pinned instant before the frame that hold was taken on is drawn rather than a frame later.</p> <p><strong>Parameters</strong></p> <ul> <li><code>listener</code> <code>(number) -> ()</code> — Called with the instant now in force, in seconds.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that removes this listener.</p> <pre><code class="language-lua">local stop = renderer.temporal.onChange(function(t) print("temporal clock", t) end) </code></pre> <h2 id="owner">modules/renderer/temporal.owner</h2> <pre><code class="language-lua">temporal.owner(): { id: string?, name: string?, at: number, exclusive: boolean }? </code></pre> <p>The hold naming the instant the clock answers right now: who took it, what instant it pinned, and whether it took the clock exclusively. Several agents drive one engine at once and a hold any of them takes moves the clock every registered field is redrawn against, so this is how a caller sees that another agent holds it before its own instant is quietly replaced — and, when <code>exclusive</code> is true, <code>id</code> is the key a hold of its own states to be admitted, and the key <code>renderer.temporal.release</code> hands the clock back by. <code>id</code> and <code>name</code> are nil for a hold that stated no key and that the engine attributes to no agent.</p> <pre><code class="language-lua">local who = renderer.temporal.owner() if who ~= nil and who.exclusive then print(who.name, "holds the clock at", who.at) end </code></pre> <h2 id="release">modules/renderer/temporal.release</h2> <pre><code class="language-lua">temporal.release(owner: string): number </code></pre> <p>Hand the clock back by the key its holds were taken under, and report how many came off. A hold stands until its release is called, and the release is a closure the call that took the hold holds: a caller that takes a hold in one call and comes back in another, and a task that ends between the two, both leave the clock pinned with nobody holding a release for it. Naming the key is how the clock runs again, and how a caller refused by an exclusive hold takes one over.</p> <p><strong>Parameters</strong></p> <ul> <li><code>owner</code> <code>string</code> — The key the holds to release were taken under — what <code>owner</code> stated when they were taken, which <code>renderer.temporal.owner</code> reports.</li> </ul> <pre><code class="language-lua">renderer.temporal.release("stage-air") </code></pre> <h2 id="capture">modules/renderer/texture.capture</h2> <pre><code class="language-lua">texture.capture(texture: string | { [string]: any } | AssetRef): string </code></pre> <p>Request a CPU readback of the GPU texture <code>texture</code> names (e.g. a camera's rendered output). Returns a result key to pass to a TextureCpuHandle's <code>:encode()</code> once the readback completes. Takes every form that names a texture — the <code>TextureHandle</code> <code>create</code> returned, the guid <code>renderer.texture.list</code> hands out, a <code>TextureCpuHandle</code> or a texture <code>AssetRef</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture to read back — a <code>TextureHandle</code>, a guid, a <code>TextureCpuHandle</code> or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local key = renderer.texture.capture(cameraTarget) </code></pre> <h2 id="cpucreate">modules/renderer/texture.cpuCreate</h2> <pre><code class="language-lua">texture.cpuCreate(width: number, height: number, fill: any?): TextureCpuHandle </code></pre> <p>Allocate a blank CPU image (RGBA8) filled with a solid colour and return a <code>TextureCpuHandle</code>. Compose into it with <code>canvas:blit(src, x, y, w, h)</code>, then <code>canvas:encodeJpeg()</code> / <code>:encodePng()</code> for the bytes; <code>:unload()</code> drops it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>width</code> <code>number</code> — number Canvas width in pixels.</li> <li><code>height</code> <code>number</code> — number Canvas height in pixels.</li> <li><code>fill</code> <code>any?</code> <em>(optional)</em> — Optional <code>{ r, g, b, a }</code> (0-255) solid fill; defaults to opaque white.</li> </ul> <pre><code class="language-lua">local c = renderer.texture.cpuCreate(1024, 576, { 255, 255, 255, 255 }) </code></pre> <h2 id="cpufrombytes">modules/renderer/texture.cpuFromBytes</h2> <pre><code class="language-lua">texture.cpuFromBytes(bytes: buffer | string, encodeOpts: any?): TextureCpuHandle </code></pre> <p>Load engine-native ZTEX bytes — or an encoded image (png / jpg / webp) — into the CPU store under a fresh guid and answer the CPU handle, for pixels that come from somewhere other than a texture asset: a <code>data.ztex</code> read as a file, a payload held in memory. The pixels stay at the format they were encoded in. DEFAULT: <code>handle:unload()</code> once done with them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — The ZTEX or image bytes.</li> <li><code>encodeOpts</code> <code>any?</code> <em>(optional)</em> — <code>{ colorSpace?, compression?, generateMipmaps?, maxDimension?, filter?, textureType?, alpha?, alphaIsTransparency? }</code> — the opts <code>renderer.texture.encodeFromImage</code> takes — applied when the bytes are an encoded image and need the engine-native encode. <code>maxDimension</code> bounds ZTEX bytes too: a payload that stores a mip chain is read from the first level whose longest side is within it, that level alone, so a thumbnail of a 4096² texture decodes a level near the thumbnail's size.</li> </ul> <pre><code class="language-lua">local cpu = renderer.texture.cpuFromBytes(assert(vfs.read(path))); local png = cpu:encodePng(); cpu:unload() local thumb = renderer.texture.cpuFromBytes(assert(vfs.read(texPath .. "/data.ztex")), { maxDimension = 256 }) </code></pre> <h2 id="create">modules/renderer/texture.create</h2> <pre><code class="language-lua">texture.create(src: any, guid: string?): TextureHandle </code></pre> <p>Create (or fetch) a GPU texture resource and return its <code>TextureHandle</code>. <code>src</code>: a <code>TextureCpuHandle</code> from <code>texRef:load()</code> (CPU→GPU under the asset's guid, idempotent); raw pixels <code>{rgba, width, height, srgb?, format?}</code> (a flat width<em>height</em>4 byte payload, 0-255, row-major, top-to-bottom, RGBA — a <code>buffer</code>, a binary string, or a number array; <code>format = "rgba16f"</code> uploads an HDR texture instead, where <code>rgba</code> carries float channel values); a <code>TextureHandle</code> (returned as-is); or render-target dimensions <code>{width, height, name?, format?}</code> with no pixel source — an empty GPU texture a render pass writes into (camera output, UI surface) and that samples like any other texture. <code>format</code> names the colour format the target is allocated in, and the passes drawing into it are built for that format: <code>"rgba8unorm"</code> / <code>"bgra8unorm"</code> (the two eight-bit channel orders, either of which a surface may carry), <code>"rgba16f"</code> / <code>"rgba32f"</code>, <code>"rg16f"</code> / <code>"rg32f"</code>, <code>"r16f"</code> / <code>"r32f"</code>. Each also answers to its spelled-out width (<code>"rgba16float"</code>, <code>"r32float"</code>, and so on), in any case. Omit it and a <code>screen</code> target takes the scene's own format (<code>"rgba16f"</code>) — a screen-sized target holds a picture of the scene, and that is the format the scene is held in: linear light with headroom above the display's white and no 256-step floor under a dark gradient. A target that is not screen-sized and names no format takes the surface's. <code>"rgba8"</code> is what a mask, a weight or an occlusion term asks for, where 0..1 is the whole range; hand one back over <code>@scene.color</code> and the frame loses every highlight above white and bands every gradient, and the engine says so in the log. A float format carries what eight bits quantize — positions, velocities, HDR. Any other <code>format</code> raises an error naming every name that works, so a target is allocated in the format it was asked for or not at all. A render target takes <code>filter</code> the way raw pixels do: <code>"nearest"</code> keeps its own pixels square wherever something draws it larger than it is — a viewport widget, a magnified capture — which is what an image whose pixels ARE the subject needs, since a 64x32 panel holds no detail between its pixels to interpolate; <code>"linear"</code> (the default) smooths between them. It also takes <code>screen</code> (the engine keeps it the size of the image being drawn), <code>screenScale</code> (the fraction of that size it takes) and <code>screenSpace</code> (<code>"scene"</code>, the default, or <code>"composite"</code> — the image the post-scene phases draw into, which is the display's own resolution while the renderer presents the viewport itself and the scene's size while a UI viewport panel owns the presentation). A scene-space target is resized for every render target drawn and cleared before an offscreen one; a composite-space target follows the presented frame alone, which is what lets a pass keep an accumulation in it. One scene-space <code>screen</code> target is therefore one resource every render target draws through in turn, so its guid holds the last one's image at the last one's size, and a value read back from it belongs to whichever render target was drawn last. A <code>screen</code> target a live Camera draws into is that camera's output instead: the frame's other render targets neither resize nor clear it, so what the camera drew there stays for as long as the camera does. A reading that has to be the viewport's own comes from <code>screenSpace = "composite"</code>, or from a target created without <code>screen</code>. <code>history = true</code> makes a history target instead, the target an accumulation across frames lives in: it is screen-sized and keeps one pair of images per view a frame draws (the viewport, every render-to-texture camera, every capture). For the length of each view's render, the handle's <code>guid</code> names the image that render writes and <code>previous.guid</code> the image that view's previous render wrote, so a pass binds the two like any other targets and reads its own view's history wherever it runs; no other view resizes, clears or overwrites them. Before each render of a view the engine writes the compute buffer named by the handle's <code>state</code> (one <code>vec4<f32></code>, bound through a pass's <code>buffers</code>): <code>x</code> is 1 when <code>previous</code> holds that view's previous image and 0 when it does not (the view's first render, its first after a cut or a change of size, or a render of the view that ran without the target, and then <code>previous</code> reads transparent black), <code>y</code> how many renders in a row have carried the history, <code>zw</code> the render's sub-pixel projection offset in pixels (<code>renderer.setProjectionJitter</code>). A view's images are freed when the view stops drawing, and all of them with the handle. NEVER takes an AssetRef — load the CPU first.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>any</code> <em>(optional)</em> — A TextureCpuHandle, raw pixels, a TextureHandle, or render-target dimensions.</li> <li><code>guid</code> <code>string?</code> <em>(optional)</em> — Optional v4 guid for a NEW runtime texture — the asset identity the texture is filed under, which a material's texture slot resolves through. Minted when absent. Ignored for the CPU-handle and render-target paths.</li> </ul> <pre><code class="language-lua">local gpu = renderer.texture.create(texRef:load()) local gpu = renderer.texture.create({ rgba = pixels, width = 16, height = 16 }) local px = buffer.create(16 * 16 * 4); local gpu = renderer.texture.create({ rgba = px, width = 16, height = 16 }) local rt = renderer.texture.create({ width = 512, height = 256, name = "panel_rt" }) local hdr = renderer.texture.create({ width = 512, height = 256, name = "cam_rt", format = "rgba16f" }) local led = renderer.texture.create({ width = 64, height = 32, name = "panel", filter = "nearest" }) local hist = renderer.texture.create({ width = 1, height = 1, history = true, name = "accum" }) </code></pre> <h2 id="createfromasset">modules/renderer/texture.createFromAsset</h2> <pre><code class="language-lua">texture.createFromAsset( </code></pre> <p>Put a <code>.texture</code> asset on the GPU under its own guid and answer its handle at once. The asset's bytes are decoded off the frame and the texture lands on the device when the decode finishes, a frame or more later: a material naming the guid draws the shader's default for that slot until then and rebinds when it arrives, and <code>renderer.texture.isResident</code> reports the arrival. The decoded pixels are dropped once uploaded unless <code>keepCpu</code> holds them in the CPU store for <code>textureRef:load()</code>-style reads. An asset the device already holds is answered from the shape the device reports, without reading the asset's bytes and without a second decode. An asset whose bytes have not arrived here yet, asked for by code that cannot wait for them (<code>onPropertyChanged</code>, a metamethod, any place <code>coroutine.isyieldable()</code> is false), is answered a handle sized 0x0 at once: a task reads the bytes and decodes them, the texture lands under the asset's guid as above, and the handle takes the decoded size when the decode is scheduled.</p> <pre><code class="language-lua">local h = renderer.texture.createFromAsset(texRef) -- bind h.guid on a material; it draws once resident </code></pre> <h2 id="decode">modules/renderer/texture.decode</h2> <pre><code class="language-lua">texture.decode(bytes: buffer | string, opts: { straightAlpha: boolean? }?): (any, any, any, any) </code></pre> <p>Decode a texture payload to its pixel buffer. Takes the two shapes the renderer's own texture loader takes, told apart by their leading bytes:</p> <ul> <li>an engine-native <code>ZTEX</code> payload — handed back at the texel format the payload was written in, so a height field read back here keeps every bit it was authored with. A <code>ZTEX</code> holding block-compressed or verbatim source-image levels decodes to <code>"rgba8"</code>.</li> <li>source image bytes — png, jpeg, gif or webp, straight off disk or out of a <code>capture</code> — decoded to <code>"rgba8"</code> at whatever colour type, bit depth or interlacing the file was written with. This is the call that reads the pixels of a screenshot.</li> </ul> <p>The fourth return names the format the buffer came back in: <code>"rgba8"</code> (4 bytes/texel, channels 0-255), <code>"rgba16"</code> (8 bytes/texel, 16-bit unsigned normalized channels 0-65535) or <code>"rgba32f"</code> (16 bytes/texel, float channels).</p> <p>The pixels are the texels the payload stores. With <code>opts.straightAlpha = true</code>, a payload encoded with <code>alpha = "premultiplied"</code> has its colour divided back by alpha, in the colour space it was premultiplied in — the straight-alpha pixels <code>renderer.texture.encode</code> takes. A texel whose alpha is 0 keeps the black premultiplying left in it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — A <code>ZTEX</code> payload or source image bytes.</li> <li><code>opts</code> <code>{ straightAlpha: boolean? }?</code> <em>(optional)</em> — <code>{ straightAlpha? }</code></li> </ul> <pre><code class="language-lua">local pixels, w, h = renderer.texture.decode(vfs.read("/source/tmp/shot.png")) local straight = renderer.texture.decode(vfs.read(texPath .. "/data.ztex"), { straightAlpha = true }) </code></pre> <h2 id="destroy">modules/renderer/texture.destroy</h2> <pre><code class="language-lua">texture.destroy(texture: string | { [string]: any } | AssetRef): boolean </code></pre> <p>Release the GPU texture <code>texture</code> names. For an empty render-into texture (camera output, UI surface) this also frees its render scratch; for an uploaded runtime texture it drops the GPU resource (and any CPU shadow). After this, <code>renderer.texture.list</code> stops answering for the guid. Takes every form that names a texture — the <code>TextureHandle</code> <code>create</code> returned, the guid the listing hands out, a <code>TextureCpuHandle</code> or a texture <code>AssetRef</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture to release — a <code>TextureHandle</code>, a guid, a <code>TextureCpuHandle</code> or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">renderer.texture.destroy(rt) renderer.texture.destroy(renderer.texture.list()[1].guid) </code></pre> <h2 id="encode">modules/renderer/texture.encode</h2> <pre><code class="language-lua">texture.encode(rgba: any, width: number, height: number, opts: any): (string?, string?) </code></pre> <p>Encode raw pixels into an engine-native <code>ZTEX</code> payload (the on-disk texture content). The CPU codec behind the texture assetType's <code>onCreate</code>. Called from a task (an <code>execute</code> chunk, a component hook the engine lets wait, <code>task.spawn</code>), a <code>"uastc"</code> or <code>"bc7"</code> encode runs where the frame loop keeps stepping: on a worker where the build has one, and otherwise a bounded slice of its blocks per frame (<code>__texture.encodeBudget</code>, reported by <code>__texture.encodeJobs</code>). The call returns on the frame the payload lands. Code that cannot yield, and a coroutine the script resumes itself (<code>coroutine.create</code> / <code>coroutine.wrap</code>), encode in place, and every other compression is encoded on the call. The payload is the same bytes either way. <code>opts.compression</code> selects what the texels are stored as: <code>"rgba8"</code> (default, 8 bits/channel, <code>rgba</code> is width<em>height</em>4 bytes), <code>"uastc"</code> (UASTC 4x4 blocks, a quarter of the RGBA8 size, transcoded on each device to the block format it holds; <code>rgba</code> is width<em>height</em>4 bytes and each side is resampled to the nearest power of two, halfway rounding up), <code>"bc7"</code> (BC7 blocks, a quarter of the RGBA8 size; <code>rgba</code> is still width<em>height</em>4 bytes and both sides a multiple of 4) or the high-precision data compressions <code>"rgba16"</code> (16-bit unsigned normalized, width<em>height</em>8 bytes) / <code>"rgba32f"</code> (32-bit float, width<em>height</em>16 bytes) — for height/displacement fields, baked lightmaps, and other data rasters 8 bits quantize visibly. The two high-precision compressions store <code>rgba</code> verbatim, reject <code>opts.generateMipmaps</code> / <code>opts.maxDimension</code>, and take <code>opts.colorSpace = "linear"</code> only. <code>opts.colorSpace</code> is <code>"srgb"</code> (default, colour data) or <code>"linear"</code> (normal maps, masks, heights). <code>opts.input</code> (<code>"rgba8"</code> / <code>"rgba16"</code> / <code>"rgba32f"</code>) names the texel format <code>rgba</code> arrives in when it is not the one <code>opts.compression</code> stores — the pixels <code>renderer.texture.decode</code> hands back at their own width, say — and the encoder carries each channel across as the same normalized value, natively, before storing it. <code>opts.textureType</code> records what the texture holds — <code>"color"</code> (default), <code>"normalMap"</code> (when the encode generates mips, every level of the chain, the base included, is renormalised to unit length), <code>"mask"</code> or <code>"sprite"</code>. <code>opts.alpha</code> is what the encode does to alpha: <code>"input"</code> (default, kept as given), <code>"premultiplied"</code> (RGB multiplied by alpha — in linear light under srgb, as stored under linear — the source a material with the <code>premultiplied</code> blend mode composites) or <code>"none"</code> (every texel opaque, RGB kept). <code>opts.alphaIsTransparency = true</code> fills each fully transparent texel with the average colour of its non-transparent neighbours, spreading outward, alpha kept at 0 — before the alpha mode and the mips, so filtering never pulls black into an edge. All three are recorded in the payload and reported by <code>renderer.texture.info</code>. The sampler opts are recorded the same way: <code>opts.filter</code> (<code>"linear"</code>, the default, or <code>"nearest"</code>), <code>opts.wrapU</code> / <code>opts.wrapV</code> (<code>"repeat"</code>, the default, <code>"clamp"</code> or <code>"mirror"</code> — how the texture is sampled past its edges) and <code>opts.anisotropy</code> (<code>"world"</code>, the default, following the world's <code>render.anisotropy</code>, or a fixed <code>"1"</code> / <code>"2"</code> / <code>"4"</code> / <code>"8"</code> / <code>"16"</code>, also taken as the number). <code>opts.effort</code> (<code>"fastest"</code>, the default, <code>"fast"</code> or <code>"thorough"</code>) is how long a uastc encode searches for each block: every effort writes a payload the same size on the GPU, and a higher one lands closer to the source. <code>opts.uastcRdo</code> (<code>"none"</code>, the default, <code>"low"</code>, <code>"medium"</code> or <code>"high"</code>) is how strongly a uastc encode trades accuracy for a payload that compresses smaller. A block-compressed encode (uastc, bc7) called from a task that can yield runs off the frame and the task waits for it, so the world keeps running while it cooks.</p> <p><strong>Parameters</strong></p> <ul> <li><code>rgba</code> <code>any</code> <em>(optional)</em> — Pixel payload at <code>opts.input</code>'s (else <code>opts.compression</code>'s) native byte width — a <code>buffer</code>, a binary string, or a number array.</li> <li><code>width</code> <code>number</code> — number</li> <li><code>height</code> <code>number</code> — number</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ colorSpace?, compression?, uastcRdo?, generateMipmaps?, maxDimension?, filter?, wrapU?, wrapV?, anisotropy?, textureType?, alpha?, alphaIsTransparency?, input? }</code></li> </ul> <h2 id="encodefromimage">modules/renderer/texture.encodeFromImage</h2> <pre><code class="language-lua">texture.encodeFromImage(bytes: buffer | string, opts: any): (string?, string?) </code></pre> <p>Encode source image bytes (png/jpg/webp/…) into an engine-native <code>ZTEX</code> payload. Used by the texture importer / assetType <code>onChange</code>. A <code>"uastc"</code> or <code>"bc7"</code> encode called from code that can yield runs where the frame loop keeps stepping, as <code>renderer.texture.encode</code> describes. <code>opts.compression</code> is <code>"rgba8"</code> (default), <code>"uastc"</code>, <code>"bc7"</code>, <code>"rgba16"</code> or <code>"rgba32f"</code>, <code>opts.colorSpace</code> <code>"srgb"</code> (default) or <code>"linear"</code>, and <code>opts.uastcRdo</code> as <code>renderer.texture.encode</code> takes it. <code>"rgba8"</code> with <code>generateMipmaps = false</code>, a <code>maxDimension</code> the image fits within (or none), <code>alpha = "input"</code> and no <code>alphaIsTransparency</code> stores the source image bytes verbatim. <code>"rgba16"</code> and <code>"rgba32f"</code> decode a still image at its own bit depth and store one linear level: a 16-bit PNG keeps every bit, an 8-bit channel <code>v</code> is stored as <code>v * 257</code> (rgba16) or <code>v / 255</code> (rgba32f). The frames of an animated image (GIF, APNG, animated WebP) are decoded at 8 bits, so a 16-bit APNG's frames are stored as <code>v * 257</code> of their 8-bit values. Like <code>encode</code>, they reject <code>opts.generateMipmaps</code> and a <code>maxDimension</code> the image exceeds. <code>opts.textureType</code> records what the texture holds — <code>"color"</code> (default), <code>"normalMap"</code> (when the encode generates mips, every level of the chain, the base included, is renormalised to unit length), <code>"mask"</code> or <code>"sprite"</code>. <code>opts.alpha</code> is what the encode does to alpha: <code>"input"</code> (default, kept as given), <code>"premultiplied"</code> (RGB multiplied by alpha — in linear light under srgb, as stored under linear — the source a material with the <code>premultiplied</code> blend mode composites) or <code>"none"</code> (every texel opaque, RGB kept). <code>opts.alphaIsTransparency = true</code> fills each fully transparent texel with the average colour of its non-transparent neighbours, spreading outward, alpha kept at 0 — before the alpha mode and the mips, so filtering never pulls black into an edge. All three are recorded in the payload and reported by <code>renderer.texture.info</code>, as are the sampler opts <code>filter</code>, <code>wrapU</code>, <code>wrapV</code> and <code>anisotropy</code>, which take the values <code>renderer.texture.encode</code> takes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>buffer | string</code> — source image bytes.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ colorSpace?, compression?, uastcRdo?, generateMipmaps?, maxDimension?, filter?, wrapU?, wrapV?, anisotropy?, textureType?, alpha?, alphaIsTransparency? }</code></li> </ul> <h2 id="frameschedule">modules/renderer/texture.frameSchedule</h2> <pre><code class="language-lua">texture.frameSchedule(texture: string | AssetRef): { number }? </code></pre> <p>The times at which each layer of a timed texture stops being shown, in seconds from the start of the sequence — the running total of the layer display times, so the last entry is the length of one pass.</p> <p>This is the form a sampler reads a sequence through: a time is turned into a layer by finding the first entry it has not passed, whatever the individual layer times are. It is what the <code>schedule</code> slot of the builtin <code>animatedTexture</code> shader holds, one entry per layer.</p> <p>A texture whose layers carry no timing — a still image, a sprite sheet, a LUT stack — has no schedule and answers nil.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | AssetRef</code> — The texture — a guid, an identity, a name, a path, or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local ends = renderer.texture.frameSchedule(texRef) -- {0.1, 0.15, 0.35} </code></pre> <h2 id="info">modules/renderer/texture.info</h2> <pre><code class="language-lua">texture.info(ztex: buffer | string): (any, any) </code></pre> <p>Read the header of an engine-native <code>ZTEX</code> payload without copying the pixels. Returns its format, its <code>compression</code> ("rgba8", "bc7", "rgba16" or "rgba32f" — what the texels are stored as) and <code>colorSpace</code> ("srgb" or "linear" — how they are read when sampled), dimensions, mip count, the sampler recorded in the payload from the asset's settings — <code>filter</code> ("nearest" or "linear"), <code>wrapU</code> and <code>wrapV</code> ("repeat", "clamp" or "mirror") and <code>anisotropy</code> ("world", or a fixed "1" / "2" / "4" / "8" / "16"); a payload written before wrap and anisotropy were recorded reads repeat / repeat / world — what the payload was encoded as — <code>textureType</code> ("color", "normalMap", "mask" or "sprite"), <code>alpha</code> ("input", "premultiplied" or "none") and <code>alphaIsTransparency</code> (boolean); a payload written before these were recorded reads color / input / false — and the payload's layer shape.</p> <p><code>layers</code> counts the array layers the payload carries and <code>isArray</code> is true past one — the answer to "am I about to sample a <code>texture_2d_array</code>?", available before anything samples it. <code>animated</code> is true when those layers are a sequence in time; then <code>frameDelaysMs</code> lists each layer's display time in milliseconds in display order, and <code>durationMs</code> totals one pass. An animated image imports as one layer per frame, so <code>layers</code> is its frame count. A still texture reports <code>layers = 1</code>, <code>isArray = false</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ztex</code> <code>buffer | string</code> — ZTEX bytes.</li> </ul> <pre><code class="language-lua">local i = renderer.texture.info(bytes); if i.animated then print(i.layers, "frames", i.durationMs, "ms") end </code></pre> <h2 id="iscpuresident">modules/renderer/texture.isCpuResident</h2> <pre><code class="language-lua">texture.isCpuResident(texture: string | { [string]: any } | AssetRef): boolean </code></pre> <p>True if this texture has a resident CPU copy in the guid-keyed CPU store.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture — a <code>TextureHandle</code>, a <code>TextureCpuHandle</code>, a guid, or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">if renderer.texture.isCpuResident(handle) then ... end </code></pre> <h2 id="isresident">modules/renderer/texture.isResident</h2> <pre><code class="language-lua">texture.isResident(texture: string | { [string]: any } | AssetRef): boolean </code></pre> <p>True if a GPU texture is resident under this texture's guid.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture — a <code>TextureHandle</code>, a <code>TextureCpuHandle</code>, a guid, or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">print(renderer.texture.isResident(handle)) </code></pre> <h2 id="keepcpuonlanding">modules/renderer/texture.keepCpuOnLanding</h2> <pre><code class="language-lua">texture.keepCpuOnLanding(texture: string | { [string]: any } | AssetRef, keep: boolean): boolean </code></pre> <p>Set whether the decode on its way to this texture — scheduled by <code>textureRef:handle()</code> or <code>renderer.texture.createFromAsset</code> and not yet on the device — keeps its decoded pixels in the CPU store once it lands. A texture's <code>keepCpu</code> setting changing while its decode is in flight reaches the texture through this.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture — a <code>TextureHandle</code>, a <code>TextureCpuHandle</code>, a guid, or a texture <code>AssetRef</code>.</li> <li><code>keep</code> <code>boolean</code> — Keep the CPU copy once the decode lands.</li> </ul> <pre><code class="language-lua">renderer.texture.keepCpuOnLanding(texRef, true) </code></pre> <h2 id="list">modules/renderer/texture.list</h2> <pre><code class="language-lua">texture.list(): { any } </code></pre> <p>Every texture currently registered, ordered by guid — the ones a script created and the ones that reached the device through an asset alike. Each entry carries the guid, where it came from (<code>origin</code> is <code>"asset"</code> for a texture the asset path uploaded), and whether the GPU still holds it. A resident entry also carries the bytes it costs, its dimensions, its texel format and the mip levels the device holds (<code>mipLevels</code>), so the listing sums to <code>renderer.textureMemory()</code>. A streamable one carries <code>streamOrigin</code> — <code>"asset"</code> when a level change reads the levels it needs back from the asset, <code>"device"</code> when no asset answers for it and its whole chain on the device is the only copy, <code>"retained"</code> when it is resident coarser and holds the levels read back off the device. A resident entry carries how a material binding it samples it, as the texture records it: <code>filter</code> (<code>"linear"</code> / <code>"nearest"</code>), <code>wrapU</code> and <code>wrapV</code> (<code>"repeat"</code> / <code>"clamp"</code> / <code>"mirror"</code>) and <code>anisotropy</code> (<code>"world"</code> or a fixed <code>"1"</code>..<code>"16"</code>), and <code>samplerAnisotropy</code>, the level the texture's sampler takes: <code>anisotropy</code> resolved against <code>renderer.anisotropy()</code> and the device's ceiling, and 1 for a nearest texture — the entry of the renderer's sampler table the texture's handle names. A script-created entry also carries <code>held</code> — whether <code>renderer.hold</code> pins it for the session — and <code>scene</code>, the load that created it. <code>renderer.references("texture", guid)</code> says what is still holding a row, and <code>renderer.collect()</code> releases the rows nothing holds.</p> <pre><code class="language-lua">for _, t in ipairs(renderer.texture.list()) do print(t.guid, t.bytes) end </code></pre> <h2 id="loadcpu">modules/renderer/texture.loadCpu</h2> <pre><code class="language-lua">texture.loadCpu( </code></pre> <p>Load a <code>.texture</code> asset's pixels into the ONE guid-keyed CPU store (the Disk→CPU step) and return a CPU handle for per-pixel access (no GPU readback). The handle holds NO pixels — only the guid, dims and texel format plus the read/write/encode/unload ops (which read the Rust store). The pixels stay at the format they were authored in: <code>handle.format</code> is <code>"rgba8"</code>, <code>"rgba16"</code> or <code>"rgba32f"</code>, and <code>:readPixel</code> reports channels in that format's own units. Called by <code>texRef:load()</code>. DEFAULT: upload to the GPU then <code>handle:unload()</code>.</p> <pre><code class="language-lua">local cpu = texRef:load(); local r,g,b,a = cpu:readPixel(3, 4); cpu:unload() </code></pre> <h2 id="readback">modules/renderer/texture.readback</h2> <pre><code class="language-lua">texture.readback(texture: string | { [string]: any } | AssetRef): TextureCpuHandle </code></pre> <p>Read a runtime GPU texture's pixels back to CPU and return a <code>TextureCpuHandle</code> for them — the GPU→CPU half of the runtime-texture freeze path. A texture made with <code>renderer.texture.create</code> keeps no CPU copy, so persisting it (<code>:encode()</code> → <code>asset.create("texture", …)</code>) reads it back here first. Yields until the readback completes (a frame or two). After it returns the pixels are resident in the guid-keyed CPU store: <code>:readPixel</code>, <code>:writePixel</code>, <code>:getInfo</code>, <code>:encode</code>, <code>:unload</code> all work. Errors if the texture never becomes GPU-resident.</p> <p>A SCENE-space <code>screen</code>-sized render target is one resource shared by every render target drawn — the viewport, an offscreen capture, a camera rendering into a texture — resized and re-derived for each of them in turn. The copy is taken ahead of all of them for the frame, so what a readback of its guid answers is the content of the last frame the renderer drew: the presented view's own image at the presented resolution, since the presented view is the sink that draws last. A request made while the renderer is holding frames back is carried to the next frame it draws rather than being answered from a target another sink left standing, so a readback can wait a frame longer than the copy itself takes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture — the <code>TextureHandle</code> <code>renderer.texture.create</code> returned, a guid, or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">local cpu = renderer.texture.readback(handle); local ztex = cpu:encode(); cpu:unload() </code></pre> <h2 id="reencode">modules/renderer/texture.reencode</h2> <pre><code class="language-lua">texture.reencode(ztex: buffer | string, opts: any): (string?, string?) </code></pre> <p>Re-encode an engine-native <code>ZTEX</code> payload against new opts. A <code>"uastc"</code> or <code>"bc7"</code> re-encode called from code that can yield runs where the frame loop keeps stepping, as <code>renderer.texture.encode</code> describes. <code>opts</code> is the complete encode spec, exactly as for <code>renderer.texture.encode</code>: every field it leaves out takes its default, so <code>textureType</code>, <code>alpha</code>, <code>alphaIsTransparency</code>, <code>filter</code>, <code>wrapU</code>, <code>wrapV</code> and <code>anisotropy</code> come from <code>opts</code> — not from the payload's header — and a payload re-encoded with <code>{ compression = "uastc" }</code> alone loses a <code>nearest</code> filter or a <code>premultiplied</code> alpha it carried. <code>renderer.texture.withSampler</code> changes how a payload is sampled without re-encoding it. The payload's layers and their display times are kept, whatever compression either side is in. Each layer's texels are read back with straight alpha (a payload encoded with <code>alpha = "premultiplied"</code> has its colour divided back by alpha), carried to the width the new <code>compression</code> stores as the same normalized value, and encoded exactly as <code>renderer.texture.encode</code> would encode them; a payload that embeds its source image is re-encoded from that image, read at its own bit depth. What an earlier encode discarded stays discarded: after <code>alpha = "none"</code> the alpha is opaque. The texture assetType re-encodes a container that holds no source image through this call when its settings change. A block-compressed encode called from a task that can yield runs off the frame while the task waits.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ztex</code> <code>buffer | string</code> — A <code>ZTEX</code> payload — a <code>buffer</code> or a binary string.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ colorSpace?, compression?, uastcRdo?, generateMipmaps?, maxDimension?, filter?, wrapU?, wrapV?, anisotropy?, textureType?, alpha?, alphaIsTransparency? }</code></li> </ul> <pre><code class="language-lua">local wide = renderer.texture.reencode(vfs.read(texPath .. "/data.ztex"), { compression = "rgba16", colorSpace = "linear", generateMipmaps = false }) </code></pre> <h2 id="tone">modules/renderer/texture.tone</h2> <pre><code class="language-lua">texture.tone(histogram: any): TextureTone </code></pre> <p>Reduce a histogram to what the picture's tone IS: where its darkest and brightest pixels sit, where the body of it sits, and how much of it is standing on the floor or the ceiling — all in code values on the 0-255 scale the pixels were delivered at.</p> <p><code>span</code> (<code>max - min</code>) is the whole range including a single stray pixel; <code>spread</code> (<code>p95 - p5</code>) is the range the body of the picture occupies, which is the reading that says whether a shot is legible. A frame whose subject is modelled and shaded but delivered inside a few code values reads a large <code>mean</code> and a tiny <code>spread</code>, and no mean alone can tell that apart from a frame with a subject in it.</p> <p><code>crushed</code> and <code>clipped</code> are the shares of the picture at code 0 and at code 255, each 0..1 — what a shot loses to the floor and to the ceiling.</p> <p><strong>Parameters</strong></p> <ul> <li><code>histogram</code> <code>any</code> <em>(optional)</em> — A histogram from <code>cpu:histogram()</code>.</li> </ul> <pre><code class="language-lua">local t = cpu:tone(); if t.spread < 24 then error("the shot is flat") end </code></pre> <h2 id="unloadcpu">modules/renderer/texture.unloadCpu</h2> <pre><code class="language-lua">texture.unloadCpu(texture: string | { [string]: any } | AssetRef) </code></pre> <p>Drop this texture's resident CPU copy from the guid-keyed CPU store. The explicit release for a texture asset's <code>keepCpu</code> setting turning off.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture — a <code>TextureHandle</code>, a <code>TextureCpuHandle</code>, a guid, or a texture <code>AssetRef</code>.</li> </ul> <pre><code class="language-lua">renderer.texture.unloadCpu(texRef) </code></pre> <h2 id="update">modules/renderer/texture.update</h2> <pre><code class="language-lua">texture.update(texture: string | { [string]: any } | AssetRef, src: any): TextureHandle </code></pre> <p>Overwrite the GPU texture <code>texture</code> names IN PLACE, under the same guid, from new pixels. Never writes a <code>.texture</code> file — the play-mode mutate path. Takes every form that names a texture — the <code>TextureHandle</code> <code>create</code> returned, the guid <code>renderer.texture.list</code> hands out, a <code>TextureCpuHandle</code> or a texture <code>AssetRef</code>. <code>src</code> is raw pixels, or a <code>TextureCpuHandle</code> (<code>texRef:load()</code>, <code>renderer.texture.loadCpu</code>, <code>renderer.texture.readback</code>), whose pixels arrive with everything they carry: texel format, colour space, mip chain, layers and filter. Every material, UI image and post-process effect sampling the guid draws the new texture. Returns a handle carrying the new dimensions: the handle it was given, refreshed, and a handle over the guid otherwise.</p> <p><strong>Parameters</strong></p> <ul> <li><code>texture</code> <code>string | { [string]: any } | AssetRef</code> — The texture to update — a <code>TextureHandle</code>, a guid, a <code>TextureCpuHandle</code> or a texture <code>AssetRef</code>.</li> <li><code>src</code> <code>any</code> <em>(optional)</em> — New raw pixels <code>{rgba, width, height, srgb?, format?}</code> — <code>rgba</code> as a <code>buffer</code>, a binary string, or a number array — or a <code>TextureCpuHandle</code>.</li> </ul> <h2 id="withsampler">modules/renderer/texture.withSampler</h2> <pre><code class="language-lua">texture.withSampler(ztex: buffer | string, opts: { filter: string?, wrapU: string?, wrapV: string?, anisotropy: (string | number)? }): (string?, string?) </code></pre> <p>Change how an engine-native <code>ZTEX</code> payload is sampled without re-encoding it. <code>opts</code> names any of <code>filter</code> (<code>"linear"</code> / <code>"nearest"</code>), <code>wrapU</code> and <code>wrapV</code> (<code>"repeat"</code> / <code>"clamp"</code> / <code>"mirror"</code>) and <code>anisotropy</code> (<code>"world"</code>, or <code>"1"</code> / <code>"2"</code> / <code>"4"</code> / <code>"8"</code> / <code>"16"</code>, also taken as the number); a field it omits keeps the payload's value. The result is the same bytes with only the sampler bits of the header replaced, so every level is byte-identical to the input — the cheap path the texture assetType takes when a settings change touches only these four fields. An unrecognised value raises.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ztex</code> <code>buffer | string</code> — A <code>ZTEX</code> payload — a <code>buffer</code> or a binary string.</li> <li><code>opts</code> <code>{ filter: string?, wrapU: string?, wrapV: string?, anisotropy: (string | number)? }</code> — <code>{ filter?, wrapU?, wrapV?, anisotropy? }</code></li> </ul> <pre><code class="language-lua">local clamped = renderer.texture.withSampler(vfs.read(texPath .. "/data.ztex"), { wrapU = "clamp", wrapV = "clamp" }) </code></pre> <h2 id="modules-renderer-texturememory">modules/renderer/textureMemory</h2> <pre><code class="language-lua">textureMemory(): { </code></pre> <p>What the GPU texture cache holds, split by whether the texture is block-compressed. <code>compressedBytes</code> and <code>uncompressedBytes</code> are what those textures cost in VRAM, measured from each texture's own format and mip chain — so a <code>.texture</code> whose settings name <code>compression = "uastc"</code> appears in the compressed columns at a quarter of what the same image costs as RGBA8. <code>blockCompressionSupported</code> is whether this adapter samples any block-compressed format; where it is false a block-compressed payload is uploaded decoded and lands in the uncompressed columns instead, so the texture is present everywhere and compressed where the hardware allows it. <code>blockFormat</code> names the format block-compressed textures are resident in on this adapter (<code>"bc7"</code> on desktop, <code>"astc4x4"</code> on a phone, <code>"etc2"</code> on a phone without ASTC) and is nil where it samples none. A texture stored as UASTC is transcoded to <code>blockFormat</code>; one stored as BC7 keeps its blocks where the adapter samples BC7 and is re-encoded into <code>blockFormat</code> where it does not, each at the same one byte per texel. Measured at the end of the last rendered frame. <code>streamableTextures</code> is how many of them the texture budget can move the base mip level of, split by where a level change reads the levels it needs from: <code>assetStreamedTextures</code> are read back from the asset they came from and hold nothing in system memory; the rest no asset answers for, so the device holds their only copy. Such a texture holds no copy of its own while it is at its whole mip chain; the first time the budget takes it coarser its chain is read back off the device before any level is freed, and it holds that copy while it stays coarser. <code>retainedTextures</code> counts the ones holding a copy, and <code>streamSourceBytes</code> is what those copies occupy in system memory — bytes that are not VRAM. <code>pinnedTextures</code> counts the textures big enough to stream that stand at a level nothing can move: no asset answers for them and they left the streamable set, the asset behind them could not be read back, or a UI image, a post-process property or a render feature holds a view of them. A texture out of the streamable set only because no measured surface wears it stands in neither count: a surface reaching it takes it back up, so its level moves again as soon as there is a footprint to move it by. <code>byFormat</code> names every GPU format a cached texture is held in, keyed by the format's name (<code>Bc7RgbaUnormSrgb</code>, <code>Astc { block: B4x4, .. }</code>, <code>Rgba8UnormSrgb</code>, ...), with how many textures it holds and their bytes. <code>heldAboveCap</code> lists the textures resident larger than <code>renderer.setMaxTextureSize</code> allows: one whose mip chain has no level within the cap, and one with no asset behind it, which keeps every level it was made with. <code>decodedFallback</code> lists the UASTC textures this device holds as texels because it samples no block format.</p> <pre><code class="language-lua">local tm = renderer.textureMemory() print(("%d compressed textures hold %.1f MiB"):format(tm.compressedTextures, tm.compressedBytes / 1048576)) print(("%d textures stream from their asset, %d hold %.1f MiB of pixels"):format( tm.assetStreamedTextures, tm.retainedTextures, tm.streamSourceBytes / 1048576)) </code></pre> <h2 id="modules-renderer-texturestreaming">modules/renderer/textureStreaming</h2> <pre><code class="language-lua">textureStreaming(): TextureStreaming </code></pre> <p>What the last frame's texture-residency plan decided. <code>budgetBytes</code> is the budget in force: the device's own, or the one <code>setTextureBudget</code> names, as much of it as the memory pressure in force leaves (half at <code>"critical"</code>). <code>deviceBudgetBytes</code> is the device's own budget (what <code>setTextureBudget</code> names <code>deviceMegabytes</code>): <code>memoryBudgetBytes</code>, held to the quality tier's cap where the tier states one. <code>memoryBudgetBytes</code> is what the device's memory affords and <code>budgetRule</code> the rule it came from: <code>"systemMemory"</code>, an eighth of the RAM the system states (<code>deviceMemoryBytes</code>, nil where it states none), on a GPU that draws from that RAM, or <code>"bufferSize"</code>, half the adapter's <code>max_buffer_size</code>; between 256 MiB and 2 GiB either way. <code>pressure</code> is the memory pressure level in force; from <code>"moderate"</code> up the plan holds every texture at the level its surfaces sample, below the budget as well as past it (<code>renderer.memoryPressure()</code>). <code>streamable</code> is how many textures the plan can move. <code>residentBytes</code> is what those textures occupy now, measured from the textures that are allocated; <code>demandedBytes</code> is what the frame asks for before the sweep — every texture's whole chain while those fit the budget, and the level each surface asked for once they exceed it — so the two part exactly where the budget is doing something. <code>starved</code> counts the textures left coarser than the frame asked for, <code>promoted</code> the ones that climbed a level this frame, and <code>changed</code> the ones whose GPU texture was replaced. A camera approaching a surface reads <code>promoted</code> above zero for a few frames and then zero once it settles.</p> <p><code>textures</code> is one row per streamable texture, ordered by key, carrying the level each one was asked for and the measurement that asked. Two byte totals can agree while a single texture sits several levels off what its surface samples, so read the row when the question is which level a texture holds and why.</p> <pre><code class="language-lua">local ts = renderer.textureStreaming() print(("textures: %.1f MiB resident of %.1f MiB demanded, %d starved"):format( ts.residentBytes / 1048576, ts.demandedBytes / 1048576, ts.starved)) </code></pre> <h2 id="modules-renderer-transmissionshadows">modules/renderer/transmissionShadows</h2> <pre><code class="language-lua">transmissionShadows(): boolean </code></pre> <p>Whether translucent casters tint the directional light they block.</p> <h2 id="modules-renderer-uploadstats">modules/renderer/uploadStats</h2> <pre><code class="language-lua">uploadStats(): { </code></pre> <p>What the last frame the renderer drew spent re-describing its renderables to the GPU, counted from the frame it drew before that, so a read after <code>task.waitDrawnFrames</code> describes the frame waited for even where the frame pacer declines frames between two drawn ones. Every renderable owns a slot in the per-instance data a draw reads (its world matrix, the bounds the culler tests it by, and the flags that decide which passes and which culling stages see it), and a frame uploads only the slots whose contents changed. A <code>renderer.mesh.drawInstanced</code> population's slots are filled by copies from its own buffers, counted the same way: a frame that copies a population's transforms or shader data counts one write per copy and the bytes it moved. <code>bytes</code> is what those uploads and copies carried, <code>fullBytes</code> what re-sending every slot would have cost, and <code>writes</code> how many buffer writes carried it. The three numbers cover that per-renderable data alone, so a scene standing still reads <code>bytes = 0</code> against a <code>fullBytes</code> that grows with the scene, and the ratio says how much of it the scene's own churn, rather than its size, is paying for.</p> <pre><code class="language-lua">local u = renderer.uploadStats() print(("instance upload: %d B of %d B in %d writes"):format(u.bytes, u.fullBytes, u.writes)) </code></pre> <h2 id="modules-renderer-upscalebackend">modules/renderer/upscaleBackend</h2> <pre><code class="language-lua">upscaleBackend(): string </code></pre> <p>The backend actually filling the upscale seam this frame — <code>"blit"</code> or <code>"dlss"</code>. Reports what the renderer resolved the last <code>renderer.setUpscaleBackend()</code> request into, which reads <code>"blit"</code> after a request for <code>"dlss"</code> on an adapter <code>renderer.upscaleCapabilities()</code> omits it from.</p> <pre><code class="language-lua">local backend = renderer.upscaleBackend() </code></pre> <h2 id="modules-renderer-upscalecapabilities">modules/renderer/upscaleCapabilities</h2> <pre><code class="language-lua">upscaleCapabilities(): { string } </code></pre> <p>The backend names this engine can actually run for the upscale seam, from a boot-time probe of the adapter. <code>"blit"</code> always answers, since the stretch needs no adapter capability; <code>"dlss"</code> joins it only where NGX DLSS Super Resolution is available. Read this before <code>renderer.setUpscaleBackend("dlss", ...)</code> to know ahead of the request what it resolves to.</p> <pre><code class="language-lua">local caps = renderer.upscaleCapabilities() </code></pre> <h2 id="modules-renderer-variantsource">modules/renderer/variantSource</h2> <pre><code class="language-lua">variantSource(program: string): string? </code></pre> <p>The WGSL one of the programs <code>renderer.shaderVariants()</code> lists holds, exactly as the shader compiler received it. <code>program</code> is the <code>program</code> field of a row's <code>base</code> or of one of its <code>variants</code>. Reading a base alongside a variant shows what a feature set selected: each program's text holds the code its own features guard. The variant-report spelling of <code>renderer.compiledSource</code>, which answers the same for every other shader.</p> <p><strong>Parameters</strong></p> <ul> <li><code>program</code> <code>string</code> — A <code>program</code> name from <code>renderer.shaderVariants()</code>.</li> </ul> <pre><code class="language-lua">local row = renderer.shaderVariants()[1] local base = renderer.variantSource(row.base.program) </code></pre> <h2 id="modules-renderer-viewportsize">modules/renderer/viewportSize</h2> <pre><code class="language-lua">viewportSize(): { width: number, height: number } </code></pre> <p>The rect the scene was drawn into in the frame just drawn, in pixels. That is the whole drawing surface in a runtime window, a browser canvas and a headless engine, and the editor's viewport panel — smaller than the surface — in an editor layout. Follows a <code>renderer.setViewportSize</code>, a window drag and a browser page resize alike, so it reports what is realized rather than what was asked for.</p> <pre><code class="language-lua">local v = renderer.viewportSize() </code></pre> <h2 id="modules-renderer-watchshadowpages">modules/renderer/watchShadowPages</h2> <pre><code class="language-lua">watchShadowPages(runs: number) </code></pre> <p>Gather a breakdown of what the sun's virtual shadow map does on each of the main camera's next <code>runs</code> runs. Each run's counts come back from the GPU a few frames after it, and <code>renderer.shadowPageBreakdown()</code> hands them over. Nothing is gathered while no reader has asked. <code>0</code> stops a gathering under way.</p> <p><strong>Parameters</strong></p> <ul> <li><code>runs</code> <code>number</code> — How many of the main camera's runs to gather.</li> </ul> <pre><code class="language-lua">renderer.watchShadowPages(120) </code></pre> <h2 id="modules-restirlighting-readme">modules/restirLighting/README</h2> <pre><code class="language-lua">require("@builtin::systems.restirLighting.restirLighting") -- restirLighting </code></pre> <p>Direct light from many small sources at a cost that does not grow with how many there are. A shading pass that loops every light pays for every light at every pixel, which is why the count has to be capped; resampled importance sampling instead draws a few candidates per pixel, keeps one in proportion to what it would contribute, and carries a weight that makes the survivor stand for the whole set. What a pixel keeps is reused — by its neighbours this frame and by itself on the next one — so the number of candidates each pixel has to draw stays small while the set it effectively samples keeps growing. A cave of glowing crystals or a street of practicals costs what a handful of lights costs. Sources registered here are additional to the lights the engine holds rows for: this lights what it is given, on top of the scene as it was drawn. Register a source with <code>set</code>, drop it with <code>remove</code>, and the pass starts and stops with the registry.</p> <p>Usage: local restirLighting = require("@builtin::systems.restirLighting.restirLighting")</p> <h2 id="modules-restirlighting-active">modules/restirLighting/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the resampling pass is currently running.</p> <pre><code class="language-lua">if restirLighting.active() then print("resampling") end </code></pre> <h2 id="modules-restirlighting-beginframe">modules/restirLighting/beginFrame</h2> <pre><code class="language-lua">beginFrame(width: number, height: number): { [string]: any }? </code></pre> <p>Size the buffers to the frame, advance the frame counter and push the settings. The render feature calls this once per frame before it enqueues the passes; it is what gives the temporal reuse a frame to count and the grid a size.</p> <p><strong>Parameters</strong></p> <ul> <li><code>width</code> <code>number</code> — Viewport width in pixels.</li> <li><code>height</code> <code>number</code> — Viewport height in pixels.</li> </ul> <pre><code class="language-lua">local f = restirLighting.beginFrame(ctx.viewport.w, ctx.viewport.h) </code></pre> <h2 id="modules-restirlighting-capacity">modules/restirLighting/capacity</h2> <pre><code class="language-lua">capacity(): number </code></pre> <p>The most sources the registry holds.</p> <pre><code class="language-lua">print(restirLighting.capacity()) </code></pre> <h2 id="modules-restirlighting-clear">modules/restirLighting/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Drop every source and release the pass. The settings are kept.</p> <pre><code class="language-lua">restirLighting.clear() </code></pre> <h2 id="modules-restirlighting-configure">modules/restirLighting/configure</h2> <pre><code class="language-lua">configure(opts: Settings?): State </code></pre> <p>Change how the resampling is run. Any omitted field keeps its current value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>Settings?</code> <em>(optional)</em> — The settings to change — see <code>Settings</code>.</li> </ul> <pre><code class="language-lua">restirLighting.configure({ candidates = 16, mode = "reference" }) </code></pre> <h2 id="modules-restirlighting-count">modules/restirLighting/count</h2> <pre><code class="language-lua">count(): number </code></pre> <p>How many sources are registered.</p> <pre><code class="language-lua">print(restirLighting.count()) </code></pre> <h2 id="modules-restirlighting-defaults">modules/restirLighting/defaults</h2> <pre><code class="language-lua">defaults(): State </code></pre> <p>The settings a registry runs under until something changes them. Pass this to <code>configure</code> to put every one of them back.</p> <pre><code class="language-lua">restirLighting.configure(restirLighting.defaults()) </code></pre> <h2 id="modules-restirlighting-memorybytes">modules/restirLighting/memoryBytes</h2> <pre><code class="language-lua">memoryBytes(): number </code></pre> <p>What the reservoir grid costs, in bytes: two vec4 per cell, twice over because reuse reads one grid and writes the other.</p> <pre><code class="language-lua">print(restirLighting.memoryBytes() // 1024, "KiB") </code></pre> <h2 id="modules-restirlighting-perpixelcost">modules/restirLighting/perPixelCost</h2> <pre><code class="language-lua">perPixelCost(): number </code></pre> <p>How many candidate evaluations a pixel pays for, per frame. It is the answer the whole technique exists to give: fresh candidates plus borrowed neighbours plus the one history, and no term in it is the source count.</p> <pre><code class="language-lua">print(restirLighting.perPixelCost(), "evaluations regardless of light count") </code></pre> <h2 id="modules-restirlighting-remove">modules/restirLighting/remove</h2> <pre><code class="language-lua">remove(key: string): boolean </code></pre> <p>Remove the source registered under <code>key</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The identifier the source was registered with.</li> </ul> <pre><code class="language-lua">restirLighting.remove("crystal") </code></pre> <h2 id="modules-restirlighting-set">modules/restirLighting/set</h2> <pre><code class="language-lua">set(key: string, source: Source): number </code></pre> <p>Add or replace the source registered under <code>key</code>. Re-submitting the same key moves that source rather than adding another, which is what lets a component push its position every frame as its entity moves.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Stable identifier — an entity id works well.</li> <li><code>source</code> <code>Source</code> — Where it is and what it emits — see <code>Source</code>.</li> </ul> <pre><code class="language-lua">restirLighting.set("crystal", { position = { 2, 1, 0 }, color = { 0.4, 0.8, 1 }, intensity = 6 }) </code></pre> <h2 id="modules-restirlighting-settings">modules/restirLighting/settings</h2> <pre><code class="language-lua">settings(): State </code></pre> <p>The settings currently in force.</p> <pre><code class="language-lua">local c = restirLighting.settings().candidates </code></pre> <h2 id="modules-restirlighting-stats">modules/restirLighting/stats</h2> <pre><code class="language-lua">stats(): Stats? </code></pre> <p>What the resampling did on the most recent frame a read-back has landed for, or nil before the first one arrives.</p> <pre><code class="language-lua">local s = restirLighting.stats(); print(s.lit, "of", s.cells, "cells lit") </code></pre> <h2 id="modules-retarget-bake-error">modules/retarget/Bake:error</h2> <pre><code class="language-lua">Bake:error(): string? </code></pre> <p>Why the bake failed, once <code>poll()</code> answers <code>"failed"</code>.</p> <h2 id="modules-retarget-bake-poll">modules/retarget/Bake:poll</h2> <pre><code class="language-lua">Bake:poll(): string </code></pre> <p>Advance the bake: dispatch once the kernel is compiled, read the result back, and build the clip.</p> <h2 id="modules-retarget-bake-report">modules/retarget/Bake:report</h2> <pre><code class="language-lua">Bake:report(): any </code></pre> <p>What the bake used and measured: <code>{ sourceBind, targetBind, mappedBones, frames, travelSpeed, left, right }</code>. A bind of <code>rest</code> means the rig had no bind pose and its rest pose stood in for it. <code>travel</code> is the velocity the clip carries the target at over the ground, in the target rig's space and units per second, over the frames the source has a foot down (the stance ground passing under a clip that plays in place, plus any root motion), pointing forward, sideways or back as the clip moves; <code>travelSpeed</code> its length. <code>left</code> / <code>right</code> describe each leg: <code>plantedFrames</code> (the frames the source's foot is planted, where the target's foot is held), <code>worstSlide</code> (how far the target's foot strays within a plant against the moving ground, in the target rig's units), <code>transferSlide</code> (the same before the hold), <code>ground</code> (the lowest the target's foot reaches before the hold), and <code>jointAngleMeanDeg</code> / <code>jointAngleMaxDeg</code> (how far the hold turns the thigh, shin and foot from the source's joint angles, over the clip).</p> <h2 id="modules-retarget-bake-result">modules/retarget/Bake:result</h2> <pre><code class="language-lua">Bake:result(): string? </code></pre> <p>The retargeted clip's <code>zanim</code> bytes, once <code>poll()</code> answers <code>"ready"</code>.</p> <h2 id="modules-retarget-readme">modules/retarget/README</h2> <pre><code class="language-lua">require("@builtin::modules.retarget") -- retarget </code></pre> <p>Skeletal animation retargeting: carry a clip authored on one humanoid rig onto another, so the target's anatomy makes the source's motion. <code>start</code> bakes a clip on the GPU and hands back a handle that is ready a few frames later; <code>bake</code> waits for it; <code>bakeBytes</code> caches a bake under a key. <code>plan</code> reports which roles map across two rigs.</p> <p>Usage: local retarget = require("@builtin::modules.retarget")</p> <h2 id="modules-retarget-aux">modules/retarget/aux</h2> <pre><code class="language-lua">aux(cacheKey: string) </code></pre> <p>Read what was cached beside the bake at <code>cacheKey</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cacheKey</code> <code>string</code></li> </ul> <h2 id="modules-retarget-bake">modules/retarget/bake</h2> <pre><code class="language-lua">bake(clipBytes: buffer | string, srcRig: any, tgtRig: any): (string?, string?) </code></pre> <p>Retarget a clip and wait for it: <code>start</code>, then poll once a frame until the bake is ready or has failed. Yields; call it where a script may wait.</p> <p><strong>Parameters</strong></p> <ul> <li><code>clipBytes</code> <code>buffer | string</code> — The source clip's <code>zanim</code> bytes.</li> <li><code>srcRig</code> <code>any</code> <em>(optional)</em> — The rig the clip was authored on (parsed rig / table / JSON).</li> <li><code>tgtRig</code> <code>any</code> <em>(optional)</em> — The rig to retarget onto (parsed rig / table / JSON).</li> </ul> <h2 id="modules-retarget-bakebytes">modules/retarget/bakeBytes</h2> <pre><code class="language-lua">bakeBytes(clipBytes, srcRig, tgtRig, cacheKey: string?): (string?, string?) </code></pre> <p>Bake with a cache: the first call for <code>cacheKey</code> bakes (and waits), every later call returns the same bytes. Pass a key that names the clip and both rigs' content.</p> <p><strong>Parameters</strong></p> <ul> <li><code>clipBytes</code> <code>any</code> <em>(optional)</em> — The source clip's <code>zanim</code> bytes.</li> <li><code>srcRig</code> <code>any</code> <em>(optional)</em> — Source rig (parsed rig / table / JSON).</li> <li><code>tgtRig</code> <code>any</code> <em>(optional)</em> — Target rig (parsed rig / table / JSON).</li> <li><code>cacheKey</code> <code>string?</code> <em>(optional)</em> — Optional stable key; when given, the result is cached and reused.</li> </ul> <h2 id="modules-retarget-clearcache">modules/retarget/clearCache</h2> <pre><code class="language-lua">clearCache(cacheKey: string?) </code></pre> <p>Drop every cached bake, and the rig pairs they were built from (or just <code>cacheKey</code> when given).</p> <p><strong>Parameters</strong></p> <ul> <li><code>cacheKey</code> <code>string?</code> <em>(optional)</em> — Optional single key to evict; omit to clear all.</li> </ul> <h2 id="modules-retarget-facing">modules/retarget/facing</h2> <pre><code class="language-lua">facing(rig: any): V? </code></pre> <p>The direction a rig's body faces at rest, in the rig's own space: forward from its body frame (the hip line crossed with the stand axis), read from its bind pose. The retarget keeps every body facing this way, so it is the forward a controller turns the body by.</p> <p><strong>Parameters</strong></p> <ul> <li><code>rig</code> <code>any</code> <em>(optional)</em> — A rig (parsed rig / table / JSON) with humanoid roles.</li> </ul> <h2 id="modules-retarget-loadrig">modules/retarget/loadRig</h2> <pre><code class="language-lua">loadRig(ref) </code></pre> <p>Resolve a <code>.rig</code> asset and return it parsed for the bake: its bones, its effective humanoid mapping (none when the rig is not a humanoid) and where its bind comes from.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>any</code> <em>(optional)</em> — A <code>.rig</code> asset ref (identity / guid / path / handle).</li> </ul> <h2 id="modules-retarget-parserig">modules/retarget/parseRig</h2> <pre><code class="language-lua">parseRig(rig) </code></pre> <p>Normalize a rig for the bake: its bones, its role -> bone map, where its bind pose comes from, and its structure. Takes a <code>.rig</code> document (table or JSON) carrying either <code>roles</code> or <code>profile.roles</code>; an already-parsed rig is returned unchanged.</p> <p><strong>Parameters</strong></p> <ul> <li><code>rig</code> <code>any</code> <em>(optional)</em> — <code>{ bones, roles | profile = { roles }, bind? }</code>, or its JSON.</li> </ul> <h2 id="modules-retarget-plan">modules/retarget/plan</h2> <pre><code class="language-lua">plan(srcRig, tgtRig) </code></pre> <p>Which roles map across two rigs: <code>{ mapped = { [role] = { source, target } }, unmappedSource, unmappedTarget, sourceRoleCount, targetRoleCount }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>srcRig</code> <code>any</code> <em>(optional)</em> — Source rig (parsed rig / table / JSON).</li> <li><code>tgtRig</code> <code>any</code> <em>(optional)</em> — Target rig (parsed rig / table / JSON).</li> </ul> <h2 id="modules-retarget-request">modules/retarget/request</h2> <pre><code class="language-lua">request(clipBytes: buffer | string, srcRig: any, tgtRig: any, cacheKey: string): any </code></pre> <p>The bake for <code>cacheKey</code>: a handle that is already ready when the key's clip has landed, the bake running for the key when one is, or a new bake otherwise, which lands its clip under the key. Every caller polls its own reference; they all read one bake.</p> <p><strong>Parameters</strong></p> <ul> <li><code>clipBytes</code> <code>buffer | string</code> — The source clip's <code>zanim</code> bytes.</li> <li><code>srcRig</code> <code>any</code> <em>(optional)</em> — Source rig (parsed rig / table / JSON).</li> <li><code>tgtRig</code> <code>any</code> <em>(optional)</em> — Target rig (parsed rig / table / JSON).</li> <li><code>cacheKey</code> <code>string</code> — A key naming the clip and both rigs' content.</li> </ul> <h2 id="modules-retarget-setaux">modules/retarget/setAux</h2> <pre><code class="language-lua">setAux(cacheKey: string, value) </code></pre> <p>Cache a value beside the bake at <code>cacheKey</code>. It is dropped whenever that bake is.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cacheKey</code> <code>string</code></li> <li><code>value</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-retarget-start">modules/retarget/start</h2> <pre><code class="language-lua">start(clipBytes: buffer | string, srcRig: any, tgtRig: any): any </code></pre> <p>Start retargeting a clip onto a target rig on the GPU. Returns a handle: <code>poll()</code> advances it and answers <code>"pending"</code>, <code>"ready"</code> or <code>"failed"</code>; once ready, <code>result()</code> is the retargeted clip's <code>zanim</code> bytes and <code>report()</code> says what the bake used. The clip's keys go to the transfer kernel as they are stored; the retargeted clip is keyed at the source's key rate (at least 30 per second).</p> <p><strong>Parameters</strong></p> <ul> <li><code>clipBytes</code> <code>buffer | string</code> — The source clip's <code>zanim</code> bytes.</li> <li><code>srcRig</code> <code>any</code> <em>(optional)</em> — The rig the clip was authored on (parsed rig / table / JSON).</li> <li><code>tgtRig</code> <code>any</code> <em>(optional)</em> — The rig to retarget onto (parsed rig / table / JSON).</li> </ul> <h2 id="modules-rigstructure-readme">modules/rigStructure/README</h2> <pre><code class="language-lua">require("@builtin::modules.rigStructure") -- rigStructure </code></pre> <p>The shape of a skeleton, read from a rig document or a live Skeleton: parents and children, world positions and rotations of the rest pose (rest scale included), and the questions an avatar type asks of that shape: which bone two others share, how far a subtree reaches along a direction, how well two subtrees mirror each other. A structure is memoized by the content hash of the bones it was built from.</p> <p>Usage: local rigStructure = require("@builtin::modules.rigStructure")</p> <h2 id="modules-rigstructure-bindpose">modules/rigStructure/bindPose</h2> <pre><code class="language-lua">bindPose(s: Structure): Structure? </code></pre> <p>The structure of the pose a rig's inverse-bind matrices state — its skin's bind pose — in place of the rest pose: the same bones and hierarchy, with <code>gPos</code>, <code>gRot</code> and <code>gScale</code> the bind's. A rest pose can be anything (a clip's first frame); the bind is the pose the mesh was modelled in. A skin states its bind in the mesh's space, which an exporter can leave turned against the bones' own (a Z-up armature under a Y-up scene), so the bind is carried into the space rest-pose forward kinematics answers in, by the turn most bones agree takes their bind to their rest.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code> — A structure whose bones carry <code>inverseBind</code> (a rig document's) or <code>inverse_bind</code> (a live Skeleton's).</li> </ul> <h2 id="modules-rigstructure-chainlength">modules/rigStructure/chainLength</h2> <pre><code class="language-lua">chainLength(s: Structure, from: number, to: number): number </code></pre> <p>The summed link lengths from <code>from</code> down to its descendant <code>to</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>from</code> <code>number</code></li> <li><code>to</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-effective">modules/rigStructure/effective</h2> <pre><code class="language-lua">effective(s: Structure, q: any, overrides: any?): any </code></pre> <p>A qualification with an author's overrides laid over it. <code>overrides.roles</code> maps a role to the bone that fills it, or to <code>false</code> to leave the role empty. An override naming a bone the skeleton does not have keeps the detected bone and is listed in <code>report.stale</code>. The qualification passed in is left as it was.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code> — The structure the qualification was made on.</li> <li><code>q</code> <code>any</code> <em>(optional)</em> — A qualification (from an avatar type's <code>qualify</code>).</li> <li><code>overrides</code> <code>any?</code> <em>(optional)</em> — <code>{ roles = { [role] = boneName | false } }</code>, or nil.</li> </ul> <h2 id="modules-rigstructure-extent">modules/rigStructure/extent</h2> <pre><code class="language-lua">extent(s: Structure, dir: { number }): (number, number) </code></pre> <p>The extent of the skeleton along <code>dir</code>: lowest and highest projections of any bone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>dir</code> <code>{ number }</code></li> </ul> <h2 id="modules-rigstructure-extremity">modules/rigStructure/extremity</h2> <pre><code class="language-lua">extremity(s: Structure, i: number, origin: { number }): number </code></pre> <p>The bone of the subtree rooted at <code>i</code> farthest from <code>origin</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>i</code> <code>number</code></li> <li><code>origin</code> <code>{ number }</code></li> </ul> <h2 id="modules-rigstructure-frombones">modules/rigStructure/fromBones</h2> <pre><code class="language-lua">fromBones(bones: { any }): Structure </code></pre> <p>The structure of a live Skeleton's bone list (<code>ecs.Skeleton.bones</code>: <code>name</code>, <code>parent</code>, <code>t</code>, <code>r</code>, <code>s</code>), memoized the same way as <code>fromDoc</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bones</code> <code>{ any }</code> — The Skeleton's bones.</li> </ul> <h2 id="modules-rigstructure-fromdoc">modules/rigStructure/fromDoc</h2> <pre><code class="language-lua">fromDoc(doc: any): Structure </code></pre> <p>The structure of a rig document, memoized by its content hash.</p> <p><strong>Parameters</strong></p> <ul> <li><code>doc</code> <code>any</code> <em>(optional)</em> — A rig document: <code>{ bones = { { name, parent (0-based, -1 for a root), rest = { t = {x,y,z}, r = {x,y,z,w}, s = {x,y,z} } } } }</code>.</li> </ul> <h2 id="modules-rigstructure-hash">modules/rigStructure/hash</h2> <pre><code class="language-lua">hash(doc: any): string </code></pre> <p>The content hash of a skeleton: its bones' names, parents, rest transforms and inverse-bind matrices. Two documents with the same hash have the same structure, whatever else they carry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>doc</code> <code>any</code> <em>(optional)</em> — A rig document (<code>{ bones = { { name, parent, rest = { t, r, s } } } }</code>) or a live Skeleton's bone list.</li> </ul> <h2 id="modules-rigstructure-lca">modules/rigStructure/lca</h2> <pre><code class="language-lua">lca(s: Structure, a: number, b: number): number </code></pre> <p>The deepest bone that is an ancestor of both <code>a</code> and <code>b</code> (a bone counts as its own ancestor), or 0 when they share no root.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>a</code> <code>number</code></li> <li><code>b</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-leaves">modules/rigStructure/leaves</h2> <pre><code class="language-lua">leaves(s: Structure, i: number): { number } </code></pre> <p>The leaves of the subtree rooted at <code>i</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>i</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-longest">modules/rigStructure/longest</h2> <pre><code class="language-lua">longest(s: Structure, i: number): (number, number, number) </code></pre> <p>The longest chain below <code>i</code> along its links: the leaf it ends at, its length, and how many bones it holds (<code>i</code> included). Independent of pose.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>i</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-longestchild">modules/rigStructure/longestChild</h2> <pre><code class="language-lua">longestChild(s: Structure, i: number): number? </code></pre> <p>The child of <code>i</code> whose chain runs longest (a limb's next joint, whatever the pose), or nil for a leaf.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>i</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-mirrorscore">modules/rigStructure/mirrorScore</h2> <pre><code class="language-lua">mirrorScore(s: Structure, a: number, b: number, origin: { number }, side: { number }): number </code></pre> <p>How well the subtrees rooted at <code>a</code> and <code>b</code> mirror each other across the plane through <code>origin</code> whose normal is <code>side</code>: 1 when <code>b</code>'s farthest point reflects onto <code>a</code>'s and their chains are the same length, falling toward 0 as either differs.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>a</code> <code>number</code></li> <li><code>b</code> <code>number</code></li> <li><code>origin</code> <code>{ number }</code> — The point the mirror plane passes through.</li> <li><code>side</code> <code>{ number }</code> — The mirror plane's normal (need not be unit length).</li> </ul> <h2 id="modules-rigstructure-path">modules/rigStructure/path</h2> <pre><code class="language-lua">path(s: Structure, from: number, to: number): { number }? </code></pre> <p>The bones from <code>from</code> down to its descendant <code>to</code>, both included, or nil when <code>to</code> is not below <code>from</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>from</code> <code>number</code></li> <li><code>to</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-reach">modules/rigStructure/reach</h2> <pre><code class="language-lua">reach(s: Structure, i: number, dir: { number }): number </code></pre> <p>How far the subtree rooted at <code>i</code> reaches along <code>dir</code>: the largest projection of any of its bones' positions onto <code>dir</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>i</code> <code>number</code></li> <li><code>dir</code> <code>{ number }</code></li> </ul> <h2 id="modules-rigstructure-sameskeleton">modules/rigStructure/sameSkeleton</h2> <pre><code class="language-lua">sameSkeleton(a: Structure, b: Structure): boolean </code></pre> <p>Whether two structures are one skeleton: every bone both carry has the same parent and the same link length as a share of the links they share (within 1%), and every bone only one of them carries is a leaf (a prop socket, an end marker). No pose and no bone roll changes the answer, so a body and the clips authored on it match.</p> <p><strong>Parameters</strong></p> <ul> <li><code>a</code> <code>Structure</code> — A structure.</li> <li><code>b</code> <code>Structure</code> — A structure.</li> </ul> <h2 id="modules-rigstructure-shapescore">modules/rigStructure/shapeScore</h2> <pre><code class="language-lua">shapeScore(s: Structure, a: number, b: number): number </code></pre> <p>How alike the subtrees at <code>a</code> and <code>b</code> are in shape, 0..1, independent of pose: the length of their longest chains, the bones along them, and their leaf counts. Two limbs of a mirrored pair score near 1 in any pose.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>a</code> <code>number</code></li> <li><code>b</code> <code>number</code></li> </ul> <h2 id="modules-rigstructure-size">modules/rigStructure/size</h2> <pre><code class="language-lua">size(s: Structure): number </code></pre> <p>The length of the longest root-to-leaf chain: a size for the skeleton that no pose changes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> </ul> <h2 id="modules-rigstructure-subtree">modules/rigStructure/subtree</h2> <pre><code class="language-lua">subtree(s: Structure, i: number): { number } </code></pre> <p>Every bone in the subtree rooted at <code>i</code>, <code>i</code> first.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>Structure</code></li> <li><code>i</code> <code>number</code></li> </ul> <h2 id="modules-rigs-readme">modules/rigs/README</h2> <pre><code class="language-lua">require("@builtin::tests.fixtures.rigs") -- rigs </code></pre> <p>Rig documents built in code for the rig structure, avatar type and retarget suites: a 1.8 m biped in six bone-naming conventions, with pose, orientation, spine-length, twist-bone, mirrored-name and extra-chain variants, plus three bodies that are not humanoids. Bones are laid out as world positions (facing +Z, up +Y, the character's left +X) and written as rest translations under identity rotations, so every fixture is exact and readable.</p> <p>Usage: local rigs = require("@builtin::tests.fixtures.rigs")</p> <h2 id="modules-rigs-bend">modules/rigs/bend</h2> <pre><code class="language-lua">bend(bones: { Bone }, key: string, axis: { number }, deg: number) </code></pre> <p>A 1.8 m biped rig document.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bones</code> <code>{ Bone }</code></li> <li><code>key</code> <code>string</code></li> <li><code>axis</code> <code>{ number }</code></li> <li><code>deg</code> <code>number</code></li> </ul> <h2 id="modules-rigs-fox">modules/rigs/fox</h2> <pre><code class="language-lua">fox(): any </code></pre> <p>A four-legged body: a horizontal spine, two leg pairs to the ground, a neck and head forward, and a tail.</p> <h2 id="modules-rigs-nameof">modules/rigs/nameOf</h2> <pre><code class="language-lua">nameOf(conv: string, role: string, opts: any?): string? </code></pre> <p>The bone name a convention gives a humanoid role, as the fixture builds it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>conv</code> <code>string</code> — mixamo | cats | vrm | unreal | synty | unnamed</li> <li><code>role</code> <code>string</code> — A humanoid role (<code>hips</code>, <code>lefthand</code>, <code>righttoebase</code>, ...).</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — The same options passed to <code>biped</code>, when they change names (spine count).</li> </ul> <h2 id="modules-rigs-onearmed">modules/rigs/oneArmed</h2> <pre><code class="language-lua">oneArmed(): any </code></pre> <p>A torso with one arm and one leg: not a humanoid, since it has no leg pair.</p> <h2 id="modules-rigs-plant">modules/rigs/plant</h2> <pre><code class="language-lua">plant(): any </code></pre> <p>A plant: a trunk rising from the ground with mirrored branches.</p> <h2 id="modules-rt-ao-readme">modules/rt_ao/README</h2> <pre><code class="language-lua">require("@builtin::modules.rt_ao") -- rt_ao </code></pre> <p>Ray-traced ambient occlusion — the settings the <code>rt_ao</code> render feature runs on, and the lifetime of the pass that draws it. A hemisphere of short rays per pixel is traced against the scene acceleration structure, and the fraction that hits nearby geometry darkens the frame.</p> <p>Usage: local rt_ao = require("@builtin::modules.rt_ao")</p> <h2 id="modules-rt-ao-active">modules/rt_ao/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the occlusion pass is running. Read from the live feature registry, so a feature created directly through <code>renderer.feature.create</code> counts the same as one this module started.</p> <pre><code class="language-lua">if rt_ao.active() then print(rt_ao.stats().raysPerFrame) end </code></pre> <h2 id="modules-rt-ao-clear">modules/rt_ao/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Turn occlusion off and release the pass. The other settings are kept, so a later <code>set({ strength = ... })</code> brings back the same look.</p> <pre><code class="language-lua">rt_ao.clear() </code></pre> <h2 id="modules-rt-ao-get">modules/rt_ao/get</h2> <pre><code class="language-lua">get(): RtAoState </code></pre> <p>The occlusion settings currently in force.</p> <pre><code class="language-lua">local rays = rt_ao.get().rays </code></pre> <h2 id="modules-rt-ao-publish">modules/rt_ao/publish</h2> <pre><code class="language-lua">publish(built: { traceWidth: number, traceHeight: number, dispatches: number, traceTarget: string? }) </code></pre> <p>Record the grid the feature allocated and the dispatches it enqueues. The feature calls this as it builds, which is what gives <code>stats</code> the shape of the chain that is running.</p> <p><strong>Parameters</strong></p> <ul> <li><code>built</code> <code>{ traceWidth: number, traceHeight: number, dispatches: number, traceTarget: string? }</code> — The grid and dispatch count the feature just created.</li> </ul> <pre><code class="language-lua">rt_ao.publish({ traceWidth = 960, traceHeight = 540, dispatches = 9, traceTarget = rt.guid }) </code></pre> <h2 id="modules-rt-ao-qualitylevels">modules/rt_ao/qualityLevels</h2> <pre><code class="language-lua">qualityLevels(): { [string]: { rays: number, resolution: number } } </code></pre> <p>What each quality level costs: <code>rays</code> hemisphere rays per pixel, cast from a grid <code>resolution</code> of the frame's own. The rays a frame traces is those two multiplied by the frame's pixels, and the trace pass's cost is linear in it.</p> <pre><code class="language-lua">local shape = rt_ao.qualityLevels().ultra </code></pre> <h2 id="modules-rt-ao-set">modules/rt_ao/set</h2> <pre><code class="language-lua">set(opts: RtAoOpts?): RtAoState </code></pre> <p>Set how the occlusion is traced. Any omitted field keeps its current value, so a call can move one knob without restating the rest. A <code>strength</code> of 0 turns occlusion off and releases the pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>RtAoOpts?</code> <em>(optional)</em> — Occlusion settings — see <code>RtAoOpts</code>.</li> </ul> <pre><code class="language-lua">rt_ao.set({ quality = "low", radius = 1.5 }) </code></pre> <h2 id="modules-rt-ao-stats">modules/rt_ao/stats</h2> <pre><code class="language-lua">stats(): RtAoStats </code></pre> <p>What the running feature built — the grid it traces from, the rays that grid costs each frame, and the dispatches it enqueues. The feature writes these as it allocates its targets, so they describe the pass that exists this frame; <code>traceTarget</code> is that grid as a resource, for a reader that wants to measure it.</p> <pre><code class="language-lua">local s = rt_ao.stats(); print(s.traceWidth, s.traceHeight, s.raysPerFrame) </code></pre> <h2 id="modules-runtime-participation-readme">modules/runtime_participation/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.runtime_participation") -- runtime_participation </code></pre> <p>Reads and writes the RuntimeParticipation lifecycle axis for an entity, and answers the save / edit-liveness / play-liveness questions that follow from a mode. One source of truth for the four modes (WorldEntity, PrototypeOnly, EditorOnly, RuntimeOnly) so save and play-mode code agree on what each mode means.</p> <p>Usage: local runtime_participation = require("@builtin::modules.api.engine.runtime_participation")</p> <h2 id="modules-runtime-participation-issaved">modules/runtime_participation/isSaved</h2> <pre><code class="language-lua">isSaved(mode: string): boolean </code></pre> <p>Whether an entity with this mode is written to the persisted world. True for WorldEntity, PrototypeOnly, and EditorOnly; false for RuntimeOnly.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — A RuntimeParticipation mode string.</li> </ul> <h2 id="modules-runtime-participation-liveinedit">modules/runtime_participation/liveInEdit</h2> <pre><code class="language-lua">liveInEdit(mode: string): boolean </code></pre> <p>Whether an entity with this mode is live while authoring in edit mode. True for WorldEntity, PrototypeOnly, and EditorOnly; false for RuntimeOnly.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — A RuntimeParticipation mode string.</li> </ul> <h2 id="modules-runtime-participation-liveinplay">modules/runtime_participation/liveInPlay</h2> <pre><code class="language-lua">liveInPlay(mode: string): boolean </code></pre> <p>Whether an entity with this mode is live during play. True for WorldEntity and RuntimeOnly; false for PrototypeOnly and EditorOnly.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — A RuntimeParticipation mode string.</li> </ul> <h2 id="modules-runtime-participation-modeof">modules/runtime_participation/modeOf</h2> <pre><code class="language-lua">modeOf(entityId: string): string </code></pre> <p>The entity's RuntimeParticipation mode. Defaults to "WorldEntity".</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string</code> — Entity id to read.</li> </ul> <h2 id="modules-runtime-participation-set">modules/runtime_participation/set</h2> <pre><code class="language-lua">set(entityId: string, mode: string) </code></pre> <p>Sets the RuntimeParticipation mode on an entity. A mode that is not saved marks the entity temporary so the scene-save exclusion drops it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string</code> — Entity id to write.</li> <li><code>mode</code> <code>string</code> — The mode to store.</li> </ul> <h2 id="modules-runtime-participation-standsdown">modules/runtime_participation/standsDown</h2> <pre><code class="language-lua">standsDown(mode: string, engineMode: string): boolean </code></pre> <p>Whether an entity with this mode stands down — stops rendering and ticking — when an EDITOR session is in <code>engineMode</code>. This is the question a mode flip actually asks, and it is not <code>liveInPlay</code>: that answers which entities a SHIPPED RUNTIME contains, where there is no authoring surface at all. A session able to flip modes is an editor session by construction (the runtime profile forbids mode swaps), so the editor's own cameras, panels and gizmos are present in both of its modes and stand down in neither. What stands down in play is a template, whose clones are what runs; what stands down in edit is a runtime entity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>mode</code> <code>string</code> — A RuntimeParticipation mode string.</li> <li><code>engineMode</code> <code>string</code> — The engine mode the session is in, "play" or "edit".</li> </ul> <pre><code class="language-lua">local rp = require("@builtin::modules.api.engine.runtime_participation") if rp.standsDown(entity(id).participation, tostring(engine.mode)) then ... end </code></pre> <h2 id="modules-sceneproxy-readme">modules/sceneProxy/README</h2> <pre><code class="language-lua">require("@builtin::systems.sceneProxy.sceneProxy") -- sceneProxy </code></pre> <p>A coarse volumetric stand-in for the scene's geometry: a grid that answers how far the nearest surface is from any point, cheaply enough to ask along a whole ray.</p> <p>Usage: local sceneProxy = require("@builtin::systems.sceneProxy.sceneProxy")</p> <h2 id="modules-sceneproxy-build">modules/sceneProxy/build</h2> <pre><code class="language-lua">build(opts: ProxyOpts?): ({ [string]: any }?, string?) </code></pre> <p>Build the proxy over the scene as it currently stands. Allocates the grid, scatters the scene's triangles into it, and floods the result into a distance field.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>ProxyOpts?</code> <em>(optional)</em> — Grid settings — see the fields below. All are optional.</li> </ul> <pre><code class="language-lua">sceneProxy.build({ resolution = 64 }) </code></pre> <h2 id="modules-sceneproxy-building">modules/sceneProxy/building</h2> <pre><code class="language-lua">building(): boolean </code></pre> <p>Whether a build or refresh is running right now. Both yield while the GPU answers, so this is what a caller checks before starting one of its own over the same scene.</p> <pre><code class="language-lua">if not sceneProxy.building() then sceneProxy.build() end </code></pre> <h2 id="modules-sceneproxy-built">modules/sceneProxy/built</h2> <pre><code class="language-lua">built(): boolean </code></pre> <p>Whether a field is built.</p> <pre><code class="language-lua">if sceneProxy.built() then ... end </code></pre> <h2 id="modules-sceneproxy-claim">modules/sceneProxy/claim</h2> <pre><code class="language-lua">claim(key: string, opts: TrackOpts?): boolean </code></pre> <p>Ask for the field to be kept current, on behalf of something that will say when it no longer wants it. Tracking follows the claims that stand: it is on while there is at least one, and the settings in force are those of the claim made most recently. Two scene objects each wanting a proxy therefore share one grid, and neither turns the other's off.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — What is asking — anything that names the claimant, an entity id for a component.</li> <li><code>opts</code> <code>TrackOpts?</code> <em>(optional)</em> — What to follow and how closely, as <code>track</code> takes them.</li> </ul> <pre><code class="language-lua">sceneProxy.claim(entityId, { resolution = 64 }) </code></pre> <h2 id="modules-sceneproxy-claimed">modules/sceneProxy/claimed</h2> <pre><code class="language-lua">claimed(key: string): boolean </code></pre> <p>Whether a claim made under this key still stands. A release from elsewhere — <code>destroy</code> drops every claim there is — is what this answers false after, so something whose presence IS the request for a proxy can make it again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The key the claim was made under.</li> </ul> <pre><code class="language-lua">if not sceneProxy.claimed(id) then sceneProxy.claim(id, opts) end </code></pre> <h2 id="modules-sceneproxy-destroy">modules/sceneProxy/destroy</h2> <pre><code class="language-lua">destroy() </code></pre> <p>Release the proxy's grid and its working volumes, and with them every claim on it and any build or refresh still in flight — what that work was making is a grid nothing asked for any more.</p> <pre><code class="language-lua">sceneProxy.destroy() </code></pre> <h2 id="modules-sceneproxy-distanceat">modules/sceneProxy/distanceAt</h2> <pre><code class="language-lua">distanceAt(point: { [string]: number }): number? </code></pre> <p>The world-space distance from <code>point</code> to the nearest surface. Reads the field the way a shader does — interpolated between voxel centres, and <code>far</code> outside the extent the grid covers. Yields while the GPU answers, so call it from a task or <code>execute</code>; a shader samples the field directly instead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>point</code> <code>{ [string]: number }</code> — <code>{ x, y, z }</code> world position.</li> </ul> <pre><code class="language-lua">local d = sceneProxy.distanceAt({ x = 0, y = 2, z = 0 }) </code></pre> <h2 id="modules-sceneproxy-gridparams">modules/sceneProxy/gridParams</h2> <pre><code class="language-lua">gridParams(): { number }? </code></pre> <p>The grid a shader needs to read the field: the two vec4 that <code>scene_proxy.shaderModule</code>'s <code>spGridFrom</code> unpacks, as eight floats ready to write into a consuming pass's own parameter buffer.</p> <pre><code class="language-lua">local p = sceneProxy.gridParams(); myParams:write(p) </code></pre> <h2 id="modules-sceneproxy-maintain">modules/sceneProxy/maintain</h2> <pre><code class="language-lua">maintain(): boolean </code></pre> <p>One maintenance tick: build the field when tracking is armed and nothing is built yet, otherwise walk a slice of the scene and start a refresh once a whole walk has found that the geometry the proxy covers moved. Cheap on a scene standing still, bounded on a big one, and never yields — the work it starts runs as its own task, which is what lets a component <code>update</code> drive it.</p> <pre><code class="language-lua">function update() sceneProxy.maintain() end </code></pre> <h2 id="modules-sceneproxy-refresh">modules/sceneProxy/refresh</h2> <pre><code class="language-lua">refresh(): (boolean, string?) </code></pre> <p>Rebuild the field from the scene's current geometry, over the grid the proxy already has. What to call after something moves.</p> <pre><code class="language-lua">sceneProxy.refresh() </code></pre> <h2 id="modules-sceneproxy-release">modules/sceneProxy/release</h2> <pre><code class="language-lua">release(key: string): boolean </code></pre> <p>Drop a claim. The grid is released when it was the last one standing; otherwise the claim made most recently before it takes the settings back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The key the claim was made under.</li> </ul> <pre><code class="language-lua">sceneProxy.release(entityId) </code></pre> <h2 id="modules-sceneproxy-sampleat">modules/sceneProxy/sampleAt</h2> <pre><code class="language-lua">sampleAt(points: { any }): ({ { distance: number, normal: { number } } }?, string?) </code></pre> <p>Ask the field about a batch of world points in one dispatch. Yields while the GPU answers, so call it from a task or <code>execute</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>points</code> <code>{ any }</code> — A list of world positions, each <code>{ x, y, z }</code> or <code>{ X, Y, Z }</code>.</li> </ul> <pre><code class="language-lua">local s = sceneProxy.sampleAt({ { x = 0, y = 2, z = 0 } }) </code></pre> <h2 id="modules-sceneproxy-settings">modules/sceneProxy/settings</h2> <pre><code class="language-lua">settings(): { [string]: any }? </code></pre> <p>The grid in force, the world extent it covers, and what it cost. <code>boundsMin</code> and <code>boundsMax</code> are the corners of the box the grid spans — <code>resolution</code> voxels along each axis from <code>boundsMin</code>, and what a point has to fall inside to have an answer. The geometry the grid was fitted to sits one voxel inside them at each end. <code>gridParams()</code> is what a consuming shader wants — the same numbers in the layout <code>spGridFrom</code> reads.</p> <pre><code class="language-lua">local s = sceneProxy.settings(); print(s.voxelSize, s.memoryBytes) </code></pre> <h2 id="modules-sceneproxy-track">modules/sceneProxy/track</h2> <pre><code class="language-lua">track(opts: TrackOpts?): boolean </code></pre> <p>Keep the field current by itself: from here on the proxy watches the geometry it covers and refreshes when that geometry moved. Returns at once — it records what to follow and leaves the work to <code>maintain</code>, which something has to drive each frame; <code>SceneProxy.component</code> is that driver for an authored scene. A proxy that is not built yet is built by the first tick, at <code>resolution</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>TrackOpts?</code> <em>(optional)</em> — What to follow and how closely — see the fields below. All are optional.</li> </ul> <pre><code class="language-lua">sceneProxy.track({ resolution = 64, interval = 0.2 }) </code></pre> <h2 id="modules-sceneproxy-tracking">modules/sceneProxy/tracking</h2> <pre><code class="language-lua">tracking(): boolean </code></pre> <p>Whether the proxy is following the scene.</p> <pre><code class="language-lua">if not sceneProxy.tracking() then sceneProxy.track() end </code></pre> <h2 id="modules-sceneproxy-trackingstats">modules/sceneProxy/trackingStats</h2> <pre><code class="language-lua">trackingStats(): { [string]: any } </code></pre> <p>What tracking is set to follow, what it has done, and what it costs: the settings in force, how many claims stand, how many entities the last completed walk over the scene looked at and how long the last slice of one took, how many GPU-driven populations the field standing in the grid covers, how many builds, refreshes and refitting rebuilds it has started, how many ticks it stood aside for work already running, and the last error a walk or a refresh reported.</p> <pre><code class="language-lua">print(sceneProxy.trackingStats().refreshes) </code></pre> <h2 id="modules-sceneproxy-untrack">modules/sceneProxy/untrack</h2> <pre><code class="language-lua">untrack() </code></pre> <p>Stop keeping the field current. The grid stays built and readable; it simply stops following the scene.</p> <pre><code class="language-lua">sceneProxy.untrack() </code></pre> <h2 id="modules-scene-instantiable-readme">modules/scene_instantiable/README</h2> <pre><code class="language-lua">require("@builtin::assetTypes.assetType.shared.instantiable") -- scene_instantiable </code></pre> <p>The instantiation contract's shared half. An asset type opts into scene instantiation by defining <code>instantiate(self, target?, opts?)</code> on its behaviour <code>ref</code> table. Both halves of that call are shared, so a caller writes the same code against every type. IN — the base opts <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>name</code>, <code>temporary</code> mean the same thing for every type, so their implementation lives here. <code>rotation</code> takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion. <code>root</code> stands the root entity (parented, born temporary, named, placed) and <code>place</code> applies the placement opts to a root the type adopted (a bundle exploding onto its target). OUT — every type returns <code>(root, idMap)</code> through <code>result</code>: the composed root as a LIVE <code>EntityRef</code>, and the <code>originalId -> runtimeId</code> map naming what the composition spawned (<code>{}</code> for a type with no addressable children). Composition is synchronous — the root is usable the moment the call returns. <code>AssetRef.instantiate</code> is dispatched through this same check whether or not the type called it, so the two values a caller gets back never depend on which asset it held. Also registers the <code>sceneInstantiable</code> field-constraint validator: a constrained value must be an asset whose type defines <code>instantiate</code> (<code>ref:canInstantiate()</code>), which is what makes <code>Field.instantiableRef</code> accept by CAPABILITY instead of a hardcoded type list. <code>nil</code> (no asset) passes — the field is optional.</p> <p>Usage: local scene_instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")</p> <h2 id="modules-scene-instantiable-isowned">modules/scene_instantiable/isOwned</h2> <pre><code class="language-lua">isOwned(opts: { [string]: any }?): boolean </code></pre> <p>Whether this <code>instantiate</code> call already has an owner. The <code>Asset</code> / <code>SceneModule</code> components drive <code>instantiate</code> themselves and tag the call with <code>sourceTag</code>; they hold the asset reference, persist the composition's <code>idMap</code>, and re-run the composition on every load. A call with no tag came straight from <code>ref:instantiate(...)</code> and has no such owner, so a type whose composition must survive a reload composes, then hands the result one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — The <code>instantiate</code> opts table (nil-safe).</li> </ul> <pre><code class="language-lua">if not Instantiable.isOwned(opts) then ... end </code></pre> <h2 id="modules-scene-instantiable-own">modules/scene_instantiable/own</h2> <pre><code class="language-lua">own(root: any, self: any, idMap: { [string]: string }?): any </code></pre> <p>Hand an ALREADY-COMPOSED root to an <code>Asset</code> component pointing at <code>self</code>. The type composes first and calls this last: the component adopts the composition standing on <code>root</code> rather than building a second one, and from then on owns the reference — it keeps <code>idMap</code> in a persisted field and re-composes with those same ids on the next load, so cross-entity references into the composition (<code>SkinnedModel.skeletonRoot</code>) stay valid. Composed children never reach <code>scene.json</code>; the scene stores the reference and re-composes from it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>root</code> <code>any</code> <em>(optional)</em> — The root entity, as an <code>EntityRef</code> proxy, with the composition live.</li> <li><code>self</code> <code>any</code> <em>(optional)</em> — The asset ref being instantiated.</li> <li><code>idMap</code> <code>{ [string]: string }?</code> <em>(optional)</em> — The <code>original_id -> runtime id</code> map naming that composition.</li> </ul> <pre><code class="language-lua">if not Instantiable.isOwned(opts) then Instantiable.own(root, self, freshMap) end </code></pre> <h2 id="modules-scene-instantiable-place">modules/scene_instantiable/place</h2> <pre><code class="language-lua">place(root: any, opts: { [string]: any }?): any </code></pre> <p>Apply the base placement opts to a root the type already has — the adopt path (a bundle exploding onto its target, a sceneModule reconciling under one). <code>position</code> / <code>rotation</code> / <code>scale</code> land on the root's local transform; <code>name</code> renames it. <code>rotation</code> takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion.</p> <p><strong>Parameters</strong></p> <ul> <li><code>root</code> <code>any</code> <em>(optional)</em> — The root entity, as an <code>EntityRef</code> proxy.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — The <code>instantiate</code> opts table (nil-safe).</li> </ul> <pre><code class="language-lua">return Instantiable.place(target, opts), idMap Instantiable.place(root, { rotation = { 0, 90, 0 } }) -- yaw 90° </code></pre> <h2 id="modules-scene-instantiable-result">modules/scene_instantiable/result</h2> <pre><code class="language-lua">result(self: any, root: any, idMap: { [string]: string }?): (any, { [string]: string }) </code></pre> <p>Return an <code>instantiate</code> through the contract — the OUT half, the counterpart of <code>root</code> / <code>place</code>. Checks that <code>root</code> is a live entity ref and normalises a missing map to <code>{}</code>, so every type hands its caller the same two values: the composed root, live on return, and the <code>originalId -> runtimeId</code> map naming what it spawned. <code>AssetRef</code> runs every <code>instantiate</code> through this on the way out, so a type that returns something else fails at its own call rather than handing a caller a nil root or a map that is sometimes absent.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em> — The asset ref being instantiated — named in the error.</li> <li><code>root</code> <code>any</code> <em>(optional)</em> — The composed root, as an <code>EntityRef</code> proxy.</li> <li><code>idMap</code> <code>{ [string]: string }?</code> <em>(optional)</em> — The <code>original_id -> runtime id</code> map, or nil for a type that spawns no addressable children.</li> </ul> <pre><code class="language-lua">return Instantiable.result(self, root, freshMap) return Instantiable.result(self, Instantiable.root(self, target, opts)) </code></pre> <h2 id="modules-scene-instantiable-root">modules/scene_instantiable/root</h2> <pre><code class="language-lua">root(self: any, target: any?, opts: { [string]: any }?): any </code></pre> <p>Stand the root entity for an asset type's <code>instantiate</code> — the whole base contract in one call. Validates <code>target</code> (an owning entity ref, or nil), spawns the root as its child (born temporary when <code>opts.temporary</code>, named <code>opts.name</code> else the asset's own name), and applies the placement opts. The type adds its components to the returned root; what it returns from <code>instantiate</code> is <code>(thisRoot, idMap)</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em> — The asset ref being instantiated.</li> <li><code>target</code> <code>any?</code> <em>(optional)</em> — Optional owning entity ref — the root spawns as its child.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — The <code>instantiate</code> opts table (nil-safe). <code>rotation</code> takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion.</li> </ul> <pre><code class="language-lua">local root = Instantiable.root(self, target, opts) </code></pre> <h2 id="modules-scene-loader-readme">modules/scene_loader/README</h2> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.loader") -- scene_loader </code></pre> <p>Luau-side scene loader. Reads scene.json v6 and v7, refuses versions outside that range with a typed error, dispatches to entity.spawn / component.add / lights.setup, auto-discovers the sibling entrypoint.luau via vfs.exists. v6 attaches the declarative player + camera blocks to the Scene proxy for procedural resolution; v7 stashes the string player intent and does no procedural player/camera spawn. Replaces the legacy Rust __layers.load FFI path; the Rust FFI remains for one release as a safety net but is no longer invoked by any in-tree caller. Delta-overlay aware: when a <code>scene_dirty/</code> directory exists next to canonical scene.json, the loader hands the merge off to <code>scene_saver.composeMerged</code> — canonical scene.json + dirty manifest overrides + per-entity overlay files = the assembled body. Single merge point so the load + promote paths can never disagree. See § 10 of the player-camera-unification integration design for the architecture rationale (pure Luau, performance budget ~5 ms per 1k-entity scene, ECS FFI is sufficient without new primitives).</p> <p>Usage: local scene_loader = require("@builtin::assetTypes.scene.shared.loader")</p> <h2 id="modules-scene-loader-afterrecordsapplied">modules/scene_loader/afterRecordsApplied</h2> <pre><code class="language-lua">afterRecordsApplied(fn: () -> ()): boolean </code></pre> <p>Run <code>fn</code> once every record of the scene load that is applying records right now has landed: every entity it states spawned, parented and carrying the components and attributes its record states. A component waking as one of those records is applied reads the entities beneath it in whatever state the records before it left them, and a record after it puts back what its own record says; work that reconciles those entities against something else (a placement rebuilding what its module now states) belongs after the last record, where nothing the load applies comes after it.</p> <p>Outside such a load, or once the load's records have all landed, there is nothing to wait for: <code>fn</code> runs before this returns, and an error it raises reaches the caller. Work that waits runs after the last record, in the order it was handed in, each function in its own protected call, so one that raises is logged and the rest still run.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> ()</code> — The work to run.</li> </ul> <pre><code class="language-lua">Loader.afterRecordsApplied(function() print(#entity.findAll(), "entities applied") end) </code></pre> <h2 id="modules-scene-loader-applypeerrecords">modules/scene_loader/applyPeerRecords</h2> <pre><code class="language-lua">applyPeerRecords(fn: () -> ()) </code></pre> <p>Apply records another session wrote: run <code>fn</code>, reading <code>applyingPeerRecords()</code> as true for as long as it runs. An error <code>fn</code> raises reaches the caller once the state is put back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>() -> ()</code> — The application.</li> </ul> <pre><code class="language-lua">Loader.applyPeerRecords(function() applyBody(eid, body) end) </code></pre> <h2 id="modules-scene-loader-applyscenefields">modules/scene_loader/applySceneFields</h2> <pre><code class="language-lua">applySceneFields(sceneProxy: any, body: any, opts: any?): (boolean, { string }) </code></pre> <p>Take a loaded scene to the scene-level fields a <code>scene.json</code> body states. A v7 body carries <code>player</code> as the string intent <code>"spawns"</code> or <code>"none"</code> (a missing or empty one reads <code>"spawns"</code>), which lands as <code>sceneProxy.playerIntent</code>; a v6 body carries <code>player</code> and <code>camera</code> as setting blocks instead, which land under <code>sceneProxy.settings</code>. A root scene's <code>lighting</code> block lands under <code>sceneProxy.settings.lighting</code>; an additive one leaves the host scene's lighting alone.</p> <p>A load applies every field. A write to the file of a scene already loaded passes <code>opts.previous</code>, the body the file held before it, and a field lands only where the two bodies differ, so a write that edits one field leaves a live change to another standing until the file states it. A write also leaves alone a field the scene already holds as the body states it, so the scene's own save, which writes what the scene holds, lands nothing and reports nothing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sceneProxy</code> <code>any</code> <em>(optional)</em> — The loaded scene's proxy.</li> <li><code>body</code> <code>any</code> <em>(optional)</em> — The decoded <code>scene.json</code> body.</li> <li><code>opts</code> <code>any?</code> <em>(optional)</em> — <code>{ additive: boolean?, source: string?, previous: table? }</code>: whether the scene is an additive layer, the path a warning names, and the body the file held before this one.</li> </ul> <pre><code class="language-lua">local Json = require("@builtin::modules.json") Loader.applySceneFields(layers.active, Json.decode(assert(vfs.read(path))), { previous = before }) </code></pre> <h2 id="modules-scene-loader-applyingpeerrecords">modules/scene_loader/applyingPeerRecords</h2> <pre><code class="language-lua">applyingPeerRecords(): boolean </code></pre> <p>Whether the records being applied to a loaded scene right now were written by another session sharing this world: a peer's edit, or what a peer's build placed, arriving as that peer's records. A build is run by the session the change that asks for it was made in, and what it places reaches every other session as the records it writes, so a placement waking while this reads true is one a peer's records put here, holding what that peer already built.</p> <pre><code class="language-lua">if Loader.applyingPeerRecords() then return end </code></pre> <h2 id="modules-scene-loader-applyingrecords">modules/scene_loader/applyingRecords</h2> <pre><code class="language-lua">applyingRecords(): boolean </code></pre> <p>Whether a scene load is applying its records, or running the work components handed in to run after them, right now. A build that runs inside such a load reruns on every load.</p> <pre><code class="language-lua">if Loader.applyingRecords() then log.info("inside a load") end </code></pre> <h2 id="modules-scene-loader-handbackhere">modules/scene_loader/handBackHere</h2> <pre><code class="language-lua">handBackHere(): boolean </code></pre> <p>Hand the frame back from inside the scene load the calling code runs in, on the terms the load hands it back between its own records: the code runs on the coroutine of a direct load outside any component's context, gameplay is paused, and this frame's slice (<code>FrameBudget</code>) is spent. The load then goes on with the caller on the next frame, reading as partly applied (<code>M.partlyApplied</code>) across the frames in between. Work a load runs as one piece (a placement that wakes in it building what its module states, run after the records) calls this between its own units, so it spreads across frames the way the records do. Anywhere else it returns at once.</p> <pre><code class="language-lua">if Loader.handBackHere() then print("this load hands the scene back here") end </code></pre> <h2 id="modules-scene-loader-partlyapplied">modules/scene_loader/partlyApplied</h2> <pre><code class="language-lua">partlyApplied(): string? </code></pre> <p>The scene a load is applying part way through its records while it has handed the frame back between two of them, or nil when no load is. Such a scene's layer holds only the records applied so far; the load goes on with the next record on a later frame. A load whose task has ended (cancelled with its records part applied) no longer counts.</p> <pre><code class="language-lua">while Loader.partlyApplied() ~= nil do task.wait() end </code></pre> <h2 id="recorddiff-readme">modules/scene_saver.recordDiff/README</h2> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver.recordDiff") -- scene_saver.recordDiff </code></pre> <p>The comparisons the saver's record diffs are built from: numeric-tolerant equality, the component name a record matches by, a record's component list read the way the comparison needs it, the fields a component wrote about its own runtime, and the attribute differences between two records.</p> <p>Usage: local scene_saver.recordDiff = require("@builtin::assetTypes.scene.shared.saver.recordDiff")</p> <h2 id="serialize-readme">modules/scene_saver.serialize/README</h2> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver.serialize") -- scene_saver.serialize </code></pre> <p>One live entity as the record a scene file carries: its transform, its components (each field value serialized, a runtime GPU handle reported once and left out), its name, parent, participation, network scope, render layer and attributes, and the reads the saver asks of an entity while it decides what to record.</p> <p>Usage: local scene_saver.serialize = require("@builtin::assetTypes.scene.shared.saver.serialize")</p> <h2 id="modules-scene-saver-readme">modules/scene_saver/README</h2> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver") -- scene_saver </code></pre> <p>Luau-side scene saver. Persists scene state via a delta-overlay model: Canonical (committed work) <scene folder>/scene.json Dirty (work-in-progress, deltas only) <scene folder>/scene_dirty/manifest.json scene-level config overrides (lighting, player, camera, format, version) <scene folder>/scene_dirty/entities/<id>.json one file per CHANGED entity: * full body — modified or newly spawned * tombstone — { tombstone = true } for despawn The dirty directory carries ONLY the deltas. Entities unchanged since the last canonical save have NO entry under entities/. Editing one cube in a 100k-entity scene writes exactly one ~1KB file per drain; the manifest is touched only when scene-level config changes. The entity-id list is NOT carried in the manifest — it's derived at load / promote time from <code>canonical.entities[]</code> plus a filesystem listing of <code>entities/</code>. Concurrent writes to per-entity files therefore never produce manifest-conflict orphans (the conflict surface is one entity at a time, not the whole-scene index). Tombstones survive in dirty until the next save (when <code>clearDirty</code> wipes the directory). An orphan tombstone — one targeting an entity that wasn't in canonical either (i.e. the entity was spawned and deleted inside the same edit session before save) — is a no-op at compose time, so the file shape is robust regardless of operation order. The composer (<code>composeMerged</code>) is the canonical merge point used by both this saver's promote/save paths AND <code>scene_loader.M.load</code>, so there's exactly one place that knows how to combine canonical with the overlay. Captures authored intent only: no spawner-managed entities (player identities, primary camera), no temporary entities, no runtime player position. FFI shapes (discovered at runtime — differ from plan): - entity.findAll() → array of {id: string, name: string} tables. - localPosition / localScale expose number components .x/.y/.z. - localRotation exposes quaternion components .x/.y/.z/.w. - entity(id).getParent() → parent entity proxy or nil; its <code>.id</code> is the id string. - entity(id).name → string name (direct field, no function call). - lighting snapshot is read directly from settings.lighting.</p> <p>Usage: local scene_saver = require("@builtin::assetTypes.scene.shared.saver")</p> <h2 id="modules-scene-saver-buildoutputof">modules/scene_saver/buildOutputOf</h2> <pre><code class="language-lua">buildOutputOf(savedRec: any?, pendingRec: any?): string? </code></pre> <p>The scene build whose output a pending edit is, or nil when an author made it. A scene build (a scene's <code>build.luau</code>, a <code>SceneModule</code> placement) writes its name, the record each entity stands for and what it stated onto every entity it places, and a rebuild that states something new restates that last part; those attributes travel in the records, so the answer is read off the two records alone. An edit is the build's output when the build placed the entity, the pending record holds exactly what the build stated (<code>SceneBuild.recordDrift</code> finds nothing an author changed or added), and, for an update, the build's statement is not the one the saved record holds. A removal is the build's when the build that placed the entity has run in this session and no longer states it (<code>SceneBuild.states</code>); an entity its build still states was taken away by an author, and the build's next run puts it back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>savedRec</code> <code>any?</code> <em>(optional)</em> — The record the scene's saved file holds for the entity, nil for an addition.</li> <li><code>pendingRec</code> <code>any?</code> <em>(optional)</em> — The record the scene's unsaved edits hold for it, nil for a removal.</li> </ul> <pre><code class="language-lua">local by = require("@builtin::assetTypes.scene.shared.saver").buildOutputOf(saved, pending) </code></pre> <h2 id="modules-scene-saver-componentdrivenfields">modules/scene_saver/componentDrivenFields</h2> <pre><code class="language-lua">componentDrivenFields(id: any): { [string]: { [string]: boolean } } </code></pre> <p>The component fields on a live entity whose current value a component callback wrote while the engine was in play, keyed by SHORT component name. A lamp component writing <code>Light.intensity</code> every frame reports <code>Light = { intensity = true }</code>; an authoring call writing that same field takes it back off. These name the running simulation's state rather than authored intent, so the record comparison passes over them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>any</code> <em>(optional)</em> — The live entity's id.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").componentDrivenFields("ent_0") </code></pre> <h2 id="modules-scene-saver-componentshortname">modules/scene_saver/componentShortName</h2> <pre><code class="language-lua">componentShortName(t: string): string </code></pre> <p>The component type's short name: the leaf of the identity every spelling of the component resolves to, so a saved record (<code>Model</code>, <code>Door</code>) and a live one (<code>@builtin::components.Model</code>, <code>doors.Door</code>) read alike.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>string</code> — A component type name in any form.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").componentShortName("@builtin::components.Model") </code></pre> <h2 id="modules-scene-saver-discardsceneedits">modules/scene_saver/discardSceneEdits</h2> <pre><code class="language-lua">discardSceneEdits(name: string, select: (string | { string })?): SceneEditDiscard </code></pre> <p>Take a scene's unbaked overlay edits back out, leaving the canonical file as what the scene carries. This is the second verdict on the edits <code>pendingSceneEdits</code> reports and a publication call refuses over: staging bakes them into the scene, this drops them. Each edit's overlay record is deleted and its entity withdrawn so no writer re-states it, whichever session wrote the record and however long it has stood.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The scene — a <code>.scene</code> folder path, a <code>scene.json</code> path, or a bare scene name.</li> <li><code>select</code> <code>(string | { string })?</code> <em>(optional)</em> — Which edits to drop: an entity id or entity name, or a list of them. Omitted, every edit the scene has pending.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").discardSceneEdits(layers.active.name) require("@builtin::assetTypes.scene.shared.saver").discardSceneEdits("/zero/source/scenes/main.scene", "look") </code></pre> <h2 id="modules-scene-saver-filessignature">modules/scene_saver/filesSignature</h2> <pre><code class="language-lua">filesSignature(target): string </code></pre> <p>What a scene's files hold, as one string: its canonical <code>scene.json</code> and every file of its dirty overlay, each with its bytes. Two reads give the same string exactly when neither the canonical file nor any overlay file changed between them, which is how a caller that rewrites files underneath a loaded scene tells whether it moved that scene.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>any</code> <em>(optional)</em> — The scene's authoring target: a <code>.scene</code> folder path, its <code>scene.json</code> path, or a bare scene name.</li> </ul> <pre><code class="language-lua">local before = require("@builtin::assetTypes.scene.shared.saver").filesSignature(layers.active.asset.path) </code></pre> <h2 id="modules-scene-saver-flushdirtyacrossframes">modules/scene_saver/flushDirtyAcrossFrames</h2> <pre><code class="language-lua">flushDirtyAcrossFrames() </code></pre> <p>Put every pending edit-mode mark into the dirty overlay the way the continuous writer does: one frame's budget of records at a time, with the frame handed back between shares, so the time the calling frame spends is set by the budget however many entities the marks name. Returns once the overlay holds every mark taken. Where the caller cannot yield the whole backlog is written in the one frame, as <code>M.flushDirty</code> does. The edit to play flip calls this before it takes the write lock.</p> <pre><code class="language-lua">saver.flushDirtyAcrossFrames() </code></pre> <h2 id="modules-scene-saver-haspendingsceneedits">modules/scene_saver/hasPendingSceneEdits</h2> <pre><code class="language-lua">hasPendingSceneEdits(name: string): boolean </code></pre> <p>Whether baking the scene's dirty overlay into its canonical file would change the scene. False when the overlay is absent, and false when it holds only records the canonical file already states.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The scene — a <code>.scene</code> folder path, a <code>scene.json</code> path, or a bare scene name.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").hasPendingSceneEdits(layers.active.name) </code></pre> <h2 id="modules-scene-saver-notebuildremoved">modules/scene_saver/noteBuildRemoved</h2> <pre><code class="language-lua">noteBuildRemoved(eid: string) </code></pre> <p>Say that a scene build took the entity <code>eid</code> out of the world. A save of the scene then states the removal of the record the scene holds for it, however the build ran. A build that runs as its scene loads reaches <code>scene.json</code> this way and records nothing in the scene's unsaved edits.</p> <p><strong>Parameters</strong></p> <ul> <li><code>eid</code> <code>string</code> — The entity id the build despawned.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").noteBuildRemoved(id) </code></pre> <h2 id="modules-scene-saver-overlayemptiesscene">modules/scene_saver/overlayEmptiesScene</h2> <pre><code class="language-lua">overlayEmptiesScene(name: string): { entities: number, tombstones: number, path: string }? </code></pre> <p>Whether the scene's dirty overlay carries a tombstone for every entity its saved <code>scene.json</code> holds, which composes the scene to nothing it was saved with. An overlay of a scene that is being edited removes some of its entities; one that removes all of them is a record of something other than an author's edits (a build that stated nothing, a damaged journal), so the loader loads the saved scene instead and the pending-edit verdict names it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The scene: its path or identity.</li> </ul> <pre><code class="language-lua">local emptied = Saver.overlayEmptiesScene("/source/scenes/arena.scene") </code></pre> <h2 id="modules-scene-saver-pendingrecords">modules/scene_saver/pendingRecords</h2> <pre><code class="language-lua">pendingRecords(): number </code></pre> <p>How many entity records the writer holds to write: the marks the engine has noticed, taken into the backlog, plus the records already waiting there. A caller about to wait for the writer reads this to know how long the wait is.</p> <pre><code class="language-lua">if saver.pendingRecords() > 64 then log.info("the writer is behind") end </code></pre> <h2 id="modules-scene-saver-pendingsceneedits">modules/scene_saver/pendingSceneEdits</h2> <pre><code class="language-lua">pendingSceneEdits(name: string): ({ PendingSceneEdit }, string?) </code></pre> <p>Every edit a scene's dirty overlay holds that baking it would write into the canonical scene file: an added entity, an updated one, a removed one, or a change to the scene's own lighting / player / camera config. An EMPTY result is the verdict that the canonical scene already says what the overlay says, whatever rows the overlay carries. Each entry names the entity and describes the difference.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The scene — a <code>.scene</code> folder path, a <code>scene.json</code> path, or a bare scene name.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").pendingSceneEdits("/zero/source/scenes/main.scene") </code></pre> <h2 id="modules-scene-saver-recorddifflines">modules/scene_saver/recordDiffLines</h2> <pre><code class="language-lua">recordDiffLines(oldRec: any, newRec: any, maxLines: number?): { string } </code></pre> <p>Human lines describing how a live entity record differs from the record the scene carries: transform channels, component fields, component additions and removals, and attribute additions, changes and removals. Compares authored intent: a component field participates where either record states it and while its live value is an author's (a field only the live record states was set after the scene's record was made, and one the instance wrote about its own runtime is left out), numbers compare within a float-noise epsilon, and asset references compare by the asset they name rather than by the shape they are written in. A field or transform channel a component drove in play holds the running simulation's state, which the scene's record puts back, so it states no difference. An EMPTY result is the "unchanged" verdict.</p> <p><strong>Parameters</strong></p> <ul> <li><code>oldRec</code> <code>any</code> <em>(optional)</em> — The scene's entity record.</li> <li><code>newRec</code> <code>any</code> <em>(optional)</em> — The live entity record.</li> <li><code>maxLines</code> <code>number?</code> <em>(optional)</em> — How many lines to report before summarizing the rest as "…".</li> </ul> <pre><code class="language-lua">local ss = require("@builtin::assetTypes.scene.shared.saver"); ss.recordDiffLines(saved, ss.serializeEntity(id)) </code></pre> <h2 id="modules-scene-saver-recordfieldvalue">modules/scene_saver/recordFieldValue</h2> <pre><code class="language-lua">recordFieldValue(v: any): (any, string?) </code></pre> <p>The durable form of one component field value — what a record states for it. An asset reference keeps the identity a later session re-resolves it from; a live GPU resource handle names a slot in this session's GPU registry, so it is left out and named in the second return instead. Reading a live field through this is what lets a consumer of a record compare what it holds against what the record says in one form.</p> <p><strong>Parameters</strong></p> <ul> <li><code>v</code> <code>any</code> <em>(optional)</em> — A field value read off a live component.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").recordFieldValue(model.material) -- { __ref = "..." }, nil </code></pre> <h2 id="modules-scene-saver-recordswritten">modules/scene_saver/recordsWritten</h2> <pre><code class="language-lua">recordsWritten(): number </code></pre> <p>How many entity records this session has written into a scene's dirty overlay, counted as each one lands. A record already holding the bytes it would be written with is left as it is and not counted. Read twice, the difference is the record writing done between the two reads, which is how a frame's share of a save or of the continuous writer is seen.</p> <pre><code class="language-lua">local before = Saver.recordsWritten() </code></pre> <h2 id="modules-scene-saver-respawnfromfiles">modules/scene_saver/respawnFromFiles</h2> <pre><code class="language-lua">respawnFromFiles(layer: any, target: string, change: (() -> any)?): any </code></pre> <p>Take a loaded scene's live layer to what its files hold: the layer is respawned from the canonical <code>scene.json</code> composed with the overlay records standing beside it, with the scene-load gate held so the entities that come back are not marked as edits. <code>change</code>, when given, runs first inside the same gate: it is the act that changes the files, such as a discard deleting the overlay. Once the overlay holds nothing, what this session keeps about the overlay's records is let go, as clearing the overlay lets it go. Returns once the respawn's detached work (player, camera, avatar) has settled. Raises when the respawn raises.</p> <p><strong>Parameters</strong></p> <ul> <li><code>layer</code> <code>any</code> <em>(optional)</em> — The layer holding the scene (a <code>SceneProxy</code> from <code>layers.list()</code>).</li> <li><code>target</code> <code>string</code> — The scene's authoring target, the path its overlay is filed under.</li> <li><code>change</code> <code>(() -> any)?</code> <em>(optional)</em> — Optional function run inside the gate before the respawn.</li> </ul> <pre><code class="language-lua">local Saver = require("@builtin::assetTypes.scene.shared.saver") Saver.respawnFromFiles(layers.active, layers.active.asset.path) </code></pre> <h2 id="modules-scene-saver-save">modules/scene_saver/save</h2> <pre><code class="language-lua">save(name, opts) </code></pre> <p>Write the live layer of scene <code>name</code> into its canonical <code>scene.json</code>: the overlay of the session's edits merged over what the file holds. A record waiting for a parent that no record and no entity provides stays in the overlay, where it is built once its parent arrives, so <code>scene.json</code> carries no record a load of it cannot place; such a record that is a build's output, when the build has since placed its record identity on a new entity or places it no longer, is taken out of the scene instead. <code>opts.layer</code> names the source layer (default: <code>name</code>), <code>opts.sourceGuid</code> and <code>opts.sourcePath</code> the scene it was loaded from. <code>opts.bake = true</code> is a build baking what it placed: an additive layer, which records no edits of its own, is then recorded into the scene's own folder for the save.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>any</code> <em>(optional)</em> — The scene to write: its path or identity.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — <code>{ layer?, sourceGuid?, sourcePath?, bake? }</code>.</li> </ul> <pre><code class="language-lua">saver.save("/source/scenes/arena.scene", { layer = "arena" }) </code></pre> <h2 id="modules-scene-saver-scenebodyforreading">modules/scene_saver/sceneBodyForReading</h2> <pre><code class="language-lua">sceneBodyForReading(name: string): any </code></pre> <p>The body a scene composes to, for a caller that only reads it. A scene with no dirty state composes to its saved <code>scene.json</code>, so the body is the one decoded for the file's current bytes and shared with every other reader of them: asking again costs no decode while the file holds still. A scene with dirty state is composed afresh, as <code>composedSceneBody</code> does. The tracker of canonical ids is left where the last load put it. The table belongs to its readers and is never written to; <code>composeMerged</code> is the call for a body the caller edits. A saved file that exists and that this engine cannot read (its bytes are not on this engine, or the caller cannot wait for them) raises, naming the file and why, as <code>composeMerged</code> does.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The scene: its path or identity.</li> </ul> <pre><code class="language-lua">local body = Saver.sceneBodyForReading("/source/scenes/arena.scene"); print(#body.entities) </code></pre> <h2 id="modules-scene-saver-sceneremoved">modules/scene_saver/sceneRemoved</h2> <pre><code class="language-lua">sceneRemoved(layerName: string) </code></pre> <p>Stop writing anything for the loaded layer <code>layerName</code>: the scene it was loaded from was removed from the world, so its dirty overlay has no scene left to belong to. The marks it holds and every mark it takes until it loads again are dropped, and nothing is written into the removed scene's folder, which would otherwise bring that folder back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>layerName</code> <code>string</code> — The layer's name, as <code>layers.list()</code> names it.</li> </ul> <pre><code class="language-lua">Saver.sceneRemoved(proxy.name) </code></pre> <h2 id="modules-scene-saver-scenereturned">modules/scene_saver/sceneReturned</h2> <pre><code class="language-lua">sceneReturned(layerName: string) </code></pre> <p>Write the loaded layer <code>layerName</code>'s edits again: a scene of that name loaded, so the layer has a scene to write into. Undoes <code>M.sceneRemoved</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>layerName</code> <code>string</code> — The layer's name, as <code>layers.list()</code> names it.</li> </ul> <pre><code class="language-lua">Saver.sceneReturned(proxy.name) </code></pre> <h2 id="modules-scene-saver-trackingreason">modules/scene_saver/trackingReason</h2> <pre><code class="language-lua">trackingReason(): string? </code></pre> <p>Why an authored scene edit made from THIS call's own context would not be recorded right now, or nil while it would be. A scene records its edits in edit mode, outside a scene load, outside the world entrypoint's respawn pass, and outside a component or entrypoint callback tick; inside any of those a spawn or a change reaches the live session and no file. This is the question an empty <code>pendingSceneEdits</code> list cannot answer on its own — the same list stands for "the scene is saved" and for "nothing is watching it".</p> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").trackingReason() </code></pre> <h2 id="modules-scene-saver-transformdifflines">modules/scene_saver/transformDiffLines</h2> <pre><code class="language-lua">transformDiffLines(oldRec: any, newRec: any, driven: { [string]: boolean }?): { string } </code></pre> <p>Diff lines for an entity's transform channels (position / rotation / scale), comparing the live record against the record the scene carries. A channel the saved record OMITTED defaults to the identity transform, so a move away from origin registers even for an entity saved at identity (which carries no transform block) — while an unmoved entity whose live record materializes the identity transform produces no line. A channel named in <code>driven</code> holds the running simulation's state and takes no part.</p> <p><strong>Parameters</strong></p> <ul> <li><code>oldRec</code> <code>any</code> <em>(optional)</em> — The scene's entity record (may omit <code>transform</code> or channels).</li> <li><code>newRec</code> <code>any</code> <em>(optional)</em> — The live entity record.</li> <li><code>driven</code> <code>{ [string]: boolean }?</code> <em>(optional)</em> — Channel names (<code>position</code> / <code>rotation</code> / <code>scale</code>) a component drove in play, as a set; nil compares every channel.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").transformDiffLines({}, { transform = { position = { 0, 1, 0 } } }) </code></pre> <h2 id="modules-scene-saver-transformdrivenchannels">modules/scene_saver/transformDrivenChannels</h2> <pre><code class="language-lua">transformDrivenChannels(id: any): { [string]: boolean } </code></pre> <p>The transform channels of a live entity whose current value a component callback wrote while the engine was in play, as a set. A camera rig turning its camera every frame reports <code>{ rotation = true }</code>; an authoring call writing that same channel takes it back off. These name the running simulation's state rather than authored intent, so the record comparison passes over them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>any</code> <em>(optional)</em> — The live entity's id.</li> </ul> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.saver").transformDrivenChannels("ent_0") </code></pre> <h2 id="modules-scene-saver-withoutdrivenstate">modules/scene_saver/withoutDrivenState</h2> <pre><code class="language-lua">withoutDrivenState(rec: any, base: any?): any </code></pre> <p>An entity record with the running simulation's state taken out: every transform channel and component field a component drove in play on the entity the record names carries the value <code>base</code> states for it, and is left out where <code>base</code> states none. With the scene's record as <code>base</code>, it is what a verdict writes into the scene for a live entity: accepting an author's change records that change and leaves the value a running component happened to hold that frame where the scene had it. With no <code>base</code>, it is what a write to the scene applies to a live entity in play, leaving each channel and field a running component drives to that component.</p> <p><strong>Parameters</strong></p> <ul> <li><code>rec</code> <code>any</code> <em>(optional)</em> — The entity record; its <code>id</code> names the entity whose driven state is read.</li> <li><code>base</code> <code>any?</code> <em>(optional)</em> — The record whose values the driven channels and fields take; nil leaves them out.</li> </ul> <pre><code class="language-lua">local ss = require("@builtin::assetTypes.scene.shared.saver"); ss.withoutDrivenState(ss.serializeEntity(id), saved) </code></pre> <h2 id="modules-scene-swap-orchestrator-readme">modules/scene_swap_orchestrator/README</h2> <pre><code class="language-lua">require("@builtin::assetTypes.scene.shared.swapOrchestrator") -- scene_swap_orchestrator </code></pre> <p>Drives multiplayer room transitions + the scene-swap gate around scene load/unload events. Subscribes to layers.onUnload + layers.onBeforeLoad and translates them into a leaveRoom -> gate.begin -> teardown -> gate.finish -> joinRoom sequence. Room key is <code>{worldGuid}/{profile}/{mode}/{sceneGuid}</code> — never name-based; world and scene segments are always GUIDs (stable across renames and collaborators), and <code>{profile}</code> (runtime/editor) keeps published and live peers apart. Without this orchestrator, scene swaps world would broadcast EntityDespawn to peers still subscribed to the old room, and transient teardown state would land in the dirty file.</p> <p>Usage: local scene_swap_orchestrator = require("@builtin::assetTypes.scene.shared.swapOrchestrator")</p> <h2 id="modules-scopes-readme">modules/scopes/README</h2> <pre><code class="language-lua">require("@builtin::modules.scopes") -- scopes (also available as global 'scopes') </code></pre> <p>Scopes: work you start together and that ends together, tied to what started it (an entity, a component, a scene, another scope), and the owning contexts every live resource is registered under. -- Load two things at once; neither outlives the load. task.spawn(function() scopes.run(function(s) s:spawn("terrain", function() task.wait(2) end) s:spawn("props", function() task.wait(1) end) end) print("both are done") end) -- What is live, and what will never be reached by a seam? for _, r in scopes.list() do if not r.endsAtASeam then print(r.kind, r.handle, r.scope, r.detail) end end</p> <p>Start several pieces of work that must not outlive the thing that started them, and wait for all of them: scopes.run(function(s) s:spawn("load-terrain", function() ... end) s:spawn("load-props", function() ... end) end) -- returns once both are finished or cancelled; the first error either -- raised is raised again here, after the other one was cancelled Start work tied to an entity, without waiting for it: local s = scopes.open({ entity = npc }) s:spawn("patrol", function() while true do ... task.wait(1) end end) s:spawn("bark", function() ... end) -- despawning npc cancels both; so does s:cancel() Everything a task in a scope starts belongs to the scope too: a <code>task.spawn</code> inside it, a job it registers, a scope it opens. A scope is lived inside what the code opening it belongs to, so a scope opened in a component's hook ends with the component (destroyed or hot-reloaded), one opened by a scene's entrypoint ends with that load of the scene, and one opened in another scope's work ends with that scope. When a scope ends, every task still running in it is cancelled, and unwinds: the cancellation is raised at the wait it is suspended in, so a <code>pcall</code> or cleanup around that wait runs (<code>task.isCancellation(err)</code> recognises it). A scope ends when:</p> <ul> <li>its <code>scopes.run</code> is over;</li> <li>work in it raises: the rest of its work is cancelled, and <code>join</code> (and so <code>scopes.run</code>) raises that first error again where it is called. <code>s.onError = function(name, err) end</code> observes every error as it is raised. The VM-wide <code>task.onError</code> is not told while a <code>scopes.run</code> or a <code>join</code> waits for the scope or <code>onError</code> observes it; an error in a scope nothing waits for or observes is reported as any task's error is;</li> <li><code>s:cancel()</code>, <code>scopes.release(s.tag)</code>, or cancelling the task waiting in <code>scopes.run</code>;</li> <li>what it is lived inside ends: the entity it is tied to despawns, the component is destroyed or hot-reloaded, the scene load ends, the enclosing scope ends. <code>s:why()</code> says which ended it first, and is nil while the scope is open. A <code>scopes.run</code> whose scope something else ended returns once the work it started is cancelled, and <code>s:why()</code> names what ended it. The registry: a resource reached through a handle (a task, a substrate job, a camera feed, an input-map activation, an input subscription) outlives the call that made it. Each one records the context it was registered from, and the engine ends what a context left behind once that context's run is over. The contexts, by scope tag:</li> <li><code>scope:<n></code>: a task scope, as above.</li> <li><code>entity:<id></code>: an entity. Ends when it is despawned.</li> <li><code>scene:<guid>#<n></code>: one load of a scene layer. Ends when the load ends: the layer is reloaded or unloaded, the scene is swapped, the mode flips.</li> <li><code>chunk:<identity></code>: a module. Ends when the module runs again (a VFS write to its source) or is dropped from the require cache (<code>vfs.reload</code>). The next run registers its own.</li> <li><code>component:<instanceId></code>: one component instance. Ends when that instance is destroyed: its entity despawned, its scene cleared, the component removed. A hot reload of the component ends its tasks and the resources Luau code holds for it (listed with kind <code>luau</code>), and the new life's <code>awake</code> takes its own.</li> <li><code>exec:<chunk></code>: a chunk submitted for a single run, such as an <code>execute</code> call. It is never re-entered, so nothing ends it on its own. A <code>luau</code> resource (an input-map activation, an input subscription) taken by work in a task scope, such as a <code>future.spawn</code> body or a future's handler, follows what that scope runs inside rather than the scope: the holder keeps it after the work is over. <code>list()</code> shows every live resource by the context it follows, and <code>release(scope)</code> ends everything under one. That last is what reaches a resource no variable names any more: a task an <code>execute</code> call started and lost the handle of is listed under <code>exec:<chunk></code>, and releasing that tag cancels it.</li> </ul> <p>Usage: local scopes = require("@builtin::modules.scopes") Also available as global: scopes</p> <h2 id="modules-scopes-cancel">modules/scopes/cancel</h2> <pre><code class="language-lua">cancel(self: Scope, name: string?): number </code></pre> <p>Cancel the work named <code>name</code>, or end the whole scope when <code>name</code> is omitted. Cancelled work unwinds: its cleanup runs at the wait it is suspended in. Returns how many tasks this cancelled.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Scope</code></li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — A child's name; omit to end the scope.</li> </ul> <pre><code class="language-lua">s:cancel("patrol") </code></pre> <h2 id="modules-scopes-children">modules/scopes/children</h2> <pre><code class="language-lua">children(self: Scope): { ScopeChild } </code></pre> <p>What this scope started with <code>spawn</code>, in the order started, each with its current <code>task.status</code>: every child still running, and the 32 that ended most recently.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Scope</code></li> </ul> <pre><code class="language-lua">for _, c in s:children() do print(c.name, c.status) end </code></pre> <h2 id="modules-scopes-current">modules/scopes/current</h2> <pre><code class="language-lua">current(): string? </code></pre> <p>The scope the calling code registers a resource under right now: the module whose body is running, the task scope its work runs in, the component instance whose lifecycle hook is on the stack, or the chunk of this call. Nil when the caller registers under no context.</p> <pre><code class="language-lua">print("resources I register follow", scopes.current()) </code></pre> <h2 id="modules-scopes-join">modules/scopes/join</h2> <pre><code class="language-lua">join(self: Scope, name: string?) </code></pre> <p>Wait until the work named <code>name</code> (all of the scope's work when omitted) is finished or cancelled, its cleanup included. Raises the first error that work raised, as it was raised. Called from work in the scope (the function <code>scopes.run</code> runs, say), it waits for the rest of the work and not for the calling task. Waits, so call it from a task.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Scope</code></li> <li><code>name</code> <code>string?</code> <em>(optional)</em> — A child's name; omit to wait for everything in the scope.</li> </ul> <pre><code class="language-lua">s:join() </code></pre> <h2 id="modules-scopes-list">modules/scopes/list</h2> <pre><code class="language-lua">list(): { LiveResource } </code></pre> <p>Every live resource that follows an owning context, across every subsystem holding them. A resource registered with no context above it, the engine's own, is not listed, because no scope reaches it.</p> <pre><code class="language-lua">for _, r in scopes.list() do print(r.scope, r.kind, r.detail) end </code></pre> <h2 id="modules-scopes-open">modules/scopes/open</h2> <pre><code class="language-lua">open(options: ScopeOptions?): Scope </code></pre> <p>Open a task scope without waiting for it: start work in it with <code>s:spawn</code>, end it with <code>s:cancel()</code>, or let what it is lived inside end it (<code>{ entity = id }</code> ties it to an entity). Wait for its work with <code>s:join()</code> from a task.</p> <p><strong>Parameters</strong></p> <ul> <li><code>options</code> <code>ScopeOptions?</code> <em>(optional)</em> — <code>{ entity = id }</code> ties the scope to an entity; <code>{ name = "..." }</code> names it.</li> </ul> <pre><code class="language-lua">local s = scopes.open({ entity = npc }); s:spawn("patrol", patrol) </code></pre> <h2 id="modules-scopes-release">modules/scopes/release</h2> <pre><code class="language-lua">release(scope: string): { Released } </code></pre> <p>End every resource registered under <code>scope</code>, across every subsystem: tasks are cancelled, jobs dropped. Reaches contexts no seam does, such as the chunk of an <code>execute</code> call that started something and ended without ending it. An <code>execute</code> call runs as a task listed under its own <code>exec:</code> scope, so releasing that scope from the same call cancels the call too: release it from a later call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>scope</code> <code>string</code> — A scope tag, as the <code>scope</code> field of a <code>list()</code> row carries it.</li> </ul> <pre><code class="language-lua">local ended = scopes.release("exec:__exec_12") </code></pre> <h2 id="modules-scopes-run">modules/scopes/run</h2> <pre><code class="language-lua">run(fn: (Scope) -> ...any, options: ScopeOptions?): ...any </code></pre> <p>Run <code>fn(s)</code> in a new task scope and wait until every piece of work started in it is finished or cancelled. Returns what <code>fn</code> returned. The first error raised by <code>fn</code> or by any work in the scope cancels the rest, and is raised again here, as it was raised, once their cleanup has run. Waits, so it must be called from a task: in code that cannot wait (a component hook called directly, a callback) it raises, naming that.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>(Scope) -> ...any</code> — The function to run; it receives the scope, and starts work with <code>s:spawn</code>.</li> <li><code>options</code> <code>ScopeOptions?</code> <em>(optional)</em> — <code>{ entity = id }</code> ties the scope to an entity; <code>{ name = "..." }</code> names it.</li> </ul> <pre><code class="language-lua">scopes.run(function(s) s:spawn("a", fa); s:spawn("b", fb) end) </code></pre> <h2 id="modules-scopes-spawn">modules/scopes/spawn</h2> <pre><code class="language-lua">spawn(self: Scope, name: string, fn: (...any) -> ...any, ...: any): number </code></pre> <p>Start <code>fn(...)</code> as a task in this scope, named <code>name</code>, and return its task handle. The task, and everything it starts, belongs to the scope: it is cancelled (and unwinds) when the scope ends. Raises when the scope has already ended, naming why.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Scope</code></li> <li><code>name</code> <code>string</code> — What the work is called, in <code>children()</code>, <code>join(name)</code>, errors and listings.</li> <li><code>fn</code> <code>(...any) -> ...any</code> — The function to run.</li> <li><code>args</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">s:spawn("props", function() task.wait(1) end) </code></pre> <h2 id="modules-scopes-why">modules/scopes/why</h2> <pre><code class="language-lua">why(self: Scope): string? </code></pre> <p>Why the scope ended: its run finished, it was cancelled, a named child raised, or what it was lived inside ended (<code>entity:<id> ended</code>, <code>component:<id> ended</code>, ...). Nil while the scope is open.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>Scope</code></li> </ul> <pre><code class="language-lua">print(s:why()) </code></pre> <h2 id="modules-screenspacegi-readme">modules/screenSpaceGI/README</h2> <pre><code class="language-lua">require("@builtin::systems.screenSpaceGI.screenSpaceGI") -- screenSpaceGI </code></pre> <p>Indirect diffuse light gathered from the scene that was just drawn, so anything on screen bounces light onto its neighbours — including geometry that moved this frame, which a baked lightmap cannot follow.</p> <p>Usage: local screenSpaceGI = require("@builtin::systems.screenSpaceGI.screenSpaceGI")</p> <h2 id="modules-screenspacegi-active">modules/screenSpaceGI/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the screen-space GI pass is running this frame.</p> <pre><code class="language-lua">if screenSpaceGI.active() then ... end </code></pre> <h2 id="modules-screenspacegi-clear">modules/screenSpaceGI/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Turn screen-space GI off and release the pass. The other settings are kept, so a later <code>set({ intensity = ... })</code> brings back the same look.</p> <pre><code class="language-lua">screenSpaceGI.clear() </code></pre> <h2 id="modules-screenspacegi-get">modules/screenSpaceGI/get</h2> <pre><code class="language-lua">get(): SSGIState </code></pre> <p>The screen-space GI settings currently in force.</p> <pre><code class="language-lua">local r = screenSpaceGI.get().radius </code></pre> <h2 id="modules-screenspacegi-paramsbuffer">modules/screenSpaceGI/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.</p> <pre><code class="language-lua">local p = screenSpaceGI.paramsBuffer() </code></pre> <h2 id="modules-screenspacegi-set">modules/screenSpaceGI/set</h2> <pre><code class="language-lua">set(opts: SSGIOpts?): SSGIState </code></pre> <p>Set the scene's screen-space GI. Any omitted field keeps its current value. An <code>intensity</code> of 0 turns it off and releases the pass.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>SSGIOpts?</code> <em>(optional)</em> — Screen-space GI settings — see <code>SSGIOpts</code>.</li> </ul> <pre><code class="language-lua">screenSpaceGI.set({ intensity = 1.0, radius = 2.0 }) </code></pre> <h2 id="modules-service-readme">modules/service/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.service") -- service (also available as global 'service') </code></pre> <p>Credit-metered service invoke, buffered or streamed. Public Luau surface over the <code>__service</code> Internal FFI namespace.</p> <p>Usage: local service = require("@builtin::modules.api.engine.service") Also available as global: service</p> <h2 id="modules-service-authenticated">modules/service/authenticated</h2> <pre><code class="language-lua">authenticated(): boolean </code></pre> <p>Whether a platform identity (JWT) is available to attach to service calls. Returns only a boolean — never the token.</p> <pre><code class="language-lua">if not service.authenticated() then error("link ZeroMind") end </code></pre> <h2 id="modules-service-balance">modules/service/balance</h2> <pre><code class="language-lua">balance(): PromiseHandle? </code></pre> <p>Read the caller's credit balance from ZeroMind. Returns a promise handle for <code>task.await()</code> resolving the balance JSON, or nil when the gateway is unconfigured or no caller identity is available.</p> <pre><code class="language-lua">local h = service.balance(); local raw = h and task.await(h) </code></pre> <h2 id="modules-service-configuregateway">modules/service/configureGateway</h2> <pre><code class="language-lua">configureGateway(baseUrl: string, tokenIssuer: string?): boolean </code></pre> <p>TRUSTED ONLY. Set the ZeroMind base URL that <code>service.invoke</code>, <code>service.balance</code> and a trusted plugin's service calls target, and the issuer the JWTs it mints name as <code>iss</code>. The trusted-VM auth bootstrap calls this with the resolved issuer and token issuer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>baseUrl</code> <code>string</code> — ZeroMind base URL (e.g. "https://origozero.ai").</li> <li><code>tokenIssuer</code> <code>string?</code> <em>(optional)</em> — The <code>iss</code> the base URL's JWTs carry (optional; e.g. "https://api.origozero.ai"). Omitted, the base URL signs its own.</li> </ul> <pre><code class="language-lua">service.configureGateway("https://origozero.ai", "https://api.origozero.ai") </code></pre> <h2 id="modules-service-configureworld">modules/service/configureWorld</h2> <pre><code class="language-lua">configureWorld(guid: string): boolean </code></pre> <p>TRUSTED ONLY. Set the bound world guid attached to metered service invocations, so the credit ledger attributes each charge to the world it happened in. The trusted-VM world-load hook calls this on every bind so a runtime world switch re-points attribution.</p> <p><strong>Parameters</strong></p> <ul> <li><code>guid</code> <code>string</code> — The bound world's guid.</li> </ul> <pre><code class="language-lua">service.configureWorld(world.guid()) </code></pre> <h2 id="modules-service-gatewayconfigured">modules/service/gatewayConfigured</h2> <pre><code class="language-lua">gatewayConfigured(): boolean </code></pre> <p>Whether the ZeroMind service gateway has been configured. Service handlers use this to distinguish "gateway not configured" from "not signed in" when <code>invoke</code> returns nil.</p> <pre><code class="language-lua">if not service.gatewayConfigured() then error("no gateway") end </code></pre> <h2 id="modules-service-invoke">modules/service/invoke</h2> <pre><code class="language-lua">invoke(offering: string, endpoint: string, opts: InvokeOpts?): PromiseHandle? </code></pre> <p>Invoke a provider offering's logical endpoint through ZeroMind. Returns a promise handle for <code>task.await()</code> resolving the InvokeResponse JSON, or nil when the gateway is unconfigured or no caller identity is available. The JWT and real upstream URL are never exposed to Luau.</p> <p><strong>Parameters</strong></p> <ul> <li><code>offering</code> <code>string</code> — Fully-qualified offering identity <code>provider/name</code> (e.g. "origozero/mesh_gen").</li> <li><code>endpoint</code> <code>string</code> — Logical endpoint name (e.g. "create_preview").</li> <li><code>opts</code> <code>InvokeOpts?</code> <em>(optional)</em> — <code>{ params?, headers?, body?, max_credits?, idempotency_key? }</code>.</li> </ul> <pre><code class="language-lua">local h = service.invoke("origozero/mesh_gen", "create_preview", { body = { prompt = p } }) </code></pre> <h2 id="modules-service-invokestream">modules/service/invokeStream</h2> <pre><code class="language-lua">invokeStream(offering: string, endpoint: string, opts: StreamOpts): ServiceStream? </code></pre> <p>Invoke a provider offering's logical endpoint as a stream. ZeroMind relays the upstream's server-sent events as they arrive, then ends with a <code>zeromind.settled</code> event whose data is <code>{"charged":N,"balance":M}</code>, or a <code>zeromind.failed</code> event whose data is <code>{"reason":...}</code> when the relay stopped early. A call it refuses before the stream starts arrives as one <code>zeromind.refused</code> event carrying the HTTP <code>status</code> and the JSON answer as <code>data</code>; a call that could not be made, or a stream that broke or went quiet, arrives as <code>zeromind.failed</code>. Returns a stream at once, or nil when the gateway is unconfigured or no caller identity is available, exactly as <code>invoke</code>. Take events with <code>stream:poll()</code> (never waits) or <code>stream:read()</code> (waits for the next event; nil at the end), and release the connection with <code>stream:close()</code>. <code>opts</code> takes what <code>invoke</code> takes, plus a required <code>idempotency_key</code> that is unique to this call (ZeroMind refuses a key a streamed call already used) and an optional <code>timeout_ms</code> bounding the wait for the response head and for each next chunk (default 120000). The JWT and the real upstream URL are never exposed to Luau.</p> <p><strong>Parameters</strong></p> <ul> <li><code>offering</code> <code>string</code> — Fully-qualified offering identity <code>provider/name</code> (e.g. "origozero/llm").</li> <li><code>endpoint</code> <code>string</code> — Logical endpoint name (e.g. "chat").</li> <li><code>opts</code> <code>StreamOpts</code> — <code>{ params?, headers?, body?, idempotency_key, timeout_ms? }</code>.</li> </ul> <pre><code class="language-lua">local s = service.invokeStream("origozero/llm", "chat", { body = { messages = msgs }, idempotency_key = id }) while s do local ev = s:read(); if ev == nil then break end; print(ev.event, ev.data) end </code></pre> <h2 id="modules-service-jobstatus">modules/service/jobStatus</h2> <pre><code class="language-lua">jobStatus(jobId: string): PromiseHandle? </code></pre> <p>Poll a submitted service job. Returns a promise handle for <code>task.await()</code> resolving the JobStatusResponse JSON <code>{ job_id, status, result?, error? }</code>: <code>status</code> walks <code>pending</code>/<code>running</code> -> <code>succeeded</code> (with <code>result</code>, the same InvokeResponse <code>invoke</code> returns) or <code>failed</code> (with <code>error</code>). nil when the gateway is unconfigured or no caller identity is available.</p> <p><strong>Parameters</strong></p> <ul> <li><code>jobId</code> <code>string</code> — Job id returned by <code>submitJob</code>.</li> </ul> <pre><code class="language-lua">local h = service.jobStatus(jobId); local raw = h and task.await(h) </code></pre> <h2 id="modules-service-quote">modules/service/quote</h2> <pre><code class="language-lua">quote(offering: string, endpoint: string, opts: InvokeOpts?): PromiseHandle? </code></pre> <p>Read an exact tier price without charging or calling a provider.</p> <p><strong>Parameters</strong></p> <ul> <li><code>offering</code> <code>string</code></li> <li><code>endpoint</code> <code>string</code></li> <li><code>opts</code> <code>InvokeOpts?</code> <em>(optional)</em></li> </ul> <h2 id="modules-service-submitjob">modules/service/submitJob</h2> <pre><code class="language-lua">submitJob(offering: string, endpoint: string, opts: InvokeOpts?): PromiseHandle? </code></pre> <p>Submit a durable async invocation of an offering endpoint. Same arguments as <code>invoke</code>, but the provider round-trip runs server-side (off this connection), so a slow synchronous provider or a dropped link no longer loses the result. Returns a promise handle for <code>task.await()</code> resolving <code>{ job_id, status }</code>; poll it with <code>jobStatus</code>. nil when the gateway is unconfigured or no caller identity is available.</p> <p><strong>Parameters</strong></p> <ul> <li><code>offering</code> <code>string</code> — Fully-qualified offering identity <code>provider/name</code> (e.g. "origozero/mesh_gen").</li> <li><code>endpoint</code> <code>string</code> — Logical endpoint name (e.g. "create_preview").</li> <li><code>opts</code> <code>InvokeOpts?</code> <em>(optional)</em> — <code>{ params?, headers?, body?, max_credits?, idempotency_key? }</code>.</li> </ul> <pre><code class="language-lua">local h = service.submitJob("origozero/mesh_gen", "create_preview", { body = { prompt = p } }) </code></pre> <h2 id="modules-shader-readme">modules/shader/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.shader") -- shader (also available as global 'shader') </code></pre> <p>Shader compilation — turning authored WGSL into registered GPU programs, and the reusable modules those programs include. Public Luau surface over the <code>__shader</code> Internal FFI namespace.</p> <p>Usage: local shader = require("@builtin::modules.api.engine.shader") Also available as global: shader</p> <h2 id="modules-shader-compile">modules/shader/compile</h2> <pre><code class="language-lua">compile(keys: string | { string }, opts: { [string]: any }): boolean </code></pre> <p>Compile a zero-scaffolding SURFACE shader: the author wrote only <code>vertex()</code> / <code>fragment()</code> and declared its material properties, and the engine generates the group(1) material interface plus every render-mode entry point. Compiles once and registers the result under its key.</p> <p><strong>Parameters</strong></p> <ul> <li><code>keys</code> <code>string | { string }</code> — The shader key (<code>guid@checksum</code> of the version compiled), alone or as a one-element array.</li> <li><code>opts</code> <code>{ [string]: any }</code> — <code>{ source, domain?, properties?, replaces? }</code> — the author's WGSL, its <code>@domain</code>, the declared property schema, and the key this shader compiled under before its source changed, whose materials move to the new key.</li> </ul> <pre><code class="language-lua">local m = asset.meta(ref); shader.compile(m.guid .. "@" .. m.checksum, { source = wgsl, properties = props }) </code></pre> <h2 id="modules-shader-forget">modules/shader/forget</h2> <pre><code class="language-lua">forget(keys: string | { string }): boolean </code></pre> <p>Drop what a removed shader registered under its key: its programs and their variants, the composed WGSL, the compile status and the diagnostic. The pipelines built from those programs are evicted and the materials drawing through them re-prepare.</p> <p><strong>Parameters</strong></p> <ul> <li><code>keys</code> <code>string | { string }</code> — The shader key (<code>guid@checksum</code>), alone or in an array.</li> </ul> <pre><code class="language-lua">local m = asset.meta(ref); shader.forget(m.guid .. "@" .. m.checksum) </code></pre> <h2 id="modules-shader-registermodule">modules/shader/registerModule</h2> <pre><code class="language-lua">registerModule(keys: string | { string }, source: string): boolean </code></pre> <p>Register a block of WGSL other shaders include. Every key names the same source, so a shader includes it by whichever name it holds — its guid, its identity, or an alias. Registering again replaces it, and the shaders that include it recompile.</p> <p><strong>Parameters</strong></p> <ul> <li><code>keys</code> <code>string | { string }</code> — One key, or the array of keys this module answers to.</li> <li><code>source</code> <code>string</code> — The module's WGSL.</li> </ul> <pre><code class="language-lua">shader.registerModule({ ref.guid, ref.identity }, wgsl) </code></pre> <h2 id="modules-shader-status">modules/shader/status</h2> <pre><code class="language-lua">status(name: string): (string, string?) </code></pre> <p>A shader's latest compile outcome, without reading the engine log: <code>"compiled"</code>, <code>"failed"</code> (with the compiler error second), or <code>"pending"</code>. Compilation is async, so a <code>"pending"</code> straight after a write means ask again next frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The key it compiled under (<code>guid@checksum</code>), or a reference to a shader asset (identity, path, guid), which reaches the key of the version that reference resolves to now.</li> </ul> <pre><code class="language-lua">local status, err = shader.status("myShader") </code></pre> <h2 id="modules-shader-includes-readme">modules/shader_includes/README</h2> <pre><code class="language-lua">require("@builtin::modules.shader_includes") -- shader_includes </code></pre> <p>Resolves the <code>#include</code> lines of a WGSL source against the asset system, so a shader includes a <code>.shaderModule</code> by every name form <code>require</code> and <code>asset.resolve</code> accept.</p> <p>Usage: local shader_includes = require("@builtin::modules.shader_includes")</p> <h2 id="modules-shader-includes-canonicalize">modules/shader_includes/canonicalize</h2> <pre><code class="language-lua">canonicalize(source: string, base: string): string </code></pre> <p>Rewrite every <code>#include</code> in <code>source</code> whose literal names a <code>.shaderModule</code> to that module's guid, resolved against <code>base</code>, and bring every module the source reaches up to date with what the VFS serves. A literal that resolves to no asset is left as it was written, so a framework name reaches the expander unchanged.</p> <p><strong>Parameters</strong></p> <ul> <li><code>source</code> <code>string</code> — The WGSL as authored.</li> <li><code>base</code> <code>string</code> — VFS path of the asset the source belongs to — what a <code>~</code> / <code>~.tail</code> / <code>.relative.tail</code> literal expands against.</li> </ul> <pre><code class="language-lua">local wgsl = ShaderIncludes.canonicalize(vfs.read(path .. "/shader.wgsl"), path) </code></pre> <h2 id="modules-shader-includes-settle">modules/shader_includes/settle</h2> <pre><code class="language-lua">settle(): { string } </code></pre> <p>Compile again every shader whose last compile named a module that no asset answered to and that one now does — a shader written before the module it includes. Called when a <code>.shaderModule</code> registers. Each such shader is sent a <code>dependency</code> change, which its type answers by compiling.</p> <pre><code class="language-lua">ShaderIncludes.settle() </code></pre> <h2 id="modules-sharpening-readme">modules/sharpening/README</h2> <pre><code class="language-lua">require("@builtin::systems.sharpening.sharpening") -- sharpening </code></pre> <p>Contrast-adaptive sharpening. Restores local acuity to the finished frame, backing off wherever the neighbourhood has no headroom left, which is what keeps a bright fringe from forming along high-contrast edges.</p> <p>Usage: local sharpening = require("@builtin::systems.sharpening.sharpening")</p> <h2 id="modules-sharpening-active">modules/sharpening/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the sharpening pass is running this frame.</p> <pre><code class="language-lua">if sharpening.active() then ... end </code></pre> <h2 id="modules-sharpening-disable">modules/sharpening/disable</h2> <pre><code class="language-lua">disable() </code></pre> <p>Turn sharpening off and release the pass.</p> <pre><code class="language-lua">sharpening.disable() </code></pre> <h2 id="modules-sharpening-enable">modules/sharpening/enable</h2> <pre><code class="language-lua">enable(sharpness: number?): State </code></pre> <p>Turn sharpening on at a given strength.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sharpness</code> <code>number?</code> <em>(optional)</em> — Strength in [0, 1]. Omit to keep the current value.</li> </ul> <pre><code class="language-lua">sharpening.enable(0.5) </code></pre> <h2 id="modules-sharpening-get">modules/sharpening/get</h2> <pre><code class="language-lua">get(): State </code></pre> <p>The sharpening settings currently in force.</p> <pre><code class="language-lua">local s = sharpening.get().sharpness </code></pre> <h2 id="modules-sharpening-paramsbuffer">modules/sharpening/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.</p> <pre><code class="language-lua">local p = sharpening.paramsBuffer() </code></pre> <h2 id="modules-sharpening-set">modules/sharpening/set</h2> <pre><code class="language-lua">set(opts: SharpenOpts?): State </code></pre> <p>Set the sharpening strength. Any omitted field keeps its current value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>SharpenOpts?</code> <em>(optional)</em> — Sharpening settings — see <code>SharpenOpts</code>.</li> </ul> <pre><code class="language-lua">sharpening.set({ sharpness = 0.6 }) </code></pre> <h2 id="modules-shell-readme">modules/shell/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.shell") -- shell (also available as global 'shell') </code></pre> <p>Engine emulated Unix shell — same shell that powers the MCP <code>bash</code> tool. Public Luau surface over the <code>__shell</code> Internal FFI namespace.</p> <p>Usage: local shell = require("@builtin::modules.api.engine.shell") Also available as global: shell</p> <h2 id="modules-shell-run">modules/shell/run</h2> <pre><code class="language-lua">run(command: string, sessionKey: string?): ShellResult </code></pre> <p>Execute a command in the engine's emulated Unix shell and return once it has completed. This is the same shell as the MCP <code>bash</code> tool — 60+ builtins (ls, cat, grep, find, echo, ...) operating on the virtual scene filesystem. A command that runs Luau (<code>run</code>, <code>luau</code>, <code>zm</code>, <code>zero</code>) needs the engine's frame loop, and a command whose work is a wait (<code>sleep</code>) cannot take its interval while holding it, so from a coroutine the whole line is queued to run off the frame loop and this yields until it finishes; everything else runs inline. The queued run starts the line again from the top, so a line that had already written a file, sent a request, read input or started a background job comes back with the explanation in <code>stderr</code> and <code>shell.runAsync</code> as the way to run it whole. A command reaches the files the code running it reaches, the jobs it starts and the Luau it runs included: run for code a world ships, it is refused the home folder (<code>/zero/home</code>) the way <code>vfs</code> refuses that code.</p> <p><strong>Parameters</strong></p> <ul> <li><code>command</code> <code>string</code> — Shell command to execute.</li> <li><code>sessionKey</code> <code>string?</code> <em>(optional)</em> — Which caller's shell session the command runs in. A session holds what a command leaves behind: the directory <code>cd</code> moved to, exports, shell functions, <code>set -e</code>. It hands that to the next command run under the same key. Omit it and the command runs in the session of the caller this code runs on behalf of, the one that caller's <code>bash</code> commands also use; code with no attested caller behind it runs in the session every caller that names none shares. A command run for code a world ships keeps what it leaves behind in a session of that code's own under the key: it reaches that code's next command and never a command of the caller's own.</li> </ul> <pre><code class="language-lua">local r = shell.run("ls /zero/source") </code></pre> <h2 id="modules-shell-runasync">modules/shell/runAsync</h2> <pre><code class="language-lua">runAsync(command: string, sessionKey: string?): string </code></pre> <p>Asynchronous version of <code>shell.run</code>. Returns a promise ID that resolves to a JSON-encoded result string. Use with <code>task.await()</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>command</code> <code>string</code> — Shell command to execute.</li> <li><code>sessionKey</code> <code>string?</code> <em>(optional)</em> — Which caller's shell session the command runs in. A session holds what a command leaves behind: the directory <code>cd</code> moved to, exports, shell functions, <code>set -e</code>. It hands that to the next command run under the same key. Omit it and the command runs in the session of the caller this code runs on behalf of, the one that caller's <code>bash</code> commands also use; code with no attested caller behind it runs in the session every caller that names none shares. A command run for code a world ships keeps what it leaves behind in a session of that code's own under the key: it reaches that code's next command and never a command of the caller's own.</li> </ul> <pre><code class="language-lua">local json = task.await(shell.runAsync("find /zero -name '*.luau'")) </code></pre> <h2 id="modules-skeleton-readme">modules/skeleton/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.skeleton") -- skeleton (also available as global 'skeleton') </code></pre> <p>The skeleton pose-data pipeline: sample a clip into a pose buffer, and bind/apply a pose buffer onto a Skeleton + Model entity. Public Luau surface over the <code>__skeleton</code> and <code>__clip</code> Internal FFI namespaces.</p> <p>Usage: local skeleton = require("@builtin::modules.api.engine.skeleton") Also available as global: skeleton</p> <h2 id="modules-skeleton-applypose">modules/skeleton/applyPose</h2> <pre><code class="language-lua">applyPose(sinkHandle: number, poseBuffer: Substrate.TypedBuffer): boolean </code></pre> <p>Snapshot the buffer's first <code>layout.total_floats</code> values and queue a pending apply for the next ECS drain. Returns false on unknown sink/buffer or buffer too small for the layout. The Buffer is unchanged.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sinkHandle</code> <code>number</code> — Sink handle from <code>bindPose</code>.</li> <li><code>poseBuffer</code> <code>Substrate.TypedBuffer</code> — The pose buffer to apply.</li> </ul> <h2 id="modules-skeleton-bindclip">modules/skeleton/bindClip</h2> <pre><code class="language-lua">bindClip(zanimBytes: buffer | string, boneOrder: { string }): ClipBindInfo? </code></pre> <p>Decode a <code>zanim</code> payload and bind it to <code>boneOrder</code>, precomputing which of the clip's channels feed each bone so per-frame <code>sampleClip</code> is allocation-free. Returns <code>{ handle, matched, total, duration }</code>, or nil on a malformed payload / empty bone order. Check <code>matched</code>: 0 means the clip drives none of these bones.</p> <p><strong>Parameters</strong></p> <ul> <li><code>zanimBytes</code> <code>buffer | string</code> — The clip's <code>data.zanim</code> payload bytes (binary-safe).</li> <li><code>boneOrder</code> <code>{ string }</code> — Output bone names — one stride-10 record per bone.</li> </ul> <h2 id="modules-skeleton-bindpose">modules/skeleton/bindPose</h2> <pre><code class="language-lua">bindPose(entityId: (string | entityRef)?, opts: SkeletonLayout): number? </code></pre> <p>Register a pose sink targeting <code>entityId</code>. The opts table carries the layout: <code>boneOrder</code> is the bone-name array (<code>{"hip", "spine", ...}</code>), <code>stride</code> defaults to 10 (translation.xyz + rotation.xyzw + scale.xyz). Pass <code>entityId</code> as nil to use the current component's owning entity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>(string | entityRef)?</code> <em>(optional)</em> — Engine entity id or proxy, or nil for the current entity.</li> <li><code>opts</code> <code>SkeletonLayout</code> — <code>{ boneOrder, stride }</code>.</li> </ul> <pre><code class="language-lua">local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 }) </code></pre> <h2 id="modules-skeleton-clipbones">modules/skeleton/clipBones</h2> <pre><code class="language-lua">clipBones(zanimBytes: buffer | string): { string }? </code></pre> <p>Decode a <code>zanim</code> payload and return its bone-name array. Pure: build a bind order or a retarget map from a clip without binding a sampler. Returns nil on bytes that aren't a valid zanim payload.</p> <p><strong>Parameters</strong></p> <ul> <li><code>zanimBytes</code> <code>buffer | string</code> — The clip's <code>data.zanim</code> payload bytes (binary-safe).</li> </ul> <pre><code class="language-lua">local names = skeleton.clipBones(assert(vfs.read(path .. "/data.zanim"))) </code></pre> <h2 id="modules-skeleton-clipdecode">modules/skeleton/clipDecode</h2> <pre><code class="language-lua">clipDecode(zanimBytes: buffer | string): string? </code></pre> <p>Decode a <code>zanim</code> payload to its readable JSON form (<code>{ name, duration, channels, bone_names }</code>), the clip as text to read, store or diff. The binary parse is the engine's. Code that transforms a clip's channels in Luau (e.g. the retarget bake) reads the same clip as a table with <code>clipToTable</code>, which skips the text. Returns nil on bytes that aren't a valid zanim payload. Inverse of <code>clipEncode</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>zanimBytes</code> <code>buffer | string</code> — The clip's <code>data.zanim</code> payload bytes (binary-safe).</li> </ul> <pre><code class="language-lua">local json = require("@builtin::modules.json") local clipJson = skeleton.clipDecode(bytes) local clip = if clipJson then json.decode(clipJson) else nil </code></pre> <h2 id="modules-skeleton-clipencode">modules/skeleton/clipEncode</h2> <pre><code class="language-lua">clipEncode(jsonString: string): string? </code></pre> <p>Encode a clip's JSON form (the shape <code>clipDecode</code> returns) back to a <code>zanim</code> payload — the bytes a <code>.animation</code> stores and <code>bindClip</code>/<code>sampleClip</code> consume. Inverse of <code>clipDecode</code>. A clip held as a table encodes with <code>clipFromTable</code>, which skips the text. Returns nil on invalid JSON.</p> <p><strong>Parameters</strong></p> <ul> <li><code>jsonString</code> <code>string</code> — A clip JSON document.</li> </ul> <pre><code class="language-lua">local json = require("@builtin::modules.json") local bytes = skeleton.clipEncode(json.encode(clip)) </code></pre> <h2 id="modules-skeleton-clipfromtable">modules/skeleton/clipFromTable</h2> <pre><code class="language-lua">clipFromTable(clip: { [string]: any }): (string?, string?) </code></pre> <p>Encode a clip table (the shape <code>clipToTable</code> returns) to a <code>zanim</code> payload, the bytes a <code>.animation</code> stores and <code>bindClip</code>/<code>sampleClip</code> consume. Every field is required and every number finite. Inverse of <code>clipToTable</code>. Returns nil and the reason, naming the field, when the table is not a clip.</p> <p><strong>Parameters</strong></p> <ul> <li><code>clip</code> <code>{ [string]: any }</code> — A clip table: <code>{ name, duration, channels, bone_names }</code>.</li> </ul> <pre><code class="language-lua">local bytes = skeleton.clipFromTable(clip) </code></pre> <h2 id="modules-skeleton-clippack">modules/skeleton/clipPack</h2> <pre><code class="language-lua">clipPack(packed: PackedClip): (string?, string?) </code></pre> <p>Encode a clip in packed form (the shape <code>clipUnpack</code> returns) into <code>zanim</code> bytes. Channels may share one run of <code>times</code> by naming the same <code>keyOffset</code>; <code>interpolation</code> defaults to Linear; with no <code>boneNames</code> the clip names its bones in the order its channels first drive them. <code>times</code> and <code>values</code> take a compute readback's <code>resultBytes()</code> directly. Returns nil and the reason, naming the channel, when a channel's keys fall outside the runs or its times run backwards.</p> <p><strong>Parameters</strong></p> <ul> <li><code>packed</code> <code>PackedClip</code> — The packed clip.</li> </ul> <pre><code class="language-lua">local bytes = skeleton.clipPack(packed) </code></pre> <h2 id="modules-skeleton-cliptotable">modules/skeleton/clipToTable</h2> <pre><code class="language-lua">clipToTable(zanimBytes: buffer | string): { [string]: any }? </code></pre> <p>Decode a <code>zanim</code> payload to a table: <code>{ name, duration, channels = { { target_bone, path, keyframes_in, keyframes_out, interpolation } }, bone_names }</code>, the shape <code>clipDecode</code> writes as JSON, built by the engine in place. <code>path</code> is <code>Translation</code>, <code>Rotation</code> or <code>Scale</code>; <code>interpolation</code> is <code>Linear</code>, <code>Step</code> or <code>CubicSpline</code>. Inverse of <code>clipFromTable</code>. Returns nil on bytes that aren't a valid zanim payload.</p> <p><strong>Parameters</strong></p> <ul> <li><code>zanimBytes</code> <code>buffer | string</code> — The clip's <code>data.zanim</code> payload bytes (binary-safe).</li> </ul> <pre><code class="language-lua">local clip = skeleton.clipToTable(bytes) </code></pre> <h2 id="modules-skeleton-clipunpack">modules/skeleton/clipUnpack</h2> <pre><code class="language-lua">clipUnpack(zanimBytes: buffer | string): PackedClip? </code></pre> <p>Decode a <code>zanim</code> payload into its packed form: a channel table (<code>{ bone, path, interpolation, keyOffset, keyCount, valueOffset }</code>, offsets 0-based element indices) and two <code>buffer</code>s of little-endian f32s, <code>times</code> and <code>values</code>, holding every channel's keys channel after channel (3 floats a key for Translation and Scale, 4 for Rotation, three times that for CubicSpline). The runs go straight into a typed buffer (<code>buf:writeBytes(packed.values)</code>) for a compute pass. Returns nil on bytes that aren't a valid zanim payload. Inverse of <code>clipPack</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>zanimBytes</code> <code>buffer | string</code> — The clip's <code>data.zanim</code> payload bytes (binary-safe).</li> </ul> <pre><code class="language-lua">local packed = skeleton.clipUnpack(bytes) </code></pre> <h2 id="modules-skeleton-jointtransforms">modules/skeleton/jointTransforms</h2> <pre><code class="language-lua">jointTransforms(entityId: string | entityRef): table </code></pre> <p>Read a skinned entity's per-joint world transforms for the current animated pose.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityId</code> <code>string | entityRef</code> — Engine entity id or proxy of a skinned entity.</li> </ul> <pre><code class="language-lua">local joints = skeleton.jointTransforms(meshId) </code></pre> <h2 id="modules-skeleton-sampleclip">modules/skeleton/sampleClip</h2> <pre><code class="language-lua">sampleClip(handle: number, time: number, poseBuffer: Substrate.TypedBuffer): boolean </code></pre> <p>Sample the bound clip at <code>time</code> (clamped to <code>[0, duration]</code>) and write one stride-10 pose record per bound bone into the Buffer, starting at index 0. Bones the clip does not drive are written as identity. Returns false on unknown handle/buffer or a buffer too small for the bone count.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>number</code> — Sampler handle from <code>bindClip</code>.</li> <li><code>time</code> <code>number</code> — Sample time in seconds.</li> <li><code>poseBuffer</code> <code>Substrate.TypedBuffer</code> — The stride-10 pose buffer written into.</li> </ul> <h2 id="modules-skeleton-unbindclip">modules/skeleton/unbindClip</h2> <pre><code class="language-lua">unbindClip(handle: number): boolean </code></pre> <p>Drop the bound clip sampler from the registry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>number</code> — Sampler handle to remove.</li> </ul> <h2 id="modules-skeleton-unbindpose">modules/skeleton/unbindPose</h2> <pre><code class="language-lua">unbindPose(sinkHandle: number): boolean </code></pre> <p>Remove the sink from the registry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sinkHandle</code> <code>number</code> — Sink handle to remove.</li> </ul> <h2 id="modules-sky-readme">modules/sky/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.sky") -- sky (also available as global 'sky') </code></pre> <p>Sky configuration — type, time of day, day/night cycle, procedural parameters, presets, explicit sun direction. Public Luau surface over the <code>__sky</code> Internal FFI namespace.</p> <p>Usage: local sky = require("@builtin::modules.api.engine.sky") Also available as global: sky</p> <h2 id="modules-sky-get">modules/sky/get</h2> <pre><code class="language-lua">get(): { [string]: any } </code></pre> <p>Get all current sky configuration as a table. Returns the same fields as <code>sky.set</code> accepts, plus read-only fields like <code>material_name</code> and <code>type</code>. Color values are returned as positional arrays <code>[r, g, b]</code>.</p> <pre><code class="language-lua">local cfg = sky.get(); print(cfg.time_of_day) </code></pre> <h2 id="modules-sky-gettimeofday">modules/sky/getTimeOfDay</h2> <pre><code class="language-lua">getTimeOfDay(): number </code></pre> <p>Get the current time of day in hours (0-24).</p> <pre><code class="language-lua">local t = sky.getTimeOfDay() </code></pre> <h2 id="modules-sky-installfallback">modules/sky/installFallback</h2> <pre><code class="language-lua">installFallback() </code></pre> <p>Register the engine fallback sky material and install it as the engine-level fallback (rendered when a scene has no sky entity). Idempotent; requires a live renderer — the scene loader calls it.</p> <pre><code class="language-lua">sky.installFallback() </code></pre> <h2 id="modules-sky-preset">modules/sky/preset</h2> <pre><code class="language-lua">preset(name: string) </code></pre> <p>Apply a named sky preset. Available: <code>clear_day</code>, <code>sunset</code>, <code>sunrise</code>, <code>overcast</code>, <code>night</code>, <code>studio</code>, <code>none</code>. Raises a Luau error for unrecognized names — wrap in <code>pcall</code> if uncertain.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Preset name (case-sensitive).</li> </ul> <pre><code class="language-lua">sky.preset("sunset") </code></pre> <h2 id="modules-sky-set">modules/sky/set</h2> <pre><code class="language-lua">set(opts: SkyOpts) </code></pre> <p>Configure the sky system. All fields are optional — only provided fields are updated. Color fields accept both named <code>{x=r, y=g, z=b}</code> and positional <code>{r, g, b}</code> forms. <code>color</code> is an alias for <code>solid_color</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>SkyOpts</code> — Sky configuration properties.</li> </ul> <pre><code class="language-lua">sky.set({ type = "procedural", time_of_day = 14, sync_sun_to_light = true }) </code></pre> <h2 id="modules-sky-setsundirection">modules/sky/setSunDirection</h2> <pre><code class="language-lua">setSunDirection(dir: SkyColor) </code></pre> <p>Set an explicit sun direction and disable time-based sun positioning. The directional light is updated to match.</p> <p><strong>Parameters</strong></p> <ul> <li><code>dir</code> <code>SkyColor</code> — Normalized sun direction vector.</li> </ul> <pre><code class="language-lua">sky.setSunDirection({ 0.5, -1, 0.3 }) </code></pre> <h2 id="modules-sky-settimeofday">modules/sky/setTimeOfDay</h2> <pre><code class="language-lua">setTimeOfDay(time: number) </code></pre> <p>Set the time of day (0-24 hours). 0 = midnight, 6 = sunrise, 12 = noon, 18 = sunset.</p> <p><strong>Parameters</strong></p> <ul> <li><code>time</code> <code>number</code> — Time of day in hours.</li> </ul> <pre><code class="language-lua">sky.setTimeOfDay(18.5) </code></pre> <h2 id="modules-spatialstreaming-readme">modules/spatialStreaming/README</h2> <pre><code class="language-lua">require("@builtin::systems.spatialStreaming.spatialStreaming") -- spatialStreaming </code></pre> <p>Usage: local spatialStreaming = require("@builtin::systems.spatialStreaming.spatialStreaming")</p> <h2 id="modules-spatialstreaming-add">modules/spatialStreaming/add</h2> <pre><code class="language-lua">add(records: { Record }, opts: { [string]: any }?): string </code></pre> <p>Register a group of records for content that is not spawned. This is the path that lets a world hold more than it can spawn at once: with <code>store = "vfs"</code> the records are written to a file under the configured directory and only the cell's manifest stays in memory until a source arrives.</p> <p><strong>Parameters</strong></p> <ul> <li><code>records</code> <code>{ Record }</code> — Array of records, root first. A record's <code>parent</code> indexes another record of the same array.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ layer = "default", store = "memory" | "vfs" }</code>.</li> </ul> <pre><code class="language-lua">spatialStreaming.add({ { name = "rock", position = { x = 300, y = 0, z = 0 }, components = { Model = { model = "@builtin::meshes.cube" } } } }, { store = "vfs" }) </code></pre> <h2 id="modules-spatialstreaming-addsource">modules/spatialStreaming/addSource</h2> <pre><code class="language-lua">addSource(target: any, opts: { [string]: any }?): number </code></pre> <p>Add a streaming source. A cell is resident while ANY source wants it resident and released only once every source wants it released, so several sources — a player and a spectator camera, two split-screen players — each keep the region they stand near.</p> <p>A source carries its own radii, which is what lets a distant spectator stream a thin shell while the player streams a deep one. Radii left out follow the layer's, and the layer's follow the configuration.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>any</code> <em>(optional)</em> — An entity id, an entity proxy, an entity name, or a fixed world position <code>{ x, y, z }</code>.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ loadRadius, unloadRadius, proxyRadius, proxyUnloadRadius }</code> — this source's own radii, for the content and for the far field it sees proxies in. A source that names an unload radius of its own sees a far field measured from it unless it names that too.</li> </ul> <pre><code class="language-lua">spatialStreaming.addSource(entity.find("Player"), { loadRadius = 90, unloadRadius = 140 }) </code></pre> <h2 id="modules-spatialstreaming-capture">modules/spatialStreaming/capture</h2> <pre><code class="language-lua">capture(targets: any, opts: { [string]: any }?): { [string]: any } </code></pre> <p>Hand live entities to the streaming store. Each target is the ROOT of a group: it and its descendants are serialized together and placed in the cell the root stands in. The entities are left standing — residency passes to the streaming rule, which releases them on the first tick that puts them past the unload radius.</p> <p>A target that is replicated to other peers is refused: residency is a local decision, and despawning a replicated entity on one client would reach the others. A target that already has a parent is refused too — its root is the group, and capturing a branch of one would leave the rest behind. A target the store already holds is refused as well, and counted: a second group over the same live entities would despawn them on the first release and leave the second group naming content that is gone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>targets</code> <code>any</code> <em>(optional)</em> — Array of entity ids, proxies, or names, or a single name glob.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ layer = "default", store = "memory" | "vfs" }</code>.</li> </ul> <pre><code class="language-lua">spatialStreaming.capture(entity.findAll("rock_*"), { layer = "props" }) </code></pre> <h2 id="modules-spatialstreaming-cellbounds">modules/spatialStreaming/cellBounds</h2> <pre><code class="language-lua">cellBounds(key: string, cellSize: number): { minX: number, minZ: number, maxX: number, maxZ: number } </code></pre> <p>Ground footprint of a cell.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Cell key.</li> <li><code>cellSize</code> <code>number</code> — Edge length of a cell in world units.</li> </ul> <pre><code class="language-lua">local b = spatialStreaming.cellBounds("2:-1", 64) </code></pre> <h2 id="modules-spatialstreaming-cellcoords">modules/spatialStreaming/cellCoords</h2> <pre><code class="language-lua">cellCoords(key: string): (number, number) </code></pre> <p>Grid coordinates a cell key names.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Cell key from <code>cellKeyAt</code>.</li> </ul> <pre><code class="language-lua">local cx, cz = spatialStreaming.cellCoords("2:-1") </code></pre> <h2 id="modules-spatialstreaming-celldistance">modules/spatialStreaming/cellDistance</h2> <pre><code class="language-lua">cellDistance(key: string, cellSize: number, x: number, z: number): number </code></pre> <p>Distance from a point to the nearest edge of a cell's footprint, zero when the point stands inside it. Measuring to the centre instead would put a cell's near corner inside the load radius while the cell itself reads as far, so a source walking along a boundary would see the ground it is on released.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Cell key.</li> <li><code>cellSize</code> <code>number</code> — Edge length of a cell in world units.</li> <li><code>x</code> <code>number</code> — World-space X of the point.</li> <li><code>z</code> <code>number</code> — World-space Z of the point.</li> </ul> <pre><code class="language-lua">local d = spatialStreaming.cellDistance("2:-1", 64, 10, 10) </code></pre> <h2 id="modules-spatialstreaming-cellkeyat">modules/spatialStreaming/cellKeyAt</h2> <pre><code class="language-lua">cellKeyAt(x: number, z: number, cellSize: number): string </code></pre> <p>Key of the cell a world-space point stands in. Cells are square columns on the ground plane: height never enters, because a world's content is spread over its ground rather than through its air, and a column keeps a tower and its foundation in one cell.</p> <p><strong>Parameters</strong></p> <ul> <li><code>x</code> <code>number</code> — World-space X.</li> <li><code>z</code> <code>number</code> — World-space Z.</li> <li><code>cellSize</code> <code>number</code> — Edge length of a cell in world units.</li> </ul> <pre><code class="language-lua">local key = spatialStreaming.cellKeyAt(130, -40, 64) </code></pre> <h2 id="modules-spatialstreaming-cells">modules/spatialStreaming/cells</h2> <pre><code class="language-lua">cells(): { any } </code></pre> <p>Per-cell view of the store, for inspection and for tests.</p> <pre><code class="language-lua">local c = spatialStreaming.cells() </code></pre> <h2 id="modules-spatialstreaming-clearproxy">modules/spatialStreaming/clearProxy</h2> <pre><code class="language-lua">clearProxy(target: any, opts: { [string]: any }?): boolean </code></pre> <p>Take a cell's proxy away, despawning it if it is standing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>any</code> <em>(optional)</em> — A cell key or a <code>{ x, y, z }</code> world position.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ layer = "default" }</code>.</li> </ul> <pre><code class="language-lua">spatialStreaming.clearProxy("4:0") </code></pre> <h2 id="modules-spatialstreaming-clearsources">modules/spatialStreaming/clearSources</h2> <pre><code class="language-lua">clearSources() </code></pre> <p>Drop every streaming source. Cells hold whatever residency they have — nothing loads or releases while no source stands anywhere.</p> <pre><code class="language-lua">spatialStreaming.clearSources() </code></pre> <h2 id="modules-spatialstreaming-config">modules/spatialStreaming/config</h2> <pre><code class="language-lua">config(): { [string]: any } </code></pre> <p>The configuration now in force. The far field is reported as the distances it is measured at, whether they were given or follow the content radii.</p> <pre><code class="language-lua">local c = spatialStreaming.config() </code></pre> <h2 id="modules-spatialstreaming-configure">modules/spatialStreaming/configure</h2> <pre><code class="language-lua">configure(opts: { [string]: any }?): { [string]: any } </code></pre> <p>Set the grid and the radii every layer inherits. Keys left out keep their current value.</p> <p>A new <code>cellSize</code> re-buckets everything the store already holds, by each group's own root position. A cell key means nothing without the size it was measured with, so content filed under the old grid would otherwise be measured against footprints it never stood in.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ cellSize, loadRadius, unloadRadius, proxyRadius, proxyUnloadRadius, budget, dir }</code>. <code>budget</code> is how many entity records one tick may spend on loading and releasing; the proxy pair is the far field a released cell's stand-in covers, and follows <code>unloadRadius</code> unless it is given — passing 0 for either hands it back to that; <code>dir</code> is the VFS directory a file-backed cell writes under.</li> </ul> <pre><code class="language-lua">spatialStreaming.configure({ cellSize = 50, loadRadius = 100, unloadRadius = 160 }) </code></pre> <h2 id="modules-spatialstreaming-decide">modules/spatialStreaming/decide</h2> <pre><code class="language-lua">decide(distance: number, loadRadius: number, unloadRadius: number): string </code></pre> <p>Residency a cell whose nearest edge is <code>distance</code> from the closest source should be in. Inside <code>loadRadius</code> it is wanted resident, past <code>unloadRadius</code> it is wanted released, and between the two it keeps whatever it already is — the band is what stops a source resting on a boundary from loading and releasing the same cell every frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>distance</code> <code>number</code> — Distance from the nearest source to the cell's nearest edge.</li> <li><code>loadRadius</code> <code>number</code> — Distance within which a cell is wanted resident.</li> <li><code>unloadRadius</code> <code>number</code> — Distance past which a cell is wanted released.</li> </ul> <pre><code class="language-lua">local want = spatialStreaming.decide(140, 128, 192) </code></pre> <h2 id="modules-spatialstreaming-deriveproxies">modules/spatialStreaming/deriveProxies</h2> <pre><code class="language-lua">deriveProxies(opts: { [string]: any }?): { [string]: any } </code></pre> <p>Build bounding-silhouette proxies from the content the store already holds — one box over a whole cell, or one over each group in it. This is the automatic path; <code>setProxy</code> is the one that takes an authored mesh or impostor.</p> <p>Each record is measured as a box of its own <code>scale</code>, placed through the whole of its parent's transform, so a group standing rotated and scaled is enclosed where it actually stands. A record whose mesh is larger than one unit at scale 1 is measured as the unit box it is scaled from — <code>padding</code> is what covers the difference, and <code>setProxy</code> is what takes an authored silhouette instead.</p> <p>A file-backed cell's records are read to measure them and dropped again, so deriving over a world that is held in files costs one read per cell and leaves the store as it found it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ layer, mesh = "@builtin::meshes.cube", material, granularity = "cell" | "group", padding = 0, replace = false }</code>. <code>layer</code> limits the walk to one layer; <code>replace</code> overwrites a proxy a cell already has.</li> </ul> <pre><code class="language-lua">spatialStreaming.deriveProxies({ granularity = "group", material = "@builtin::materials.default" }) </code></pre> <h2 id="modules-spatialstreaming-flush">modules/spatialStreaming/flush</h2> <pre><code class="language-lua">flush(): number </code></pre> <p>Write every file-backed cell that has no group standing out to its file and drop its records. A cell already written is left alone. This is what bounds memory while a world's content is being registered; <code>tick</code> calls it each step, so a world that streams needs it only to measure the store between an <code>add</code> batch and the first step.</p> <pre><code class="language-lua">spatialStreaming.flush() </code></pre> <h2 id="modules-spatialstreaming-layers">modules/spatialStreaming/layers</h2> <pre><code class="language-lua">layers(): { [string]: any } </code></pre> <p>Settings of every configured layer, keyed by name.</p> <pre><code class="language-lua">local l = spatialStreaming.layers() </code></pre> <h2 id="modules-spatialstreaming-proxies">modules/spatialStreaming/proxies</h2> <pre><code class="language-lua">proxies(): { any } </code></pre> <p>Per-cell view of the proxies the store holds. <code>records</code> is what the stand-in is made of; <code>entities</code> is how much of it is standing right now, which is zero while the cell's content covers it.</p> <pre><code class="language-lua">local p = spatialStreaming.proxies() </code></pre> <h2 id="modules-spatialstreaming-proxydecide">modules/spatialStreaming/proxyDecide</h2> <pre><code class="language-lua">proxyDecide(distance: number, proxyRadius: number, proxyUnloadRadius: number): string </code></pre> <p>Residency a cell's PROXY should be in, for a cell whose content is not standing. Same shape as <code>decide</code> and for the same reason: the band between the two radii is what keeps a source resting at the far edge from spawning and despawning the same stand-in every frame.</p> <p>The rule reads a distance and the far-field radii alone. Whether the cell's own content is standing is the tick's question, and the tick lets the content's answer win: a proxy stands where the content does not.</p> <p><strong>Parameters</strong></p> <ul> <li><code>distance</code> <code>number</code> — Distance from the nearest source to the cell's nearest edge.</li> <li><code>proxyRadius</code> <code>number</code> — Distance within which a released cell's proxy stands.</li> <li><code>proxyUnloadRadius</code> <code>number</code> — Distance past which the proxy is dropped too.</li> </ul> <pre><code class="language-lua">local want = spatialStreaming.proxyDecide(560, 512, 640) </code></pre> <h2 id="modules-spatialstreaming-proxyids">modules/spatialStreaming/proxyIds</h2> <pre><code class="language-lua">proxyIds(): { string } </code></pre> <p>Entity ids the proxies currently have standing, across every cell.</p> <pre><code class="language-lua">local ids = spatialStreaming.proxyIds() </code></pre> <h2 id="modules-spatialstreaming-removesource">modules/spatialStreaming/removeSource</h2> <pre><code class="language-lua">removeSource(target: any): boolean </code></pre> <p>Remove a streaming source by the entity it follows.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>any</code> <em>(optional)</em> — Entity id, proxy, or name the source was added with.</li> </ul> <pre><code class="language-lua">spatialStreaming.removeSource(entity.find("Player")) </code></pre> <h2 id="modules-spatialstreaming-reset">modules/spatialStreaming/reset</h2> <pre><code class="language-lua">reset() </code></pre> <p>Release every resident group, forget the store, and drop every source. Live entities the store owns are despawned; nothing else in the scene is touched.</p> <pre><code class="language-lua">spatialStreaming.reset() </code></pre> <h2 id="modules-spatialstreaming-residentids">modules/spatialStreaming/residentIds</h2> <pre><code class="language-lua">residentIds(): { string } </code></pre> <p>Entity ids the store currently has standing, across every cell.</p> <pre><code class="language-lua">local ids = spatialStreaming.residentIds() </code></pre> <h2 id="modules-spatialstreaming-setlayer">modules/spatialStreaming/setLayer</h2> <pre><code class="language-lua">setLayer(name: string, opts: { [string]: any }?): { [string]: any } </code></pre> <p>Configure a content layer. A layer streams on its own radii and can be switched off entirely, so decorative content can be released long before the content a player interacts with. Each layer keeps its own grid, so two layers standing in the same square still stream apart.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Layer name. Content lands in "default" unless <code>capture</code> / <code>add</code> say otherwise.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ enabled, loadRadius, unloadRadius, proxyRadius, proxyUnloadRadius }</code>. A nil radius follows the global configuration, and a layer that names an unload radius of its own gets a far field measured from it. A disabled layer holds nothing standing — neither its content nor its proxies.</li> </ul> <pre><code class="language-lua">spatialStreaming.setLayer("props", { loadRadius = 60, unloadRadius = 90 }) </code></pre> <h2 id="modules-spatialstreaming-setproxy">modules/spatialStreaming/setProxy</h2> <pre><code class="language-lua">setProxy(target: any, records: { Record }, opts: { [string]: any }?): string </code></pre> <p>Give a cell the stand-in it is drawn as while its content is released. The proxy is a group of records like any other, so what it represents is the caller's: a merged low-detail mesh, a billboard impostor, a bounding silhouette. It stands whenever the cell's content does not and the cell is inside the proxy radius, and it is exchanged for the content in an order that leaves no frame with neither standing.</p> <p>A cell that has no content yet takes a proxy too, which is what lets a skyline be registered before — or instead of — the content it stands for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>any</code> <em>(optional)</em> — A cell key from <code>cellKeyAt</code> / <code>cells()</code>, or a <code>{ x, y, z }</code> world position the cell is looked up from.</li> <li><code>records</code> <code>{ Record }</code> — Array of records, the same shape <code>add</code> takes. Positions are world-space for a record with no parent.</li> <li><code>opts</code> <code>{ [string]: any }?</code> <em>(optional)</em> — <code>{ layer = "default" }</code>.</li> </ul> <pre><code class="language-lua">spatialStreaming.setProxy("4:0", { { name = "skyline", position = { x = 288, y = 8, z = 32 }, scale = { x = 64, y = 16, z = 64 }, components = { Model = { model = "@builtin::meshes.cube" } } } }) </code></pre> <h2 id="modules-spatialstreaming-sourcepositions">modules/spatialStreaming/sourcePositions</h2> <pre><code class="language-lua">sourcePositions(): { Vec3 } </code></pre> <p>World positions of the live sources. An entity source that has been despawned is dropped here rather than reporting a stale position.</p> <pre><code class="language-lua">local p = spatialStreaming.sourcePositions() </code></pre> <h2 id="modules-spatialstreaming-stats">modules/spatialStreaming/stats</h2> <pre><code class="language-lua">stats(): { [string]: any } </code></pre> <p>What the store holds and how much of it is standing. <code>bytesResident</code> is the encoded size of the groups that are live; <code>bytesStored</code> is the encoded size of everything the store knows about, resident or not. The gap between the two is what streaming bought.</p> <p>The proxy figures stand apart from the content's: <code>entitiesProxy</code> and <code>bytesProxy</code> are what the far field costs right now, against the <code>entitiesTotal</code> / <code>bytesStored</code> it stands in for.</p> <pre><code class="language-lua">local s = spatialStreaming.stats() </code></pre> <h2 id="modules-spatialstreaming-tick">modules/spatialStreaming/tick</h2> <pre><code class="language-lua">tick(_dt: number?): { [string]: any } </code></pre> <p>Advance streaming by one step. Loads and releases at most <code>budget</code> entity records, so crossing a cell boundary spreads its cost over frames instead of spending it all in one. A group is atomic: a group larger than the whole budget still moves in one piece, so the budget is the floor a step stops at rather than a ceiling it never passes.</p> <p>Releases are spent before loads, which keeps the peak residency at what the radius bought rather than at the sum of the cells on both sides of a boundary.</p> <p>A cell that has a proxy raises it before the first of its groups is released and drops it once the cell's own content has landed, so the exchange between the two representations leaves neither a gap nor a silhouette standing over the content it covered for.</p> <p>One step belongs to the frame, not to the caller: a second call inside the same engine frame reports <code>stepped = false</code> and changes nothing, so a world carrying several <code>StreamingSource</code> components spends one budget rather than one per source. Waiting a frame is what advances it again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>_dt</code> <code>number?</code> <em>(optional)</em> — Frame delta, accepted so a component can pass its own and ignored — the step is driven by distance, not by time.</li> </ul> <pre><code class="language-lua">spatialStreaming.tick(dt) </code></pre> <h2 id="modules-ssr-readme">modules/ssr/README</h2> <pre><code class="language-lua">require("@builtin::systems.reflections.ssr") -- ssr </code></pre> <p>Screen-space reflections — polished floors reflect what is standing on them, and wet ground reflects what is above it, from the frame the engine has already drawn.</p> <p>Usage: local ssr = require("@builtin::systems.reflections.ssr")</p> <h2 id="modules-ssr-active">modules/ssr/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the reflection passes are currently running.</p> <pre><code class="language-lua">if ssr.active() then print("reflecting") end </code></pre> <h2 id="modules-ssr-clear">modules/ssr/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Take the reflection passes down. The next <code>set</code> brings them up again at the feature's declared defaults laid under whatever it passes.</p> <pre><code class="language-lua">ssr.clear() </code></pre> <h2 id="modules-ssr-get">modules/ssr/get</h2> <pre><code class="language-lua">get(): { [string]: any } </code></pre> <p>The reflection settings currently in force: every knob the feature declares at its value, or the declared defaults while none is live.</p> <pre><code class="language-lua">local d = ssr.get().maxDistance </code></pre> <h2 id="modules-ssr-set">modules/ssr/set</h2> <pre><code class="language-lua">set(opts: SsrOpts?): { [string]: any }? </code></pre> <p>Set the scene's screen-space reflections. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. Brings the feature up when none is live; an <code>intensity</code> of 0 takes it down. A knob the feature does not declare is refused by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>SsrOpts?</code> <em>(optional)</em> — Reflection settings — see <code>SsrOpts</code>.</li> </ul> <pre><code class="language-lua">ssr.set({ intensity = 1, maxDistance = 40, quality = "high" }) </code></pre> <h2 id="modules-state">modules/state</h2> <pre><code class="language-lua">modules.state(path?) -> table </code></pre> <p>The calling module's durable state table — the same table on every call for the life of the VM, kept across an in-place hot reload of that module and across the re-run a reloaded dependency triggers in it. A module-level <code>local</code> is an upvalue of the chunk that declared it, so it starts again from its initial value each time that chunk runs; a field on this table is held by the engine and does not. Put here what a reload has to survive: a built flag, the entities and particle systems a build owns, an unsubscribe list, a generation counter. Pass a module require path to read another module's table.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> <em>(optional)</em> — Module require path. Omit inside a module to get that module's own table.</li> </ul> <p><strong>Returns</strong> <code>table</code> — The module's durable state table</p> <h2 id="modules-stream-readme">modules/stream/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.stream") -- stream (also available as global 'stream') </code></pre> <p>Named byte streams by URL scheme — TCP, tty, Bluetooth Low Energy and loopback transports, dialled out or listened for, with a common read/write/status/close surface over every one of them. Public Luau surface over the <code>__stream</code> Internal FFI namespace.</p> <p>Usage: local stream = require("@builtin::modules.api.engine.stream") Also available as global: stream</p> <h2 id="modules-stream-accept">modules/stream/accept</h2> <pre><code class="language-lua">accept(listener: string): string? </code></pre> <p>Take the connection that has waited longest on the listener, as a stream handle that reads, writes, and closes exactly like one <code>stream.open</code> returned. Returns nil when nothing is waiting, so call it in a loop each tick to take every peer that arrived. <code>stream.listenerStatus(listener).pending</code> is how many are still waiting.</p> <p><strong>Parameters</strong></p> <ul> <li><code>listener</code> <code>string</code> — Listener handle from stream.listen.</li> </ul> <pre><code class="language-lua">while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end </code></pre> <h2 id="modules-stream-close">modules/stream/close</h2> <pre><code class="language-lua">close(handle: string): boolean </code></pre> <p>Finish whatever <code>handle</code> names — a stream or a listener — and drop it from the registry.</p> <p>Closing a stream refuses every later write and carries the bytes already queued to the peer before the connection ends, so a write and a close in the same tick deliver — the shape a request answered with one response has. A peer that has stopped reading altogether holds that finish for thirty seconds; past that the connection ends and what is still queued ends with it, so a caller that must know its bytes went out watches <code>stream.status(handle).pending</code> reach zero before it closes.</p> <p>Closing a listener stops it answering new peers and closes the connections nobody took; the connections <code>stream.accept</code> already handed over keep running until they are closed themselves.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Stream handle from stream.open or stream.accept, or listener handle from stream.listen.</li> </ul> <pre><code class="language-lua">stream.write(peer, response); stream.close(peer) </code></pre> <h2 id="modules-stream-listen">modules/stream/listen</h2> <pre><code class="language-lua">listen(url: string, opts: StreamListenOpts?): PromiseHandle </code></pre> <p>Hold the address <code>url</code> names and answer the peers that dial it — the other direction from <code>stream.open</code>, for when the thing you are talking to starts the conversation and restarts on its own schedule. Returns a promise handle: <code>task.await()</code> it to get the listener handle once the address is held, or it raises the reason a malformed url, a scheme that cannot listen, or a failed bind was refused with. Take the connections with <code>stream.accept</code>.</p> <p><strong>The host in the url is the interface bound, and the whole of what decides who can reach it.</strong> <code>tcp://127.0.0.1:9000</code> answers only programs on this same machine. <code>tcp://0.0.0.0:9000</code> answers any host that can route to this machine on that port — every device on the wifi, and anything beyond it the network lets through. Write the one you mean; there is no default, and <code>stream.listenerStatus</code> reports which of the two you got. A port of <code>0</code> asks the operating system to choose one, which that same status then reports.</p> <p><code>opts.inboundCapacity</code> and <code>opts.outboundCapacity</code> bound each answered connection (65536 bytes each by default); <code>opts.backlog</code> bounds the connections held for <code>stream.accept</code> before the listener stops taking them from the operating system, which leaves the rest queued in the kernel rather than answered and forgotten (16 by default, and at least 1).</p> <p>The listener belongs to the chunk that opened it — the chunk whose own code called <code>stream.listen</code>, which is the module holding that line even when something else called into it. When that chunk runs again — a module hot-reload, a cleared require cache — the listener and the connections it answered are closed, and the new run binds the address for itself. Peers see the connection close and dial again. <code>stream.listeners()</code> names that chunk as each entry's <code>owner</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — Listen URL — scheme://host:port.</li> <li><code>opts</code> <code>StreamListenOpts?</code> <em>(optional)</em> — Per-connection capacities and the accept backlog (optional).</li> </ul> <pre><code class="language-lua">local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending) </code></pre> <h2 id="modules-stream-listenerstatus">modules/stream/listenerStatus</h2> <pre><code class="language-lua">listenerStatus(listener: string): ListenerStatus? </code></pre> <p>Report what the listener holds and has handed over. <code>address</code> is the address the operating system resolved the bind to, port included — the one to hand a peer. <code>reach</code> says who can connect to it: <code>"thisMachine"</code> when it is a loopback address and only programs on this machine can, <code>"network"</code> when any host that can route here can. <code>accepted</code> counts the connections <code>stream.accept</code> handed over, <code>pending</code> the ones still waiting, and <code>capacity</code> the value <code>pending</code> may reach before the listener stops taking connections from the operating system. nil when handle names no open listener.</p> <p><strong>Parameters</strong></p> <ul> <li><code>listener</code> <code>string</code> — Listener handle from stream.listen.</li> </ul> <pre><code class="language-lua">local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending) </code></pre> <h2 id="modules-stream-listeners">modules/stream/listeners</h2> <pre><code class="language-lua">listeners(): { OpenListener } </code></pre> <p>Every listener this engine currently holds an address for, in the order they were opened. Each entry is what <code>stream.listenerStatus</code> reports about it, plus the <code>handle</code> it is addressed by and the <code>owner</code> chunk its life follows.</p> <p>This is how an address is reached again once nothing holds its handle: filter on <code>address</code> for the port you want and close the entry by its <code>handle</code>, rather than guessing at handles.</p> <pre><code class="language-lua">for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end </code></pre> <h2 id="modules-stream-open">modules/stream/open</h2> <pre><code class="language-lua">open(url: string, opts: StreamOpenOpts?): PromiseHandle </code></pre> <p>Open a byte stream at url (<code>scheme://target[?k=v]</code>). <code>loopback</code> carries written bytes back out of the same stream and works on every platform; <code>tcp</code> dials <code>host:port</code>; <code>tty</code> opens a serial device node — <code>/dev/ttyACM0</code> or <code>/dev/ttyUSB0</code> for a USB CDC board such as an ESP32, <code>/dev/rfcomm0</code> for a Bluetooth controller paired over classic SPP (both present as a tty on Linux, so one transport serves either peer), <code>COM5</code> on Windows. <code>tty</code> query parameters: <code>baud</code> (default 115200), <code>dataBits</code> (5-8, default 8), <code>parity</code> (<code>none</code> | <code>odd</code> | <code>even</code>, default <code>none</code>), <code>stopBits</code> (1 or 2, default 1).</p> <p><code>ble</code> connects to a Bluetooth Low Energy device over GATT, on a desktop engine and in a browser alike — the wireless transport a web world reaches a device through: <code>ble://<device>?service=<uuid>&write=<uuid>&notify=<uuid></code>. The device is the name it advertises, <code>*</code> any device offering the service, a trailing <code>*</code> a name prefix (<code>Paw*</code>). <code>write</code> is the characteristic this engine writes to and <code>notify</code> the one it subscribes to, which on a Nordic UART peripheral are that peripheral's RX and TX; a module with one bidirectional characteristic names it for both. UUIDs may be 16-bit (<code>ffe0</code>), 32-bit, or full. Optional: <code>chunk</code> (bytes per packet, 1-512 — otherwise what the connection carries), <code>writeMode</code> (<code>withResponse</code> | <code>withoutResponse</code>, default <code>withResponse</code>), <code>timeout</code> (seconds to find and connect to the device, default 15).</p> <p><code>opts</code> bounds the stream's undrained inbound buffer and in-flight outbound bytes (default 65536 each). Returns a promise handle: <code>task.await()</code> it to get the stream handle once the transport is open, or it raises the reason a malformed url, an unknown or unsupported scheme, or a failed connect was refused with. A <code>ble</code> stream resolves as soon as it exists and reports the rest as state — watch <code>stream.status(handle).state</code> go <code>opening</code>, <code>permissionPending</code> while the browser asks the person at the machine to pick a device, then <code>open</code>; writes made meanwhile are queued and go out when it connects. Check <code>stream.transports()</code> first to tell a mistyped scheme from one this build does not carry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — Stream URL — scheme://target[?k=v&k=v].</li> <li><code>opts</code> <code>StreamOpenOpts?</code> <em>(optional)</em> — Buffer capacities (optional).</li> </ul> <pre><code class="language-lua">local pending = stream.open("loopback://echo"); local handle = task.await(pending) local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1&notify=ffe1")) </code></pre> <h2 id="modules-stream-read">modules/stream/read</h2> <pre><code class="language-lua">read(handle: string, max: number?): string </code></pre> <p>Drain up to max buffered inbound bytes from the stream.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Stream handle from stream.open.</li> <li><code>max</code> <code>number?</code> <em>(optional)</em> — Maximum bytes to drain (optional). Omit to drain everything buffered.</li> </ul> <pre><code class="language-lua">local chunk = stream.read(handle) </code></pre> <h2 id="modules-stream-serialports">modules/stream/serialPorts</h2> <pre><code class="language-lua">serialPorts(): SerialPorts </code></pre> <p>Every serial device this machine has, for picking the one to open. <code>ports</code> is an array ordered by path. Each entry carries the <code>path</code> the device is at (<code>/dev/ttyACM0</code> on Linux, <code>COM3</code> on Windows), the <code>url</code> that opens it, the <code>kind</code> of bus it attaches by, and — for a USB device — the <code>vendorId</code>, <code>productId</code>, <code>serialNumber</code>, <code>manufacturer</code> and <code>product</code> it advertises.</p> <p>A device's path moves with enumeration order: a board that came up at <code>/dev/ttyACM0</code> is at <code>/dev/ttyACM1</code> once something else is plugged in first, and moves across <code>COM3</code>-<code>COM5</code> on Windows. What the device advertises holds still across those moves, so match on <code>vendorId</code>/<code>productId</code> — or on <code>serialNumber</code> to tell two of the same board apart — and open the <code>url</code> that entry carries, appending the port settings <code>stream.open</code> documents.</p> <p>Three answers are distinct. <code>supported</code> false with a <code>reason</code> means this platform has no serial bus to enumerate at all. <code>error</code> set means it has one and the operating system refused this enumeration, so a later call may answer. An empty <code>ports</code> with neither means the machine has no serial device attached, which is an ordinary result.</p> <pre><code class="language-lua">for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end </code></pre> <h2 id="modules-stream-status">modules/stream/status</h2> <pre><code class="language-lua">status(handle: string): StreamStatus? </code></pre> <p>Report what the stream has carried and lost. <code>state</code> is where the stream is in its life: <code>opening</code>, <code>permissionPending</code> while the platform asks the person at the machine to allow the connection, <code>open</code>, <code>denied</code> when that permission was refused, and <code>closed</code> when it is finished. <code>pending</code> is bytes accepted and not yet handed to the peer; <code>capacity</code> is the value <code>pending</code> may reach before a write is refused. <code>error</code> holds the most recent transport failure and the refusal a <code>denied</code> stream carries, retained for the life of the stream. nil when handle names no open stream.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code></li> </ul> <pre><code class="language-lua">local s = stream.status(handle); print(s.pending, s.capacity) </code></pre> <h2 id="modules-stream-streams">modules/stream/streams</h2> <pre><code class="language-lua">streams(): { OpenStream } </code></pre> <p>Every open stream, dialled or answered, in the order they were opened. Each entry is what <code>stream.status</code> reports about it, plus the <code>handle</code> it is addressed by and the <code>owner</code> chunk its life follows — a connection <code>stream.accept</code> handed over carries the owner of the listener that answered it.</p> <pre><code class="language-lua">for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end </code></pre> <h2 id="modules-stream-transports">modules/stream/transports</h2> <pre><code class="language-lua">transports(): { [string]: TransportSupport } </code></pre> <p>Every stream scheme this build knows about — a capability probe, in both directions. <code>supported</code> answers <code>stream.open</code> and <code>listen</code> answers <code>stream.listen</code>, since a scheme can carry one and not the other. Each reason is nil when its direction works, otherwise it names why not: an unbuilt transport names its own absence, a transport this platform lacks (<code>tcp</code> and <code>tty</code> on wasm; <code>ble</code> in a browser without Web Bluetooth or with the radio off, which the page itself answers) names that, and a loopback stream, whose peer is itself, names that nothing dials it. A typo'd scheme is absent from this table entirely, which is what tells it apart from a real transport this build lacks.</p> <pre><code class="language-lua">local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end </code></pre> <h2 id="modules-stream-write">modules/stream/write</h2> <pre><code class="language-lua">write(handle: string, bytes: string): WriteOutcome </code></pre> <p>Queue bytes for the stream's peer. Never blocks. <code>"accepted"</code> means the bytes were queued. <code>"full"</code> means the outbound queue has no room right now — backpressure, not failure: the peer is alive and draining slower than this call is producing, so a retry after it catches up can succeed. Compare <code>pending</code> against <code>capacity</code> on <code>stream.status()</code> to see it coming before a write is refused. <code>"closed"</code> means the stream is finished, or handle names no open stream — reopen to continue, retrying never succeeds. <code>"tooLarge"</code> means bytes is bigger than the stream's whole outbound capacity, so it can never fit at any queue depth — retrying the same write returns this again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Stream handle from stream.open.</li> <li><code>bytes</code> <code>string</code> — Bytes to queue, byte-safe.</li> </ul> <pre><code class="language-lua">local outcome = stream.write(handle, data) </code></pre> <h2 id="modules-streaming-readme">modules/streaming/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.streaming") -- streaming (also available as global 'streaming') </code></pre> <p>What the engine has resident of a world's detail right now, and why a piece of it is not on screen. Covers the four systems that decide how much of a world stands at any moment: terrain's LOD cut, a voxel world's chunk meshes, spatial streaming's cell store, and mesh LOD's level selection. Reading a <code>Terrain</code>, <code>VoxelWorld</code>, <code>StreamingSource</code> or <code>MeshLod</code> component's fields back tells you what was asked for. <code>streaming.observe()</code> answers the other question: what is standing, what it costs, and for a chunk that is not drawn, which of a closed set of reasons it is not drawn for. The same reading is served at <code>/zero/runtime/observations/streaming</code>. It is built by the call and published as its last act, so both transports carry the document one pass produced.</p> <p>Usage: local streaming = require("@builtin::modules.api.engine.streaming") Also available as global: streaming</p> <h2 id="modules-streaming-cells">modules/streaming/cells</h2> <pre><code class="language-lua">cells(): { [string]: any } </code></pre> <p>What the spatial-streaming store has resident: the configured radii and budget, the counters the store keeps, and one row per cell with how many of its groups are standing, what it costs, and whether a release wrote it to a file it now reads back from.</p> <pre><code class="language-lua">local s = streaming.cells() </code></pre> <h2 id="modules-streaming-levels">modules/streaming/levels</h2> <pre><code class="language-lua">levels(scene: any?): { [string]: any } </code></pre> <p>Which level every mesh-LOD receiver is drawing at, and the screen fraction that selection was measured from. A receiver whose entity the scene no longer holds is reported as <code>standing = false</code>: the chain is registered and there is nothing left for it to draw.</p> <p><strong>Parameters</strong></p> <ul> <li><code>scene</code> <code>any?</code> <em>(optional)</em> — The scene walk to read against. Omitted, the call takes its own.</li> </ul> <pre><code class="language-lua">local l = streaming.levels() </code></pre> <h2 id="modules-streaming-observe">modules/streaming/observe</h2> <pre><code class="language-lua">observe(): { [string]: any } </code></pre> <p>The whole reading in one document: terrain, voxel, streaming cells and mesh LOD, plus the totals those rows sum to.</p> <p>Built by this call and published as its last act, so <code>/zero/runtime/observations/streaming</code> serves the same document rather than a second derivation of it.</p> <pre><code class="language-lua">local r = streaming.observe() </code></pre> <h2 id="modules-streaming-reasons">modules/streaming/reasons</h2> <pre><code class="language-lua">reasons(): { string } </code></pre> <p>Every reason <code>whyNotDrawn</code> can answer with, so a caller can enumerate the set rather than meeting it one failure at a time.</p> <pre><code class="language-lua">local r = streaming.reasons() </code></pre> <h2 id="modules-streaming-terrain">modules/streaming/terrain</h2> <pre><code class="language-lua">terrain(scene: any?): { [string]: any } </code></pre> <p>What each terrain entity is drawing: whether a heightfield is bound to it, the LOD cut it settled on, what that cut costs in indices and in the vertex pool, and the eye the cut was refined under: the camera in the entity's own coordinates, where its ground's vertices stand.</p> <p><strong>Parameters</strong></p> <ul> <li><code>scene</code> <code>any?</code> <em>(optional)</em> — The scene walk to read against. Omitted, the call takes its own, which is what makes a whole reading one walk rather than four.</li> </ul> <pre><code class="language-lua">local t = streaming.terrain() </code></pre> <h2 id="modules-streaming-voxel">modules/streaming/voxel</h2> <pre><code class="language-lua">voxel(scene: any?): { [string]: any } </code></pre> <p>What became of every chunk of every voxel world: how many are meshed, queued, failed or empty, and one row per chunk carrying the state, the engine's reason when a build failed, and what the build reserved on the device.</p> <p><strong>Parameters</strong></p> <ul> <li><code>scene</code> <code>any?</code> <em>(optional)</em> — The scene walk to read against. Omitted, the call takes its own.</li> </ul> <pre><code class="language-lua">local v = streaming.voxel() </code></pre> <h2 id="modules-streaming-whynotdrawn">modules/streaming/whyNotDrawn</h2> <pre><code class="language-lua">whyNotDrawn(subject: any): { [string]: any } </code></pre> <p>Why a piece of a world's detail is not on screen, as one reason from the closed set <code>streaming.reasons()</code> enumerates, with a detail line naming what that reason is about.</p> <p>The subject picks which system answers:</p> <ul> <li>an entity ref, id or name — whichever of the four systems holds it</li> <li><code>{ entity = ..., chunk = "cx_cy_cz" }</code> — one chunk of a voxel world</li> <li><code>{ entity = ..., level = n }</code> — one level of a mesh-LOD chain</li> <li><code>{ cell = "x_z" }</code> — one cell of the spatial-streaming store</li> </ul> <p><strong>Parameters</strong></p> <ul> <li><code>subject</code> <code>any</code> <em>(optional)</em> — The entity, chunk, level or cell to answer about.</li> </ul> <pre><code class="language-lua">local w = streaming.whyNotDrawn({ entity = "Vox", chunk = "0_0_0" }) </code></pre> <h2 id="modules-stringx-readme">modules/stringx/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.stringx") -- stringx (also available as global 'stringx') </code></pre> <p>Batch text-scanning kernels — read a whole run of numbers out of a string in one call. Public Luau surface over the <code>__stringx</code> Internal FFI namespace.</p> <p>Usage: local stringx = require("@builtin::modules.api.engine.stringx") Also available as global: stringx</p> <h2 id="modules-stringx-decodejson">modules/stringx/decodeJson</h2> <pre><code class="language-lua">decodeJson(s: string): (boolean, any) </code></pre> <p>Decode a JSON document in one call, when the document is one the engine's Luau JSON decoder reads the same way. <code>ok</code> is true and <code>value</code> holds the decoded value (nil for a document that is <code>null</code>); <code>ok</code> is false for text this leaves to that decoder: malformed text, nesting past 96 levels, escapes or control bytes the two could read differently, and text after the value. <code>modules.json</code> calls this first and falls back to its own scanners on <code>false</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>string</code> — The JSON text.</li> </ul> <pre><code class="language-lua">local ok, value = stringx.decodeJson('{"a":[1,2,3]}') </code></pre> <h2 id="modules-stringx-encodejson">modules/stringx/encodeJson</h2> <pre><code class="language-lua">encodeJson(value: any): (boolean, string?) </code></pre> <p>Encode a value as compact JSON in one call, when the value is one the engine's Luau JSON encoder writes the same way. <code>ok</code> is true and <code>text</code> holds the JSON; <code>ok</code> is false for a value this leaves to that encoder: a table with a metatable, a table that contains itself, a number key that is not finite, and nesting past 192 levels. <code>modules.json</code> calls this first and falls back to its own walk on <code>false</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>value</code> <code>any</code> <em>(optional)</em> — The value to encode.</li> </ul> <pre><code class="language-lua">local ok, text = stringx.encodeJson({ a = { 1, 2, 3 } }) </code></pre> <h2 id="modules-stringx-scannumbers">modules/stringx/scanNumbers</h2> <pre><code class="language-lua">scanNumbers(s: string, pos: number?): ({ number }, number) </code></pre> <p>Read the run of numbers starting at <code>pos</code> — separated by commas and/or whitespace — and report where the run ended.</p> <p>The run stops at the first character that neither continues a number nor separates two of them (<code>]</code>, <code>}</code>, a quote, a letter), and <code>nextPos</code> is that character's index, so the caller's own parser resumes exactly there. A token that is not a valid number also ends the run, with <code>nextPos</code> left ON it rather than past it, so nothing is skipped without the caller seeing it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>string</code> — The text to read.</li> <li><code>pos</code> <code>number?</code> <em>(optional)</em> — 1-based index to start at. Defaults to 1.</li> </ul> <pre><code class="language-lua">-- A JSON array of numbers, in one crossing instead of one per token. local values, nextPos = stringx.scanNumbers(payload, afterBracket) -- A whitespace-separated block (OBJ, PLY, a matrix dump). local m = stringx.scanNumbers("1 0 0 0 0 1 0 0", 1) </code></pre> <h2 id="modules-subscriptions-readme">modules/subscriptions/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.subscriptions") -- subscriptions (also available as global 'subscriptions') </code></pre> <p>Runtime inspection and cancellation of component-event subscriptions. Public Luau surface over the <code>__event_inspect_*</code> / <code>__event_track_*</code> Internal FFI globals; the same data is browsable at <code>/zero/runtime/events/</code>.</p> <p>Usage: local subscriptions = require("@builtin::modules.api.engine.subscriptions") Also available as global: subscriptions</p> <h2 id="modules-subscriptions-cancel">modules/subscriptions/cancel</h2> <pre><code class="language-lua">cancel(id: string): boolean </code></pre> <p>Cancel a subscription by id: disconnects the live connection immediately and marks the row cancelled. Returns true when a live subscription was cancelled, false for an unknown or already-disconnected id.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Subscription id to cancel.</li> </ul> <pre><code class="language-lua">subscriptions.cancel(conn.id) </code></pre> <h2 id="modules-subscriptions-get">modules/subscriptions/get</h2> <pre><code class="language-lua">get(id: string): SubscriptionRow? </code></pre> <p>One subscription row by id, or nil when the id is unknown (never tracked, or evicted after its publisher was destroyed).</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Subscription id (<code>conn.id</code>, or a <code>/zero/runtime/events/subscriptions/</code> entry).</li> </ul> <pre><code class="language-lua">local row = subscriptions.get(conn.id); print(row and row.deliveries) </code></pre> <h2 id="modules-subscriptions-list">modules/subscriptions/list</h2> <pre><code class="language-lua">list(filter: SubscriptionFilter?): { SubscriptionRow } </code></pre> <p>Every tracked subscription row, optionally filtered by publisher instance id, publisher entity id, event name, and/or connected state.</p> <p><strong>Parameters</strong></p> <ul> <li><code>filter</code> <code>SubscriptionFilter?</code> <em>(optional)</em> — Optional filter table.</li> </ul> <pre><code class="language-lua">for _, s in ipairs(subscriptions.list({ connected = true })) do print(s.id, s.event, s.deliveries) end </code></pre> <h2 id="modules-subscriptions-publishers">modules/subscriptions/publishers</h2> <pre><code class="language-lua">publishers(): { PublisherRow } </code></pre> <p>Every live event publisher: component instance, entity, and per-event fire stats (fires happen whether or not anyone subscribes) plus current subscriber ids.</p> <pre><code class="language-lua">for _, p in ipairs(subscriptions.publishers()) do print(p.component, p.entityName) end </code></pre> <h2 id="modules-substrate-readme">modules/substrate/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.substrate") -- substrate (also available as global 'substrate') </code></pre> <p>Typed data buffers, on the CPU or the GPU — the engine's one buffer primitive. Public Luau surface over the <code>__substrate</code> Internal FFI namespace.</p> <p>Usage: local substrate = require("@builtin::modules.api.engine.substrate") Also available as global: substrate</p> <h2 id="modules-substrate-await">modules/substrate/await</h2> <pre><code class="language-lua">await(self): ({ number }?, string?) </code></pre> <p>Wait for this read to arrive and drain it as f32 values. The wait runs on every frame the renderer draws, a frame drawn while pipeline builds are still landing among them, since the render system delivers a read on each of those frames. It ends on the read's own terminal state: the values once it delivered, or nil and the reason once the engine has given it up (a buffer that was not there, a device that was lost). A read that is still on its way is waited for, however long the GPU takes to answer it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">local values, why = pending:await() </code></pre> <h2 id="modules-substrate-bindframes">modules/substrate/bindFrames</h2> <pre><code class="language-lua">bindFrames(key: string, id: string, slots: { any }): (boolean, string?) </code></pre> <p>Bind the play <code>key</code> to the sequence <code>id</code> through <code>slots</code>, so <code>showFrame(key, f)</code> places its elements from frame f. <code>slots[s]</code> is <code>{ kind = "instances", key, transforms, data?, capacity, draw, interval? }</code> for elements an instanced draw copies from <code>transforms</code> — the scatter binding <code>key</code> places up to <code>capacity</code> of them there and the draw <code>draw</code> draws the frame's count — or <code>{ kind = "entities", key, binding?, interval? }</code> for elements standing in entities: the scatter binding <code>key</code>, whose entities <code>scatterTransforms</code> has already set, places them, and the frame's fields are written across the entity binding <code>binding</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The play's name.</li> <li><code>id</code> <code>string</code> — The sequence the play shows.</li> <li><code>slots</code> <code>{ any }</code> — What each numbered slot places, for this play.</li> </ul> <pre><code class="language-lua">substrate.bindFrames("spark#1", "spark", { { kind = "instances", key = "spark#1", transforms = "spark#1.xf", capacity = 64, draw = drawId } }) </code></pre> <h2 id="modules-substrate-buffersource">modules/substrate/bufferSource</h2> <pre><code class="language-lua">bufferSource(name: string): string? </code></pre> <p>The chunk that created the GPU buffer <code>name</code> holds, as <code>debug.info</code> names a chunk — a module's identity, a test suite's, an <code>execute</code> body's.</p> <p>The record is the name's for as long as a buffer holds the name, across a device rebuilt underneath it. A name created again after its buffer went answers for the call that created it again. A buffer the engine made itself, or one made through another surface, has no record.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The name the buffer was created under.</li> </ul> <pre><code class="language-lua">if substrate.bufferSource("debug_viz.vtx") then print("made through the substrate") end </code></pre> <h2 id="modules-substrate-clearscattertransforms">modules/substrate/clearScatterTransforms</h2> <pre><code class="language-lua">clearScatterTransforms(key: string): boolean </code></pre> <p>Drop the device transform binding under <code>key</code>. Its entities go back to being placed by whatever writes their <code>Transform</code>. Queued.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The key the binding was made under.</li> </ul> <pre><code class="language-lua">substrate.clearScatterTransforms("wave") </code></pre> <h2 id="modules-substrate-createbuffer">modules/substrate/createBuffer</h2> <pre><code class="language-lua">createBuffer(opts: BufferOpts): TypedBuffer? </code></pre> <p>Allocate a typed buffer and return its handle.</p> <p>A <code>"gpu"</code> buffer is storage a compute shader binds; <code>usage</code> adds <code>"vertex"</code>, <code>"index"</code>, <code>"indirect"</code> or <code>"readback"</code> on top of the storage it always has. A <code>"cpu"</code> buffer is storage the engine holds outside the Luau heap, and reads back as a flat array of floats. Reading its bytes into Luau (<code>:bytes()</code>, <code>:read():resultBytes()</code>) makes a copy in the Luau heap.</p> <p>The handle's <code>write</code> answers whether the words landed: a payload whose end falls past the end of the buffer is refused whole on both kinds, so the buffer keeps what it held and the call answers false. <code>writeU32</code> and <code>writeBytes</code> answer the same way, against the same extent.</p> <p>A <code>"cpu"</code> buffer holds at most 1 GiB of storage: 268435456 <code>f32</code> words, 16777216 <code>mat4</code> records. A <code>len</code> past that raises an error naming the bytes it needs and the limit, and a <code>len</code> within it whose storage the heap cannot supply raises an error naming the bytes. Neither allocates anything.</p> <p>A <code>"gpu"</code> buffer holds at most what the device creates in one buffer, its <code>max_buffer_size</code>. A <code>len</code> whose bytes pass it raises an error naming the records, the bytes they need, that limit and the most records of the type one buffer holds; nothing is allocated and the <code>name</code> stays free.</p> <p><code>class</code> makes a <code>"gpu"</code> buffer a run: it stands in a range of a page the engine's device arenas share among many runs of the class, rather than in an allocation of its own, so thousands of short-lived buffers cost a handful of device allocations. <code>"effect"</code> is for runs made and let go of as an effect plays, <code>"persistent"</code> for runs held until let go of. A run binds, writes, reads and frees like any <code>"gpu"</code> buffer under the name its handle carries, which the engine mints, so a run takes no <code>name</code>; a compute program declared with <code>ranges = true</code> binds any number of runs of one page through one bind group. A run carries <code>"readback"</code> usage and no other: a vertex, index or indirect buffer is one of its own. <code>owner</code> names whom a run is held for, and <code>substrate.releaseOwner(owner)</code> lets go of every run created with it at once.</p> <p>A job on a worker thread makes a run the same way. The handle answers at once, with the type and length asked for and a name the worker mints; <code>substrate.listBuffers</code> lists the run at once; and the engine thread places it in its arenas ahead of every write, dispatch and read the job makes over it afterwards, so one job can make a run, fill it, dispatch over it and read it back. A class, usage or <code>len</code> the engine thread refuses raises at the call in the job too. When the engine thread cannot place the run, the handle's next method call raises why. <code>substrate.setOwner</code> and <code>substrate.releaseOwner</code> answer in a job as they do on the engine thread, counting the job's own changes at once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>BufferOpts</code> — <code>{ type, len, kind?, usage?, name?, class?, group?, owner? }</code> — <code>type</code> is <code>"f32"</code>, <code>"vec3"</code>, <code>"vec4"</code>, <code>"quat"</code> or <code>"mat4"</code>; <code>kind</code> is <code>"cpu"</code> (the default) or <code>"gpu"</code>. <code>name</code> is the name a dispatch binds a <code>"gpu"</code> buffer by, and the name <code>substrate.getBuffer</code> and <code>substrate.destroyBuffer</code> reach it under. <code>class</code> (<code>"effect"</code> or <code>"persistent"</code>) makes a <code>"gpu"</code> buffer a run; <code>owner</code> names whom the run is held for.</li> </ul> <pre><code class="language-lua">local pose = substrate.createBuffer({ type = "mat4", len = boneCount }) local field = substrate.createBuffer({ type = "vec3", len = 4096, kind = "gpu" }) local column = substrate.createBuffer({ type = "f32", len = n, kind = "gpu", class = "effect", owner = "burst:" .. id }) local values = pose:read(0, 16):result() </code></pre> <h2 id="modules-substrate-defineframes">modules/substrate/defineFrames</h2> <pre><code class="language-lua">defineFrames(id: string, frames: { any }): (boolean, string?) </code></pre> <p>Hold a sequence of frames on the engine under <code>id</code>, for plays bound to it (<code>bindFrames</code>) to show by index (<code>showFrame</code>) with no script running per frame. <code>frames[f]</code> is <code>{ scatters = {...}, fields = {...} }</code>. A scatter <code>{ slot, positions, rotations?, scales?, lanes?, identities?, count }</code> names the buffers the slot's elements are placed from in that frame and how many elements they hold; <code>lanes[k]</code> is a buffer name or false. A field <code>{ slot, component, field, values }</code> gives the value the field takes on each of the slot's entities, in binding order. Slots are numbered from 1. The buffers stay the caller's and must stand for as long as the sequence is held. Defining <code>id</code> again replaces it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The sequence's name.</li> <li><code>frames</code> <code>{ any }</code> — The frames, in order.</li> </ul> <pre><code class="language-lua">substrate.defineFrames("spark", { { scatters = { { slot = 1, positions = P:bufferName(), count = 64 } } } }) </code></pre> <h2 id="modules-substrate-destroybuffer">modules/substrate/destroyBuffer</h2> <pre><code class="language-lua">destroyBuffer(name: string): boolean </code></pre> <p>Free the GPU buffer <code>name</code> denotes, whatever else still holds a handle to it.</p> <p>The allocation goes and the name is free to be created again at any type and length; every handle that pointed at it answers <code>:alive()</code> false. This is what releases a name whose creating handle is gone, so a build that re-runs at a different size gets its name back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The name the buffer was created under.</li> </ul> <pre><code class="language-lua">substrate.destroyBuffer("env.town.xf") </code></pre> <h2 id="modules-substrate-forgetframes">modules/substrate/forgetFrames</h2> <pre><code class="language-lua">forgetFrames(id: string): boolean </code></pre> <p>Drop the sequence held under <code>id</code>. A play still bound to it shows nothing further.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — The sequence's name.</li> </ul> <pre><code class="language-lua">substrate.forgetFrames("spark") </code></pre> <h2 id="modules-substrate-getbuffer">modules/substrate/getBuffer</h2> <pre><code class="language-lua">getBuffer(name: string): TypedBuffer? </code></pre> <p>The GPU buffer <code>name</code> denotes, as a handle you now hold.</p> <p>A name is how a dispatch binds a buffer, so the name is what an owner asks by once the handle it created with has gone out of scope — a <code>.module</code> that hot-reloaded, a build that ran in an earlier <code>execute</code>. The handle carries everything <code>createBuffer</code>'s does and releases its reference with <code>:destroy()</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The name the buffer was created under.</li> </ul> <pre><code class="language-lua">local xf = substrate.getBuffer("env.town.xf") local shape = xf and { xf:type(), xf:length() } </code></pre> <h2 id="modules-substrate-gpureadback">modules/substrate/gpuReadback</h2> <pre><code class="language-lua">gpuReadback(key: string?): Readback? </code></pre> <p>Wrap the key an FFI read handed back as the <code>Readback</code> that polls it. Every GPU→CPU read reaches the caller through this, so a texture's read and a buffer's read answer with the same thing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string?</code> <em>(optional)</em> — The key the read returned.</li> </ul> <pre><code class="language-lua">local pending = substrate.gpuReadback(compute.readTexture3D(handle)) </code></pre> <h2 id="modules-substrate-listbuffers">modules/substrate/listBuffers</h2> <pre><code class="language-lua">listBuffers(): { NamedBuffer } </code></pre> <p>Every named GPU buffer the engine holds, in name order.</p> <p>Each record states <code>id</code>, <code>name</code>, <code>type</code> (<code>"F32"</code>, <code>"Vec3"</code>, <code>"Vec4"</code>, <code>"Quat"</code>, <code>"Mat4"</code>), <code>len</code> in records, <code>refs</code> — how many holders it has — <code>holder</code>, the owner a run is held for when one was named (<code>createBuffer</code>'s <code>owner</code>), <code>class</code>, the lifetime class of a run (<code>"effect"</code> or <code>"persistent"</code>; absent for a buffer of its own), and <code>bytes</code>, the device bytes it takes: the range a run stands in, or the extent of a buffer of its own. This is what states which names are taken, at what shape, and what an owner holds.</p> <pre><code class="language-lua">for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end </code></pre> <h2 id="modules-substrate-losses">modules/substrate/losses</h2> <pre><code class="language-lua">losses(): number </code></pre> <p>How many times a buffer has been freed by name or by owner: <code>destroyBuffer</code>, <code>releaseOwner</code> and the reset to a baseline. A holder that checked that the buffers it names exist at one count knows they still do while the count is the same, whatever else it asks.</p> <pre><code class="language-lua">local at = substrate.losses() </code></pre> <h2 id="modules-substrate-markbaseline">modules/substrate/markBaseline</h2> <pre><code class="language-lua">markBaseline(): number </code></pre> <p>Record which named GPU buffers the engine holds right now, and make that the point <code>substrate.resetToBaseline</code> returns to.</p> <p>A caller running work in an engine it shares — a sweep of test suites, a tool driving a session — leaves buffers behind under the names the work chose: a view's geometry built on its first capture, a kernel's storage. This is how such a caller says where its own allocations start.</p> <pre><code class="language-lua">local mark = substrate.markBaseline() -- then run, then substrate.resetToBaseline(mark) </code></pre> <h2 id="modules-substrate-mintname">modules/substrate/mintName</h2> <pre><code class="language-lua">mintName(prefix: string): string </code></pre> <p><code>prefix</code> followed by a number no VM of this engine was handed before. The engine thread and every worker VM draw from one count, so a name minted in one VM is never one another VM minted: what names a buffer, a run's owner or anything else found by name when several VMs make them at once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>prefix</code> <code>string</code> — What the name starts with.</li> </ul> <pre><code class="language-lua">local name = substrate.mintName("cook.points.") </code></pre> <h2 id="modules-substrate-placement">modules/substrate/placement</h2> <pre><code class="language-lua">placement(name: string): BufferPlacement? </code></pre> <p>Where the GPU buffer <code>name</code> names stands on the device.</p> <p>A run stands in a range of an arena page that many runs share: <code>page</code> is the buffer a dispatch binds to reach it, <code>offset</code> the byte it starts at in that buffer (a multiple of 256) and <code>bytes</code> the bytes it holds. A buffer of its own is its own page at offset 0. A kernel that binds a page once and addresses every run of it by offset reads this to state each run's base. A job on a worker thread is answered by the engine thread, after it has placed every run the job created before the call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The name the buffer binds under (its handle's <code>name</code>).</li> </ul> <pre><code class="language-lua">local at = substrate.placement(column.name) </code></pre> <h2 id="modules-substrate-ready">modules/substrate/ready</h2> <pre><code class="language-lua">ready(self): boolean </code></pre> <p>Whether this read has arrived.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-substrate-releasebaseline">modules/substrate/releaseBaseline</h2> <pre><code class="language-lua">releaseBaseline(generation: number): boolean </code></pre> <p>Drop the mark <code>generation</code> names, and every mark taken after it, once the caller that took it is done resetting to it. It touches the marks alone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>generation</code> <code>number</code> — The mark to drop, as <code>substrate.markBaseline</code> returned it.</li> </ul> <pre><code class="language-lua">substrate.releaseBaseline(mark) -- the run is over </code></pre> <h2 id="modules-substrate-releaseowner">modules/substrate/releaseOwner</h2> <pre><code class="language-lua">releaseOwner(owner: string): number </code></pre> <p>Let go of every run created with <code>owner</code> (<code>createBuffer</code>'s <code>class</code> and <code>owner</code>), whatever else still holds a handle to it.</p> <p>Each run's range goes back to its arena and every handle to it answers <code>:alive()</code> false. This is what something that makes runs as it plays — an effect, a cook, a cache — calls when it ends, so everything it made goes with it at once, runs it handed on to others included.</p> <p><strong>Parameters</strong></p> <ul> <li><code>owner</code> <code>string</code> — The owner the runs were created with.</li> </ul> <pre><code class="language-lua">substrate.releaseOwner("burst:" .. id) </code></pre> <h2 id="modules-substrate-resettobaseline">modules/substrate/resetToBaseline</h2> <pre><code class="language-lua">resetToBaseline(generation: number?): number </code></pre> <p>Free every named GPU buffer created since <code>substrate.markBaseline</code>, leaving the ones it recorded where they are.</p> <p>A module that created one of these holds its own handle to it. Put that module's state back alongside this (<code>engine.restoreModuleState</code>) or it goes on addressing storage this freed. is touched.</p> <p><strong>Parameters</strong></p> <ul> <li><code>generation</code> <code>number?</code> <em>(optional)</em> — The mark to restore to, as <code>substrate.markBaseline</code> returned it; the newest mark when omitted. The marks taken after it go with the reset.</li> </ul> <pre><code class="language-lua">local freed = substrate.resetToBaseline(mark) </code></pre> <h2 id="modules-substrate-result">modules/substrate/result</h2> <pre><code class="language-lua">result(self): { number }? </code></pre> <p>Drain this read as f32 values, nil while it is still on its way. Every later call on this handle answers the same values.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-substrate-resultbytes">modules/substrate/resultBytes</h2> <pre><code class="language-lua">resultBytes(self): buffer? </code></pre> <p>Drain this read as a Luau <code>buffer</code>, nil while it is still on its way. Every later call on this handle answers the same buffer.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-substrate-resultu32">modules/substrate/resultU32</h2> <pre><code class="language-lua">resultU32(self): { number }? </code></pre> <p>Drain this read as u32 values, nil while it is still on its way. Every later call on this handle answers the same values.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-substrate-scattertransforms">modules/substrate/scatterTransforms</h2> <pre><code class="language-lua">scatterTransforms(key: string, entityIds: { string }?, positions: string, rotations: string?, scales: string?, lanes: { string? }?, interval: number?, identities: string?): boolean </code></pre> <p>Place entities from the buffers that already hold their transform, on the device, instead of from a <code>Transform</code> written per entity.</p> <p><code>entityIds[i]</code> is placed from row <code>i</code> of every named buffer, so the order is the buffers' own. Pass nil for <code>entityIds</code> to keep the entities a binding already has and replace only the buffers — what a re-cook does, since a field chain mints a fresh run each time while its membership stands still, and sending the element list again each time would put back the per-element crossing the binding exists to remove.</p> <p>Re-binding the same <code>key</code> with a list replaces the previous binding. Queued: it takes effect on the next frame, and an entity whose renderable has not landed yet is placed on the frame it does.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Stable identity of the binding.</li> <li><code>entityIds</code> <code>{ string }?</code> <em>(optional)</em> — The entities in buffer order, or nil to rebind the buffers alone.</li> <li><code>positions</code> <code>string</code> — Name of a buffer holding three f32 per element.</li> <li><code>rotations</code> <code>string?</code> <em>(optional)</em> — Name of a buffer holding four f32 per element.</li> <li><code>scales</code> <code>string?</code> <em>(optional)</em> — Name of a buffer holding three f32 per element.</li> <li><code>lanes</code> <code>{ string? }?</code> <em>(optional)</em> — Per-instance shader data, by lane: <code>lanes[k + 1]</code> names a buffer holding one vec4 per element, written into lane <code>k</code> of each element's block — what its material reads back as <code>input.shader_data[k]</code>. A lane left out is left to whatever else writes it.</li> <li><code>interval</code> <code>number?</code> <em>(optional)</em> — Seconds between the presents of the domain the buffers hold. Given, every frame draws each element between the last two presents — position, rotation, scale and lanes blended — the way a fixed-rate simulation is drawn between its ticks, so its motion is continuous at any frame rate while the domain presents at its own. An element new to a slot starts where it was presented. Omitted or 0, each present is drawn as it lands.</li> <li><code>identities</code> <code>string?</code> <em>(optional)</em> — Name of a buffer holding one f32 per element naming which element stands in that row at each present — for a domain whose rows are taken by different elements over time, as a particle's slot is when it dies and another is born into it. A row whose identity moved since the last present starts where it was presented rather than blending from the element that held it before. -1 names a row no element stands in. Omitted, each row's element is the entity placed from it.</li> </ul> <pre><code class="language-lua">substrate.scatterTransforms("wave", ids, positions:bufferName()) substrate.scatterTransforms("wave", nil, positions:bufferName()) substrate.scatterTransforms("wave", nil, positions:bufferName(), nil, nil, { colors:bufferName() }) substrate.scatterTransforms("sparks", ids, positions:bufferName(), nil, nil, nil, 1 / 30) substrate.scatterTransforms("sparks", nil, positions:bufferName(), nil, scales:bufferName(), nil, 1 / 30, ids:bufferName()) </code></pre> <h2 id="modules-substrate-scattertransformsinto">modules/substrate/scatterTransformsInto</h2> <pre><code class="language-lua">scatterTransformsInto(key: string, into: { transforms: string, data: string?, count: number }, positions: string, rotations: string?, scales: string?, lanes: { string? }?, interval: number?, identities: string?): boolean </code></pre> <p>Place elements that stand in no entity from the substrate buffers that hold their transform, on the device: row i of the columns is composed into matrix i of <code>into.transforms</code>, and lane k of block i of <code>into.data</code> — the two buffers an instanced draw (<code>renderer.mesh.drawInstanced</code> with <code>transformBuffer</code> and <code>instanceDataBuffer</code>) copies into its slots every frame. It is <code>scatterTransforms</code> for a population: the same columns, the same blend between presents when <code>interval</code> is given, the same <code>identities</code>. Binding the same <code>key</code> into the same buffers again replaces only the columns it reads. Queued: it takes effect on the next frame, ahead of the instanced copies that frame records.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Stable identity of the binding.</li> <li><code>into</code> <code>{ transforms: string, data: string?, count: number }</code> — <code>{ transforms, data?, count }</code>: a buffer of <code>count</code> column-major mat4, a buffer of <code>count</code> 64-byte blocks of four vec4 lanes when lanes are bound, and how many elements there are.</li> <li><code>positions</code> <code>string</code> — Name of a buffer holding three f32 per element.</li> <li><code>rotations</code> <code>string?</code> <em>(optional)</em> — Name of a buffer holding four f32 per element.</li> <li><code>scales</code> <code>string?</code> <em>(optional)</em> — Name of a buffer holding three f32 per element.</li> <li><code>lanes</code> <code>{ string? }?</code> <em>(optional)</em> — <code>lanes[k + 1]</code> names a buffer holding one vec4 per element, written into lane <code>k</code> of each element's block in <code>into.data</code>.</li> <li><code>interval</code> <code>number?</code> <em>(optional)</em> — Seconds between the domain's presents; given, each element is drawn between the last two presents.</li> <li><code>identities</code> <code>string?</code> <em>(optional)</em> — Name of a buffer holding one f32 per element naming which element stands in that row at each present; -1 names a row no element stands in.</li> </ul> <pre><code class="language-lua">substrate.scatterTransformsInto("sparks", { transforms = "sparks.xf", count = 4096 }, P:bufferName()) </code></pre> <h2 id="modules-substrate-scatterworlds">modules/substrate/scatterWorlds</h2> <pre><code class="language-lua">scatterWorlds(key: string): any </code></pre> <p>Read back where the device drew the elements of the transform binding under <code>key</code> in the last frame drawn — the device's own answer, since the CPU never learns where an element it places stands. 16 floats per placed element, column-major world matrices, in the binding's element order, leaving out elements whose renderable has not landed. A binding drawn between its presents answers the blended pose that frame drew.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The key the binding was made under.</li> </ul> <pre><code class="language-lua">local pending = substrate.scatterWorlds("sparks") while not pending:ready() do task.wait() end local x = pending:result()[13] </code></pre> <h2 id="modules-substrate-setowner">modules/substrate/setOwner</h2> <pre><code class="language-lua">setOwner(name: string, owner: string?): boolean </code></pre> <p>Hold the run <code>name</code> names for <code>owner</code> from now on, in place of the owner it was created with, or for no one when <code>owner</code> is nil.</p> <p><code>substrate.releaseOwner(owner)</code> then lets go of it with the rest of what <code>owner</code> holds, and releasing the owner it was made for leaves it standing. What hands a run on to whatever keeps it longer than the one that made it — a cache several makers read from.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The name the run binds under (its handle's <code>name</code>).</li> <li><code>owner</code> <code>string?</code> <em>(optional)</em> — The owner to hold it for, or nil.</li> </ul> <pre><code class="language-lua">substrate.setOwner(column.name, "cache#3") </code></pre> <h2 id="modules-substrate-settled">modules/substrate/settled</h2> <pre><code class="language-lua">settled(self): string </code></pre> <p>Wait until this read leaves <code>"pending"</code>, without draining it, and answer its state. The wait is on the read itself: where the engine can wait on the device the task resumes in the frame the device answered, so a task that reads, uses the answer and reads again does all of it in one frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <pre><code class="language-lua">if pending:settled() == "ready" then print(#pending:resultBytes(), "bytes read back") end </code></pre> <h2 id="modules-substrate-showframe">modules/substrate/showFrame</h2> <pre><code class="language-lua">showFrame(key: string, frame: number): boolean </code></pre> <p>Show frame <code>frame</code> (from 1) of the sequence the play <code>key</code> is bound to: each slot is placed from that frame's buffers, each instanced draw draws that frame's count, and that frame's fields are written. Showing the frame already shown changes nothing. Queued.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The play's name.</li> <li><code>frame</code> <code>number</code> — The frame, from 1.</li> </ul> <pre><code class="language-lua">substrate.showFrame("spark#1", 12) </code></pre> <h2 id="modules-substrate-state">modules/substrate/state</h2> <pre><code class="language-lua">state(self): string </code></pre> <p>Where this read stands, without draining it and without raising: <code>"pending"</code>, <code>"ready"</code>, <code>"failed"</code> (given up on, with the reason as a second return), or <code>"unknown"</code> (already drained). Every read leaves <code>"pending"</code> — one the GPU does not answer inside the bound the engine holds it to reaches <code>"failed"</code> naming the buffer — so a wait on <code>state() ~= "pending"</code> ends on what the read did.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-substrate-unbindframes">modules/substrate/unbindFrames</h2> <pre><code class="language-lua">unbindFrames(key: string): boolean </code></pre> <p>Drop the play <code>key</code>'s binding to its sequence. What it placed stays as the last frame shown left it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The play's name.</li> </ul> <pre><code class="language-lua">substrate.unbindFrames("spark#1") </code></pre> <h2 id="modules-subsurface-readme">modules/subsurface/README</h2> <pre><code class="language-lua">require("@builtin::systems.subsurfaceScattering.subsurface") -- subsurface </code></pre> <p>Light that enters a surface at one point and leaves at another — the transport that softens and reddens the terminator on skin, and that a per-pixel shading model cannot produce.</p> <p>A surface shaded per-pixel answers only for the light that arrived at that pixel. Skin does not work that way: light entering the lit side keeps travelling beneath the surface and emerges past where a cosine has already reached zero, and it emerges red, because red travels furthest through flesh. That is what makes the terminator on a face soft and warm instead of a hard line, and it is why a face with no scattering reads as plastic. The transport is a spread of exitant light across the surface, so it is computed on the drawn frame: marked meshes write their scattering into a mask, and the lit colour is spread along that mask. This module owns which entities are marked and what they scatter, and ties the pass's lifetime to whether anything is marked at all.</p> <p>Usage: local subsurface = require("@builtin::systems.subsurfaceScattering.subsurface")</p> <h2 id="modules-subsurface-active">modules/subsurface/active</h2> <pre><code class="language-lua">active() -> boolean </code></pre> <p><strong>Returns</strong> <code>boolean</code></p> <h2 id="modules-subsurface-clear">modules/subsurface/clear</h2> <pre><code class="language-lua">clear() </code></pre> <h2 id="modules-subsurface-groups">modules/subsurface/groups</h2> <pre><code class="language-lua">groups() -> table </code></pre> <p><strong>Returns</strong> <code>table</code></p> <h2 id="modules-subsurface-list">modules/subsurface/list</h2> <pre><code class="language-lua">list() -> { string } </code></pre> <p><strong>Returns</strong> <code>{ string }</code></p> <h2 id="modules-subsurface-mark">modules/subsurface/mark</h2> <pre><code class="language-lua">mark(entityRef: string, opts: MarkOpts?) -> table </code></pre> <p>Give an entity's meshes a scattering tint and radius.</p> <p><strong>Parameters</strong></p> <ul> <li><code>entityRef</code> <code>string</code></li> <li><code>opts</code> <code>MarkOpts?</code> <em>(optional)</em></li> </ul> <p><strong>Returns</strong> <code>table</code></p> <h2 id="modules-subsurface-marked">modules/subsurface/marked</h2> <pre><code class="language-lua">marked(entityRef: string) -> table? </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>entityRef</code> <code>string</code></li> </ul> <p><strong>Returns</strong> <code>table?</code></p> <h2 id="modules-subsurface-paramsbuffer">modules/subsurface/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.</p> <pre><code class="language-lua">local p = subsurface.paramsBuffer() </code></pre> <h2 id="modules-subsurface-setstrength">modules/subsurface/setStrength</h2> <pre><code class="language-lua">setStrength(strength: number) -> number </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>strength</code> <code>number</code></li> </ul> <p><strong>Returns</strong> <code>number</code></p> <h2 id="modules-subsurface-strength">modules/subsurface/strength</h2> <pre><code class="language-lua">strength() -> number </code></pre> <p><strong>Returns</strong> <code>number</code></p> <h2 id="modules-subsurface-unmark">modules/subsurface/unmark</h2> <pre><code class="language-lua">unmark(entityRef: string) -> boolean </code></pre> <p><strong>Parameters</strong></p> <ul> <li><code>entityRef</code> <code>string</code></li> </ul> <p><strong>Returns</strong> <code>boolean</code></p> <h2 id="modules-surfacelight-readme">modules/surfaceLight/README</h2> <pre><code class="language-lua">require("@builtin::systems.areaLights.surfaceLight") -- surfaceLight </code></pre> <p>Rectangular area lights — a screen, a window or a strip that emits from its whole surface, so its highlight is a shape rather than a dot.</p> <p>Usage: local surfaceLight = require("@builtin::systems.areaLights.surfaceLight")</p> <h2 id="modules-surfacelight-active">modules/surfaceLight/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the area-lighting pass is currently running.</p> <pre><code class="language-lua">if surfaceLight.active() then print("emitting") end </code></pre> <h2 id="modules-surfacelight-buffers">modules/surfaceLight/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The buffers the area-lighting pass reads: the settings and the packed light records. The render feature binds what this hands it.</p> <pre><code class="language-lua">local b = surfaceLight.buffers() </code></pre> <h2 id="modules-surfacelight-clear">modules/surfaceLight/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Drop every area light and release the pass. The scales are kept.</p> <pre><code class="language-lua">surfaceLight.clear() </code></pre> <h2 id="modules-surfacelight-configure">modules/surfaceLight/configure</h2> <pre><code class="language-lua">configure(opts: SurfaceLightOpts?): SurfaceLightState </code></pre> <p>Adjust how the two terms are weighted. Any omitted field keeps its current value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>SurfaceLightOpts?</code> <em>(optional)</em> — Settings — see <code>SurfaceLightOpts</code>.</li> </ul> <pre><code class="language-lua">surfaceLight.configure({ specular = 0.5 }) </code></pre> <h2 id="modules-surfacelight-count">modules/surfaceLight/count</h2> <pre><code class="language-lua">count(): number </code></pre> <p>How many area lights are registered.</p> <pre><code class="language-lua">print(surfaceLight.count()) </code></pre> <h2 id="modules-surfacelight-remove">modules/surfaceLight/remove</h2> <pre><code class="language-lua">remove(key: string): boolean </code></pre> <p>Remove the area light registered under <code>key</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The identifier the light was registered with.</li> </ul> <pre><code class="language-lua">surfaceLight.remove("tv") </code></pre> <h2 id="modules-surfacelight-set">modules/surfaceLight/set</h2> <pre><code class="language-lua">set(key: string, shape: SurfaceLightShape): number </code></pre> <p>Add or replace the area light registered under <code>key</code>. Re-submitting the same key moves that light rather than adding another, which is what lets a component push its rectangle every frame as its entity moves.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Stable identifier — an entity id works well.</li> <li><code>shape</code> <code>SurfaceLightShape</code> — The emitting rectangle — see <code>SurfaceLightShape</code>.</li> </ul> <pre><code class="language-lua">surfaceLight.set("tv", { position = { 0, 2, 0 }, right = { 1, 0, 0 }, up = { 0, 1, 0 }, width = 2, height = 1.2 }) </code></pre> <h2 id="modules-surfacelight-settings">modules/surfaceLight/settings</h2> <pre><code class="language-lua">settings(): SurfaceLightState </code></pre> <p>The settings currently in force.</p> <pre><code class="language-lua">local d = surfaceLight.settings().diffuse </code></pre> <h2 id="modules-systemsettings-readme">modules/systemSettings/README</h2> <pre><code class="language-lua">require("@builtin::modules.systemSettings") -- systemSettings </code></pre> <p>Which of several components stating one scene-wide system's settings is in force, so a component going away never takes down a system another live component still states.</p> <p>Usage: local systemSettings = require("@builtin::modules.systemSettings")</p> <h2 id="modules-systemsettings-claim">modules/systemSettings/claim</h2> <pre><code class="language-lua">claim(key: string, spec: ClaimSpec): Claim </code></pre> <p>A claim on the scene-wide system named <code>key</code>. Every claim made with the same key shares one registry, so a component makes one per instance, at the top of its script, keyed by the path of the module that drives the system.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The system's name: the path of the module that drives it.</li> <li><code>spec</code> <code>ClaimSpec</code> — How to put settings in force, take the system down and read whether it is up.</li> </ul> <pre><code class="language-lua">local claim = systemSettings.claim("@builtin::systems.reflections.ssr", { push = Ssr.set, release = Ssr.clear, live = Ssr.active }) </code></pre> <h2 id="modules-systemsettings-count">modules/systemSettings/count</h2> <pre><code class="language-lua">count(key: string): number </code></pre> <p>How many live claims state the system named <code>key</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The system's name, as given to <code>claim</code>.</li> </ul> <h2 id="modules-systemsettings-inforce">modules/systemSettings/inForce</h2> <pre><code class="language-lua">inForce(self: any): boolean </code></pre> <p>Whether this claim's settings are the ones in force.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-systemsettings-release">modules/systemSettings/release</h2> <pre><code class="language-lua">release(self: any) </code></pre> <p>Let the system go; call it from the component's <code>onDestroy</code>. The newest other live claim on the system takes over, and the system is taken down only when this was the last one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> </ul> <h2 id="modules-systemsettings-state">modules/systemSettings/state</h2> <pre><code class="language-lua">state(self: any, sig: string, opts: any) </code></pre> <p>State this claim's settings for the frame. Call it every frame the component runs; it pushes only when this claim is in force and the settings changed, or the system it brought up is no longer up.</p> <p><strong>Parameters</strong></p> <ul> <li><code>self</code> <code>any</code> <em>(optional)</em></li> <li><code>sig</code> <code>string</code> — A string that differs whenever <code>opts</code> differs.</li> <li><code>opts</code> <code>any</code> <em>(optional)</em> — The settings, handed to the spec's <code>push</code> when they are pushed.</li> </ul> <h2 id="modules-temporalantialiasing-readme">modules/temporalAntiAliasing/README</h2> <pre><code class="language-lua">require("@builtin::systems.antiAliasing.temporalAntiAliasing") -- temporalAntiAliasing </code></pre> <p>Antialiasing that accumulates one sample per pixel per frame. The projection samples a different point inside each pixel every frame, and the frames are carried forward through the scene's own motion, so edges, highlights and fine texture settle instead of crawling.</p> <p>Usage: local temporalAntiAliasing = require("@builtin::systems.antiAliasing.temporalAntiAliasing")</p> <h2 id="modules-temporalantialiasing-active">modules/temporalAntiAliasing/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the temporal pass is running this frame.</p> <pre><code class="language-lua">if temporalAntiAliasing.active() then ... end </code></pre> <h2 id="modules-temporalantialiasing-buffers">modules/temporalAntiAliasing/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The parameter buffer the accumulation reads, carrying the settings this module packs. The render feature binds what this hands it.</p> <pre><code class="language-lua">local b = temporalAntiAliasing.buffers() </code></pre> <h2 id="modules-temporalantialiasing-disable">modules/temporalAntiAliasing/disable</h2> <pre><code class="language-lua">disable() </code></pre> <p>Turn temporal antialiasing off and release the pass. The projection goes back to sampling pixel centres. The settings are kept, so a later <code>enable()</code> brings back the same tuning.</p> <pre><code class="language-lua">temporalAntiAliasing.disable() </code></pre> <h2 id="modules-temporalantialiasing-enable">modules/temporalAntiAliasing/enable</h2> <pre><code class="language-lua">enable(opts: TemporalAntiAliasingOpts?): TemporalAntiAliasingState </code></pre> <p>Turn temporal antialiasing on and set it. Any omitted field keeps its current value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>TemporalAntiAliasingOpts?</code> <em>(optional)</em> — Temporal antialiasing settings — see <code>TemporalAntiAliasingOpts</code>.</li> </ul> <pre><code class="language-lua">temporalAntiAliasing.enable({ historyWeight = 0.9 }) </code></pre> <h2 id="modules-temporalantialiasing-get">modules/temporalAntiAliasing/get</h2> <pre><code class="language-lua">get(): TemporalAntiAliasingState </code></pre> <p>The temporal antialiasing settings currently in force.</p> <pre><code class="language-lua">local w = temporalAntiAliasing.get().historyWeight </code></pre> <h2 id="modules-temporalupscale-readme">modules/temporalUpscale/README</h2> <pre><code class="language-lua">require("@builtin::systems.temporalUpscale.temporalUpscale") -- temporalUpscale </code></pre> <p>Reconstruction of a display-resolution image from a lower-resolution render. The projection samples a different point inside each pixel every frame and the frames are carried forward through the scene's own motion, so a scene rasterized at a fraction of the display's pixels resolves detail no single one of those frames holds. <code>enable</code>'s <code>backend</code> field chooses what fills the upscale seam this reconstruction sits after — <code>"auto"</code>, <code>"dlss"</code>, <code>"taau"</code> or <code>"none"</code> — and whether this module's own reconstruction runs alongside it; see <code>TemporalUpscaleOpts</code>.</p> <p>Usage: local temporalUpscale = require("@builtin::systems.temporalUpscale.temporalUpscale")</p> <h2 id="modules-temporalupscale-active">modules/temporalUpscale/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the reconstruction is running this frame.</p> <pre><code class="language-lua">if temporalUpscale.active() then ... end </code></pre> <h2 id="modules-temporalupscale-buffers">modules/temporalUpscale/buffers</h2> <pre><code class="language-lua">buffers(): { [string]: any }? </code></pre> <p>The parameter buffer the reconstruction reads, carrying the settings this module packs. The render feature binds what this hands it.</p> <pre><code class="language-lua">local b = temporalUpscale.buffers() </code></pre> <h2 id="modules-temporalupscale-disable">modules/temporalUpscale/disable</h2> <pre><code class="language-lua">disable() </code></pre> <p>Turn temporal upsampling off and release the pass. The projection goes back to sampling pixel centres, the upscale seam returns to the blit, and the render scale returns to what it was before this module first moved it — unless something else has steered it since, which keeps that number. The settings are kept, so a later <code>enable()</code> brings back the same tuning.</p> <pre><code class="language-lua">temporalUpscale.disable() </code></pre> <h2 id="modules-temporalupscale-enable">modules/temporalUpscale/enable</h2> <pre><code class="language-lua">enable(opts: TemporalUpscaleOpts?): TemporalUpscaleState </code></pre> <p>Turn temporal upsampling on and set it. Any omitted field keeps its current value. <code>backend</code> chooses what fills the upscale seam and whether this module's own reconstruction runs alongside it — see <code>TemporalUpscaleOpts</code>. Passing <code>renderScale</code> also moves the resolution the scene rasterizes at, and <code>disable</code> puts back the scale that was in force before the first such move.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>TemporalUpscaleOpts?</code> <em>(optional)</em> — Temporal upsampling settings — see <code>TemporalUpscaleOpts</code>.</li> </ul> <pre><code class="language-lua">temporalUpscale.enable({ backend = "auto", quality = "balanced" }) </code></pre> <h2 id="modules-temporalupscale-get">modules/temporalUpscale/get</h2> <pre><code class="language-lua">get(): TemporalUpscaleState </code></pre> <p>The temporal upsampling settings currently in force.</p> <pre><code class="language-lua">local s = temporalUpscale.get().renderScale </code></pre> <h2 id="modules-text-readme">modules/text/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.text") -- text (also available as global 'text') </code></pre> <p>Text rasterisation resource — create a text handle, set its content and style, then rasterise it to a texture for display, and observe what the text system is holding. Public Luau surface over the <code>__text</code> and <code>__textObserve</code> Internal FFI namespaces.</p> <p>Usage: local text = require("@builtin::modules.api.engine.text") Also available as global: text</p> <h2 id="modules-text-alive">modules/text/alive</h2> <pre><code class="language-lua">alive(handle: any): boolean </code></pre> <p>Whether the text system still holds this handle — true between <code>text.create</code> and the <code>text.destroy</code> that released it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> </ul> <pre><code class="language-lua">if not text.alive(h) then h = text.create({ content = "again" }) end </code></pre> <h2 id="modules-text-count">modules/text/count</h2> <pre><code class="language-lua">count(): number </code></pre> <p>How many text objects the text system is holding — the number that moves when <code>text.create</code> and <code>text.destroy</code> are called.</p> <pre><code class="language-lua">local before = text.count() </code></pre> <h2 id="modules-text-create">modules/text/create</h2> <pre><code class="language-lua">create(options: table): any </code></pre> <p>Create a text handle from an initial content + style table. The handle owns a runtime GPU texture (see <code>text.textureGuid</code>); pass it to every other call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>options</code> <code>table</code> — Table of <code>content</code> plus style fields (fontSize, color, alignment, richText, maxWidth, ...).</li> </ul> <pre><code class="language-lua">local h = text.create({ content = "Hello", fontSize = 48 }) </code></pre> <h2 id="modules-text-destroy">modules/text/destroy</h2> <pre><code class="language-lua">destroy(handle: any): boolean </code></pre> <p>Destroy a text handle and release its raster + glyph layout.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> </ul> <pre><code class="language-lua">text.destroy(h) </code></pre> <h2 id="modules-text-face">modules/text/face</h2> <pre><code class="language-lua">face(handle: any): any </code></pre> <p>Which font face one handle actually shaped with, and whether that is the family its style selects. <code>requested</code> is the name the style wrote, <code>selected</code> is the family that name selects — itself for a family the shaper's own database carries, and the family a font file declares for a name registered onto it — <code>resolved</code> is the face that answered, <code>matched</code> says whether the face is the family the name selected and <code>reason</code> says why when it is not — one of <code>text.faceReasons()</code>. A style that named no family reports <code>noFamilyRequested</code>: it got the default because it asked for nothing, so <code>reason</code> rather than <code>matched</code> is what an alert switches on. <code>faces</code> lists every face the shaper used, most glyphs first, so a fallback that covered part of the string is visible alongside the face that covered the rest. <code>missing</code> lists the characters no face drew (the missing glyph stood in); a character an on-demand face covers requests it, and the object is shaped again once the face is installed (<code>font.onDemand()</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> </ul> <pre><code class="language-lua">local r = text.face(h).reason; if r == "familyUnknown" or r == "familyNotSelectable" then print(r) end </code></pre> <h2 id="modules-text-facereasons">modules/text/faceReasons</h2> <pre><code class="language-lua">faceReasons(): { string } </code></pre> <p>Every reason the face readings give for a label or a family not being in the family a style named, nearest cause first. <code>text.face</code> gives them for one label; <code>font.reconcile()</code> also gives <code>familyCoveredNoGlyph</code>, which it can only reach by laying the family out under its own weights and over several scripts.</p> <pre><code class="language-lua">for _, r in ipairs(text.faceReasons()) do print(r) end </code></pre> <h2 id="modules-text-isdirty">modules/text/isDirty</h2> <pre><code class="language-lua">isDirty(handle: any): boolean </code></pre> <p>Whether the handle was laid out again since it was last rasterised: its content or style changed, or a font arriving, leaving or changing the names it answers to changed the face it draws with. Rasterise it again when this answers true to show what it now says.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> </ul> <pre><code class="language-lua">if text.isDirty(h) then text.rasterize(h, text.textureGuid(h)) end </code></pre> <h2 id="modules-text-listfonts">modules/text/listFonts</h2> <pre><code class="language-lua">listFonts(): { string } </code></pre> <p>List the font families currently available to the text system.</p> <pre><code class="language-lua">local fonts = text.listFonts() </code></pre> <h2 id="modules-text-loadfont">modules/text/loadFont</h2> <pre><code class="language-lua">loadFont(ref: any): any </code></pre> <p>Load a font from an asset reference so it becomes available to <code>setStyle</code>'s <code>fontFamily</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ref</code> <code>any</code> <em>(optional)</em> — Font asset reference or path.</li> </ul> <pre><code class="language-lua">text.loadFont(asset.ref("fonts.inter", "font")) </code></pre> <h2 id="modules-text-measure">modules/text/measure</h2> <pre><code class="language-lua">measure(handle: any): any </code></pre> <p>Measure the rasterised text in pixels without producing a texture.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> </ul> <pre><code class="language-lua">local size = text.measure(h) </code></pre> <h2 id="modules-text-observe">modules/text/observe</h2> <pre><code class="language-lua">observe(): any </code></pre> <p>Everything the text system is holding right now. <code>count</code> is the live text objects; <code>objects</code> is one row each, carrying its content, the style it was laid out with, its measured extent, whether it is dirty, the <code>face</code> the shaper actually used, the <code>owner</code> entity whose component created it with whether that entity is still there, and the <code>raster</code> texture its last rasterisation landed in with the bytes it costs. <code>orphans</code> is the subset whose owning entity is gone, <code>fonts</code> the families the shaper can resolve, and <code>raster</code> the glyph-raster bytes with the pool they belong to named. Built when you ask, so it costs nothing per frame and reads the same in edit mode as in play.</p> <pre><code class="language-lua">local live = text.observe().count </code></pre> <h2 id="modules-text-orphans">modules/text/orphans</h2> <pre><code class="language-lua">orphans(): { any } </code></pre> <p>The text objects whose owning entity no longer exists — a quad the engine is still holding for something that has been despawned. Each row is the same shape <code>text.observe().objects</code> carries.</p> <pre><code class="language-lua">print(#text.orphans() .. " labels outlived their entity") </code></pre> <h2 id="modules-text-rastermemory">modules/text/rasterMemory</h2> <pre><code class="language-lua">rasterMemory(): any </code></pre> <p>The glyph-raster bytes, broken out of the runtime GPU texture pool. <code>bytes</code> is summed off the same map <code>renderer.gpuMemory().textures</code> is totalled from, so <code>shareOfPool</code> is a share of that number rather than a second count of the same memory.</p> <pre><code class="language-lua">local r = text.rasterMemory(); print(r.bytes .. " of " .. r.poolBytes) </code></pre> <h2 id="modules-text-rasterize">modules/text/rasterize</h2> <pre><code class="language-lua">rasterize(handle: any, texture: any, scale: number?): any </code></pre> <p>Rasterise the handle's current text + style into the given runtime GPU texture. Bind that texture's guid as a material's <code>base_color_texture</code> to display the text; re-rasterising the same texture overwrites it in place.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> <li><code>texture</code> <code>any</code> <em>(optional)</em> — Destination GPU texture handle (<code>renderer.texture.create</code>) or its guid string — WHERE the raster lands.</li> <li><code>scale</code> <code>number?</code> <em>(optional)</em> — World/pixel scale factor for the raster (default 1.0).</li> </ul> <pre><code class="language-lua">local tex = renderer.texture.create({ width = 256, height = 64 }) local r = text.rasterize(h, tex, 1.0) </code></pre> <h2 id="modules-text-setstyle">modules/text/setStyle</h2> <pre><code class="language-lua">setStyle(handle: any, style: table): boolean </code></pre> <p>Replace the handle's style. Fields not present keep their current value.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> <li><code>style</code> <code>table</code> — Style table (fontSize, color, alignment, outline, ...).</li> </ul> <pre><code class="language-lua">text.setStyle(h, { fontSize = 64, color = "yellow" }) </code></pre> <h2 id="modules-text-settext">modules/text/setText</h2> <pre><code class="language-lua">setText(handle: any, content: string): boolean </code></pre> <p>Replace the handle's text content.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> <li><code>content</code> <code>string</code> — New text string.</li> </ul> <pre><code class="language-lua">if not text.setText(h, "HP: 100") then h = text.create({ content = "HP: 100" }) end </code></pre> <h2 id="modules-text-textureguid">modules/text/textureGuid</h2> <pre><code class="language-lua">textureGuid(handle: any): string? </code></pre> <p>The runtime GPU texture guid this handle rasterises into — bind it as a material texture (<code>base_color_texture</code>) to display the text.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>any</code> <em>(optional)</em> — Text handle from <code>text.create</code>.</li> </ul> <pre><code class="language-lua">entity(id).component.get("Material"):setTexture("base_color_texture", text.textureGuid(h)) </code></pre> <h2 id="modules-trusted-readme">modules/trusted/README</h2> <pre><code class="language-lua">require("@builtin::modules.trusted_client") -- trusted </code></pre> <p>User-space client for the trusted -> user call bridge (<code>trusted.call</code>).</p> <p>Lets user-space scripts TRIGGER orchestration that lives in the engine's trusted VM (auth / connection / service flows we author in trusted Luau) WITHOUT exposing the trusted source or the privileged primitives those flows use. Backed by the single <code>__trustedBridge.invoke</code> Rust transport: the named call is marshalled into the trusted VM, run against its __TRUSTED_EXPORTS registry, and the result marshalled back as plain data. Trusted methods are registered on the other side by the trusted module <code>/zero/trusted/exports</code> (its <code>expose(name, handler)</code>). This module is auto-injected as the global <code>trusted</code> by the prelude.</p> <p>Usage: local trusted = require("@builtin::modules.trusted_client")</p> <h2 id="modules-ui-readme">modules/ui/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.ui") -- ui (also available as global 'ui') </code></pre> <p>UI system — screens, styles, themes, widget responses, focus, custom-widget builders, per-widget state. Public Luau surface over the <code>__ui</code> Internal FFI namespace.</p> <p>Usage: local ui = require("@builtin::modules.api.engine.ui") Also available as global: ui</p> <h2 id="modules-ui-blur">modules/ui/blur</h2> <pre><code class="language-lua">blur() </code></pre> <p>Surrender keyboard focus from whichever widget currently holds it, in the editor's UI and the game's alike.</p> <h2 id="modules-ui-bringareatofront">modules/ui/bringAreaToFront</h2> <pre><code class="language-lua">bringAreaToFront(id: string) </code></pre> <p>Raise a movable <code>area</code> to the top of the window stacking order — the programmatic equivalent of clicking it. Areas sharing a stacking band order by interaction, so this is the call that brings one forward from code: use it when a taskbar button, focus change, or app launch should raise a window. Moving a screen to a higher <code>layer</code> band raises it over the bands below.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Area widget id.</li> </ul> <h2 id="modules-ui-capturewindow">modules/ui/captureWindow</h2> <pre><code class="language-lua">captureWindow(screen: string, window: string, opts: CaptureOpts?): CaptureResult? </code></pre> <p>Render a single Window widget to its own offscreen texture and write the result as PNG at <code>/runtime/render_surfaces/<rtHandle>.png</code>. The screen does NOT need to be visible. Returns <code>{ rtHandle, texturePath }</code> or nil on invalid inputs (width/height clamped to <code>[1, 8192]</code>, defaults 600x400).</p> <p><strong>Parameters</strong></p> <ul> <li><code>screen</code> <code>string</code> — Screen id containing the target Window.</li> <li><code>window</code> <code>string</code> — Widget id of the Window.</li> <li><code>opts</code> <code>CaptureOpts?</code> <em>(optional)</em> — <code>{ width, height }</code> (optional).</li> </ul> <h2 id="modules-ui-click">modules/ui/click</h2> <pre><code class="language-lua">click(callbackId: string, value: any?): boolean </code></pre> <p>Simulate a widget click / interaction by its callback id. The call carries no screen, so an id that names widgets on several screens reaches every component that declared it, once each. The callback id is the one a widget emits: the string its <code>onClick</code> / <code>onChange</code> / other <code>on*</code> prop names, or its element <code>id</code> where it leaves that prop unnamed (a button clicked without an <code>onClick</code>, a slider changed without an <code>onChange</code>). Given the element id of a widget whose interactions emit other ids, it raises an error naming those ids, and clicks nothing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>callbackId</code> <code>string</code> — Callback id the widget emits (its <code>onClick</code> / <code>onChange</code> / <code>on*</code> prop).</li> <li><code>value</code> <code>any?</code> <em>(optional)</em> — Optional value the callback receives as <code>data.value</code>: a number, a string, a boolean, or a table with the fields a structured interaction delivers (a code editor's <code>{ token, line, col }</code>, a canvas drag's deltas).</li> </ul> <h2 id="modules-ui-copytext">modules/ui/copyText</h2> <pre><code class="language-lua">copyText(text: string) </code></pre> <p>Put <code>text</code> on the host clipboard — the operating system's clipboard on a native build, the browser's in a web build. What a "Copy path" or "Copy reference" action calls.</p> <p><strong>Parameters</strong></p> <ul> <li><code>text</code> <code>string</code> — The text to copy.</li> </ul> <pre><code class="language-lua">ui.copyText("/zero/source/scenes/main.scene") </code></pre> <h2 id="modules-ui-definestyle">modules/ui/defineStyle</h2> <pre><code class="language-lua">defineStyle(name: string, style: StyleProps) </code></pre> <p>Define a named style. Style keys follow <code><widgetType>.<className></code> (e.g. <code>"label.h1"</code>, <code>"button.primary"</code>) or bare <code><className></code> to apply across widget types. Widgets reference styles via the <code>classes</code> (or <code>class</code>) prop.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Style name.</li> <li><code>style</code> <code>StyleProps</code> — Style properties table.</li> </ul> <h2 id="modules-ui-definestyles">modules/ui/defineStyles</h2> <pre><code class="language-lua">defineStyles(styles: { [string]: StyleProps }) </code></pre> <p>Define multiple named styles at once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>styles</code> <code>{ [string]: StyleProps }</code> — Map of style name to style properties.</li> </ul> <h2 id="modules-ui-definewidget">modules/ui/defineWidget</h2> <pre><code class="language-lua">defineWidget(name: string, builderFn: (WidgetTree, { WidgetTree }) -> WidgetTree) </code></pre> <p>Register a custom widget kind. When a tree contains <code>{ type = name, props = ..., children = ... }</code>, the decoder calls <code>builderFn(props, children)</code> at register / update time and substitutes the returned widget table in place. Errors surface through <code>ui.lastValidation()</code> with codes <code>widget-builder-error</code> / <code>widget-builder-bad-return</code> / <code>decode-recursion-depth-exceeded</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Custom widget kind name.</li> <li><code>builderFn</code> <code>(WidgetTree, { WidgetTree }) -> WidgetTree</code> — Builder closure <code>(props, children) -> widgetTable</code>.</li> </ul> <h2 id="modules-ui-diagnose">modules/ui/diagnose</h2> <pre><code class="language-lua">diagnose(widgetId: string): WidgetPaint? </code></pre> <p>Why one widget did or did not reach the last frame. Returns that widget's row from <code>ui.observe()</code> — the same fields, resolved against the same reading. An id no registered screen carries reads <code>noSuchWidget</code>, which is how a misspelling separates from a widget whose screen is hidden and from one the frame laid out no box for.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — The id the widget records layout under.</li> </ul> <pre><code class="language-lua">local row = ui.diagnose("hud-healthbar") </code></pre> <h2 id="modules-ui-displayscale">modules/ui/displayScale</h2> <pre><code class="language-lua">displayScale(): DisplayScale </code></pre> <p>The physical scale the UI is sized at. <code>density</code> is the physical pixels one density-independent point covers on a handheld display (a phone, a tablet, a page in a touch browser): a point of the UI covers at least that many pixels whatever the screen's width or orientation, so text and buttons keep their physical size. It is nil on a desktop window, whose UI is sized by its width alone. <code>textScale</code> is the user's text size setting, which multiplies every font size the UI typesets (labels, buttons, inputs, editor chrome; canvas text is drawn at the size it names). <code>touchTarget</code> is the smallest target, in points, a touch reaches an interactive widget within: 48 on a handheld display, 0 on a desktop. <code>pixelRatio</code> is <code>ui.pixelRatio()</code> of the same frame. <code>ui.simulateDisplayScale</code> previews a phone's on any engine.</p> <pre><code class="language-lua">local scale = ui.displayScale() local buttonHeight = math.max(32, scale.touchTarget) </code></pre> <h2 id="modules-ui-dragstate">modules/ui/dragState</h2> <pre><code class="language-lua">dragState(): { payload: string, x: number, y: number, label: string?, ghost: boolean }? </code></pre> <p>The in-flight drag-and-drop payload while a <code>dragPayload</code> widget is being dragged, else nil. <code>x</code>/<code>y</code> are the pointer's position in the logical space <code>ui.getLayoutInfo</code> rects live in, so the reading resolves directly against widget rects. <code>label</code> is the text the drag ghost beside the pointer reads (<code>dragLabel</code>, else the source's <code>text</code>; nil for an outline ghost), and <code>ghost</code> whether the engine draws one (<code>dragGhost</code>) — a capture never shows the ghost, so this is how a drag's name-at-cursor is checked. Poll during a drag to drive live feedback; the drop itself still lands through the target's <code>onDrop</code>. Snapshotted each frame.</p> <h2 id="modules-ui-elementtree">modules/ui/elementTree</h2> <pre><code class="language-lua">elementTree(screenName: string): ElementNode? </code></pre> <p>Introspect a screen's rendered widget hierarchy with each element's layout rect. Every node the renderer draws appears, nested exactly as the widgets nest, under the id it records layout against: the <code>id</code> set on the node when the author gave it one, otherwise <code><screen>/<type>@<path></code>. <code>bounds</code> is that element's rect — the same table <code>ui.getLayoutInfo(id)</code> returns — and appears once the element has been measured. Kinds registered through <code>ui.defineWidget</code> appear expanded into the primitives they build. Feeds the <code>gui.captureElement</code> tool: list the tree, pick the ids to frame, capture their region.</p> <p><strong>Parameters</strong></p> <ul> <li><code>screenName</code> <code>string</code> — Screen id passed to <code>ui.registerScreen</code>.</li> </ul> <h2 id="modules-ui-focus">modules/ui/focus</h2> <pre><code class="language-lua">focus(widgetId: string) </code></pre> <p>Programmatically request keyboard focus on a widget. Queued as a one-shot; the next render of the matching widget calls <code>response.request_focus()</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — Widget id to focus.</li> </ul> <h2 id="modules-ui-focusedwidget">modules/ui/focusedWidget</h2> <pre><code class="language-lua">focusedWidget(): (string?, string?) </code></pre> <p>Return the widget id of whichever widget currently holds keyboard focus, or nil, and the space it draws in: <code>"window"</code> for the editor's UI around the game view, <code>"camera"</code> for the game's own UI in the game view. Snapshotted post-render each frame.</p> <pre><code class="language-lua">local id, space = ui.focusedWidget() </code></pre> <h2 id="modules-ui-getareapos">modules/ui/getAreaPos</h2> <pre><code class="language-lua">getAreaPos(id: string): AreaPos? </code></pre> <p>Read the current pivot position of an <code>area</code> widget, including any user drag deltas. Returns <code>{ x, y }</code> or nil if the area didn't render this frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Area widget id.</li> </ul> <h2 id="modules-ui-getareasize">modules/ui/getAreaSize</h2> <pre><code class="language-lua">getAreaSize(id: string): AreaSize? </code></pre> <p>Read the measured size of an <code>area</code> widget, including any user resize-grip drags if the area is <code>resizable</code>. Returns <code>{ w, h }</code> or nil if the area didn't render this frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Area widget id.</li> </ul> <h2 id="modules-ui-getdocklayout">modules/ui/getDockLayout</h2> <pre><code class="language-lua">getDockLayout(id: string): string? </code></pre> <p>Read the current serialized layout (split/tab arrangement) of a <code>dockArea</code> widget as a JSON string. Returns nil if the dockArea didn't render this frame. Persist the string and pass it back via the dockArea's <code>layout</code> prop to restore the arrangement.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — DockArea widget id.</li> </ul> <h2 id="modules-ui-getlayoutinfo">modules/ui/getLayoutInfo</h2> <pre><code class="language-lua">getLayoutInfo(widgetId: string?): LayoutInfo? </code></pre> <p>Get layout info (position, size, content bounds) for UI containers. If <code>widgetId</code> is given, returns info for that widget only; otherwise returns all.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string?</code> <em>(optional)</em> — Optional widget id to query.</li> </ul> <h2 id="modules-ui-getpersistentdocklayout">modules/ui/getPersistentDockLayout</h2> <pre><code class="language-lua">getPersistentDockLayout(id: string): string? </code></pre> <p>Read a <code>dockArea</code>'s arrangement as a JSON string with every tab whose <code>dockPanel</code> is <code>transient</code> left out — the form to save and restore across sessions. Returns nil if the dockArea didn't render this frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — DockArea widget id.</li> </ul> <h2 id="modules-ui-getscreentree">modules/ui/getScreenTree</h2> <pre><code class="language-lua">getScreenTree(screenName: string): WidgetTree? </code></pre> <p>Return the last widget tree table passed to <code>registerScreen</code> / <code>updateScreen</code> for <code>screenName</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>screenName</code> <code>string</code> — Screen name to query.</li> </ul> <h2 id="modules-ui-gettheme">modules/ui/getTheme</h2> <pre><code class="language-lua">getTheme(): string </code></pre> <p>Get the name of the currently active theme.</p> <h2 id="modules-ui-gettoken">modules/ui/getToken</h2> <pre><code class="language-lua">getToken(name: string): string? </code></pre> <p>Look up a single design token value from the active theme.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Token name (without <code>$</code> prefix).</li> </ul> <h2 id="modules-ui-gettokens">modules/ui/getTokens</h2> <pre><code class="language-lua">getTokens(): { [string]: string } </code></pre> <p>Get all design tokens from the active theme as a key-value map.</p> <h2 id="modules-ui-getwidgetprops">modules/ui/getWidgetProps</h2> <pre><code class="language-lua">getWidgetProps(typeName: string): { WidgetPropDescriptor }? </code></pre> <p>Get the property definitions for a widget type.</p> <p><strong>Parameters</strong></p> <ul> <li><code>typeName</code> <code>string</code> — Widget type name.</li> </ul> <h2 id="modules-ui-getwidgettypes">modules/ui/getWidgetTypes</h2> <pre><code class="language-lua">getWidgetTypes(): { string } </code></pre> <p>Get all available widget type names that can be used in widget trees.</p> <h2 id="modules-ui-hidescreen">modules/ui/hideScreen</h2> <pre><code class="language-lua">hideScreen(name: string): boolean </code></pre> <p>Hide a registered screen, and report whether a screen by that name is registered. The engine applies the hide later in the frame; <code>listScreens</code> reflects it from the next call onwards.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier to hide.</li> </ul> <h2 id="modules-ui-hittest">modules/ui/hitTest</h2> <pre><code class="language-lua">hitTest(x: number, y: number): PaintHitTest? </code></pre> <p>Which widget a pointer at <code>(x, y)</code> reaches, and the stack beneath it. Coordinates are logical points on the window, the space <code>getLayoutInfo</code> and <code>gui.bounds</code> rects are in wherever a widget's screen draws (a screen in the Scene view's picture included). A widget that takes no pointer, a canvas declaring <code>pointerEvents = "none"</code>, is passed over: the answer is the widget beneath it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>x</code> <code>number</code> — Logical X.</li> <li><code>y</code> <code>number</code> — Logical Y.</li> </ul> <pre><code class="language-lua">local hit = ui.hitTest(640, 360) </code></pre> <h2 id="modules-ui-idcollisions">modules/ui/idCollisions</h2> <pre><code class="language-lua">idCollisions(): { [string]: { string } } </code></pre> <p>Element ids more than one screen recorded, each mapped to the screens that recorded it. An id addresses one element: two screens sharing one leave <code>ui.getLayoutInfo</code> and the <code>gui</code> element tools answering for whichever painted last, and the <code>gui</code> tools refuse such an id. Empty while every id is unique.</p> <pre><code class="language-lua">for id, screens in ui.idCollisions() do print(id, table.concat(screens, ", ")) end </code></pre> <h2 id="modules-ui-invisibilityreasons">modules/ui/invisibilityReasons</h2> <pre><code class="language-lua">invisibilityReasons(): { string } </code></pre> <p>Every verdict <code>ui.diagnose</code> can report, as a closed list.</p> <h2 id="modules-ui-lastregistration">modules/ui/lastRegistration</h2> <pre><code class="language-lua">lastRegistration(): { name: string, layer: number? }? </code></pre> <p>The name and layer passed to the most recent <code>ui.registerScreen</code> call, recorded synchronously at call time. A host that mounts a nested app reads this immediately after the mount to learn which screen the nested code registered, without intercepting the <code>ui</code> table.</p> <h2 id="modules-ui-lastvalidation">modules/ui/lastValidation</h2> <pre><code class="language-lua">lastValidation(screenName: string?): any </code></pre> <p>Validation diagnostics produced at the most recent <code>registerScreen</code> / <code>updateScreen</code>, plus what the render stage found while painting — <code>unknown-font-family</code> reports a <code>style.fontFamily</code> that named no registered font family, once per family per screen, <code>wrap-label-no-width</code> reports a label with <code>props.wrap</code> whose box came out narrower than its own longest word, once per label per screen, and <code>svg-not-as-declared</code> reports an <code>svg</code> widget whose document declared something the picture does not hold — a <code><text></code> whose <code>font-family</code> selected no registered family, a <code><text></code> the picture holds no glyph of, or a shape whose <code>url(#id)</code> paint resolved to nothing — once per finding per widget per screen. With no args returns a <code>{ [screen] = entry }</code> map; with a name returns that screen's entry or nil. Validation gated by world setting <code>ui.validation</code> = <code>"off" | "warn" | "strict"</code> (default <code>"warn"</code>).</p> <p><strong>Parameters</strong></p> <ul> <li><code>screenName</code> <code>string?</code> <em>(optional)</em> — Optional screen name.</li> </ul> <h2 id="modules-ui-listfonts">modules/ui/listFonts</h2> <pre><code class="language-lua">listFonts(): { FontFamilyInfo } </code></pre> <p>Every font family a <code>style.fontFamily</code> can select. Read from the registry the UI text renderer resolves a family token through, so a family this returns is one a label renders in. <code>family</code> and every name in <code>aliases</code> are accepted as a <code>fontFamily</code>, case-insensitively; <code>aliases</code> carries the web-font names, CSS generic families and face names that select the same group. <code>faces</code> names the concrete face in each weight/style slot, so a <code>fontWeight = 700</code> against a family with no <code>bold</code> face gets a synthesised heavy. <code>more</code> lists the other faces registered in the family's slots, each drawing the codepoints the slot's face lacks: the installed chunks of an on-demand face (<code>font.onDemand()</code> lists them all). <code>system = true</code> marks a family taken from the host OS — present on this machine, absent on one without it, and absent on WASM — so a UI that must look the same everywhere picks a family with <code>system = false</code>. A <code>fontFamily</code> naming nothing in this list is reported as an <code>unknown-font-family</code> warning through <code>ui.lastValidation(screen)</code> once the screen paints, and the text renders in the default proportional face.</p> <pre><code class="language-lua">for _, f in ui.listFonts() do print(f.family) end </code></pre> <h2 id="modules-ui-listscreens">modules/ui/listScreens</h2> <pre><code class="language-lua">listScreens(): { ScreenSummary } </code></pre> <p>List every registered screen with its current visibility, z-order <code>layer</code>, <code>renderLayer</code> (the render-layer membership mask it draws under: the mask <code>setScreenRenderLayer</code> stated, else the one the surface <code>setScreenSpace</code> named implies, else the <code>ui</code> bit), <code>editorChrome</code> (whether the mask <code>setScreenRenderLayer</code> stated carries the <code>EditorUI</code> bit, which makes the screen the editor's own chrome; a screen placed on the window without stating it is not chrome), whether the screen has a populated root widget tree, and the space it draws in (<code>"window"</code> for the editor's UI, <code>"camera"</code> for the game's), including the register / show / hide / unregister calls the running script has already made. Sorted by layer ascending, then name.</p> <h2 id="modules-ui-listthemes">modules/ui/listThemes</h2> <pre><code class="language-lua">listThemes(): { string } </code></pre> <p>List all registered theme names.</p> <h2 id="modules-ui-lockorientation">modules/ui/lockOrientation</h2> <pre><code class="language-lua">lockOrientation(lock: OrientationLock) </code></pre> <p>Hold the display to the orientations <code>lock</code> allows: <code>"landscape"</code> (both landscape directions, so turning the device over turns the app with it), <code>"portrait"</code> (both portrait directions) or <code>"any"</code> (the display follows the device). A world states the orientation it plays in as <code>world.orientation</code>, which the engine locks to in play mode and releases in edit mode, so the editor follows the device; call this to change it while the world runs. Android turns the app at once, a browser grants the lock only to a page in fullscreen in most cases (the page asks again each time it enters fullscreen), and a desktop window presents in the shape it is given. <code>ui.orientation()</code> reports the lock and whether the platform holds it from the next frame. Any other value is refused by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>lock</code> <code>OrientationLock</code> — <code>"any"</code>, <code>"landscape"</code> or <code>"portrait"</code>.</li> </ul> <pre><code class="language-lua">ui.lockOrientation("portrait") ui.lockOrientation("any") </code></pre> <h2 id="modules-ui-missingglyphs">modules/ui/missingGlyphs</h2> <pre><code class="language-lua">missingGlyphs(): { string } </code></pre> <p>The characters the UI is showing without a glyph: each appears in text a UI surface (the window, the game view, a camera's UI overlay, a screen drawn into a texture) drew in what it last painted, and no face of the family it was drawn in has a glyph for it, so it shows as the replacement box. Every family falls back glyph by glyph to every face the UI holds, so a character here is one no installed face covers. A character a declared on-demand face covers (<code>font.onDemand()</code>) requests that face the frame it is met, and leaves this list once the face is installed and the next frame draws it.</p> <pre><code class="language-lua">if #ui.missingGlyphs() > 0 then print(table.concat(ui.missingGlyphs(), " ")) end </code></pre> <h2 id="modules-ui-observe">modules/ui/observe</h2> <pre><code class="language-lua">observe(screenName: string?): PaintObservation? </code></pre> <p>What the last UI frame painted. Returns <code>{ generation, viewport, pointer, pointerOverUi, pointerWidget, widgets }</code> with one <code>widgets</code> row per widget any registered screen holds — its layout box, the clip chain it painted under, the part of that box which reached the frame (<code>visible</code>), the order it painted in (<code>paintIndex</code>), and its <code>reason</code> from the closed set <code>ui.invisibilityReasons()</code> lists. Every rect, the reading's <code>viewport</code> (the window) and <code>pointer</code> are in logical points on the window; a row's <code>viewport</code> is the region the pass that drew it paints into (the whole window, or the rect a Scene panel presents the camera's picture in), and <code>offViewport</code> is judged against it. <code>generation</code> advances once per re-rendered frame, so two calls reporting the same number describe the same frame.</p> <p><strong>Parameters</strong></p> <ul> <li><code>screenName</code> <code>string?</code> <em>(optional)</em> — Narrow the rows to one screen. Omit for every screen.</li> </ul> <pre><code class="language-lua">local reading = ui.observe("hud") </code></pre> <h2 id="modules-ui-onorientationchange">modules/ui/onOrientationChange</h2> <pre><code class="language-lua">onOrientationChange(callback: (reading: OrientationReading) -> ()): () -> () </code></pre> <p>Call <code>callback</code> with the new reading every time <code>ui.orientation()</code> changes: the display turns between landscape and portrait or to another rotation, the lock changes, or the platform starts or stops holding it. Each listener is watched by a task of the context that registered it, so a listener a component registered ends when that component is removed, and every other listener goes on hearing changes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>callback</code> <code>(reading: OrientationReading) -> ()</code> — Receives the reading, as <code>ui.orientation()</code> returns it.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that removes this listener.</p> <pre><code class="language-lua">local off = ui.onOrientationChange(function(o) print(o.current, o.rotation) end) </code></pre> <h2 id="modules-ui-orientation">modules/ui/orientation</h2> <pre><code class="language-lua">orientation(): OrientationReading </code></pre> <p>The orientation the display presents the app in, and the lock it is held to. <code>current</code> is <code>"landscape"</code> (wider than tall) or <code>"portrait"</code>: the shape of the window the app presents in. <code>lock</code> is what <code>ui.lockOrientation</code> last asked, <code>"any"</code> when the display follows the device. <code>enforced</code> is whether the platform holds the display to it: Android always does, a browser does once it grants the Screen Orientation lock, and a desktop window never does, presenting in the shape it is given; <code>reason</code> names why when it does not. <code>rotation</code> is the display's rotation from its natural orientation in degrees (0, 90, 180, 270) where the platform states one, and nil on a desktop window. <code>revision</code> advances every time any of these changes; <code>ui.onOrientationChange</code> calls back on each change.</p> <pre><code class="language-lua">if ui.orientation().current == "portrait" then print("lay the HUD out for a tall screen") end </code></pre> <h2 id="modules-ui-paintorder">modules/ui/paintOrder</h2> <pre><code class="language-lua">paintOrder(a: string, b: string): number? </code></pre> <p>Which of two widgets paints later: <code>-1</code> when <code>a</code> paints before <code>b</code>, <code>1</code> when after, <code>0</code> when level. This is what separates two widgets whose rects are identical.</p> <p><strong>Parameters</strong></p> <ul> <li><code>a</code> <code>string</code> — First widget id.</li> <li><code>b</code> <code>string</code> — Second widget id.</li> </ul> <pre><code class="language-lua">local order = ui.paintOrder("panel-a", "panel-b") </code></pre> <h2 id="modules-ui-pixelratio">modules/ui/pixelRatio</h2> <pre><code class="language-lua">pixelRatio(): number </code></pre> <p>Physical pixels per logical point — the factor between the logical space <code>ui.screenSize()</code> / <code>getLayoutInfo</code> rects live in and the physical space <code>input.mousePosition</code>, the camera viewport rect and <code>input.simulateMouse*</code> coordinates live in. Multiply a layout coordinate by this to aim a simulated pointer at a widget.</p> <h2 id="modules-ui-pointerwidget">modules/ui/pointerWidget</h2> <pre><code class="language-lua">pointerWidget(): PointerRead? </code></pre> <p>Whether the UI is consuming the pointer, and which widget holds it — the pointer counterpart of <code>ui.focusedWidget()</code>. <code>x</code>, <code>y</code> are in logical points on the window, the space <code>getLayoutInfo</code> rects are in.</p> <h2 id="modules-ui-refreshlivetext">modules/ui/refreshLiveText</h2> <pre><code class="language-lua">refreshLiveText(): number </code></pre> <p>Read again the text of every screen this VM registered whose text resolves when it is read: a <code>text</code>, <code>title</code>, <code>header</code>, <code>placeholder</code>, <code>tooltip</code> or dropdown option given as a value with a <code>__tostring</code> metamethod, such as an <code>i18n.text</code> value, rather than as a string. The <code>i18n</code> module calls it whenever the player's language, the pseudo-locale or a catalog changes, so a screen built from <code>i18n.text</code> values follows them without being built again.</p> <pre><code class="language-lua">local tree = { type = "label", props = { text = i18n.text("hud.title") } } </code></pre> <h2 id="modules-ui-registerbackgroundshader">modules/ui/registerBackgroundShader</h2> <pre><code class="language-lua">registerBackgroundShader(shaderHandle: any, width: number?, height: number?) </code></pre> <p>Register a screen-domain <code>.shader</code> as a UI background, drawn via the <code>backgroundShader</code> style. Takes the shader's asset handle from <code>asset.resolve</code>. A shader whose compile failed (<code>shaderHandle:compileStatus()</code> reads <code>failed</code>), or whose program the compile gate refuses, is not registered: the call raises an error naming the shader and the compiler's message, the one <code>compileStatus().error</code> reads. A shader whose <code>compileStatus()</code> still reads <code>pending</code> raises too: register it once it reads <code>compiled</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>shaderHandle</code> <code>any</code> <em>(optional)</em> — The screen <code>.shader</code>'s asset handle, from <code>asset.resolve</code>.</li> <li><code>width</code> <code>number?</code> <em>(optional)</em> — Render target width (default 1280).</li> <li><code>height</code> <code>number?</code> <em>(optional)</em> — Render target height (default 720).</li> </ul> <h2 id="modules-ui-registercallbackenv">modules/ui/registerCallbackEnv</h2> <pre><code class="language-lua">registerCallbackEnv(key: string, env: { [string]: any }) </code></pre> <p>Register an environment table to receive widget-callback broadcasts: its global <code>onCallback(id, value)</code> fires for any widget callback not owned by a specific component instance — the same broadcast a component's <code>onCallback</code> receives. Keyed by <code>key</code>; re-registering the same key replaces the previous env. A component instance is folded into the callback dispatch automatically, so reach for this from a non-component context that hosts a UI surface (a scene entrypoint registering its own screen). Pair with <code>ui.unregisterCallbackEnv(key)</code> so the ref is released.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — Stable identifier for this registration (re-register replaces).</li> <li><code>env</code> <code>{ [string]: any }</code> — Environment table whose <code>onCallback</code> receives the broadcasts.</li> </ul> <h2 id="modules-ui-registerscreen">modules/ui/registerScreen</h2> <pre><code class="language-lua">registerScreen(name: string, widgetTree: WidgetTree, layer: number?) </code></pre> <p>Register a named UI screen with a widget tree. Optional <code>layer</code> controls z-ordering (higher = on top), in bands: below 0 behind everything, 0-99 ordinary app depth, 100-999 always-on-top chrome, 1000+ menu and popup depth. A screen in a higher band covers one in a lower band whatever their roots are; inside a band a floating <code>area</code> or <code>window</code> root sits over ordinary content, and a <code>modal</code> root sits over the whole stack. Tag-based grouping lives in <code>Z.tags</code> (<code>Z.tags.set(name, { "editor" })</code> after register). A screen belongs to the component instance whose own code registers it (its hooks and methods, and any closure they create, such as a listener it subscribes): that instance's <code>onCallback</code> receives its callbacks, and the screen is unregistered when the instance ends. Code that is not an instance's own (a scene entrypoint, a module, a listener either subscribed) registers a screen no instance owns, which stays until it is unregistered, whichever component's hook ran that code. <code>topics/ui</code> has the rule.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Unique screen identifier.</li> <li><code>widgetTree</code> <code>WidgetTree</code> — Root widget table.</li> <li><code>layer</code> <code>number?</code> <em>(optional)</em> — Z-order layer (optional).</li> </ul> <pre><code class="language-lua">ui.registerScreen("hud", tree) </code></pre> <h2 id="modules-ui-registertheme">modules/ui/registerTheme</h2> <pre><code class="language-lua">registerTheme(name: string, theme: ThemeDefinition) </code></pre> <p>Register a theme from a flat Luau table. Most callers should use <code>Z.theme.register(name, table)</code> which runs the cascade for them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Theme name to register.</li> <li><code>theme</code> <code>ThemeDefinition</code> — Flat-resolved theme table.</li> </ul> <h2 id="modules-ui-removescreen">modules/ui/removeScreen</h2> <pre><code class="language-lua">removeScreen(name: string): boolean </code></pre> <p>Alias for <code>ui.unregisterScreen</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier to remove.</li> </ul> <h2 id="modules-ui-resetareasize">modules/ui/resetAreaSize</h2> <pre><code class="language-lua">resetAreaSize(id: string) </code></pre> <p>Clear a <code>resizable</code> <code>area</code>'s remembered size (from a grip drag or <code>ui.setAreaSize</code>) so its declared — or content — size takes over again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Area widget id.</li> </ul> <h2 id="modules-ui-response">modules/ui/response</h2> <pre><code class="language-lua">response(widgetId: string): WidgetResponse? </code></pre> <p>Per-widget interaction snapshot for the most recent frame. Returns <code>{ clicked, hovered, focused, changed, value }</code> where <code>clicked</code> / <code>changed</code> mark transitions and <code>hovered</code> / <code>focused</code> mark current state.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — The widget id (NOT the onClick / onChange callback id).</li> </ul> <h2 id="modules-ui-safearea">modules/ui/safeArea</h2> <pre><code class="language-lua">safeArea(): SafeArea </code></pre> <p>The part of the screen no system bar, display cutout or on-screen keyboard covers, in the logical points <code>ui.screenSize()</code> reports. <code>top</code> / <code>right</code> / <code>bottom</code> / <code>left</code> are how far in from each edge the covered band reaches; <code>x</code> / <code>y</code> / <code>width</code> / <code>height</code> are the rect they leave; <code>cutouts</code> lists each display cutout (a camera hole or a notch) as <code>{ x, y, width, height }</code>. Screens lay out inside this rect, so anchors, top-level <code>%</code> sizes and a window's default position already keep clear of it; read it to place something by absolute <code>pos</code>, or to run a background under the bars while its controls stay inside. <code>revision</code> advances every time the area changes (a rotation, the system bars shown or hidden, the keyboard opening, a window resize): compare it with the one you last laid out against. All zero on a desktop window; <code>ui.simulateSafeArea</code> lays the UI out as a phone would on any engine.</p> <pre><code class="language-lua">local safe = ui.safeArea() ui.setAreaPos("hud", safe.x + 8, safe.y + 8) </code></pre> <h2 id="modules-ui-screen">modules/ui/screen</h2> <pre><code class="language-lua">screen(name: string): { [string]: any }? </code></pre> <p>Get a screen proxy with methods like <code>setResolution</code> and <code>rasterize</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen name.</li> </ul> <h2 id="modules-ui-screensize">modules/ui/screenSize</h2> <pre><code class="language-lua">screenSize(): { width: number, height: number } </code></pre> <p>The UI coordinate space as <code>{ width, height }</code> (logical points). This is the space <code>area</code> <code>pos</code>, anchors, and <code>getLayoutInfo</code> rects use — and it is NOT the pixel size of a <code>capture</code> screenshot, which may be downscaled. Use this for absolute <code>area</code> positioning (e.g. pinning a menu above a bottom taskbar) instead of guessing the size from a capture image.</p> <p>It follows the size the engine draws at, and <code>renderer.setViewportSize</code> changes that size for the task that asks, which is how a layout written against this is checked at a second shape without rebooting the engine, read in the call that set the size before the surface goes back. The pixel size behind these points is <code>renderer.surfaceSize()</code> — the whole drawing surface a screen is laid out over, which an editor layout makes larger than the rect <code>renderer.viewportSize()</code> draws the scene into — and <code>ui.pixelRatio()</code> is the factor between the two spaces.</p> <h2 id="modules-ui-scroll">modules/ui/scroll</h2> <pre><code class="language-lua">scroll(deltaX: number, deltaY: number) </code></pre> <p>Simulate a mouse-wheel scroll event on the UI.</p> <p><strong>Parameters</strong></p> <ul> <li><code>deltaX</code> <code>number</code> — Horizontal scroll delta.</li> <li><code>deltaY</code> <code>number</code> — Vertical scroll delta.</li> </ul> <h2 id="modules-ui-setareapos">modules/ui/setAreaPos</h2> <pre><code class="language-lua">setAreaPos(id: string, x: number, y: number) </code></pre> <p>Programmatically move a movable <code>area</code> widget to <code>(x, y)</code>. Applied for one frame; subsequent frames let drag tracking take over.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Area widget id.</li> <li><code>x</code> <code>number</code> — Target pivot x (screen coords).</li> <li><code>y</code> <code>number</code> — Target pivot y (screen coords).</li> </ul> <h2 id="modules-ui-setareasize">modules/ui/setAreaSize</h2> <pre><code class="language-lua">setAreaSize(id: string, w: number, h: number) </code></pre> <p>Programmatically set a <code>resizable</code> <code>area</code>'s size (the user-size override) — for maximize / restore / tile. Persists until the area's declared width/height changes or <code>ui.resetAreaSize(id)</code> clears it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>id</code> <code>string</code> — Area widget id.</li> <li><code>w</code> <code>number</code> — Target width (screen coords).</li> <li><code>h</code> <code>number</code> — Target height (screen coords).</li> </ul> <h2 id="modules-ui-setdockwindowrect">modules/ui/setDockWindowRect</h2> <pre><code class="language-lua">setDockWindowRect( </code></pre> <p>Place the floating window of a <code>dockArea</code> panel at <code>(x, y)</code> with size <code>(width, height)</code>. Applies once the panel occupies a window — a request made before then waits for it.</p> <h2 id="modules-ui-setscreenrenderlayer">modules/ui/setScreenRenderLayer</h2> <pre><code class="language-lua">setScreenRenderLayer(name: string, mask: number) </code></pre> <p>Set a screen's render-layer membership bitmask. A screen draws into a camera or capture only when this mask intersects the camera's include mask, the same rule geometry follows. Content UI carries the <code>ui</code> bit; the editor places its chrome on <code>EditorUI</code> so agent captures can drop it, and a screen carrying that bit is part of the window rather than the camera's picture. Stating a mask here fixes the membership: a screen left to itself takes the one the surface it is placed on implies, <code>EditorUI</code> for the window and <code>ui</code> for the camera's picture. <code>__renderLayers.bit(name)</code> answers with a layer's bit INDEX, so a mask sets the bit at that index and <code>__renderLayers.names(mask)</code> reads it back as the names it holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier.</li> <li><code>mask</code> <code>number</code> — Render-layer membership bitmask — the OR of <code>bit32.lshift(1, __renderLayers.bit(name))</code> over the layers the screen is on.</li> </ul> <pre><code class="language-lua">ui.setScreenRenderLayer("hud", bit32.lshift(1, __renderLayers.bit("ui"))) </code></pre> <h2 id="modules-ui-setscreenspace">modules/ui/setScreenSpace</h2> <pre><code class="language-lua">setScreenSpace(name: string, space: string) </code></pre> <p>Set which surface a screen is part of. <code>"camera"</code> is the picture the viewport camera produces: the screen lays out over the camera's target and is drawn into it, so it sits inside the Scene panel presenting the scene and appears in every capture of the camera — the default for content UI. <code>"window"</code> is the window itself, whatever presents the scene — where the editor's chrome lives, and the default for a screen on the <code>EditorUI</code> layer. The two are one statement made from either end, so a screen placed on the window takes the <code>EditorUI</code> membership unless <code>setScreenRenderLayer</code> states another: the window goes on drawing it while a capture that drops the authoring layers, which is what an agent takes by default, leaves it out. Where the scene presents on the window, the two surfaces coincide. A name that is neither raises.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier.</li> <li><code>space</code> <code>string</code> — <code>"camera"</code> | <code>"window"</code>.</li> </ul> <pre><code class="language-lua">ui.setScreenSpace("agent-banner", "window") </code></pre> <h2 id="modules-ui-setscrollposition">modules/ui/setScrollPosition</h2> <pre><code class="language-lua">setScrollPosition(widgetId: string, offsetY: number) </code></pre> <p>Set the scroll offset of a scrollArea widget.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — Scroll area widget id.</li> <li><code>offsetY</code> <code>number</code> — Vertical scroll offset in pixels.</li> </ul> <h2 id="modules-ui-setshaderuniforms">modules/ui/setShaderUniforms</h2> <pre><code class="language-lua">setShaderUniforms(name: string, uniforms: { [string]: number }) </code></pre> <p>Set uniform values on a registered background shader.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Shader name identifier.</li> <li><code>uniforms</code> <code>{ [string]: number }</code> — Map of uniform name to number value.</li> </ul> <h2 id="modules-ui-settheme">modules/ui/setTheme</h2> <pre><code class="language-lua">setTheme(name: string) </code></pre> <p>Switch the active global theme by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Theme name to activate.</li> </ul> <h2 id="modules-ui-showscreen">modules/ui/showScreen</h2> <pre><code class="language-lua">showScreen(name: string): boolean </code></pre> <p>Make a registered screen visible, and report whether a screen by that name is registered. The engine applies the show later in the frame; <code>listScreens</code> reflects it from the next call onwards.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier to show.</li> </ul> <h2 id="modules-ui-simulatedisplayscale">modules/ui/simulateDisplayScale</h2> <pre><code class="language-lua">simulateDisplayScale(scale: DisplayScaleSpec?) </code></pre> <p>Size the UI as a display stating <code>scale</code> would, in place of what the platform states. A <code>density</code> sizes it as a handheld display of that many physical pixels per point (omit it for a desktop display sized by its width), and <code>textScale</code> multiplies every font size the UI typesets (1 when omitted). Previews a phone's UI on a desktop or headless engine, and makes a layout test independent of the device it runs on. <code>ui.displayScale()</code>, <code>ui.screenSize()</code> and <code>ui.pixelRatio()</code> report it from the next frame. Call it with no argument (or <code>nil</code>) to return to the platform's own scale. A value that is not a number above 0 and at most 8 is refused by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>scale</code> <code>DisplayScaleSpec?</code> <em>(optional)</em> — <code>{ density?, textScale? }</code>; nil returns to the platform's scale.</li> </ul> <pre><code class="language-lua">ui.simulateDisplayScale({ density = 2.625, textScale = 1.3 }) ui.simulateDisplayScale() </code></pre> <h2 id="modules-ui-simulatesafearea">modules/ui/simulateSafeArea</h2> <pre><code class="language-lua">simulateSafeArea(area: SafeAreaSpec?) </code></pre> <p>Lay the UI out as if the system covered the screen by <code>area</code> (logical points) in place of what the platform states: each inset defaults to 0 and <code>cutouts</code> is a list of <code>{ x, y, width, height }</code>. Previews a phone's status bar, camera cutout and navigation bar on a desktop or headless engine, and makes a layout test independent of the device it runs on. <code>ui.safeArea()</code> reports it from the next frame. Call it with no argument (or <code>nil</code>) to return to the platform's own safe area. A negative or non-number inset is refused by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>area</code> <code>SafeAreaSpec?</code> <em>(optional)</em> — <code>{ top?, right?, bottom?, left?, cutouts? }</code> in logical points; nil returns to the platform's safe area.</li> </ul> <pre><code class="language-lua">ui.simulateSafeArea({ top = 40, bottom = 30, cutouts = { { x = 600, y = 0, width = 80, height = 40 } } }) ui.simulateSafeArea() </code></pre> <h2 id="modules-ui-unregistercallbackenv">modules/ui/unregisterCallbackEnv</h2> <pre><code class="language-lua">unregisterCallbackEnv(key: string) </code></pre> <p>Remove an environment registered with <code>ui.registerCallbackEnv</code>. Its <code>onCallback</code> stops receiving broadcasts. No-op if <code>key</code> isn't registered.</p> <p><strong>Parameters</strong></p> <ul> <li><code>key</code> <code>string</code> — The key passed to <code>ui.registerCallbackEnv</code>.</li> </ul> <h2 id="modules-ui-unregisterscreen">modules/ui/unregisterScreen</h2> <pre><code class="language-lua">unregisterScreen(name: string): boolean </code></pre> <p>Remove a screen from the registry entirely. Unlike <code>hideScreen</code>, this deletes the entry so it no longer appears in <code>listScreens</code> or render iteration.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier to unregister.</li> </ul> <h2 id="modules-ui-unregisterwidget">modules/ui/unregisterWidget</h2> <pre><code class="language-lua">unregisterWidget(name: string) </code></pre> <p>Drop a registered custom widget kind. Subsequent references produce an <code>unknown-widget-type</code> diagnostic.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Custom widget kind name.</li> </ul> <h2 id="modules-ui-updatescreen">modules/ui/updateScreen</h2> <pre><code class="language-lua">updateScreen(name: string, widgetTree: WidgetTree) </code></pre> <p>Replace the widget tree of an already-registered screen.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Screen identifier to update.</li> <li><code>widgetTree</code> <code>WidgetTree</code> — New root widget table.</li> </ul> <h2 id="modules-ui-usestyles">modules/ui/useStyles</h2> <pre><code class="language-lua">useStyles(themeName: string) </code></pre> <p>Apply a registered style file's classes additively without changing the active theme.</p> <p><strong>Parameters</strong></p> <ul> <li><code>themeName</code> <code>string</code> — Name of the registered style / theme asset.</li> </ul> <h2 id="modules-ui-widgetstate">modules/ui/widgetState</h2> <pre><code class="language-lua">widgetState(widgetId: string, key: string, default: any?): any </code></pre> <p>Read per-widget cross-frame state. Returns the value previously written via <code>widgetStateSet</code>, or <code>default</code> (or nil). State is keyed by widget id and persists across re-renders within a screen's lifetime; cleared automatically when the owning screen is unregistered.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — Widget id whose state to read.</li> <li><code>key</code> <code>string</code> — State key.</li> <li><code>default</code> <code>any?</code> <em>(optional)</em> — Value to return when nothing has been written.</li> </ul> <h2 id="modules-ui-widgetstateclear">modules/ui/widgetStateClear</h2> <pre><code class="language-lua">widgetStateClear(widgetId: string, key: string) </code></pre> <p>Remove a per-widget state entry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — Widget id whose state to clear.</li> <li><code>key</code> <code>string</code> — State key.</li> </ul> <h2 id="modules-ui-widgetstateset">modules/ui/widgetStateSet</h2> <pre><code class="language-lua">widgetStateSet(widgetId: string, key: string, value: any) </code></pre> <p>Write per-widget cross-frame state. Replaces any existing value under <code>(widgetId, key)</code>. Tables are stored by reference.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — Widget id to scope the state under.</li> <li><code>key</code> <code>string</code> — State key.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — Value to store (must be non-nil).</li> </ul> <h2 id="modules-unwatch">modules/unwatch</h2> <pre><code class="language-lua">modules.unwatch(watcherId) -> boolean </code></pre> <p>Remove a previously registered module source watcher by its ID.</p> <p><strong>Parameters</strong></p> <ul> <li><code>watcherId</code> <code>number</code> — Watcher ID returned by modules.watch()</li> </ul> <p><strong>Returns</strong> <code>boolean</code> — true if watcher was found and removed</p> <h2 id="modules-userfile-readme">modules/userfile/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.userfile") -- userfile (also available as global 'userfile') </code></pre> <p>The files of the person at the screen, both ways. <code>pick</code> opens their own file picker (native OS dialog / Android / web browser) and brings the chosen file(s) into the engine; <code>save</code> puts a file the engine holds onto their device (a download in the browser). Public Luau surface over the <code>__userfile</code> Internal FFI namespace.</p> <p>Usage: local userfile = require("@builtin::modules.api.engine.userfile") Also available as global: userfile</p> <h2 id="modules-userfile-pick">modules/userfile/pick</h2> <pre><code class="language-lua">pick(opts: PickOpts?): PickResult </code></pre> <p>Open the user's system file picker and bring the chosen file(s) into the engine. Yields until the user finishes (call from a coroutine / task, like any <code>task.await</code>) and returns <code>{ cancelled, files = {{ name, mime, size, bytes?, vfsPath? }} }</code>. Without <code>writeTo</code> each file carries <code>bytes</code> (a binary-safe string); with <code>writeTo</code> each carries <code>vfsPath</code> (read it with <code>vfs.read</code>). Cancelling returns <code>{ cancelled = true, files = {} }</code>; a genuine failure (e.g. a lost browser user-activation gesture) raises an error.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>PickOpts?</code> <em>(optional)</em> — Picker options (optional): multiple, folder, title, filters, writeTo.</li> </ul> <pre><code class="language-lua">local r = userfile.pick({ filters = {{ name = "Images", extensions = {"png","jpg"} }} }) if not r.cancelled then vfs.write("/source/textures/wall.png", assert(r.files[1].bytes)) end </code></pre> <h2 id="modules-userfile-pickfolder">modules/userfile/pickFolder</h2> <pre><code class="language-lua">pickFolder(opts: PickOpts?): PickResult </code></pre> <p>Convenience for <code>userfile.pick({ folder = true })</code> — pick a whole directory tree. Yields until the user finishes and returns the same result table as <code>pick</code>. On the web this degrades to a multi-file selection.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>PickOpts?</code> <em>(optional)</em> — Picker options (optional); <code>folder</code> is forced true.</li> </ul> <pre><code class="language-lua">local r = userfile.pickFolder({ writeTo = "/source/imported/" }) </code></pre> <h2 id="modules-userfile-save">modules/userfile/save</h2> <pre><code class="language-lua">save(path: string, opts: SaveOpts?): (boolean, string, SaveReport?) </code></pre> <p>Save a file the engine holds onto the device of the person at the screen: the way a person keeps a recording, a capture or an export. The person decides where it lands. In the browser the file downloads at once when the save rides a click or key press of the person at the screen (call it from a button's click handler), and otherwise the page shows them an offer they save with a click of their own, since a browser starts a download only for a person who just acted. A native engine opens the host's save dialog in their downloads folder, with the name filled in, and writes the file where they choose. An engine with no window has no person at its screen and refuses. The call waits for the first answer, so run it where it can yield (<code>task.spawn</code> from a callback). Returns true, a sentence saying where the file went, and the report <code>{ state, detail, name, size, path }</code> whose <code>state</code> is <code>"saved"</code> (the file is on the device) or <code>"offered"</code> (it is in front of the person, as the page's offer or the save dialog, and lands when they act); or false and the reason the save was refused: no file at the path, a folder, a name with a folder in it, no window, or a file too large for the browser to hold (which the page also shows).</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path of the file to save.</li> <li><code>opts</code> <code>SaveOpts?</code> <em>(optional)</em> — <code>{ name }</code>, the file name the device gets (the file's own when omitted).</li> </ul> <pre><code class="language-lua">local ok, where = userfile.save("/zero/runtime/recordings/take.mp4") local ok, where, report = userfile.save("/zero/source/export/level.json", { name = "level-3.json" }) </code></pre> <h2 id="modules-values-readme">modules/values/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.values") -- values (also available as global 'values') </code></pre> <p>Named values an entity holds: typed numbers, vectors and booleans (<code>throttle</code>, <code>rpm</code>, <code>gear</code>, <code>headlights</code>, <code>doorOpen</code>) that scripts and engine systems write and read cheaply, and that materials, audio voices, the UI and replication follow without a script copying them every frame. A value stays on this engine unless it is declared to replicate, at a rate in Hz.</p> <p>Usage: local values = require("@builtin::modules.api.engine.values") Also available as global: values</p> <h2 id="modules-values-bind">modules/values/bind</h2> <pre><code class="language-lua">bind( </code></pre> <p>A UI prop that follows a named value, with no script updating the screen: put it where a widget takes <code>text</code>, <code>value</code>, <code>visible</code>, <code>checked</code> or <code>enabled</code>, and the widget reads the value every frame. <code>format</code> is a <code>string.format</code> pattern for <code>text</code> (<code>"%d rpm"</code>; the value as it reads when absent); <code>scale</code> and <code>offset</code> map a number (<code>value * scale + offset</code>) before it is shown. A value the entity does not hold shows <code>fallback</code> (empty text, 0 or false when absent).</p> <pre><code class="language-lua">ui.registerScreen("dash", { type = "vertical", children = { { type = "label", props = { text = values.bind(car, "rpm", { format = "%.0f rpm" }) } }, { type = "slider", props = { min = 0, max = 1, value = values.bind(car, "throttle") } }, } }) </code></pre> <h2 id="modules-values-clear">modules/values/clear</h2> <pre><code class="language-lua">clear(target: string | entityRef): number </code></pre> <p>Drop every named value an entity holds. An entity's values also go when the entity leaves the world.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> </ul> <pre><code class="language-lua">values.clear(car) </code></pre> <h2 id="modules-values-declare">modules/values/declare</h2> <pre><code class="language-lua">declare(target: string | entityRef, name: string, declaration: ValueDeclaration?) </code></pre> <p>State what a named value is: its <code>type</code> ("number", "vector" or "boolean"), the <code>default</code> it starts at, and whether it replicates. A name the entity already holds keeps its value. <code>replicate</code> is a rate in Hz (up to 60): the owner of the entity sends the value to every other peer at most that often while it changes, and always sends the latest one, and the other peers read it like any value. A value with no <code>replicate</code> is never sent anywhere.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> <li><code>name</code> <code>string</code> — The value's name.</li> <li><code>declaration</code> <code>ValueDeclaration?</code> <em>(optional)</em> — <code>{ type?, default?, replicate? }</code>; give <code>type</code> or <code>default</code> for a name the entity does not hold yet.</li> </ul> <pre><code class="language-lua">values.declare(car, "throttle", { type = "number", default = 0, replicate = 10 }) values.declare(car, "brake", { default = false, replicate = 20 }) </code></pre> <h2 id="modules-values-describe">modules/values/describe</h2> <pre><code class="language-lua">describe(target: string | entityRef): { ValueRow } </code></pre> <p>Every named value an entity holds, sorted by name, each with its <code>type</code>, its <code>value</code>, who wrote it last (<code>writer</code>: "script", "engine" or "remote"), the store <code>revision</code> of its last change, its <code>replicate</code> rate (absent for a value kept on this engine) and whether a declaration states it. The same rows <code>/zero/runtime/values/<entityId></code> and the <code>values</code> toolbox list.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> </ul> <pre><code class="language-lua">for _, row in values.describe(car) do print(row.name, row.type, row.value, row.writer) end </code></pre> <h2 id="modules-values-entities">modules/values/entities</h2> <pre><code class="language-lua">entities(): { string } </code></pre> <p>Every entity holding at least one named value, sorted by id.</p> <pre><code class="language-lua">for _, id in values.entities() do print(id, #values.describe(id)) end </code></pre> <h2 id="modules-values-get">modules/values/get</h2> <pre><code class="language-lua">get(target: string | entityRef, name: string): Value? </code></pre> <p>Read a named value an entity holds, or nil when it holds none under that name. Engine systems write values too: a <code>WheelCollider</code> writes its own state (<code>wheel.slipRatio</code>, <code>wheel.angularVelocity</code>, ...), so a script reads it here the way it reads a value a script wrote.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> <li><code>name</code> <code>string</code> — The value's name.</li> </ul> <pre><code class="language-lua">local rpm = values.get(car, "rpm") or 0 </code></pre> <h2 id="modules-values-list">modules/values/list</h2> <pre><code class="language-lua">list(target: string | entityRef): { [string]: Value } </code></pre> <p>Every named value an entity holds, by name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> </ul> <pre><code class="language-lua">for name, v in values.list(car) do print(name, v) end </code></pre> <h2 id="modules-values-onanychange">modules/values/onAnyChange</h2> <pre><code class="language-lua">onAnyChange(callback: (Value?, string, string) -> ()): ValueConnection </code></pre> <p>Call <code>callback(value, name, entityId)</code> for every change of every named value on every entity, once per frame in which it changed: what a debug view or a recorder of a session's values follows.</p> <p><strong>Parameters</strong></p> <ul> <li><code>callback</code> <code>(Value?, string, string) -> ()</code> — <code>function(value, name, entityId)</code>.</li> </ul> <pre><code class="language-lua">local conn = values.onAnyChange(function(v, name, id) print(id, name, v) end) </code></pre> <h2 id="modules-values-remove">modules/values/remove</h2> <pre><code class="language-lua">remove(target: string | entityRef, name: string): boolean </code></pre> <p>Drop a named value from an entity.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> <li><code>name</code> <code>string</code> — The value's name.</li> </ul> <pre><code class="language-lua">values.remove(car, "rpm") </code></pre> <h2 id="modules-values-set">modules/values/set</h2> <pre><code class="language-lua">set(target: string | entityRef, name: string, value: Value?) </code></pre> <p>Write a named value on an entity: a number, a vector or a boolean; <code>nil</code> removes it. A name takes the kind of its first value and keeps it: writing another kind raises (remove the value first to change its kind). The write is a store, with nothing replicated and nothing copied, and every reader sees it from the next read: <code>values.get</code>, a material property bound to the name, an <code>Audio</code> voice following it, a UI prop <code>values.bind</code> made. Only a value <code>values.declare</code> gives a <code>replicate</code> rate is ever sent to other peers.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string | entityRef</code> — The entity: a proxy from <code>entity(...)</code>, or its id.</li> <li><code>name</code> <code>string</code> — The value's name.</li> <li><code>value</code> <code>Value?</code> <em>(optional)</em> — A number, a vector or a boolean; <code>nil</code> removes the value.</li> </ul> <pre><code class="language-lua">values.set(car, "rpm", 3200) values.set(car, "headlights", true) values.set(car, "wheelSpin", vector.create(0, 12.5, 0)) </code></pre> <h2 id="modules-values-stats">modules/values/stats</h2> <pre><code class="language-lua">stats(): { entities: number, values: number, revision: number, replication: ValueReplicationStats } </code></pre> <p>How many entities hold values, how many values there are, the store's revision (which advances on every change), and what replication has carried: a value with no <code>replicate</code> rate adds nothing to it.</p> <pre><code class="language-lua">print(values.stats().values, values.stats().replication.sentBytes) </code></pre> <h2 id="modules-values-subscribe">modules/values/subscribe</h2> <pre><code class="language-lua">subscribe( </code></pre> <p>Call <code>callback(value, name, entityId)</code> when a named value of an entity changes, whoever changed it: a script, an engine system or another peer. Give <code>name</code> to follow one value, or <code>nil</code> to follow every value of the entity. The callback runs once for the writes a frame made, on the engine's next pass over its tasks, with the value as it stands then: three writes in one frame are one call with the last of them. A value that went (<code>values.remove</code>, its entity left the world) arrives as <code>nil</code>. A subscription made by a component ends with the component.</p> <pre><code class="language-lua">local conn = values.subscribe(car, "gear", function(gear) print("now in gear", gear) end) conn:Disconnect() </code></pre> <h2 id="modules-velocitydilation-readme">modules/velocityDilation/README</h2> <pre><code class="language-lua">require("@builtin::systems.velocityDilation.velocityDilation") -- velocityDilation </code></pre> <p>How far a moving surface's velocity reaches past its own silhouette in the frame's velocity buffer. Every temporal technique reads that buffer one texel per pixel, so widening it is what lets a reprojection and a blur follow a moving object over the pixels it is about to cover.</p> <p>Usage: local velocityDilation = require("@builtin::systems.velocityDilation.velocityDilation")</p> <h2 id="modules-velocitydilation-active">modules/velocityDilation/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the dilation passes are running this frame.</p> <pre><code class="language-lua">if velocityDilation.active() then ... end </code></pre> <h2 id="modules-velocitydilation-clear">modules/velocityDilation/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Set the radius to 0 and release the passes, leaving the velocity buffer as the geometry passes wrote it.</p> <pre><code class="language-lua">velocityDilation.clear() </code></pre> <h2 id="modules-velocitydilation-get">modules/velocityDilation/get</h2> <pre><code class="language-lua">get(): VelocityDilationState </code></pre> <p>The dilation settings currently in force.</p> <pre><code class="language-lua">local r = velocityDilation.get().radius </code></pre> <h2 id="modules-velocitydilation-maxradius">modules/velocityDilation/maxRadius</h2> <pre><code class="language-lua">maxRadius(): number </code></pre> <p>The widest neighbourhood <code>set</code> accepts, in pixels.</p> <pre><code class="language-lua">local ceiling = velocityDilation.maxRadius() </code></pre> <h2 id="modules-velocitydilation-paramsbuffer">modules/velocityDilation/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any </code></pre> <p>The parameter buffer the dilation pass reads, carrying the radius this module packs. The render feature binds what this hands it.</p> <pre><code class="language-lua">local b = velocityDilation.paramsBuffer() </code></pre> <h2 id="modules-velocitydilation-set">modules/velocityDilation/set</h2> <pre><code class="language-lua">set(opts: VelocityDilationOpts?): VelocityDilationState </code></pre> <p>Set how far velocity reaches past a silhouette. Any omitted field keeps its current value. A <code>radius</code> of 0 releases the passes and leaves the velocity buffer as the geometry passes wrote it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>VelocityDilationOpts?</code> <em>(optional)</em> — Dilation settings — see <code>VelocityDilationOpts</code>.</li> </ul> <pre><code class="language-lua">velocityDilation.set({ radius = 2 }) </code></pre> <h2 id="modules-vfs-readme">modules/vfs/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.vfs") -- vfs (also available as global 'vfs') </code></pre> <p>Virtual filesystem — read, write, list, watch. Public Luau surface over the <code>__vfs</code> Internal FFI namespace.</p> <p>A write to authored <code>/source</code> WHILE PLAY IS RUNNING lands on the play shadow: the bytes are live in the session immediately, and a guarded play-exit puts the pre-play copy back unless they were kept. Where the bound world syncs the path, the write is carried to it as it lands, so a restart or crash before that exit keeps it; where nothing syncs the path, a restart ends it. <code>vfs.durability(path)</code> answers where any write's bytes went: <code>durable</code>, whether a restart keeps them in <code>survivesRestart</code>, the state in <code>warning</code>, the routes out in <code>playShadow</code>, and which of the paths you asked about the shadow holds in <code>shadowed</code>. <code>vfs.playShadowPaths()</code> lists everything the session is holding, and <code>vfs.playShadowAuthors()</code> says whose each one is. Three routes keep a write, differing in what they cost the session: vfs.promotePlayShadow(path) -- or a list of paths: THOSE -- paths onto canonical source, -- play keeps running, the rest -- of the set untouched vfs.write(path, bytes, { durable = true }) -- the write skips the -- shadow, nothing to promote tools.use("sceneAuthoring", "changes") -- then acceptChanges: reaches -- ENTITY changes too, the -- session keeps running <code>vfs.revertPlayShadow</code> takes the same path or list and does the opposite. A folder asset settles whole: a path carrying an asset-type suffix names the asset, so one <code>.component</code> write shadows its entry file, its README and its metadata together, and promoting or reverting any one of them settles all three, naming each in its second return value. A play-exit refuses over whatever is still shadowed; <code>vfs.playShadowPaths()</code> lists it. The <code>core/vfs</code> guide has the model and what each route costs.</p> <p>Usage: local vfs = require("@builtin::modules.api.engine.vfs") Also available as global: vfs</p> <h2 id="modules-vfs-claimwalks">modules/vfs/claimWalks</h2> <pre><code class="language-lua">claimWalks(): number </code></pre> <p>Lifetime count of walks over every asset the world offers: the part of an enumeration's cost that grows with how many assets exist. <code>asset.list</code> reads a snapshot of every registered claim, built once for each change to what the world offers and shared by every list after it; <code>asset.categories</code> reads the category registry and walks nothing. Read it around a burst to learn what that burst cost in walks, a number that reads the same on a loaded machine and a quiet one.</p> <pre><code class="language-lua">local at = vfs.claimWalks() vfs.write("/zero/source/probe.module/init.luau", "return {}") asset.list("module") print(vfs.claimWalks() - at) </code></pre> <h2 id="modules-vfs-clearplayshadow">modules/vfs/clearPlayShadow</h2> <pre><code class="language-lua">clearPlayShadow(): boolean </code></pre> <p>Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.</p> <pre><code class="language-lua">vfs.clearPlayShadow() </code></pre> <h2 id="modules-vfs-contentaddress">modules/vfs/contentAddress</h2> <pre><code class="language-lua">contentAddress(path: string, opts: VfsOpts?): string? </code></pre> <p>The content address (sha-256 hex) of the bytes <code>vfs.read(path)</code> returns, for a path whose bytes the blob store holds, answered synchronously and without the bytes. Two answers that agree name the same bytes, and a write that changes them changes it. Nil for a path whose bytes are held in memory (text under the inline size threshold, a play shadow, a kept read), which a synchronous <code>vfs.read</code> already returns, and for a path nothing is stored at. Keep it beside a copy of a file you read to tell, from a caller that cannot yield, whether the copy is still what the path holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local address = vfs.contentAddress("/zero/source/scenes/main.scene/scene.json") </code></pre> <h2 id="modules-vfs-contentversion">modules/vfs/contentVersion</h2> <pre><code class="language-lua">contentVersion(path: string): number </code></pre> <p>A number that moves every time the file at <code>path</code> changes this session: each write, and each move onto it, stamps it with a version no file has held this session, and while nothing is stored at <code>path</code> (before it is written, after it is removed or moved away) it reads 0. It moves the moment any change lands, from every origin: a script's write, a peer's, a dev-mode disk pickup, a world-load or branch-swap write made before the world's content is ready, a world's unmount, both ends of a move, and a directory removal. Keep the value you saw when you last read the file, and read it again only when the number moved.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The file's path, in any form <code>vfs.read</code> takes.</li> </ul> <pre><code class="language-lua">local seen = vfs.contentVersion("/zero/source/.world_settings") </code></pre> <h2 id="modules-vfs-copy">modules/vfs/copy</h2> <pre><code class="language-lua">copy(src: string, dst: string): (boolean, (WriteReport | string)?) </code></pre> <p>Copy a file OR directory from <code>src</code> to <code>dst</code>, <code>cp -r</code> style. A directory recurses — every descendant is replicated at the same relative path under <code>dst</code>, <code>.refs</code> sidecars included. <code>.meta</code> sidecars are minted fresh, so a copy is a distinct asset with its own identity. Both paths are absolute. The source may live in any layer (writable, library mount, builtin, runtime-generated); the destination must be a writable route.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — Source absolute VFS path (file or directory).</li> <li><code>dst</code> <code>string</code> — Destination absolute VFS path.</li> </ul> <pre><code class="language-lua">vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4") local ok, report = vfs.copy(mod, "/zero/source/game/Vent.component") </code></pre> <h2 id="modules-vfs-currentauthor">modules/vfs/currentAuthor</h2> <pre><code class="language-lua">currentAuthor(): { id: string, stable: string, connection: string, agent: string?, name: string?, account: string? }? </code></pre> <p>The caller this call is attributed to: the author a <code>/source</code> write made right now would be recorded under in the play shadow. <code>id</code> names the calling session where the transport attested one, so two agent sessions driving one engine read as two callers even when one linked ZeroMind account minted both their tokens. <code>stable</code> is the key the caller had before the world's roster named it, which a rename leaves where it was; one of several agents sharing a connection, which names itself with the <code>as</code> argument every tool takes, has a <code>stable</code> of its own. <code>connection</code> is the key of the connection the caller called over, the one the caller's staging area and the writer its dirty rows record are kept under, so the agents of one connection stage and commit one another's work. <code>agent</code> is the name the world's roster holds for the caller now; <code>name</code> is its account's username, which every agent of the account shares; <code>account</code> is that shared account. Nil when the engine can name no caller at all, which is the case for engine-authored work and for a caller that presented neither a session nor a token. Compare its <code>id</code> against <code>vfs.playShadowAuthors()</code> to separate your own pending edits from a co-author's.</p> <pre><code class="language-lua">local me = vfs.currentAuthor() print(if me ~= nil then me.id else "unattributed") </code></pre> <h2 id="modules-vfs-dirsink">modules/vfs/dirSink</h2> <pre><code class="language-lua">dirSink(path: string, opts: DirSinkOpts): PluginHandle </code></pre> <p>A sink for new files under the folder at <code>path</code>, subfolders included, to pass a plugin in a call. The plugin makes whatever files it needs within <code>opts.maxFiles</code> files and <code>opts.maxBytes</code> bytes in all. Only the world's own files take a sink.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The folder to write under.</li> <li><code>opts</code> <code>DirSinkOpts</code> — <code>{ maxFiles = <n>, maxBytes = <bytes>, extensions = { "png" }? }</code>.</li> </ul> <pre><code class="language-lua">local frames = vfs.dirSink("/source/frames", { maxFiles = 240, maxBytes = 200 * 1024 * 1024, extensions = { "png" } }) </code></pre> <h2 id="modules-vfs-discardplayshadow">modules/vfs/discardPlayShadow</h2> <pre><code class="language-lua">discardPlayShadow(): { discarded: { string }, owed: { { path: string, reason: string } } } </code></pre> <p>Throw away every play-shadow edit: the discard a play-exit takes when the play session's work is not kept. Each path reverts to its pre-play copy, as vfs.revertPlayShadow does. Every path it throws away is returned in <code>discarded</code>, posted as a notice and logged by name. A path whose pre-play copy cannot be put back yet (the world's blob, still being pulled; the world's manifest row, read once the manifest has taken the update it is applying; or a copy whose write back is refused) leaves the play shadow all the same and is OWED: it holds the play bytes until the copy lands and its write is taken, and the copy is put back then, unless the path has changed in the meantime. Every owed path is returned in <code>owed</code>, posted as a notice and logged by name. Calling it again asks for every owed restore again, pulling a copy whose pull failed.</p> <pre><code class="language-lua">local result = vfs.discardPlayShadow() for _, path in ipairs(result.discarded) do print("discarded", path) end for _, owed in ipairs(result.owed) do print(owed.path, owed.reason) end </code></pre> <h2 id="modules-vfs-durability">modules/vfs/durability</h2> <pre><code class="language-lua">durability(paths: string | { string }, opts: VfsOpts?): Durability </code></pre> <p>Did that write save to disk, and if not how is it kept? What became of the bytes just written at <code>paths</code> — one path, or an array of them answered as ONE write. <code>durable</code> is true when they are where the call filed them and the next session reads them back, and false when the play shadow took any of them (live in this session and undone by a guarded play-exit unless kept) or any of them is in <code>/source/tmp/</code> scratch (this session's working space: it reads back while the engine runs and is gone when it stops or restarts, in edit mode and in play alike). <code>/source/local/</code> is durable: the machine keeps it. <code>durable</code> is present whatever the answer is, so its absence is never a reading. A non-durable answer carries <code>survivesRestart</code> (true when a restart of this engine keeps the bytes, because the bound world syncs them as they land; false when a restart ends them, which it always does for scratch) and <code>warning</code> (the state and what a restart does with it, for a reader scanning values rather than checking a field; it opens with <code>PLAY SHADOW</code> or <code>SCRATCH</code>). When the shadow holds a path the answer also carries <code>playShadow</code> (the routes to disk and what each costs a session other people are running in) and <code>shadowed</code> (which of the given paths the shadow has, or under <code>pending</code> would take); when a path is in scratch it carries <code>scratch</code> (which of them are in scratch).</p> <p>This is the answer, off the same shadow set and in the same words, that the <code>write_file</code> / <code>edit_file</code> / <code>capture</code> tools attach to their own results and that <code>asset.create</code> reports as its second return value. Ask it here at any other site that lands files, so every write surface states where the bytes went in one set of terms.</p> <p><strong>Parameters</strong></p> <ul> <li><code>paths</code> <code>string | { string }</code> — One VFS path, or an array of paths answered together as one write. A path resolves the way <code>vfs.write</code> resolves its own — absolute or <code>@</code>-rooted as it stands, a bare one under <code>/source/</code> — so the answer is about the file that write landed. A value that is not a path — an AssetRef, a record, a number, a string with nothing in it — raises rather than being answered off the empty set it reads as; a list with no entries names nothing and answers durable.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/", pending = false }</code>. <code>pending = true</code> asks the same question of a write that has NOT landed yet: where bytes written at these paths would go, read off the same play-lock verdict that will decide it. That is what a surface arranging a write it files later reports from — <code>av.record</code> opens a take here and the film is written when the take ends — because until those bytes land the shadow is holding nothing under the path and the answer would be durable at the one moment the caller can still act on it. The fields and the words are the same either way; the state names the tense it is true in.</li> </ul> <pre><code class="language-lua">local ok = vfs.write(path, body) local d = vfs.durability(path) if not d.durable then log.warn(d.warning) end local ahead = vfs.durability("/zero/source/clips/take.mp4", { pending = true }) </code></pre> <h2 id="modules-vfs-evict">modules/vfs/evict</h2> <pre><code class="language-lua">evict(path: string, opts: VfsOpts?): boolean </code></pre> <p>Drop the in-memory bytes for <code>path</code> from the writable MemFs layer without removing the asset. Use after processing large binaries to reclaim RAM.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path whose bytes should be evicted.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">vfs.evict("/zero/source/textures/imported_big.png") </code></pre> <h2 id="modules-vfs-exists">modules/vfs/exists</h2> <pre><code class="language-lua">exists(path: string, opts: VfsOpts?): boolean </code></pre> <p>Is the path known to the VFS? Checks the Stage-1 metadata (<code>.meta</code> sidecar / ManifestView) — NOT "are the bytes locally cached?". Use <code>vfs.read(path) ~= nil</code> to confirm bytes are reachable.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path to check.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">assert(vfs.exists("@builtin/models/Cube")) </code></pre> <h2 id="modules-vfs-folderhashwalks">modules/vfs/folderHashWalks</h2> <pre><code class="language-lua">folderHashWalks(): number </code></pre> <p>Lifetime count of category-folder checksum walks — the part of a write's cost that grows with how much content already exists. A folder's checksum is the tree hash over the descendants that describe the asset, so deriving one reads every key under it, and a write re-derives it for each enclosing category folder whose bytes that write reaches. Content the checksum is made of therefore moves this once per enclosing folder per write; bytes that reach no checksum — a managed sidecar, a scene's autosave overlay — move it by nothing. Read it around a burst to learn what that burst cost in walks, a number that reads the same on a loaded machine and a quiet one.</p> <pre><code class="language-lua">local at = vfs.folderHashWalks() vfs.write("/zero/source/scenes/probe.scene/scene_dirty/entities/e1.json", "{}") print(vfs.folderHashWalks() - at) </code></pre> <h2 id="modules-vfs-holdcontentsync">modules/vfs/holdContentSync</h2> <pre><code class="language-lua">holdContentSync(held: boolean) </code></pre> <p>Hold (<code>true</code>) or release (<code>false</code>) the content sync of the world this engine binds. While it is held, a take of the world's content (the initial sync a binding runs, or the one a session that lost its connection runs when it subscribes again) applies no row: the rows the backend sends wait unapplied, in order, and are applied the moment the hold is released. The binding stands and writes to authored source are taken and uploaded meanwhile, so the engine sits between binding a world and being shown its content for as long as a test needs it there: <code>world.syncStatus()</code> reads <code>content_synced = false</code> and <code>content_sync_held = true</code>, and the world's load waits at its content step. A take that already finished carries on applying live changes; the hold stops the next take at its first row. A boot started with <code>--hold-content-sync</code> (<code>holdContentSync=1</code> in a browser launch) raises the hold before the world binds, which is how the initial sync of a fresh engine is held.</p> <p><strong>Parameters</strong></p> <ul> <li><code>held</code> <code>boolean</code> — True to hold the content sync, false to release it.</li> </ul> <pre><code class="language-lua">vfs.holdContentSync(true) vfs.holdContentSync(false) </code></pre> <h2 id="modules-vfs-installedfrom">modules/vfs/installedFrom</h2> <pre><code class="language-lua">installedFrom(path: string, opts: VfsOpts?): InstalledFrom? </code></pre> <p>Where the asset holding <code>path</code> comes from, when the library carries it by reference. The library's source then holds an install record naming the asset, the world that published it and the commit its bytes are, and the engine serves the asset's files from those. <code>path</code> is the asset's own folder or anything inside it. Nil for a path whose bytes the library or the world holds itself.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path of the asset or of anything inside it.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local from = vfs.installedFrom(ref.path) if from then print(from.world, from.commit) end </code></pre> <h2 id="modules-vfs-isdirectory">modules/vfs/isDirectory</h2> <pre><code class="language-lua">isDirectory(path: string, opts: VfsOpts?): boolean </code></pre> <p>Is ONE path a directory? Answers from reality — the writable layer's children, a resolver-served folder listing, an explicit empty-directory marker — so a loose file whose extension collides with an assetType name (<code>notes.json</code>) reads as the file it is while a real <code><name>.<type>/</code> folder reads as a folder. Costs the same whatever the containing folder holds; use <code>vfs.list</code> when you want every entry's kind, this when you hold one path.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path to classify.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end </code></pre> <h2 id="modules-vfs-issaveexcluded">modules/vfs/isSaveExcluded</h2> <pre><code class="language-lua">isSaveExcluded(path: string, opts: VfsOpts?): boolean </code></pre> <p>Does this path hold content the machine keeps to itself? <code>/source/tmp/</code> is session scratch and <code>/source/local/</code> is this machine's own durable content — each directory itself included, and everything under it. Both are writable, hot-reloadable and enumerable like the rest of <code>/source/</code>; what separates them is where they stop. The engine filters them out of every world save and every peer broadcast, so they reach no world, carry no manifest row there, and a staging verb handed one refuses it by name. The match reads a whole path segment, so <code>/source/tmpfoo/</code> is ordinary content. Ask here whenever your code has to agree with what a world can hold.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path to classify.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end </code></pre> <h2 id="modules-vfs-list">modules/vfs/list</h2> <pre><code class="language-lua">list(path: string?): { VfsListEntry } </code></pre> <p>List entries in a VFS directory. A directory whose current listing the next frame assembles (<code>/zero/runtime/sync/entities</code> after a stretch nothing read the replication surface) is waited for where the calling code can yield (<code>coroutine.isyieldable()</code>), as <code>vfs.read</code> waits; where it cannot, the listing raises <code>VFS_READ_CANNOT_WAIT</code>, naming the path.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string?</code> <em>(optional)</em> — Directory path (defaults to <code>/zero</code>).</li> </ul> <pre><code class="language-lua">for _, e in ipairs(vfs.list("/zero/source")) do print(e.name) end </code></pre> <h2 id="modules-vfs-memresident">modules/vfs/memResident</h2> <pre><code class="language-lua">memResident(): { { path: string, bytes: number, kind: string } } </code></pre> <p>List the MemFs entries that are NOT resident-by-default — the writable in-memory layer's binary blobs and its large text files (text at or above the inline-text size threshold). These are the bytes <code>vfs.evict</code> can reclaim: the ones kept in RAM rather than left to fall through to the on-disk BlobStore cache. Small text (resident by default) is omitted. The audit counterpart to <code>vfs.evict</code> and to reading with <code>{ keep = true }</code> — use it to see what encoded bytes are held in RAM, and why.</p> <pre><code class="language-lua">for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end </code></pre> <h2 id="modules-vfs-mkdir">modules/vfs/mkdir</h2> <pre><code class="language-lua">mkdir(path: string, opts: VfsOpts?): boolean </code></pre> <p>Create a directory. <code>mkdir -p</code> semantics — idempotent. Errors if a file already exists at the same path. While play is running an authored <code>/source</code> directory waits for the lock to lift and the refusal RAISES with the reason; writing a file under the path creates it as part of that write, and scratch under <code>/source/tmp/</code> creates as in edit mode.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Directory path.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">vfs.mkdir("/zero/source/scenes/") </code></pre> <h2 id="modules-vfs-move">modules/vfs/move</h2> <pre><code class="language-lua">move(src: string, dst: string, opts: { quiet: boolean? }?): (boolean, (WriteReport | string)?) </code></pre> <p>Move a file from <code>src</code> to <code>dst</code>. By default fires the destination's write side effects; pass <code>opts.quiet = true</code> to suppress them. A quiet move still returns the destination's report and still posts the resolve rule the publish gate reads, which is what <code>vfs.write</code> does with the same option: a publish takes no override, so the finding is stated at the line that carries it. While play is running a move takes the source away, so authored <code>/source</code> content that predates play is refused and the refusal RAISES with the reason; content this play session created moves and stays tracked on the play shadow.</p> <p><strong>Parameters</strong></p> <ul> <li><code>src</code> <code>string</code> — Source absolute VFS path.</li> <li><code>dst</code> <code>string</code> — Destination absolute VFS path.</li> <li><code>opts</code> <code>{ quiet: boolean? }?</code> <em>(optional)</em> — Optional <code>{ quiet: boolean? }</code>.</li> </ul> <pre><code class="language-lua">vfs.move("/zero/source/a.luau", "/zero/source/b.luau") local ok, report = vfs.move(mod .. "/init.luau", comp .. "/init.luau") </code></pre> <h2 id="modules-vfs-movedreferencenames">modules/vfs/movedReferenceNames</h2> <pre><code class="language-lua">movedReferenceNames(path: string?, opts: VfsOpts?): { MovedReferenceSource } </code></pre> <p>The world sources that spell a reference by a name its target no longer has. A source records, beside each name it uses, the guid of the asset the name reached; when that asset later sits at another path (it was moved, or the engine's library files it elsewhere), the reference keeps resolving through the guid while the text keeps the old name. This lists those sources and changes nothing. Writing an entry's <code>currentText</code> back with <code>vfs.write(source, currentText)</code> brings the text up to date without changing what any reference resolves to. Only sources the world authors are considered: the engine library and <code>/source/deps/</code> keep the text they were published with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string?</code> <em>(optional)</em> — One source to ask about; every source the world authors when absent.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">for _, s in ipairs(vfs.movedReferenceNames()) do print(s.source, #s.names) end </code></pre> <h2 id="modules-vfs-mutationseq">modules/vfs/mutationSeq</h2> <pre><code class="language-lua">mutationSeq(): number </code></pre> <p>Lifetime count of VFS mutations the engine has APPLIED — the drain's clock. A write queues its side effects (an asset's content reload, the assetType's <code>onChange</code>, a component or scene registration) and a later frame runs them; this number advances as each one completes. Read it, write, then poll for a larger value to learn the queue has moved past the point you wrote at — instead of waiting a guessed number of frames. It counts every mutation kind, so it answers about the pipeline rather than about one file; <code>asset.reloadSeq(ref)</code> is the per-asset reading.</p> <pre><code class="language-lua">local at = vfs.mutationSeq() vfs.write("/zero/source/tmp/note.txt", "hi") repeat task.wait() until vfs.mutationSeq() > at </code></pre> <h2 id="modules-vfs-open">modules/vfs/open</h2> <pre><code class="language-lua">open(path: string, opts: VfsOpts?): PluginHandle </code></pre> <p>A reader of the file at <code>path</code>, to pass a plugin in a call. The plugin reads it by range and reaches nothing else through it; it is refused, raising the reason, where this script could not read. A reader whose file leaves its path answers the plugin that the asset is gone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The file to read.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local weights = vfs.open(asset.containing(__FILE__).path .. "/weights.gguf") </code></pre> <h2 id="modules-vfs-opendir">modules/vfs/openDir</h2> <pre><code class="language-lua">openDir(path: string, opts: VfsOpts?): PluginHandle </code></pre> <p>A reader of the folder at <code>path</code> and everything under it, to pass a plugin in a call. The plugin lists it and opens readers of its files, by paths relative to the folder that cannot leave it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The folder to read.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local model = vfs.openDir(asset.containing(__FILE__).path .. "/model") </code></pre> <h2 id="modules-vfs-pendingwrites">modules/vfs/pendingWrites</h2> <pre><code class="language-lua">pendingWrites(): { string } </code></pre> <p>List the <code>/source</code> paths with a local write or removal the synced manifest has not reflected yet: everything this client still owes the server. A just-written or just-removed file appears here until its upload round-trips and the synced dirty state catches up; <code>world.vcsStatus</code> unions these so a fresh edit reads back as dirty immediately, and <code>world.syncStatus().owed_paths</code> reports the same list. Empty when fully synced.</p> <pre><code class="language-lua">for _, p in ipairs(vfs.pendingWrites()) do print(p) end </code></pre> <h2 id="modules-vfs-playdiscardsowed">modules/vfs/playDiscardsOwed</h2> <pre><code class="language-lua">playDiscardsOwed(): { { path: string, reason: string } } </code></pre> <p>The play edits a discard threw away whose pre-play copy has not been put back yet. Each of these paths holds the play session's bytes until its restore lands, which happens on its own once the copy arrives and its write is taken. A path that changes first keeps its new content and leaves this list.</p> <pre><code class="language-lua">for _, owed in ipairs(vfs.playDiscardsOwed()) do print(owed.path, owed.reason) end </code></pre> <h2 id="modules-vfs-playholdsinplace">modules/vfs/playHoldsInPlace</h2> <pre><code class="language-lua">playHoldsInPlace(path: string, opts: VfsOpts?): boolean </code></pre> <p>Whether the running play session holds <code>path</code> where it stands: <code>vfs.move</code> of it, and <code>vfs.remove</code> of it without <code>durable = true</code>, are refused while play runs, because what they would take away is content that predates play (or that a durable write made canonical). Content only the play shadow holds (this play session wrote it) answers false and moves freely, and so does every path once play ends or pauses, a path in <code>/source/tmp/</code> or <code>/source/local/</code>, and a path outside authored source. Ask it before the first step of a change that ends by taking <code>path</code> away, so a change the lock would refuse at its last step is not started: the importers ask it before relocating a source into the asset they produce, and owe the import until play ends.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The VFS path: absolute, <code>@</code>-rooted, or bare under <code>/source/</code>.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">if vfs.playHoldsInPlace(src) then log.warn(src .. " moves once play ends") end </code></pre> <h2 id="modules-vfs-playshadowauthors">modules/vfs/playShadowAuthors</h2> <pre><code class="language-lua">playShadowAuthors(): { [string]: { id: string, stable: string, connection: string, agent: string?, name: string?, account: string? } } </code></pre> <p>The caller behind each currently-shadowed <code>/source</code> path, in the shape <code>vfs.currentAuthor()</code> answers in: the id the write was attributed to, the name the world's roster holds for that caller now (<code>agent</code>, the one to show), its account's username, and the ZeroMind account it authenticated as. Several agents drive one engine at once and every one of their in-play source edits sits in the same shadow set, so this is how a review, a refusal or a verdict tells one agent's pending work from another's. <code>id</code> names the calling session where the call carried one, so two agent sessions on a machine linked to one ZeroMind account are two authors. A path written by a caller the engine could not name carries no entry, because it belongs to nobody in particular, and stays settleable by anyone.</p> <pre><code class="language-lua">local mine = vfs.currentAuthor() for path, who in pairs(vfs.playShadowAuthors()) do if mine == nil or who.stable ~= mine.stable then print(path, "belongs to", who.agent or who.name) end end </code></pre> <h2 id="modules-vfs-playshadowpaths">modules/vfs/playShadowPaths</h2> <pre><code class="language-lua">playShadowPaths(): { string } </code></pre> <p>Every source write made during play that is not durable yet — the set to keep or discard before leaving play. Lists the <code>/source</code> paths edited during running play that the play shadow holds, each beside the pre-play copy a guarded discard puts back: the universal play shadow-copy set. These are the in-play edits persist promotes over the originals on confirm, or drops on a guarded discard. Empty outside play or when nothing was edited.</p> <pre><code class="language-lua">for _, p in ipairs(vfs.playShadowPaths()) do print(p) end </code></pre> <h2 id="modules-vfs-promoteplayshadow">modules/vfs/promotePlayShadow</h2> <pre><code class="language-lua">promotePlayShadow(path: string | { string }): ((string | { string })?, { string }, { string }) </code></pre> <p>KEEP a source write made while play was running — the call that saves a play-mode edit onto disk and makes it durable. Promotes play-shadow edits into canonical writes: re-asserts the live overlay bytes through the full write pipeline, then unmarks each path. The bytes stay in the engine end to end, so binary content promotes exactly. It answers while play is RUNNING and leaves the mode, the clock and every shadow entry it did not name exactly where they stood. Takes ONE path, or an ARRAY of them — a single folder-asset write shadows the entry file, the README and the metadata together, so a slice is tens of paths, and naming them keeps the call to the caller's own work on an engine other sessions are running in. Every named path is attempted; one that refuses does not stop the ones after it. A path with nothing on the play shadow at it or beneath it settles nothing and raises nothing, since no play edit stands there: the third return value names it. A promotion that cannot happen raises with the reason: the path is a plain folder covering shadowed edits (the refusal names the first five assets and loose files under it by the paths that settle them, and counts the rest), the workspace is read-only, or the write-through failed. A shadow entry whose bytes are gone is dropped as promoted, so the path comes back with nothing written for it.</p> <p>A folder asset is kept whole: a file of one promotes with every other shadowed file of the same asset, so the files its type requires reach source with whichever of them was named, and naming the folder asset's own path (<code>Gold.material</code>, <code>controls.inputMap</code>) promotes every shadowed file of it, the assets nested in it included. An asset nested inside another settles with its own files, and takes the own files of every enclosing asset the play session made with it, since it stands on source only inside them. A path an earlier call settled, by name or carried this way, answers as settled when it is named again, so a walk over a <code>vfs.playShadowPaths()</code> snapshot one path at a time settles every path in it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string | { string }</code> — A shadowed VFS path to promote, or an array of them (from vfs.playShadowPaths()).</li> </ul> <pre><code class="language-lua">local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg") local _, settled = vfs.promotePlayShadow("/zero/source/Hero.module/init.luau") local _, kept = vfs.promotePlayShadow("/zero/source/game/Gold.material") local named, _, unshadowed = vfs.promotePlayShadow("/zero/source/maybe_written.md") local mine = {} for _, p in ipairs(vfs.playShadowPaths()) do if string.find(p, "/zero/source/mine/", 1, true) == 1 then table.insert(mine, p) end end local settled = vfs.promotePlayShadow(mine) </code></pre> <h2 id="modules-vfs-read">modules/vfs/read</h2> <pre><code class="language-lua">read(path: string, opts: VfsOpts?): (string?, string?) </code></pre> <p>Read a file from the virtual filesystem. Binary-safe. Returns file contents as a string. A read that finds no bytes returns nil and the reason: one sentence naming the path and why, the text the <code>read_file</code> tool and the VFS HTTP route give for the same path (<code>/zero/source/a.txt: not found</code>, a directory's refusal, a <code>/runtime</code> node's account of what it holds). Handed straight to another call, the read passes that reason on as the next argument too; wrap it in parentheses, <code>Json.decode((vfs.read(path)))</code>, to pass the bytes alone. Relative paths resolve under <code>opts.root</code> (default <code>/source/</code>). When the bytes aren't locally cached and the calling code can yield (<code>coroutine.isyieldable()</code>: a task, <code>awake</code>, <code>start</code>, <code>update</code>, <code>onEnable</code>, <code>onOwnerChanged</code>, <code>onSyncReceived</code>, a replicated event's handler), the read waits while the lazy fetch runs and answers the bytes. Code that cannot yield (<code>onDisable</code>, <code>onDestroy</code>, <code>onPropertyChanged</code>, a received synced-function call, a <code>vfs.watch</code> callback, a module's top level while <code>require</code> runs it, a metamethod, a <code>table.sort</code> comparator) cannot wait: a read there of bytes still on their way (a file <code>vfs.exists</code> knows, a render surface being captured, a <code>/runtime/sync</code> node whose current reading the next frame assembles) raises <code>VFS_READ_CANNOT_WAIT</code>, naming the path and where to read it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local src, why = vfs.read("/zero/source/notes.txt"); if src == nil then print(why) end </code></pre> <h2 id="modules-vfs-readasync">modules/vfs/readAsync</h2> <pre><code class="language-lua">readAsync(path: string, opts: VfsOpts?): string </code></pre> <p>Asynchronous binary-safe read. Returns a promise ID that resolves to the file contents. Useful for reading render textures from the main thread without blocking.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png")) </code></pre> <h2 id="modules-vfs-readbounded">modules/vfs/readBounded</h2> <pre><code class="language-lua">readBounded(path: string, maxBytes: number): (string?, string?) </code></pre> <p>Read a file from the virtual filesystem, at most <code>maxBytes</code> long. Binary-safe, and read the way <code>vfs.read</code> reads it. A file longer than <code>maxBytes</code> answers <code>nil, "too_large"</code>; where the layer holding it states its length (a file held in memory, a synced or staged file) its bytes are never read. Relative paths resolve under <code>/source/</code>. Where the calling code can yield, a file whose bytes are not cached is fetched and held to the same bound; where it cannot, a known file whose bytes have not arrived raises <code>VFS_READ_CANNOT_WAIT</code>, as <code>vfs.read</code> does. A read that finds no bytes answers nil and the reason <code>vfs.read</code> gives.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path.</li> <li><code>maxBytes</code> <code>number</code> — The most bytes the read answers, a whole number 0 or more.</li> </ul> <pre><code class="language-lua">local raw, why = vfs.readBounded("/zero/source/notes.json", 65536) </code></pre> <h2 id="modules-vfs-reload">modules/vfs/reload</h2> <pre><code class="language-lua">reload(modulePath: string?): boolean </code></pre> <p>Clear entries from the <code>require()</code> cache so the next <code>require(name)</code> re-runs the module's source. Pass a single module identity to drop only that entry; call with no arguments to drop every cached module of the world's own. A module of the built-in library runs once for the process — its body registers into registries that outlive it — and stands through a reload with no argument. The next <code>require</code> runs the source the module's file holds when the reload is called, whichever write brought those bytes there, a <code>quiet</code> one included.</p> <p><strong>Parameters</strong></p> <ul> <li><code>modulePath</code> <code>string?</code> <em>(optional)</em> — Module identity to reload (omit to reload every module of the world's own).</li> </ul> <pre><code class="language-lua">vfs.reload("@mylib/utils.helpers") </code></pre> <h2 id="modules-vfs-remove">modules/vfs/remove</h2> <pre><code class="language-lua">remove(path: string, opts: VfsOpts?): (boolean, string?) </code></pre> <p>Remove a file. Refuses to remove directories unless <code>opts.recursive = true</code>. Refuses protected system roots. While play is running, authored <code>/source</code> content that predates play is refused and the refusal RAISES with the reason; content this play session created is removable, a folder included. Pass <code>opts.durable = true</code> to say the removal IS a change to the source: it takes the route edit mode takes with play still running and the session still in play, the counterpart of <code>vfs.write(path, bytes, { durable = true })</code> for taking a path away rather than replacing its bytes. Every other refusal stands in either terms — a protected root, a read-only route, a plain directory without <code>recursive</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path to remove.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/", recursive = false, durable = false }</code>.</li> </ul> <pre><code class="language-lua">vfs.remove("/zero/source/scratch.luau") </code></pre> <h2 id="modules-vfs-revertplayshadow">modules/vfs/revertPlayShadow</h2> <pre><code class="language-lua">revertPlayShadow(path: string | { string }): ((string | { string })?, { string }, { string }) </code></pre> <p>DISCARD a source write made while play was running, keeping nothing — the opposite of promoting it. Reverts play-shadow edits: restores the pre-play copy captured at the first play-mode write (the last edit-mode state, unstaged edits included) into the live slot — or remove the file when it did not exist at that moment — then unmark the path. Hot-reload picks the original back up, so the running session actually reverts. It answers while play is RUNNING and takes ONE path or an ARRAY of them, on the same terms as vfs.promotePlayShadow. A revert that cannot happen raises with the reason. A file of a folder asset reverts with every other shadowed file of the same asset, as a promotion carries them, and a file of an asset the play session made reverts with every shadowed file beneath it, the assets nested in it included. Naming the folder asset's own path reverts every shadowed file of it. A path with nothing on the play shadow settles nothing and raises nothing, as for vfs.promotePlayShadow. A path an earlier revert settled answers as settled when it is named again; one an earlier promotion settled raises, naming that promotion.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string | { string }</code> — A shadowed VFS path to revert, or an array of them (from vfs.playShadowPaths()).</li> </ul> <pre><code class="language-lua">local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau") local dropped = vfs.revertPlayShadow({ "/zero/source/a.md", "/zero/source/b.md" }) </code></pre> <h2 id="modules-vfs-sessionlost">modules/vfs/sessionLost</h2> <pre><code class="language-lua">sessionLost(): boolean </code></pre> <p>Whether the live world session this engine established is lost and reconnecting. While it is, every write this engine makes to authored source (<code>/source</code> outside <code>/source/tmp/</code>) is refused, because the backend of a dropped session can never receive it; the refusal names the reconnect. It answers false again from the frame the session re-establishes, and is false on an engine whose session never established (an offline boot, or a world still binding) unless <code>vfs.simulateSessionLoss</code> put it in that state. Ask it before, or after a failure of, work that writes source in steps: the importers do, and owe such an import until the session is back.</p> <pre><code class="language-lua">if vfs.sessionLost() then log.warn("writes wait for the live session") end </code></pre> <h2 id="modules-vfs-simulatefailedfetch">modules/vfs/simulateFailedFetch</h2> <pre><code class="language-lua">simulateFailedFetch(path: string, opts: VfsOpts?): (boolean, string?) </code></pre> <p>Test support. Put the file at <code>path</code> in the state a failed fetch of its bytes leaves it in: the world holds the path (<code>vfs.exists</code> answers true, and its identity and listing stand) and its bytes are not on this engine, so <code>vfs.read</code> of it answers nil and the reason <code><path>: this world holds the path, and its bytes are not on this engine</code>. It is the state a world session lands a file in when the fetch of its bytes finds none upstream, reached for a file only this engine's memory holds (any file on an offline engine, and <code>/source/tmp/</code> scratch on a world-bound one), so a test drives the readers that meet such a file. A write of the path puts bytes there again. Relative paths are resolved under <code>opts.root</code> (default '/source/').</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The file whose bytes this engine stops holding.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — Options: <code>{ root = '/source/' }</code>.</li> </ul> <pre><code class="language-lua">local ok, why = vfs.simulateFailedFetch("/source/tmp/probe.scene/scene.json") </code></pre> <h2 id="modules-vfs-simulatesessionloss">modules/vfs/simulateSessionLoss</h2> <pre><code class="language-lua">simulateSessionLoss(lost: boolean) </code></pre> <p>Put the VFS in the state a lost live session puts it in (<code>true</code>), or take it out of it (<code>false</code>), without touching the connection: <code>vfs.sessionLost()</code> answers it, and every write to authored source is refused exactly as it is while a real session is reconnecting. Once a live session has established, the engine's connection observer states the real session every frame, so the simulation holds on an engine whose live session never established (an offline boot), which is where a test drives the paths that answer a lost session.</p> <p><strong>Parameters</strong></p> <ul> <li><code>lost</code> <code>boolean</code> — True to refuse writes to authored source as a lost session does, false to accept them again.</li> </ul> <pre><code class="language-lua">vfs.simulateSessionLoss(true) vfs.simulateSessionLoss(false) </code></pre> <h2 id="modules-vfs-sink">modules/vfs/sink</h2> <pre><code class="language-lua">sink(path: string, opts: SinkOpts): PluginHandle </code></pre> <p>A sink for the file at <code>path</code>, to pass a plugin in a call. The plugin writes it from empty, up to <code>opts.maxBytes</code>, and the file lands when the plugin closes the sink or drops it. Only the world's own files take a sink.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The file to write.</li> <li><code>opts</code> <code>SinkOpts</code> — <code>{ maxBytes = <bytes> }</code>, required.</li> </ul> <pre><code class="language-lua">local out = vfs.sink("/source/imported/sphere.zvol", { maxBytes = 64 * 1024 * 1024 }) </code></pre> <h2 id="modules-vfs-stat">modules/vfs/stat</h2> <pre><code class="language-lua">stat(path: string, opts: VfsOpts?): (VfsStat?, string?) </code></pre> <p>What is at <code>path</code>, how big it is and when it last changed, without reading it. <code>size</code> is how many bytes <code>vfs.read(path)</code> returns: the length the layer holding the file states (a file held in memory, a synced or staged file), so a large file costs nothing to measure, and where no layer states one (a runtime node, a built-in library file) the length of the bytes this engine holds. It is absent for a directory and for a file whose length nothing states and whose bytes are not on this engine. <code>modified</code> is when this engine's copy last changed, in seconds since the Unix epoch (the scale <code>os.time()</code> reads): the write or arrival that put the current content there, which a move carries along with the content. The memory layer records it for every file whose bytes it holds: text up to the inline size threshold, and everything on an engine with no world bound. It is absent where no time is recorded: a directory nothing was written into as such, a runtime node, and a file whose bytes the blob store holds (binary content, and text past the inline size threshold, on a world-bound engine). For a file, the shell's <code>stat</code> and <code>ls -l</code> print the same size and time. A path nothing is at answers nil and the reason, the sentence a <code>vfs.read</code> of it gives.</p> <p>To tell whether a file changed without reading it: <code>vfs.contentAddress(path)</code> names the bytes the blob store holds, and <code>vfs.contentVersion(path)</code> moves on every change this session to a file the memory layer holds.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path.</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/" }</code>.</li> </ul> <pre><code class="language-lua">local s = vfs.stat("/zero/source/scenes/main.scene/scene.json"); if s then print(s.size, s.modified) end </code></pre> <h2 id="modules-vfs-unmarkplayshadow">modules/vfs/unmarkPlayShadow</h2> <pre><code class="language-lua">unmarkPlayShadow(path: string): boolean </code></pre> <p>Forget a single play-shadow path after it has been promoted (saved over source) or discarded. Tracking only — never touches the bytes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path to unmark.</li> </ul> <pre><code class="language-lua">vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau") </code></pre> <h2 id="modules-vfs-unwatch">modules/vfs/unwatch</h2> <pre><code class="language-lua">unwatch(watcherId: number): boolean </code></pre> <p>Remove a previously registered VFS watcher.</p> <p><strong>Parameters</strong></p> <ul> <li><code>watcherId</code> <code>number</code> — Watcher id returned by <code>vfs.watch</code>.</li> </ul> <pre><code class="language-lua">vfs.unwatch(id) </code></pre> <h2 id="modules-vfs-watch">modules/vfs/watch</h2> <pre><code class="language-lua">watch(path: string, callback: (string, string, string) -> ()): number </code></pre> <p>Register a callback that fires when a VFS path is written or removed. Two match modes: exact, or folder/prefix (key ends with <code>/</code>, and fires for any descendant). The callback runs in the VM that registered it. Returns a watcher id for <code>vfs.unwatch</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Exact path, or folder path ending in <code>/</code>.</li> <li><code>callback</code> <code>(string, string, string) -> ()</code> — <code>(mutated_path, kind, origin) -> ()</code>, kind <code>"write"</code> or <code>"remove"</code>; origin <code>"local"</code> for a change this engine made (through <code>vfs.*</code>, the shell or any other surface) and <code>"remote"</code> for one that arrived from another participant in the world.</li> </ul> <pre><code class="language-lua">local id = vfs.watch("/zero/source/", function(path, kind, origin) print(kind, origin, path) end) </code></pre> <h2 id="modules-vfs-write">modules/vfs/write</h2> <pre><code class="language-lua">write(path: string, content: string, opts: VfsOpts?): (boolean, (WriteReport | string)?) </code></pre> <p>Write content to a file. Binary-safe. Overwrites existing files by default — pass <code>opts.overwrite = false</code> to refuse to clobber. While play is running a <code>/source</code> write lands on the play shadow: it succeeds and reads back, live in the session, and is undone by a guarded play-exit unless accepted; the report's <code>survivesRestart</code> says whether a restart keeps it. Scratch under <code>/source/tmp/</code> writes through untouched, in edit mode and in play, and is gone when the engine stops or restarts, so its report says <code>durable = false</code>, <code>survivesRestart = false</code> and a <code>warning</code> opening <code>SCRATCH</code>. Pass <code>opts.durable = true</code> to say these bytes ARE the source: the write reaches canonical <code>/source</code> with play still running and the session still in play, hot-reloading the modules and components that read it, so the edit is observed running in the same play session with nothing left to promote. A durable write RAISES with the reason when the bytes cannot become canonical source. One write carries at most 256 MiB: longer content RAISES, naming its size and that limit, and the path keeps what it held.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — VFS path to write to.</li> <li><code>content</code> <code>string</code> — File content (binary-safe).</li> <li><code>opts</code> <code>VfsOpts?</code> <em>(optional)</em> — <code>{ root = "/source/", overwrite = true, quiet = false, durable = false }</code>.</li> </ul> <pre><code class="language-lua">vfs.write("/zero/source/notes.md", body) local ok, report = vfs.write("/zero/source/game/Vent.component/init.luau", src) if report.diagnostics then for _, d in report.diagnostics do print(d.severity, d.path, d.line, d.message) end end if report.contentRequirements then for _, r in report.contentRequirements do print(r.path, r.detail) end end if not report.durable then log.warn(report.warning) end vfs.write("/zero/source/game/Vent.component/init.luau", src, { durable = true }) </code></pre> <h2 id="modules-video-readme">modules/video/README</h2> <pre><code class="language-lua">require("@builtin::modules.api.engine.video") -- video (also available as global 'video') </code></pre> <p>Video playback — create / play / pause / seek / setRate / setLoop / destroy on render-target-backed video players. Public Luau surface over the <code>__video</code> Internal FFI namespace. The live picture of a webcam or phone camera is <code>cameraInput</code>.</p> <p>Usage: local video = require("@builtin::modules.api.engine.video") Also available as global: video</p> <h2 id="modules-video-create">modules/video/create</h2> <pre><code class="language-lua">create(url: string, options: VideoOptions?): string </code></pre> <p>Create a video player. Returns a texture handle (e.g. <code>"video_0"</code>) usable directly in <code>material.setTexture()</code> — its frames sample like any other texture.</p> <p><strong>Parameters</strong></p> <ul> <li><code>url</code> <code>string</code> — URL or asset path to an MP4 video file.</li> <li><code>options</code> <code>VideoOptions?</code> <em>(optional)</em> — Playback options: <code>loop</code> (default false), <code>autoplay</code> (default false), <code>rate</code> (default 1.0).</li> </ul> <pre><code class="language-lua">local tex = video.create("http://example.com/clip.mp4", { autoplay = true }) </code></pre> <h2 id="modules-video-destroy">modules/video/destroy</h2> <pre><code class="language-lua">destroy(handle: string): boolean </code></pre> <p>Destroy a video player and free the render target and all resources.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle from <code>video.create</code>.</li> </ul> <pre><code class="language-lua">video.destroy(rt) </code></pre> <h2 id="modules-video-getinfo">modules/video/getInfo</h2> <pre><code class="language-lua">getInfo(handle: string): VideoInfo? </code></pre> <p>Get video information and current playback state. <code>codec</code> is the configuration the stream declares, such as <code>"avc1.64002a"</code>, and <code>codecSupported</code> is this host's answer about decoding it: true where it has a decoder, false where it has none, and nil while the answer is still being fetched. <code>state</code> reads <code>"unsupported"</code> for a stream this host has no decoder for, with <code>error</code> naming the codec and what refused it, and no frame is produced for it. The frame counts describe playback since <code>create</code>: <code>framesDecoded</code> is how many frames the decoder produced, <code>framesPresented</code> how many the player handed to its texture, <code>framesExpected</code> how many frames the playback clock reached (one per frame interval a tick landed in), and <code>frameThreadDecodes</code> how many samples were decoded on the engine's frame thread rather than on the decoder's own.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle.</li> </ul> <pre><code class="language-lua">local i = video.getInfo(rt); print(i.currentTime, "/", i.duration) </code></pre> <h2 id="modules-video-pause">modules/video/pause</h2> <pre><code class="language-lua">pause(handle: string): boolean </code></pre> <p>Pause video playback. Can be resumed with <code>video.play</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle.</li> </ul> <pre><code class="language-lua">video.pause(rt) </code></pre> <h2 id="modules-video-play">modules/video/play</h2> <pre><code class="language-lua">play(handle: string): boolean </code></pre> <p>Start or resume video playback.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle from <code>video.create</code>.</li> </ul> <pre><code class="language-lua">video.play(rt) </code></pre> <h2 id="modules-video-seek">modules/video/seek</h2> <pre><code class="language-lua">seek(handle: string, time: number): boolean </code></pre> <p>Seek to a specific time (seconds) in the video.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle.</li> <li><code>time</code> <code>number</code> — Target time in seconds.</li> </ul> <pre><code class="language-lua">video.seek(rt, 30.5) </code></pre> <h2 id="modules-video-setloop">modules/video/setLoop</h2> <pre><code class="language-lua">setLoop(handle: string, loop: boolean): boolean </code></pre> <p>Enable or disable looping.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle.</li> <li><code>loop</code> <code>boolean</code> — Whether to loop playback.</li> </ul> <pre><code class="language-lua">video.setLoop(rt, true) </code></pre> <h2 id="modules-video-setrate">modules/video/setRate</h2> <pre><code class="language-lua">setRate(handle: string, rate: number): boolean </code></pre> <p>Set the playback speed multiplier. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle.</li> <li><code>rate</code> <code>number</code> — Playback rate.</li> </ul> <pre><code class="language-lua">video.setRate(rt, 2.0) </code></pre> <h2 id="modules-video-stop">modules/video/stop</h2> <pre><code class="language-lua">stop(handle: string): boolean </code></pre> <p>Stop video playback and reset to the beginning.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>string</code> — Video handle.</li> </ul> <pre><code class="language-lua">video.stop(rt) </code></pre> <h2 id="modules-volume-readme">modules/volume/README</h2> <pre><code class="language-lua">require("@builtin::systems.volumetrics.volume") -- volume </code></pre> <p>3D textures as volumes: build one procedurally or from imported voxels, page it into bricks so it costs what it holds, and raymarch it through an entity's transform.</p> <p>Usage: local volume = require("@builtin::systems.volumetrics.volume")</p> <h2 id="modules-volume-volume-buildsparse">modules/volume/Volume:buildSparse</h2> <pre><code class="language-lua">Volume:buildSparse(opts: SparseOpts?) </code></pre> <p>Store this volume as bricks, keeping only the ones holding anything over <code>threshold</code>. What the volume costs on the GPU becomes what it contains rather than the size of its bounding box, and the page table built along the way is the grid the raymarcher skips empty space with. Counting the bricks reads the allocation counter back from the GPU, so this yields — twice unless <code>capacity</code> says how many slots to take; <code>refreshSparse</code> re-pages without waiting on either.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>SparseOpts?</code> <em>(optional)</em> — See <code>SparseOpts</code> — brick size, density threshold, which channel carries density, and how much slot headroom to leave for a volume that will grow.</li> </ul> <pre><code class="language-lua">local stats = v:buildSparse({ brickSize = 8, threshold = 0.01 }) print(stats.residentBytes, stats.denseBytes) </code></pre> <h2 id="modules-volume-volume-destroy">modules/volume/Volume:destroy</h2> <pre><code class="language-lua">Volume:destroy() </code></pre> <p>Free the GPU resources backing this Volume — the named 3D texture, any companion occupancy R8 texture, and the brick pool and page table if the volume is paged. Subsequent operations on the wrapper return false silently.</p> <pre><code class="language-lua">v:destroy() </code></pre> <h2 id="modules-volume-volume-fillfrombuffer">modules/volume/Volume:fillFromBuffer</h2> <pre><code class="language-lua">Volume:fillFromBuffer(buffer: any, lanes: number): boolean </code></pre> <p>Fill this volume from a buffer on the device: one voxel per element, x fastest, then y, then z, each element <code>lanes</code> floats (1 to 4) written to the voxel's channels in order, the rest 0. What puts a grid a graph made into a volume without reading it back. The volume is created with <code>storage = true</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>buffer</code> <code>any</code> <em>(optional)</em> — The buffer's name, or a handle with <code>bufferName()</code>.</li> <li><code>lanes</code> <code>number</code> — Floats per voxel, 1 to 4.</li> </ul> <pre><code class="language-lua">cloud:fillFromBuffer(densityColumn, 1) </code></pre> <h2 id="modules-volume-volume-readsparsecounts">modules/volume/Volume:readSparseCounts</h2> <pre><code class="language-lua">Volume:readSparseCounts(): { [string]: number } </code></pre> <p>Read how many bricks the last paging pass stored and how many found the pool full. Yields until the counter arrives.</p> <pre><code class="language-lua">local counts = v:readSparseCounts() </code></pre> <h2 id="modules-volume-volume-refreshsparse">modules/volume/Volume:refreshSparse</h2> <pre><code class="language-lua">Volume:refreshSparse(): boolean </code></pre> <p>Re-page this volume from its current contents. Two dispatches and no wait, so a volume a compute shader rewrites every frame keeps a page table that matches what it now holds. The brick counts the pass produces are collected on a later call rather than waited on, so a volume that grows past its pool reports the overflow — and warns — a frame or two after the refresh that caused it.</p> <pre><code class="language-lua">v:fillCompute(myShader); v:refreshSparse() </code></pre> <h2 id="modules-volume-volume-releasedense">modules/volume/Volume:releaseDense</h2> <pre><code class="language-lua">Volume:releaseDense(): boolean </code></pre> <p>Free the dense volume, leaving the brick pool as the only copy. The volume goes on rendering from the pool; re-paging it needs the dense copy, so this is for a volume that is finished changing.</p> <pre><code class="language-lua">v:buildSparse(); v:releaseDense() </code></pre> <h2 id="modules-volume-volume-releasesparse">modules/volume/Volume:releaseSparse</h2> <pre><code class="language-lua">Volume:releaseSparse(): boolean </code></pre> <p>Free the brick pool and page table, returning the volume to rendering from its dense texture. The dense copy has to still be there, so a volume that released it stays paged.</p> <pre><code class="language-lua">v:buildSparse(); v:releaseSparse() </code></pre> <h2 id="modules-volume-volume-render">modules/volume/Volume:render</h2> <pre><code class="language-lua">Volume:render(opts: RenderOpts): string </code></pre> <p>Dispatch the shared compute raymarcher to render this Volume through the entity's transform AABB. Output goes to a 2D storage texture (auto-created) — composite from there via a post-process effect, a material sampler, or another GPU copy.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>RenderOpts</code> — See <code>RenderOpts</code> above.</li> </ul> <pre><code class="language-lua">v:render({ entity = entity(id), targetWidth = 1280, targetHeight = 720, density = 8.0 }) </code></pre> <h2 id="modules-volume-volume-sparsestats">modules/volume/Volume:sparseStats</h2> <pre><code class="language-lua">Volume:sparseStats(): { [string]: any } </code></pre> <p>What this volume costs on the GPU, and what it would cost dense.</p> <pre><code class="language-lua">local s = v:sparseStats(); print(s.residentBytes / s.denseBytes) </code></pre> <h2 id="modules-volume-bindcomposite">modules/volume/bindComposite</h2> <pre><code class="language-lua">bindComposite(target: string) </code></pre> <p>Point the composite <code>holdComposite</code> stands for at its render target. Called every frame a raymarch writes it: the target is made on a later engine queue than the post effect, and a size change makes it again.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string</code> — The render target.</li> </ul> <pre><code class="language-lua">Volume.bindComposite("scene_volume_rt") </code></pre> <h2 id="modules-volume-box">modules/volume/box</h2> <pre><code class="language-lua">box(opts: { [string]: any }) </code></pre> <p>Build a 3D volume filled with a solid-box SDF.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }</code> — <code>{name, size, color={1,1,1}, format="rgba16f"}</code>.</li> </ul> <pre><code class="language-lua">local v = M.box({ name = "cube_volume", size = 64 }) </code></pre> <h2 id="modules-volume-compositeshadersource">modules/volume/compositeShaderSource</h2> <pre><code class="language-lua">compositeShaderSource(): (string, string) </code></pre> <p>WGSL source for the post-process composite that blends the volume render target over the scene with premultiplied-alpha. Zero-scaffolding: author only <code>fragment()</code>; <code>vol_rt</code> is a declared <code>texture</code> property. Register with <code>postprocess.add(name, src, { properties = {{ name="vol_rt", type="texture", textureDefault="transparent" }} })</code> and bind the target via <code>postprocess.setTexture(name, "vol_rt", <render-target>)</code>.</p> <pre><code class="language-lua">local src, name = M.compositeShaderSource() </code></pre> <h2 id="modules-volume-create">modules/volume/create</h2> <pre><code class="language-lua">create(opts: CreateOpts) </code></pre> <p>Allocate a fresh, zero-filled 3D texture and return a Volume wrapper bound to its name. The volume answers to that name in <code>M.get</code> from here on; building over a name a live volume already holds releases that volume first, so one volume owns the name and its brick pool.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>CreateOpts</code> — Creation options; see <code>CreateOpts</code>.</li> </ul> <pre><code class="language-lua">local v = M.create({ name = "cloud", width = 64, height = 64, depth = 64, storage = true }) </code></pre> <h2 id="modules-volume-get">modules/volume/get</h2> <pre><code class="language-lua">get(name: string): any </code></pre> <p>The live volume built under <code>name</code>, with everything it knows about itself: its voxel dimensions, its channel count, its occupancy grid, and the brick pool it is paged into. Every volume this module builds answers to its texture name here, so a script or a component that has only the name renders the volume as it stands — including a paged one, whose voxels live in the pool rather than under that name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The texture name the volume was built under.</li> </ul> <pre><code class="language-lua">local v = M.get("cloud") if v then v:render({ entity = e }) end </code></pre> <h2 id="modules-volume-holdcomposite">modules/volume/holdComposite</h2> <pre><code class="language-lua">holdComposite(target: string): string </code></pre> <p>Keep the composite that blends render target <code>target</code> over the scene standing while something draws into it — a Volume component takes one hold for the target it renders to. The first hold adds the post effect; <code>bindComposite</code> points it at the target each frame, and the last <code>releaseComposite</code> removes it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string</code> — The render target a raymarch writes (<code>scene_volume_rt</code> by default).</li> </ul> <pre><code class="language-lua">Volume.holdComposite("scene_volume_rt") </code></pre> <h2 id="modules-volume-names">modules/volume/names</h2> <pre><code class="language-lua">names(): { string } </code></pre> <p>The names every live volume is registered under, sorted.</p> <pre><code class="language-lua">for _, name in ipairs(M.names()) do print(name, M.get(name):sparseStats().residentBytes) end </code></pre> <h2 id="modules-volume-noise">modules/volume/noise</h2> <pre><code class="language-lua">noise(opts: { [string]: any }) </code></pre> <p>Build a 3D volume filled with FBM value-noise. Good base for procedural clouds, smoke, nebulae.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }</code> — <code>{name, size=64, scale=4, octaves=4, threshold=0.5, seed=42, color={1,1,1}, format="rgba16f"}</code>.</li> </ul> <pre><code class="language-lua">local cloud = M.noise({ name = "cloud", size = 64, scale = 4, threshold = 0.55 }) </code></pre> <h2 id="modules-volume-raymarchshaderidentity">modules/volume/raymarchShaderIdentity</h2> <pre><code class="language-lua">raymarchShaderIdentity(): string </code></pre> <p>Return the built-in raymarch shader's asset identity. Callers that want to diff or override the default raymarcher pull source via <code>asset.inspect(Volume.raymarchShaderIdentity())</code> and register their variant under a different name.</p> <pre><code class="language-lua">local src = vfs.read(asset.inspect(M.raymarchShaderIdentity()).source .. "/shader.wgsl") </code></pre> <h2 id="modules-volume-register">modules/volume/register</h2> <pre><code class="language-lua">register(volume: any): any </code></pre> <p>Publish a wrapped volume under its own texture name, so holders of the name reach this object rather than building a wrapper of their own. Volumes this module builds are published as they are created; a wrapper over a texture another system owns is published by this call.</p> <p><strong>Parameters</strong></p> <ul> <li><code>volume</code> <code>any</code> <em>(optional)</em> — A Volume — the value <code>M.wrap</code> or a builder returned.</li> </ul> <pre><code class="language-lua">M.register(M.wrap({ name = "imported", width = 64, height = 64, depth = 64 })) </code></pre> <h2 id="modules-volume-releasecomposite">modules/volume/releaseComposite</h2> <pre><code class="language-lua">releaseComposite(target: string) </code></pre> <p>Let go of one hold <code>holdComposite</code> took; the last one removes the composite, so a scene whose volumes are gone composites nothing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>target</code> <code>string</code> — The render target.</li> </ul> <pre><code class="language-lua">Volume.releaseComposite("scene_volume_rt") </code></pre> <h2 id="modules-volume-releasetarget">modules/volume/releaseTarget</h2> <pre><code class="language-lua">releaseTarget(name: string): boolean </code></pre> <p>Destroy the 2D storage texture a render names as its <code>target</code> and forget the size this module built it at, so the next render builds it again. A target is created on the first render that names it and shared by every volume marching into it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The render-target name, as passed to <code>Volume:render</code>.</li> </ul> <pre><code class="language-lua">M.releaseTarget("scene_volume_rt") </code></pre> <h2 id="modules-volume-sphere">modules/volume/sphere</h2> <pre><code class="language-lua">sphere(opts: { [string]: any }) </code></pre> <p>Build a 3D volume filled with a soft sphere SDF (centred at the volume's middle).</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }</code> — <code>{name, size, falloffStart=0.3, falloffEnd=0.5, color={1,1,1}, format="rgba16f"}</code>.</li> </ul> <pre><code class="language-lua">local v = M.sphere({ name = "blob", size = 64, falloffStart = 0.3 }) </code></pre> <h2 id="modules-volume-unregister">modules/volume/unregister</h2> <pre><code class="language-lua">unregister(name: string): any </code></pre> <p>Drop the registration under <code>name</code> and hand back the volume that held it, leaving its GPU resources alone. The returned volume is the way back to the data — <code>destroy</code> it to free the texture and any brick pool.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The texture name to stop answering for.</li> </ul> <pre><code class="language-lua">local v = M.unregister("imported") if v then v:destroy() end </code></pre> <h2 id="modules-volume-wrap">modules/volume/wrap</h2> <pre><code class="language-lua">wrap(opts: { [string]: any }) </code></pre> <p>Wrap an existing named 3D texture as a Volume without creating a new one. Use when a producer outside this module owns the texture lifetime (e.g. the <code>.zvol</code> viewer uploads voxels through <code>compute.writeFloatsTexture3D</code> and then wraps the result for rendering).</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ [string]: any }</code> — <code>{name, width?, height?, depth?, format?, occupancyName?, occupancyBrick?}</code>.</li> </ul> <pre><code class="language-lua">local v = M.wrap({ name = "imported", format = "rgba16f", occupancyName = "imported_occ", occupancyBrick = 8 }) </code></pre> <h2 id="modules-volume-zvolheader">modules/volume/zvolHeader</h2> <pre><code class="language-lua">zvolHeader(bytes: string): (ZvolHeader?, string?) </code></pre> <p>Read a <code>.zvol</code> file's header. The payload it describes uploads verbatim — <code>payloadOffset</code> and <code>payloadBytes</code> cut the voxel bytes out of the same string, and <code>writeBytes</code> takes them as they are.</p> <p><strong>Parameters</strong></p> <ul> <li><code>bytes</code> <code>string</code> — The file's contents.</li> </ul> <pre><code class="language-lua">local h = M.zvolHeader(vfs.read(path)) v:writeBytes(string.sub(bytes, h.payloadOffset, h.payloadOffset + h.payloadBytes - 1)) </code></pre> <h2 id="modules-volumesequence-readme">modules/volumeSequence/README</h2> <pre><code class="language-lua">require("@builtin::systems.volumetrics.volumeSequence") -- volumeSequence </code></pre> <p>An ordered run of <code>.zvol</code> frames played back through a fixed ring of resident volumes: frames are read from the VFS a little ahead of the playhead and uploaded into whichever ring slot is furthest from it, so what the sequence costs on the GPU is the ring, whatever the run's length.</p> <p>Usage: local volumeSequence = require("@builtin::systems.volumetrics.volumeSequence")</p> <h2 id="modules-volumesequence-sequence-bounds">modules/volumeSequence/Sequence:bounds</h2> <pre><code class="language-lua">Sequence:bounds(): ({ number }, { number }) </code></pre> <p>The world-space bounds frame 1's header declared, as the importer wrote them.</p> <pre><code class="language-lua">local mn, mx = seq:bounds() </code></pre> <h2 id="modules-volumesequence-sequence-close">modules/volumeSequence/Sequence:close</h2> <pre><code class="language-lua">Sequence:close(): boolean </code></pre> <p>Cancel any read still in flight, free the ring and drop the sequence from the registry.</p> <pre><code class="language-lua">seq:close() </code></pre> <h2 id="modules-volumesequence-sequence-current">modules/volumeSequence/Sequence:current</h2> <pre><code class="language-lua">Sequence:current() </code></pre> <p>The volume holding the frame currently on screen. Render it the way any other volume renders — the wrapper it answers with is one of the ring's slots, so the same handle comes back for as long as that frame is shown.</p> <pre><code class="language-lua">local v = seq:current(); if v then v:render({ entity = e, density = 6 }) end </code></pre> <h2 id="modules-volumesequence-sequence-pause">modules/volumeSequence/Sequence:pause</h2> <pre><code class="language-lua">Sequence:pause(): boolean </code></pre> <p>Hold the playhead where it is. Reads already in flight still land.</p> <pre><code class="language-lua">seq:pause() </code></pre> <h2 id="modules-volumesequence-sequence-play">modules/volumeSequence/Sequence:play</h2> <pre><code class="language-lua">Sequence:play(): boolean </code></pre> <p>Resume advancing the playhead.</p> <pre><code class="language-lua">seq:play() </code></pre> <h2 id="modules-volumesequence-sequence-residency">modules/volumeSequence/Sequence:residency</h2> <pre><code class="language-lua">Sequence:residency(): { { frame: number?, loading: boolean } } </code></pre> <p>What each ring slot holds, in slot order — one entry per slot, whether or not it has ever been filled. A closed sequence holds no slots and answers with an empty array.</p> <pre><code class="language-lua">for i, slot in ipairs(seq:residency()) do print(i, slot.frame, slot.loading) end </code></pre> <h2 id="modules-volumesequence-sequence-seek">modules/volumeSequence/Sequence:seek</h2> <pre><code class="language-lua">Sequence:seek(frame: number): number </code></pre> <p>Put the playhead on a frame. The clock moves with it, so playback resumes from there rather than jumping back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>frame</code> <code>number</code> — 1-based frame index; wrapped for a looping sequence, clamped otherwise.</li> </ul> <pre><code class="language-lua">seq:seek(12) </code></pre> <h2 id="modules-volumesequence-sequence-setfps">modules/volumeSequence/Sequence:setFps</h2> <pre><code class="language-lua">Sequence:setFps(fps: number): number </code></pre> <p>Set the playback rate. The playhead keeps the frame it stands on and the fraction of the way through it, so playback carries on from there at the new rate. Zero holds the playhead while leaving <code>playing</code> alone.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fps</code> <code>number</code> — Frames per second.</li> </ul> <pre><code class="language-lua">seq:setFps(12) </code></pre> <h2 id="modules-volumesequence-sequence-stats">modules/volumeSequence/Sequence:stats</h2> <pre><code class="language-lua">Sequence:stats(): { [string]: any } </code></pre> <p>What the sequence costs and how playback is keeping up. <code>residentBytes</code> counts the ring, <code>allBytes</code> counts what holding every frame at once would cost, <code>stalls</code> counts the frames playback reached before their read did, and <code>errors</code> counts the frames of the run that could not be read — each one counted and logged once, and stepped over from then on.</p> <pre><code class="language-lua">local s = seq:stats(); print(s.residentBytes, s.allBytes) </code></pre> <h2 id="modules-volumesequence-sequence-tick">modules/volumeSequence/Sequence:tick</h2> <pre><code class="language-lua">Sequence:tick(dt: number): number? </code></pre> <p>Advance the playhead by <code>dt</code> seconds, start the reads the new position calls for, and promote the newest frame whose slot has filled. The playhead moves once per engine frame however many callers tick the sequence, so two entities can play one run without it running double speed; every call promotes and reads ahead.</p> <p><strong>Parameters</strong></p> <ul> <li><code>dt</code> <code>number</code> — Seconds since the last call.</li> </ul> <pre><code class="language-lua">seq:tick(dt) </code></pre> <h2 id="modules-volumesequence-all">modules/volumeSequence/all</h2> <pre><code class="language-lua">all(): { [string]: any } </code></pre> <p>Every sequence currently open, by name.</p> <pre><code class="language-lua">for name, seq in pairs(M.all()) do print(name, seq:stats().residentBytes) end </code></pre> <h2 id="modules-volumesequence-close">modules/volumeSequence/close</h2> <pre><code class="language-lua">close(name: string): boolean </code></pre> <p>Close the sequence open under this name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Name passed to <code>open</code>.</li> </ul> <pre><code class="language-lua">M.close("plume") </code></pre> <h2 id="modules-volumesequence-get">modules/volumeSequence/get</h2> <pre><code class="language-lua">get(name: string) </code></pre> <p>The sequence open under this name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Name passed to <code>open</code>.</li> </ul> <pre><code class="language-lua">local seq = M.get("plume") </code></pre> <h2 id="modules-volumesequence-open">modules/volumeSequence/open</h2> <pre><code class="language-lua">open(opts: OpenOpts) </code></pre> <p>Open a run of <code>.zvol</code> frames for playback. Frame 1's header sizes the ring, so every frame of the run has to carry the same dimensions; the ring itself is <code>resident</code> volumes and is the whole GPU cost of the sequence. Reading frame 1 yields.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>OpenOpts</code> — See <code>OpenOpts</code> — the frame paths, the ring size, and the playback rate.</li> </ul> <pre><code class="language-lua">local seq = M.open({ name = "plume", frames = paths, resident = 3, fps = 24 }) seq:tick(dt); local v = seq:current(); if v then v:render({ entity = e }) end </code></pre> <h2 id="modules-volumetriclighting-readme">modules/volumetricLighting/README</h2> <pre><code class="language-lua">require("@builtin::systems.volumetrics.volumetricLighting") -- volumetricLighting </code></pre> <p>Light scattered by the air between the camera and the scene — a spotlight cone visible in fog, a shaft where the sun cuts past an occluder, haze that brightens toward a source.</p> <p>Usage: local volumetricLighting = require("@builtin::systems.volumetrics.volumetricLighting")</p> <h2 id="modules-volumetriclighting-active">modules/volumetricLighting/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the volumetric passes are running this frame.</p> <pre><code class="language-lua">if volumetricLighting.active() then ... end </code></pre> <h2 id="modules-volumetriclighting-clear">modules/volumetricLighting/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Empty the air and release the passes. The other settings are kept, so a later <code>set({ density = ... })</code> brings back the same medium.</p> <pre><code class="language-lua">volumetricLighting.clear() </code></pre> <h2 id="modules-volumetriclighting-get">modules/volumetricLighting/get</h2> <pre><code class="language-lua">get(): VolumetricState </code></pre> <p>The medium settings currently in force.</p> <pre><code class="language-lua">local d = volumetricLighting.get().density </code></pre> <h2 id="modules-volumetriclighting-paramsbuffer">modules/volumetricLighting/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The buffer the marching passes read. <code>lightScattering.renderFeature</code> binds what this hands it, so both passes carry the values this module packed.</p> <pre><code class="language-lua">local p = volumetricLighting.paramsBuffer() </code></pre> <h2 id="modules-volumetriclighting-set">modules/volumetricLighting/set</h2> <pre><code class="language-lua">set(opts: VolumetricOpts?): VolumetricState </code></pre> <p>Set the scene's participating medium. Any omitted field keeps its current value. A <code>density</code> of 0 empties the air and releases the passes.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>VolumetricOpts?</code> <em>(optional)</em> — Medium settings — see <code>VolumetricOpts</code>.</li> </ul> <pre><code class="language-lua">volumetricLighting.set({ density = 0.06, anisotropy = 0.7 }) </code></pre> <h2 id="modules-watch">modules/watch</h2> <pre><code class="language-lua">modules.watch(path, callback) -> number </code></pre> <p>Register a callback that fires when a module's source code changes via VFS write. Use for live-reload: re-require the module inside the callback. Returns a watcher ID. The subscription is recorded under the key the module reloads under, resolved from this call site the way <code>require</code> resolves the same string, so every spelling of one module reaches it: the folder <code>foo.module</code>, its <code>init.luau</code>, the dotted identity, the <code>@local.</code> world form, a <code>~package</code> alias and a caller-relative <code>.sibling</code>. A spelling naming a module the engine holds no source for yet is recorded under that spelling's own canonical form and warns, so a watch taken out before the module is written is called by the write that creates it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — Any spelling <code>require</code> accepts for the module to watch: an identity like '@builtin::modules.json', a VFS path such as '/source/hud.module', or a caller-relative form like '.sibling'.</li> <li><code>callback</code> <code>function</code> — Called with (path) when the module source changes</li> </ul> <p><strong>Returns</strong> <code>number</code> — Watcher ID (pass to modules.unwatch to remove)</p> <h2 id="modules-water-surfaces-readme">modules/water_surfaces/README</h2> <pre><code class="language-lua">require("@builtin::modules.water_surfaces") -- water_surfaces </code></pre> <p>The water surfaces a scene holds, gathered from the systems that make them. A system with water — a terrain's lakes and rivers, an ocean — registers a provider here, and whatever needs to stand things on water asks every provider at once: <code>scene.water</code> in a procedural graph reads them all through this list. A provider is <code>{ version = fn() -> string, stand = fn(request) }</code>. <code>version</code> names what its water is now (a surface that changes every frame answers differently every frame). <code>stand</code> puts points on its water on the device: <code>request</code> carries <code>run</code> (the points' GPU positions, in the graph's space), <code>count</code>, <code>out</code> (<code>P</code>, <code>normal</code>, <code>slope</code> and <code>wet</code> GPU columns to write for the points over its water, leaving the rest as they are), <code>place</code> / <code>unplace</code> (graph to world and back, 16 numbers each, column major), <code>offset</code> (metres above the surface) and <code>keepY</code> (whether a point's own height rides on the surface's); it returns the names of the kernels it dispatched, which wrote those columns.</p> <p>Usage: local water_surfaces = require("@builtin::modules.water_surfaces")</p> <h2 id="modules-water-surfaces-list">modules/water_surfaces/list</h2> <pre><code class="language-lua">list(): { { name: string, provider: { [string]: any } } } </code></pre> <p>Every registered provider, by name in order.</p> <pre><code class="language-lua">for _, p in water_surfaces.list() do print(p.name) end </code></pre> <h2 id="modules-water-surfaces-register">modules/water_surfaces/register</h2> <pre><code class="language-lua">register(name: string, provider: { [string]: any }) </code></pre> <p>Register a water provider under <code>name</code>, replacing one registered under it before.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The provider's name — the system it speaks for.</li> <li><code>provider</code> <code>{ [string]: any }</code> — <code>{ version = fn() -> string, stand = fn(request) -> { string } }</code>.</li> </ul> <pre><code class="language-lua">water_surfaces.register("ocean", { version = oceanVersion, stand = standOnOcean }) </code></pre> <h2 id="modules-water-surfaces-unregister">modules/water_surfaces/unregister</h2> <pre><code class="language-lua">unregister(name: string): boolean </code></pre> <p>Remove the provider registered under <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The provider's name.</li> </ul> <pre><code class="language-lua">water_surfaces.unregister("ocean") </code></pre> <h2 id="modules-weather-readme">modules/weather/README</h2> <pre><code class="language-lua">require("@builtin::systems.weather.weather") -- weather </code></pre> <p>How surfaces respond to weather — wetness darkens and sharpens, snow settles on what faces up, and anything under cover stays clear.</p> <p>Usage: local weather = require("@builtin::systems.weather.weather")</p> <h2 id="modules-weather-active">modules/weather/active</h2> <pre><code class="language-lua">active(): boolean </code></pre> <p>Whether the weather pass is currently running.</p> <pre><code class="language-lua">if weather.active() then print("wet") end </code></pre> <h2 id="modules-weather-bakeshelter">modules/weather/bakeShelter</h2> <pre><code class="language-lua">bakeShelter(): ShelterField </code></pre> <p>Re-derive the cover field the shelter test reads from the geometry the renderer currently draws. The field is baked when weather starts and re-derived when the count of renderables moves; call this after moving or reshaping cover that left that count where it was.</p> <pre><code class="language-lua">weather.bakeShelter() </code></pre> <h2 id="modules-weather-clear">modules/weather/clear</h2> <pre><code class="language-lua">clear() </code></pre> <p>Clear the weather and release the pass. The other settings are kept, so a later <code>set</code> brings back the same look.</p> <pre><code class="language-lua">weather.clear() </code></pre> <h2 id="modules-weather-coverbuffer">modules/weather/coverBuffer</h2> <pre><code class="language-lua">coverBuffer(): any? </code></pre> <p>The cover field the shelter test reads, as a buffer a pass binds.</p> <pre><code class="language-lua">local b = weather.coverBuffer() </code></pre> <h2 id="modules-weather-get">modules/weather/get</h2> <pre><code class="language-lua">get(): WeatherState </code></pre> <p>The weather currently in force.</p> <pre><code class="language-lua">local w = weather.get().wetness </code></pre> <h2 id="modules-weather-paramsbuffer">modules/weather/paramsBuffer</h2> <pre><code class="language-lua">paramsBuffer(): any? </code></pre> <p>The parameter buffer this system's passes read. A pass binds what this hands it, so it has the values this module packed.</p> <pre><code class="language-lua">local b = weather.paramsBuffer() </code></pre> <h2 id="modules-weather-refresh">modules/weather/refresh</h2> <pre><code class="language-lua">refresh() </code></pre> <p>Re-pack the buffer against the light the scene is standing in now — its ambient and its sun — and bring the cover field along with the scene. The pass runs every frame and the scene's lighting moves between frames, so the sheen follows the light rather than the value it had when the weather was last set. Writes only when something in the buffer has moved.</p> <pre><code class="language-lua">weather.refresh() </code></pre> <h2 id="modules-weather-set">modules/weather/set</h2> <pre><code class="language-lua">set(opts: WeatherOpts?): WeatherState </code></pre> <p>Set the scene's weather. Any omitted field keeps its current value, so a call can move one knob without restating the rest. With both <code>wetness</code> and <code>snow</code> at 0 nothing is falling and the pass is released.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>WeatherOpts?</code> <em>(optional)</em> — Weather settings — see <code>WeatherOpts</code>.</li> </ul> <pre><code class="language-lua">weather.set({ wetness = 0.8, snow = 0 }) </code></pre> <h2 id="modules-weather-shelterfield">modules/weather/shelterField</h2> <pre><code class="language-lua">shelterField(): ShelterField </code></pre> <p>Where the cover field the shelter test reads currently stands. A <code>resolution</code> of 0 means no field is baked, and every surface is then open to the sky.</p> <pre><code class="language-lua">print(weather.shelterField().filled, "columns hold a surface") </code></pre> <h2 id="modules-world-defaults-readme">modules/world_defaults/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_defaults") -- world_defaults (also available as global 'world') </code></pre> <p>Grafts the per-world defaults + args + lifecycle-callback registry onto the <code>world</code> global. Companion to <code>world_vcs.module</code>. Backed by <code>.world_settings</code> for the persisted fields; callbacks live in in-process registries. Slot fields exposed on <code>world.*</code> (4 per-mode + 2 mode-agnostic): - <code>avatar_default_edit</code> AssetRef<bundle> (tag: playerAvatar) - <code>avatar_default_play</code> AssetRef<bundle> (tag: playerAvatar) - <code>camera_default_edit</code> AssetRef<component> (tag: cameraBehavior) - <code>camera_default_play</code> AssetRef<component> (tag: cameraBehavior) - <code>startup_scene</code> AssetRef<scene> — engine auto-loads this scene at boot when set - <code>loading_screen</code> AssetRef<loadingScreen>: the screen every boot of the world shows while it loads - <code>orientation</code> "any" | "landscape" | "portrait": the screen orientation the world plays in; <code>installOrientationLock</code> locks the display to it in play mode Per-mode slots are selected by <code>wld.mode()</code> at spawner-resolution time (player_spawner / camera_spawner). Worlds that don't care about the split set both halves to the same value. All setters are play-mode-gated: writes target <code>.world_settings</code> (a source-VFS file) which is locked in play mode. The gate raises a typed error at the API boundary instead of letting the call hit a downstream "VFS write refused" error. Set <code>engine.mode = "edit"</code> first. All bundle-typed setters enforce the slot's required tag (§ 7 of the player-camera-unification plan). Writing a wrongly-tagged ref is refused with a clear message naming the missing tag.</p> <p>Usage: local world_defaults = require("@builtin::modules.world_defaults") Also available as global: world</p> <h2 id="modules-world-defaults-offloaded">modules/world_defaults/offLoaded</h2> <pre><code class="language-lua">offLoaded(handle: number): boolean </code></pre> <p>Stop a callback registered with <code>world.onLoaded</code> from running.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>number</code> — The handle <code>world.onLoaded</code> returned.</li> </ul> <pre><code class="language-lua">world.offLoaded(h) </code></pre> <h2 id="modules-world-defaults-offsaved">modules/world_defaults/offSaved</h2> <pre><code class="language-lua">offSaved(handle: number): boolean </code></pre> <p>Stop a callback registered with <code>world.onSaved</code> from running.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>number</code> — The handle <code>world.onSaved</code> returned.</li> </ul> <pre><code class="language-lua">world.offSaved(h) </code></pre> <h2 id="modules-world-defaults-offunloaded">modules/world_defaults/offUnloaded</h2> <pre><code class="language-lua">offUnloaded(handle: number): boolean </code></pre> <p>Stop a callback registered with <code>world.onUnloaded</code> from running.</p> <p><strong>Parameters</strong></p> <ul> <li><code>handle</code> <code>number</code> — The handle <code>world.onUnloaded</code> returned.</li> </ul> <pre><code class="language-lua">world.offUnloaded(h) </code></pre> <h2 id="modules-world-defaults-onloaded">modules/world_defaults/onLoaded</h2> <pre><code class="language-lua">onLoaded(cb: (...any) -> ()): number </code></pre> <p>Register a callback to run after a world finishes loading.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(...any) -> ()</code> — Called when the event fires, with whatever the event supplies.</li> </ul> <pre><code class="language-lua">local h = world.onLoaded(function() log.info("loaded") end) </code></pre> <h2 id="modules-world-defaults-onsaved">modules/world_defaults/onSaved</h2> <pre><code class="language-lua">onSaved(cb: (...any) -> ()): number </code></pre> <p>Register a callback to run after a world is saved.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(...any) -> ()</code> — Called when the event fires, with whatever the event supplies.</li> </ul> <pre><code class="language-lua">local h = world.onSaved(function() log.info("saved") end) </code></pre> <h2 id="modules-world-defaults-onunloaded">modules/world_defaults/onUnloaded</h2> <pre><code class="language-lua">onUnloaded(cb: (...any) -> ()): number </code></pre> <p>Register a callback to run after a world is unloaded.</p> <p><strong>Parameters</strong></p> <ul> <li><code>cb</code> <code>(...any) -> ()</code> — Called when the event fires, with whatever the event supplies.</li> </ul> <pre><code class="language-lua">local h = world.onUnloaded(function() log.info("unloaded") end) </code></pre> <h2 id="modules-world-defaults-reload">modules/world_defaults/reload</h2> <pre><code class="language-lua">reload(): { started: boolean?, guid: string?, branch: string?, error: string? } </code></pre> <p>Run the bound world again from the top, the way a fresh boot into it would: the world unloads, its content is taken again, its entrypoint and startup scene run. The load is asynchronous; the answer says whether it started. An engine with no world bound answers <code>{ error }</code>.</p> <pre><code class="language-lua">world.reload() </code></pre> <h2 id="modules-world-status-readme">modules/world_status/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_status") -- world_status </code></pre> <p>Composes <code>world.status()</code> + <code>world.status_text()</code> — a snapshot of world identity, mode, running flag, primary scene, all loaded scenes, and players across all scenes. Pure-Luau (no Rust); uses existing globals: world.guid / world.branch / world.title, engine.mode, layers.active, layers.list, scene proxy's players / camera registries, world.connectedUsers.localUser. Installed onto the <code>world</code> global by <code>world_defaults.module/init.luau</code> via the world-namespace metatable machinery (see § 4b of the player-camera-unification integration design).</p> <p>Usage: local world_status = require("@builtin::modules.world_status")</p> <h2 id="dependencies-readme">modules/world_vcs.dependencies/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.dependencies") -- world_vcs.dependencies (also available as global 'world') </code></pre> <p>The dependency copies under <code>/zero/source/deps/</code> and what uses them: which ones nothing in the world uses, removing those on an explicit call, uninstalling an install together with the dependencies only it used, and the notice that says the world holds unused ones. What uses a dependency is the engine's answer (<code>install.dependencyUse</code>): an install record that lists or pins it, a source whose reference table depends on it, and a used dependency that depends on it. Nothing here commits; a removed row of the world leaves as a deletion in the next commit, and a changed install record is the change the next commit carries.</p> <p>Usage: local world_vcs.dependencies = require("@builtin::modules.world_vcs.dependencies") Also available as global: world</p> <h2 id="dependencies-canonical">modules/world_vcs.dependencies/canonical</h2> <pre><code class="language-lua">canonical(path: string): string </code></pre> <p>A path in the <code>/zero/source/...</code> spelling: <code>/source/...</code> and a path relative to <code>/zero/source</code> are taken there, a trailing <code>/</code> dropped.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — A path.</li> </ul> <pre><code class="language-lua">dependencies.canonical("/source/deps/w/a.module") --> "/zero/source/deps/w/a.module" </code></pre> <h2 id="dependencies-installinto">modules/world_vcs.dependencies/installInto</h2> <pre><code class="language-lua">installInto(world: WorldNamespace, opsOf: () -> DependencyOps) </code></pre> <p>Graft <code>uninstall</code> and <code>removeUnusedDeps</code> onto the supplied <code>world</code> namespace and, in the engine, start the unused-dependency notice. <code>world_vcs.installInto</code> calls this once at boot.</p> <p><strong>Parameters</strong></p> <ul> <li><code>world</code> <code>WorldNamespace</code> — The target namespace.</li> <li><code>opsOf</code> <code>() -> DependencyOps</code> — Answers the operations to run through.</li> </ul> <pre><code class="language-lua">require("modules.world_vcs.dependencies").installInto(world, opsOf) </code></pre> <h2 id="dependencies-noticefor">modules/world_vcs.dependencies/noticeFor</h2> <pre><code class="language-lua">noticeFor(unused: { UnusedDependency }, lastKey: string?): (string, boolean, { [string]: any }) </code></pre> <p>The notice state for one look at the world: the unused paths, and whether they are a change from the last notice posted.</p> <p><strong>Parameters</strong></p> <ul> <li><code>unused</code> <code>{ UnusedDependency }</code> — The unused dependencies.</li> <li><code>lastKey</code> <code>string?</code> <em>(optional)</em> — The key of the last notice posted, or nil.</li> </ul> <pre><code class="language-lua">dependencies.noticeFor({}, nil) </code></pre> <h2 id="dependencies-onlypaths">modules/world_vcs.dependencies/onlyPaths</h2> <pre><code class="language-lua">onlyPaths(only: any, fn: string): { string }? </code></pre> <p>The <code>only</code> option as a list of canonical paths: nil, one path, or a list.</p> <p><strong>Parameters</strong></p> <ul> <li><code>only</code> <code>any</code> <em>(optional)</em> — What the caller passed.</li> <li><code>fn</code> <code>string</code> — The calling function's name, for the error.</li> </ul> <pre><code class="language-lua">dependencies.onlyPaths("/source/deps/w", "world.removeUnusedDeps") </code></pre> <h2 id="dependencies-removeunused">modules/world_vcs.dependencies/removeUnused</h2> <pre><code class="language-lua">removeUnused(opts: { dryRun: boolean?, only: any? }?, ops: DependencyOps): RemoveUnusedReport </code></pre> <p>Remove the dependencies under <code>/zero/source/deps/</code> that nothing in the world uses, or with <code>dryRun</code> list them. A dependency an install placed is deleted from that install's record (the record is taken out once it holds nothing); one the world holds as rows of its own is removed as files.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ dryRun: boolean?, only: any? }?</code> <em>(optional)</em> — <code>{ dryRun?, only? }</code>.</li> <li><code>ops</code> <code>DependencyOps</code> — The operations to run through.</li> </ul> <pre><code class="language-lua">dependencies.removeUnused({ dryRun = true }, ops) </code></pre> <h2 id="dependencies-removeunuseddeps">modules/world_vcs.dependencies/removeUnusedDeps</h2> <pre><code class="language-lua">removeUnusedDeps(opts: { dryRun: boolean?, only: any? }?): RemoveUnusedReport </code></pre> <p>Remove the dependency copies under <code>/zero/source/deps/</code> that nothing in this world uses, or with <code>dryRun</code> only list them. A dependency is used when an install record lists or pins it, when a source of the world (its own content or an install's) depends on it by its reference table, or when a used dependency depends on it. A dependency an install placed is deleted from that install's record; one the world holds as files of its own (written by hand, or left by an earlier engine's install) is removed as files. Nothing is committed: removed files leave as deletions in the next commit, and a changed install record is a change the next commit carries. The engine posts a notice whenever the world comes to hold unused dependencies, naming them.</p> <p><strong>Parameters</strong></p> <ul> <li><code>opts</code> <code>{ dryRun: boolean?, only: any? }?</code> <em>(optional)</em> — <code>{ dryRun?: boolean, only?: string | { string } }</code>: <code>only</code> holds the call to the dependencies at or under those paths.</li> </ul> <pre><code class="language-lua">world.removeUnusedDeps({ dryRun = true }) world.removeUnusedDeps() world.removeUnusedDeps({ only = "/zero/source/deps/0d6c9a2e-4b1f-4c3e-9a7d-2f5e8b1c4d6a" }) </code></pre> <h2 id="dependencies-uninstall">modules/world_vcs.dependencies/uninstall</h2> <pre><code class="language-lua">uninstall(path: string, opts: { dryRun: boolean? }?, ops: DependencyOps): UninstallReport </code></pre> <p>Remove the install at <code>path</code> with the dependencies only it used. A dependency something else in the world uses stays and is named with what uses it; a file of the install or of a dependency this world changed or wrote itself stays where it stands as a file of the world.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The install's root.</li> <li><code>opts</code> <code>{ dryRun: boolean? }?</code> <em>(optional)</em> — <code>{ dryRun? }</code>.</li> <li><code>ops</code> <code>DependencyOps</code> — The operations to run through.</li> </ul> <pre><code class="language-lua">dependencies.uninstall("/zero/source/skate_physics.package", { dryRun = true }, ops) </code></pre> <h2 id="dependencies-unused">modules/world_vcs.dependencies/unused</h2> <pre><code class="language-lua">unused(ops: DependencyOps, only: { string }?): { UnusedDependency } </code></pre> <p>The dependencies nothing in the world uses, as the engine answers it, held to <code>only</code> when given: a dependency is kept when its root is one of those paths or lies under one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ops</code> <code>DependencyOps</code> — The operations to read through.</li> <li><code>only</code> <code>{ string }?</code> <em>(optional)</em> — The paths to hold the answer to, or nil.</li> </ul> <pre><code class="language-lua">dependencies.unused(ops, nil) </code></pre> <h2 id="dependencies-watch">modules/world_vcs.dependencies/watch</h2> <pre><code class="language-lua">watch(ops: DependencyOps, ready: () -> boolean) </code></pre> <p>Watch the world for unused dependencies and post a notice when the set of them changes. Reads only: it writes nothing to the world. Looks again only when the engine's dependency stamp moved, every install is materialized and the world's content has arrived. Started once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ops</code> <code>DependencyOps</code> — The operations to read through.</li> <li><code>ready</code> <code>() -> boolean</code> — Whether the world's content has arrived.</li> </ul> <pre><code class="language-lua">dependencies.watch(ops, function() return true end) </code></pre> <h2 id="runner-readme">modules/world_vcs.derive.runner/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.derive.runner") -- world_vcs.derive.runner </code></pre> <p>Turns staged assets into reservations and computes them on workers: a staged asset a derive hook accepts is reserved on its stage in one call as staging returns, then each output is computed on a job-system worker, written beside the asset, and its reservation settled <code>done</code>, which stages the output (or <code>failed</code> with the reason), the settle carrying the fingerprint of the inputs the output was computed from and the generation of the reservation read before those inputs were.</p> <p>Usage: local world_vcs.derive.runner = require("@builtin::modules.world_vcs.derive.runner")</p> <h2 id="runner-grouprows">modules/world_vcs.derive.runner/__groupRows</h2> <pre><code class="language-lua">__groupRows(rows: { any }, roots: { string }): { [string]: { any } } </code></pre> <p>Group staged rows by the asset roots that contain them: one pass over the rows, each looked up against the set of roots on its way up through its folders. A row inside a nested asset belongs to every root above it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>rows</code> <code>{ any }</code> — The staged rows.</li> <li><code>roots</code> <code>{ string }</code> — The asset roots.</li> </ul> <h2 id="runner-includescratchfortests">modules/world_vcs.derive.runner/__includeScratchForTests</h2> <pre><code class="language-lua">__includeScratchForTests(on: boolean) </code></pre> <p>Plan for assets under <code>/zero/source/tmp/tests/</code> as well as for world content, and resolve a staged path in that folder to an asset by its extension when no asset resolves there: the seam suites use to exercise the runner on scratch assets.</p> <p><strong>Parameters</strong></p> <ul> <li><code>on</code> <code>boolean</code> — Whether scratch test assets are included.</li> </ul> <h2 id="runner-sealedbefore">modules/world_vcs.derive.runner/__sealedBefore</h2> <pre><code class="language-lua">__sealedBefore(sealed: { any }, stageId: string): { any } </code></pre> <p>The sealed stages of <code>sealed</code> a commit of the stage <code>stageId</code> lands after, oldest first: every one sealed before it, or every one when the stage is not among them (a stage still taking entries lands last).</p> <p><strong>Parameters</strong></p> <ul> <li><code>sealed</code> <code>{ any }</code> — The caller's sealed stages, each with <code>stage_row_id</code> and <code>sealed_at</code>.</li> <li><code>stageId</code> <code>string</code> — The stage's row id.</li> </ul> <h2 id="runner-setnoticesink">modules/world_vcs.derive.runner/__setNoticeSink</h2> <pre><code class="language-lua">__setNoticeSink(sink: ((string) -> ())?) </code></pre> <p>Replace where the runner's notices go with <code>sink</code>, which receives each message; <code>nil</code> puts the notice store back.</p> <p><strong>Parameters</strong></p> <ul> <li><code>sink</code> <code>((string) -> ())?</code> <em>(optional)</em> — A function taking the notice message, or nil.</li> </ul> <h2 id="runner-settransport">modules/world_vcs.derive.runner/__setTransport</h2> <pre><code class="language-lua">__setTransport(t: { [string]: any }): () -> () </code></pre> <p>Replace the transport the runner reserves, settles, reads and writes through: the seam suites use to observe the reservation calls without a live world. A field the replacement leaves out keeps the default.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>{ [string]: any }</code> — The replacement fields: <code>stageContext</code>, <code>reserve</code>, <code>unavailable</code>, <code>settle</code>, <code>drop</code>, <code>reservations</code>, <code>stageRead</code>, <code>sealed</code>, <code>stageEntries</code>, <code>baseEntries</code>, <code>write</code>, <code>generation</code>, <code>syncOutput</code>, <code>stageAsset</code>, <code>inputsAreLive</code>, <code>outputExists</code>, <code>liveInputs</code>, <code>putBlob</code>.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that puts the previous transport back.</p> <h2 id="runner-carry">modules/world_vcs.derive.runner/carry</h2> <pre><code class="language-lua">carry(stageName: string, stageId: string, rows: { Row }): { carried: number } </code></pre> <p>Reserve on the stage with row id <code>stageId</code> the derived outputs <code>rows</code> that a stage it replaces held, and compute them from what the stage holds: a stage closed and opened again over the branch head keeps every output it reserved, so its commit records each one with the fingerprint of the inputs it lands. An output the user supplied (<code>authored</code>) is taken again as the file stands; a dropped one stays out.</p> <p><strong>Parameters</strong></p> <ul> <li><code>stageName</code> <code>string</code> — The stage's name.</li> <li><code>stageId</code> <code>string</code> — The row id of the stage that replaces the one <code>rows</code> were read from.</li> <li><code>rows</code> <code>{ Row }</code> — The reservations of the replaced stage, as <code>reservationsOf</code> reads them.</li> </ul> <pre><code class="language-lua">runner.carry("default", newStageId, runner.reservationsOf("default", oldStageId)) </code></pre> <h2 id="runner-drop">modules/world_vcs.derive.runner/drop</h2> <pre><code class="language-lua">drop(outputPath: string, stageName: string?): { dropped: number } </code></pre> <p>Leave the derived output at <code>outputPath</code> out of the commit: its reservation is dropped. A required output is refused, except one whose asset holds no input file in the tree its commit lands on nor in the working tree: the commit removes that asset, and its reservation settles <code>empty</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>outputPath</code> <code>string</code> — The output's VFS path.</li> <li><code>stageName</code> <code>string?</code> <em>(optional)</em> — The stage the output is reserved on; the shared stage when omitted.</li> </ul> <pre><code class="language-lua">runner.drop("/zero/source/gold.material/extra.json") </code></pre> <h2 id="runner-onsettled">modules/world_vcs.derive.runner/onSettled</h2> <pre><code class="language-lua">onSettled(name: string, fn: (() -> ())?) </code></pre> <p>Call <code>fn</code> on a task of its own after every settle the backend accepts, <code>done</code>, <code>empty</code> or <code>failed</code>. A name registers once: registering it again replaces what it held, and <code>fn = nil</code> removes it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — What identifies the listener.</li> <li><code>fn</code> <code>(() -> ())?</code> <em>(optional)</em> — The listener, or nil to remove it.</li> </ul> <h2 id="runner-provide">modules/world_vcs.derive.runner/provide</h2> <pre><code class="language-lua">provide(outputPath: string, stageName: string?): { provided: number } </code></pre> <p>Take the file at <code>outputPath</code> as the derived output: the reservation is reserved anew so no computation answers for it, and settled <code>done</code> with the fingerprint <code>"authored"</code>, which stages the file as it stands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>outputPath</code> <code>string</code> — The output's VFS path; a file must exist there.</li> <li><code>stageName</code> <code>string?</code> <em>(optional)</em> — The stage the output is reserved on; the shared stage when omitted.</li> </ul> <pre><code class="language-lua">runner.provide("/zero/source/gold.material/preview.png") </code></pre> <h2 id="runner-reservationsof">modules/world_vcs.derive.runner/reservationsOf</h2> <pre><code class="language-lua">reservationsOf(stageName: string, stageId: string): { Row } </code></pre> <p>The reservations the stage with row id <code>stageId</code> holds now: what a call that replaces the stage hands <code>carry</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>stageName</code> <code>string</code> — The stage's name.</li> <li><code>stageId</code> <code>string</code> — The stage's row id.</li> </ul> <h2 id="runner-reserveandstart">modules/world_vcs.derive.runner/reserveAndStart</h2> <pre><code class="language-lua">reserveAndStart( </code></pre> <p>Reserve, on the stage <code>stageName</code>, the derived outputs of the assets holding <code>stagedPaths</code> in one call, with pending fingerprints, and start computing them on workers. The reservations exist when this returns; each output lands on the stage when it is computed, and its settle records the fingerprint of the inputs it was computed from. The generation of each reservation is read before its inputs are, and the settle answers that generation.</p> <pre><code class="language-lua">runner.reserveAndStart("default", { "/zero/source/gold.material/mat.yaml" }) </code></pre> <h2 id="runner-resume">modules/world_vcs.derive.runner/resume</h2> <pre><code class="language-lua">resume(stageName: string?): { resumed: number } </code></pre> <p>Re-dispatch every <code>computing</code> reservation of the caller's stage and of its sealed stages that this engine is not computing, whichever engine or session reserved it: the work a restart or another session left unfinished. A reservation computes from the staged entries of its stage laid over the tree that stage lands on, a sealed stage's included.</p> <p><strong>Parameters</strong></p> <ul> <li><code>stageName</code> <code>string?</code> <em>(optional)</em> — The stage to resume; the shared stage when omitted.</li> </ul> <pre><code class="language-lua">runner.resume() </code></pre> <h2 id="runner-retry">modules/world_vcs.derive.runner/retry</h2> <pre><code class="language-lua">retry(outputPath: string, stageName: string?): { retried: number } </code></pre> <p>Compute the derived output at <code>outputPath</code> again: its reservation, on the stage or on a sealed commit of the caller, is reserved anew and computed from the inputs as they stand. A reservation that failed, was dropped, or is done is returned to <code>computing</code>. An output no stage of the caller reserves, one already committed, is reserved on the stage with its asset staged beside it, so the next commit carries the new output.</p> <p><strong>Parameters</strong></p> <ul> <li><code>outputPath</code> <code>string</code> — The output's VFS path.</li> <li><code>stageName</code> <code>string?</code> <em>(optional)</em> — The stage the output is reserved on; the shared stage when omitted.</li> </ul> <pre><code class="language-lua">runner.retry("/zero/source/gold.material/preview.png") </code></pre> <h2 id="runner-status">modules/world_vcs.derive.runner/status</h2> <pre><code class="language-lua">status(): { computing: number, preparing: number, failed: number, statusReads: number } </code></pre> <p>How many reservations are being computed, how many staged batches are being prepared, how many failed settles the backend recorded since the engine started, and how many whole-status reads the runner made.</p> <h2 id="snapshot-readme">modules/world_vcs.derive.snapshot/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.derive.snapshot") -- world_vcs.derive.snapshot </code></pre> <p>A read-only view of an asset as a stage holds it, and the one fingerprint rule shared by that view and the live asset: the lines a fingerprint hashes are <code>f <relative path> <content address></code> for every file of the asset that can change a derived output, each followed by <code>d <guid> <version></code> for every asset the file references. A folder asset of the asset's own world is pinned at the version of its inputs (<code>inputsVersion</code>), so a derived output landing in it moves no pin.</p> <p>Usage: local world_vcs.derive.snapshot = require("@builtin::modules.world_vcs.derive.snapshot")</p> <h2 id="snapshot-dependencypaths">modules/world_vcs.derive.snapshot/dependencyPaths</h2> <pre><code class="language-lua">dependencyPaths(assetPath: string, staged: { StagedEntry }, opts: Options?): { string } </code></pre> <p>The paths of the assets of the world that the files of the asset at <code>assetPath</code> reference outside it, as the stage holds them laid over <code>opts.base</code>, sorted: the paths whose files <code>opts.dependencies</code> carries into <code>of</code>, so every pin of the fingerprint states the version of that asset's inputs in the tree the commit lands on.</p> <p><strong>Parameters</strong></p> <ul> <li><code>assetPath</code> <code>string</code> — The asset's path.</li> <li><code>staged</code> <code>{ StagedEntry }</code> — Staged entries of the asset.</li> <li><code>opts</code> <code>Options?</code> <em>(optional)</em> — <code>base</code> entries of the asset in the tree the commit lands on.</li> </ul> <h2 id="snapshot-depsofrefs">modules/world_vcs.derive.snapshot/depsOfRefs</h2> <pre><code class="language-lua">depsOfRefs(refsJson: string?): { Dep } </code></pre> <p>The assets the reference table <code>refsJson</code> pins, as <code>depsOfTable</code> states them; none for a missing or unreadable table. The one reader a staged file's, a committed file's and a live file's dependencies go through.</p> <p><strong>Parameters</strong></p> <ul> <li><code>refsJson</code> <code>string?</code> <em>(optional)</em> — The table as a manifest row carries it.</li> </ul> <h2 id="snapshot-depsoftable">modules/world_vcs.derive.snapshot/depsOfTable</h2> <pre><code class="language-lua">depsOfTable(decoded: any): { Dep } </code></pre> <p>The assets a decoded reference table pins, each as the guid and the version it is pinned at: sorted by guid then version, once each. One rule reads both forms a table takes: the <code>.refs</code> file (<code>deps</code> entries with their <code>sites</code>, and <code>worlds</code> entries whose sites name the assets of another world) and the table <code>asset.deps</code> answers for a live file (one <code>deps</code> entry per naming site). An asset of the file's own world is pinned at the checksum of the entry that covers it; an asset of another world, at the commit that world is pinned at, which an engine-surface site does not have. A pin of an asset of the file's own world carries <code>own = true</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>decoded</code> <code>any</code> <em>(optional)</em> — The decoded table.</li> </ul> <h2 id="snapshot-fingerprintlines">modules/world_vcs.derive.snapshot/fingerprintLines</h2> <pre><code class="language-lua">fingerprintLines(s: derive.DeriveSnapshot): { string } </code></pre> <p>The lines a fingerprint hashes, in order: files by relative path, each followed by the dependencies it references. Empty when the snapshot holds no file.</p> <p><strong>Parameters</strong></p> <ul> <li><code>s</code> <code>derive.DeriveSnapshot</code> — The snapshot.</li> </ul> <h2 id="snapshot-foldersof">modules/world_vcs.derive.snapshot/foldersOf</h2> <pre><code class="language-lua">foldersOf(paths: { [string]: boolean }): { [string]: boolean } </code></pre> <p>The relative paths of <code>paths</code> that are folders of the asset: every path another path of <code>paths</code> lies beneath. A folder asset nested in the asset (a material's <code>textures/basecolor.texture</code>) is an entry of a stage and of a commit beside the files it holds, and it is one of these.</p> <p><strong>Parameters</strong></p> <ul> <li><code>paths</code> <code>{ [string]: boolean }</code> — Relative paths, as a set.</li> </ul> <h2 id="snapshot-inputsversion">modules/world_vcs.derive.snapshot/inputsVersion</h2> <pre><code class="language-lua">inputsVersion(files: { [string]: string }): string? </code></pre> <p>The version a pin of a folder asset of the world states: the digest of the <code>f <relative path> <content address></code> lines of the files beneath it that can change a derived output, the lines a fingerprint of that asset states for its files. A derived output, a README.md or a .metadata landing in the folder leaves it as it is. Nil when the folder holds no such file; a file asset is pinned at its checksum.</p> <p><strong>Parameters</strong></p> <ul> <li><code>files</code> <code>{ [string]: string }</code> — Content address by path relative to the folder.</li> </ul> <h2 id="snapshot-laidover">modules/world_vcs.derive.snapshot/laidOver</h2> <pre><code class="language-lua">laidOver(base: { BaseEntry }, stages: { { StagedEntry } }): { BaseEntry } </code></pre> <p>The files a commit laid over <code>base</code> holds: the entries of <code>base</code> with the entries of each of <code>stages</code> laid over them in order, oldest first, each staged entry taking the place of the base entry at its path and a staged delete removing it. The entries keep the base shape, sorted by path.</p> <p><strong>Parameters</strong></p> <ul> <li><code>base</code> <code>{ BaseEntry }</code> — The tree the commits are laid over, as <code>BaseEntry</code> rows.</li> <li><code>stages</code> <code>{ { StagedEntry } }</code> — The staged entries of each commit, oldest first.</li> </ul> <h2 id="snapshot-live">modules/world_vcs.derive.snapshot/live</h2> <pre><code class="language-lua">live(path: string, opts: Options?): derive.DeriveSnapshot </code></pre> <p>The snapshot of the asset at <code>path</code> over its live files: each file's address is the VFS content address and its dependencies are the assets it references now, an asset of the world pinned at the <code>inputsVersion</code> of its live files. The files <code>opts.exclude</code> names are left out.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The asset's path.</li> <li><code>opts</code> <code>Options?</code> <em>(optional)</em> — <code>exclude</code> relative paths.</li> </ul> <h2 id="snapshot-liveaddresses">modules/world_vcs.derive.snapshot/liveAddresses</h2> <pre><code class="language-lua">liveAddresses(path: string, opts: Options?): { [string]: string } </code></pre> <p>The content address of each live file of the asset at <code>path</code>, by path relative to the asset: the files a live snapshot of it holds, without the dependencies they reference. The files <code>opts.exclude</code> names are left out.</p> <p><strong>Parameters</strong></p> <ul> <li><code>path</code> <code>string</code> — The asset's path.</li> <li><code>opts</code> <code>Options?</code> <em>(optional)</em> — <code>exclude</code> relative paths.</li> </ul> <h2 id="snapshot-of">modules/world_vcs.derive.snapshot/of</h2> <pre><code class="language-lua">of(assetPath: string, staged: { StagedEntry }, opts: Options?): derive.DeriveSnapshot </code></pre> <p>The snapshot of the asset at <code>assetPath</code> as a stage holds it: the files of the tree its commit lands on (<code>opts.base</code>) with the staged entries laid over them. <code>files</code> maps each file's path relative to the asset to its content address; <code>read</code> returns a file's bytes, fetched from its blob on first use and kept on the snapshot, or nil when the file is absent or its blob cannot be read. A staged delete removes the file; the files <code>opts.exclude</code> names are left out, and so is every entry that holds other entries: a folder asset nested in the asset is the files beneath it. A pin of an asset of the world states the <code>inputsVersion</code> of that asset's files, read from the same entries when the asset lies within this one and from <code>opts.dependencies</code> when it lies outside; an asset with no files there is pinned at the checksum its reference table states.</p> <p><strong>Parameters</strong></p> <ul> <li><code>assetPath</code> <code>string</code> — The asset's path.</li> <li><code>staged</code> <code>{ StagedEntry }</code> — Staged entries: path, checksum, blob_sha256 and the dependencies the file references.</li> <li><code>opts</code> <code>Options?</code> <em>(optional)</em> — <code>base</code> entries of the asset in the tree the commit lands on, <code>exclude</code> relative paths, and <code>dependencies</code> by path.</li> </ul> <h2 id="snapshot-outputsexcluded">modules/world_vcs.derive.snapshot/outputsExcluded</h2> <pre><code class="language-lua">outputsExcluded(): { [string]: boolean } </code></pre> <p>The relative paths no fingerprint reads: the output of every asset-scope hook, the ones asset types export included. An output is computed from the asset's inputs, so another hook's output landing beside it never changes what it was computed from. The one exclusion set the runner and the push gate share.</p> <h2 id="worker-readme">modules/world_vcs.derive.worker/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.derive.worker") -- world_vcs.derive.worker </code></pre> <p>Worker entries for derive hooks: each runs one entry of a hook's module on a worker VM, over a snapshot handed across as data.</p> <p>Usage: local world_vcs.derive.worker = require("@builtin::modules.world_vcs.derive.worker")</p> <h2 id="worker-compute">modules/world_vcs.derive.worker/compute</h2> <pre><code class="language-lua">compute(args: any): any </code></pre> <p>The result <code>args.hook.compute</code> of <code>args.hook.module</code> computes for <code>args.snapshot</code>: <code>{ bytes: string?, error: string? }</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>args</code> <code>any</code> <em>(optional)</em> — <code>{ hook, snapshot }</code>: the hook without its functions, and the snapshot as data.</li> </ul> <h2 id="worker-fingerprint">modules/world_vcs.derive.worker/fingerprint</h2> <pre><code class="language-lua">fingerprint(args: any): string </code></pre> <p>The fingerprint <code>args.hook.fingerprint</code> of <code>args.hook.module</code> states for <code>args.snapshot</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>args</code> <code>any</code> <em>(optional)</em> — <code>{ hook, snapshot }</code>: the hook without its functions, and the snapshot as data.</li> </ul> <h2 id="derive-readme">modules/world_vcs.derive/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.derive") -- world_vcs.derive </code></pre> <p>The derive-hook registry: computations that produce an output from the content a stage holds, so the output lands in the commit with it.</p> <p>Usage: local world_vcs.derive = require("@builtin::modules.world_vcs.derive")</p> <h2 id="derive-setincludescratch">modules/world_vcs.derive/__setIncludeScratch</h2> <pre><code class="language-lua">__setIncludeScratch(on: boolean) </code></pre> <p>Plan for assets under <code>/zero/source/tmp/tests/</code> as well as for world content: the seam suites use to exercise planning on scratch assets.</p> <p><strong>Parameters</strong></p> <ul> <li><code>on</code> <code>boolean</code> — Whether scratch test assets are planned for.</li> </ul> <h2 id="derive-allhooks">modules/world_vcs.derive/allHooks</h2> <pre><code class="language-lua">allHooks(): { DeriveHook } </code></pre> <p>Every hook: the registered ones and the one each asset type's <code>behavior.luau</code> exports as <code>derive</code>, in name order. A registered hook stands over a type's export of the same name. The set an output path is looked up in: the outputs left out of an asset's inputs, and the hook a retry of a committed output reserves it for.</p> <h2 id="setbytelimit">modules/world_vcs.derive/cache.__setByteLimit</h2> <pre><code class="language-lua">cache.__setByteLimit(limit: number): () -> () </code></pre> <p>Replace the byte bound of the cache, dropping entries beyond it: the seam suites use to exercise the bound with small results.</p> <p><strong>Parameters</strong></p> <ul> <li><code>limit</code> <code>number</code> — The new bound in bytes.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that puts the previous bound back.</p> <h2 id="bytes">modules/world_vcs.derive/cache.bytes</h2> <pre><code class="language-lua">cache.bytes(): number </code></pre> <p>How many bytes the cache holds.</p> <h2 id="get">modules/world_vcs.derive/cache.get</h2> <pre><code class="language-lua">cache.get(hook: string, fingerprint: string): string? </code></pre> <p>The cached result of <code>hook</code> for <code>fingerprint</code>, refreshing its recency.</p> <p><strong>Parameters</strong></p> <ul> <li><code>hook</code> <code>string</code> — The hook's name.</li> <li><code>fingerprint</code> <code>string</code> — The fingerprint the result was computed for.</li> </ul> <h2 id="put">modules/world_vcs.derive/cache.put</h2> <pre><code class="language-lua">cache.put(hook: string, fingerprint: string, bytes: string) </code></pre> <p>Keep the result of <code>hook</code> for <code>fingerprint</code>; the least recently used entries leave when more than 64 are held or their bytes exceed 32 MiB. A result larger than 32 MiB is not kept.</p> <p><strong>Parameters</strong></p> <ul> <li><code>hook</code> <code>string</code> — The hook's name.</li> <li><code>fingerprint</code> <code>string</code> — The fingerprint the result was computed for.</li> <li><code>bytes</code> <code>string</code> — The result.</li> </ul> <h2 id="derive-hooknamed">modules/world_vcs.derive/hookNamed</h2> <pre><code class="language-lua">hookNamed(name: string): DeriveHook? </code></pre> <p>The hook named <code>name</code>: a registered one, or the one an asset type's <code>behavior.luau</code> exports as <code>derive</code> when <code>name</code> is <code><type>.<output></code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The hook's name.</li> </ul> <h2 id="derive-hooks">modules/world_vcs.derive/hooks</h2> <pre><code class="language-lua">hooks(): { DeriveHook } </code></pre> <p>Every registered hook, in name order.</p> <h2 id="derive-planfor">modules/world_vcs.derive/planFor</h2> <pre><code class="language-lua">planFor(refs: { any }): { { hook: DeriveHook, ref: any } } </code></pre> <p>The reservations a set of staged assets calls for: one per asset and asset-scope hook whose <code>matches</code> accepts it, in hook-name then path order.</p> <p><strong>Parameters</strong></p> <ul> <li><code>refs</code> <code>{ any }</code> — The staged assets.</li> </ul> <h2 id="derive-register">modules/world_vcs.derive/register</h2> <pre><code class="language-lua">register(hook: DeriveHook) </code></pre> <p>Register a derive hook, replacing one of the same name.</p> <p><strong>Parameters</strong></p> <ul> <li><code>hook</code> <code>DeriveHook</code> — The hook.</li> </ul> <h2 id="derive-unregister">modules/world_vcs.derive/unregister</h2> <pre><code class="language-lua">unregister(name: string) </code></pre> <p>Remove the hook registered under <code>name</code>.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — The hook's name.</li> </ul> <h2 id="derivedgate-readme">modules/world_vcs.derivedGate/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.derivedGate") -- world_vcs.derivedGate </code></pre> <p>The push gate's freshness class for derived outputs: an output a commit records as computed by a hook must still match the inputs it was computed from. Each output's recorded fingerprint is compared with the fingerprint its hook states over the branch head's tree, so a source committed after its output was computed names the output as stale. A pin of an asset of the world states the version of that asset's inputs in the same tree. An output recorded as <code>authored</code> is the user's and always passes.</p> <p>Usage: local world_vcs.derivedGate = require("@builtin::modules.world_vcs.derivedGate")</p> <h2 id="derivedgate-check">modules/world_vcs.derivedGate/check</h2> <pre><code class="language-lua">check(): Verdict </code></pre> <p>Judge the derived outputs of the unpushed commits. Each output path takes the newest recorded row; one the head tree still holds is compared with the fingerprint its hook states over the files of its asset in the head tree. Only the derived-output rows of the unpushed commits, the files of the assets they name and the files of the assets of the world those reference are read. A required output that differs becomes an item of the returned section; a best-effort one is returned as a warning, once per output and recorded fingerprint. Nothing is judged when the backend does not serve derived outputs, the history cannot be read, or a hook is not registered here.</p> <pre><code class="language-lua">local verdict = derivedGate.check() </code></pre> <h2 id="finalize-readme">modules/world_vcs.finalize/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.finalize") -- world_vcs.finalize </code></pre> <p>Lands the commits <code>world.commit</code> sealed: a sealed stage whose required derived outputs are all <code>done</code> is committed under its sealed commit id, oldest first, and a commit that is not ready holds the ones sealed after it. A stage the branch head moved away from is rebased in place and committed again. A push asked for while commits are sealed is queued on the newest one and runs once it lands, and its outcome is recorded for the caller waiting on it and for <code>zm status</code>.</p> <p>Usage: local world_vcs.finalize = require("@builtin::modules.world_vcs.finalize")</p> <h2 id="finalize-settransport">modules/world_vcs.finalize/__setTransport</h2> <pre><code class="language-lua">__setTransport(t: { [string]: any }): () -> () </code></pre> <p>Replace the calls the finalizer reads, commits, rebases and tells the caller through: the seam suites use to run it without a live world. A field the replacement leaves out keeps the default. A watch running over the transport being replaced ends, here and when the previous one is put back, so it never lands commits through a transport it did not start with.</p> <p><strong>Parameters</strong></p> <ul> <li><code>t</code> <code>{ [string]: any }</code> — The replacement fields: <code>status</code>, <code>commit</code>, <code>commitExists</code>, <code>rebase</code>, <code>unseal</code>, <code>setPushOnLand</code>, <code>push</code>, <code>notify</code>.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that puts the previous transport back.</p> <h2 id="finalize-awaitpush">modules/world_vcs.finalize/awaitPush</h2> <pre><code class="language-lua">awaitPush(after: number, waitingOn: { string }, seconds: number): PushOutcome </code></pre> <p>Wait for the queued push that runs after push number <code>after</code> (read with <code>pushSeq</code> before queueing it), for at most <code>seconds</code>. A sealed commit it waits for that cannot land holds it: the refusal the commit met, or a required output whose computation failed, ends the wait. The watch that lands the commits keeps running whatever this answers.</p> <p><strong>Parameters</strong></p> <ul> <li><code>after</code> <code>number</code> — The <code>pushSeq</code> read before the push was queued.</li> <li><code>waitingOn</code> <code>{ string }</code> — The sealed commit ids the push waits for, as <code>queuePush</code> answered them.</li> <li><code>seconds</code> <code>number</code> — How long to wait.</li> </ul> <h2 id="finalize-clearlastpush">modules/world_vcs.finalize/clearLastPush</h2> <pre><code class="language-lua">clearLastPush() </code></pre> <p>Forget the outcome of the last queued push: a push that published since answers for the branch.</p> <h2 id="finalize-kick">modules/world_vcs.finalize/kick</h2> <pre><code class="language-lua">kick() </code></pre> <p>Run <code>pass</code> on a task of its own and return at once.</p> <h2 id="finalize-landsealed">modules/world_vcs.finalize/landSealed</h2> <pre><code class="language-lua">landSealed(commitId: string): (boolean, string?, string?) </code></pre> <p>Land the sealed commit <code>commitId</code> when every required output of it is <code>done</code>, waiting for it; otherwise queue it behind its outputs.</p> <p><strong>Parameters</strong></p> <ul> <li><code>commitId</code> <code>string</code> — The sealed commit's id.</li> </ul> <h2 id="finalize-lastpush">modules/world_vcs.finalize/lastPush</h2> <pre><code class="language-lua">lastPush(): PushRecord? </code></pre> <p>The outcome of the last queued push this engine ran, or nil when none has run or a push published since. <code>zm status</code> states a refused one.</p> <h2 id="finalize-onlanded">modules/world_vcs.finalize/onLanded</h2> <pre><code class="language-lua">onLanded(fn: (commitId: string) -> ()): () -> () </code></pre> <p>Call <code>fn(commitId)</code> whenever a sealed commit lands.</p> <p><strong>Parameters</strong></p> <ul> <li><code>fn</code> <code>(commitId: string) -> ()</code> — The listener.</li> </ul> <p><strong>Returns</strong> <code>()</code> — A function that stops the calls.</p> <h2 id="finalize-pass">modules/world_vcs.finalize/pass</h2> <pre><code class="language-lua">pass(quietFor: string?): Result </code></pre> <p>Land the caller's sealed commits that are ready, oldest first, and wait for each commit it lands. It stops at the first sealed commit that is not ready (a required output not <code>done</code>) or that cannot land, so later commits queue behind it. A commit call that the connection fails is made again after a wait; a commit that exists once its call was refused was landed by another engine of the account and counts as landed. Only a refusal of the commit itself raises a notice.</p> <p><strong>Parameters</strong></p> <ul> <li><code>quietFor</code> <code>string?</code> <em>(optional)</em> — A commit id whose refusal is recorded without a notice: the caller waiting on that commit raises it itself.</li> </ul> <h2 id="finalize-pushseq">modules/world_vcs.finalize/pushSeq</h2> <pre><code class="language-lua">pushSeq(): number </code></pre> <p>How many queued pushes this engine has run: a caller reads it before queueing a push and hands it to <code>awaitPush</code>, which answers the first push that ran after it.</p> <h2 id="finalize-queuepush">modules/world_vcs.finalize/queuePush</h2> <pre><code class="language-lua">queuePush(): ({ string }?, { commitId: string, reason: string }?) </code></pre> <p>Queue a push behind the caller's sealed commits: the ready ones land first, then the newest one left is marked to push once it lands, and a push that finds nothing sealed is left to the caller. A sealed commit that cannot land holds every push behind it, so when the oldest one left holds a refusal the push is not queued and the refusal is answered instead.</p> <h2 id="finalize-refusals">modules/world_vcs.finalize/refusals</h2> <pre><code class="language-lua">refusals(): { [string]: string } </code></pre> <p>The refusal this engine holds for each sealed commit that could not land, by commit id: what <code>zm status</code> states beside the commit.</p> <h2 id="finalize-sealedids">modules/world_vcs.finalize/sealedIds</h2> <pre><code class="language-lua">sealedIds(): { string } </code></pre> <p>The ids of the caller's sealed commits, oldest first.</p> <h2 id="finalize-unseal">modules/world_vcs.finalize/unseal</h2> <pre><code class="language-lua">unseal(commitId: string?): string </code></pre> <p>Return the sealed commit <code>commitId</code> to the staging area it was sealed from. Without an id, the oldest sealed commit that holds a failure, else the newest.</p> <p><strong>Parameters</strong></p> <ul> <li><code>commitId</code> <code>string?</code> <em>(optional)</em> — The sealed commit's id.</li> </ul> <h2 id="finalize-watch">modules/world_vcs.finalize/watch</h2> <pre><code class="language-lua">watch(resume: (() -> ())?) </code></pre> <p>Land the sealed commits as they become ready until none is sealed: a pass every 2 seconds (every 4, 8 and up to 30 seconds while the oldest sealed commit holds a refusal), which also picks up an output another session settled. With <code>resume</code>, call it once the backend shows reservations or sealed commits (and every 10 seconds while a commit stays sealed), or give up after a minute with none. A status read that fails is logged and read again, so a watch over sealed commits keeps watching. A call made while the watch runs wakes it for a pass at once.</p> <p><strong>Parameters</strong></p> <ul> <li><code>resume</code> <code>(() -> ())?</code> <em>(optional)</em> — Computes the <code>computing</code> reservations nothing in this engine is computing.</li> </ul> <h2 id="history-readme">modules/world_vcs.history/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.history") -- world_vcs.history </code></pre> <p>The reads <code>world.diff</code>, <code>world.show</code>, <code>world.log</code> and the checkout and merge calls build on: a file's content at a commit, in the staging area or in the working tree, the paths two sources differ on and the per-file diff between them, a commit's ancestry and full tree, the history of one path, and the commit a git-style revision names.</p> <p>Usage: local world_vcs.history = require("@builtin::modules.world_vcs.history")</p> <h2 id="history-bind">modules/world_vcs.history/bind</h2> <pre><code class="language-lua">bind(worldVcs: any, stageNameOfCaller: () -> string, workingSet: string) </code></pre> <p>Bind these reads to the world_vcs module <code>worldVcs</code> they run under.</p> <p><strong>Parameters</strong></p> <ul> <li><code>worldVcs</code> <code>any</code> <em>(optional)</em> — The world_vcs module table.</li> <li><code>stageNameOfCaller</code> <code>() -> string</code> — Answers the staging area a call from the calling caller acts on.</li> <li><code>workingSet</code> <code>string</code> — The area a status read names to read only the working set.</li> </ul> <h2 id="history-diffcandidatepaths">modules/world_vcs.history/diffCandidatePaths</h2> <pre><code class="language-lua">diffCandidatePaths(status: any, set: Ignore.IgnoreSet, includeIgnored: boolean): { string } </code></pre> <p>The paths a status read names for a diff to compare: every row the staging area holds, and every working-tree row but the ones a <code>.zmignore</code> / <code>.gitignore</code> rule matches, which <code>includeIgnored</code> names too. A row the area holds is named whatever rule matches it, since it goes into the next commit. A scene's autosave overlay row stands for its scene's <code>scene.json</code>, which the same rules decide, and an ignore file itself is always named.</p> <p><strong>Parameters</strong></p> <ul> <li><code>status</code> <code>any</code> <em>(optional)</em> — The decoded status read: its <code>dirty</code> and <code>staged</code> rows.</li> <li><code>set</code> <code>Ignore.IgnoreSet</code> — The ignore set the world's ignore files state.</li> <li><code>includeIgnored</code> <code>boolean</code> — Whether the working-tree rows a rule matches are named.</li> </ul> <h2 id="ignore-readme">modules/world_vcs.ignore/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.ignore") -- world_vcs.ignore </code></pre> <p>The <code>.zmignore</code> / <code>.gitignore</code> rules of a world: parses the ignore files in gitignore(5) syntax into one ignore set and decides whether a path matches it. The world's staging gates read it through world_vcs.</p> <p>Usage: local world_vcs.ignore = require("@builtin::modules.world_vcs.ignore")</p> <h2 id="ignore-isignored">modules/world_vcs.ignore/isIgnored</h2> <pre><code class="language-lua">isIgnored(set: IgnoreSet, path: string): boolean </code></pre> <p>Whether the ignore set ignores <code>path</code> (<code>/source/...</code> or <code>/zero/source/...</code>): the last rule that matches it is not a <code>!</code> negation.</p> <p><strong>Parameters</strong></p> <ul> <li><code>set</code> <code>IgnoreSet</code> — The ignore set <code>setFrom</code> answered.</li> <li><code>path</code> <code>string</code> — The path to decide.</li> </ul> <pre><code class="language-lua">print(Ignore.isIgnored(set, "/source/scene_dirty/entities/a.json")) </code></pre> <h2 id="ignore-rulefor">modules/world_vcs.ignore/ruleFor</h2> <pre><code class="language-lua">ruleFor(set: IgnoreSet, path: string): IgnoreRule? </code></pre> <p>The rule that ignores <code>path</code>: the last rule of the set that matches it, when that rule is not a <code>!</code> negation. Rules are walked in load order, so a deeper ignore file overrides its parents and a later line an earlier one.</p> <p><strong>Parameters</strong></p> <ul> <li><code>set</code> <code>IgnoreSet</code> — The ignore set <code>setFrom</code> answered.</li> <li><code>path</code> <code>string</code> — The path to decide, as <code>/source/...</code> or <code>/zero/source/...</code>.</li> </ul> <pre><code class="language-lua">local why = Ignore.ruleFor(set, "/zero/source/generated/runs/a.serviceRun/run.json") </code></pre> <h2 id="ignore-setfrom">modules/world_vcs.ignore/setFrom</h2> <pre><code class="language-lua">setFrom(ignorePaths: { string }, read: (string) -> any): IgnoreSet </code></pre> <p>The ignore set the <code>.zmignore</code> / <code>.gitignore</code> files at <code>ignorePaths</code> state, each file read through <code>read</code> and parsed fresh, so an edit to an existing ignore file is always reflected. O(ignore files): no <code>/source</code> tree walk.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ignorePaths</code> <code>{ string }</code> — The ignore-file paths, as the engine's index lists them.</li> <li><code>read</code> <code>(string) -> any</code> — Reads a file's bytes by path.</li> </ul> <h2 id="publishreminders-readme">modules/world_vcs.publishReminders/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.publishReminders") -- world_vcs.publishReminders </code></pre> <p>Tells the agents building in a world, without being asked, what keeps their work from being published: changes not committed or commits not pushed, a world still marked as a draft, and a world with no description or no cover image. Each is an engine notice addressed to every caller, naming the counts or the missing fields and the call that puts it right, and each is withdrawn once the state it reports is gone. When each one is posted: * Unpublished work, at intervals while an agent is at work: once the world has held uncommitted changes or unpushed commits for <code>UNSAVED_INTERVAL_S</code>, and again each <code>UNSAVED_INTERVAL_S</code> after that, only when the counts moved since the last one. An agent that stopped working is not told again about work that has not changed. * A draft world, and a missing description or cover image, while an agent is at work on a world whose work ZeroMind holds: after the push that leaves either state standing, so the agent that just published hears what is left of publishing within <code>STATUS_S</code>, unless the last turn end already told it the same. * At the end of an agent's turn, every notice whose state holds then: unpublished work, a draft world, a missing description or cover image. A turn end tells a notice again only when what it reports changed since the turn end that last read it: new counts of unpublished work, another intended visibility, another set of missing fields. An agent that leaves a world as it was (a person asked it to keep the world a draft) is not told again, however much other work it does. * A change to a notice a turn end already told, while the notice stood since (more commits held back on a person's word, a count the engine moved, one missing field of two set), is held for the agent's next work and posted then, read again, without waking anyone: DotBot hears it with the model request after its next tool call, inside the turn that makes it, and an MCP caller with its next tool call. Once no agent has been at work for <code>UNSAVED_INTERVAL_S</code>, the reminders read neither the world's status nor its ZeroMind record until one is again: an idle editor costs ZeroMind nothing. A turn ends, as the engine sees it, when DotBot reports a turn of its as completed (a stopped or failed turn is not one), and for an agent driving the engine over MCP when its tool calls go quiet for <code>QUIET_S</code> after one: an MCP caller states no turn boundary, so a pause in the calls made on the engine, over any transport, is the one the engine reads. DotBot's calls are not among them: the calls its host makes between turns to read the notices are no agent at work. While a DotBot turn is open its own events say where it ends, so the calls it makes and the pauses between them (a long reply being written) end no turn. A notice posted at a turn end is actionable, so an idle agent whose host wakes it for one is woken to act on it, and an MCP caller reads it on its next tool call. Every other notice is posted at a moment an agent at work reads what is posted with what it does next: an MCP caller's tool call, and a tool call of an open DotBot turn, after which DotBot makes another model request and reads the notices (<code>driver</code>). So it rides that work: a host that wakes an idle agent for any notice that lands is woken only by a turn end's first word on a state. The uncommitted count is what <code>zm status</code> reads as changes to commit: a file listed only because its references moved (its bytes are the branch head's) is not counted (<code>uncommittedOf</code>). The reminders of each world are kept while the engine runs, so a world swapped away from and back to is not told again what it was told.</p> <p>Usage: local world_vcs.publishReminders = require("@builtin::modules.world_vcs.publishReminders")</p> <h2 id="publishreminders-driver">modules/world_vcs.publishReminders/driver</h2> <pre><code class="language-lua">driver(worlds: Worlds, turns: Turns): Driver </code></pre> <p>The live driver's two entries, over the reminders of each world (<code>worlds</code>) and where agents' turns end (<code>turns</code>): <code>poll(guid, newestCall, now, epochS)</code> is one read, every <code>TICK_S</code>, of who is at work on the world <code>guid</code>, and <code>dotbot(guid, event, now)</code> takes one of DotBot's turn events. An agent at work hears what the reminders post with what it does next: an MCP caller with its next tool call, and DotBot with the model request that follows a tool call of its, which reads the notices. So while a DotBot turn is open the reminders speak at its tool calls (<code>tool_start</code>), and a poll then posts nothing: a post at another moment of the turn can land after the turn's last model request, where DotBot's host starts a turn of its own for it. The world's status is read at most every <code>STATUS_S</code>, by either entry.</p> <p><strong>Parameters</strong></p> <ul> <li><code>worlds</code> <code>Worlds</code> — The reminders of each world (<code>worlds</code>).</li> <li><code>turns</code> <code>Turns</code> — Where agents' turns end (<code>turns</code>).</li> </ul> <h2 id="publishreminders-engineops">modules/world_vcs.publishReminders/engineOps</h2> <pre><code class="language-lua">engineOps(): Ops </code></pre> <p>The reminders' operations over this engine: the session branch's unpublished counts from <code>world.vcsStatus</code> (<code>uncommittedOf</code>) and the commits ZeroMind does not hold, the world record from ZeroMind, and notices addressed to every caller, each read by a person as its catalog key.</p> <h2 id="publishreminders-new">modules/world_vcs.publishReminders/new</h2> <pre><code class="language-lua">new(ops: Ops): Reminders </code></pre> <p>Reminders over <code>ops</code>: <code>activity()</code> says an agent is at work now and hands it what a turn end held for it, <code>idle()</code> says no agent is at work now, <code>tick()</code> runs the rules that hold while an agent works, and <code>turnEnded()</code> runs the turn-end rule for every notice.</p> <p><strong>Parameters</strong></p> <ul> <li><code>ops</code> <code>Ops</code> — <code>{ now, counts, presentation, post, withdraw }</code>.</li> </ul> <h2 id="publishreminders-presentationof">modules/world_vcs.publishReminders/presentationOf</h2> <pre><code class="language-lua">presentationOf(record: any): Presentation? </code></pre> <p>What a world record states about the world's release: whether it is a draft and who it is published to once it is not, whether it holds a description and a cover image, and whether the reader can edit it.</p> <p><strong>Parameters</strong></p> <ul> <li><code>record</code> <code>any</code> <em>(optional)</em> — A world record as <code>zeromind.world</code>'s <code>getWorld</code> answers it.</li> </ul> <h2 id="publishreminders-start">modules/world_vcs.publishReminders/start</h2> <pre><code class="language-lua">start() </code></pre> <p>Start the reminders over the live engine, once per engine: a task reads whether anybody is at work every <code>TICK_S</code> and the world's unpublished counts every <code>STATUS_S</code> while an agent works, and DotBot's turn events and the quiet of the engine's tool calls end turns (<code>driver</code>). Called as a world becomes ready.</p> <pre><code class="language-lua">require("modules.world_vcs.publishReminders").start() </code></pre> <h2 id="publishreminders-turns">modules/world_vcs.publishReminders/turns</h2> <pre><code class="language-lua">turns(): Turns </code></pre> <p>Where agents' turns end, from what the engine sees of them: <code>dotbot(event, now)</code> takes one of DotBot's turn events and answers whether it starts a tool call (<code>tool_start</code>), after which the turn makes another model request and DotBot reads the notices before it, and <code>poll(newestCall, now, epochS)</code> takes the newest tool call a caller that states no turns of its own made (milliseconds since the Unix epoch, <code>__presence.newestTurnlessCall</code>: every caller but DotBot, whose own events say where its turns are and whose host calls the engine between them, reading the notices it might wake on) and answers whether an agent is at work now, whether a turn ended since the last poll, and whether the work is a DotBot turn that is open. A DotBot turn ends at its <code>end</code> event whose reason is <code>completed</code>; any other turn ends once the tool calls go quiet for <code>QUIET_S</code> after one. While a DotBot turn is open (an event of its within <code>TURN_OPEN_S</code>, no <code>end</code> yet) the calls and the quiet are DotBot's, and end no turn. Once DotBot has worked here, it sits between turns whenever no turn of its is open, and its host starts a turn for any notice that lands then: while it does, another caller's calls end that caller's turns but read as no agent at work, so nothing but a turn end posts.</p> <h2 id="publishreminders-uncommittedof">modules/world_vcs.publishReminders/uncommittedOf</h2> <pre><code class="language-lua">uncommittedOf(status: any): number </code></pre> <p>How many changes a world's status holds for a commit to take: each path its staging area or working tree holds changed, counted once, and each installed asset whose rows differ from the branch head. A file listed only because its references moved (<code>reason = "references moved"</code>: its bytes are the ones the branch head holds) is left out, as <code>zm status</code> leaves it out when it reads a working tree with nothing to commit.</p> <p><strong>Parameters</strong></p> <ul> <li><code>status</code> <code>any</code> <em>(optional)</em> — A status as <code>world.vcsStatus</code> answers it.</li> </ul> <pre><code class="language-lua">require("@builtin::modules.world_vcs.publishReminders").uncommittedOf(world.vcsStatus()) </code></pre> <h2 id="publishreminders-worlds">modules/world_vcs.publishReminders/worlds</h2> <pre><code class="language-lua">worlds(make: () -> Reminders): Worlds </code></pre> <p>The reminders of each world an engine authors, made by <code>make</code> the first time a world is asked for and kept for as long as the engine runs: a world swapped away from and back to keeps what its reminders told.</p> <p><strong>Parameters</strong></p> <ul> <li><code>make</code> <code>() -> Reminders</code> — The reminders for a world, made once per world guid.</li> </ul> <h2 id="sceneedits-readme">modules/world_vcs.sceneEdits/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.sceneEdits") -- world_vcs.sceneEdits </code></pre> <p>The live scene edits the world's publication calls are judged on: a scene's runtime dirty overlay and the paths that name it, the edits an overlay holds that the saved scene file does not carry, the notice that names the ones a call left out, and the refusal a call raises over them. Reads the scene saver, <code>assetTypes.scene.shared.saver</code>, where it loads.</p> <p>Usage: local world_vcs.sceneEdits = require("@builtin::modules.world_vcs.sceneEdits")</p> <h2 id="statusread-readme">modules/world_vcs.statusRead/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.statusRead") -- world_vcs.statusRead </code></pre> <p>Reads the derived-output part of a staging-area status payload: the reservations of the named stage and of the caller's sealed stages, the sealed commits with what each waits on, and the capabilities the backend lacks.</p> <p>Usage: local world_vcs.statusRead = require("@builtin::modules.world_vcs.statusRead")</p> <h2 id="statusread-derivedview">modules/world_vcs.statusRead/derivedView</h2> <pre><code class="language-lua">derivedView(decoded: any): DerivedView </code></pre> <p>The derived-output view of a decoded status payload: <code>derived</code> holds the named stage's reservations and then each sealed stage's (with <code>sealedIn</code> the commit id), <code>sealed</code> each sealed commit in <code>sealedAt</code> order with the required outputs neither <code>done</code> nor <code>empty</code> as <code>waitingOn</code> and the <code>failed</code> ones with their error, and <code>unavailable</code> the refusal text of each capability the backend lacks.</p> <p><strong>Parameters</strong></p> <ul> <li><code>decoded</code> <code>any</code> <em>(optional)</em> — The decoded <code>__spacetime_vcs.status</code> payload.</li> </ul> <h2 id="statusread-statuslines">modules/world_vcs.statusRead/statusLines</h2> <pre><code class="language-lua">statusLines(status: any): { string } </code></pre> <p>The lines <code>zm status</code> prints for a status table's computed outputs and sealed commits, in the order printed: a blank line and the group title, then one line per <code>computing</code> or <code>failed</code> output (a <code>computing</code> output whose hook is not registered in this engine says so and names <code>zm derived provide</code> and <code>zm unseal</code> as the ways out), and the same for each sealed commit with what it waits on or the output that failed. A sealed commit the backend refused to land prints the refusal under it with the <code>zm unseal</code> that returns it, and the commits sealed after it read as queued behind it. A group with nothing to list prints nothing, and a sealed commit that publishes once it lands says so on a line of its own. A push queued behind sealed commits that ZeroMind refused once it ran prints the refusal under a title of its own.</p> <p><strong>Parameters</strong></p> <ul> <li><code>status</code> <code>any</code> <em>(optional)</em> — A table with <code>derived</code>, <code>sealed</code> and <code>queued_push</code> as <code>world.vcsStatus</code> returns them; a sealed commit's <code>refusal</code> is the backend's answer when it could not land.</li> </ul> <pre><code class="language-lua">for _, line in ipairs(statusRead.statusLines(world.vcsStatus())) do print(line) end </code></pre> <h2 id="statusread-updateline">modules/world_vcs.statusRead/updateLine</h2> <pre><code class="language-lua">updateLine(u: any): string </code></pre> <p>The one line <code>zm status</code> and <code>zm diff</code> print for a library update the working tree holds: the library, the commit the head pins it at and the one the working tree does, how many files it repins, and how many files holding edits of their own it reaches (each listed on its own as well).</p> <p><strong>Parameters</strong></p> <ul> <li><code>u</code> <code>any</code> <em>(optional)</em> — A <code>PinUpdate</code> as <code>world.vcsStatus().updates</code> lists it, or the <code>update</code> of a <code>world.diff</code> entry (counts in place of lists).</li> </ul> <pre><code class="language-lua">statusRead.updateLine({ library = "@builtin", from = "2836c81aa", to = "6865af4bb", repins = { "/zero/source/a.luau" }, edited = {} }) </code></pre> <h2 id="statusread-updatelines">modules/world_vcs.statusRead/updateLines</h2> <pre><code class="language-lua">updateLines(status: any, staged: boolean): { string } </code></pre> <p>The lines <code>zm status</code> prints for the library updates the working tree holds, one per update under the section it belongs in: <code>staged</code> picks the updates the calling area holds, otherwise the ones it does not.</p> <p><strong>Parameters</strong></p> <ul> <li><code>status</code> <code>any</code> <em>(optional)</em> — A table with <code>updates</code> as <code>world.vcsStatus</code> returns it.</li> <li><code>staged</code> <code>boolean</code> — Whether to list the staged updates.</li> </ul> <pre><code class="language-lua">statusRead.updateLines(world.vcsStatus(), false) </code></pre> <h2 id="ulid-readme">modules/world_vcs.ulid/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.ulid") -- world_vcs.ulid </code></pre> <p>The commit ids world_vcs allocates: ULIDs in Crockford base32, 48 bits of millisecond timestamp and 80 bits of <code>math.random()</code> randomness, and the check that a ref has that shape.</p> <p>Usage: local world_vcs.ulid = require("@builtin::modules.world_vcs.ulid")</p> <h2 id="vcswire-readme">modules/world_vcs.vcsWire/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.vcsWire") -- world_vcs.vcsWire </code></pre> <p>The wire helper every <code>__spacetime_vcs.*</code> caller shares: waits for one of its promises and unwraps the payload, restating the backend errors that read as something other than what happened, and reads the answers a read or a publish gives back.</p> <p>Usage: local world_vcs.vcsWire = require("@builtin::modules.world_vcs.vcsWire")</p> <h2 id="vcswire-awaitvcs">modules/world_vcs.vcsWire/awaitVcs</h2> <pre><code class="language-lua">awaitVcs(promiseId: any): (boolean, string) </code></pre> <p>Wait for one of the <code>__spacetime_vcs.*</code> promises and unwrap its payload.</p> <p><strong>Parameters</strong></p> <ul> <li><code>promiseId</code> <code>any</code> <em>(optional)</em> — The promise a <code>__spacetime_vcs.*</code> call answered.</li> </ul> <h2 id="vcswire-clarifyvcserror">modules/world_vcs.vcsWire/clarifyVcsError</h2> <pre><code class="language-lua">clarifyVcsError(errMsg: string): string </code></pre> <p>Restate the real cause and the action of a backend error, keeping the original for debugging.</p> <p><strong>Parameters</strong></p> <ul> <li><code>errMsg</code> <code>string</code> — The error text after the <code>ERR:</code> marker.</li> </ul> <h2 id="worldready-readme">modules/world_vcs.worldReady/README</h2> <pre><code class="language-lua">require("@builtin::modules.world_vcs.worldReady") -- world_vcs.worldReady </code></pre> <p>Counts the times a world's content finishes landing, so a call that swaps what the session is bound to (a checkout, the return trip after a failed one) waits for the swap to land rather than for a fixed time.</p> <p>Usage: local world_vcs.worldReady = require("@builtin::modules.world_vcs.worldReady")</p> <h2 id="compiler-readme">modules/zerojs.compiler/README</h2> <pre><code class="language-lua">require("@builtin::packages.zerojs.compiler") -- zerojs.compiler </code></pre> <p>Compiles a JavaScript AST (zerojs.parser) to Luau source that executes against the SAME realm value model as the interpreter (zerojs.runtime). Objects, prototypes, the stdlib, typed arrays and the bridge are shared, so compiled code interoperates with interpreted code and host marshaling with no second value model. Speed comes from eliminating per-node dispatch and environment lookups, plus integer-indexed array fast paths (rt.iget/iset). <code>compile</code> returns Luau source; <code>load</code> returns a chunk taking the runtime companion <code>rtc</code> (built by <code>makeRtc</code> from a realm + its global object). A construct outside the covered subset raises "compile bail: <node>" so the caller can fall back to the interpreter for that program.</p> <p>Usage: local zerojs.compiler = require("@builtin::packages.zerojs.compiler")</p> <h2 id="modules-zui-readme">modules/zui/README</h2> <pre><code class="language-lua">require("@builtin::modules.deprecated.zui") -- zui </code></pre> <p>DEPRECATED: Author screens as raw widget trees ({ type, style, props, children }) styled with the engine's CSS-parity <code>ui.*</code> surface; read @builtin::examples.ui.* for complete worked screens. This convenience layer predates CSS parity and writes unlike CSS.</p> <p>Usage: local zui = require("@builtin::modules.deprecated.zui")</p> <h2 id="modules-zui-definewidget">modules/zui/defineWidget</h2> <pre><code class="language-lua">defineWidget(name: string, builderFn: (any, any) -> any) </code></pre> <p>Register a custom widget kind. <code>builderFn(props, children)</code> is called at registerScreen / updateScreen time and its return value is decoded in place — the renderer never sees the custom name. Validates args locally so misuse fails immediately rather than silently. Requires the engine <code>ui</code> global; off-host execution raises a clear error.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Non-empty widget name (the value of <code>{ type = name, ... }</code>).</li> <li><code>builderFn</code> <code>(any, any) -> any</code> — <code>(props, children) -> widget</code> builder.</li> </ul> <pre><code class="language-lua">Z.defineWidget("myCard", function(props, children) return ... end) </code></pre> <h2 id="modules-zui-set">modules/zui/set</h2> <pre><code class="language-lua">set(widgetId: string, key: string, value: any) </code></pre> <p>Per-widget state write. Persists <code>value</code> against <code>(widgetId, key)</code> so a Luau-defined widget can own state across frames without threading it through the app's reactive store. No-op when the engine <code>ui.widgetStateSet</code> global is missing.</p> <p><strong>Parameters</strong></p> <ul> <li><code>widgetId</code> <code>string</code> — The widget instance id.</li> <li><code>key</code> <code>string</code> — State key within the widget.</li> <li><code>value</code> <code>any</code> <em>(optional)</em> — New value.</li> </ul> <pre><code class="language-lua">Z.widgetState.set("toggle:1", "on", true) </code></pre> <h2 id="modules-zui-unregisterwidget">modules/zui/unregisterWidget</h2> <pre><code class="language-lua">unregisterWidget(name: string) </code></pre> <p>Unregister a previously-defined custom widget. No-op when the engine <code>ui</code> global is absent.</p> <p><strong>Parameters</strong></p> <ul> <li><code>name</code> <code>string</code> — Non-empty widget name.</li> </ul> <pre><code class="language-lua">Z.unregisterWidget("myCard") </code></pre> </div><ul class="zc-docs-tags"><li>api</li><li>reference</li><!></ul><nav aria-label="Documentation sequence" class="zc-docs-seq"><a href="/docs/api-modelimport" rel="prev" class="zc-docs-seq-prev"><span class="zc-docs-seq-dir">← Previous</span><span class="zc-docs-seq-title">modelImport</span></a><a href="/docs/api-multiplayer" rel="next" class="zc-docs-seq-next"><span class="zc-docs-seq-dir">Next →</span><span class="zc-docs-seq-title">multiplayer</span></a></nav><footer class="zc-docs-article-foot"><a href="/docs" class="zc-docs-back">← All docs</a><a href="/docs/api-modules.md" rel="alternate" type="text/markdown" class="zc-docs-md">Read as markdown</a></footer></article></section><nav aria-label="Documentation" class="zc-docs-nav"><a href="/docs" class="zc-docs-nav-home">⌂ Docs home</a><div lang="en" class="zc-docs-nav-group"><div class="zc-docs-nav-label"><a href="/docs/introduction">Introduction</a></div><ul><li><a href="/docs/readme" class="">Zero</a></li><li><a href="/docs/content-graph" class="">The content graph</a></li><!></ul></div><div lang="en" class="zc-docs-nav-group"><div class="zc-docs-nav-label"><a href="/docs/overview">Overview</a></div><ul><li><a href="/docs/welcome" class="">Welcome to the Zero docs</a></li><li><a href="/docs/zeromind" class="">ZeroMind, the shared capability network</a></li><li><a href="/docs/publishing-to-zeromind" class="">Publishing to ZeroMind</a></li><li><a href="/docs/what-makes-zero-different" class="">What makes Zero different</a></li><li><a href="/docs/capability-compounding" class="">An engine where capability compounds</a></li><li><a href="/docs/showcase" class="">Showcase: built by agents on Zero</a></li><!></ul></div><div lang="en" class="zc-docs-nav-group"><div class="zc-docs-nav-label"><a href="/docs/core">Core</a></div><ul><li><a href="/docs/core-getting-started" class="">Getting started</a></li><li><a href="/docs/core-worlds" class="">Worlds & ZeroMind</a></li><li><a href="/docs/core-scenes" class="">Scenes</a></li><li><a href="/docs/core-engine" class="">The engine</a></li><li><a href="/docs/core-ecs" class="">ECS</a></li><li><a href="/docs/core-entities" class="">Entities</a></li><li><a href="/docs/core-components" class="">Components</a></li><li><a href="/docs/core-requiring-modules" class="">Requiring modules</a></li><li><a href="/docs/core-generating-assets-and-content" class="">Generating assets & content</a></li><li><a href="/docs/core-scripting-and-tasks" class="">Scripting & tasks</a></li><li><a href="/docs/core-runtime-data" class="">Runtime data</a></li><li><a href="/docs/core-asset-system" class="">Assets & asset types</a></li><li><a href="/docs/core-vfs" class="">The filesystem (VFS)</a></li><li><a href="/docs/core-tools" class="">The tool system</a></li><li><a href="/docs/core-multiplayer" class="">Multiplayer</a></li><li><a href="/docs/core-discovering" class="">Discovering the engine</a></li><li><a href="/docs/core-development" class="">Development & versioning</a></li><li><a href="/docs/core-performance" class="">Performance: run each job where it belongs</a></li><li><a href="/docs/core-authoring" class="">Authoring content other people can use</a></li><li><a href="/docs/core-carried" class="">Carried mounts: another world's content, read-only, for this session</a></li><li><a href="/docs/core-describing-assets" class="">Describing your assets</a></li><li><a href="/docs/core-home" class="">Home: your own folder, in every world</a></li><li><a href="/docs/core-observing" class="">Observing a running world</a></li><li><a href="/docs/core-plugin-development" class="">Building a plugin</a></li><li><a href="/docs/core-resource-model" class="">The resource model: asset, CPU, and GPU</a></li><li><a href="/docs/core-scenes-as-code" class="">Scenes, authored as code</a></li><li><a href="/docs/core-troubleshooting" class="">Troubleshooting</a></li><li><a href="/docs/core-version-control" class="">Version control</a></li><!></ul></div><div lang="en" class="zc-docs-nav-group"><div class="zc-docs-nav-label"><a href="/docs/topics">Topics</a></div><ul><li><a href="/docs/topics-animated-images" class="">Showing an animated image</a></li><li><a href="/docs/topics-animation" class="">Animation</a></li><li><a href="/docs/topics-assemblies" class="">Assemblies: slots, parts and swapping them at runtime</a></li><li><a href="/docs/topics-audio" class="">Audio</a></li><li><a href="/docs/topics-bash" class="">The shell</a></li><li><a href="/docs/topics-building-a-game" class="">Building a game — the play-mode authoring loop</a></li><li><a href="/docs/topics-byte-streams" class="">Byte streams: talking to things outside the engine</a></li><li><a href="/docs/topics-camera-input" class="">Camera input: a webcam or phone camera in the world</a></li><li><a href="/docs/topics-cameras" class="">Cameras — which one is rendering, with what projection, into what</a></li><li><a href="/docs/topics-cutscenes" class="">Cutscenes</a></li><li><a href="/docs/topics-editor-editor-ui" class="">The editor UI</a></li><li><a href="/docs/topics-external-editor" class="">Engine API in an external editor</a></li><li><a href="/docs/topics-frame-streaming" class="">Streaming frames out of the engine</a></li><li><a href="/docs/topics-graphics-quality" class="">Graphics quality on phones and desktops: the device's quality tier</a></li><li><a href="/docs/topics-http-server" class="">Serving HTTP from the running world</a></li><li><a href="/docs/topics-ik" class="">Inverse kinematics</a></li><li><a href="/docs/topics-input" class="">Input</a></li><li><a href="/docs/topics-localization" class="">The player's language</a></li><li><a href="/docs/topics-microphone" class="">The microphone: sound coming in</a></li><li><a href="/docs/topics-paths" class="">Paths: splines, roads, tracks, rails, lanes and routes</a></li><li><a href="/docs/topics-physics" class="">Physics</a></li><li><a href="/docs/topics-raytracing" class="">Ray tracing</a></li><li><a href="/docs/topics-recording" class="">Recording: filming the world and keeping the video</a></li><li><a href="/docs/topics-render-layers" class="">Render layers</a></li><li><a href="/docs/topics-render-textures" class="">Render textures</a></li><li><a href="/docs/topics-rendering" class="">Rendering</a></li><li><a href="/docs/topics-rendering-budgets" class="">Quality tiers, render target cost and loaded content</a></li><li><a href="/docs/topics-rendering-capture-passes" class="">Debug camera views</a></li><li><a href="/docs/topics-rendering-draw-cost" class="">What drawing costs: culling, materials and crowds</a></li><li><a href="/docs/topics-rendering-geometry" class="">Morph targets, clusters and lines</a></li><li><a href="/docs/topics-rendering-instance-data" class="">Per-entity shader data</a></li><li><a href="/docs/topics-rendering-lighting" class="">Lighting and global illumination</a></li><li><a href="/docs/topics-rendering-material-graphs" class="">Material graphs</a></li><li><a href="/docs/topics-rendering-motion" class="">Motion blur and the velocity buffer</a></li><li><a href="/docs/topics-rendering-post-process" class="">Post-process</a></li><li><a href="/docs/topics-rendering-reflections" class="">Reflections</a></li><li><a href="/docs/topics-rendering-render-features" class="">Render features</a></li><li><a href="/docs/topics-rendering-scene-queries" class="">Asking where the drawn world is along a ray</a></li><li><a href="/docs/topics-rendering-settings" class="">Scene-wide renderer settings</a></li><li><a href="/docs/topics-rendering-shadows" class="">Shadows: budgets, casters and what each light costs</a></li><li><a href="/docs/topics-ui-styling" class="">Styling</a></li><li><a href="/docs/topics-text" class="">Text and labels</a></li><li><a href="/docs/topics-ui" class="">The UI window system</a></li><li><a href="/docs/topics-values" class="">Named values: the numbers a machine runs on</a></li><li><a href="/docs/topics-world-detail" class="">World detail — terrain, voxels, streaming, LOD</a></li><!></ul></div><div lang="en" class="zc-docs-nav-group"><div class="zc-docs-nav-label"><a href="/docs/api-reference">API Reference</a></div><ul><li><a href="/docs/api-agentsessions" class="">agentSessions</a></li><li><a href="/docs/api-animation" class="">animation</a></li><li><a href="/docs/api-asset" class="">asset</a></li><li><a href="/docs/api-audio" class="">audio</a></li><li><a href="/docs/api-av" class="">av</a></li><li><a href="/docs/api-base64" class="">base64</a></li><li><a href="/docs/api-blend" class="">blend</a></li><li><a href="/docs/api-bundle" class="">bundle</a></li><li><a href="/docs/api-camera" class="">camera</a></li><li><a href="/docs/api-camerainput" class="">cameraInput</a></li><li><a href="/docs/api-carried" class="">carried</a></li><li><a href="/docs/api-channel" class="">channel</a></li><li><a href="/docs/api-color" class="">color</a></li><li><a href="/docs/api-colorsequence" class="">ColorSequence</a></li><li><a href="/docs/api-component" class="">component</a></li><li><a href="/docs/api-compute" class="">compute</a></li><li><a href="/docs/api-cursor" class="">cursor</a></li><li><a href="/docs/api-debugger" class="">debugger</a></li><li><a href="/docs/api-ecs" class="">ecs</a></li><li><a href="/docs/api-egress" class="">egress</a></li><li><a href="/docs/api-engine" class="">engine</a></li><li><a href="/docs/api-entity" class="">entity</a></li><li><a href="/docs/api-entity-2" class="">Entity</a></li><li><a href="/docs/api-environment" class="">environment</a></li><li><a href="/docs/api-field" class="">Field</a></li><li><a href="/docs/api-font" class="">font</a></li><li><a href="/docs/api-framestream" class="">frameStream</a></li><li><a href="/docs/api-globals" class="">globals</a></li><li><a href="/docs/api-home" class="">home</a></li><li><a href="/docs/api-http" class="">http</a></li><li><a href="/docs/api-httpserver" class="">httpServer</a></li><li><a href="/docs/api-i18n" class="">i18n</a></li><li><a href="/docs/api-input" class="">input</a></li><li><a href="/docs/api-jobs" class="">jobs</a></li><li><a href="/docs/api-layers" class="">layers</a></li><li><a href="/docs/api-library" class="">library</a></li><li><a href="/docs/api-live" class="">live</a></li><li><a href="/docs/api-log" class="">log</a></li><li><a href="/docs/api-logs" class="">logs</a></li><li><a href="/docs/api-lsp" class="">lsp</a></li><li><a href="/docs/api-luau-codegen" class="">luau_codegen</a></li><li><a href="/docs/api-luau-profile" class="">luau_profile</a></li><li><a href="/docs/api-material" class="">Material</a></li><li><a href="/docs/api-mathx" class="">mathx</a></li><li><a href="/docs/api-mcp" class="">mcp</a></li><li><a href="/docs/api-mcplog" class="">mcpLog</a></li><li><a href="/docs/api-microphone" class="">microphone</a></li><li><a href="/docs/api-modelimport" class="">modelImport</a></li><li><a href="/docs/api-modules" aria-current="page" class="active">modules</a></li><li><a href="/docs/api-multiplayer" class="">multiplayer</a></li><li><a href="/docs/api-notices" class="">notices</a></li><li><a href="/docs/api-numberrange" class="">NumberRange</a></li><li><a href="/docs/api-numbersequence" class="">NumberSequence</a></li><li><a href="/docs/api-nx" class="">nx</a></li><li><a href="/docs/api-packages" class="">packages</a></li><li><a href="/docs/api-physics" class="">physics</a></li><li><a href="/docs/api-physics-2" class="">Physics</a></li><li><a href="/docs/api-plugins" class="">plugins</a></li><li><a href="/docs/api-postprocess" class="">postprocess</a></li><li><a href="/docs/api-preset" class="">preset</a></li><li><a href="/docs/api-profiler" class="">profiler</a></li><li><a href="/docs/api-random" class="">Random</a></li><li><a href="/docs/api-reflectionprobe" class="">reflectionProbe</a></li><li><a href="/docs/api-renderer" class="">renderer</a></li><li><a href="/docs/api-scopes" class="">scopes</a></li><li><a href="/docs/api-service" class="">service</a></li><li><a href="/docs/api-settings" class="">settings</a></li><li><a href="/docs/api-shader" class="">shader</a></li><li><a href="/docs/api-shell" class="">shell</a></li><li><a href="/docs/api-skeleton" class="">skeleton</a></li><li><a href="/docs/api-sky" class="">sky</a></li><li><a href="/docs/api-stream" class="">stream</a></li><li><a href="/docs/api-streaming" class="">streaming</a></li><li><a href="/docs/api-stringx" class="">stringx</a></li><li><a href="/docs/api-subscriptions" class="">subscriptions</a></li><li><a href="/docs/api-substrate" class="">substrate</a></li><li><a href="/docs/api-task" class="">task</a></li><li><a href="/docs/api-terminal" class="">terminal</a></li><li><a href="/docs/api-text" class="">text</a></li><li><a href="/docs/api-time" class="">time</a></li><li><a href="/docs/api-toml" class="">toml</a></li><li><a href="/docs/api-tools" class="">tools</a></li><li><a href="/docs/api-transform" class="">Transform</a></li><li><a href="/docs/api-typed" class="">typed</a></li><li><a href="/docs/api-ui" class="">ui</a></li><li><a href="/docs/api-userfile" class="">userfile</a></li><li><a href="/docs/api-values" class="">values</a></li><li><a href="/docs/api-vfs" class="">vfs</a></li><li><a href="/docs/api-video" class="">video</a></li><li><a href="/docs/api-world" class="">world</a></li><li><a href="/docs/api-ws" class="">ws</a></li><!></ul></div><div lang="en" class="zc-docs-nav-group"><div class="zc-docs-nav-label"><a href="/docs/types">Types</a></div><ul><li><a href="/docs/types-agentskill" class="">agentSkill</a></li><li><a href="/docs/types-animation" class="">animation asset type</a></li><li><a href="/docs/types-assettype" class="">assetType (asset type)</a></li><li><a href="/docs/types-avatar" class="">avatar</a></li><li><a href="/docs/types-avatartype" class="">avatarType</a></li><li><a href="/docs/types-bundle" class="">Bundle (asset type)</a></li><li><a href="/docs/types-component" class="">Component (asset type)</a></li><li><a href="/docs/types-computeshader" class="">Compute shader (asset type)</a></li><li><a href="/docs/types-data" class="">data (asset type)</a></li><li><a href="/docs/types-datatype" class="">dataType (asset type)</a></li><li><a href="/docs/types-dynamicasset" class="">dynamicAsset (asset type)</a></li><li><a href="/docs/types-editorlayout" class="">editorLayout</a></li><li><a href="/docs/types-editorpanel" class="">editorPanel</a></li><li><a href="/docs/types-effect" class="">effect</a></li><li><a href="/docs/types-font" class="">.font asset type</a></li><li><a href="/docs/types-gaussiansplat" class="">gaussianSplat</a></li><li><a href="/docs/types-guide" class="">guide</a></li><li><a href="/docs/types-importer" class="">Importer (asset type)</a></li><li><a href="/docs/types-inputbinding" class="">inputBinding assetType</a></li><li><a href="/docs/types-inputmacro" class="">Input macro (asset type)</a></li><li><a href="/docs/types-inputmap" class="">inputMap assetType</a></li><li><a href="/docs/types-lightmapdata" class="">lightmapData</a></li><li><a href="/docs/types-loadingscreen" class="">loadingScreen</a></li><li><a href="/docs/types-material" class="">Material (asset type)</a></li><li><a href="/docs/types-mesh" class="">mesh</a></li><li><a href="/docs/types-module" class="">Module (asset type)</a></li><li><a href="/docs/types-nodebox" class="">Nodebox (asset type)</a></li><li><a href="/docs/types-package" class="">Package (asset type)</a></li><li><a href="/docs/types-pixeltemplate" class="">pixelTemplate (asset type)</a></li><li><a href="/docs/types-plugin" class="">plugin</a></li><li><a href="/docs/types-population" class="">population</a></li><li><a href="/docs/types-preset" class="">Preset (asset type)</a></li><li><a href="/docs/types-procgraph" class="">procGraph (asset type)</a></li><li><a href="/docs/types-procnode" class="">procNode: one reusable step of procedural generation</a></li><li><a href="/docs/types-procpack" class="">procPack — a family of procedural ops as content</a></li><li><a href="/docs/types-procrecord" class="">procRecord: a kind of record a graph makes and saves</a></li><li><a href="/docs/types-renderfeature" class="">renderFeature</a></li><li><a href="/docs/types-rig" class="">rig asset type</a></li><li><a href="/docs/types-scene" class="">Scene (asset type)</a></li><li><a href="/docs/types-scenemodule" class="">sceneModule — a part of a scene, authored as code</a></li><li><a href="/docs/types-service" class="">Service (asset type)</a></li><li><a href="/docs/types-servicerun" class="">serviceRun</a></li><li><a href="/docs/types-shader" class="">Shader (asset type)</a></li><li><a href="/docs/types-shadermodule" class="">Shader module (asset type)</a></li><li><a href="/docs/types-soundclip" class="">soundClip</a></li><li><a href="/docs/types-style" class="">Style (asset type)</a></li><li><a href="/docs/types-terraindata" class="">terrainData</a></li><li><a href="/docs/types-testsuite" class="">Test Suite (asset type)</a></li><li><a href="/docs/types-texture" class="">texture</a></li><li><a href="/docs/types-tool" class="">tool (asset type)</a></li><li><a href="/docs/types-toolbox" class="">toolbox (asset type)</a></li><li><a href="/docs/types-voxeltemplate" class="">Voxel Template (asset type)</a></li><li><a href="/docs/types-workflow" class="">workflow</a></li><!></ul></div><!></nav></div></main><footer class="zm-footer"><div data-nosnippet="" class="zm-footer-inner"><nav aria-label="Footer" class="zm-footer-cols"><div class="zm-footer-col"><h2 class="zm-footer-h">Explore</h2><ul><li><a href="/worlds">Worlds</a></li><li><a href="/feed">Feed</a></li><li><a href="/packages">Packages</a></li></ul></div><div class="zm-footer-col"><h2 class="zm-footer-h">Make a game</h2><ul><li><a href="/ai-game-generator">AI game generator</a></li><li><a href="/text-to-game">Text to game</a></li><li><a href="/3d-game-maker">3D game maker</a></li><li><a href="/zero-engine">Zero engine</a></li><li><a href="/mcp-server">MCP server</a></li><!></ul></div><div class="zm-footer-col"><h2 class="zm-footer-h">Coding agents</h2><ul><li><a href="/agents">All agents</a></li><li><a href="/agents/claude-code">Claude Code</a></li><li><a href="/agents/cursor">Cursor</a></li><li><a href="/agents/codex">Codex</a></li><!></ul></div><div class="zm-footer-col"><h2 class="zm-footer-h">Community</h2><ul><li><a href="/community">Community</a></li><li><a href="/users">Users</a></li><li><a href="/vibes">Vibes</a></li></ul></div><div class="zm-footer-col"><h2 class="zm-footer-h">Learn</h2><ul><li><a href="/docs" aria-current="page">Docs</a></li><li><a href="/blog">Blog</a></li></ul></div><div class="zm-footer-col"><h2 class="zm-footer-h">Platform story</h2><ul><li><a href="/docs/zeromind">ZeroMind</a></li><li><a href="/docs/what-makes-zero-different">What makes Zero different</a></li><li><a href="/docs/capability-compounding">Capability compounding</a></li><li><a href="/docs/content-graph">The content graph</a></li><li><a href="/docs/showcase">Showcase</a></li></ul></div><div class="zm-footer-col"><h2 class="zm-footer-h">Company</h2><ul><li><a href="/about">About</a></li><li><a href="mailto:admin@origozero.com">Contact</a></li><li><a href="https://www.linkedin.com/company/origozero" target="_blank" rel="noopener">LinkedIn</a></li><li><a href="https://github.com/OrigoZero" target="_blank" rel="noopener">GitHub</a></li></ul></div></nav><div class="zm-footer-legal"><p class="zm-footer-brand">OrigoZero. Build 3D games and worlds with AI in the Zero engine, and play them in your browser.</p><div class="zm-footer-legal-end"><nav aria-label="Legal" class="zm-footer-legal-nav"><a href="/privacy">Privacy</a><a href="/terms">Terms</a></nav><nav aria-label="Language" class="zm-footer-lang"><a href="/docs/api-modules" hreflang="en" lang="en" aria-current="page">English</a><a href="/pt-br" hreflang="pt" lang="pt-BR">Português</a><a href="/es" hreflang="es" lang="es-419">Español</a><a href="/ja" hreflang="ja" lang="ja">日本語</a><a href="/ko" hreflang="ko" lang="ko">한국어</a><a href="/zh-hans" hreflang="zh-Hans" lang="zh-Hans">简体中文</a><a href="/tr" hreflang="tr" lang="tr">Türkçe</a><!></nav><p class="zm-footer-copy">© 2026 OrigoZero Oy</p></div></div></div></footer><nav aria-label="Primary" class="zc-bottombar"><a href="/" class="zc-bb-item"><span aria-hidden="true" class="zc-bb-ico"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5"></path><path d="M5.5 9.5V20a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1V9.5"></path></svg></span><span data-nosnippet="" class="zc-bb-label">Home</span></a><a href="/create" class="zc-bb-item"><span aria-hidden="true" class="zc-bb-ico"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"></path><path d="M5 12h14"></path></svg></span><span data-nosnippet="" class="zc-bb-label">Create</span></a><a href="/worlds" class="zc-bb-item"><span aria-hidden="true" class="zc-bb-ico"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="m15.8 8.2-2.1 5.5-5.5 2.1 2.1-5.5z"></path></svg></span><span data-nosnippet="" class="zc-bb-label">Explore</span></a><a href="/docs" aria-current="page" class="zc-bb-item active"><span aria-hidden="true" class="zc-bb-ico"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H6.5A2.5 2.5 0 0 1 4 17.5z"></path><path d="M4 17.5A2.5 2.5 0 0 1 6.5 15H20"></path></svg></span><span data-nosnippet="" class="zc-bb-label">Docs</span></a><a href="/blog" class="zc-bb-item"><span aria-hidden="true" class="zc-bb-ico"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.5 5.5 18 11 8 21H2.5v-5.5z"></path><path d="m15 3 6 6"></path></svg></span><span data-nosnippet="" class="zc-bb-label">Blog</span></a></nav><!><!><!><!><!></div></body></html><script nonce="3d1FZ0p4P5GAxDSnyQL3Iw">__RESOLVED_RESOURCES=[];__SERIALIZED_ERRORS=[];__PENDING_RESOURCES=[0,];__RESOURCE_RESOLVERS=[];</script><script nonce="3d1FZ0p4P5GAxDSnyQL3Iw">__RESOLVED_RESOURCES[0] = "{\"nav\":[{\"slug\":\"readme\",\"title\":\"Zero\",\"section\":\"Introduction\"},{\"slug\":\"content-graph\",\"title\":\"The content graph\",\"section\":\"Introduction\"},{\"slug\":\"welcome\",\"title\":\"Welcome to the Zero docs\",\"section\":\"Overview\"},{\"slug\":\"zeromind\",\"title\":\"ZeroMind, the shared capability network\",\"section\":\"Overview\"},{\"slug\":\"publishing-to-zeromind\",\"title\":\"Publishing to ZeroMind\",\"section\":\"Overview\"},{\"slug\":\"what-makes-zero-different\",\"title\":\"What makes Zero different\",\"section\":\"Overview\"},{\"slug\":\"capability-compounding\",\"title\":\"An engine where capability compounds\",\"section\":\"Overview\"},{\"slug\":\"showcase\",\"title\":\"Showcase: built by agents on Zero\",\"section\":\"Overview\"},{\"slug\":\"core-getting-started\",\"title\":\"Getting started\",\"section\":\"Core\"},{\"slug\":\"core-worlds\",\"title\":\"Worlds & ZeroMind\",\"section\":\"Core\"},{\"slug\":\"core-scenes\",\"title\":\"Scenes\",\"section\":\"Core\"},{\"slug\":\"core-engine\",\"title\":\"The engine\",\"section\":\"Core\"},{\"slug\":\"core-ecs\",\"title\":\"ECS\",\"section\":\"Core\"},{\"slug\":\"core-entities\",\"title\":\"Entities\",\"section\":\"Core\"},{\"slug\":\"core-components\",\"title\":\"Components\",\"section\":\"Core\"},{\"slug\":\"core-requiring-modules\",\"title\":\"Requiring modules\",\"section\":\"Core\"},{\"slug\":\"core-generating-assets-and-content\",\"title\":\"Generating assets & content\",\"section\":\"Core\"},{\"slug\":\"core-scripting-and-tasks\",\"title\":\"Scripting & tasks\",\"section\":\"Core\"},{\"slug\":\"core-runtime-data\",\"title\":\"Runtime data\",\"section\":\"Core\"},{\"slug\":\"core-asset-system\",\"title\":\"Assets & asset types\",\"section\":\"Core\"},{\"slug\":\"core-vfs\",\"title\":\"The filesystem (VFS)\",\"section\":\"Core\"},{\"slug\":\"core-tools\",\"title\":\"The tool system\",\"section\":\"Core\"},{\"slug\":\"core-multiplayer\",\"title\":\"Multiplayer\",\"section\":\"Core\"},{\"slug\":\"core-discovering\",\"title\":\"Discovering the engine\",\"section\":\"Core\"},{\"slug\":\"core-development\",\"title\":\"Development & versioning\",\"section\":\"Core\"},{\"slug\":\"core-performance\",\"title\":\"Performance: run each job where it belongs\",\"section\":\"Core\"},{\"slug\":\"core-authoring\",\"title\":\"Authoring content other people can use\",\"section\":\"Core\"},{\"slug\":\"core-carried\",\"title\":\"Carried mounts: another world's content, read-only, for this session\",\"section\":\"Core\"},{\"slug\":\"core-describing-assets\",\"title\":\"Describing your assets\",\"section\":\"Core\"},{\"slug\":\"core-home\",\"title\":\"Home: your own folder, in every world\",\"section\":\"Core\"},{\"slug\":\"core-observing\",\"title\":\"Observing a running world\",\"section\":\"Core\"},{\"slug\":\"core-plugin-development\",\"title\":\"Building a plugin\",\"section\":\"Core\"},{\"slug\":\"core-resource-model\",\"title\":\"The resource model: asset, CPU, and GPU\",\"section\":\"Core\"},{\"slug\":\"core-scenes-as-code\",\"title\":\"Scenes, authored as code\",\"section\":\"Core\"},{\"slug\":\"core-troubleshooting\",\"title\":\"Troubleshooting\",\"section\":\"Core\"},{\"slug\":\"core-version-control\",\"title\":\"Version control\",\"section\":\"Core\"},{\"slug\":\"topics-animated-images\",\"title\":\"Showing an animated image\",\"section\":\"Topics\"},{\"slug\":\"topics-animation\",\"title\":\"Animation\",\"section\":\"Topics\"},{\"slug\":\"topics-assemblies\",\"title\":\"Assemblies: slots, parts and swapping them at runtime\",\"section\":\"Topics\"},{\"slug\":\"topics-audio\",\"title\":\"Audio\",\"section\":\"Topics\"},{\"slug\":\"topics-bash\",\"title\":\"The shell\",\"section\":\"Topics\"},{\"slug\":\"topics-building-a-game\",\"title\":\"Building a game — the play-mode authoring loop\",\"section\":\"Topics\"},{\"slug\":\"topics-byte-streams\",\"title\":\"Byte streams: talking to things outside the engine\",\"section\":\"Topics\"},{\"slug\":\"topics-camera-input\",\"title\":\"Camera input: a webcam or phone camera in the world\",\"section\":\"Topics\"},{\"slug\":\"topics-cameras\",\"title\":\"Cameras — which one is rendering, with what projection, into what\",\"section\":\"Topics\"},{\"slug\":\"topics-cutscenes\",\"title\":\"Cutscenes\",\"section\":\"Topics\"},{\"slug\":\"topics-editor-editor-ui\",\"title\":\"The editor UI\",\"section\":\"Topics\"},{\"slug\":\"topics-external-editor\",\"title\":\"Engine API in an external editor\",\"section\":\"Topics\"},{\"slug\":\"topics-frame-streaming\",\"title\":\"Streaming frames out of the engine\",\"section\":\"Topics\"},{\"slug\":\"topics-graphics-quality\",\"title\":\"Graphics quality on phones and desktops: the device's quality tier\",\"section\":\"Topics\"},{\"slug\":\"topics-http-server\",\"title\":\"Serving HTTP from the running world\",\"section\":\"Topics\"},{\"slug\":\"topics-ik\",\"title\":\"Inverse kinematics\",\"section\":\"Topics\"},{\"slug\":\"topics-input\",\"title\":\"Input\",\"section\":\"Topics\"},{\"slug\":\"topics-localization\",\"title\":\"The player's language\",\"section\":\"Topics\"},{\"slug\":\"topics-microphone\",\"title\":\"The microphone: sound coming in\",\"section\":\"Topics\"},{\"slug\":\"topics-paths\",\"title\":\"Paths: splines, roads, tracks, rails, lanes and routes\",\"section\":\"Topics\"},{\"slug\":\"topics-physics\",\"title\":\"Physics\",\"section\":\"Topics\"},{\"slug\":\"topics-raytracing\",\"title\":\"Ray tracing\",\"section\":\"Topics\"},{\"slug\":\"topics-recording\",\"title\":\"Recording: filming the world and keeping the video\",\"section\":\"Topics\"},{\"slug\":\"topics-render-layers\",\"title\":\"Render layers\",\"section\":\"Topics\"},{\"slug\":\"topics-render-textures\",\"title\":\"Render textures\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering\",\"title\":\"Rendering\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-budgets\",\"title\":\"Quality tiers, render target cost and loaded content\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-capture-passes\",\"title\":\"Debug camera views\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-draw-cost\",\"title\":\"What drawing costs: culling, materials and crowds\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-geometry\",\"title\":\"Morph targets, clusters and lines\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-instance-data\",\"title\":\"Per-entity shader data\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-lighting\",\"title\":\"Lighting and global illumination\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-material-graphs\",\"title\":\"Material graphs\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-motion\",\"title\":\"Motion blur and the velocity buffer\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-post-process\",\"title\":\"Post-process\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-reflections\",\"title\":\"Reflections\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-render-features\",\"title\":\"Render features\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-scene-queries\",\"title\":\"Asking where the drawn world is along a ray\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-settings\",\"title\":\"Scene-wide renderer settings\",\"section\":\"Topics\"},{\"slug\":\"topics-rendering-shadows\",\"title\":\"Shadows: budgets, casters and what each light costs\",\"section\":\"Topics\"},{\"slug\":\"topics-ui-styling\",\"title\":\"Styling\",\"section\":\"Topics\"},{\"slug\":\"topics-text\",\"title\":\"Text and labels\",\"section\":\"Topics\"},{\"slug\":\"topics-ui\",\"title\":\"The UI window system\",\"section\":\"Topics\"},{\"slug\":\"topics-values\",\"title\":\"Named values: the numbers a machine runs on\",\"section\":\"Topics\"},{\"slug\":\"topics-world-detail\",\"title\":\"World detail — terrain, voxels, streaming, LOD\",\"section\":\"Topics\"},{\"slug\":\"api-agentsessions\",\"title\":\"agentSessions\",\"section\":\"API Reference\"},{\"slug\":\"api-animation\",\"title\":\"animation\",\"section\":\"API Reference\"},{\"slug\":\"api-asset\",\"title\":\"asset\",\"section\":\"API Reference\"},{\"slug\":\"api-audio\",\"title\":\"audio\",\"section\":\"API Reference\"},{\"slug\":\"api-av\",\"title\":\"av\",\"section\":\"API Reference\"},{\"slug\":\"api-base64\",\"title\":\"base64\",\"section\":\"API Reference\"},{\"slug\":\"api-blend\",\"title\":\"blend\",\"section\":\"API Reference\"},{\"slug\":\"api-bundle\",\"title\":\"bundle\",\"section\":\"API Reference\"},{\"slug\":\"api-camera\",\"title\":\"camera\",\"section\":\"API Reference\"},{\"slug\":\"api-camerainput\",\"title\":\"cameraInput\",\"section\":\"API Reference\"},{\"slug\":\"api-carried\",\"title\":\"carried\",\"section\":\"API Reference\"},{\"slug\":\"api-channel\",\"title\":\"channel\",\"section\":\"API Reference\"},{\"slug\":\"api-color\",\"title\":\"color\",\"section\":\"API Reference\"},{\"slug\":\"api-colorsequence\",\"title\":\"ColorSequence\",\"section\":\"API Reference\"},{\"slug\":\"api-component\",\"title\":\"component\",\"section\":\"API Reference\"},{\"slug\":\"api-compute\",\"title\":\"compute\",\"section\":\"API Reference\"},{\"slug\":\"api-cursor\",\"title\":\"cursor\",\"section\":\"API Reference\"},{\"slug\":\"api-debugger\",\"title\":\"debugger\",\"section\":\"API Reference\"},{\"slug\":\"api-ecs\",\"title\":\"ecs\",\"section\":\"API Reference\"},{\"slug\":\"api-egress\",\"title\":\"egress\",\"section\":\"API Reference\"},{\"slug\":\"api-engine\",\"title\":\"engine\",\"section\":\"API Reference\"},{\"slug\":\"api-entity\",\"title\":\"entity\",\"section\":\"API Reference\"},{\"slug\":\"api-entity-2\",\"title\":\"Entity\",\"section\":\"API Reference\"},{\"slug\":\"api-environment\",\"title\":\"environment\",\"section\":\"API Reference\"},{\"slug\":\"api-field\",\"title\":\"Field\",\"section\":\"API Reference\"},{\"slug\":\"api-font\",\"title\":\"font\",\"section\":\"API Reference\"},{\"slug\":\"api-framestream\",\"title\":\"frameStream\",\"section\":\"API Reference\"},{\"slug\":\"api-globals\",\"title\":\"globals\",\"section\":\"API Reference\"},{\"slug\":\"api-home\",\"title\":\"home\",\"section\":\"API Reference\"},{\"slug\":\"api-http\",\"title\":\"http\",\"section\":\"API Reference\"},{\"slug\":\"api-httpserver\",\"title\":\"httpServer\",\"section\":\"API Reference\"},{\"slug\":\"api-i18n\",\"title\":\"i18n\",\"section\":\"API Reference\"},{\"slug\":\"api-input\",\"title\":\"input\",\"section\":\"API Reference\"},{\"slug\":\"api-jobs\",\"title\":\"jobs\",\"section\":\"API Reference\"},{\"slug\":\"api-layers\",\"title\":\"layers\",\"section\":\"API Reference\"},{\"slug\":\"api-library\",\"title\":\"library\",\"section\":\"API Reference\"},{\"slug\":\"api-live\",\"title\":\"live\",\"section\":\"API Reference\"},{\"slug\":\"api-log\",\"title\":\"log\",\"section\":\"API Reference\"},{\"slug\":\"api-logs\",\"title\":\"logs\",\"section\":\"API Reference\"},{\"slug\":\"api-lsp\",\"title\":\"lsp\",\"section\":\"API Reference\"},{\"slug\":\"api-luau-codegen\",\"title\":\"luau_codegen\",\"section\":\"API Reference\"},{\"slug\":\"api-luau-profile\",\"title\":\"luau_profile\",\"section\":\"API Reference\"},{\"slug\":\"api-material\",\"title\":\"Material\",\"section\":\"API Reference\"},{\"slug\":\"api-mathx\",\"title\":\"mathx\",\"section\":\"API Reference\"},{\"slug\":\"api-mcp\",\"title\":\"mcp\",\"section\":\"API Reference\"},{\"slug\":\"api-mcplog\",\"title\":\"mcpLog\",\"section\":\"API Reference\"},{\"slug\":\"api-microphone\",\"title\":\"microphone\",\"section\":\"API Reference\"},{\"slug\":\"api-modelimport\",\"title\":\"modelImport\",\"section\":\"API Reference\"},{\"slug\":\"api-modules\",\"title\":\"modules\",\"section\":\"API Reference\"},{\"slug\":\"api-multiplayer\",\"title\":\"multiplayer\",\"section\":\"API Reference\"},{\"slug\":\"api-notices\",\"title\":\"notices\",\"section\":\"API Reference\"},{\"slug\":\"api-numberrange\",\"title\":\"NumberRange\",\"section\":\"API Reference\"},{\"slug\":\"api-numbersequence\",\"title\":\"NumberSequence\",\"section\":\"API Reference\"},{\"slug\":\"api-nx\",\"title\":\"nx\",\"section\":\"API Reference\"},{\"slug\":\"api-packages\",\"title\":\"packages\",\"section\":\"API Reference\"},{\"slug\":\"api-physics\",\"title\":\"physics\",\"section\":\"API Reference\"},{\"slug\":\"api-physics-2\",\"title\":\"Physics\",\"section\":\"API Reference\"},{\"slug\":\"api-plugins\",\"title\":\"plugins\",\"section\":\"API Reference\"},{\"slug\":\"api-postprocess\",\"title\":\"postprocess\",\"section\":\"API Reference\"},{\"slug\":\"api-preset\",\"title\":\"preset\",\"section\":\"API Reference\"},{\"slug\":\"api-profiler\",\"title\":\"profiler\",\"section\":\"API Reference\"},{\"slug\":\"api-random\",\"title\":\"Random\",\"section\":\"API Reference\"},{\"slug\":\"api-reflectionprobe\",\"title\":\"reflectionProbe\",\"section\":\"API Reference\"},{\"slug\":\"api-renderer\",\"title\":\"renderer\",\"section\":\"API Reference\"},{\"slug\":\"api-scopes\",\"title\":\"scopes\",\"section\":\"API Reference\"},{\"slug\":\"api-service\",\"title\":\"service\",\"section\":\"API Reference\"},{\"slug\":\"api-settings\",\"title\":\"settings\",\"section\":\"API Reference\"},{\"slug\":\"api-shader\",\"title\":\"shader\",\"section\":\"API Reference\"},{\"slug\":\"api-shell\",\"title\":\"shell\",\"section\":\"API Reference\"},{\"slug\":\"api-skeleton\",\"title\":\"skeleton\",\"section\":\"API Reference\"},{\"slug\":\"api-sky\",\"title\":\"sky\",\"section\":\"API Reference\"},{\"slug\":\"api-stream\",\"title\":\"stream\",\"section\":\"API Reference\"},{\"slug\":\"api-streaming\",\"title\":\"streaming\",\"section\":\"API Reference\"},{\"slug\":\"api-stringx\",\"title\":\"stringx\",\"section\":\"API Reference\"},{\"slug\":\"api-subscriptions\",\"title\":\"subscriptions\",\"section\":\"API Reference\"},{\"slug\":\"api-substrate\",\"title\":\"substrate\",\"section\":\"API Reference\"},{\"slug\":\"api-task\",\"title\":\"task\",\"section\":\"API Reference\"},{\"slug\":\"api-terminal\",\"title\":\"terminal\",\"section\":\"API Reference\"},{\"slug\":\"api-text\",\"title\":\"text\",\"section\":\"API Reference\"},{\"slug\":\"api-time\",\"title\":\"time\",\"section\":\"API Reference\"},{\"slug\":\"api-toml\",\"title\":\"toml\",\"section\":\"API Reference\"},{\"slug\":\"api-tools\",\"title\":\"tools\",\"section\":\"API Reference\"},{\"slug\":\"api-transform\",\"title\":\"Transform\",\"section\":\"API Reference\"},{\"slug\":\"api-typed\",\"title\":\"typed\",\"section\":\"API Reference\"},{\"slug\":\"api-ui\",\"title\":\"ui\",\"section\":\"API Reference\"},{\"slug\":\"api-userfile\",\"title\":\"userfile\",\"section\":\"API Reference\"},{\"slug\":\"api-values\",\"title\":\"values\",\"section\":\"API Reference\"},{\"slug\":\"api-vfs\",\"title\":\"vfs\",\"section\":\"API Reference\"},{\"slug\":\"api-video\",\"title\":\"video\",\"section\":\"API Reference\"},{\"slug\":\"api-world\",\"title\":\"world\",\"section\":\"API Reference\"},{\"slug\":\"api-ws\",\"title\":\"ws\",\"section\":\"API Reference\"},{\"slug\":\"types-agentskill\",\"title\":\"agentSkill\",\"section\":\"Types\"},{\"slug\":\"types-animation\",\"title\":\"animation asset type\",\"section\":\"Types\"},{\"slug\":\"types-assettype\",\"title\":\"assetType (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-avatar\",\"title\":\"avatar\",\"section\":\"Types\"},{\"slug\":\"types-avatartype\",\"title\":\"avatarType\",\"section\":\"Types\"},{\"slug\":\"types-bundle\",\"title\":\"Bundle (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-component\",\"title\":\"Component (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-computeshader\",\"title\":\"Compute shader (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-data\",\"title\":\"data (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-datatype\",\"title\":\"dataType (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-dynamicasset\",\"title\":\"dynamicAsset (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-editorlayout\",\"title\":\"editorLayout\",\"section\":\"Types\"},{\"slug\":\"types-editorpanel\",\"title\":\"editorPanel\",\"section\":\"Types\"},{\"slug\":\"types-effect\",\"title\":\"effect\",\"section\":\"Types\"},{\"slug\":\"types-font\",\"title\":\".font asset type\",\"section\":\"Types\"},{\"slug\":\"types-gaussiansplat\",\"title\":\"gaussianSplat\",\"section\":\"Types\"},{\"slug\":\"types-guide\",\"title\":\"guide\",\"section\":\"Types\"},{\"slug\":\"types-importer\",\"title\":\"Importer (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-inputbinding\",\"title\":\"inputBinding assetType\",\"section\":\"Types\"},{\"slug\":\"types-inputmacro\",\"title\":\"Input macro (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-inputmap\",\"title\":\"inputMap assetType\",\"section\":\"Types\"},{\"slug\":\"types-lightmapdata\",\"title\":\"lightmapData\",\"section\":\"Types\"},{\"slug\":\"types-loadingscreen\",\"title\":\"loadingScreen\",\"section\":\"Types\"},{\"slug\":\"types-material\",\"title\":\"Material (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-mesh\",\"title\":\"mesh\",\"section\":\"Types\"},{\"slug\":\"types-module\",\"title\":\"Module (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-nodebox\",\"title\":\"Nodebox (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-package\",\"title\":\"Package (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-pixeltemplate\",\"title\":\"pixelTemplate (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-plugin\",\"title\":\"plugin\",\"section\":\"Types\"},{\"slug\":\"types-population\",\"title\":\"population\",\"section\":\"Types\"},{\"slug\":\"types-preset\",\"title\":\"Preset (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-procgraph\",\"title\":\"procGraph (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-procnode\",\"title\":\"procNode: one reusable step of procedural generation\",\"section\":\"Types\"},{\"slug\":\"types-procpack\",\"title\":\"procPack — a family of procedural ops as content\",\"section\":\"Types\"},{\"slug\":\"types-procrecord\",\"title\":\"procRecord: a kind of record a graph makes and saves\",\"section\":\"Types\"},{\"slug\":\"types-renderfeature\",\"title\":\"renderFeature\",\"section\":\"Types\"},{\"slug\":\"types-rig\",\"title\":\"rig asset type\",\"section\":\"Types\"},{\"slug\":\"types-scene\",\"title\":\"Scene (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-scenemodule\",\"title\":\"sceneModule — a part of a scene, authored as code\",\"section\":\"Types\"},{\"slug\":\"types-service\",\"title\":\"Service (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-servicerun\",\"title\":\"serviceRun\",\"section\":\"Types\"},{\"slug\":\"types-shader\",\"title\":\"Shader (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-shadermodule\",\"title\":\"Shader module (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-soundclip\",\"title\":\"soundClip\",\"section\":\"Types\"},{\"slug\":\"types-style\",\"title\":\"Style (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-terraindata\",\"title\":\"terrainData\",\"section\":\"Types\"},{\"slug\":\"types-testsuite\",\"title\":\"Test Suite (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-texture\",\"title\":\"texture\",\"section\":\"Types\"},{\"slug\":\"types-tool\",\"title\":\"tool (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-toolbox\",\"title\":\"toolbox (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-voxeltemplate\",\"title\":\"Voxel Template (asset type)\",\"section\":\"Types\"},{\"slug\":\"types-workflow\",\"title\":\"workflow\",\"section\":\"Types\"}],\"body\":{\"Page\":{\"slug\":\"api-modules\",\"title\":\"modules\",\"description\":\"modules is a Luau API namespace of the Zero engine, holding 2429 functions, among them getInstructions, getManifest, getReadme, inspect and onChange.\",\"section\":\"API Reference\",\"tags\":[\"api\",\"reference\"],\"word_count\":316699,\"published_at\":\"2026-07-25T13:50:03.374777972Z\",\"created_at\":\"2026-07-25T13:50:03.374964281Z\",\"updated_at\":\"2026-10-28T19:05:46.463162228Z\",\"outline\":[{\"level\":2,\"id\":\"modules-agentskillassettyperef-readme\",\"text\":\"modules/AgentSkillAssetTypeRef/README\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-getinstructions\",\"text\":\"modules/AgentSkillAssetTypeRef/getInstructions\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-getmanifest\",\"text\":\"modules/AgentSkillAssetTypeRef/getManifest\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-getreadme\",\"text\":\"modules/AgentSkillAssetTypeRef/getReadme\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-inspect\",\"text\":\"modules/AgentSkillAssetTypeRef/inspect\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-onchange\",\"text\":\"modules/AgentSkillAssetTypeRef/onChange\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-ondelete\",\"text\":\"modules/AgentSkillAssetTypeRef/onDelete\"},{\"level\":2,\"id\":\"modules-agentskillassettyperef-onregister\",\"text\":\"modules/AgentSkillAssetTypeRef/onRegister\"},{\"level\":2,\"id\":\"modules-animationassettypebehavior-readme\",\"text\":\"modules/AnimationAssetTypeBehavior/README\"},{\"level\":2,\"id\":\"modules-animationassettypebehavior-onchange\",\"text\":\"modules/AnimationAssetTypeBehavior/onChange\"},{\"level\":2,\"id\":\"modules-animationassettypebehavior-reindexsearchmeta\",\"text\":\"modules/AnimationAssetTypeBehavior/reindexSearchMeta\"},{\"level\":2,\"id\":\"modules-assetchangedispatch-readme\",\"text\":\"modules/AssetChangeDispatch/README\"},{\"level\":2,\"id\":\"modules-assetchangedispatch-dispatch\",\"text\":\"modules/AssetChangeDispatch/dispatch\"},{\"level\":2,\"id\":\"modules-assetchangedispatch-dispatchdelete\",\"text\":\"modules/AssetChangeDispatch/dispatchDelete\"},{\"level\":2,\"id\":\"modules-assetref-readme\",\"text\":\"modules/AssetRef/README\"},{\"level\":2,\"id\":\"modules-assetref-build\",\"text\":\"modules/AssetRef/build\"},{\"level\":2,\"id\":\"modules-assetref-caninstantiate\",\"text\":\"modules/AssetRef/canInstantiate\"},{\"level\":2,\"id\":\"modules-assetref-canpreview\",\"text\":\"modules/AssetRef/canPreview\"},{\"level\":2,\"id\":\"modules-assetref-deps\",\"text\":\"modules/AssetRef/deps\"},{\"level\":2,\"id\":\"modules-assetref-flushpendingpersists\",\"text\":\"modules/AssetRef/flushPendingPersists\"},{\"level\":2,\"id\":\"modules-assetref-forgetruntime\",\"text\":\"modules/AssetRef/forgetRuntime\"},{\"level\":2,\"id\":\"modules-assetref-loadtypebehavior\",\"text\":\"modules/AssetRef/loadTypeBehavior\"},{\"level\":2,\"id\":\"modules-assetref-loadtypebehaviorof\",\"text\":\"modules/AssetRef/loadTypeBehaviorOf\"},{\"level\":2,\"id\":\"modules-assetref-loadtypemodule\",\"text\":\"modules/AssetRef/loadTypeModule\"},{\"level\":2,\"id\":\"modules-assetref-persistineditmode\",\"text\":\"modules/AssetRef/persistInEditMode\"},{\"level\":2,\"id\":\"modules-assetref-preview\",\"text\":\"modules/AssetRef/preview\"},{\"level\":2,\"id\":\"modules-assetref-requirekeyof\",\"text\":\"modules/AssetRef/requireKeyOf\"},{\"level\":2,\"id\":\"modules-assetref-requiretypemodule\",\"text\":\"modules/AssetRef/requireTypeModule\"},{\"level\":2,\"id\":\"modules-assetref-typemodulekeys\",\"text\":\"modules/AssetRef/typeModuleKeys\"},{\"level\":2,\"id\":\"modules-assetrefshapes-readme\",\"text\":\"modules/AssetRefShapes/README\"},{\"level\":2,\"id\":\"modules-assetrefshapes-ensure\",\"text\":\"modules/AssetRefShapes/ensure\"},{\"level\":2,\"id\":\"modules-assetrefshapes-install\",\"text\":\"modules/AssetRefShapes/install\"},{\"level\":2,\"id\":\"modules-assetrefshapes-instancereturnsof\",\"text\":\"modules/AssetRefShapes/instanceReturnsOf\"},{\"level\":2,\"id\":\"modules-assetrefshapes-invalidate\",\"text\":\"modules/AssetRefShapes/invalidate\"},{\"level\":2,\"id\":\"modules-assetrefshapes-publish\",\"text\":\"modules/AssetRefShapes/publish\"},{\"level\":2,\"id\":\"modules-assetrefshapes-published\",\"text\":\"modules/AssetRefShapes/published\"},{\"level\":2,\"id\":\"modules-assetrefshapes-shapeof\",\"text\":\"modules/AssetRefShapes/shapeOf\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-readme\",\"text\":\"modules/AssetTypeAssetTypeRef/README\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-getabout\",\"text\":\"modules/AssetTypeAssetTypeRef/getAbout\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-getreadme\",\"text\":\"modules/AssetTypeAssetTypeRef/getReadme\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-inspect\",\"text\":\"modules/AssetTypeAssetTypeRef/inspect\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-onchange\",\"text\":\"modules/AssetTypeAssetTypeRef/onChange\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-oncreate\",\"text\":\"modules/AssetTypeAssetTypeRef/onCreate\"},{\"level\":2,\"id\":\"modules-assettypeassettyperef-validate\",\"text\":\"modules/AssetTypeAssetTypeRef/validate\"},{\"level\":2,\"id\":\"modules-assettypedeclaration-readme\",\"text\":\"modules/AssetTypeDeclaration/README\"},{\"level\":2,\"id\":\"modules-assettypedeclaration-forget\",\"text\":\"modules/AssetTypeDeclaration/forget\"},{\"level\":2,\"id\":\"modules-assettypedeclaration-of\",\"text\":\"modules/AssetTypeDeclaration/of\"},{\"level\":2,\"id\":\"modules-assettypepreviewrecipe-readme\",\"text\":\"modules/AssetTypePreviewRecipe/README\"},{\"level\":2,\"id\":\"modules-assettypepreviewrecipe-forget\",\"text\":\"modules/AssetTypePreviewRecipe/forget\"},{\"level\":2,\"id\":\"modules-assettypepreviewrecipe-of\",\"text\":\"modules/AssetTypePreviewRecipe/of\"},{\"level\":2,\"id\":\"modules-assettyperuntime-readme\",\"text\":\"modules/AssetTypeRuntime/README\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-readme\",\"text\":\"modules/AssetTypeSharedInspector/README\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-blockfor\",\"text\":\"modules/AssetTypeSharedInspector/blockFor\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-blockproblem\",\"text\":\"modules/AssetTypeSharedInspector/blockProblem\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-declarationfor\",\"text\":\"modules/AssetTypeSharedInspector/declarationFor\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-declarationstamp\",\"text\":\"modules/AssetTypeSharedInspector/declarationStamp\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-declarestypeinspector\",\"text\":\"modules/AssetTypeSharedInspector/declaresTypeInspector\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-instancedeclarationfor\",\"text\":\"modules/AssetTypeSharedInspector/instanceDeclarationFor\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-loadin\",\"text\":\"modules/AssetTypeSharedInspector/loadIn\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-requiredseverity\",\"text\":\"modules/AssetTypeSharedInspector/requiredSeverity\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-shapeproblem\",\"text\":\"modules/AssetTypeSharedInspector/shapeProblem\"},{\"level\":2,\"id\":\"modules-assettypesharedinspector-typedeclaration\",\"text\":\"modules/AssetTypeSharedInspector/typeDeclaration\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-readme\",\"text\":\"modules/AssetTypeSharedSettings/README\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-acceptedfieldnames\",\"text\":\"modules/AssetTypeSharedSettings/acceptedFieldNames\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-applycorrections\",\"text\":\"modules/AssetTypeSharedSettings/applyCorrections\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-applyderiveddefaults\",\"text\":\"modules/AssetTypeSharedSettings/applyDerivedDefaults\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-blockfor\",\"text\":\"modules/AssetTypeSharedSettings/blockFor\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-clonevalues\",\"text\":\"modules/AssetTypeSharedSettings/cloneValues\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-creationinputsof\",\"text\":\"modules/AssetTypeSharedSettings/creationInputsOf\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-defaultsof\",\"text\":\"modules/AssetTypeSharedSettings/defaultsOf\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-derivedhookof\",\"text\":\"modules/AssetTypeSharedSettings/derivedHookOf\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-difffor\",\"text\":\"modules/AssetTypeSharedSettings/diffFor\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-fieldnames\",\"text\":\"modules/AssetTypeSharedSettings/fieldNames\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-forgetchangebaseline\",\"text\":\"modules/AssetTypeSharedSettings/forgetChangeBaseline\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-heallegacyifneeded\",\"text\":\"modules/AssetTypeSharedSettings/healLegacyIfNeeded\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-isforeignorigin\",\"text\":\"modules/AssetTypeSharedSettings/isForeignOrigin\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-mergevalues\",\"text\":\"modules/AssetTypeSharedSettings/mergeValues\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-normalizefor\",\"text\":\"modules/AssetTypeSharedSettings/normalizeFor\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-normalizehookof\",\"text\":\"modules/AssetTypeSharedSettings/normalizeHookOf\"},{\"level\":2,\"id\":\"modules-assettypesharedsettings-orderednames\",\"text\":\"modules/AssetTypeSharedSettings/orderedNames\"}],\"outline_total\":3711}}}";</script><script nonce="3d1FZ0p4P5GAxDSnyQL3Iw">__INCOMPLETE_CHUNKS=[];</script>