typed
The typed namespace: 2256 functions.
typed/builtin//assetTypes/assetType/shared/ref/M/build
M.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
envelopeany(optional) — The freshly-built envelope table.
Returns any — The same table with AssetRefMT attached. Returning the table rather than relying on side-effects makes the Rust factory's pcall-then-replace flow simpler.
local r = require("@builtin::assetTypes.assetType.shared.ref").build({ type = "material", path = "/zero/source/Gold.material", ... })
typed/builtin//assetTypes/assetType/shared/ref/M/flushPendingPersists
M.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()
typed/builtin//assetTypes/assetType/shared/ref/M/forgetRuntime
M.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
guidstring— The asset's stable guid; the copy the world holds under it is the one forgotten.
Returns boolean — True when there was runtime state to forget.
require("@builtin::assetTypes.assetType.shared.ref").forgetRuntime(ref.guid)
typed/builtin//assetTypes/assetType/shared/ref/M/loadTypeBehavior
M.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_typestring— The type name (e.g."dynamicAsset","material").
Returns ({ [string]: any }?, string?) — The type module table, or nil. The load error, or nil.
local mod, err = require("@builtin::assetTypes.assetType.shared.ref").loadTypeBehavior("dialogue")
typed/builtin//assetTypes/assetType/shared/ref/M/loadTypeModule
M.loadTypeModule(asset_type: string) -> { [string]: any }?
Load the full behavior.luau module table for an asset type
({ ref?, global?, onChange? }), 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_typestring— The type name (e.g."dynamicAsset","material").
Returns { [string]: any }? — The type module table, or nil.
local m = require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("dynamicAsset")
typed/builtin//assetTypes/assetType/shared/ref/M/persistInEditMode
M.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
selfany(optional) — Any AssetRef.
require("@builtin::assetTypes.assetType.shared.ref").persistInEditMode(matRef)
typed/builtin//assetTypes/assetType/shared/ref/M/requireKeyOf
M.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
refany(optional) — The asset ref.
Returns string? — The require key, or nil when the ref names nothing.
local ok, mod = pcall(require, AssetRef.requireKeyOf(moduleRef))
typed/builtin//assetTypes/assetType/shared/ref/M/requireTypeModule
M.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
identitystring(optional) — The asset's own resolved identity (ref.identity), or nil.pathstring(optional) — The asset's own resolved folder path (ref.path), or nil.stemstring(optional) — The module's file stem (its filename with.luaustripped), 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.
Returns (any?, string?) — The loaded module, or nil. The load error, when a module existed at some route but failed to load — the require key and what it raised.
local schema, err = require("@builtin::assetTypes.assetType.shared.ref").requireTypeModule(identity, path, "settings")
typed/builtin//assetTypes/assetType/shared/ref/M/typeModuleKeys
M.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
identitystring(optional) — The asset's own resolved identity (ref.identity), or nil.pathstring(optional) — The asset's own resolved folder path (ref.path), or nil.stemstring(optional) — The module's file stem, or nil for the asset's entry module.
Returns { string } — The keys.
for _, key in ipairs(AssetRef.typeModuleKeys(ref.identity, ref.path, "schema")) do print(library.has(key)) end
typed/builtin//assetTypes/assetType/shared/ref/assetRef/canInstantiate
assetRef.canInstantiate(self) -> boolean
Whether this asset can be instantiated into a scene, which is true exactly when its type defines an instantiate method.
Parameters
self
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/canPreview
assetRef.canPreview(self) -> boolean
Whether this asset can render a preview, which is true when its type declares a preview recipe in its type.yaml, defines its own preview, or defines an instantiate method.
Parameters
self
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/cpu_resident
assetRef.cpu_resident -> boolean
Whether this asset's bytes are warm in memory for a live script-component context.
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/deps
assetRef.deps(self) -> { deps: { any }, unresolved_deps: { any }, problems: { any } }
This asset's outbound references, the literals nothing answered, and the problems attached to it.
Parameters
self
Returns { deps: { any }, unresolved_deps: { any }, problems: { any } }
typed/builtin//assetTypes/assetType/shared/ref/assetRef/events
assetRef.events -> { [string]: any }?
The subscribe-only view over the events this asset's type declares, each entry carrying connect / once / wait.
Returns { [string]: any }?
typed/builtin//assetTypes/assetType/shared/ref/assetRef/exists
assetRef.exists(self) -> boolean
Whether the VFS holds this asset at its path: a folder asset's folder or a loose asset's file.
Parameters
self
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/getBytes
assetRef.getBytes(self, filename: string?) -> string?
This asset's content bytes, or the bytes of one named file inside a composite asset.
Parameters
selffilenamestring(optional)
Returns string?
typed/builtin//assetTypes/assetType/shared/ref/assetRef/getSource
assetRef.getSource(self, filename: string?) -> string?
This asset's content bytes, or the bytes of one named file inside a composite asset.
Parameters
selffilenamestring(optional)
Returns string?
typed/builtin//assetTypes/assetType/shared/ref/assetRef/getText
assetRef.getText(self, filename: string?) -> string?
This asset's content bytes, or the bytes of one named file inside a composite asset.
Parameters
selffilenamestring(optional)
Returns string?
typed/builtin//assetTypes/assetType/shared/ref/assetRef/gpu_resident
assetRef.gpu_resident -> boolean
Whether the device holds a texture or mesh under this asset's guid.
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/has_backing_asset
assetRef.has_backing_asset -> boolean
Whether a /zero/source/ asset backs this ref.
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/has_runtime_changes
assetRef.has_runtime_changes -> boolean
Whether a runtime copy of this asset exists under /zero/runtime/assets/.
Returns boolean
typed/builtin//assetTypes/assetType/shared/ref/assetRef/meta
assetRef.meta -> { [string]: any }?
This asset's .meta sidecar, parsed.
Returns { [string]: any }?
typed/builtin//assetTypes/assetType/shared/ref/assetRef/modules
assetRef.modules -> { [string]: any }?
The shared modules this asset's type ships, reached as ref.modules.<name>.
Returns { [string]: any }?
typed/builtin//assetTypes/assetType/shared/ref/assetRef/preview
assetRef.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.
Parameters
selfopts{ [string]: any }(optional)
Returns { [string]: any }
typed/builtin//assetTypes/assetType/shared/ref/assetRef/runtime
assetRef.runtime -> { [string]: any }
The live per-asset table every resolver of this asset shares, for values that do not round-trip through the asset's bytes.
Returns { [string]: any }
typed/builtin//assetTypes/assetType/shared/ref/assetRef/typeRef
assetRef.typeRef -> string?
The guid of the asset type this asset was authored against, read from its .refs sidecar.
Returns string?
typed/builtin//assetTypes/font/behavior/M/onChange
M.onChange(self: any?, change: any?)
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
selfany(optional) — The.fontinstance's AssetRef.changeany(optional) —{ path, asset, kind, ... }: the written file and how it arrived.
typed/builtin//assetTypes/font/behavior/M/onRegister
M.onRegister(self: any?)
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
selfany(optional)
typed/builtin//assetTypes/plugin/behavior/M/onChange
M.onChange(self: any?, change: any?)
Load the plugin again when its component or its manifest changes.
Parameters
selfany(optional) — TheAssetRef<plugin>whose files changed.changeany(optional) —{ path, kind, origin }describing the write.
typed/builtin//assetTypes/plugin/behavior/M/onDelete
M.onDelete(self: any?)
Stop the plugin this asset loaded, because the asset is gone.
Parameters
selfany(optional) — TheAssetRef<plugin>that was removed.
typed/builtin//assetTypes/plugin/behavior/M/onRegister
M.onRegister(self: any?)
Load the plugin this asset carries.
Parameters
selfany(optional) — TheAssetRef<plugin>registering.
typed/builtin//assetTypes/scene/shared/build/M/attribute
M.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 arrayentity.capturehands back.
Returns { Refusal } — The same entries with source filled in where a frame names one.
local named = SceneBuild.attribute(select(4, entity.capture(fn)))
typed/builtin//assetTypes/scene/shared/build/M/buildSurface
M.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
folderstring— The build's own folder, which the assets it produces are authored inside.sourceDigeststring— The digest of the build script running now, asM.digestreports it — what decides whether an asset it produced is still the asset the code states.
Returns BuildSurface — The table bound as the build global for that build.
local surface = SceneBuild.buildSurface(dir, SceneBuild.digest(src))
typed/builtin//assetTypes/scene/shared/build/M/digest
M.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
sourcestring— The script body to digest.
Returns string — The digest, as a hex string.
local key = SceneBuild.digest(vfs.read(path))
typed/builtin//assetTypes/scene/shared/build/M/drift
M.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
ownerstring(optional) — Optional build name, asM.ownerOfreports it, to read just that build. Omitted, every build-owned entity in the scene is read.
Returns { Drift } — Array of { entity, name, owner, property, baked, live }, one per drifted property, sorted by entity then property.
for _, d in ipairs(SceneBuild.drift()) do print(d.name, d.property) end
typed/builtin//assetTypes/scene/shared/build/M/notePreview
M.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
entityIdstring— Runtime entity id of the owned entity.values{ [string]: any }— The values the entity carries now, by name.componentTypestring(optional) — The component that states them, so a rebuild knows to state that type again instead of leaving it to the scene.
Returns nil
SceneBuild.notePreview(id, { count = 9 }, "SceneModule")
typed/builtin//assetTypes/scene/shared/build/M/ownerOf
M.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
entityIdstring— Runtime entity id.
Returns string? — The owner the reconcile that placed it was called with, or nil.
if SceneBuild.ownerOf(id) ~= nil then print("a build states this") end
typed/builtin//assetTypes/scene/shared/build/M/previewedComponentType
M.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
entityIdstring— Runtime entity id of the owned entity.
Returns string? — The component type name, or nil.
if SceneBuild.previewedComponentType(id) == "SceneModule" then end
typed/builtin//assetTypes/scene/shared/build/M/reconcile
M.reconcile(records: { any }, target: EntityRef | string | { guid: string }, owner: string, opts: {
participation: string?,
source: string?,
frameBudget: number?,
handBack: (() -> boolean)?,
record: boolean?,
}?) -> ({ [string]: string }, number)
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. 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.
Parameters
records{ any }— Flat record array, parents before children — whatM.runreturns.targetEntityRef | string | { guid: string }— Entity ref, entity id, or scene layer every root record lands in.ownerstring— Name of the build, unique among the builds sharing this target.opts{ participation: string?, source: string?, frameBudget: number?, handBack: (() -> boolean)?, record: boolean?, }(optional) —participationsets the lifecycle mode every placed entity takes, ahead of the mode any record carries.sourcenames the file the build is written in, whichM.sourceOfreports for every entity the build places.frameBudgetspreads the reconcile over frames: each frame applies at most that many records' worth of work (a record placed, its components, its active flag, an entity despawned each count one) and the rest waits for the frames after. It takes effect where the caller can yield, the scheduler task a build runs in; elsewhere the reconcile lands in the one frame.handBackspreads it by time instead, for a caller that owns when its frames are handed back: the reconcile applies records until this frame's slice (FrameBudget) is spent, then callshandBack, which hands the frame back and answers true, or answers false when the caller cannot, and the reconcile then lands the rest in the frame it is in. A scene load passesSceneLoader.handBackHere.frameBudgettakes precedence. Across each frame a spread reconcile hands back, other code runs: a reconcile of the sameownerat the sametargetstarted in those frames lands what the build states, and the earlier one returns at its next frame boundary with what it had landed; an entitytargettaken away in them stops the reconcile there with an error naming it. A reconcile that states no records while a scene loads, over entities the build placed before, raises and leaves them: a build that stated nothing during a load did not run to the end, and a rebuild after the load applies an empty statement.record(default true) states to the scene saver every entity the reconcile creates, changes or takes away, so the scene's unsaved edits carry them;record = falselands them in the live scene alone, for a caller that decides itself what the scene's files hold.
Returns ({ [string]: string }, number) — The ids this reconcile landed on, keyed by record identity, and how many of them it had to create. A reconcile that created nothing landed on entities the saved scene already holds; one that created something is why the saved scene is now behind the live one.
local ids = SceneBuild.reconcile(records, root, "chairs")
local ids, created = SceneBuild.reconcile(records, layer, "build")
SceneBuild.reconcile(SceneBuild.run(build), layers.active, "build")
typed/builtin//assetTypes/scene/shared/build/M/recordDrift
M.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
recany(optional) — An entity record:name,transform = { position, rotation, scale }andattributes.
Returns { string } — The drifted property names, sorted (n name, p position, r rotation, s scale, a:<name> an attribute). Empty for a record that holds what its build stated, and for a record no build placed.
local drifted = SceneBuild.recordDrift(record)
typed/builtin//assetTypes/scene/shared/build/M/run
M.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.
Returns ({ any }, { Refusal }) — Flat array of records, parents before children, siblings in the order the builder created them; and the array of refusals the scope issued, each with message, at and the source frame naming who was refused.
local records, refused = SceneBuild.run(function() entity.spawn("chair") end)
typed/builtin//assetTypes/scene/shared/build/M/sourceOf
M.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
ownerstring— Build name, asM.ownerOfreports it.
Returns string? — Path of the file the build is written in, or nil.
local file = SceneBuild.sourceOf(SceneBuild.ownerOf(id))
typed/builtin//assetTypes/scene/shared/build/M/states
M.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
ownerstring— Build name, asM.ownerOfreports it.keystring— The record identity, as the entity'sbuildKeyattribute holds it.
Returns boolean? — True when the build's last run placed an entity for key; false when it ran and placed none, or another build has since taken the build's placement out of the scene; nil when the build has not run in this session.
local stillStated = SceneBuild.states(owner, key)
typed/builtin//assetTypes/scene/shared/build/M/takePreview
M.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
entityIdstring— Runtime entity id of the owned entity.
Returns { [string]: any }? — The recorded values by name, or nil when none are waiting.
local set = SceneBuild.takePreview(id)
typed/builtin//editor/edui/editorPanel/behavior/M/onChange
M.onChange(self: any?, change: any?)
Parameters
selfany(optional)changeany(optional)
typed/builtin//editor/edui/editorPanel/behavior/M/onDelete
M.onDelete(self: any?)
Parameters
selfany(optional)
typed/builtin//editor/edui/editorPanel/behavior/M/onRegister
M.onRegister(self: any?)
Parameters
selfany(optional)
typed/builtin//importers/plugin/M/canImport
M.canImport(path: string, bytes: string) -> boolean
Claim a loose .wasm OUTSIDE any .plugin/ (-> promote). A component
already inside a .plugin/ is that asset's own content and is NOT claimed:
re-importing it would wrap a plugin in a plugin.
Parameters
pathstring— Source file path inside the VFS.bytesstring— Raw file contents, checked for the WebAssembly preamble.
Returns boolean — Boolean — true when this importer wants to promote the file.
typed/builtin//importers/plugin/M/import
M.import(ctx: any?) -> any
Promote a loose .wasm into a managed <stem>.plugin/ asset. The
manifest, README and .metadata come from the plugin type's own scaffold;
this moves the component in and takes the loose file away.
Parameters
ctxany(optional) — { path, bytes, extension, stem, dir }
Returns any — A result table on success, or nil when nothing needed doing.
typed/builtin//modules/api/editor/agents/_ended
_ended(slot: number, termId: number) -> boolean
Deliver the ending a drain would have delivered for a slot whose program was closed in its pane — the one ending that brings nothing back and spawns no process. Served only by an engine that booted offline.
Parameters
slotnumber— Slot index.termIdnumber— The slot's terminal.
Returns boolean — Whether the ending was delivered.
typed/builtin//modules/api/editor/agents/_spawnPending
_spawnPending(pending: boolean) -> boolean
Hold a spawn as being on its way up, or release it — the window a slot spends waiting on the agent host before it can spawn. The scheduled watch reads it, so this is how a test asks what the watch does during it. Served only by an engine that booted offline.
Parameters
pendingboolean— True to hold a spawn in flight, false to release it.
Returns boolean — Whether the state was taken.
typed/builtin//modules/api/editor/agents/_spawned
_spawned(slot: number, termId: number) -> boolean
Leave the record a successful spawn leaves — the slot registered, the claim reported, the watch armed — for a terminal the caller supplies, without launching a process. Served only by an engine that booted offline.
Parameters
slotnumber— Slot index.termIdnumber— Terminal the slot's run would have been in.
Returns boolean — Whether the record was left.
typed/builtin//modules/api/editor/agents/chooseEffort
chooseEffort(agent: string, effort: string, offered: { any }, suggested: string?) -> any
Keep effort as the person's choice for agent, on the model the
agent runs on. An effort that model does not take is refused with the
efforts it does take.
Parameters
agentstring— The agent's id.effortstring— The effort to run at (low,medium,high,xhigh,max, or any other the model lists).offered{ any }— The models the agent offers, asmodelChoicetakes them.suggestedstring(optional) — The model the agent itself would run on.
Returns any — { ok = true, choice } or { ok = false, reason, efforts? }.
typed/builtin//modules/api/editor/agents/chooseModel
chooseModel(agent: string, model: string, offered: { any }, suggested: string?) -> any
Keep model as the person's choice for agent. A model the offer
does not list is refused with the models it does list; it is never
replaced by another. A kept effort the new model does not take is dropped
and named in effortDropped.
Parameters
agentstring— The agent's id.modelstring— The model to run on.offered{ any }— The models the agent offers, asmodelChoicetakes them.suggestedstring(optional) — The model the agent itself would run on.
Returns any — { ok = true, choice, effortDropped? }, choice being what modelChoice now answers, or { ok = false, reason, offered? }.
typed/builtin//modules/api/editor/agents/deliver
deliver(i: number, message: any?) -> any
Queue a current-world event for a slot: { text, id?, actionable? }. An actionable event (the default) wakes an idle agent once it can act, and a busy agent receives it when its CLI accepts another prompt; with actionable false it joins the agent's context without starting a turn. A stable id makes retries idempotent. Check ok before considering a message queued.
Parameters
inumbermessageany(optional)
Returns any
typed/builtin//modules/api/editor/agents/deliveryStatus
deliveryStatus(i: number) -> any
Delivery state for this slot: event phase, pending count, whether a pending event wakes the agent (wakes), whether a Codex slot's engine tools have connected (toolsReady), reactive toggle, watched tasks and errors. contextActive reports request-time snapshots; contextRequests counts injected requests and contextError describes provider availability.
Parameters
inumber
Returns any
typed/builtin//modules/api/editor/agents/ensureSlot
ensureSlot(i: number)
Ensure slot i's agent process is running, spawning it (and waiting on
the agent host's boot dependency) when it is not. Opening a slot's panel
calls this; calling it again on a running slot is a no-op, so it is also
how a panel relaunches an exited agent.
Parameters
inumber
typed/builtin//modules/api/editor/agents/label
label(i: number) -> string
The name slot i carries: its harness's own name — "Claude Code",
"Codex CLI", "Cursor CLI" — numbered only where another slot seats the
same harness. This is what an agent tab and the Agents menu are titled by.
Parameters
inumber— The slot number, from 1 toslotCount().
Returns string — The slot's label.
print(agents.slotLabel(2)) -- the title of the panel "agent2"
typed/builtin//modules/api/editor/agents/modelChoice
modelChoice(agent: string, offered: { any }, suggested: string?) -> any
The model and the effort the engine runs agent on, judged against
what the agent offers right now. The person's own choice comes first, then
the engine's default for the agent (DotBot on Claude: claude-sonnet-5-5),
then the model the agent suggests, then the first it offers; the effort is
the person's choice when the model takes it, else high when it takes
that. The choice is kept for the person's account on this device, across
conversations and restarts.
Parameters
agentstring— The agent's id, as its host names it (dotbot-claude,dotbot-chatgpt,dotbot). An agent on a further account of its provider (dotbot-claude~2) shares the agent's default and choice.offered{ any }— The models the agent offers: a list of{ id, efforts?, defaultEffort? },effortsnaming the efforts that model takes anddefaultEffortthe one the model states for itself.suggestedstring(optional) — The model the agent itself would run on, used when neither the person nor the engine names one the offer holds.
Returns any — { ok, agent, model, effort, modelFrom, effortFrom, efforts, kept, notOffered?, effortNotTaken?, storeError? }. modelFrom is person, engine or offer; effortFrom is person, engine or model; kept is the person's stored { model?, effort? }, whether or not the offer holds it. notOffered / effortNotTaken name a kept choice the offer no longer holds, with the reason. storeError says why the kept choice could not be read, and the engine's default answers in its place. { ok = false, reason } for an agent id or an offer of the wrong shape. An empty offer answers no model.
typed/builtin//modules/api/editor/agents/resetModelChoice
resetModelChoice(agent: string) -> any
Forget the person's choice of model and effort for agent, so the
engine's default applies again.
Parameters
agentstring— The agent's id.
Returns any — { ok = true } or { ok = false, reason }.
typed/builtin//modules/api/editor/agents/setReactive
setReactive(i: number, enabled: boolean) -> any
Enable automatic engine-event delivery, or pause it. Paused, the slot's notices reach it on its tool results, and its own messages and task completions stay queued until delivery resumes.
Parameters
inumberenabledboolean
Returns any
typed/builtin//modules/api/editor/agents/slotCount
slotCount() -> number
How many concurrent agent slots the editor offers.
Returns number — The slot count.
typed/builtin//modules/api/editor/agents/slotStatus
slotStatus(i: number) -> any
Slot i's terminal and live state, for a panel to render: drains the
slot lifecycle events, then reports the backing terminal id (created on
first ask), whether its process is running, the slot's display label, how
many automatic relaunches it has spent, whether this host has the slot's
harness, and the step and the site that install it.
Parameters
inumber
Returns any — {termId, running, label, relaunches, installed, install, site}.
typed/builtin//modules/api/editor/agents/spec
spec(kickoffPrompt: string?, slot: number?) -> any
The process spec the launcher runs for a slot's harness — the command,
its flags, its working directory, and the environment it inherits, with
the agent's bearer replaced by <redacted> wherever the spec carries it.
Parameters
kickoffPromptstring(optional) — Text submitted as the agent's first message.slotnumber(optional) — Which slot to build for: its harness, and the caller id that slot's agent presents to the engine on every call. The first slot when omitted; a number the dock has no slot for is refused.
Returns any — A terminal.spawn spec: {cmd, args, cwd, env, envRemove}, plus files for an agent whose configuration is read from its working directory rather than from its flags.
typed/builtin//modules/api/editor/agents/watchSlotExits
watchSlotExits() -> boolean
Watch every slot's ending on a schedule, so an agent that ends while its tab is not the drawn one is still noticed — brought back if that is what its ending calls for (the same relaunch a drawn pane would have run), and reported as no longer working either way. A spawn starts this and every later spawn starts it again; it ends on a tick with no slot registered and no spawn on its way up, and one chain holds it at a time.
Returns boolean — True when this call took the watch, false when this module already held it.
typed/builtin//modules/api/engine/animation/animation/animating
animation.animating() -> { AnimationBody }
The bodies the engine measured a changing pose on — what is animating right now.
Returns { AnimationBody } — An array of AnimationBody.
for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end
typed/builtin//modules/api/engine/animation/animation/bodies
animation.bodies() -> { AnimationBody }
Every body the engine holds animation state for.
Returns { AnimationBody } — An array of AnimationBody.
for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end
typed/builtin//modules/api/engine/animation/animation/body
animation.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
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns AnimationBody? — An AnimationBody, or nil.
local b = animation.body(hero.id); print(b and b.reason)
typed/builtin//modules/api/engine/animation/animation/clips
animation.clips(entityId: string | EntityRef) -> { AnimationClip }
The clips contributing to a body's pose right now, with their playheads and their retarget coverage.
Parameters
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns { AnimationClip } — An array of AnimationClip.
for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end
typed/builtin//modules/api/engine/animation/animation/coverage
animation.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
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns (number, number) — (matched, total).
local m, t = animation.coverage(hero.id); print(m .. "/" .. t)
typed/builtin//modules/api/engine/animation/animation/declare
animation.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
entityIdstring— 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 = {} })
typed/builtin//modules/api/engine/animation/animation/forget
animation.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
entityIdstring— The body to drop.
animation.forget(body.id)
typed/builtin//modules/api/engine/animation/animation/observe
animation.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.
Returns AnimationObservation — An AnimationObservation.
local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end
typed/builtin//modules/api/engine/animation/animation/whyStill
animation.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
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns (string?, string?) — (reason, detail).
local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end
typed/builtin//modules/api/engine/asset/asset/add_tag
asset.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
refRefArg— Any name the asset has.tagstring— Tag to add.
asset.add_tag("brick", "wip")
typed/builtin//modules/api/engine/asset/asset/alias
asset.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
refRefArg— The asset gaining the name.aliasstring— The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
Returns boolean — True when newly added, false when the asset already answered to it.
asset.alias("@builtin::shaders.pbr", "standard")
typed/builtin//modules/api/engine/asset/asset/aliases
asset.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
refRefArg— Any name the asset has.
Returns { string } — Array of alias names in canonical identity form.
for _, n in asset.aliases("pbr") do print(n) end
typed/builtin//modules/api/engine/asset/asset/canCreate
asset.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
typeNamestring— Registered asset type (e.g. "material", "scene").
Returns boolean — true when asset.create(typeName, …) can produce one.
if asset.canCreate(kind) then asset.create(kind, name) end
typed/builtin//modules/api/engine/asset/asset/categories
asset.categories() -> { string }
List every asset category the engine currently recognises.
Use to discover valid type argument values for the rest of
asset.*.
Returns { string } — Array of category names.
for _, c in asset.categories() do print(c) end
typed/builtin//modules/api/engine/asset/asset/containing
asset.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
pathstring— VFS path to inspect.
Returns AssetRef? — AssetRef handle, or nil.
local a = asset.containing("/source/scenes/main.scene/scene.json")
typed/builtin//modules/api/engine/asset/asset/cpuResident
asset.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
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true when a live context holds it.
if asset.cpuResident(ref) then print("bytes are warm") end
typed/builtin//modules/api/engine/asset/asset/create
asset.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
typeNamestring— Registered asset type to instance (e.g. "material", "texture").namestring— Destination asset name (becomes/source/<name>.<typeName>). A bare identity — passopts.folderto place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare:^[A-Za-z][A-Za-z0-9_]*$unless itsbehavior.luauexports anamePattern, asguidedoes to takegetting-startedand01-overview. This call names the category FIRST and the asset second. Every otherasset.*call taking both names them the other way round —asset.exists(name, category),asset.tryResolve(ref, category)— so a create-then-check pair readsif not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.opts{ [string]: any }(optional) — Optional table forwarded to the type'sonCreatehook, minus five framework keys consumed here and never seen by the hook:folder(a relative subfolder path under/sourceto author the asset in, with/between its folders as ingame/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), anddurable(file every content file this create writes on canonical/sourcewith 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, thatvfs.writetakes). Every type accepts all five whatever its own contract declares;asset.describe(typeName).frameworkParamsnames them, and a rejection prints them under the type's own parameters.
Returns (AssetRef, { durable: boolean, survivesRestart: boolean?, warning: string?, playShadow: string?, shadowed: { string }?, scratch: { string }? }) — The created asset's AssetRef, the SAME interned instance asset.resolve returns. ref.path is the VFS path the asset was written to (its folder: /zero/source/<folder>/<name>.<type>, the dest or into destination when one is given, /zero/runtime/assets/<identity>.<type> for a create filed in the ephemeral store), ref.primary the content file inside it (a shader's shader.wgsl, a module's init.luau), and both read and write with vfs.read / vfs.write or the read_file / write_file tools as they stand. ref.identity is the dotted name references use (<folder>.<name>, folder separators spelled .) and is what tostring(ref) prints. ref.guid is the stable id, and the type's ref methods (:getBytes, :ensureHandle, :serialize, ...) ride on it. Returned from execute, the ref reads back with all of these fields. Disk-only: nothing is uploaded to CPU/GPU. Where the bytes this call landed went, as a SECOND return value: { durable = true } when the files are where the call filed them, and { durable = false, survivesRestart = …, warning = …, playShadow = …, shadowed = { … } } when the play shadow took them: live in this session and undone by a guarded play-exit unless kept, with survivesRestart saying whether a restart of this engine keeps them, playShadow naming the routes that keep them and shadowed the paths. A create under /source/tmp/ answers durable = false with survivesRestart = false, a warning opening SCRATCH and scratch naming the paths: scratch reads back while the engine runs and is gone when it stops or restarts. durable is present whatever the answer is, so its absence is never a reading, and one call answers once for every file it wrote. It is the same answer, off the same shadow set and in the same words, that the MCP write_file / edit_file / capture tools attach to their own results — ask vfs.durability for it directly at any other write site.
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
typed/builtin//modules/api/engine/asset/asset/declareReferenceArg
asset.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
callstring— The callee as it is written at a call site.positionnumber— Which argument holds the name, counting from 1.assetTypestring
asset.declareReferenceArg("spawnModel", 3, "mesh")
typed/builtin//modules/api/engine/asset/asset/declareReferenceField
asset.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
callstring— The callee as it is written at a call site.fieldstring— The options-table field holding the name, read at the table's own level.assetTypestring
asset.declareReferenceField("fx.beam", "material", "material")
typed/builtin//modules/api/engine/asset/asset/declareReferenceKey
asset.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
assetTypestring— The category owning the file, e.g. "material".keystring— The top-level key holding the name(s).refTypestringvia("asset_ref" | "settings_of")(optional) — "asset_ref" or "settings_of".
asset.declareReferenceKey("material", "shader", "shader")
asset.declareReferenceKey("preset", "target", "*", "settings_of")
typed/builtin//modules/api/engine/asset/asset/deprecation
asset.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
refRefArg— Asset handle, identity, guid or VFS path.
Returns Deprecation.Deprecation? — { reason, replacedBy, via, source }, where via is "asset", "container" or "type" and source is the folder holding the marker; nil when the asset is not deprecated.
local d = asset.deprecation("@builtin::deprecated.deprecatedParticles.DeprecatedParticleEmitter")
typed/builtin//modules/api/engine/asset/asset/deps
asset.deps(ref: RefArg, type: string?) -> DepsResult
Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns DepsResult — { deps = { { asset_guid, origin, literal, via, ... } }, worlds = { { world, name, commit, sites } }, unresolved_deps, problems }.
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end
typed/builtin//modules/api/engine/asset/asset/describe
asset.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
typeNamestring— Registered asset type to describe (e.g. "texture").
Returns DescribeResult — the creation contract.
local contract = asset.describe("texture").contract
typed/builtin//modules/api/engine/asset/asset/diagnose
asset.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
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — DiagnoseRecord
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end
typed/builtin//modules/api/engine/asset/asset/exists
asset.exists(name: string, typeName: string) -> boolean
Parameters
namestringtypeNamestring
Returns boolean
typed/builtin//modules/api/engine/asset/asset/get_field
asset.get_field(ref: RefArg, key: string) -> any
Read one top-level field from the asset's .metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns any — Field value or nil.
local author = asset.get_field("brick", "author")
local tags = asset.get_field("tree", "tags") -- → a Luau table
typed/builtin//modules/api/engine/asset/asset/gpuResident
asset.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
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns boolean — true when the device holds it.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))
typed/builtin//modules/api/engine/asset/asset/guid
asset.guid(ref: RefArg, type: string?) -> string
Return the guid for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Guid.
local g = asset.guid("@builtin::components.Camera")
typed/builtin//modules/api/engine/asset/asset/has_field
asset.has_field(ref: RefArg, key: string) -> boolean
True when the asset's .metadata carries the named field.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns boolean — True when present.
if asset.has_field("brick", "author") then end
typed/builtin//modules/api/engine/asset/asset/has_tag
asset.has_tag(ref: RefArg, tag: string) -> boolean
True when the asset's .metadata.tags contains tag.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to check for.
Returns boolean — True when present.
if asset.has_tag("brick", "wip") then end
typed/builtin//modules/api/engine/asset/asset/identity
asset.identity(ref: RefArg, type: string?) -> string
Return the canonical identity for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Canonical identity.
local id = asset.identity("brick")
typed/builtin//modules/api/engine/asset/asset/import
asset.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
pathstring— 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.
Returns string? — The produced asset path, or nil when nothing claimed it.
local bundle = asset.import("/zero/source/generated/chest.glb")
local bundle = asset.import("/zero/source/generated/chest.glb", { importerSettings = { model = { keepSource = false } } })
typed/builtin//modules/api/engine/asset/asset/inspect
asset.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
refRefArg— AnAssetRef, an identity string, or a path.typestring(optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
Returns InspectRecord — The inspect record.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)
typed/builtin//modules/api/engine/asset/asset/interface
asset.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
refRefArg— Any name the asset has.categorystring(optional) — Category hint (optional).
Returns AssetInterface? — { form, id?, description?, inputs, outputs, problems }, or nil when the asset's type declares no interface or its source hands no builder.
local iface = asset.interface("@builtin::systems.procgen.nodes.points.disc", "procNode")
typed/builtin//modules/api/engine/asset/asset/isPrivateSubasset
asset.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
pathstring— The asset's VFS folder path.
Returns boolean — True for a private subasset; false for any other asset, and for a path that names no asset.
asset.isPrivateSubasset("/zero/source/models/tree.bundle/material/Bark.material") -- true
typed/builtin//modules/api/engine/asset/asset/list
asset.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.scopestring(optional) — Scope filter (when first arg is a type).optsListOpts(optional) — The query table — seeListOpts.
Returns ListResult — The matched AssetRef handles, as a result carrying query methods.
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
typed/builtin//modules/api/engine/asset/asset/listRevision
asset.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.
Returns (number, number) — The index epoch and the generation within it.
local e, g = asset.listRevision() if e ~= held.e or g ~= held.g then held.list = asset.list("texture") end
typed/builtin//modules/api/engine/asset/asset/list_field_values
asset.list_field_values(key: string) -> { any }
Distinct values seen for the named field across every
asset's .metadata.
Parameters
keystring— Field name.
Returns { any } — Array of distinct values.
local authors = asset.list_field_values("author")
typed/builtin//modules/api/engine/asset/asset/list_fields
asset.list_fields() -> { string }
Distinct top-level field keys observed across every asset's
.metadata. Useful for tooling discovering custom keys in use.
Returns { string } — Array of field names.
for _, k in asset.list_fields() do print(k) end
typed/builtin//modules/api/engine/asset/asset/meta
asset.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
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — Metadata table.
local m = asset.meta("brick") -- { guid = ..., checksum = ... }
typed/builtin//modules/api/engine/asset/asset/metadata
asset.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
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — JSON-shaped table; empty when no sidecar exists.
local md = asset.metadata("brick")
typed/builtin//modules/api/engine/asset/asset/observe
asset.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.
Returns any — ResidencyReading
local r = asset.observe() print(#r.textures, r.totals.textureBytes)
typed/builtin//modules/api/engine/asset/asset/pathHolding
asset.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
guidstring— The asset's guid.checksumstring— The content checksum of the version wanted.
Returns string? — The path holding that version, or nil.
local m = asset.meta(asset.resolve("@builtin::shaders.pbr", "shader")); local path = asset.pathHolding(m.guid, m.checksum)
typed/builtin//modules/api/engine/asset/asset/preview
asset.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
refRefArg— Any name the asset has.opts{ [string]: any }(optional) — Optional{ size = { width, height }, angle = { yaw, pitch } }.typestring(optional) — Category hint (optional).
Returns { [string]: any } — { available, imageBase64?, width?, height?, bounds?, stats?, reason? }.
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })
typed/builtin//modules/api/engine/asset/asset/primaryFile
asset.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
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — { path: string?, resolved: boolean, isFolder: boolean, declared: { string } }
print(asset.primaryFile("myTex").path)
typed/builtin//modules/api/engine/asset/asset/ref
asset.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
refRefArg— The asset to reference — an identity, a guid, a VFS path, or a handle.typestring(optional) — Category hint (optional).
Returns AssetRef — Ref handle. Raises on a miss or ambiguity, as asset.resolve does.
local r = asset.ref("animations.idle", "animation")
typed/builtin//modules/api/engine/asset/asset/reloadPending
asset.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
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true while a content-change reload is still owed.
repeat task.wait() until not asset.reloadPending(ref)
typed/builtin//modules/api/engine/asset/asset/reloadSeq
asset.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
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns number — content-change reloads dispatched for this asset.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at
typed/builtin//modules/api/engine/asset/asset/remove_field
asset.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
refRefArg— Any name the asset has.keystring— Field name.
asset.remove_field("brick", "author")
typed/builtin//modules/api/engine/asset/asset/remove_tag
asset.remove_tag(ref: RefArg, tag: string)
Remove a tag from the asset's .metadata.tags. No-op when
the tag isn't present.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to remove.
asset.remove_tag("brick", "wip")
typed/builtin//modules/api/engine/asset/asset/resolve
asset.resolve(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
refRefArg— 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.pngafter the texture importer turned it intowall.texture) resolves to the promoted asset, and says so in the log once per reference.
Returns AssetRef<C> — Asset handle, carrying the category when one was named — so the methods that category defines are checked on the result. Raises (rather than returning nil) on a miss, and on a name that reaches more than one asset.
local a = asset.resolve("@builtin::components.Camera")
typed/builtin//modules/api/engine/asset/asset/schema
asset.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
refRefArg— The asset (ref, identity, guid or path).
Returns any? — The schema table, or nil when this asset carries no settings.
local schema = asset.schema(texRef); print(schema.filter.constraint.values)
typed/builtin//modules/api/engine/asset/asset/setSettings
asset.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
refRefArg— The asset (ref, identity, guid or path).patch{ [string]: any }— Any subset of the schema's fields.
asset.setSettings(texRef, { filter = "nearest" })
typed/builtin//modules/api/engine/asset/asset/set_field
asset.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
refRefArg— Any name the asset has.keystring— Field name.valueany(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
typed/builtin//modules/api/engine/asset/asset/set_metadata
asset.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
refRefArg— Any name the asset has.dataAssetMeta— Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })
typed/builtin//modules/api/engine/asset/asset/settings
asset.settings(ref: RefArg) -> any?
This asset's settings values, with every declared default filled in.
Parameters
refRefArg— The asset (ref, identity, guid or path).
Returns any? — The values table, or nil when this asset carries no settings.
if asset.settings(texRef).filter == "nearest" then ... end
typed/builtin//modules/api/engine/asset/asset/source
asset.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
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — VFS source path.
local p = asset.source("brick") -- "/source/brick.material"
typed/builtin//modules/api/engine/asset/asset/tags
asset.tags(ref: RefArg) -> { string }
Convenience read of the .metadata.tags array.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of tag strings.
for _, t in asset.tags("brick") do print(t) end
typed/builtin//modules/api/engine/asset/asset/tryResolve
asset.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
refRefArg— The asset to look up — an identity, a guid, a VFS path, or a handle.typestring(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 asasset.resolve.basestring(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 itsmat.yaml. A~/~.tailref expands against the root enclosing it, and a ref its.refsrecorded reaches the asset recorded there. Without it, both are read at the calling Luau source, asasset.resolvereads them.
Returns AssetRef? — Asset handle, or nil when the reference resolves to no asset.
local mat = asset.tryResolve(name, "material")
typed/builtin//modules/api/engine/asset/asset/typeRef
asset.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
targetRefArg— Asset handle / identity / guid / VFS path.
Returns string? — Pinned type's guid, or nil.
local t = asset.resolve(assert(asset.typeRef("brick")))
typed/builtin//modules/api/engine/asset/asset/unusableReasons
asset.unusableReasons() -> { string }
Every reason asset.diagnose can report an asset unusable for, sorted.
Returns { string } — Array of reason names.
for _, r in ipairs(asset.unusableReasons()) do print(r) end
typed/builtin//modules/api/engine/asset/asset/validate
asset.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
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns ValidateResult — { ok, typeName, validated, problems }.
local v = asset.validate("@builtin::components.Camera")
typed/builtin//modules/api/engine/asset/asset/warmup
asset.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
refRefArg— Any name the root asset has — handle, identity, guid, or path.optsWarmupOpts(optional) — Optional{ vias, max }— restrict ref-edge kinds / cap closure size.
Returns WarmupResult — { closure, count } — the deduped guid closure warmed and its size.
local w = asset.warmup("@builtin::scenes.test_arena")
typed/builtin//modules/api/engine/audio/audio/decode
audio.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
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.
Returns (string?, number?, number?) — (pcm, sampleRate, channels), or (nil, err).
typed/builtin//modules/api/engine/audio/audio/device
audio.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.
Returns AudioDeviceStatus — An AudioDeviceStatus.
local d = audio.device(); print(d.state, d.device, d.reopens)
typed/builtin//modules/api/engine/audio/audio/encode
audio.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
sourceBytesbuffer | 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? }
Returns (string?, string?) — The ZAUD bytes, or (nil, err).
local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })
typed/builtin//modules/api/engine/audio/audio/encodePcm
audio.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
pcmany(optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shapemicrophone.samplesandaudio.decodehand 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.sampleRatenumber— Source sample rate in Hz.channelsnumber— 1 or 2, and a divisor of the sample count.opts{ [string]: any }(optional) — Same shape asaudio.encode.
Returns (string?, string?) — The ZAUD bytes, or (nil, err).
local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)
typed/builtin//modules/api/engine/audio/audio/info
audio.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
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.
Returns (AudioInfo?, string?) — An AudioInfo table, or (nil, err).
local info = audio.info(zaud); print(info.durationMs)
typed/builtin//modules/api/engine/audio/audio/levels
audio.levels() -> AudioLevels
The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.
Returns AudioLevels — An AudioLevels.
local l = audio.levels(); print(l.peak, l.rms, l.windowMs)
typed/builtin//modules/api/engine/audio/audio/listener
audio.listener() -> AudioListenerState
Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.
Returns AudioListenerState — An AudioListenerState.
local l = audio.listener(); print(l.present, l.count, l.entity)
typed/builtin//modules/api/engine/audio/audio/loopSeam
audio.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
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.
Returns (AudioLoopSeam?, string?) — An AudioLoopSeam table, or (nil, err).
local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)
typed/builtin//modules/api/engine/audio/audio/mixer
audio.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.
Returns AudioMixerLevels — An AudioMixerLevels.
local m = audio.mixer(); print(m.master, m.muted, m.channels.music)
typed/builtin//modules/api/engine/audio/audio/observe
audio.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.
Returns AudioObservation — An AudioObservation.
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
typed/builtin//modules/api/engine/audio/audio/peakSince
audio.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
windownumber— Awindowscount taken fromaudio.levels()earlier.
Returns number? — The loudest window peak in the span, or nil when the meter holds no peak for it — nothing has closed since window, or the span reaches further back than the meter's history of recent windows, so a reader that came back too late learns that instead of reading the maximum of the part that survived.
local mark = audio.levels().windows
local peak = audio.peakSince(mark)
typed/builtin//modules/api/engine/audio/audio/peaks
audio.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
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.bucketsnumber— How many spans to divide the clip into: an integer from 1 to 4096.
Returns ({ number }?, string?) — One peak per bucket, or (nil, err).
local bars = audio.peaks(clipRef:getBytes(), 96)
typed/builtin//modules/api/engine/audio/audio/profile
audio.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.
Returns AudioProfile — An AudioProfile.
audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)
typed/builtin//modules/api/engine/audio/audio/resetProfile
audio.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()
typed/builtin//modules/api/engine/audio/audio/setChannelVolume
audio.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
channelstring— The channel name, matchingAudio.channel.volumenumber— Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end
typed/builtin//modules/api/engine/audio/audio/setMasterVolume
audio.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
volumenumber— Master level, 0..1.
audio.setMasterVolume(0.5)
typed/builtin//modules/api/engine/audio/audio/setMuted
audio.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
mutedboolean— Whether the mix is silenced.
audio.setMuted(true)
typed/builtin//modules/api/engine/audio/audio/voice
audio.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
entityIdstring— The entity's stable id.
Returns AudioVoice? — An AudioVoice, or nil.
local v = audio.voice(e.id); print(v and v.mixerState)
typed/builtin//modules/api/engine/audio/audio/voiceAccounting
audio.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.
Returns AudioVoiceAccounting — An AudioVoiceAccounting.
local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))
typed/builtin//modules/api/engine/audio/audio/voices
audio.voices() -> { AudioVoice }
Every live audio source with the mixer's opinion of it.
Returns { AudioVoice } — An array of AudioVoice.
for _, v in audio.voices() do print(v.clip, v.gain.effective) end
typed/builtin//modules/api/engine/audio/audio/whySilent
audio.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
entityIdstring— The entity's stable id.
Returns (string?, string?) — (reason, detail).
local why = audio.whySilent(e.id); if why then print(why) end
typed/builtin//modules/api/engine/av/av/is_live
av.is_live() -> boolean
True if a live-stream session is currently active.
Returns boolean — Whether the live encoder is running.
if av.is_live() then av.stop_live() end
typed/builtin//modules/api/engine/av/av/is_recording
av.is_recording() -> boolean
True if a recording session is currently active.
Returns boolean — Whether a recording is in progress.
print("recording:", av.is_recording())
typed/builtin//modules/api/engine/av/av/live
av.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
optsLiveOpts(optional) — Encoder options.
Returns (string?, string?) — Stream URL, or nil. Why the stream was refused, when it was.
local url, why = av.live({ width = 1280, height = 720, fps = 60 })
typed/builtin//modules/api/engine/av/av/record
av.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
pathstring— VFS destination path, returned in its/zeroform; a relative name records under/zero/runtime/recordings/.optsRecordOpts(optional) — Encoder options (optional).
Returns (string?, string?, Durability?) — Destination VFS path of the open recording, or nil. Why the recording was refused, when it was. Where the film will land, as a THIRD return value: { durable = true } when the take is filed where this call named it and the next session reads it back, { durable = false, survivesRestart = …, warning = …, playShadow = …, shadowed = { … } } when the play shadow takes it: live in this session and discarded on a guarded play-exit unless kept, with survivesRestart saying whether a restart keeps it and playShadow naming the routes that keep it, and { durable = false, survivesRestart = false, warning = …, scratch = { … } } when it is filed in /source/tmp/ scratch, which is gone when the engine stops or restarts. The take is filed when it ENDS, so this is the verdict a write landing there gets on the terms standing at THIS call, answered here because here is where a caller can still act on it: a film recorded during play is kept with vfs.promotePlayShadow(path) and play keeps running. durable is present whatever the answer is, so its absence is never a reading. It is the same answer, in the same words, that asset.create returns and the MCP write_file / capture tools attach to their own results. A take opened in edit that runs on into play is filed on play's terms, so av.status().recordingDurability — which answers for the take still running — is where a take spanning a mode change reads.
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
typed/builtin//modules/api/engine/av/av/save
av.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
pathstring— The take: its name under/zero/runtime/recordings/, or a VFS path.optsSaveOpts(optional) —{ name }, the file name the device gets (the take's own when omitted).
Returns (boolean, string, SaveReport?) — Whether the device took the take. Where it went, or why it was refused. The save report, when the device took the take.
local ok, where = av.save("take.mp4")
local ok, where, report = av.save("take.mp4", { name = "boss-fight.mp4" })
typed/builtin//modules/api/engine/av/av/status
av.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.
Returns AvStatus — Encoder status table.
local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)
typed/builtin//modules/api/engine/av/av/stop_live
av.stop_live() -> boolean
Stop any active live-stream session.
Returns boolean — True if a session was stopped, false if none was active.
av.stop_live()
typed/builtin//modules/api/engine/av/av/stop_recording
av.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
handlestring(optional) — Promise handle of a specific recording (optional).
Returns (boolean, string?) — True if a recording was stopped. How the most recent recording ended, when nothing was armed.
local stopped, ended = av.stop_recording()
typed/builtin//modules/api/engine/base64/base64/decode
base64.decode(text: string) -> (string?, string?)
Decode standard-alphabet base64 text back to the original binary string.
Parameters
textstring— Base64 text to decode.
Returns (string?, string?) decoded bytes on success, or (nil, errmsg).
local bytes = base64.decode(encoded)
typed/builtin//modules/api/engine/base64/base64/encode
base64.encode(bytes: buffer | string) -> string
Encode a binary string to standard-alphabet (padded) base64 text.
Parameters
bytesbuffer | string— Binary bytes to encode.
Returns string — Base64 text.
local text = base64.encode(jpegBytes)
typed/builtin//modules/api/engine/blend/blend/destroyLayout
blend.destroyLayout(handle: number) -> boolean
Drop the layout from the registry.
Parameters
handlenumber— Layout handle.
Returns boolean — True if the layout existed and was removed.
typed/builtin//modules/api/engine/blend/blend/layout
blend.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.totalStridenumber(optional) — Optional explicit record stride.
Returns number? — Layout handle, or nil.
local l = blend.layout({ { offset = 0, stride = 3, op = "lerp" } })
typed/builtin//modules/api/engine/blend/blend/lerpInto
blend.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
outBufferSubstrate.TypedBuffer— The buffer written into.layoutnumber— Layout handle.aBufferSubstrate.TypedBuffer— The A side of the fade.bBufferSubstrate.TypedBuffer— The B side of the fade.tnumber— Crossfade weight on B (0..1).
Returns boolean — True on success.
typed/builtin//modules/api/engine/blend/blend/weightedInto
blend.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
outBufferSubstrate.TypedBuffer— The buffer written into.layoutnumber— Layout handle.inputs{ BlendInput }— Array of{ buffer, weight }.
Returns boolean — True on success.
typed/builtin//modules/api/engine/camera/camera/active
camera.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.
Returns string? — Entity id of the on-screen camera, or nil if none is active — including the frame after that camera's entity is despawned.
local camId = camera.active()
typed/builtin//modules/api/engine/camera/camera/cut
camera.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 }
typed/builtin//modules/api/engine/camera/camera/editor
camera.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.
Returns string? — Entity id of the editor camera, or nil.
local camId = camera.editor()
local id = camera.editor(); entity(id).position = { 12, 8, 12 }; tools.use("camera", "lookAt", id, { 0, 0, 0 })
typed/builtin//modules/api/engine/camera/camera/editorOverride
camera.editorOverride() -> string?
Entity id currently overriding viewport selection, or nil when the viewport is decided by highest-priority-wins.
Returns string? — Entity id of the overriding camera, or nil.
local owner = camera.editorOverride()
typed/builtin//modules/api/engine/camera/camera/get
camera.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.
Returns CameraReport? — The report, or nil when nothing resolves or that camera has none.
local c = camera.get(camera.active()); print(c.frame.far, c.authored.far)
local c = camera.get("minimapCam"); print(c.rendering, c.reason)
typed/builtin//modules/api/engine/camera/camera/list
camera.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.
Returns { CameraReport } — One row per camera entity, or an empty list before the first frame.
for _, c in ipairs(camera.list()) do print(c.name, c.enabled, c.rendering) end
typed/builtin//modules/api/engine/camera/camera/main
camera.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().
Returns string? — Entity id of the main scene camera, or nil — including the frame after that camera's entity is despawned, before the scene elects another.
local camId = camera.main(); local cam = camId and entity(camId)
typed/builtin//modules/api/engine/camera/camera/motionTally
camera.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.
Returns { frames: number, withoutHistory: number } — { frames, withoutHistory }.
local before = camera.motionTally().withoutHistory
typed/builtin//modules/api/engine/camera/camera/observe
camera.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.
Returns CameraObservation? — The observation, or nil before the first frame has been drawn.
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
typed/builtin//modules/api/engine/camera/camera/setEditorOverride
camera.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
entityIdstring(optional) — Entity id of the camera to route the viewport to, or nil to clear.
camera.setEditorOverride(camera.editor())
camera.setEditorOverride(nil)
typed/builtin//modules/api/engine/camera/camera/viewData
camera.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.
Returns CameraView? — The camera view data, or nil when that camera drew no frame.
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)
typed/builtin//modules/api/engine/cameraInput/cameraInput/devices
cameraInput.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.
Returns { CameraDevice } — Array of { id, name, facing, default }.
for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end
typed/builtin//modules/api/engine/cameraInput/cameraInput/feeds
cameraInput.feeds() -> { CameraFeedEntry }
Every open feed, with the owning context that opened it (scope,
the same tag scopes.list() shows), whose end stops the feed.
Returns { CameraFeedEntry } — Array of { id, scope, state, device, texture }.
for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end
typed/builtin//modules/api/engine/cameraInput/cameraInput/onDeviceChange
cameraInput.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, ascameraInput.devices()returns it.
Returns () -> () — A function that removes this listener.
local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)
typed/builtin//modules/api/engine/cameraInput/cameraInput/open
cameraInput.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
optsCameraOpenOpts(optional) —{ device, facing, width, height, fps }, every field optional.
Returns (CameraFeed?, string?) — The feed, or nil and the reason.
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")
typed/builtin//modules/api/engine/cameraInput/cameraInput/stopAll
cameraInput.stopAll() -> number
Stop every open feed and release every camera, whatever opened them.
Returns number — How many feeds stopped.
cameraInput.stopAll()
typed/builtin//modules/api/engine/channel/channel/create
channel.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
optsChannelOpts—{ times, values, stride, interp }.
Returns number? — Channel handle, or nil.
local h = channel.create({ times = ts, values = vs, stride = 3, interp = "linear" })
typed/builtin//modules/api/engine/channel/channel/destroy
channel.destroy(handle: number) -> boolean
Drop the channel from the registry.
Parameters
handlenumber— Channel handle.
Returns boolean — True if the channel existed and was removed.
typed/builtin//modules/api/engine/channel/channel/sampleInto
channel.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
chnumber— Channel handle.timenumber— Sample time in seconds.bufSubstrate.TypedBuffer— The buffer written into.offsetnumber— Starting f32 index in the buffer.
Returns boolean — True on success.
typed/builtin//modules/api/engine/channel/channel/sampleManyInto
channel.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
chnumber— Channel handle.timenumber— Sample time in seconds.bufSubstrate.TypedBuffer— The buffer written into.offsets{ number }— Array of f32 indices.
Returns boolean — True on success.
typed/builtin//modules/api/engine/channel/channel/sampleQuat
channel.sampleQuat(ch: number, time: number) -> (number?, number?, number?, number?)
Convenience accessor for stride-4 quaternion channels.
Parameters
chnumber— Channel handle.timenumber— Sample time.
Returns (number?, number?, number?, number?) — (x, y, z, w) or nil.
typed/builtin//modules/api/engine/channel/channel/sampleVec3
channel.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
chnumber— Channel handle.timenumber— Sample time.
Returns (number?, number?, number?) — (x, y, z) or nil.
local x, y, z = channel.sampleVec3(h, t)
typed/builtin//modules/api/engine/color/color/coerce
color.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
valueany(optional) — Value to read as a colour.
Returns Color? — sRGB color table, or nil when value is not a colour.
local c = color.coerce("#5a5a62") or color.coerce({ 0.2, 0.7, 0.2 })
typed/builtin//modules/api/engine/color/color/complementary
color.complementary(c: Color) -> Color
Complementary color — rotate hue 180° in Oklch space.
Parameters
cColor— Input color.
Returns Color — Complementary sRGB color.
local accent = color.complementary(primary)
typed/builtin//modules/api/engine/color/color/darken
color.darken(c: Color, amount: number) -> Color
Decrease the lightness of a color in Oklch perceptual space.
Parameters
cColor— Input color.amountnumber— Lightness decrease 0-1.
Returns Color — Darkened sRGB color.
local pressed = color.darken(base, 0.1)
typed/builtin//modules/api/engine/color/color/desaturate
color.desaturate(c: Color, amount: number) -> Color
Decrease the chroma (saturation) of a color in Oklch space.
Parameters
cColor— Input color.amountnumber— Chroma decrease (typically 0-0.2).
Returns Color — Less saturated sRGB color.
local muted = color.desaturate(base, 0.05)
typed/builtin//modules/api/engine/color/color/hex
color.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
hexStringstring— Hex color string.
Returns Color? — sRGB color table or nil.
local fromCss = color.hex("#ff8800")
typed/builtin//modules/api/engine/color/color/hsl
color.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
hnumber— Hue (degrees, 0-360).snumber— Saturation (0-1).lnumber— Lightness (0-1).
Returns Color — sRGB color table { r, g, b, a = 1 }.
local teal = color.hsl(180, 0.5, 0.5)
typed/builtin//modules/api/engine/color/color/hsla
color.hsla(h: number, s: number, l: number, a: number) -> Color
Build a color from HSLA, returned as sRGB.
Parameters
hnumber— Hue (0-360).snumber— Saturation (0-1).lnumber— Lightness (0-1).anumber— Alpha (0-1).
Returns Color — sRGB color table { r, g, b, a }.
local fadedTeal = color.hsla(180, 0.5, 0.5, 0.3)
typed/builtin//modules/api/engine/color/color/hsv
color.hsv(h: number, s: number, v: number) -> Color
Build a color from HSV (h: 0-360, s: 0-1, v: 0-1).
Parameters
hnumber— Hue (0-360).snumber— Saturation (0-1).vnumber— Value / brightness (0-1).
Returns Color — sRGB color table { r, g, b, a = 1 }.
local primary = color.hsv(220, 0.7, 0.9)
typed/builtin//modules/api/engine/color/color/lighten
color.lighten(c: Color, amount: number) -> Color
Increase the lightness of a color in Oklch perceptual space.
Parameters
cColor— Input color.amountnumber— Lightness increase 0-1.
Returns Color — Lightened sRGB color.
local hover = color.lighten(base, 0.1)
typed/builtin//modules/api/engine/color/color/linear
color.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
rnumber— Linear red (0-1).gnumber— Linear green (0-1).bnumber— Linear blue (0-1).anumber(optional) — Alpha (0-1, default 1).
Returns Color — sRGB color table { r, g, b, a }.
local gpuBlue = color.linear(0.0, 0.0, 1.0)
typed/builtin//modules/api/engine/color/color/mix
color.mix(c1: Color, c2: Color, t: number) -> Color
Perceptually blend two colors in Oklch space — better than RGB mixing for gradients.
Parameters
c1Color— First color.c2Color— Second color.tnumber— Blend factor 0-1 (0 = c1, 1 = c2).
Returns Color — Blended sRGB color.
local mid = color.mix(color.rgb(255, 0, 0), color.rgb(0, 0, 255), 0.5)
typed/builtin//modules/api/engine/color/color/mixRgb
color.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
c1Color— First color.c2Color— Second color.tnumber— Blend factor 0-1.
Returns Color — Blended sRGB color.
local plain = color.mixRgb(a, b, 0.5)
typed/builtin//modules/api/engine/color/color/oklch
color.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
lnumber— Lightness (0-1).cnumber— Chroma / saturation (0-0.4).hnumber— Hue (0-360).
Returns Color — sRGB color table { r, g, b, a = 1 }.
local accent = color.oklch(0.7, 0.15, 30)
typed/builtin//modules/api/engine/color/color/rgb
color.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
rnumber— Red channel (0-255).gnumber— Green channel (0-255).bnumber— Blue channel (0-255).
Returns Color — sRGB color table { r, g, b, a = 1 }, normalised to 0-1.
local red = color.rgb(255, 0, 0)
typed/builtin//modules/api/engine/color/color/rgba
color.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
rnumber— Red channel (0-255).gnumber— Green channel (0-255).bnumber— Blue channel (0-255).anumber— Alpha (0-1).
Returns Color — sRGB color table { r, g, b, a }.
local halfRed = color.rgba(255, 0, 0, 0.5)
typed/builtin//modules/api/engine/color/color/rotateHue
color.rotateHue(c: Color, degrees: number) -> Color
Rotate the hue of a color by a given number of degrees in Oklch space.
Parameters
cColor— Input color.degreesnumber— Hue rotation (positive or negative).
Returns Color — Hue-rotated sRGB color.
local triadic = color.rotateHue(base, 120)
typed/builtin//modules/api/engine/color/color/saturate
color.saturate(c: Color, amount: number) -> Color
Increase the chroma (saturation) of a color in Oklch space.
Parameters
cColor— Input color.amountnumber— Chroma increase (typically 0-0.2).
Returns Color — More saturated sRGB color.
local pop = color.saturate(base, 0.05)
typed/builtin//modules/api/engine/color/color/toHex
color.toHex(c: Color) -> string
Convert a color to a hex string. Returns "#rrggbb" or
"#rrggbbaa" if alpha is not 1.
Parameters
cColor— Input color.
Returns string — Hex color string.
print(color.toHex(color.rgb(255, 136, 0))) -- "#ff8800"
typed/builtin//modules/api/engine/color/color/toHsl
color.toHsl(c: Color) -> HslColor
Convert a color to HSL.
Parameters
cColor— Input color.
Returns HslColor — HSL color table { h, s, l, a } (h: 0-360, s/l: 0-1).
local hsl = color.toHsl(base)
typed/builtin//modules/api/engine/color/color/toLinear
color.toLinear(c: Color) -> Color
Convert a color from sRGB to linear RGB space — useful for GPU calculations that need linear-space values.
Parameters
cColor— Input sRGB color.
Returns Color — Linear RGB color table.
local gpu = color.toLinear(base)
typed/builtin//modules/api/engine/color/color/toOklch
color.toOklch(c: Color) -> OklchColor
Convert a color to Oklch perceptual color space.
Parameters
cColor— Input color.
Returns OklchColor — Oklch color table { l, c, h, a } (l: 0-1, c: 0-0.4, h: 0-360).
local okl = color.toOklch(base)
typed/builtin//modules/api/engine/color/color/withAlpha
color.withAlpha(c: Color, a: number) -> Color
Return a copy of a color with a different alpha value.
Parameters
cColor— Input color.anumber— New alpha (0-1).
Returns Color with modified alpha.
local ghost = color.withAlpha(base, 0.3)
typed/builtin//modules/api/engine/compute/compute/absentReasons
compute.absentReasons() -> { string }
Every reason compute.diagnose reports, sorted. resident is the one
that means the resource is there.
Returns { string } — The closed set, as strings.
for _, r in ipairs(compute.absentReasons()) do print(r) end
typed/builtin//modules/api/engine/compute/compute/beginBvh
compute.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.
Returns (number?, string?) — The build id, or (nil, err) naming any non-resident guid.
local id = compute.beginBvh(gather.instances)
typed/builtin//modules/api/engine/compute/compute/buildBvh
compute.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.
Returns (any, string?) — { nodes, tris, nodeCount, triCount } — nodes and tris are buffer handles the caller owns, passed to a dispatch like any other and destroyed when the hierarchy is done with. Or (nil, err) naming any non-resident guid.
local bvh = compute.buildBvh(instances)
asset.resolve("raytrace", "computeShader"):dispatch({ buffers = { bvh.nodes, bvh.tris }, workgroups = { 64, 1, 1 } })
typed/builtin//modules/api/engine/compute/compute/buildBvhSliced
compute.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).
Returns (any, string?) — { nodes, tris, nodeCount, triCount }, or (nil, err).
local built, err = compute.buildBvhSliced(gather.instances, { budgetMs = 4 })
typed/builtin//modules/api/engine/compute/compute/bvhBuilds
compute.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.
Returns { any } — Array of build rows.
print(#compute.bvhBuilds(), "hierarchies in flight")
typed/builtin//modules/api/engine/compute/compute/cancelBvh
compute.cancelBvh(id: number) -> boolean
Drop a build along with the triangles it has gathered.
Parameters
idnumber— Build id fromcompute.beginBvh.
Returns boolean — True when the id named a build.
compute.cancelBvh(id)
typed/builtin//modules/api/engine/compute/compute/compile
compute.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
nameOrHandlestring | { [string]: any } | AssetRef— Shader identity to register under (string or asset handle).opts{ [string]: any }(optional) —{ source, entryPoint?, bindings, params? }—bindingsis an ordered list of{ name, kind, access?, element?, format?, array?, sample? }.
Returns (boolean, string?) — True when the program is queued to register; false with the reason when the schema or the module is one the device would refuse.
compute.compile("my_sim", { source = WGSL, bindings = { { name = "data", kind = "buffer", access = "read_write", element = "f32" } } })
typed/builtin//modules/api/engine/compute/compute/compileByName
compute.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
refstring | { [string]: any } | AssetRef— A.computeShaderidentity/guid string, or a resolved asset handle.
compute.compileByName("@builtin::shaders.compute_double")
typed/builtin//modules/api/engine/compute/compute/copyBufferToTexture
compute.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
bufferNamestring— Source compute buffer.textureKeystring— Cache key to register the texture under.widthnumber— Texture width in texels.heightnumber— Texture height in texels.formatstring(optional) — Texel format:"rgba16f"(default),"rgba32f","rgba8".rowBytesnumber(optional) — Bytes from one row's start to the next when rows are padded pastwidthtexels — a multiple of 256 at least a row's texels long.
Returns boolean — True when the copy was queued.
compute.copyBufferToTexture("gi_resolved", "lm_wall", 128, 128)
compute.copyBufferToTexture("heights_packed", "heights", 300, 300, "rgba16f", 2560)
typed/builtin//modules/api/engine/compute/compute/createBuffer
compute.createBuffer(name: string, opts: { [string]: any }) -> boolean
Allocate a buffer under name, sized in bytes.
Parameters
namestring— The name a dispatch binds it by.opts{ [string]: any }—{ size, readback? }—sizein bytes.
Returns boolean — True once allocated.
typed/builtin//modules/api/engine/compute/compute/createSampler
compute.createSampler(name: string, opts: { [string]: any }?) -> boolean
Create a named GPU sampler. opts: filter/wrap settings.
Parameters
namestringopts{ [string]: any }(optional)
Returns boolean
typed/builtin//modules/api/engine/compute/compute/createStorageTexture2D
compute.createStorageTexture2D(name: string, opts: { [string]: any }) -> boolean
Create a 2D storage texture (compute-writable render target). opts: { width, height, format? }.
Parameters
namestringopts{ [string]: any }
Returns boolean
typed/builtin//modules/api/engine/compute/compute/createTexture3D
compute.createTexture3D(name: string, opts: { [string]: any }) -> boolean
Create a 3D texture volume. opts: { width, height, depth, format?, storage? }.
Parameters
namestring— Unique volume name.opts{ [string]: any }— Dimensions + format (r8/r16f/r32f/rgba8/rgba16f/rgba32f).
Returns boolean — True on success (mutation queued).
typed/builtin//modules/api/engine/compute/compute/createTextureHistory
compute.createTextureHistory(name: string, opts: { [string]: any }) -> boolean
Create a temporal history buffer (ping-pong textures) for a target. opts: { width, height, format? }.
Parameters
namestringopts{ [string]: any }
Returns boolean
typed/builtin//modules/api/engine/compute/compute/destroyBuffer
compute.destroyBuffer(name: string) -> boolean
Release the buffer allocated under name.
Parameters
namestring— The name it was created under.
Returns boolean — True if a buffer under that name was released.
typed/builtin//modules/api/engine/compute/compute/destroySampler
compute.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
namestring— Sampler name.
Returns boolean — True when the release was queued.
compute.createSampler("soft", { filter = true }); compute.destroySampler("soft")
typed/builtin//modules/api/engine/compute/compute/destroyShader
compute.destroyShader(name: string) -> boolean
Destroy a named compute shader pipeline.
Parameters
namestring— Shader name.
Returns boolean — True on success.
typed/builtin//modules/api/engine/compute/compute/destroyShaderEx
compute.destroyShaderEx(name: string) -> boolean
Destroy a shader registered via registerShaderEx.
Parameters
namestring
Returns boolean
typed/builtin//modules/api/engine/compute/compute/destroyStorageTexture2D
compute.destroyStorageTexture2D(name: string) -> boolean
Destroy a named 2D storage texture.
Parameters
namestring
Returns boolean
typed/builtin//modules/api/engine/compute/compute/destroyTexture
compute.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
textureKeystring— Cache key the texture was registered under.
Returns boolean — True when the release was queued.
compute.destroyTexture("lm_wall")
typed/builtin//modules/api/engine/compute/compute/destroyTexture3D
compute.destroyTexture3D(name: string) -> boolean
Destroy a named 3D volume and free its GPU memory.
Parameters
namestring
Returns boolean
typed/builtin//modules/api/engine/compute/compute/destroyTextureHistory
compute.destroyTextureHistory(name: string) -> boolean
Destroy a named texture-history buffer.
Parameters
namestring
Returns boolean
typed/builtin//modules/api/engine/compute/compute/diagnose
compute.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
keystring— The resource key, verbatim.
Returns { [string]: any } — { key, exists, reason, resource?, current? }. resource is the row when one is filed under the key. reason is one of compute.absentReasons(). current names the live key when the owner holds a resource under the same name at a different serial.
local d = compute.diagnose(key)
if not d.exists then print(d.reason, d.current) end
typed/builtin//modules/api/engine/compute/compute/dispatch
compute.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
shaderNameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.optsDispatchOpts—{ buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.failing().
compute.dispatch("blur", { buffers = { "src", "dst" }, workgroups = { 8, 8 } })
typed/builtin//modules/api/engine/compute/compute/dispatchEx
compute.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
shaderNameOrHandlestring | { [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 withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.failing().
typed/builtin//modules/api/engine/compute/compute/dispatchOnVertices
compute.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
shaderNameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.optsDispatchOnVerticesOpts—{ model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.failing().
compute.dispatchOnVertices("flatten", { model = "@user/mesh", workgroups = { 64 } })
typed/builtin//modules/api/engine/compute/compute/failing
compute.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.
Returns { { [string]: any } } — Array of { shader, target, dispatches, failures, ok, lastFrame, lastFailedFrame?, lastError? }.
for _, d in ipairs(compute.failing()) do print(d.shader, d.lastError) end
typed/builtin//modules/api/engine/compute/compute/finishBvh
compute.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
idnumber— Build id fromcompute.beginBvh, stepped until"ready".
Returns (any, string?) — { nodes, tris, nodeCount, triCount }, or (nil, err) when the id names no build or the build still has work left.
local built = compute.finishBvh(id)
typed/builtin//modules/api/engine/compute/compute/getReadbackResult
compute.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
resultKeystring— Key returned byreadBuffer().
Returns { number }? — 1-indexed array of f32 values, or nil if not ready.
local floats = compute.getReadbackResult(key)
typed/builtin//modules/api/engine/compute/compute/getReadbackResultBytes
compute.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
resultKeystring— Key returned byreadBuffer().
Returns buffer? — The read-back's bytes, or nil if not ready.
local bytes = compute.getReadbackResultBytes(key)
local firstTexel = buffer.readf32(bytes, 0)
typed/builtin//modules/api/engine/compute/compute/getReadbackResultU32
compute.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
resultKeystring— Key returned byreadBuffer().
Returns { number }? — Array of u32 values, or nil if not ready.
typed/builtin//modules/api/engine/compute/compute/isReadbackReady
compute.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
resultKeystring— Key returned byreadBuffer().
Returns boolean — True if the result is ready.
typed/builtin//modules/api/engine/compute/compute/markBaseline
compute.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.
Returns number — The mark's generation, counting from 1 — what compute.resetToBaseline takes to restore to this mark.
local mark = compute.markBaseline() -- then run, then compute.resetToBaseline(mark)
typed/builtin//modules/api/engine/compute/compute/observe
compute.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.
Returns { [string]: any } — { published, generation, resources, totals }. Each row of resources carries key, kind (buffer / uniformBuffer / texture3d / storageTexture2d / textureHistory / sampler), owner ({ shader, name, serial }, read off the key), bytes, format, width, height, depth, usage (the bits it was created with — storage, copySrc, copyDst, vertex, index, indirect, uniform, sampled, sampler) and createdFrame. totals is { count, bytes, byKind }, what the rows sum to — and totals.bytes is the compute figure of renderer.gpuMemory(), read off the same registries. published is false when no renderer has published a reading yet, which is the engine saying it cannot answer rather than answering with nothing. The reading is the one the renderer published, republished on a frame where a registry gained or lost an entry: a resource created earlier in this same script is in the next reading, so wait a frame before asking about it, and generation moves when it arrives.
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
typed/builtin//modules/api/engine/compute/compute/program/compile
compute.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
keystring— The key to register under.spec{ [string]: any }—{ source, entryPoint?, bindings, params, ranges? }— the parsed schema. A buffer binding statingrange = true(or every array buffer binding, withranges = true) takes a range of a buffer: a dispatch binds it withzero_base_NAME/zero_len_NAMEin its params, and the body'sNAME[i]andarrayLength(&NAME)address that range. One that states no range binds the whole buffer.
Returns (boolean, string?) — True when the program is queued to register; false with the reason when the schema or the module is one the device would refuse.
typed/builtin//modules/api/engine/compute/compute/program/destroy
compute.program.destroy(key: string) -> boolean
Release the program registered under key.
Parameters
keystring— The program's key.
Returns boolean — True on success.
typed/builtin//modules/api/engine/compute/compute/program/dispatch
compute.program.dispatch(key: string, opts: { [string]: any }) -> boolean
Dispatch the program under key with one buffer per declared storage
binding, in declaration order.
Parameters
keystring— The program's key.opts{ [string]: any }—{ buffers, workgroups, params? }.paramsgives values of the program'sparams: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 withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.program.status(key).
typed/builtin//modules/api/engine/compute/compute/program/dispatchEx
compute.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
keystring— 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 withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.program.status(key).
typed/builtin//modules/api/engine/compute/compute/program/dispatchOnVertices
compute.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
keystring— The program's key.opts{ [string]: any }—{ model, buffers?, workgroups, params? }.paramsgives values of the program'sparams:uniform to this dispatch alone. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.program.status(key).
typed/builtin//modules/api/engine/compute/compute/program/dropList
compute.program.dropList(key: string) -> boolean
Forget the compute list under key.
Parameters
keystring— The list's key.
Returns boolean — True once the removal is queued.
typed/builtin//modules/api/engine/compute/compute/program/forPlugin
compute.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
keystring— The program's key.bindingsnumber | { string }— A count of buffers, or each binding's kind.
Returns any — A handle for a plugin call.
typed/builtin//modules/api/engine/compute/compute/program/list
compute.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
keystring— 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?, when? },slotsmapping a param name to the slot that gives it its value. A write is{ write = bufferName, offset?, values, unsigned?, slots?, when? }:offsetin 32-bit words,valueswritten as f32 (u32 whenunsigned),slotsmapping a value's place, from 1, to the slot that gives it.whennames a slot: a run giving it 0 leaves the entry out.
Returns boolean — True once the registration is queued.
typed/builtin//modules/api/engine/compute/compute/program/record
compute.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.
Returns { { [string]: any } } — The dispatches fn made, as list entries.
typed/builtin//modules/api/engine/compute/compute/program/runList
compute.program.runList(key: string, slots: { [string]: number }?) -> 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.
Parameters
keystring— The list's key.slots{ [string]: number }(optional) — The value of each slot for this run, by name.
Returns boolean — True once the run is queued.
typed/builtin//modules/api/engine/compute/compute/program/setParam
compute.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
keystring— The program's key.propstring— Parameter name as declared.valuenumber— New scalar value.
Returns boolean — True on success.
typed/builtin//modules/api/engine/compute/compute/program/status
compute.program.status(key: string) -> { { [string]: any } }
What the engine did with the dispatches of the program under key,
one record per target.
Parameters
keystring— The program's key.
Returns { { [string]: any } } — Array of dispatch records, most recently dispatched first.
typed/builtin//modules/api/engine/compute/compute/programState
compute.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
refstring | { [string]: any } | AssetRef— A.computeShaderidentity/guid string, a resolved asset handle, or the name a raw registration chose.
Returns (string, string?) — "absent", "pending", "ready" or "failed", and the reason when "failed".
if compute.programState(carve) == "ready" then carve:dispatch(opts) end
typed/builtin//modules/api/engine/compute/compute/readBuffer
compute.readBuffer(name: string) -> string
Start a GPU→CPU read of the buffer under name.
Parameters
namestring— The name it was created under.
Returns string — The result key to poll with getReadbackResult*. A read that could not start answers with the empty key, which every drain reports as unknown — the same shape a caller already handles.
typed/builtin//modules/api/engine/compute/compute/readTexture3D
compute.readTexture3D(name: string) -> string
Start a GPU→CPU readback of a named 3D volume. Returns a result key to poll.
Parameters
namestring
Returns string
typed/builtin//modules/api/engine/compute/compute/readbackPromise
compute.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
resultKeystring— Key returned byreadBuffer().
Returns any — A promise to await, or nil when there is nothing to wait for.
local promise = compute.readbackPromise(key); if promise then task.await(promise) end
typed/builtin//modules/api/engine/compute/compute/readbackState
compute.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
resultKeystring— Key returned byreadBuffer().
Returns string — "pending", "ready", "failed" (and the reason), or "unknown".
if compute.readbackState(key) == "ready" then ... end
typed/builtin//modules/api/engine/compute/compute/registerShader
compute.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
nameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.optsShaderOpts(optional) — Shader options.bindingscomes from the source's own@group(0) @binding(n)declarations when omitted; supplying a count that disagrees with them raises. EveryreadOnlyBindingsentry names one of those declared bindings, as a whole number from 0 tobindings - 1; an entry outside that run raises.
Returns boolean — True on success.
compute.registerShader("blur", { source = WGSL, entryPoint = "main" })
typed/builtin//modules/api/engine/compute/compute/registerShaderEx
compute.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
nameOrHandlestring | { [string]: any } | AssetRefopts{ [string]: any }(optional)
Returns boolean
typed/builtin//modules/api/engine/compute/compute/releaseBaseline
compute.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
generationnumber— The mark to drop, ascompute.markBaselinereturned it.
Returns boolean — True when the mark stood.
compute.releaseBaseline(mark) -- the run is over
typed/builtin//modules/api/engine/compute/compute/resetToBaseline
compute.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
generationnumber(optional) — The mark to restore to, ascompute.markBaselinereturned it; the newest mark when omitted. The marks taken after it go with the reset.
Returns number — How many resources were released.
local released = compute.resetToBaseline(mark)
typed/builtin//modules/api/engine/compute/compute/resources
compute.resources(owner: any?) -> { any }
The resource rows on their own, optionally narrowed to what one shader owns.
Parameters
ownerany(optional) — A.computeShaderref, 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.
Returns { any } — An array of rows in the shape compute.observe().resources carries. Empty when the owner holds nothing.
for _, r in ipairs(compute.resources(shaderRef)) do print(r.key, r.bytes) end
typed/builtin//modules/api/engine/compute/compute/setParam
compute.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
nameOrHandlestring | { [string]: any } | AssetRef— Shader identity (the.computeShaderasset name), or the handleasset.load/asset.resolvereturns — the same formsdispatchtakes.propstring— Parameter name as declared inbindings.yaml.valuenumber— New scalar value (numbers only).
Returns boolean — True on success.
compute.setParam("my_sim", "scale", 4.0)
typed/builtin//modules/api/engine/compute/compute/stepBvh
compute.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
idnumber— Build id fromcompute.beginBvh.budgetMsnumber(optional) — Wall time this slice may spend, in milliseconds (default 4).
Returns (string?, string?) — "pending" or "ready", or (nil, err) when the id names no build.
while compute.stepBvh(id, 4) == "pending" do task.wait() end
typed/builtin//modules/api/engine/compute/compute/textureForPlugin
compute.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
namestring— The texture's name.
Returns any — A handle for a plugin call.
typed/builtin//modules/api/engine/compute/compute/textureFormatBytes
compute.textureFormatBytes(format: string) -> number
Bytes-per-voxel for a texture format string (rgba16f, r8, ...).
Parameters
formatstring
Returns number
typed/builtin//modules/api/engine/compute/compute/writeBuffer
compute.writeBuffer(name: string, values: { number } | buffer | string, offset: number?) -> boolean
Write words into the buffer under name.
Parameters
namestring— The name it was created under.values{ number } | buffer | string— The floats to write, or abuffer/ binary string already holding them.offsetnumber(optional) — 32-bit word offset to write at.
Returns boolean — True on success.
typed/builtin//modules/api/engine/compute/compute/writeBufferBytes
compute.writeBufferBytes(name: string, bytes: buffer | string, offsetBytes: number?) -> boolean
Write packed bytes into the buffer under name.
Parameters
namestring— The name it was created under.bytesbuffer | string— The payload.offsetBytesnumber(optional) — Byte offset to write at.
Returns boolean — True on success.
typed/builtin//modules/api/engine/compute/compute/writeBufferU32
compute.writeBufferU32(name: string, values: { number } | buffer | string, offsetBytes: number?) -> boolean
Write 32-bit words into the buffer under name.
Parameters
namestring— The name it was created under.values{ number } | buffer | string— The words to write.offsetBytesnumber(optional) — Byte offset to write at.
Returns boolean — True on success.
typed/builtin//modules/api/engine/compute/compute/writeFloatsTexture3D
compute.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
namestringfloats{ number }formatOrOpts(string | { [string]: any })(optional)
Returns boolean
typed/builtin//modules/api/engine/compute/compute/writeTexture3D
compute.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
namestring— Volume name.databuffer | string | { number }— Voxel bytes as abuffer, a binary string, or an array of bytes.
Returns boolean — True on success (mutation queued).
typed/builtin//modules/api/engine/debugger/debugger/__diagnostics
debugger.__diagnostics() -> DebuggerDiagnostics
Internal diagnostic counters for debugging the debugger
itself: { installs, debugbreakHits }.
Returns DebuggerDiagnostics — Diagnostic counters.
typed/builtin//modules/api/engine/debugger/debugger/addWatch
debugger.addWatch(expr: string) -> number
Register an expression to re-evaluate on every pause.
Parameters
exprstring— Luau expression.
Returns number — Watch id.
typed/builtin//modules/api/engine/debugger/debugger/continue_
debugger.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.
Returns boolean — True if a thread was paused, false if nothing was paused.
typed/builtin//modules/api/engine/debugger/debugger/disableAll
debugger.disableAll()
Disable every registered breakpoint. Records persist; bytecode BREAK ops are cleared.
typed/builtin//modules/api/engine/debugger/debugger/disconnect
debugger.disconnect(handle: number) -> boolean
Disconnect an onBreak or onResume callback.
Parameters
handlenumber— Handle returned by onBreak/onResume.
Returns boolean — True if the handle existed.
typed/builtin//modules/api/engine/debugger/debugger/enableAll
debugger.enableAll()
Enable every registered breakpoint and re-install them in the VM bytecode.
typed/builtin//modules/api/engine/debugger/debugger/evaluate
debugger.evaluate(expr: string, frame: number?) -> (string?, string?)
Evaluate an expression against the paused frame's
environment. Returns (value, error).
Parameters
exprstring— Luau expression.framenumber(optional) — 1-based frame index (default 1).
Returns (string?, string?) — (value, error).
typed/builtin//modules/api/engine/debugger/debugger/getLocals
debugger.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
framenumber(optional) — 1-based frame index (default 1).
Returns { [string]: string } — { [name] = string }.
typed/builtin//modules/api/engine/debugger/debugger/getPauseInfo
debugger.getPauseInfo() -> PauseInfo?
Info about the active pause, or nil if nothing is paused.
Returns PauseInfo? — { path, line, reason } or nil.
typed/builtin//modules/api/engine/debugger/debugger/getStack
debugger.getStack() -> { Frame }
Captured stack from the active pause, top frame first. Empty when nothing is paused.
Returns { Frame } — Array of Frame tables.
typed/builtin//modules/api/engine/debugger/debugger/getUpvalues
debugger.getUpvalues(frame: number?) -> { [string]: string }
Upvalues captured at the active pause for the given frame.
Parameters
framenumber(optional) — 1-based frame index.
Returns { [string]: string } — { [name] = string }.
typed/builtin//modules/api/engine/debugger/debugger/getWatchValue
debugger.getWatchValue(id: number) -> (string?, string?)
Re-evaluate the watch expression against the paused frame's
environment and return (value, error).
Parameters
idnumber— Watch id.
Returns (string?, string?) — (value, error).
typed/builtin//modules/api/engine/debugger/debugger/getWatches
debugger.getWatches() -> { Watch }
Snapshot of all watches with their last evaluated value and error, sorted by id.
Returns { Watch } — Array of Watch tables.
typed/builtin//modules/api/engine/debugger/debugger/isPauseOnError
debugger.isPauseOnError() -> boolean
Current pause-on-error toggle state for this VM.
Returns boolean — True if enabled.
typed/builtin//modules/api/engine/debugger/debugger/isPaused
debugger.isPaused() -> boolean
Whether the debugger currently has a paused thread.
Returns boolean — True if paused.
typed/builtin//modules/api/engine/debugger/debugger/listBreakpoints
debugger.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.
Returns { Breakpoint } — Array of breakpoint tables.
typed/builtin//modules/api/engine/debugger/debugger/onBreak
debugger.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.
Returns number — Handle.
typed/builtin//modules/api/engine/debugger/debugger/onResume
debugger.onResume(fn: () -> ()) -> number
Register a callback invoked when the paused thread is resumed.
Parameters
fn() -> ()— Callback.
Returns number — Handle.
typed/builtin//modules/api/engine/debugger/debugger/removeBreakpoint
debugger.removeBreakpoint(id: number) -> boolean
Remove the breakpoint with the given id.
Parameters
idnumber— Breakpoint id returned by setBreakpoint.
Returns boolean — True if removed, false if the id was unknown.
typed/builtin//modules/api/engine/debugger/debugger/removeWatch
debugger.removeWatch(id: number) -> boolean
Remove the watch with the given id.
Parameters
idnumber— Watch id.
Returns boolean — True if removed.
typed/builtin//modules/api/engine/debugger/debugger/setBreakpoint
debugger.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
pathstring— VFS path, require identity, or chunk name.linenumber— 1-based source line.optsBreakpointOpts(optional) —{ condition?, logMessage?, hitCount?, notify?, enabled? }.notifymakes it a TRACEPOINT: the hit firesonBreakhandlers against the frame it stopped in —getStack,getLocals,getUpvaluesandevaluateall 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.
Returns Breakpoint — The breakpoint table.
local bp = debugger.setBreakpoint("/zero/source/main.luau", 42)
print(bp.pendingReason or ("installed in " .. bp.chunkNames[1]))
typed/builtin//modules/api/engine/debugger/debugger/setPauseOnError
debugger.setPauseOnError(enabled: boolean)
When true, uncaught Luau errors fire the onBreak callback (observation only — the error still propagates).
Parameters
enabledboolean— Toggle state.
typed/builtin//modules/api/engine/debugger/debugger/stepInto
debugger.stepInto() -> boolean
Run until the next line, descending into any function call.
Returns boolean — True if a step was scheduled.
typed/builtin//modules/api/engine/debugger/debugger/stepOut
debugger.stepOut() -> boolean
Run until the current frame returns; pauses in the caller.
Returns boolean — True if a step was scheduled.
typed/builtin//modules/api/engine/debugger/debugger/stepOver
debugger.stepOver() -> boolean
Run until the next line in the current frame. Calls inside the current line are skipped.
Returns boolean — True if a step was scheduled.
typed/builtin//modules/api/engine/debugger/debugger/toggleBreakpoint
debugger.toggleBreakpoint(path: string, line: number) -> Breakpoint?
Toggle a breakpoint at the given line: removes if present, adds otherwise.
Parameters
pathstring— VFS path, require identity, or chunk name.linenumber— 1-based line.
Returns Breakpoint? — Breakpoint table if added, nil if removed.
typed/builtin//modules/api/engine/ecs/blobs/blobs/clear
blobs.clear(handle: string)
Drop a staged payload that will not be consumed, freeing its memory.
Parameters
handlestring— Handle returned byecs.blobs.set.
ecs.blobs.clear(handle)
typed/builtin//modules/api/engine/ecs/blobs/blobs/get
blobs.get(handle: string) -> string?
Read back a copy of the bytes staged under handle without removing
them. Returns nil for an unknown or already-cleared handle.
Parameters
handlestring— Handle returned byecs.blobs.set.
Returns string? — The staged bytes, or nil.
local bytes = ecs.blobs.get(handle)
typed/builtin//modules/api/engine/ecs/blobs/blobs/set
blobs.set(bytes: string) -> string
Stage binary bytes and return a handle to pass through a component field; a native consumer reads the bytes back by that handle.
Parameters
bytesstring— Binary payload (e.g.vfs.read(clipPath .. "/data.zaud")).
Returns string — String handle for the staged bytes.
local h = ecs.blobs.set(assert(vfs.read(path .. "/data.zaud")))
typed/builtin//modules/api/engine/egress/egress/credentialNames
egress.credentialNames() -> { string }
List the names in the local credential store. Names only — secret values are never exposed to Luau.
Returns { string } — Array of configured credential names.
for _, n in ipairs(egress.credentialNames()) do print(n) end
typed/builtin//modules/api/engine/egress/egress/fetch
egress.fetch(name: string, method: string, url: string, headers: Headers?, body: JsonBody?, response: EgressResponseType?) -> PromiseHandle?
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 __http_error = true.
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.
Parameters
namestring— Local credential name or bound-world secret handle.methodstring— HTTP method, e.g. "GET" or "POST".urlstring— Request URL allowed by the selected credential's destination rules.headersHeaders(optional) — Extra header key-value pairs.bodyJsonBody(optional) — JSON body (encoded automatically).responseEgressResponseType(optional) —"json"(default) or"bytes".
Returns PromiseHandle? — Promise handle for task.await(), or nil on an immediate local refusal.
local h = egress.fetch("meshy", "POST", url, nil, { prompt = p })
typed/builtin//modules/api/engine/egress/egress/hasCredential
egress.hasCredential(name: string) -> boolean
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.
Parameters
namestring— Credential name.
Returns boolean — True if configured.
if not egress.hasCredential("meshy") then error("set MESHY_API_KEY") end
typed/builtin//modules/api/engine/engine/M/editWaiters
M.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.
Returns { EditWaiter } — The waiter records.
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
typed/builtin//modules/api/engine/engine/M/engineRecord
M.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)
typed/builtin//modules/api/engine/engine/M/holdTimeScale
M.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
scalenumber— The rate to run at: 1 is real time, 0.5 half speed, 0 frozen, 2 double. Finite and>= 0.optionsTimeScaleHoldOptions(optional) —owneris 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 mintedhold#nwhen the call carries no agent identity, so every hold answers to a keyengine.releaseTimeScaleaccepts andengine.timeScaleOwnerreports asid.exclusivetakes 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" })
typed/builtin//modules/api/engine/engine/M/markBaseline
M.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.
Returns number — The generation of this reading.
local mark = engine.markBaseline() -- then run, then engine.resetToBaseline(mark)
typed/builtin//modules/api/engine/engine/M/markScriptingBaseline
M.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.
Returns number
engine.markScriptingBaseline()
world.on("player_join", function() end)
engine.resetScriptingState() -- the subscription above is gone
typed/builtin//modules/api/engine/engine/M/offDeviceRebuilt
M.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
idnumber— Watcher id returned byengine.onDeviceRebuilt.
Returns boolean
local id = engine.onDeviceRebuilt(function() end)
engine.offDeviceRebuilt(id)
typed/builtin//modules/api/engine/engine/M/offMemoryPressure
M.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
idnumber— Watcher id returned byengine.onMemoryPressure.
Returns boolean
local id = engine.onMemoryPressure(function() end)
engine.offMemoryPressure(id)
typed/builtin//modules/api/engine/engine/M/offModeChange
M.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
idnumber— Watcher id returned byengine.onModeChange.
Returns boolean
local id = engine.onModeChange(function() end)
engine.offModeChange(id)
typed/builtin//modules/api/engine/engine/M/offPauseChange
M.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
idnumber— Watcher id returned byengine.onPauseChange.
Returns boolean
typed/builtin//modules/api/engine/engine/M/offTimeScaleChange
M.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
idnumber— Watcher id returned byengine.onTimeScaleChange.
Returns boolean
local id = engine.onTimeScaleChange(function() end)
engine.offTimeScaleChange(id)
typed/builtin//modules/api/engine/engine/M/offWorldLoaded
M.offWorldLoaded(id: number) -> boolean
Remove an onWorldLoaded subscriber by its watcher id.
Parameters
idnumber
Returns boolean
typed/builtin//modules/api/engine/engine/M/offWorldReady
M.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
idnumber— Watcher id returned byengine.onWorldReady.
Returns boolean
typed/builtin//modules/api/engine/engine/M/offWorldUnloading
M.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
idnumber— Watcher id returned byengine.onWorldUnloading.
Returns boolean
typed/builtin//modules/api/engine/engine/M/onDeviceRebuilt
M.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).
Returns 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)
typed/builtin//modules/api/engine/engine/M/onMemoryPressure
M.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).
Returns number
engine.onMemoryPressure(function(level, source)
if level ~= "normal" then
decodedClips = {} -- decoded again on next play
end
end)
typed/builtin//modules/api/engine/engine/M/onModeChange
M.onModeChange(callback: (string, string) -> ()) -> number
Parameters
callback(string, string) -> ()
Returns number
typed/builtin//modules/api/engine/engine/M/onPauseChange
M.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).
Returns number
local id = engine.onPauseChange(function(paused)
print(paused and "frozen" or "running")
end)
typed/builtin//modules/api/engine/engine/M/onTimeScaleChange
M.onTimeScaleChange(callback: (number, number, TimeScaleOwner?) -> ()) -> number
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.
Parameters
callback(number, number, TimeScaleOwner?) -> ()— Function invoked as(newScale: number, oldScale: number, owner: TimeScaleOwner?).
Returns number
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)
typed/builtin//modules/api/engine/engine/M/onWorldLoaded
M.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.
Returns number
typed/builtin//modules/api/engine/engine/M/onWorldReady
M.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.
Returns number
typed/builtin//modules/api/engine/engine/M/onWorldUnloading
M.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.
Returns number
typed/builtin//modules/api/engine/engine/M/releaseBaseline
M.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
generationnumber— The mark to drop, asengine.markBaselinereturned it.
Returns boolean — True when the mark stood.
engine.releaseBaseline(mark) -- the run is over
typed/builtin//modules/api/engine/engine/M/releaseScriptingBaseline
M.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
generationnumber— The mark to drop, asengine.markScriptingBaselinereturned it.
Returns boolean — True when the mark was in force.
engine.releaseScriptingBaseline(mark) -- the run is over
typed/builtin//modules/api/engine/engine/M/releaseTimeScale
M.releaseTimeScale(owner: string) -> number
Parameters
ownerstring
Returns number
typed/builtin//modules/api/engine/engine/M/resetScriptingState
M.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
generationnumber(optional) — The generation to restore to, asmarkScriptingBaselinereturned it; the newest mark when omitted.
Returns { [string]: number }
typed/builtin//modules/api/engine/engine/M/resetToBaseline
M.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
generationnumber(optional) — The mark to restore to; the newest when omitted.
Returns number — How many things were put back.
engine.resetToBaseline(mark)
typed/builtin//modules/api/engine/engine/M/restoreModuleState
M.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)?, successor: ((any) -> any?)?, only: { [string]: boolean }? }(optional) —remintmaps 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.goneanswers 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 answeringalivefalse is known without it.successoranswers, 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 answeredalivefalse when the reading was taken is the module's own record of something gone before it, and is put back as it is.sameanswers 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.onlynames 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.
Returns { [string]: any } — { restored, refused, recached, stale, staleModules, staleObjects, failed, failedModules } — how many values were written, how many the engine would not write, how many require-cache entries were put back to the module they held, how many modules hold a value left as it stood because its reading names something the engine no longer holds, and those modules by name, each with the gone object the reading named; then how many modules hold a value whose restore raised, and those by name with the error. A value whose restore raises is counted and passed over, and the rest of the reading is put back.
engine.restoreModuleState(snap)
engine.restoreModuleState(snap, { remint = { [oldGuid] = newGuid } })
typed/builtin//modules/api/engine/engine/M/scriptingRegistryCounts
M.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).
Returns { [string]: number }
typed/builtin//modules/api/engine/engine/M/setMode
M.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
modestring—"edit"or"play".options{ strict: boolean?, endSession: boolean? }(optional) —{ strict: boolean?, endSession: boolean? }.strict = falsewaives the play gate for this call;trueor omitted honours the world'slsp.strict_mode.endSession = truewith"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.
Returns { mode: string, bypassed: { any }, leftPlaySession: boolean } — { mode, bypassed, leftPlaySession }: the mode now in force, the diagnostics this call entered play past (empty when it went past none), and whether an "edit" took this caller out of a play session that keeps running for other callers, in which case mode still reads "play".
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
typed/builtin//modules/api/engine/engine/M/snapshotModuleState
M.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.
Returns { [string]: any } — { entries, cache, modules, inUse, unreachable } — what was captured, the require cache as { key, value } pairs, how many modules it covers, how many were left out for executing, and the count of values that could not be read.
local snap = engine.snapshotModuleState()
typed/builtin//modules/api/engine/engine/M/timeScaleHolds
M.timeScaleHolds() -> { string }
Returns { string }
typed/builtin//modules/api/engine/engine/M/waitForEdit
M.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
reasonstring(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()
typed/builtin//modules/api/engine/engine/M/watcherSources
M.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.
Returns { [string]: { [number]: string } } — The sources, by registry and watcher id.
for id, at in pairs(engine.watcherSources().modeWatchers) do print(id, at) end
typed/builtin//modules/api/engine/engine/engine/discardPlayChanges
engine.discardPlayChanges() -> { paths: { string }, keep: string }
Arm the leave-play safeguard's deliberate discard for your next engine.mode = "edit" in the play session this is called from, so it proceeds and discards your unaccepted changes. While other callers stay in the session that is your own work: the /source edits you made in play, which revert, and the entities you tuned. The last caller to leave takes the engine to edit, and its discard throws away everything play made: the entities and tuning, and every /source file edit play holds on its shadow. Called by a caller outside the session, it arms the discard of engine.setMode("edit", { endSession = true }), which ends the session for every caller in it. A call that names no caller (a transport that attests no caller for it) cannot be told apart from any caller, so the exit that ends the session takes an arm made for the whole session from the other side: an exit that names no caller takes the arm a named caller made, and a named caller's exit takes the arm a call that named no caller made. Returns paths, the source files the flip would throw away if it ran now, and keep, the routes that keep any of them first; a notice names the same paths. The flip throws away what the shadow holds when it runs, so an edit made after the arm goes with the rest, and the flip names every path it took in a notice and the engine log (vfs.discardPlayShadow() states the same set). A discarded source edit whose pre-play copy is not on this engine yet keeps the play bytes until that copy lands and is put back; the flip names each such path, and vfs.playDiscardsOwed() lists them until then.
Returns { paths: { string }, keep: string }
typed/builtin//modules/api/engine/engine/engine/editWaiters
engine.editWaiters() -> { { key: string, id: string?, agent: string?, name: string?, reason: string?, heldSeconds: number } }
Everyone waiting for this engine to leave play, oldest first. A session holding play reads this to see whose work it is holding; heldSeconds is how long each has been waiting. Empty in edit mode and whenever nobody has said they are waiting.
Returns { { key: string, id: string?, agent: string?, name: string?, reason: string?, heldSeconds: number } }
typed/builtin//modules/api/engine/engine/engine/gameplayReady
engine.gameplayReady -> boolean
Whether gameplay simulation is running: not paused, and the play scene materialized. Read-only.
Returns boolean
typed/builtin//modules/api/engine/engine/engine/gpuCompute
engine.gpuCompute -> boolean
Whether this process holds a live GPU device, so compute dispatch is available. Read-only.
Returns boolean
typed/builtin//modules/api/engine/engine/engine/headless
engine.headless -> boolean
Whether this boot renders offscreen with no window a person can see. Content that only serves someone at a display stands down when it reads true. Read-only.
Returns boolean
typed/builtin//modules/api/engine/engine/engine/mode
engine.mode -> "edit" | "play"
The engine mode this process is in, edit or play. Assigning it takes the flip, side effects and all.
Returns "edit" | "play"
typed/builtin//modules/api/engine/engine/engine/modeOwner
engine.modeOwner -> { id: string?, agent: string?, name: string?, heldSeconds: number }?
Who flipped the engine out of the mode it booted into, and how long ago, while that caller still holds it. In play, engine.playSession names every caller the session belongs to now. id is the caller key, the same value vfs.currentAuthor().id reads, and nil when that flip carried no agent identity; agent is the name the world's roster holds for that caller now, the one to ask by, and name the username of its account, which every agent of the account shares. Nil in the mode the engine booted into (a flip back to it ends the hold), once the caller that flipped it has left the play session it entered, and once that caller is no longer on the world's roster. Read-only.
Returns { id: string?, agent: string?, name: string?, heldSeconds: number }?
typed/builtin//modules/api/engine/engine/engine/pauseOwner
engine.pauseOwner -> { id: string?, agent: string?, name: string?, heldSeconds: number }?
Who asked for the standing gameplay pause, and for how long they have held it. One process runs one simulation, so a pause another agent took freezes update(dt), fixedUpdate(dt), physics and every gameplay clock for everybody: a component reading its own state while this is non-nil is reading the same instant over and over, and a sampling loop across task.wait returns the identical sample every time. id is the caller key, the same value vfs.currentAuthor().id reads, and nil when that call carried no agent identity; agent is the name the world's roster holds for that caller now, and name the username of its account, which every agent of the account shares. Nil while the engine is running, and nil when the standing pause is the mode's own rather than something somebody asked for. Read-only.
Returns { id: string?, agent: string?, name: string?, heldSeconds: number }?
typed/builtin//modules/api/engine/engine/engine/paused
engine.paused -> boolean
Whether gameplay is paused: update(dt) component callbacks are gated off while editorUpdate(dt) keeps firing in edit mode. A component's gameplay task (one spawned from awake, start, onEnable, update, fixedUpdate or another gameplay task) is held for the whole pause: a task.wait it started keeps the time it had left, and a task.waitRealtime is held too, but its deadline runs through the pause, so it comes due on the first frame after it. A task spawned from editorUpdate and a task.spawnSystem task run while paused. The scripting-and-tasks guide's "When a task runs" states the whole rule.
Returns boolean
typed/builtin//modules/api/engine/engine/engine/platform
engine.platform -> "native" | "browser" | "android" | "unknown"
Which build of the engine this process is: native for the desktop binary, browser for the wasm one, android for the Android app. unknown says the binary named no build, which a binary older than the field does. Read-only.
Returns "native" | "browser" | "android" | "unknown"
typed/builtin//modules/api/engine/engine/engine/playSession
engine.playSession -> { agents: { { key: string, agent: string? } }, takesPart: boolean }?
The play session the engine is running, as you stand towards it. Play belongs to the callers that take part in it: the caller that entered play, and every caller that writes engine.mode = "play" while it runs. agents are those callers still connected, in the order they joined, each with its stable key (vfs.currentAuthor().stable) and the name the world's roster holds for it. takesPart is whether you are one of them: your /source writes are play work, and your engine.mode = "edit" takes you out of the session, which keeps running for the others and ends with the last of them. While it is false you author beside the session: your /source writes land on canonical source, and the session is theirs to end. Nil in edit mode, in the runtime profile, and while the engine plays for nobody in particular (a Play button press, a boot into play). Read-only.
Returns { agents: { { key: string, agent: string? } }, takesPart: boolean }?
typed/builtin//modules/api/engine/engine/engine/profile
engine.profile -> "editor" | "runtime"
The boot profile this process started under, editor or runtime. Read-only.
Returns "editor" | "runtime"
typed/builtin//modules/api/engine/engine/engine/timeScale
engine.timeScale -> number
The global time scale applied to the fixed-timestep accumulator, to update(dt), and to the seconds task.wait and task.delay count: 1.0 is real time, 0.0 frozen, 2.0 double speed. So a task.wait(1) returns after four wall-clock seconds at 0.25 and never comes due at 0. task.waitRealtime(1) is one wall-clock second at every scale. One number for the whole process, shared by every agent driving this engine: engine.timeScaleOwner names who it answers to, engine.onTimeScaleChange reports every move of it, and engine.holdTimeScale takes it for a named owner so a write from anywhere else is refused while the hold stands. A script computing a delta of its own from a wall clock multiplies it by this, and one reading time.deltaTime() or a callback's dt already has it applied.
Returns number
typed/builtin//modules/api/engine/engine/engine/timeScaleOwner
engine.timeScaleOwner -> { id: string?, name: string?, scale: number, exclusive: boolean, held: boolean }?
Who engine.timeScale answers to: the hold naming the rate while one stands, and otherwise whoever wrote the rate outright last. scale is the rate in force, the same number engine.timeScale reads. While held is true, id is a key engine.releaseTimeScale hands the rate back by; for a write it is the agent the call was attributed to, and nil when the call carried no agent identity. name is that agent's username when its token carried one. exclusive is true while the hold refuses every other key. Nil when nothing has moved the rate this session. Read-only.
Returns { id: string?, name: string?, scale: number, exclusive: boolean, held: boolean }?
typed/builtin//modules/api/engine/engine/engine/vertexStride
engine.vertexStride -> number
Byte stride of the engine's standard GPU Vertex layout, which a mesh built from a compute buffer sizes and strides its writes to. Read-only.
Returns number
typed/builtin//modules/api/engine/engine/engine/waitForEdit
engine.waitForEdit(reason: string?) -> () -> ()
Say that this caller's edit-mode work is waiting on whoever holds play, and get back the call that takes the wait off. While the wait stands it rides the run-state block of every response any session gets from this engine, so the session holding play reads that somebody is waiting for it rather than finding out hours later. reason is what you are waiting to do, and it is shown beside your name. The wait is keyed by the caller the call was attributed to, so a named caller asking twice holds one entry carrying its newer reason, and a call carrying no agent identity stands as a waiter of its own. Reaching edit mode takes every wait off, so a caller that never calls the release leaves nothing behind.
Parameters
reasonstring(optional)
Returns () -> ()
typed/builtin//modules/api/engine/engine/engine/worldLoaded
engine.worldLoaded -> boolean
Whether the world entrypoint's onWorldLoad has run to completion. Read-only.
Returns boolean
typed/builtin//modules/api/engine/entity/E/batchAddComponent
E.batchAddComponent(targets: { string | entityRef }, type_name: string, data: table?) -> number
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.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies (e.g. the return ofentity.batchSpawnorentity.findAll).type_namestring— Component type to add to every entity.datatable(optional) — Init data: one table applied identically to every entity — the same shape the second arg toentity(id).component.add(type, data)takes — or an array holding one such table per target, in the targets' order, so each entity is added with its own values.
Returns number — How many entities had the component added.
local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 })
entity.batchAddComponent({ a, b }, "Debris", { { lifetime = 2 }, { lifetime = 7 } })
typed/builtin//modules/api/engine/entity/E/batchDespawn
E.batchDespawn(targets: { string | entityRef }) -> number
Despawn many entities in one call. Locked or unresolvable entities are skipped. Returns the count queued for despawn.
Parameters
targets{ string | entityRef }— Array of entity ids, entity proxies, or display names (e.g. the return ofentity.batchSpawn/entity.findAll).
Returns number — Count of entities queued for despawn.
local n = entity.batchDespawn(ids)
typed/builtin//modules/api/engine/entity/E/batchProxy
E.batchProxy(targets: { string | entityRef }) -> { entityRef? }
Resolve an array of entity ids to proxies in one call. Each output
slot is the standard entity(id) 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.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies.
Returns { entityRef? } — Array of proxies (nil for missing ids).
local proxies = entity.batchProxy(ids)
typed/builtin//modules/api/engine/entity/E/batchRead
E.batchRead(target: { string | entityRef } | binding, component: string?, field: string?, sink: TypedBuffer?) -> { any? } | number
Read a component-field across many entities in one call.
Polymorphic on the shape of target and sink:
entity.batchRead(ids)/(ids, comp)/(ids, comp, field)— 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.entity.batchRead(binding, comp, field, buffer)— reads each entity's field directly into a typed CPU substrate buffer (substrate.createBuffer({type="vec3"}), etc.) with no per-entity Lua table allocation. Returns the count of successful reads.Transform.worldMatrixreads each entity's world matrix — its own transform folded with its ancestors', the oneentity(id).worldMatrix()answers — as 16 column-major numbers: into amat4buffer, or as an array of 16 per entity. It is read, never written.targetaccepts an entity-id array or aecs.bindEntities(ids)handle. Buffer sinks require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring(optional) — Component type name (e.g. "Transform").fieldstring(optional) — Field name (e.g. "position").sinkTypedBuffer(optional) — Typed CPU buffer fromsubstrate.createBuffer({...})to memcpy field values into. Required whentargetis a binding.
Returns { any? } | number — Per-entity values when reading into Lua tables; count of reads when reading into a buffer.
local snapshot = entity.batchRead(ids)
local positions = entity.batchRead(ids, "Transform", "position")
entity.batchRead(ecs.bindEntities(ids), "Transform", "worldMatrix", matrices)
typed/builtin//modules/api/engine/entity/E/batchReadToBuffer
E.batchReadToBuffer(binding: number, component: string, field: string, buffer: number) -> number
FFI primitive backing entity.batchRead(binding, ..., buffer).
Prefer the unified entity.batchRead, 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 buf:read(0, count*stride).
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id, live while its handle is held.componentstring— Component type name.fieldstring— Field name to read.buffernumber— Destination buffer id (must be the matching type).
Returns number — Count of successful reads.
entity.batchReadToBuffer(binding.id, "Transform", "position", buf.id)
typed/builtin//modules/api/engine/entity/E/batchSpawn
E.batchSpawn(count: number, name_prefix: string?, opts: { temporary: boolean?, parent: (string | entityRef)? }?) -> { string }
Spawn count entities in one call. Returns an array of the new
entity ids in spawn order. Each entity is given a display name of
<name_prefix><i> (or entity<i> if the prefix is omitted). Prefer this
over looping entity.spawn when creating large entity counts.
Parameters
countnumber— How many entities to spawn (capped at 1,000,000).name_prefixstring(optional) — Display-name prefix appended with the 1-based index. Defaults to "entity".opts{ temporary: boolean?, parent: (string | entityRef)? }(optional) —{ temporary?, parent? }— a temporary batch is one the scene does not save, the same thingentity.spawn({ temporary = true })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.parent— 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.
Returns { string } — Array of newly-spawned entity ids.
local ids = entity.batchSpawn(100, "grass_")
local particles = entity.batchSpawn(5000, "spark_", { temporary = true })
local parts = entity.batchSpawn(8, "part_", { temporary = true, parent = owner })
typed/builtin//modules/api/engine/entity/E/batchWrite
E.batchWrite(target: { string | entityRef } | binding, component: string, field: string, source: { any? } | TypedBuffer) -> number
Write a single component-field across many entities in one call.
Polymorphic on the shape of target and source:
entity.batchWrite(ids, comp, field, values)— per-call entity-id resolution;valuesis an array the same length asids(nil slots are skipped). Use for one-shot writes.entity.batchWrite(binding, comp, field, values)— binding handle fromecs.bindEntities(ids); skips per-call id resolution. Use for per-frame writes against a stable entity set.entity.batchWrite(binding, comp, field, buffer)— typed CPU buffer source (substrate.createBuffer({type="vec3"}), etc.), with no per-entity table allocation. Returns the count of successful writes. Buffer sources require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring— Component type name.fieldstring— Field name to write.source{ any? } | TypedBuffer— Per-entity values array (nil entries are skipped), or a typed CPU buffer fromsubstrate.createBuffer({...}). A buffer source requires a bindingtarget.
Returns number — Count of successful writes.
entity.batchWrite(ids, "Transform", "position", positions)
typed/builtin//modules/api/engine/entity/E/batchWriteBound
E.batchWriteBound(binding: number, component: string, field: string, values: { any? }) -> number
FFI primitive backing entity.batchWrite(binding, ...) with a
per-entity values table. Prefer the unified entity.batchWrite, which
auto-dispatches by argument shape; this entry stays for power users /
debug code that wants to skip dispatch overhead.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id, live while its handle is held.componentstring— Component type name.fieldstring— Field name to write.values{ any? }— Per-entity source values (nil = skip). Length must match the binding's entity count.
Returns number — Count of successful writes.
entity.batchWriteBound(binding.id, "Transform", "position", values)
typed/builtin//modules/api/engine/entity/E/batchWriteFromBuffer
E.batchWriteFromBuffer(binding: number, component: string, field: string, buffer: number) -> number
FFI primitive backing entity.batchWrite(binding, ..., buffer).
Prefer the unified entity.batchWrite, which auto-dispatches by
argument shape. Caller fills a typed substrate buffer
(substrate.createBuffer({type="vec3"})) once via buf:write(...),
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.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id, live while its handle is held.componentstring— Component type name.fieldstring— Field name to write.buffernumber— Buffer id fromsubstrate.createBuffer({type="vec3", len=N}).id.
Returns number — Count of successful writes.
entity.batchWriteFromBuffer(binding.id, "Transform", "position", buf.id)
typed/builtin//modules/api/engine/entity/E/capture
E.capture(builder: () -> ()) -> ({ string }, any?, { string }, { any })
Run builder 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
entity.spawn, entity.spawnSynced, entity.batchSpawn, and
entity.instantiate 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.
Parameters
builder() -> ()— Function run inside the scope; the entities it creates are what comes back.
Returns ({ string }, any?, { string }, { any }) — Entity ids minted while the builder ran, in creation order; the error it raised (or the refusals it hit), or nil; the ids a component the builder attached minted in its own lifecycle; and the refusals the scope issued, each with message, at and source.
local ids, err, reproduced, refusals = entity.capture(function() entity.spawn("chair") end)
typed/builtin//modules/api/engine/entity/E/capturing
E.capturing() -> boolean
Whether an entity.capture 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.
Returns boolean — true inside a capture scope.
if entity.capturing() then return end
typed/builtin//modules/api/engine/entity/E/despawn
E.despawn(target: string | entityRef)
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.
Parameters
targetstring | entityRef— Entity id, name, or entity proxy. A name despawns all entities sharing that name.
entity.despawn(id)
entity.despawn("Enemy") -- despawns every entity named "Enemy"
typed/builtin//modules/api/engine/entity/E/duplicate
E.duplicate(sourceId: string | entityRef, name: string?, opts: table?) -> string?
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 sourceId 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. includeTemporary copies them too, for the
hierarchy that IS the temporary thing.
Parameters
sourceIdstring | entityRef— Entity id or entity proxy of the source entity to clone.namestring(optional) — Display name for the copy (defaults to source name + " (copy)").optstable(optional) —{ includeTemporary?: boolean, descendants?: boolean, components?: table, name?: string }:descendants = falsecopies the source entity alone, leaving its children to it;componentslays fields over the copy's script components by type ({ SoftBody = { mass = 2 } }), merged over the state copied from the source;nameis the same field thenameargument sets, and wins when both are given.
Returns string? — The new entity's id, or nil when the source is not live.
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 } } })
typed/builtin//modules/api/engine/entity/E/exists
E.exists(idOrProxy: string | entityRef) -> boolean
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 entity(id). 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 entity.find(name) ~= nil.
Parameters
idOrProxystring | entityRef— Entity id or an entity proxy.
Returns boolean — true if the entity exists.
if entity.exists(id) then ... end
typed/builtin//modules/api/engine/entity/E/find
E.find(nameOrGlob: string) -> entityRef?
Find the first entity matching nameOrGlob. A plain string matches
an exact id or Name component; a string containing * (any run of
characters) or ? (any single character) matches Names as a glob, so
entity.find("enemy_*") is the first entity whose name starts with
enemy_. 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 entity.findAll when every
match matters; find is the first entity findAll lists. Inside a build
(a scene's content(), a module's builder), a name the build's own
entities carry resolves to those first, so entity.spawn("door")
followed by entity.find("door") is the door the build just made even
when the world around it holds another door.
Parameters
nameOrGlobstring— Exact entity Name or id, or a*/?glob over Names.
Returns entityRef? — First matching entity proxy, or nil.
local e = entity.find("enemy_*")
typed/builtin//modules/api/engine/entity/E/findAll
E.findAll(nameOrGlob: string?) -> { entityRef }
Enumerate entity proxies. With a nameOrGlob argument, returns every
entity whose Name component or id matches (names are not unique): a
plain string matches exactly, while a * / ? glob matches Names. With
no argument, returns every entity in the current snapshot —
findAll("") 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: entity.ids(entity.findAll(...)).
Inside a build (a scene's content(), a module's builder), the entities
the build made come first and the world's same-named entities follow.
Parameters
nameOrGlobstring(optional) — Exact entity Name or id to filter by, or a*/?glob over Names. Omit to enumerate every entity.
Returns { entityRef } — Array of entity proxies, possibly empty.
for _, e in entity.findAll("enemy_*") do e:despawn() end
typed/builtin//modules/api/engine/entity/E/getChildren
E.getChildren(id: string | entityRef) -> { entityRef }
Get an array of the direct children as entity proxies. Each element
carries .name, .id, .position, .component, and the rest of the
per-entity surface, the same shape entity.findAll returns. A string
that names a live entity rather than identifying it raises, naming
entity.find(name), the lookup that takes a name.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns { entityRef } — Array of child entity proxies, possibly empty.
for _, c in entity.getChildren(id) do c.internal = true end
typed/builtin//modules/api/engine/entity/E/getDescendants
E.getDescendants(id: string | entityRef) -> { entityRef }
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
entity.find(name), the lookup that takes a name.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns { entityRef } — Array of descendant entity proxies, possibly empty.
local all = entity.getDescendants(id)
typed/builtin//modules/api/engine/entity/E/getParent
E.getParent(id: string | entityRef) -> entityRef?
Get the parent entity proxy, or nil if the entity is a root entity.
The returned proxy carries .name, .id, .position, .component,
and the rest of the per-entity surface — the same shape entity.find
returns. A string that names a live entity rather than identifying it
raises, naming entity.find(name), the lookup that takes a name:
answering it as an id would read the same as a root entity.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns entityRef? — Parent entity proxy, or nil.
local p = entity.getParent(id)
typed/builtin//modules/api/engine/entity/E/instantiate
E.instantiate(handle: EntityTemplate, count: number, fn: ((number) -> EntityInstantiateOverrides?)?) -> { string }
Spawn count instances of a template registered with
entity.template. Each instance gets a fresh entity id; the optional
fn(i) callback runs per instance (i in 1..=count) and may return an
overrides table. Override keys: name, position, rotation, scale,
parent, temporary / active / internal, attributes, components
(script components, merged over the template body's data for that type
— a type the template lacks is added fresh), and ecs (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 queue() 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.
Parameters
handleEntityTemplate— Template handle fromentity.template.countnumber— Number of instances to spawn (capped at 1,000,000).fn((number) -> EntityInstantiateOverrides?)(optional) — Per-instance override callback(i) -> table?.
Returns { string } — Array of newly-spawned entity ids in spawn order.
local ids = entity.instantiate(h, 50, function(i) return { position = { i, 0, 0 } } end)
typed/builtin//modules/api/engine/entity/E/sceneWalks
E.sceneWalks() -> number
How many whole-scene walks entity.findAll 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.
Returns number — The count of walks.
local before = entity.sceneWalks(); entity(id):rename("door"); assert(entity.sceneWalks() == before)
typed/builtin//modules/api/engine/entity/E/spawn
E.spawn(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?) -> entityRef
Spawn a new entity and return its PROXY (the same value entity(id)
yields) — act on it immediately (entity.spawn(name).component.add(...),
.localPosition = ...) with no second entity(id) round trip. The proxy
still exposes .id for the rare site that needs the raw string. An
entity with no components carries only a Transform and is invisible;
pass components to give it the components that make it visible in the
same call — entity.spawn { name = "crate", components = { Model = { model = "cube" } } } — or add them afterwards through the returned
proxy. Mirrors entity.find / entity.findAll, which also return
proxies. The options table can be passed on its own with the name inside
it — entity.spawn { name = "turret", position = { 1, 2, 3 } } is the
same call as entity.spawn("turret", { position = { 1, 2, 3 } }).
Parameters
nameOrOpts(string | SpawnOpts)(optional) — Display name for the entity, or the options table itself.optsSpawnOpts(optional) — Options:components= 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),internal= take the entity out of the default entity listings (it still renders —entity(id):hide()stops the draw),parent= parent entity id or proxy,temporary= skip this entity (and descendants) from scene/world saves,position/rotation/scale= place the entity's Transform at spawn,attributes= per-entity attribute values keyed by name ({ role = "guard" }, the same valuesentity(id).attribute.set(key, value)writes andentity.template/entity.instantiatetake),synced= flag the entity multiplayer-synced (whatentity.spawnSyncedimplies),active= false spawns it inactive,scene_owner= the scene layer guid the entity is attributed to (inherited from the spawning scene when unset),id= restore a previously-assigned entity id (scene_loader use; leave unset for a normal spawn). An unrecognised key is rejected loudly.
Returns entityRef — Proxy for the new entity (carries .id, .component, transform properties, etc.).
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 } })
typed/builtin//modules/api/engine/entity/E/spawnSynced
E.spawnSynced(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?) -> entityRef
Spawn an entity already flagged multiplayer-synced at the root — the
explicit form of entity.spawn 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
entity.spawn) 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 onHostLoad
(host-only) phase, or behind multiplayer.isHost(). Calling it in
all-client code makes every client spawn+sync its own copy — the
double-spawn "explosion". Equivalent to
entity.spawn(name, { synced = true }); identical in every other
respect.
Parameters
nameOrOpts(string | SpawnOpts)(optional) — Display name for the entity, or the options table itself.optsSpawnOpts(optional) — Same options asentity.spawn(syncedis already implied).
Returns entityRef — Proxy for the new synced entity.
if multiplayer.isHost() then entity.spawnSynced("Goblin") end
typed/builtin//modules/api/engine/entity/E/template
E.template(def: EntityTemplateDef) -> EntityTemplate
Construct a reusable spawn template. Captures a shared entity config
ONCE and returns an EntityTemplate handle for entity.instantiate(handle, count, fn?) — 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. def keys:
components (script components, { [type] = init-data }), ecs
(array of native ecs.X{...} components), temporary (instances skip
scene/world saves), active (spawn state), internal (instances are
taken out of the default entity listings; they still render),
attributes ({ key = value } applied to every instance). Every value
is a shared default; a per-instance entity.instantiate override
supersedes it. The template body is captured by value — later edits to
the source table do not affect templates already created.
Parameters
defEntityTemplateDef— Template definition:components/ecs/temporary/active/internal/attributes. Per-instancename/position/rotation/scale/parentand any override go through theinstantiatecallback.
Returns EntityTemplate — The template's handle for entity.instantiate.
local h = entity.template({ components = { Model = { model = "cube" } } })
typed/builtin//modules/api/engine/entity/E/tree
E.tree(opts: { [string]: any }?) -> { [string]: any }
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. expanded names the ids whose
children unfold, and a collapsed node still reports its childCount;
filter 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
matched. offset / limit window the flattened rows, layer scopes the
window and its total to one layer while layers still reports every
layer's row count, and revision echoes
getEntitiesRevision("structure"), which moves only on structural change.
Parameters
opts{ [string]: any }(optional) —{ layer?, expanded?, filter?, offset?, limit? }.
Returns { [string]: any } — { rows, total, layers, revision }.
local view = entity.tree({ filter = "crate", limit = 50 })
typed/builtin//modules/api/engine/entity/entityHierarchy/swap
entityHierarchy.swap(entityId: string, assetPath: string, opts: { [string]: any }?) -> (string?, string?)
Replace a blockout entity with a generated or imported asset, fitting the asset to the source's bounds. Answers the new entity id, or nil and an error string.
Parameters
entityIdstringassetPathstringopts{ [string]: any }(optional)
Returns (string?, string?)
typed/builtin//modules/api/engine/entity/members/entityAttributes/get
entityAttributes.get(key: string) -> any?
The value stored under a key, or nil when the entity carries none.
Parameters
keystring
Returns any?
typed/builtin//modules/api/engine/entity/members/entityAttributes/list
entityAttributes.list() -> { string }
Every attribute key this entity carries.
Returns { string }
typed/builtin//modules/api/engine/entity/members/entityAttributes/remove
entityAttributes.remove(key: string) -> ()
Drop the value stored under a key.
Parameters
keystring
Returns ()
typed/builtin//modules/api/engine/entity/members/entityAttributes/set
entityAttributes.set(key: string, value: any) -> ()
Store a value under a key on this entity.
Parameters
keystringvalueany
Returns ()
typed/builtin//modules/api/engine/entity/members/entityComponents/add
entityComponents.add(type: AssetRef | string, data: table?) -> table?
Attach a component, answering its live public proxy — nil when the add was deferred or skipped as a duplicate.
Parameters
typeAssetRef | stringdatatable(optional)
Returns table?
typed/builtin//modules/api/engine/entity/members/entityComponents/addSynced
entityComponents.addSynced(type: AssetRef | string, data: table?) -> table?
Attach a component and replicate it to every peer.
Parameters
typeAssetRef | stringdatatable(optional)
Returns table?
local health = entity(id).component.addSynced('Health', { hp = 100 })
typed/builtin//modules/api/engine/entity/members/entityComponents/clear
entityComponents.clear() -> ()
Detach every component this entity carries.
Returns ()
typed/builtin//modules/api/engine/entity/members/entityComponents/create
entityComponents.create(type: AssetRef | string, data: table?) -> table?
Attach a fresh instance even where one of the type is already present.
Parameters
typeAssetRef | stringdatatable(optional)
Returns table?
typed/builtin//modules/api/engine/entity/members/entityComponents/get
entityComponents.get(type: AssetRef | string, instanceName: string?) -> table?
This entity's live component proxy of the type, or nil when it carries none.
Parameters
typeAssetRef | stringinstanceNamestring(optional)
Returns table?
typed/builtin//modules/api/engine/entity/members/entityComponents/getAll
entityComponents.getAll(type: (AssetRef | string)?) -> table
Every component on this entity, or every instance of one type.
Parameters
type(AssetRef | string)(optional)
Returns table
typed/builtin//modules/api/engine/entity/members/entityComponents/getFromChildren
entityComponents.getFromChildren(type: AssetRef | string) -> table?
The first descendant's component of the type, or nil when no descendant carries one.
Parameters
typeAssetRef | string
Returns table?
typed/builtin//modules/api/engine/entity/members/entityComponents/getFromParent
entityComponents.getFromParent(type: AssetRef | string) -> table?
The nearest ancestor's component of the type, or nil when no ancestor carries one.
Parameters
typeAssetRef | string
Returns table?
typed/builtin//modules/api/engine/entity/members/entityComponents/has
entityComponents.has(type: AssetRef | string) -> boolean
Whether this entity carries a component of the type.
Parameters
typeAssetRef | string
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityComponents/list
entityComponents.list() -> { string }
The identity string of every component type this entity carries, each of which get takes.
Returns { string }
for _, identity in ipairs(entity(id).component.list()) do
local component = entity(id).component.get(identity)
component.enabled = false
end
typed/builtin//modules/api/engine/entity/members/entityComponents/lock
entityComponents.lock(names: { string } | string, flags: { [string]: any }?) -> ()
Lock named components on this entity against removal or writes.
Parameters
names{ string } | stringflags{ [string]: any }(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityComponents/locks
entityComponents.locks() -> { [string]: any }
The lock state of this entity's components.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityComponents/pending
entityComponents.pending(type: (AssetRef | string)?) -> { { type: string, instance: string?, instanceId: string, fields: { { name: string, spec: string, value: string } } } }
One record per component whose awake() is held back waiting for an asset field to register, naming the component and the fields it waits on; every such component when no type is given.
Parameters
type(AssetRef | string)(optional)
Returns { { type: string, instance: string?, instanceId: string, fields: { { name: string, spec: string, value: string } } } }
local waiting = entity(id).component.pending('Terrain')
if #waiting > 0 then print(waiting[1].fields[1].name, waiting[1].fields[1].value) end
typed/builtin//modules/api/engine/entity/members/entityComponents/remove
entityComponents.remove(type: AssetRef | string, instanceName: string?) -> ()
Detach a component, by type and optionally by instance name.
Parameters
typeAssetRef | stringinstanceNamestring(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityComponents/setEnabled
entityComponents.setEnabled(type: AssetRef | string, enabled: boolean) -> ()
Enable or disable a component without detaching it.
Parameters
typeAssetRef | stringenabledboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityComponents/unlock
entityComponents.unlock(names: ({ string } | string)?, flags: { [string]: any }?) -> ()
Release locks this entity's components hold; every one of them when no name is given.
Parameters
names({ string } | string)(optional)flags{ [string]: any }(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/AddDebris
entityRef.AddDebris(self, lifetime: number?) -> ()
Despawn this entity after a lifetime, defaulting to 10 seconds.
Parameters
selflifetimenumber(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/active
entityRef.active -> boolean
Whether this entity is active.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/activeInHierarchy
entityRef.activeInHierarchy -> boolean
Whether this entity is active via its ancestors.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/attribute
entityRef.attribute -> entityAttributes
Free-form key/value attributes stored on this entity.
Returns entityAttributes
typed/builtin//modules/api/engine/entity/members/entityRef/bounds
entityRef.bounds(self) -> { min: any, max: any, center: any, size: any }?
This entity's world-axis bounding box.
Parameters
self
Returns { min: any, max: any, center: any, size: any }?
typed/builtin//modules/api/engine/entity/members/entityRef/bundleLink
entityRef.bundleLink -> any?
This entity's link back to its bundle.
Returns any?
typed/builtin//modules/api/engine/entity/members/entityRef/bundleProvenance
entityRef.bundleProvenance -> { [string]: any }?
Which bundle produced this entity.
Returns { [string]: any }?
typed/builtin//modules/api/engine/entity/members/entityRef/component
entityRef.component -> entityComponents
Script components attached to this entity.
Returns entityComponents
typed/builtin//modules/api/engine/entity/members/entityRef/despawn
entityRef.despawn(self) -> ()
Remove this entity from the scene.
Parameters
self
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/destroy
entityRef.destroy(self) -> ()
Remove this entity from the scene.
Parameters
self
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/duplicate
entityRef.duplicate(self, name: string?, opts: { [string]: any }?) -> string?
Copy this entity.
Parameters
selfnamestring(optional)opts{ [string]: any }(optional)
Returns string?
typed/builtin//modules/api/engine/entity/members/entityRef/eulerAngles
entityRef.eulerAngles -> { [string]: any }
World rotation, as euler degrees.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/exists
entityRef.exists -> boolean
Whether this entity is still in the world. Never raises — a proxy over an id nothing carries reads false.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/getChildren
entityRef.getChildren(self) -> { entityRef }
This entity's direct children.
Parameters
self
Returns { entityRef }
typed/builtin//modules/api/engine/entity/members/entityRef/getDescendants
entityRef.getDescendants(self) -> { entityRef }
Every entity below this one.
Parameters
self
Returns { entityRef }
typed/builtin//modules/api/engine/entity/members/entityRef/getParent
entityRef.getParent(self) -> entityRef?
This entity's parent, or nil.
Parameters
self
Returns entityRef?
typed/builtin//modules/api/engine/entity/members/entityRef/hide
entityRef.hide(self) -> ()
Stop drawing this entity.
Parameters
self
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/hierarchyBounds
entityRef.hierarchyBounds(self) -> { min: any, max: any, center: any, size: any }?
The world-axis box around this entity's subtree.
Parameters
self
Returns { min: any, max: any, center: any, size: any }?
typed/builtin//modules/api/engine/entity/members/entityRef/id
entityRef.id -> string
This entity's id string.
Returns string
typed/builtin//modules/api/engine/entity/members/entityRef/internal
entityRef.internal -> boolean
Whether default listings skip this entity.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/isLocal
entityRef.isLocal(self) -> boolean
Whether this peer owns this entity.
Parameters
self
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/localEulerAngles
entityRef.localEulerAngles -> { [string]: any }
Parent-relative euler degrees.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/localPosition
entityRef.localPosition -> { [string]: any }
Parent-relative position.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/localRotation
entityRef.localRotation -> { [string]: any }
Parent-relative rotation quaternion.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/localScale
entityRef.localScale -> { [string]: any }
Parent-relative scale.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/lock
entityRef.lock(self) -> ()
Set this entity's destroy lock.
Parameters
self
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/lock_components
entityRef.lock_components(self, names: { string }, flags: { [string]: any }?) -> ()
Lock named components on this entity.
Parameters
selfnames{ string }flags{ [string]: any }(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/locked
entityRef.locked -> boolean
Whether this entity carries a destroy lock.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/locks
entityRef.locks(self) -> { destroy: boolean, components: { [string]: any } }
This entity's current lock state.
Parameters
self
Returns { destroy: boolean, components: { [string]: any } }
typed/builtin//modules/api/engine/entity/members/entityRef/lookAt
entityRef.lookAt(self, target: any, up: any?) -> (boolean, string?)
Aim this entity at a world point: writes the world rotation whose forward points at the target. Takes three coordinates, one point table or vec handle, or an entity by id, name or proxy. The optional up decides the roll around the aim.
Parameters
selftargetanyupany(optional)
Returns (boolean, string?)
typed/builtin//modules/api/engine/entity/members/entityRef/lossyScale
entityRef.lossyScale -> { [string]: any }
World scale, as the hierarchy leaves it.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/name
entityRef.name -> string
This entity's Name component value.
Returns string
typed/builtin//modules/api/engine/entity/members/entityRef/networkScope
entityRef.networkScope -> string
How far this entity replicates.
Returns string
typed/builtin//modules/api/engine/entity/members/entityRef/orientedBounds
entityRef.orientedBounds(self) -> { min: any, max: any, center: any, size: any }?
The subtree's extents in this entity's own frame.
Parameters
self
Returns { min: any, max: any, center: any, size: any }?
typed/builtin//modules/api/engine/entity/members/entityRef/origin
entityRef.origin -> string
What produced this entity.
Returns string
typed/builtin//modules/api/engine/entity/members/entityRef/owner
entityRef.owner(self) -> number
The peer owning this entity.
Parameters
self
Returns number
typed/builtin//modules/api/engine/entity/members/entityRef/participation
entityRef.participation -> string
This entity's runtime participation.
Returns string
typed/builtin//modules/api/engine/entity/members/entityRef/position
entityRef.position -> { [string]: any }
World position.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/rename
entityRef.rename(self, newName: string) -> ()
Change this entity's Name component.
Parameters
selfnewNamestring
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/renderLayer
entityRef.renderLayer -> string
The render layers this entity is on, space-separated.
Returns string
typed/builtin//modules/api/engine/entity/members/entityRef/rotation
entityRef.rotation -> { [string]: any }
World rotation, as a quaternion.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/saveAs
entityRef.saveAs(self, name: string) -> any
Save this entity as a content asset.
Parameters
selfnamestring
Returns any
typed/builtin//modules/api/engine/entity/members/entityRef/setActive
entityRef.setActive(self, active: boolean) -> ()
Set whether this entity is active.
Parameters
selfactiveboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setInternal
entityRef.setInternal(self, internal: boolean) -> ()
Set whether default listings skip this entity.
Parameters
selfinternalboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setNetworkScope
entityRef.setNetworkScope(self, scope: string) -> ()
Set how far this entity replicates.
Parameters
selfscopestring
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setParent
entityRef.setParent(self, parentId: string, opts: { [string]: any }?) -> ()
Reparent this entity.
Parameters
selfparentIdstringopts{ [string]: any }(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setParticipation
entityRef.setParticipation(self, mode: string) -> ()
Set this entity's runtime participation.
Parameters
selfmodestring
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setRenderLayer
entityRef.setRenderLayer(self, names: any) -> ()
Set this entity's render layer.
Parameters
selfnamesany
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setRenderLayerTree
entityRef.setRenderLayerTree(self, names: any) -> ()
Set render layer for this entity and its descendants.
Parameters
selfnamesany
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setRenderLayers
entityRef.setRenderLayers(self, names: any) -> ()
Set this entity's render layers.
Parameters
selfnamesany
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setSessionScoped
entityRef.setSessionScoped(self, scoped: boolean) -> ()
Scope this entity to the session.
Parameters
selfscopedboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setSynced
entityRef.setSynced(self, synced: boolean) -> ()
Set whether this entity replicates.
Parameters
selfsyncedboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setSyncedTree
entityRef.setSyncedTree(self, synced: boolean) -> ()
Set replication for this entity and its descendants.
Parameters
selfsyncedboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/setTemporary
entityRef.setTemporary(self, temporary: boolean) -> ()
Set whether saves skip this entity.
Parameters
selftemporaryboolean
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/show
entityRef.show(self) -> ()
Resume drawing this entity.
Parameters
self
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/synced
entityRef.synced -> boolean
Whether this entity replicates.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/temporary
entityRef.temporary -> boolean
Whether saves skip this entity.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/temporaryInHierarchy
entityRef.temporaryInHierarchy -> boolean
Whether saves skip this entity via an ancestor.
Returns boolean
typed/builtin//modules/api/engine/entity/members/entityRef/transform
entityRef.transform -> { [string]: any }
This entity's transform.
Returns { [string]: any }
typed/builtin//modules/api/engine/entity/members/entityRef/unlock
entityRef.unlock(self) -> ()
Clear every lock on this entity.
Parameters
self
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/unparent
entityRef.unparent(self, opts: { [string]: any }?) -> ()
Detach this entity from its parent.
Parameters
selfopts{ [string]: any }(optional)
Returns ()
typed/builtin//modules/api/engine/entity/members/entityRef/worldMatrix
entityRef.worldMatrix -> { number }
This entity's world matrix.
Returns { number }
typed/builtin//modules/api/engine/entity/ref/M/build
M.build(id: string) -> EntityRef
Factory that backs entity(id). Returns a cached proxy when one already exists for id; otherwise allocates and caches a fresh one. The cache is weak-valued so unreferenced proxies are GC'd.
Parameters
idstring— The entity id string.
Returns EntityRef — The (cached) entity proxy — an EntityRef.
local EntityProxy = require("@builtin::modules.api.engine.entity.ref")
local proxy = EntityProxy.build("some-entity-id")
typed/builtin//modules/api/engine/environment/environment/capture
environment.capture(x: number, y: number, z: number) -> boolean
Bake the scene into the environment from (x, y, z) 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.
Parameters
xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.
Returns boolean — True — the capture was queued.
environment.capture(0, 2, 0)
typed/builtin//modules/api/engine/environment/environment/captureSky
environment.captureSky(x: number?, y: number?, z: number?) -> boolean
Render the SKY alone into the environment's sky slot from (x, y, z) 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.
Parameters
xnumber(optional) — World X of the capture position. Defaults to 0.ynumber(optional) — World Y of the capture position — the altitude a height-dependent atmosphere is sampled at. Defaults to 0.znumber(optional) — World Z of the capture position. Defaults to 0.
Returns boolean — True — the sky capture was queued.
environment.captureSky()
typed/builtin//modules/api/engine/environment/environment/captureSlot
environment.captureSlot(slot: number, x: number, y: number, z: number) -> boolean
Bake the scene into reflection-probe slot from (x, y, z).
Renders the FULL scene (geometry + sky) six times from that
point into that slot. Register the probe's position+radius via setProbes
so surfaces blend it by proximity. Queued — takes effect next frame.
Parameters
slotnumber— Reflection-probe slot (0-based).xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.
Returns boolean — True — the capture was queued.
environment.captureSlot(0, 0, 2, 0)
typed/builtin//modules/api/engine/environment/environment/captureSlotToAsset
environment.captureSlotToAsset(name: string, slot: number, x: number, y: number, z: number, timeoutFrames: number?) -> (string?, string?)
Bake the scene into reflection-probe slot from (x, y, z) AND persist
the 6 rendered faces into a faces6 .texture cubemap asset at
/source/<name>.texture/ (px/nx/py/ny/pz/nz PNGs + a cube.yaml 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, task.spawn, or execute).
Parameters
namestring— Destination asset identity (writes/source/<name>.texture/).slotnumber— Reflection-probe slot (0-based).xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.timeoutFramesnumber(optional) — 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).
Returns (string?, string?) — The asset path on success, or (nil, errorMessage) on failure.
environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0)
typed/builtin//modules/api/engine/environment/environment/captureToAsset
environment.captureToAsset(name: string, x: number, y: number, z: number) -> (string?, string?)
Bake the single global reflection AND persist it to a faces6 .texture
asset (slot 0). Yields a few frames; call from a task/coroutine context.
Parameters
namestring— Destination asset identity (writes/source/<name>.texture/).xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.
Returns (string?, string?) — The asset path on success, or (nil, errorMessage) on failure.
environment.captureToAsset("env_main", 0, 2, 0)
typed/builtin//modules/api/engine/environment/environment/ensureSkyFallback
environment.ensureSkyFallback() -> boolean
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.
Returns boolean — True if a capture was queued, false if the sky slot already holds one.
environment.ensureSkyFallback()
typed/builtin//modules/api/engine/environment/environment/loadFromAsset
environment.loadFromAsset(name: string) -> (boolean, string?)
Load a persisted global reflection asset into slot 0 and make it the active single reflection (one full-coverage probe).
Parameters
namestring— Source asset identity (reads/source/<name>.texture/).
Returns (boolean, string?) — True on success, or (false, errorMessage) on failure.
environment.loadFromAsset("env_main")
typed/builtin//modules/api/engine/environment/environment/loadSlotFromAsset
environment.loadSlotFromAsset(name: string, slot: number) -> (boolean, string?)
Load a persisted faces6 .texture cubemap (written by
captureSlotToAsset) into reflection-probe slot WITHOUT re-rendering the
scene. Reads the 6 face PNGs from /source/<name>.texture/ and uploads them
into the slot's cube layers. How a persisted probe restores its baked
environment on reload.
Parameters
namestring— Source asset identity (reads/source/<name>.texture/).slotnumber— Reflection-probe slot (0-based).
Returns (boolean, string?) — True on success, or (false, errorMessage) on failure.
environment.loadSlotFromAsset("probe_lobby", 0)
typed/builtin//modules/api/engine/environment/environment/setProbes
environment.setProbes(probes: { any }) -> boolean
Set the active reflection probes' blend data. probes is an array of
{ x, y, z, radius } (or { position = {x,y,z}, radius = r }); index i is
probe slot i. Surfaces blend the probe slots by proximity to these
positions, gathering the highest priority 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 captureSky has run. Queued for next frame.
Parameters
probes{ any }— Array of{ x, y, z, radius, priority? }, one per active probe slot.prioritydefaults to 0.
Returns boolean — True — the probe data was queued.
environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } })
typed/builtin//modules/api/engine/environment/environment/setSkyFallback
environment.setSkyFallback(active: boolean) -> boolean
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 (captureSky fills it), since
an uncaptured slot reflects black; renderer.reflectionEnvironment()
reports whether the fallback ended up armed.
Parameters
activeboolean— Whether reflections fall back to the captured sky.
Returns boolean — True — the change was queued.
environment.setSkyFallback(false)
typed/builtin//modules/api/engine/environment/environment/stateFromAsset
environment.stateFromAsset(identity: string) -> (boolean, string?)
Make a faces6 .texture 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
identity, which is the name a camera's environmentMap 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.
Parameters
identitystring— The cube texture, by the identityasset.resolvetakes.
Returns (boolean, string?) — True once the environment is queued, or (false, errorMessage).
task.spawn(environment.stateFromAsset, "@builtin::textures.studio_environment")
typed/builtin//modules/api/engine/font/font/coverage
font.coverage(bytes: buffer | string) -> string?
The codepoints a font file maps to a glyph, as the coverage text
font.declare reads: comma-separated inclusive hexadecimal ranges.
Parameters
bytesbuffer | string— Raw font-file bytes (TTF / OTF).
Returns string? — Coverage text, or nil if the bytes don't parse as a font.
local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))
typed/builtin//modules/api/engine/font/font/declare
font.declare(name: string, coverage: string, opts: table) -> number
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 text.* object, or font.prefetch
reaches a covered character, the engine requests the face: its file is read
and registered with font.register(name, bytes, opts), and every text
surface draws the character from it from then on. A .font asset whose
face.json lists onDemand chunks declares each of them through this call.
Parameters
namestring— The name the face is declared and registered under.coveragestring— The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the textfont.coverage(bytes)writes.optstable—{ file: string, family: string?, role: string?, aliases: {string}? }naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and thefont.registeroptions it installs with.
Returns number — How many codepoints the coverage holds.
font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })
typed/builtin//modules/api/engine/font/font/glyph
font.glyph(name: string, codepoint: number) -> any
Read one glyph's vectorized outline from a registered font, in font
units (resolution-independent — scale by fontSize / unitsPerEm).
Parameters
namestring— Registered family name.codepointnumber— Unicode codepoint (e.g.string.byte("A")).
Returns any — { advance, unitsPerEm, bbox = {xMin,yMin,xMax,yMax}, contours } where each contour is { start = {x,y}, segments = { {kind="line|quad|cubic", ...} } }, or nil if the font isn't registered.
local g = font.glyph("Inter", string.byte("A"))
typed/builtin//modules/api/engine/font/font/list
font.list() -> { string }
List every registered font family name.
Returns { string } — Array of family-name strings.
for _, fam in font.list() do print(fam) end
typed/builtin//modules/api/engine/font/font/observe
font.observe() -> { any }
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. weights is what a
style's weight can name for that family. The same rows are fonts in
text.observe().
Returns { any } — Array of { family, faces, weights, italic, loaded }.
for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end
typed/builtin//modules/api/engine/font/font/onDemand
font.onDemand() -> { any }
Every on-demand face font.declare declared, in declaration order, and
where each stands: declared (its file never read), requested (a
character it covers reached a text surface that could not draw it, and its
file is being read), installed (registered on every text surface; bytes
is the size of the file it installed from) or failed (error says why).
requestedBy is the character whose miss requested it, and family the
family group it installs into (opts.family of its font.declare, else
its own name); ui.listFonts() lists each face under that family's row.
Returns { any } — Array of { name, family, state, codepoints, requestedBy, bytes, error }.
for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end
typed/builtin//modules/api/engine/font/font/parse
font.parse(bytes: buffer | string) -> string?
Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized
glyph format (ZFNT): per-glyph vector outlines + metrics + character map,
plus the original bytes. Heavy — run at import time (the .font assetType's
onCreate / the font importer), then store the result as the asset payload.
font.register loads it cheaply.
Parameters
bytesbuffer | string— Raw font-file bytes (binary-safe) — TTF / OTF.
Returns string? — Baked ZFNT payload (binary-safe string), or nil if the bytes don't parse as a font.
local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))
typed/builtin//modules/api/engine/font/font/prefetch
font.prefetch(text: string) -> number
Request, ahead of drawing it, every on-demand face covering a character
of text that no requested or installed face covers: a screen about to
show a script starts reading its faces before its first frame.
Parameters
textstring— Text whose characters the faces must cover.
Returns number — How many faces this call requested.
font.prefetch("日本語")
typed/builtin//modules/api/engine/font/font/ready
font.ready(text: string) -> boolean
Whether text draws without waiting on an on-demand face: no
character of it is covered by a face still declared or requested.
Call font.prefetch(text) 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 (ui.missingGlyphs() names it once drawn) and for
a face that failed (font.onDemand() says why).
Parameters
textstring— Text to be drawn.
Returns boolean — Whether every on-demand face the text needs is installed.
font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)
typed/builtin//modules/api/engine/font/font/reconcile
font.reconcile() -> { any }
Every family the text shaper can resolve, held against what the shaper
does with it. family is the name, faces how many faces of it the font
database holds, weights the numeric weights those faces carry, loaded
whether it arrived through a registration rather than from the platform,
registered whether content registered the name, selectable whether some
style naming the family reaches it, matched whether fontFamily = family
on its own reaches it — the family name at the default weight over Latin
text — weight the weight it needs when the default is not it, shapedWith
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 reason 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 matched false and weight naming what the style
must carry. Every probe object is destroyed again, so the live text-object
count is where it was.
Returns { any } — Array of { family, faces, weights, loaded, registered, selectable, matched, weight, shapedWith, reason }.
for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end
typed/builtin//modules/api/engine/font/font/register
font.register(name: string, zfnt: string, opts: table?) -> any
Register a baked font (ZFNT from font.parse) under name, making it
usable on every text surface via fontFamily = "<name>". Loads the
vectorized glyph data into the runtime store (for font.glyph /
font.textMesh) 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 font.glyph /
font.textMesh reads one. A name declared with font.declare 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.
opts groups several weight/style faces under one CSS family and maps
web-font names onto it: opts.family is the shared group key, opts.role
is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a
list of extra selectable names (web fonts + CSS generics like "Arial",
"sans-serif") that resolve to this group, matched case-insensitively.
With a group set, font-weight / font-style on a font-family pick the
real metric-compatible face instead of a synthesized one.
Parameters
namestring— Family name to register under.zfntstring— BakedZFNTpayload fromfont.parse(binary-safe string).optstable(optional) —{ family: string?, role: string?, aliases: {string}? }— group key, weight/style role, and case-insensitive selectable aliases.
Returns any — { family, faces, glyphCount } on success, or nil on failure.
local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))
typed/builtin//modules/api/engine/font/font/textMesh
font.textMesh(name: string, text: string, opts: table?) -> any
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 renderer.mesh.create() (GPU)
or asset.create("mesh") (persistable).
Parameters
namestring— Registered family name.textstring— String to lay out.optstable(optional) —{ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
Returns any — { positions, indices, normals, uvs } as flat float / u32 arrays, or nil if the font isn't registered or the string is all whitespace.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })
typed/builtin//modules/api/engine/frameStream/frameStream/attach
frameStream.attach(texture: string, stream: string, opts: AttachOpts?) -> (string?, string?)
Carry an image the GPU drew out to an open byte stream, frame
after frame. texture is the guid of the render target it was
drawn into — renderer.texture.create({ width = W, height = H })
makes one, and a Camera component draws into it as its
textureHandle; the session reads that target back when a frame
comes due, so what the camera drew last reaches the far end.
stream is a handle from stream.open. What reaches the stream
is one frame's pixels then the next frame's, with nothing between
them: a frame is width * height * bytesPerPixel 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. fps caps how often a
frame is taken and defaults to one per rendered frame; format
accepts "rgb24" (3 bytes per pixel, the default) or "rgba8"
(4) — a call with a format outside those two raises, naming both;
flipY 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.
Parameters
texturestring— Guid of the render target the image was drawn into (a Camera's textureHandle).streamstring— Stream handle from stream.open.optsAttachOpts(optional) — Rate, pixel layout and row order (optional).
Returns (string?, string?) — Session handle, or nil and the refusal reason.
local session = frameStream.attach(rt.guid, handle, { fps = 30 })
typed/builtin//modules/api/engine/frameStream/frameStream/detach
frameStream.detach(handle: string) -> boolean
End the session and free the staging buffers it read frames back through. The stream stays open — whoever opened it closes it.
Parameters
handlestring— Session handle from frameStream.attach.
Returns boolean — True if a session was ended, false if handle already named none.
frameStream.detach(session)
typed/builtin//modules/api/engine/frameStream/frameStream/list
frameStream.list() -> { string }
Every live session handle, in a stable order.
Returns { string } — Array of session handles.
for _, h in frameStream.list() do frameStream.detach(h) end
typed/builtin//modules/api/engine/frameStream/frameStream/status
frameStream.status(handle: string) -> FrameStreamStatus?
Report what the session has carried and lost. frames counts
the frames the stream accepted and bytes the bytes they
carried. dropped counts the frames it refused, of which
droppedBackpressure is the part refused because the consumer was
behind; stalledReadbacks counts the frames that came due while
every staging buffer still held a copy on its way from the GPU.
achievedFps 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 requestedFps to see a
display running slower than it was asked to. lastCopyFrame is
the engine frame, in time.frameCount() 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 lastCopyFrame
agree hold the same image as the newest frame of their streams.
lastOutcome names what became of the most recent frame offered.
nil when handle names no live session.
Parameters
handlestring— Session handle from frameStream.attach.
Returns FrameStreamStatus? — Session status, or nil when handle names no live session.
local s = frameStream.status(session); print(s.frames, s.dropped, s.achievedFps)
typed/builtin//modules/api/engine/home/home/folder
home.folder() -> (string?, string?)
The home folder mounted in this session, /zero/home/<username>,
or nil and the reason no home folder is mounted.
Returns (string?, string?) the folder, or nil and why there is none.
local folder, why = home.folder()
typed/builtin//modules/api/engine/home/home/status
home.status() -> Status
Where the home folder stands in this session. state is
"mounted" (with folder, the path to read and write, owner, the
account it belongs to, world, the guid of the home world that holds it,
files, how many files the home world held when it was mounted, and
from, "launch" or "zeromind", saying where that world was named),
"mounting" while a world load is mounting it, or "unmounted" with
reason saying why no home folder is mounted.
Returns Status — The home folder's status.
local s = home.status(); if s.state == "mounted" then vfs.write(s.folder .. "/notes.md", "hi") end
typed/builtin//modules/api/engine/http/http/get_bytes
http.get_bytes(url: string, headers: Headers?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?) -> PromiseId
Async HTTP GET returning raw bytes (binary-safe string).
Suitable for piping into vfs.write to download a file.
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).timeoutMsTimeoutMs(optional) — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).expectedStatusesExpectedStatuses(optional) — HTTP statuses this caller handles as an answer it expected, e.g.{ 404 }: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).
Returns PromiseId — A future (PromiseHandle): f:await() or task.await(f) delivers the response, f:state() reads whether it has.
local bytes = task.await(http.get_bytes("https://example.com/sound.ogg"))
typed/builtin//modules/api/engine/http/http/get_json
http.get_json(url: string, headers: Headers?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?) -> PromiseId
Async HTTP GET returning JSON. Returns a future (a
PromiseHandle) at once, with the request in flight: keep it,
f:await() (or task.await(f)) when the response is needed, and
f:state() to see whether it has arrived without waiting.
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).timeoutMsTimeoutMs(optional) — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).expectedStatusesExpectedStatuses(optional) — HTTP statuses this caller handles as an answer it expected, e.g.{ 404 }: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).
Returns PromiseId — A future (PromiseHandle): f:await() or task.await(f) delivers the response, f:state() reads whether it has.
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
typed/builtin//modules/api/engine/http/http/post_bytes
http.post_bytes(url: string, headers: Headers?, body: JsonBody?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?) -> PromiseId
Async HTTP POST returning raw bytes — use for APIs that accept JSON input but return binary output (audio, images).
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodyJsonBody(optional) — JSON body (optional).timeoutMsTimeoutMs(optional) — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).expectedStatusesExpectedStatuses(optional) — HTTP statuses this caller handles as an answer it expected, e.g.{ 404 }: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).
Returns PromiseId — A future (PromiseHandle): f:await() or task.await(f) delivers the response, f:state() reads whether it has.
local audio = task.await(http.post_bytes(ttsUrl, nil, { text = "hello" }))
typed/builtin//modules/api/engine/http/http/post_json
http.post_json(url: string, headers: Headers?, body: JsonBody?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?) -> PromiseId
Async HTTP POST returning JSON. Body is a Luau table; the FFI layer JSON-encodes it before the request goes out.
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodyJsonBody(optional) — JSON body (optional).timeoutMsTimeoutMs(optional) — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).expectedStatusesExpectedStatuses(optional) — HTTP statuses this caller handles as an answer it expected, e.g.{ 404 }: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).
Returns PromiseId — A future (PromiseHandle): f:await() or task.await(f) delivers the response, f:state() reads whether it has.
local r = task.await(http.post_json(url, nil, { name = "Alice" }))
typed/builtin//modules/api/engine/http/http/request
http.request(method: string, url: string, headers: Headers?, body: JsonBody?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?) -> PromiseId
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.
Parameters
methodstring— HTTP verb (case-insensitive).urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodyJsonBody(optional) — JSON body (optional).timeoutMsTimeoutMs(optional) — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).expectedStatusesExpectedStatuses(optional) — HTTP statuses this caller handles as an answer it expected, e.g.{ 404 }: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).
Returns PromiseId — A future (PromiseHandle): f:await() or task.await(f) delivers the response, f:state() reads whether it has.
local w = task.await(http.request("PATCH", url, hdrs, { description = "hi" }))
typed/builtin//modules/api/engine/http/http/request_raw
http.request_raw(method: string, url: string, headers: Headers?, body: buffer | string | nil?, timeoutMs: TimeoutMs?, expectedStatuses: ExpectedStatuses?) -> PromiseId
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.
Parameters
methodstring— HTTP verb (case-insensitive).urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodybuffer | string | nil(optional) — Raw binary request body (optional).timeoutMsTimeoutMs(optional) — How long the request is given to be answered, in milliseconds; the engine's own bound when left out, none when 0 (optional).expectedStatusesExpectedStatuses(optional) — HTTP statuses this caller handles as an answer it expected, e.g.{ 404 }: a response with one settles the future as any other non-2xx does and is logged as information rather than an error (optional).
Returns PromiseId — A future (PromiseHandle): f:await() or task.await(f) delivers the response, f:state() reads whether it has.
local r = task.await(http.request_raw("POST", blobsUrl, hdrs, pngBytes))
typed/builtin//modules/api/engine/httpServer/httpServer/address
httpServer.address(path: string) -> (string?, string?)
The URL a path answers on — scheme, host, port and the /app mount,
ready to be fetched or printed for someone to open. Takes the same path
spelling route does, and reads the interface and port from the socket
routes answer on: the address listen opened while one is open, and the
engine's own server otherwise.
Parameters
pathstring— Path under the/appmount, e.g. "/status".
Returns (string?, string?) — The URL, or nil plus the reason there is none — this engine holds no address, or the path is not one a route can be registered at.
print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status
typed/builtin//modules/api/engine/httpServer/httpServer/listen
httpServer.listen(target: string) -> (HttpListener?, string?)
Hold an interface and port of this world's own, and answer content routes on it.
The host in target is the interface bound, and the whole of what decides
who can reach those routes: "127.0.0.1:8080" answers programs on this
machine, "0.0.0.0:8080" 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 0 asks the
operating system for a free one, which the returned record reports, and
http:// may be spelled out in front.
This address serves the routes registered under the /app mount. The
engine's own /engine/* tree answers on the loopback server it booted
with, whose interface stays what the boot bound.
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.
Parameters
targetstring— Interface and port to hold, e.g. "0.0.0.0:8080".
Returns (HttpListener?, string?) — The listener record, or nil plus the reason the target or the bind was refused.
local l = assert(httpServer.listen("0.0.0.0:8080"))
typed/builtin//modules/api/engine/httpServer/httpServer/route
httpServer.route(method: string, path: string, handler: HttpHandler, options: HttpRouteOptions?) -> (number?, string?)
Serve one method and path from this engine, answering each matching
request with handler.
The path is relative to the /app mount, and a trailing /* segment
matches the rest of the path — "/files/*" answers /app/files/a/b, with
"a/b" in request.wildcard. An exact path answers ahead of a wildcard,
and among wildcards the longest one wins.
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; httpServer.routes() finds that handle and
httpServer.unroute 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.
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.
Parameters
methodstring— HTTP verb, e.g. "GET" or "POST".pathstring— Path under the/appmount, e.g. "/status" or "/files/*".handlerHttpHandler— Called with the request table; returns a response table or a body string.optionsHttpRouteOptions(optional) —{ timeoutMs? }— how long a request waits for this handler.
Returns (number?, string?) — The route handle, or nil plus the reason it was not registered.
local h = httpServer.route("GET", "/status", function(req)
typed/builtin//modules/api/engine/httpServer/httpServer/routes
httpServer.routes() -> { HttpRoute }
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.
Returns { HttpRoute } — An array of route records.
for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end
typed/builtin//modules/api/engine/httpServer/httpServer/status
httpServer.status() -> HttpServerStatus
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. host, port, url and reach are read from the socket routes
answer on — the one listen opened while one is open, and the engine's
own server otherwise — and listeners carries every address, each with
its own reach. When supported is false, reason says why: a browser tab
answers HTTP requests and holds no address of its own.
Returns HttpServerStatus — { supported, reason?, host?, port?, url?, reach?, prefix, routeCount, pending, listeners }.
local s = httpServer.status(); print(s.url, s.reach)
typed/builtin//modules/api/engine/httpServer/httpServer/unlisten
httpServer.unlisten() -> boolean
Release the address listen opened. Returns once the socket is free,
so the same port binds again straight after.
Returns boolean — True when an address was held.
httpServer.unlisten()
typed/builtin//modules/api/engine/httpServer/httpServer/unroute
httpServer.unroute(handle: number) -> boolean
Stop serving a route and release its handler. The address is free for another registration once this returns true.
Parameters
handlenumber— The handlehttpServer.routereturned.
Returns boolean — True when a route with this handle was registered.
httpServer.unroute(h)
typed/builtin//modules/api/engine/i18n/i18n/choice
i18n.choice() -> string?
The locale the player chose for themselves, or nil when they chose none and the chain decides.
Returns string? — The player's choice.
typed/builtin//modules/api/engine/i18n/i18n/choose
i18n.choose(tag: string?) -> ChooseResult
Choose tag 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. nil clears the choice and the
chain decides again. A tag no offered locale matches is refused, naming
the offered tags.
Parameters
tagstring(optional) — A tag fromi18n.offered(), or nil to clear the choice.
Returns ChooseResult — { ok, locale?, decidedBy?, reason?, offered? }.
local r = i18n.choose("pt-BR")
if not r.ok then log.warn(r.reason) end
typed/builtin//modules/api/engine/i18n/i18n/emulate
i18n.emulate(tags: { string }?) -> { ok: boolean, locale: string?, platform: { string }?, reason: string? }
Stand tags in for the platform's preferred languages, the way a
visitor whose device or browser reads them would arrive, and negotiate
again: i18n.preferred(), i18n.pick, i18n.locale() and
i18n.onChange all answer as they would for that visitor. nil 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 (i18n.choose) still comes first in the chain, so
clear it with i18n.choose(nil) to see the list decide.
Parameters
tags{ string }(optional) — BCP 47 tags, most preferred first, or nil for the platform's own list.
Returns { ok: boolean, locale: string?, platform: { string }?, reason: string? } — { ok, locale?, platform?, reason? }: the locale and list now in force.
i18n.emulate({ "pt-BR", "en" }) -- try the world as a Brazilian visitor
i18n.emulate(nil) -- back to this machine's own languages
typed/builtin//modules/api/engine/i18n/i18n/fallback
i18n.fallback() -> { string }
The chosen locale's fallback chain: the locales a string missing from its catalog is looked up in, in order.
Returns { string } — The fallback locales, en last; empty for en itself.
typed/builtin//modules/api/engine/i18n/i18n/locale
i18n.locale() -> string
The locale the engine chose for the player for its own text, a BCP
47 tag such as en or pt-BR. It is always one of i18n.offered();
en, the engine's source language, until a negotiation states another.
To choose among your own content's languages, use i18n.pick.
Returns string — The chosen locale.
if i18n.locale() == "pt-BR" then label.text = "Olá" end
typed/builtin//modules/api/engine/i18n/i18n/offChange
i18n.offChange(id: number) -> boolean
Remove an i18n.onChange 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).
Parameters
idnumber— Watcher id returned byi18n.onChange.
Returns boolean
typed/builtin//modules/api/engine/i18n/i18n/offered
i18n.offered() -> { Locale }
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 i18n.pick.
Returns { Locale } — One { tag, name, nativeName, fallback } per offered locale.
typed/builtin//modules/api/engine/i18n/i18n/onChange
i18n.onChange(callback: (string, { string }) -> ()) -> number
Run callback 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 i18n.emulate stood a list
in), the player chooses or clears a choice, the ZeroMind language moves,
or the offered locales change. Called as (locale, preferred), after the
engine renegotiated, so i18n.pick inside it answers for the new state.
A change i18n.emulate or i18n.choose 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 i18n.offChange.
Parameters
callback(string, { string }) -> ()— Function invoked as(locale: string, preferred: {string}).
Returns number — The watcher id.
local id = i18n.onChange(function(locale, preferred)
log.info(("locale %s, platform %s"):format(locale, table.concat(preferred, ",")))
end)
typed/builtin//modules/api/engine/i18n/i18n/pick
i18n.pick(available: { string }) -> string
Which of available, 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 available when none of them matches, so list
your fallback first. Call it again (or from i18n.onChange) to follow a
change while they play.
Parameters
available{ string }— BCP 47 tags your content has text for, fallback first.
Returns string — One of available, exactly as written there.
local lang = i18n.pick({ "en", "pt", "es" })
label.text = ({ en = "Welcome!", pt = "Bem-vindo!", es = "¡Bienvenido!" })[lang]
typed/builtin//modules/api/engine/i18n/i18n/preferred
i18n.preferred() -> { string }
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,
navigator.languages in a browser, the device (or per-app) language list
on Android. Empty when the platform states none.
Returns { string } — The platform's preferred languages.
local first = i18n.preferred()[1]
typed/builtin//modules/api/engine/i18n/i18n/sources
i18n.sources() -> Sources
Every link of the chain the locale was chosen from: the player's
choice, the zeromind language, the platform list, and which link
decided (decidedBy: choice, zeromind, platform or default)
with the tag it matched.
Returns Sources — { locale, decidedBy, matched, choice?, zeromind?, platform, signedIn }.
typed/builtin//modules/api/engine/jobs/JobHandle/cancel
JobHandle.cancel(self) -> boolean
Drop one refcount. The last drop unregisters the job and the dispatcher stops invoking it.
Parameters
self
Returns boolean
typed/builtin//modules/api/engine/jobs/JobHandle/destroy
JobHandle.destroy(self) -> boolean
Drop one refcount. The last drop unregisters the job and the dispatcher stops invoking it.
Parameters
self
Returns boolean
typed/builtin//modules/api/engine/jobs/JobHandle/id
JobHandle.id -> number
This job's substrate id. Zero once the handle has been cancelled.
Returns number
typed/builtin//modules/api/engine/jobs/JobHandle/info
JobHandle.info(self) -> JobInfo?
This job's latest snapshot row, or nil once the substrate no longer tracks it.
Parameters
self
Returns JobInfo?
typed/builtin//modules/api/engine/jobs/JobHandle/pause
JobHandle.pause(self) -> boolean
Skip this job on subsequent ticks, leaving it registered. False once the substrate no longer tracks it.
Parameters
self
Returns boolean
typed/builtin//modules/api/engine/jobs/JobHandle/resume
JobHandle.resume(self) -> boolean
Run this job again, clearing an errored status. False once the substrate no longer tracks it.
Parameters
self
Returns boolean
typed/builtin//modules/api/engine/jobs/JobHandle/status
JobHandle.status(self) -> JobStatus?
This job's scheduler status, or nil once the substrate no longer tracks it.
Parameters
self
Returns JobStatus?
typed/builtin//modules/api/engine/jobs/JobHandle/valid
JobHandle.valid(self) -> boolean
Whether the substrate still tracks this job.
Parameters
self
Returns boolean
typed/builtin//modules/api/engine/jobs/M/context
M.context() -> JobContext?
What the running item runs as, on a worker: worker, the pool slot of its worker; key, the item's key (nil for an item dispatched without one, and outside an item, as in a watcher); fresh, true for the first item a key runs in this VM since the key bound, so a module tells a cold cache from a missing entry. On the engine thread it answers nil, which is also how code asks whether it runs on a worker.
Returns JobContext? — { worker, key, fresh } on a worker; nil on the engine thread.
local ctx = jobs.context(); if ctx and ctx.fresh then cache = {} end
typed/builtin//modules/api/engine/jobs/M/dispatch
M.dispatch(module: string, entry: string, args: any?, opts: DispatchOptions?) -> PromiseHandle
Run the function entry of the module module on a worker thread and answer a promise of its result, so a long computation holds no frame. module is a require path, read from this line as require reads it. The module and the modules it requires run in a worker VM that boots the prelude and requires modules as the engine thread's VM does, so it has the same globals and modules/api modules: the entry computes from its arguments and returns a value, which the code awaiting the promise applies to the world. A call to a function only the engine thread serves (one that reads or changes the ECS world, the component instances its VM holds, or a table the engine thread has not published yet) is forwarded: from a job it runs on the engine thread, with the same arguments, after every write the job recorded before it, as the principal the job runs as, and its answer or its error returns to the job. The task that made it waits as it waits on a promise, so the worker keeps serving its other tasks and the time parked never counts toward the deadline; each forwarded call takes at least one engine frame, and job:info().forwarded_calls and the system.schedule.frame.forwarded_calls profiler count report them. Arguments and answers cross as copies; a call whose argument holds a function, coroutine or userdata handle, whose answer holds an object of the engine thread's VM, or that is made where the script cannot wait (a metamethod, the top level of a module being required, a batch or queue body) raises an error naming the function and the reason. A watcher the module registers (engine.onModeChange, modules.watch, world.on, vfs.watch and the other engine events) is kept by the worker VM and fires when that VM next starts an item, for the events raised since it registered; save.onMarksLanded is refused there, because the engine reads the value its callback returns inside its own frame. A function the module registers with renderfeature.setTick, jobs.register or httpserver.route stays in the worker VM that registered it, and that VM stays in service for it after the item returns, until the function is unregistered or the world changes or the scripting state resets: the engine posts each call to the worker and never waits for it, and reads the latest completed output (a tick's passes are drawn one frame after the tick that made them, and a frame whose call is still running keeps the last completed output and counts a late call). The entry is a task of the worker VM's own scheduler: it waits on time (task.wait), on frames (task.waitFrames) and on promises (task.await), starts tasks and scopes, and returns its result. The promise settles once the engine thread has applied every write the entry and the tasks it started made before it returned, so the code awaiting it reads all of those writes, as it would after making the same calls on the engine thread; an entry whose last write the engine thread applies in the frame the entry returned settles as soon as one that writes nothing, and otherwise one frame later. It waits as long as the writes do (while no world is open, say) and then resolves with the entry's result. It rejects only when the engine resets the scripting state or changes its world first, and then none of those writes is applied. The tasks it started keep running until they end or the item's deadline ends them; a write one of them makes after the entry returned is not part of what the promise waits for. A promise the worker makes settles on that worker (delay, http.* and jobs.dispatch serve one), and a call whose promise an engine-thread system settles (queue, shell.run of a command it would queue) is refused, naming it. An item runs on whichever worker takes it. With opts.key (a string of 1 to 256 bytes) it runs in the worker VM the key is bound to, after every item of the key dispatched before it has settled and its writes have applied, one at a time and in order, so module state an item of the key leaves (a cache, a plan, a parsed asset) is there for the next; jobs.context().fresh tells the first item of a key in a VM since the key bound, jobs.release ends the binding, and jobs.warm runs a module's one-time setup in every worker VM before items need it. A key's item waits for its VM even while other workers are idle, and /runtime/workers reports the wait per key. An item awaiting an item of its own key is refused at the await, naming the key: the awaiting item holds the key's turn, and the awaited item starts only after it settles; dispatching to its own key without awaiting runs that item after it. Items of a key queued before the world changes or the scripting state resets are refused unrun, naming the reset. opts.supersede = true with a key rejects every item of the key that has not started, naming this one; the running item is untouched. The pool holds at most 65,536 keys at once, and a dispatch with a key past that is refused at the call, naming the bound. args and the result cross as copies: nil, booleans, numbers, strings, buffers, vectors and tables of those, and any other value is refused at this call naming its key path. The promise rejects with the entry's error and stack, or names the deadline when one run of the entry, from a resume to the next yield, takes longer than opts.deadline seconds (default and at most the longest hold on one run, 60 seconds on every platform, native and browser alike; any number above 0 up to it, which a caller may lower and may not raise: a larger one is refused at the call naming the cap). The clock starts at each resume and stops at each yield, so time parked on a promise, task.wait or a frame never counts, every yield resets it, and no sum across runs is kept: an entry that keeps yielding runs until it returns or is cancelled. The tasks it resumed are held to the same deadline. Cancelling the promise (p:cancel()) halts a running entry and keeps a queued one from running. An edit of the module reaches the next dispatch.
Parameters
modulestring— Require path of the module, asrequiretakes it.entrystring— Name of the function in the module's export table.argsany(optional) — The value the entry receives, copied.optsDispatchOptions(optional) — Optional{ deadline: number?, name: string?, key: string?, supersede: boolean? }: the deadline in seconds, what a rejection calls the item, the key it runs as, and whether it supersedes the key's items that have not started.
Returns PromiseHandle — A PromiseHandle settling with a copy of the entry's result.
local heights = jobs.dispatch("@root::terrain.noise", "heights", { size = 256, seed = 7 }):await()
local p = jobs.dispatch("@root::sim.solver", "solve", state, { deadline = 2, name = "solver" }); p:cancel()
local route = jobs.dispatch("@root::paths.graph", "route", { map = "level3", from = a, to = b }, { key = "level3" }):await()
local preview = jobs.dispatch("@root::graph.cook", "cook", edit, { key = graphId, supersede = true })
typed/builtin//modules/api/engine/jobs/M/find
M.find(name: string) -> JobHandle?
Look up a registered job by name. Returns a JobHandle or nil for anonymous / unknown names. Single FFI crossing — returns the id directly, no registry snapshot.
Parameters
namestring— Job name supplied tojobs.register.
Returns JobHandle? — JobHandle or nil.
local job = jobs.find("animation_blend_main")
typed/builtin//modules/api/engine/jobs/M/inspect
M.inspect(target: JobHandle | string) -> JobInfo?
Return a snapshot row by job handle or by name without retrieving a full handle. Single FFI crossing — pulls only the matching row.
Parameters
targetJobHandle | string— JobHandle, or job name string.
Returns JobInfo? — Snapshot row or nil.
local info = jobs.inspect("animation_blend_main")
typed/builtin//modules/api/engine/jobs/M/list
M.list(phase: string?) -> { JobInfo }
List registered job summaries. Pass a phase name to filter to a single phase. Single FFI crossing — only the requested rows cross the bridge.
Parameters
phasestring(optional) — Optional phase filter — nil returns every job.
Returns { JobInfo } — Array of JobInfo rows.
local rows = jobs.list("main")
typed/builtin//modules/api/engine/jobs/M/onRelease
M.onRelease(watcher: (key: string) -> ()) -> () -> ()
Hold watcher in the VM of the module that calls it: each time a key bound to that VM is released (jobs.release), watcher(key) runs there as a task of the VM's scheduler, once per release, so the module drops what it keeps for the key. A worker VM keeps its own watchers, as it keeps engine.on* watchers; a module registers at its top level, which every worker VM that requires it runs. The engine thread's VM holds no key, so a watcher there is held and never called. Answers the function that removes the watcher.
Parameters
watcher(key: string) -> ()— Called with the released key.
Returns () -> () — A function that removes the watcher.
jobs.onRelease(function(key) plans[key] = nil end)
typed/builtin//modules/api/engine/jobs/M/parallelFor
M.parallelFor(module: string, entry: string, over: number | { first: number, last: number } | buffer, args: any?, opts: { deadline: number?, name: string?, chunks: number?, stride: number? }?) -> PromiseHandle
Split a loop across the worker pool and answer a promise of the chunks' results in chunk order, so a loop over many indices or many elements of a buffer holds no frame and uses every worker. over is a count of indices (1..over), a table { first = a, last = b } of whole numbers, or a buffer whose elements are opts.stride bytes each (default 1). The range is cut into opts.chunks runs (default four per worker, at most one per element), as even as it divides, and each run is an item the pool's workers take: entry, a function of the module module, is called with one table chunk = { index, count, first, last, args, data }, where first and last are the run's indices (for a buffer, its element numbers from 1), args is a copy of args, and data is the run's slice of a buffer. The promise settles once every chunk has returned and the engine thread has applied every write the chunks made before they returned, with { chunk 1 result, chunk 2 result, ... } in chunk order whichever chunk finished first. A chunk that raises, or runs past its deadline (opts.deadline seconds for one run of each chunk between a resume and the next yield, default and at most the hold on one run, 60 seconds on every platform), rejects the promise naming the chunk (chunk 3 of 8) and cancels the chunks still queued or running; cancelling the promise (p:cancel()) cancels every chunk. It runs from the engine thread and from a worker: a worker that awaits the call runs queued chunks itself, so a call made inside a chunk completes however many workers are busy. The module, its worker VMs, args and result copies follow the rules of jobs.dispatch.
Parameters
modulestring— Require path of the module, asrequiretakes it.entrystring— Name of the function in the module's export table; it receives the chunk table.overnumber | { first: number, last: number } | buffer— A count of indices, a{ first, last }range, or a buffer.argsany(optional) — A value every chunk receives aschunk.args, copied.opts{ deadline: number?, name: string?, chunks: number?, stride: number? }(optional) — Optional{ deadline: number?, name: string?, chunks: number?, stride: number? }: the deadline in seconds of one run of each chunk, what a rejection calls the call, the number of chunks, and the bytes in one element of a buffer.
Returns PromiseHandle — A PromiseHandle settling with a table of the chunks' results in chunk order.
local sums = jobs.parallelFor("@root::sim.reduce", "sumChunk", 100000, { scale = 2 }):await()
local out = jobs.parallelFor("@root::pixels.tint", "tint", pixels, nil, { stride = 4, chunks = 16 }):await()
typed/builtin//modules/api/engine/jobs/M/register
M.register(descriptor: table) -> JobHandle?
Register a substrate job. Returns a JobHandle on success, nil on validation failure. Dispatches by executor.kind:\n - "kernel" / "stub" → standard __jobs.register (JSON-only descriptor).\n - "luau" → __jobs.register_luau(descriptor, executor.run) so the Luau function survives the JSON crossing as a stable registry ref. The dispatcher invokes the run closure once per frame; the closure captures any bindings/buffers it needs.\n - "compute" → the shader reference resolves to its registration key, and the job queues one dispatch per frame.\n - "worker" → __jobs.register_worker(descriptor, executor.input, executor.apply): each frame input() runs on the engine thread, its value is copied to a worker where the module's entry runs, and the result reaches apply(result) on the engine thread the frame it lands, one item in flight at a time. An input that returns nil queues no item that frame. A job that errors logs its name and message once, and job:info().last_error holds it. Without a key, its items can run on different workers, each with module state of its own, so what carries from one item to the next travels through the result and input. key (a string of 1 to 256 bytes) runs every item of the job as that key, in the worker VM the key is bound to, so module state one item leaves there is there for the next (see jobs.dispatch); job:info().waited_for_vm_ms is the time its items waited for that VM. A job registered from a worker VM keeps its function (run, or input and apply) in that VM: the dispatcher posts each frame's call to the worker and never waits for it, the writes it records apply one frame later, a raise marks the job errored one frame later, and job:info().worker reports the calls, the late calls and the failed calls. A worker job whose input or apply is not a function, whose deadline is not a number of seconds above 0 and at most the hold on one run (60 seconds on every platform), or whose key is not a string of 1 to 256 bytes, is raised at the call, naming why. deadline (default and at most that hold: it may be lowered and never raised) is the longest one run of an item may take between a resume and the next yield, counted on monotonic time per run and never summed across runs.\n\norigin is auto-filled with the VFS path of the calling script unless the descriptor already supplies one — surfaced under /zero/runtime/jobs/<phase>/<key>/origin.txt for agent traceability. Single FFI crossing — auto-origin runs Rust-side via lua_getinfo, no separate stack-inspection trip.
Parameters
descriptortable— Job declaration with theJobDescriptorshape —name?,phase,reads?,writes?,executor,ordering?,pure?,origin?,metadata?. Param is typed astablerather thanJobDescriptorbecause the LSP doesn't yet narrow string literals to their literal types in record fields, so aJobDescriptorannotation rejects the tagged-unionexecutordiscriminator on every call site (literalkind = "kernel"infers askind: string, doesn't subtypeKernelExecutor.kind: "kernel"). Runtime validation in__jobs.registerenforces the actual structure; see theJobDescriptortype alias above for the canonical shape.
Returns JobHandle? — JobHandle or nil.
local job = jobs.register({ phase = "main", executor = { kind = "kernel", kernel = "copy_buffer" }, reads = {{resource={kind="buffer",id=src.id},mode="r"}}, writes = {{resource={kind="buffer",id=dst.id},mode="w"}} })
local job = jobs.register({ phase = "main", executor = { kind = "luau", run = function() print("tick") end } })
local job = jobs.register({ phase = "main", executor = { kind = "compute", shader = asset.resolve("carve", "computeShader"), buffers = { "heights" }, workgroups = { 64 } } })
local job = jobs.register({ name = "crowd", phase = "main", executor = { kind = "worker", module = "@root::crowd.sim", entry = "step", input = function() return { count = 100 } end, apply = function(result) print(result) end } })
typed/builtin//modules/api/engine/jobs/M/release
M.release(key: string) -> boolean
End the binding of key to its worker VM once the items of the key dispatched before this call have settled. That VM then runs its jobs.onRelease watchers with the key, each as a task of its scheduler, and the key's next item binds anew and runs with jobs.context().fresh true. Without a release, a VM keeps each key's state for as long as it lives (until the world changes or the scripting state resets); /runtime/workers lists the keys each worker holds. Runs from the engine thread and from a worker.
Parameters
keystring— The key, asjobs.dispatchwas given it inopts.key.
Returns boolean — True when the pool held the key: a binding, or items queued or running, which the release runs after; false, doing nothing, for a key with none of those, a key whose only entry is a release under way among them.
jobs.release(graphId)
typed/builtin//modules/api/engine/jobs/M/shared/atomic
M.shared.atomic(t: SharedTable<T>, key: string | number, fn: (current: T?) -> T?) -> T?
Read-modify-write of one key of a shared table without a race. fn receives the key's current value (nil when absent; a table arrives as a plain copy) and returns the new one (nil removes the key); the new value is stored only if no other write reached the table meanwhile, otherwise fn runs again on the newer value, so fn must be quick, pure and must not yield. A call that lost four attempts in a row takes its turn at the table, and the other atomic calls on a worker wait for it, so atomic calls never starve one another; after 256 attempts that all lost to plain writes the call raises naming the table and the key. Counters, flags and compare-and-set are this one call; a change across several keys is one atomic on the parent key that returns the whole new table.
Parameters
tSharedTable<T>— A shared table.keystring | number— The key to update.fn(current: T?) -> T(optional) —(current) -> new.
Returns T? — The value now stored under the key.
jobs.shared.atomic(counts, "done", function(n) return (n or 0) + 1 end)
jobs.shared.atomic(flags, "claimed", function(was) if was then return was end return true end)
typed/builtin//modules/api/engine/jobs/M/shared/queue
M.shared.queue(name: string, opts: SharedQueueOptions?) -> SharedQueue<T>
The shared queue named name, made on first use: a bounded first-in-first-out queue between any VMs, for handing work or results from one job to another, or from a job to the game. push(v) puts a copy of v at the back from anywhere; pop() takes the front value and waits for one when there is none (a worker parks, and on the engine thread only the calling task suspends, so a frame is never held); tryPop() takes it without waiting and answers nil when empty. A value is any non-nil data that a shared table holds. A full queue raises naming the queue and its capacity: it never drops. The capacity is opts.capacity (default 1024, at most 65,536); asking for the name again with another capacity raises. The queue is process memory with the lifetime of a shared table: when the world changes or the scripting state resets, every task parked on pop is rejected with the reason.
Parameters
namestring— The queue's name, at most 256 bytes.optsSharedQueueOptions(optional) — Optional{ capacity: number? }.
Returns SharedQueue<T> — The queue.
local work = jobs.shared.queue("terrain.chunks", { capacity = 256 }); work:push({ x = 3, z = 4 })
-- in a job: while true do local chunk = work:pop(); build(chunk) end
typed/builtin//modules/api/engine/jobs/M/shared/revision
M.shared.revision(t: SharedTable<T>) -> number
A number that every write to the shared table, or to any table nested in it, advances. One atomic read: game code that polls a table each frame compares it with the number it saw and reads the table only when it changed.
Parameters
tSharedTable<T>— A shared table.
Returns number — The revision.
local seen = 0; -- each frame: local now = jobs.shared.revision(progress); if now ~= seen then seen = now; redraw(progress) end
typed/builtin//modules/api/engine/jobs/M/shared/snapshot
M.shared.snapshot(t: SharedTable<T>) -> { [string | number]: T }
An ordinary table holding a deep copy of a shared table, for the code that wants a stable view (JSON, comparison, the table library on a frozen picture). Each table of the copy is read at one version; tables read at different moments may be of different versions.
Parameters
tSharedTable<T>— A shared table.
Returns { [string | number]: T } — A plain table.
local picture = jobs.shared.snapshot(progress)
typed/builtin//modules/api/engine/jobs/M/shared/table
M.shared.table(name: string) -> SharedTable<T>
The shared table named name, made on first use. The engine's VM and every worker VM read and write the same table with the syntax of an ordinary table: t.x = 1, t.player.hp -= 5, #t, for k, v in t, pairs, next, table.insert, table.remove, table.sort, table.concat, table.find, table.unpack, table.clone, table.clear, rawget, rawset. A job streams results to the game through it while it runs, and the game reads them each frame. It holds data: booleans, numbers, integers, strings, vectors, buffers and tables of those; assigning a table copies it in (later edits of the local table are not seen), and a function, coroutine, userdata or a table with a metatable raises naming the key path. A value reads back as the type it was written with. t.a where t.a is a table is that table, so t.a.b = 1 writes through; a table replaced or removed raises on any use, naming where it was. Each access is atomic on its table; a loop walks the table as it stood when the loop started. The store is process memory: not persisted and not replicated (runtime_data and synced component fields are those). It lasts until the world changes or the scripting state resets (play entry keeps it); after that a held table raises, naming the reason. A table holds at most 1,048,576 entries and 64 MiB (32 MiB in the browser), nested at most 64 deep; the process holds at most 4,096 names and 256 MiB (128 MiB in the browser); a write past a bound raises and changes nothing.
Parameters
namestring— The table's name, at most 256 bytes.
Returns SharedTable<T> — The shared table.
local progress = jobs.shared.table("terrain.progress"); progress.done = 0
-- in a job: progress.done += 1 -- in the game each frame: print(progress.done)
typed/builtin//modules/api/engine/jobs/M/warm
M.warm(module: string, entry: string) -> PromiseHandle
Run the function entry of the module module once in every worker VM, to build the state the module keeps in its own VM (a registry, compiled rules, a table it loads) before an item needs it. The pool starts every worker it may start; each VM up runs the entry as its next item, and a VM made later (after a panic, or one the pool starts later) runs it before its first item, whichever boot made it. The entry is called with nil and builds state in its own VM: it runs under the hold on one run (60 seconds from a resume to the next yield), and its whole run, waits included, under the warm bound of 60 seconds of wall time from its start to its end, read between runs, so a run in progress at the bound goes on to its next yield: an entry that does not yield runs at most the bound plus one hold, about 120 seconds. It may wait on time and frames, and it does not wait on the pool: an await of a jobs.dispatch, a jobs.parallelFor or a jobs.warm promise by the entry or a task it started, while the entry runs, raises, naming what it awaited, whether or not the promise has settled, and so does making a future combinator over one, at the call (dispatching without awaiting is fine: the item runs once the VM is warm). No item starts on the VM until its warm pass is over; tasks the VM already holds go on between the entry's waits. The promise settles once every VM up at the call has run it: it resolves when every run ended well, and rejects naming each worker whose run raised, panicked or ran past the warm bound, and its error. It is kept for the life of the scripting state: a second call for the same module and entry runs nothing more and answers the same. An entry that raises is logged, naming its module, entry and worker, and the VM serves on. An entry that panics the engine or runs past the warm bound runs on no later VM until the scripting state resets: it is logged once and ended, and the VM made after a panic runs the rest of the pass before any item. A panic in code outside the entry while it runs (the VM's boot, a watcher, a task another entry left behind) ends that VM, and the VM made next runs it once more; these endings are counted per entry, across every worker of the pool, for the epoch, and the second one fails the entry for the epoch, naming the panic. The entry runs on workers only; the calling thread pays the dispatch. Call it from the engine thread or from an item, awaited or not, whatever principal the item runs as: the item's worker runs the warm job for its own VM before the item resumes. Call it when the module is first used in a world, so a world that never uses it pays nothing; /runtime/workers lists each worker's warm runs with their milliseconds, and the failed ones with why.
Parameters
modulestring— Require path of the module, asrequiretakes it.entrystring— Name of the function in the module's export table.
Returns PromiseHandle — A PromiseHandle settling once every worker VM up at the call has run the entry: nil when every run ended well, otherwise rejected naming each failed run.
jobs.warm("@root::graph.nodes", "buildRegistry")
jobs.warm("@root::paths.graph", "loadTables"):await()
typed/builtin//modules/api/engine/jobs/jobhandle_cancel
jobhandle_cancel(self: JobHandle) -> boolean
Cancel the job — drops one refcount, last drop unregisters and the dispatcher stops invoking it. Symmetric with vfs.remove("/runtime/jobs/<phase>/<id>").
Parameters
selfJobHandle— JobHandle returned byjobs.register.
Returns boolean — True if the substrate still tracked the job at call time.
job:cancel()
typed/builtin//modules/api/engine/jobs/jobhandle_info
jobhandle_info(self: JobHandle) -> JobInfo?
Latest snapshot row for this job (id, name, phase, status, origin, metadata, reads, writes…). Single FFI crossing — pulls only this job's row, not the full registry.
Parameters
selfJobHandle— JobHandle returned byjobs.register.
Returns JobInfo? — Snapshot row or nil if the job is no longer tracked.
local info = job:info(); print(info.origin)
typed/builtin//modules/api/engine/jobs/jobhandle_pause
jobhandle_pause(self: JobHandle) -> boolean
Pause the job — skipped on subsequent ticks but stays registered.
Parameters
selfJobHandle— JobHandle returned byjobs.register.
Returns boolean — True if the substrate still tracked the job.
local job = jobs.register({...}); job:pause()
typed/builtin//modules/api/engine/jobs/jobhandle_resume
jobhandle_resume(self: JobHandle) -> boolean
Resume a paused or errored job (clears Errored → Pending).
Parameters
selfJobHandle— JobHandle returned byjobs.register.
Returns boolean — True if the substrate still tracked the job.
job:resume()
typed/builtin//modules/api/engine/jobs/jobhandle_status
jobhandle_status(self: JobHandle) -> JobStatus?
Read the job's current scheduler status.
Parameters
selfJobHandle— JobHandle returned byjobs.register.
Returns JobStatus? — "pending" / "running" / "errored", or nil if the id is no longer tracked.
local s = job:status() -- "pending"
typed/builtin//modules/api/engine/jobs/jobhandle_valid
jobhandle_valid(self: JobHandle) -> boolean
Whether the substrate still tracks this job (false after :cancel/:destroy).
Parameters
selfJobHandle— JobHandle returned byjobs.register.
Returns boolean — True if the underlying job is still registered.
if not job:valid() then return end
typed/builtin//modules/api/engine/layers/M/cost
M.cost() -> { SceneLayerCost }
What each loaded scene's per-frame tick costs, attributed to the layer
that owns it — the update / editorUpdate its entrypoint declares,
timed where it runs. totalMs is a SUM across the window
layers.observe().window reports, so divide by calls (or read avgMs)
for the per-tick figure; a tick that runs every frame makes that the
per-frame figure. Call layers.resetCostWindow() first to time a
particular stretch. A layer whose entrypoint declares no tick is absent.
Returns { SceneLayerCost } — An array of SceneLayerCost.
layers.resetCostWindow(); task.wait(1); for _, c in layers.cost() do print(c.name, c.avgMs) end
typed/builtin//modules/api/engine/layers/M/find
M.find(ref: AssetRef<scene> | string) -> any?
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.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string resolved throughasset.ref.
Returns any? — The scene proxy, or nil when that scene has no loaded layer.
local layer = layers.find("scenes.arena")
typed/builtin//modules/api/engine/layers/M/fireBeforeLoad
M.fireBeforeLoad(proxy: any?) -> nil
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
layers.onBeforeLoad subscriber. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The scene proxy about to load.
Returns nil
layers.fireBeforeLoad(sceneProxy)
typed/builtin//modules/api/engine/layers/M/fireLoad
M.fireLoad(proxy: any?) -> nil
Announce that a scene layer has loaded, fanning out to every
layers.onLoad 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.
Parameters
proxyany(optional) — The loaded scene proxy.
Returns nil
layers.fireLoad(sceneProxy)
typed/builtin//modules/api/engine/layers/M/fireUnload
M.fireUnload(proxy: any?) -> nil
Announce that a scene layer is unloading: fans out to every
layers.onUnload 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.
Parameters
proxyany(optional) — The scene proxy being unloaded.
Returns nil
layers.fireUnload(sceneProxy)
typed/builtin//modules/api/engine/layers/M/install
M.install() -> nil
Install the layers global. layers.active 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.
Returns nil
layers.install()
typed/builtin//modules/api/engine/layers/M/inventory
M.inventory() -> { SceneLayerInventory }
What each loaded layer holds: the entities the engine attributes to it,
whether it came up whole, and how many failures it carries. unattributed
in layers.observe().totals counts what exists in the world that no layer
claims.
Returns { SceneLayerInventory } — An array of SceneLayerInventory.
for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end
typed/builtin//modules/api/engine/layers/M/is_loaded
M.is_loaded(ref: AssetRef<scene> | string) -> boolean
Whether a scene currently has a loaded layer — the boolean form of
layers.find. 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
if layers.is_loaded(ref) then layers.unload(ref) end sees the layer on
the frame its entities exist.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string.
Returns boolean — True when the scene is loaded as a layer.
if not layers.is_loaded("scenes.hud") then layers.load("scenes.hud", { additive = true }) end
typed/builtin//modules/api/engine/layers/M/lastLoad
M.lastLoad() -> SceneLoadReport?
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.
Returns SceneLoadReport? — A SceneLoadReport, or nil.
local r = layers.lastLoad(); print(r.name, r.outcome, r.entities.added)
typed/builtin//modules/api/engine/layers/M/lastUnload
M.lastUnload() -> SceneUnloadReport?
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.
Returns SceneUnloadReport? — A SceneUnloadReport, or nil.
local u = layers.lastUnload(); print(u.name, u.entities.removed)
typed/builtin//modules/api/engine/layers/M/list
M.list() -> { any }
Every loaded scene layer as a proxy, root and additive alike, in the
order the engine reports them. A proxy carries name, sceneIdentity
(the scene asset it was loaded from, e.g. scenes.main), guid, path,
additive, persistent and visible. The root one — the open scene — is
also layers.active.
Returns { any } — Array of scene proxies — empty before any scene is loaded.
for _, layer in ipairs(layers.list()) do print(layer.name, layer.sceneIdentity, layer.additive) end
print("open scene:", layers.active.sceneIdentity)
typed/builtin//modules/api/engine/layers/M/load
M.load(ref: AssetRef<scene> | string, opts: LoadOpts?) -> any
Parameters
refAssetRef<scene> | stringoptsLoadOpts(optional)
Returns any
typed/builtin//modules/api/engine/layers/M/loadHistory
M.loadHistory() -> { SceneLoadReport }
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.
Returns { SceneLoadReport } — An array of SceneLoadReport.
for _, r in layers.loadHistory() do print(r.name, r.durationMs) end
typed/builtin//modules/api/engine/layers/M/loadInFlight
M.loadInFlight() -> number
Returns the number of scene loads currently in flight (queued
but not yet visible via onLoad dispatch). Returns 0 when the
engine is in a stable load state. Used by engine.mode = ... 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.
Returns number
typed/builtin//modules/api/engine/layers/M/observe
M.observe() -> SceneObservation
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.
Returns SceneObservation — A SceneObservation.
local o = layers.observe(); print(o.lastLoad.outcome, o.lastLoad.durationMs)
for _, c in layers.observe().cost do print(c.name, c.avgMs) end
typed/builtin//modules/api/engine/layers/M/offBeforeLoad
M.offBeforeLoad(h: number) -> boolean
Cancel a layers.onBeforeLoad subscription.
Parameters
hnumber— The handlelayers.onBeforeLoadreturned.
Returns boolean — True when a subscription was removed.
layers.offBeforeLoad(h)
typed/builtin//modules/api/engine/layers/M/offEntityChanged
M.offEntityChanged(h: number) -> boolean
Remove a subscription made with layers.onEntityChanged.
Parameters
hnumber— The handle returned bylayers.onEntityChanged.
Returns boolean — True when the subscription existed and was removed.
layers.offEntityChanged(handle)
typed/builtin//modules/api/engine/layers/M/offLoad
M.offLoad(h: number) -> boolean
Cancel a layers.onLoad subscription.
Parameters
hnumber— The handlelayers.onLoadreturned.
Returns boolean — True when a subscription was removed.
layers.offLoad(h)
typed/builtin//modules/api/engine/layers/M/offUnload
M.offUnload(h: number) -> boolean
Cancel a layers.onUnload subscription.
Parameters
hnumber— The handlelayers.onUnloadreturned.
Returns boolean — True when a subscription was removed.
layers.offUnload(h)
typed/builtin//modules/api/engine/layers/M/onBeforeLoad
M.onBeforeLoad(cb: (any) -> ()) -> number
Run a callback just before a scene layer loads, while the previous layer's entities are still present.
Parameters
cb(any) -> ()— Receives the scene proxy about to load.
Returns number — A handle to pass to layers.offBeforeLoad.
local h = layers.onBeforeLoad(function(scene) print("loading", scene.name) end)
typed/builtin//modules/api/engine/layers/M/onEntityChanged
M.onEntityChanged(cb: (any) -> ()) -> number
Subscribe to authored entity changes. The callback runs once per
frame with every entity edited since the previous frame, batched by
layer as { { scene = string, entities = { string } } } — a moved
transform, an edited component field, a spawn, or a despawn (the id
of a despawned entity arrives with entity.exists already false).
Any number of subscribers can watch the same edits.
Scope: authored edits in edit mode — what lands in the scene's dirty
overlay. Mutations a component makes from its own update are runtime
behavior and do not appear, so a subscriber that rebuilds derived data
cannot re-trigger itself.
Parameters
cb(any) -> ()— Called with the change batch.
Returns number — A handle for layers.offEntityChanged.
layers.onEntityChanged(function(batch)
for _, row in ipairs(batch) do
for _, id in ipairs(row.entities) do print("changed", id) end
end
end)
typed/builtin//modules/api/engine/layers/M/onLoad
M.onLoad(cb: (any) -> ()) -> number
Run a callback once a scene layer has loaded — the point where its entities exist and player / camera spawners can attach to them.
Parameters
cb(any) -> ()— Receives the loaded scene proxy.
Returns number — A handle to pass to layers.offLoad.
local h = layers.onLoad(function(scene) print("loaded", scene.name) end)
typed/builtin//modules/api/engine/layers/M/onUnload
M.onUnload(cb: (any) -> ()) -> number
Run a callback as a scene layer unloads, while its entities are still addressable — the place to release anything keyed to them.
Parameters
cb(any) -> ()— Receives the scene proxy being unloaded.
Returns number — A handle to pass to layers.offUnload.
local h = layers.onUnload(function(scene) print("unloaded", scene.name) end)
typed/builtin//modules/api/engine/layers/M/problems
M.problems(ref: (AssetRef<scene> | string | any)?) -> { SceneLoadFailure }
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.
Parameters
ref(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
Returns { SceneLoadFailure } — An array of SceneLoadFailure — empty for a layer that came up whole.
for _, f in layers.problems() do print(f.reason, f.entity, f.message) end
typed/builtin//modules/api/engine/layers/M/rebuildInFlight
M.rebuildInFlight() -> boolean
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 build.luau 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.
Returns boolean — true while a load, a mode-flip transition or a scene build is converging.
if not layers.rebuildInFlight() then print(#entity.findAll(), "entities settled") end
typed/builtin//modules/api/engine/layers/M/reload
M.reload(ref: (AssetRef<scene> | string)?) -> any?
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
build.luau 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 build.luau does, so scene.json holds what the
reload built and the next play loads it; unsaved edits in the scene are
saved with it. A library scene (under /libs/) is rebuilt in the live
scene alone. A reload asked for while
a build of the scene is landing (a write to build.luau rebuilds over
several frames) waits for that build to land and save first.
Parameters
ref(AssetRef<scene> | string)(optional) — A sceneAssetRef, or an identity string. Omit to reload the active root scene.
Returns any? — What the scene's build.luau did: { built = true, content, editorOnly, saved } with the entity counts each half placed and whether scene.json now holds them (false for a library scene), carrying refused and a message reading them out when an operation the build ran was refused, or { built = false, reason, message } naming what stood in the way. Nil when no layer matched, which is a no-op.
layers.reload("scenes.arena")
typed/builtin//modules/api/engine/layers/M/resetCostWindow
M.resetCostWindow() -> nil
Open a new cost window, discarding what the previous one measured. Call this before timing a stretch of frames; the load history is untouched.
Returns nil
layers.resetCostWindow()
typed/builtin//modules/api/engine/layers/M/unload
M.unload(refOrProxy: (AssetRef<scene> | string | any)?) -> nil
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.
Parameters
refOrProxy(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit to unload the active root scene.
Returns nil
layers.unload("scenes.hud")
layers.unload() -- the active root, plus its non-persistent overlays
typed/builtin//modules/api/engine/layers/M/whyPartial
M.whyPartial(ref: (AssetRef<scene> | string | any)?) -> (string?, string?)
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
loaderRaised, entrypointCompileFailed, entrypointBodyRaised,
entrypointRaised, buildRaised, entityFailed, parentMissing,
parentRefused, parentAbandoned, componentUnresolved,
componentRefused, subscriberRaised, updateRaised. A second return
carries the engine's own words for that cause.
Parameters
ref(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
Returns (string?, string?) — (reason, detail).
local why, detail = layers.whyPartial(); if why then print(why, detail) end
typed/builtin//modules/api/engine/layers/layers/active
layers.active -> any
The root scene's proxy, re-resolved on every read.
Returns any
typed/builtin//modules/api/engine/layers/layers/camera
layers.camera -> any
The active root scene's camera handle, the same value layers.active.camera answers.
Returns any
typed/builtin//modules/api/engine/layers/layers/localPlayer
layers.localPlayer -> any
The active root scene's local player handle, the same value layers.active.players.localPlayer answers.
Returns any
typed/builtin//modules/api/engine/library/library/has
library.has(path: string) -> boolean
Check if a library asset exists at the given path. Answers from
the content the engine holds: an imported library's namespace
("@mylib") and its assets answer true once its pinned commit is
mounted, and false while only its marker is there.
Parameters
pathstring— Library asset path (e.g. "@builtin/models/Sample/DamagedHelmet").
Returns boolean — True if the asset exists in the library.
assert(library.has("@builtin/models/Cube"))
typed/builtin//modules/api/engine/library/library/import
library.import(namespace: string, worldRef: string) -> LibraryImport
Import another world as a library under the given namespace.
Resolves the world, pins its current commit, and writes the
library marker at /source/libs/@<namespace>, then waits while the
engine fetches the pinned commit from ZeroMind and mounts it. When
it returns, the imported tree answers to
require("@<namespace>::path") and is listed by library.list().
A library that does not mount raises naming why, and running the
import again retries it.
Parameters
namespacestring— Library namespace, with or without the leading@(e.g."@mylib"or"mylib").worldRefstring— The upstream world's guid, or its name as it appears inworld.list().
Returns LibraryImport — Record describing the import: the local name, the marker path, the upstream world_guid, and the pinned commit as version.
library.import("@mylib", "my-shared-world")
typed/builtin//modules/api/engine/library/library/list
library.list(assetType: string?) -> { LibraryAsset }
List all available library assets. Optionally filter by asset
type — call asset.categories() for the live set.
Parameters
assetTypestring(optional) — Asset type filter (optional).
Returns { LibraryAsset } — Array of asset descriptors. path is the @builtin::* identity, type is one of the valid type strings, format is the original file extension (e.g. "glb", "luau", "wgsl").
for _, a in ipairs(library.list("model")) do print(a.path) end
typed/builtin//modules/api/engine/logs/logs/clear
logs.clear() -> boolean
Drop all buffered log entries. Lifetime per-level counts
(logs.count) are preserved.
Returns boolean — True on success.
logs.clear()
typed/builtin//modules/api/engine/logs/logs/count
logs.count(opts: LogQueryOpts?) -> LogCounts
Aggregate counters for the log ring. Lifetime counts survive
eviction, so errors reflects the total seen even if the lines
have scrolled out of the buffer. opts takes the same filter table
as logs.query, and matched is how many held entries it selects,
counted without materialising them — limit and newest_first bound
and order what a query RETURNS, so they leave matched alone. mcp
is how many held entries record your own tool traffic; a query leaves
those out, so with no opts, matched + mcp is everything held.
last_seq is the cursor for
incremental polling: read it before an action, then pass it as
logs.query({ since = <that> }) afterwards to see only what the
action logged.
Parameters
optsLogQueryOpts(optional) — Filter options, aslogs.querytakes.
Returns LogCounts — Counts summary table.
print("errors:", logs.count().errors)
local before = logs.count().last_seq
typed/builtin//modules/api/engine/logs/logs/errors
logs.errors(limit: number?) -> { LogEntry }
Most-recent ERROR-level entries (newest first). limit
defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of ERROR log-entry tables.
for _, e in ipairs(logs.errors(20)) do print(e.message) end
typed/builtin//modules/api/engine/logs/logs/find
logs.find(text: string, limit: number?) -> { LogEntry }
Case-insensitive substring search over log messages. limit
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; logs.query({ contains = ..., include_mcp = true })
searches your own tool traffic too.
Parameters
textstring— Substring to search for.limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of matching log-entry tables in chronological order.
local hits = logs.find("MY_MARKER")
typed/builtin//modules/api/engine/logs/logs/query
logs.query(opts: LogQueryOpts?) -> { LogEntry }
Query the engine's in-memory log ring — the filtered view of what
also reads as plain text at /zero/runtime/logs/engine. 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. type = "MCP" selects them;
include_mcp = true mixes them in with everything else. On a world
several sessions share, origin = "local" narrows the answer to the
lines this session's own authoring caused.
Parameters
optsLogQueryOpts(optional) — Filter options.
Returns { LogEntry } — Array of matching log-entry tables.
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
typed/builtin//modules/api/engine/logs/logs/tail
logs.tail(limit: number?) -> { LogEntry }
Most-recent entries of any level in chronological order.
limit defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of the most recent log-entry tables.
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
typed/builtin//modules/api/engine/logs/logs/template
logs.template(message: string) -> string
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.
Parameters
messagestring— Log message to normalize.
Returns string — The message template.
local key = logs.template(entry.message)
typed/builtin//modules/api/engine/logs/logs/warnings
logs.warnings(limit: number?) -> { LogEntry }
Most-recent WARN+ entries (newest first). limit defaults
to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of WARN+ log-entry tables.
print(#logs.warnings(), "warnings")
typed/builtin//modules/api/engine/lsp/lsp/analysisFlags
lsp.analysisFlags() -> AnalysisFlags
The feature-flag configuration upstream Luau's analysis library
(the linter and .luaurc parser behind these checks) runs under,
read back from Luau's flag registry. It is upstream's own: every
Luau* 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 shared: they run at the value the
engine's runtime gives them, and one that differs from upstream's
carries its deviation. flags lists every flag those sources
declare or read, sorted by name; atUpstream counts the linked ones
whose value is upstream's. available is false, with reason, on a
build that does not link the analysis library.
Returns AnalysisFlags — { available, reason?, declared, shared, linked, atUpstream, flags }.
typed/builtin//modules/api/engine/lsp/lsp/check
lsp.check(path: string, opts: CheckFileOpts?) -> DiagnosticsResult
Validate a single .luau file in the VFS and return its
diagnostics. A path the check could not read comes back as one
lsp-check-* error naming the path and the reason, and a file past the
2 MiB analysis bound as one source-too-large warning with
checked = false, so checked with errors == 0 means a code body was
analysed and is clean.
opts.upstream (true, or the options of lsp.upstreamCheck) also
checks the file with upstream Luau's type checker and sets upstream to
what lsp.upstreamCheck returns for it, whose modules say how the
check of each module it reached ended and how long it took; the
diagnostics and counts stay the engine checker's.
Parameters
pathstring— VFS path.optsCheckFileOpts(optional) —{ severity?, limit?, context?, upstream? }.
Returns DiagnosticsResult — Array of diagnostic tables.
local diags = lsp.check("/zero/source/main.luau")
typed/builtin//modules/api/engine/lsp/lsp/checkAll
lsp.checkAll(opts: CheckAllOpts?) -> CheckAllResult
Validate the user's Luau scripts and return an aggregate
summary plus diagnostic list. opts.scope = "user" (default)
skips library mounts; "all" 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 budgetExceeded = true rather than blocking the engine.
opts.budgetMs sets the budget for the call and opts.maxFiles caps
the files it checks. opts.upstream (true, or the options of
lsp.upstreamCheck) also checks each file with upstream Luau's type
checker once the engine's own pass is done, within the same budget, and
sets upstream 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, reused 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.
Parameters
optsCheckAllOpts(optional) —{ scope?, severity?, limit?, budgetMs?, maxFiles?, upstream? }.
Returns CheckAllResult — { filesChecked, errors, warnings, info, hints, budgetExceeded, diagnostics, upstream? }.
typed/builtin//modules/api/engine/lsp/lsp/checkCode
lsp.checkCode(source: string, opts: CheckOpts?) -> DiagnosticsResult
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 source-too-large warning with
checked = false.
Parameters
sourcestring— Luau source.optsCheckOpts(optional) —{ severity?, limit?, context? }.
Returns DiagnosticsResult — Array of diagnostic tables.
typed/builtin//modules/api/engine/lsp/lsp/checkDirty
lsp.checkDirty() -> DiagnosticsResult
Drain the dirty-file set populated by the hot-reload hook, validate each, and return the combined diagnostic list.
Returns DiagnosticsResult — Array of diagnostic tables.
typed/builtin//modules/api/engine/lsp/lsp/definitions
lsp.definitions(opts: DefinitionsOpts?) -> Definitions
The engine's API as a Luau definitions source, upstream Luau's
.d.luau form (declare statements and export type aliases),
generated from the live API registry: the documented members, the
--!global-types aliases and the asset-category shapes lsp.check
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.
lsp.upstreamCheck loads the same source into every module it checks,
and an external editor running luau-lsp reads it as a definitions file
(lsp.exportDefinitions writes it out). It is generated again whenever
the registry changes, a library hot reload included. package (@zero)
keys each global's documentation symbol (@zero/global/<name>,
@zero/global/<name>.<member>). Each type string the registry holds is
declared as written, or listed in unrepresented with the reason and
declared any; 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. opts.documentation = true also returns
documentation, the JSON documentation file luau-lsp reads beside the
definitions: each symbol's signature and description. available is
false, with reason, on a build that does not link the analysis library.
Parameters
optsDefinitionsOpts(optional) —{ documentation? }.
Returns Definitions — { available, reason?, package, source, bytes, globals, types, unrepresented, documentation? }.
local d = lsp.definitions()
print(d.bytes, #d.globals, #d.unrepresented)
typed/builtin//modules/api/engine/lsp/lsp/describe
lsp.describe(path: string, opts: DescribeOpts?) -> DocEntry?
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 (renderer.texture.create) finds the entry
registered as globals/renderer/texture/create. 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; opts.includeInternal answers with the internally
registered entry itself. When a path does not resolve,
lsp.describePaths says what the registry holds near it.
Parameters
pathstring— Doc path (e.g."asset/resolve","renderer.texture.create").optsDescribeOpts(optional) — Optional{ includeInternal? }— default prefers the published entry.
Returns DocEntry? — Full doc table or nil.
local doc = lsp.describe("renderer.texture.create")
typed/builtin//modules/api/engine/lsp/lsp/describePaths
lsp.describePaths(path: string) -> { string }
List the registered doc paths related to path. A path that names
an entry returns every root it is registered under (the first is what
lsp.describe 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.
Parameters
pathstring— Doc path in any spelling ("renderer.texture","ecs/query").
Returns { string } — Array of registered doc paths, most canonical first.
for _, p in ipairs(lsp.describePaths("renderer.texture")) do print(p) end
typed/builtin//modules/api/engine/lsp/lsp/describeTool
lsp.describeTool(path: string) -> string?
Return the full documentation text for a code-mode tool.
Parameters
pathstring— Tool path (e.g."scene/spawnLight").
Returns string? — Full tool docs or nil.
typed/builtin//modules/api/engine/lsp/lsp/docsByKind
lsp.docsByKind(kind: string) -> { MethodSummary }
List every doc whose registration kind matches kind.
Valid: "binding", "runtime_tool", "module", "component",
"library", "lua_export".
Parameters
kindstring— Registration kind.
Returns { MethodSummary } — Array of doc summary tables.
typed/builtin//modules/api/engine/lsp/lsp/exportDefinitions
lsp.exportDefinitions(dir: string) -> ExportDefinitionsResult
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: <dir>/zero.d.luau
(the definitions lsp.definitions returns), <dir>/zero.docs.json (the
documentation file naming each global's and member's signature and
description) and <dir>/.luaurc (nonstrict mode). Point luau-lsp at the
first two (its types.definitionFiles and types.documentationFiles
settings) and it types the engine's globals: entity.spawn("x") checks
against its signature and entity.spwan("x") 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: userfile.save(path) (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 GET /engine/vfs/<path> reads it. The engine's own checker
reads a *.d.luau file the way upstream does, by loading it, so the
files can stay in a world's content. Writing again replaces them.
globals, types and unrepresented are the lists lsp.definitions
returns; the guide topics/external-editor walks through the editor
setup.
Parameters
dirstring— VFS directory to write into, such as/zero/source/editor.
Returns ExportDefinitionsResult — { available, reason?, files, bytes, globals, types, unrepresented }.
local r = lsp.exportDefinitions("/zero/source/editor")
print(r.files[1], r.bytes, #r.globals)
typed/builtin//modules/api/engine/lsp/lsp/getStrictMode
lsp.getStrictMode() -> StrictMode
Return the current strict mode.
Returns StrictMode — "off" | "soft" | "strict".
typed/builtin//modules/api/engine/lsp/lsp/isStrict
lsp.isStrict() -> boolean
Is the pre-execute LSP gate fully strict? False when off or in soft mode.
Returns boolean — True when fully strict.
typed/builtin//modules/api/engine/lsp/lsp/lastCheckGen
lsp.lastCheckGen() -> number
Generation counter — bumped each time the cache is rebuilt. UI polls this to know when to redraw.
Returns number — Generation number.
typed/builtin//modules/api/engine/lsp/lsp/methods
lsp.methods(namespace: string, opts: MethodsOpts?) -> { MethodSummary } | { string }
List every documented method / entry under a namespace. A broad
namespace (ui, renderer) returns a large dump by default, so two
options narrow it: opts.filter keeps only methods whose name (or
doc path) contains the substring, case-insensitively; opts.namesOnly
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 name field or a
namesOnly string), is the member's path below the namespace,
dot-joined (texture.create under renderer, proxy.component.get
under entity), so each one names a single member and
lsp.describe(namespace .. "." .. name) answers for it. The listing
answers with the surface content calls: an entry registered internally
is left out where its signature spells the __ binding or a Luau
module publishes the same member, and opts.includeInternal lists
every registered entry instead.
Parameters
namespacestring— Namespace name (e.g."entity","modules/Transform").optsMethodsOpts(optional) — Optional{ filter?, namesOnly?, includeInternal? }.
Returns { MethodSummary } | { string } — Array of method summary tables, or plain name strings when namesOnly is set (empty when the namespace is unknown or nothing matches the filter).
for _, name in ipairs(lsp.methods("ui", { namesOnly = true })) do print(name) end
lsp.methods("renderer", { filter = "shadow" })
typed/builtin//modules/api/engine/lsp/lsp/modules
lsp.modules() -> { ModuleEntry }
List every Luau library module the engine currently knows
about — discovered via --!module headers, library scans, and
manually-recorded docs.
Returns { ModuleEntry } — Array of module summary tables.
typed/builtin//modules/api/engine/lsp/lsp/namespaces
lsp.namespaces(opts: NamespacesOpts?) -> { NamespaceEntry }
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. pause, native_entity), whose public surface
lives elsewhere (engine.paused, the entity proxy, …), is left
out. Pass { includeInternal = true } to list every namespace,
internal ones included.
Parameters
optsNamespacesOpts(optional) — Optional{ includeInternal? }— default lists public only.
Returns { NamespaceEntry } — Array of namespace summary tables.
for _, ns in ipairs(lsp.namespaces()) do print(ns.name) end
typed/builtin//modules/api/engine/lsp/lsp/readDirectives
lsp.readDirectives(source: string) -> DirectiveBlock
Parse the leading --! directive block of a Luau source
string. Used by UIs that audit which files have skip directives
and what they suppress.
Parameters
sourcestring— Luau source text.
Returns DirectiveBlock — { mode, codes? }.
typed/builtin//modules/api/engine/lsp/lsp/search
lsp.search(query: string, opts: SearchOpts?) -> { MethodSummary }
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 __ binding or a Luau module publishes the same
member, and opts.includeInternal searches every registered entry.
Parameters
querystring— Substring to search for.optsSearchOpts(optional) —{ limit? = 50, includeInternal? }.
Returns { MethodSummary } — Array of method summary tables.
typed/builtin//modules/api/engine/lsp/lsp/setStrict
lsp.setStrict(enabled: boolean) -> boolean
Toggle the pre-execute LSP gate. Returns true when the change
was persisted to .world_settings, false when the play-mode write
lock blocked the write.
Parameters
enabledboolean— True = strict, false = off.
Returns boolean — Persistence signal.
typed/builtin//modules/api/engine/lsp/lsp/setStrictMode
lsp.setStrictMode(mode: StrictMode) -> boolean
Set the pre-execute strict gate's mode. Returns true when the
change was persisted to .world_settings, false when the
play-mode write lock blocked the write.
Parameters
modeStrictMode—"off"|"soft"|"strict".
Returns boolean — Persistence signal.
typed/builtin//modules/api/engine/lsp/lsp/summary
lsp.summary() -> Summary
Counts only — does not re-run validation.
Returns Summary — Counts of cached diagnostics by severity.
typed/builtin//modules/api/engine/lsp/lsp/symbolAt
lsp.symbolAt(path: string, line: number, col: number, name: string, chain: string?) -> SymbolHit?
The identifier name where it stands in path at line:col
(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, declaredAt = {line, col}. The answer a hover shows and a go-to-definition jumps
to. nil when nothing at that position is a reference to name: a
word in a comment or a string, a keyword.
Parameters
pathstring— VFS path of the file.linenumber— 1-based line.colnumber— 1-based column in the file's own text.namestring— The identifier at that position.chainstring(optional) — The access path the identifier ends —entity.component.addfor a pointer onadd— which is where its API page is filed.
Returns SymbolHit? — The type descriptor of the reference (kind, display, and for a function its params and returns), with name, symbolKind (local, parameter, function, field, global, namespace, module), declaredAt when the file introduced the name, members and memberCount for a table, modulePath and moduleFile for a required module, and doc — the comment block above the declaration (--!desc, --!arg, --!return, --!example), the required module's header, or the API page under chain. nil when nothing there is a reference to name.
local hit = lsp.symbolAt("/zero/source/main.luau", 12, 9, "add", "entity.component.add")
typed/builtin//modules/api/engine/lsp/lsp/tools
lsp.tools() -> { ToolEntry }
List every code-mode tool registered in the VFS under
/zero/docs/tools/<category>/<tool>.
Returns { ToolEntry } — Array of tool summary tables.
typed/builtin//modules/api/engine/lsp/lsp/typeOf
lsp.typeOf(expr_source: string, context_path: string?) -> TypeDescriptor
Infer the static type of a Luau expression. When
context_path 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 unknown,
and a context file past it adds nothing to the env.
Parameters
expr_sourcestring— Luau expression source (no surrounding chunk).context_pathstring(optional) — VFS path whose scope should be visible.
Returns TypeDescriptor — Type descriptor table.
local td = lsp.typeOf("Transform.lookAt", "/zero/source/main.luau")
typed/builtin//modules/api/engine/lsp/lsp/upstreamCheck
lsp.upstreamCheck(path: string, opts: UpstreamCheckOpts?) -> UpstreamCheck
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 lsp.check, the execute gate and the play gate
report is unchanged by it. Each require literal resolves to the source
file lsp.check's require-unresolved pass holds it to, which is the
file the runtime loads wherever the runtime loads one, in whichever form
it is written (@builtin::a.b, @ns::, @root::, a bare dotted a.b,
.x, ..x, ./x, ../x, ~.x, ~pkg.x, an absolute path, a folder
asset), and
requires lists each literal of the file with the file it reached
(module) and the file the runtime's require loads for it from this
file (runtimeModule), nil where either reaches none.
findings 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 opts.includeRequired the type errors of every module it
reaches come back as well. Upstream reads every module through its
analysis view: the engine's typed declarations read as the plain Luau
functions they declare, their generic, parameter and return annotations
kept, a return type written after -> read after :, and every line
and column as in the source; view lists where the view of the file
differs from it as { line, col, written, reads }. A module whose
findings come back that upstream cannot parse through its view or past
the 2 MiB analysis bound is listed in notAnalysable with the reason,
and when that is the file itself analysable is false. The checked
modules are kept between calls: checkedModules 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 opts.timeLimitMs, and modules
says of each module the call reached how its check ended ("completed",
"timedOut" or "internal") and how long it took, reused 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 held.limitBytes, the modules the least recent calls reached
going first, and held and evicted report it. A failure
inside the analysis library comes back as an "internal" finding with
completed false, and the internal errors upstream reports for a
module come back as one "internal" finding whose count says how many. opts.typeCheck = false reads the file and every
module it reaches and resolves their requires without checking types. available is false, with reason, on a build that
does not link the analysis library.
Parameters
pathstring— VFS path of a .luau file, a module identity, or a@scope::path.optsUpstreamCheckOpts(optional) —{ solver?, mode?, timeLimitMs?, includeRequired?, typeCheck? }.
Returns UpstreamCheck — { available, path, analysable, reason?, completed, solver, mode, elapsedMs, findings, notAnalysable, requires, view, checkedModules, timedOut, modules, held, evicted, errors, warnings }.
local r = lsp.upstreamCheck("@builtin::modules.boids", { mode = "strict" })
typed/builtin//modules/api/engine/luau_profile/luau_profile/begin
luau_profile.begin(name: string) -> number
Open a named manual region. Returns an opaque integer id;
pass it back to end_region(id) to close and record elapsed
wall-clock under name.
Parameters
namestring— Region name; aggregated across opens.
Returns number — Region id.
local id = luau_profile.begin("walk"); ...; luau_profile.end_region(id)
typed/builtin//modules/api/engine/luau_profile/luau_profile/end_region
luau_profile.end_region(id: number)
Close a region previously opened by begin(name). Records
elapsed wall-clock under the region's name. Silently no-ops on
unknown id (typically a double-close or swapped-out VM).
Parameters
idnumber— Region id returned bybegin().
luau_profile.end_region(id)
typed/builtin//modules/api/engine/luau_profile/luau_profile/folded
luau_profile.folded() -> string
The folded-stack dump as a string, one line per stack as
<ticks> <stack_csv> — the format upstream Luau emits and
tools/perfgraph.py consumes unchanged. The same bytes dump
writes, handed back instead of written, so the profile can go
wherever the caller keeps it: vfs.write puts it in the engine
filesystem, where bash and vfs.read reach it.
Returns string — Folded-stack text; empty when nothing was sampled.
vfs.write("/source/tmp/sample.folded", luau_profile.folded())
typed/builtin//modules/api/engine/luau_profile/luau_profile/is_running
luau_profile.is_running() -> boolean
True iff the background sampler is currently running.
Returns boolean — Sampler running state.
if luau_profile.is_running() then luau_profile.stop() end
typed/builtin//modules/api/engine/luau_profile/luau_profile/reset
luau_profile.reset()
Clear every accumulated sample and region stat. The sampler keeps running if it was already on; only the data is wiped.
luau_profile.reset()
typed/builtin//modules/api/engine/luau_profile/luau_profile/sampling_available
luau_profile.sampling_available() -> boolean
True on platforms where the background sampler can run (native targets), false on WASM. Manual regions work everywhere — only the sampler is platform-gated.
Returns boolean — Whether start() would actually spawn a sampler.
if luau_profile.sampling_available() then luau_profile.start() end
typed/builtin//modules/api/engine/luau_profile/luau_profile/snapshot
luau_profile.snapshot(top_n: number?) -> Snapshot
Snapshot the current accumulator without touching the
filesystem — cheap enough for per-frame UI polling. top_n
truncates stacks to the N hottest entries; omitting it
returns all stacks sorted descending by self_us. regions
is always returned in full (sorted by total_us).
Parameters
top_nnumber(optional) — Truncate stacks to this many entries; omit for all.
Returns Snapshot — Profile snapshot.
local snap = luau_profile.snapshot(10)
typed/builtin//modules/api/engine/luau_profile/luau_profile/start
luau_profile.start(hz: number?) -> StartResult
Start the background Luau sampling profiler at hz samples
per second (default 1000, clamped to [1, 100000]). Idempotent
— calling while already running is a no-op. Returns
{ available, hz } — available = false on WASM (no
std::thread). Manual regions work regardless.
Parameters
hznumber(optional) — Sampling rate in Hz.
Returns StartResult — Effective sampler state.
luau_profile.start(500)
typed/builtin//modules/api/engine/luau_profile/luau_profile/stop
luau_profile.stop()
Stop the background sampler. Blocks until the sampler thread
joins (typically <1ms). Safe when not running. Does not clear
the accumulator — call reset() to drop samples.
luau_profile.stop()
typed/builtin//modules/api/engine/mathx/mathx/addScaledVec3
mathx.addScaledVec3(dstBuffer: Substrate.TypedBuffer, srcBuffer: Substrate.TypedBuffer, count: number, scale: number) -> boolean
dst[i] += src[i] * scale for count vec3 elements. Both
buffers must hold at least count * 3 floats. Useful for
particle integration (position += velocity * dt) and accumulator
passes.
Parameters
dstBufferSubstrate.TypedBuffer— The buffer written into.srcBufferSubstrate.TypedBuffer— The buffer read from.countnumber— Number of vec3 elements.scalenumber— Multiplier applied to every src element.
Returns boolean — True on success.
mathx.addScaledVec3(positions, velocities, n, dt)
typed/builtin//modules/api/engine/mathx/mathx/dampScalar
mathx.dampScalar(buffer: Substrate.TypedBuffer, offset: number, count: number, target: number, smoothTime: number, dt: number) -> boolean
Critically-damped exponential approach toward target for
count scalars at buffer[offset .. offset+count]. smoothTime
is the time constant (~ 0.16 ⇒ ~63% per frame at 60 Hz). Pass
smoothTime <= 0 to snap to the target.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of scalars.targetnumber— Target value all scalars approach.smoothTimenumber— Time constant (≤ 0 snaps to target).dtnumber— Frame time in seconds.
Returns boolean — True on success, false on a bad handle or out-of-range slice.
mathx.dampScalar(buf, 0, 16, 0.0, 0.16, dt)
typed/builtin//modules/api/engine/mathx/mathx/lerpVec3
mathx.lerpVec3(buffer: Substrate.TypedBuffer, offset: number, count: number, tx: number, ty: number, tz: number, t: number) -> boolean
Element-wise linear blend of count vec3s in
buffer[offset .. offset+count*3] toward (tx, ty, tz) by t.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of vec3 elements.txnumber— Target X.tynumber— Target Y.tznumber— Target Z.tnumber— Blend amount (0..1).
Returns boolean — True on success.
mathx.lerpVec3(buf, 0, n, 0, 1, 0, 0.5)
typed/builtin//modules/api/engine/mathx/mathx/normalizeQuat
mathx.normalizeQuat(buffer: Substrate.TypedBuffer, offset: number, count: number) -> boolean
Re-normalise count quaternions in place. Zero-length quats
become identity (0, 0, 0, 1) so downstream code never sees NaN.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of quaternions.
Returns boolean — True on success.
mathx.normalizeQuat(buf, 0, n)
typed/builtin//modules/api/engine/mathx/mathx/slerpQuat
mathx.slerpQuat(buffer: Substrate.TypedBuffer, offset: number, count: number, tx: number, ty: number, tz: number, tw: number, t: number) -> boolean
Slerp count quaternions (xyzw) at buffer[offset..] toward
(tx, ty, tz, tw) by t. Falls back to nlerp+normalize for
very-close quats. Always picks the shortest-arc path.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of quaternions.txnumber— Target quat X.tynumber— Target quat Y.tznumber— Target quat Z.twnumber— Target quat W.tnumber— Slerp amount (0..1).
Returns boolean — True on success.
mathx.slerpQuat(buf, 0, n, 0, 0, 0, 1, 0.25)
typed/builtin//modules/api/engine/mathx/mathx/transformVec3
mathx.transformVec3(buffer: Substrate.TypedBuffer, offset: number, count: number, mat16: { number }) -> boolean
Treat each vec3 in buffer[offset..] as a position (w = 1),
multiply by the 4x4 column-major matrix mat16 (16-element
array), write .xyz of the result back. Layout matches glam,
wgpu, and GLSL conventions.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of vec3 elements.mat16{ number }— Column-major 4x4 matrix as a 16-element array.
Returns boolean — True on success.
mathx.transformVec3(positions, 0, n, worldMatrix)
typed/builtin//modules/api/engine/mcpLog/mcpLog/clear
mcpLog.clear() -> boolean
Clear all entries from the engine's MCP log ring buffer.
Returns boolean — True on success.
mcpLog.clear()
typed/builtin//modules/api/engine/mcpLog/mcpLog/query
mcpLog.query(limit: number?) -> { McpLogEntry }
Return the most-recent MCP tool-call entries from the engine's
MCP log ring buffer (newest last). Pass limit to cap how many
entries are returned — omit for the full ring (up to 500 entries).
Parameters
limitnumber(optional) — Maximum number of entries to return.
Returns { McpLogEntry } — Array of tool-call entry tables.
for _, e in ipairs(mcpLog.query(50)) do print(e.tool_name, e.status) end
typed/builtin//modules/api/engine/microphone/microphone/awaitRunning
microphone.awaitRunning(timeout: number?) -> (MicState, string?)
Wait until the capture settles out of starting and
permissionPending, and report where it landed. Returns as soon as the
state settles, or when timeout seconds have passed, whichever comes
first — a browser permission prompt nobody answers never settles, so
the wait is always bounded.
Parameters
timeoutnumber(optional) — Seconds to wait at most. Defaults to 10.
Returns (MicState, string?) — The state reached, and its reason where it has one.
microphone.start(); local state, why = microphone.awaitRunning()
typed/builtin//modules/api/engine/microphone/microphone/devices
microphone.devices() -> { MicDevice }
Every input device the platform offers. id is what
microphone.start takes to select one and is stable across reboots
where the platform provides a stable identifier; name is the label a
person recognises.
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.
Returns { MicDevice } — Array of { id, name, default }.
for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end
typed/builtin//modules/api/engine/microphone/microphone/frequencies
microphone.frequencies() -> { number }
The frequency each spectrum bin is centred on, in Hz, as an array
parallel to microphone.spectrum(). 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.
Returns { number } — Array of centre frequencies, one per bin.
local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency
typed/builtin//modules/api/engine/microphone/microphone/level
microphone.level() -> number
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.
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.
Returns number — RMS amplitude, 0..1.
if microphone.level() > 0.05 then print("someone is talking") end
typed/builtin//modules/api/engine/microphone/microphone/peak
microphone.peak() -> { [string]: number }?
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.
Returns { [string]: number }? — { bin, hz, amplitude, level }, or nil while no capture is running.
local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end
typed/builtin//modules/api/engine/microphone/microphone/samples
microphone.samples(max: number?) -> buffer?
Captured mono PCM no caller has taken yet, oldest sample first, as a
buffer of little-endian f32 read with buffer.readf32. The samples are
removed, so successive calls walk forward through the capture and a
caller doing its own analysis sees every frame once.
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 status().overruns counts
every one.
Parameters
maxnumber(optional) — How many samples to take at most. Omitted, everything held comes back.
Returns buffer? — Buffer of f32 samples, or nil when no capture is running.
local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end
typed/builtin//modules/api/engine/microphone/microphone/spectrum
microphone.spectrum() -> { number }
Amplitude per frequency bin over the most recent analysis window:
fftSize / 2 + 1 numbers, DC at index 1 through the Nyquist frequency
at the last. Bin i covers (i - 1) * status().binHz Hz.
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.
The window is multiplied by a Hann 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".
Reading this takes no samples away from microphone.samples(). Empty
while no capture is running.
Returns { number } — Array of amplitudes, one per bin.
local bins = microphone.spectrum(); print(#bins, bins[1])
typed/builtin//modules/api/engine/microphone/microphone/start
microphone.start(opts: MicOpts?) -> (MicState?, string?)
Open an input device and begin capturing. Returns the state the
capture reached — "running" once a device is delivering, or
"permissionPending" where the platform must ask for access first,
which is the browser's normal path. Poll microphone.status() from
there, or use microphone.awaitRunning(). Where access is refused
without asking (a native engine's fake microphone presented as deny),
it returns "denied".
A request that cannot be made at all returns nil and the reason: an
fftSize 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.
Omitting device opens the platform default. Omitting sampleRate
takes the device's own rate, which is what avoids a resample.
fftSize 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.
Parameters
optsMicOpts(optional) —{ device, sampleRate, fftSize }.
Returns (MicState?, string?) — The state reached, or nil and the reason the request was refused.
local state, why = microphone.start({ fftSize = 2048 })
typed/builtin//modules/api/engine/microphone/microphone/status
microphone.status() -> MicStatus
Where the capture stands.
reason carries the platform's own message: the refusal for denied,
the device's message for failed, what is being waited on for
permissionPending. binHz is the width of one spectrum bin and
bins how many microphone.spectrum() returns.
framesCaptured counts every mono frame the device delivered whether
or not anything drained it, so a silent room reads differently from a
stalled device. overruns 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.
Returns MicStatus — { state, reason, device, sampleRate, fftSize, binHz, bins, framesCaptured, overruns }.
local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns)
typed/builtin//modules/api/engine/microphone/microphone/stop
microphone.stop() -> boolean
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.
Returns boolean — Whether a capture was active.
microphone.stop()
typed/builtin//modules/api/engine/mode_flip_guard/M/acrossRebuild
M.acrossRebuild(taken: (mode: string) -> (), settled: (mode: string) -> ()) -> nil
Carry state across the rebuild a mode flip makes. The rebuild unloads
the scene's layers and loads them again for the new mode, so every entity
the scene holds is despawned and materialised again over the frames after
the flip. taken runs as the flip is taken, before the teardown removes
anything, with the mode being entered; settled runs once the rebuild has
ended, with the mode it materialised, before isInFlight reads false. A
flip that arrives while a rebuild runs is carried by that rebuild, so each
rebuild runs one taken and one settled. A watcher that raises is
logged and the others still run.
Parameters
taken(mode: string) -> ()— Called with the mode being entered as the flip is taken.settled(mode: string) -> ()— Called with the mode the rebuild materialised once it has ended.
Returns nil
typed/builtin//modules/api/engine/mode_flip_guard/M/beginPlaySession
M.beginPlaySession() -> number
Open a play session, advancing the play-session id. engine.module
calls this from its mode-change watcher as the engine enters play, so
every play session the engine runs carries an id of its own whichever
route flipped the mode.
Returns number — The id of the play session being opened.
typed/builtin//modules/api/engine/mode_flip_guard/M/isInFlight
M.isInFlight() -> boolean
Whether a mode-flip transition is materialising the scene. The
transition unloads the root layer and reloads it for the new mode across
many frames, so while this reads true the live entities are a partial
rebuild of the scene; code that judges authored scene state waits for it
to clear.
Returns boolean — true while the transition is running.
typed/builtin//modules/api/engine/mode_flip_guard/M/isOwned
M.isOwned() -> boolean
Whether the layers module currently owns the mode-flip reset.
Returns boolean — true while the layers transition owns the flip; spawners stand down.
typed/builtin//modules/api/engine/mode_flip_guard/M/playSession
M.playSession() -> number
The id of the play session the engine is in. State that belongs to a single play session records this id when it is armed and compares it before it is spent, so an arm outlives exactly the session that made it.
Returns number — The current play-session id.
typed/builtin//modules/api/engine/mode_flip_guard/M/rebuildSettled
M.rebuildSettled(mode: string) -> nil
Run every acrossRebuild watcher's settled. The layers module calls
this once the rebuild has materialised the mode the engine is in.
Parameters
modestring— The mode the rebuild materialised.
Returns nil
typed/builtin//modules/api/engine/mode_flip_guard/M/rebuildTaken
M.rebuildTaken(mode: string) -> nil
Run every acrossRebuild watcher's taken. The layers module calls
this as it takes a flip that starts a rebuild, before the teardown.
Parameters
modestring— The mode being entered.
Returns nil
typed/builtin//modules/api/engine/mode_flip_guard/M/setInFlight
M.setInFlight(v: boolean) -> nil
Set whether a mode-flip transition is materialising the scene.
Parameters
vboolean—truefor the span of the transition,falseonce it has settled.
Returns nil
typed/builtin//modules/api/engine/mode_flip_guard/M/setOwned
M.setOwned(v: boolean) -> nil
Set whether the layers module owns the current mode-flip reset.
Parameters
vboolean—trueto claim ownership,falseto release.
Returns nil
typed/builtin//modules/api/engine/modelImport/modelImport/decompose
modelImport.decompose(bytes: buffer | string, format: string) -> PromiseHandle
Parse raw model bytes on a background thread. format is the real source
extension ("fbx", "obj", "dae", "gltf", "glb", "stl", "ply",
"3ds", …), forwarded to assimp as the format hint. Returns a promise
handle: task.await it, then read the data with result(handle).
Parameters
bytesbuffer | string— Raw model file bytes (fromvfs.readAsync).formatstring— The source file extension (lowercase, no dot).
Returns PromiseHandle — Promise handle for task.await.
local h = modelImport.decompose(bytes, "obj"); task.await(h)
typed/builtin//modules/api/engine/modelImport/modelImport/decomposeFiles
modelImport.decomposeFiles(files: { ModelFile }, mainName: string) -> PromiseHandle
Parse a model plus its companion files on a background thread, so assimp
resolves the model's external references (a .gltf's external .bin and
image files, an .obj's .mtl colors/textures, MD5's .md5anim, …).
files is an array of { name = basename, bytes = <bytes> } that MUST
include the model file itself; mainName is that file's basename. Returns a
promise handle: task.await it, then read the data with result(handle) —
the same shape decompose produces.
Parameters
files{ ModelFile }— Array of{ name, bytes }: the model file plus its companions.mainNamestring— Basename of the model file to import (one offiles' names).
Returns PromiseHandle — Promise handle for task.await.
local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h)
typed/builtin//modules/api/engine/modelImport/modelImport/extractAnimation
modelImport.extractAnimation(sourcePath: string, clipName: string) -> PromiseHandle
Read a model source file and extract one animation clip to its .zanim
payload, stashed for retrieval. The source extension decides the parser, so
this is format-agnostic. Returns a promise handle: task.await it, then
extractAnimationResult(handle) returns the bytes.
Parameters
sourcePathstring— VFS path to the source model file.clipNamestring— Clip name as returned byresult(handle).animations[i].name.
Returns PromiseHandle — Promise handle for task.await.
typed/builtin//modules/api/engine/modelImport/modelImport/extractAnimationResult
modelImport.extractAnimationResult(handle: PromiseHandle | string) -> string?
After awaiting an extractAnimation handle, return the extracted
.zanim bytes (binary-safe), consuming them. The bytes to hand to
asset.create("animation", name, { bytes }). Returns nil on failure or if
already taken.
Parameters
handlePromiseHandle | string— Promise handle fromextractAnimation.
Returns string? — The clip's .zanim payload bytes, or nil.
typed/builtin//modules/api/engine/modelImport/modelImport/result
modelImport.result(handle: PromiseHandle | string) -> any?
Read the decomposed model after decompose'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.
Parameters
handlePromiseHandle | string— Promise handle fromdecompose.
Returns any? — { nodes, meshes, materials, textures, animations, hasSkeleton, skeletonRootNode?, skeleton? }, or nil.
typed/builtin//modules/api/engine/modelImport/modelImport/retryHandle
modelImport.retryHandle(makeHandle: () -> any, retries: number?, yield: (() -> ())?) -> (PromiseHandle | string)?
Parameters
makeHandle() -> anyretriesnumber(optional)yield(() -> ())(optional)
Returns (PromiseHandle | string)?
typed/builtin//modules/api/engine/modelImport/modelImport/rigFromMeshSkin
modelImport.rigFromMeshSkin(meshBytes: buffer | string) -> string?
Lift the skeleton out of a skinned .mesh (ZMSH) payload and return it
as a .rig 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.
Parameters
meshBytesbuffer | string— Raw ZMSH mesh bytes carrying a skin.
Returns string? — .rig JSON document, or nil.
typed/builtin//modules/api/engine/modelImport/modelImport/rigFromSkeleton
modelImport.rigFromSkeleton(skeleton: AnimationSkeleton) -> string?
Build a .rig JSON document from the skeleton an animation-only file was
authored on — result(handle).skeleton, 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.
Parameters
skeletonAnimationSkeleton—{ names, parents, locals }(adecomposeresult'sskeleton).
Returns string? — .rig JSON document, or nil.
local rigJson = modelImport.rigFromSkeleton(data.skeleton)
typed/builtin//modules/api/engine/multiplayer/multiplayer/beginOperation
multiplayer.beginOperation(description: string)
Begin recording an undoable operation. All mutations until
commitOperation() are grouped into one undo entry.
Parameters
descriptionstring— Human-readable label.
multiplayer.beginOperation("move cube")
typed/builtin//modules/api/engine/multiplayer/multiplayer/canRedo
multiplayer.canRedo() -> boolean
Check if this client has any redoable operations.
Returns boolean — True if redo is available.
typed/builtin//modules/api/engine/multiplayer/multiplayer/canUndo
multiplayer.canUndo() -> boolean
Check if this client has any undoable operations.
Returns boolean — True if undo is available.
typed/builtin//modules/api/engine/multiplayer/multiplayer/cancelOperation
multiplayer.cancelOperation()
Cancel the current operation and restore all properties to their values at begin time.
multiplayer.cancelOperation()
typed/builtin//modules/api/engine/multiplayer/multiplayer/claimOwnership
multiplayer.claimOwnership(entityId: (string | entityRef)?) -> boolean
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 getDiagnostics().refusals and named by
lastRefusal. The owner hands an entity on by releasing it.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to claim.
Returns boolean — True once the claim is on its way to the relay.
typed/builtin//modules/api/engine/multiplayer/multiplayer/clearHistory
multiplayer.clearHistory()
Drop this client's whole undo/redo history — for boundaries where old edits stop being meaningful (a scene load, a test rig reset).
multiplayer.clearHistory()
typed/builtin//modules/api/engine/multiplayer/multiplayer/commitOperation
multiplayer.commitOperation()
Finalize the current operation and push it onto the undo stack. Only changes that actually differ from the start state are recorded.
multiplayer.commitOperation()
typed/builtin//modules/api/engine/multiplayer/multiplayer/connect
multiplayer.connect(relayUrl: string)
Connect to a multiplayer relay server for the current world.
Uses the loaded world's world_id as the room prefix for scene
isolation. A world must be loaded before connecting.
Parameters
relayUrlstring— Relay server URL.
multiplayer.connect("https://relay.example.com")
typed/builtin//modules/api/engine/multiplayer/multiplayer/disconnect
multiplayer.disconnect()
Disconnect from the multiplayer relay server.
multiplayer.disconnect()
typed/builtin//modules/api/engine/multiplayer/multiplayer/explain
multiplayer.explain(entityId: (string | entityRef), componentType: string, property: string) -> DeliveryVerdict
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.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name, e.g. "Health".propertystring— Property name as written in the component'ssync {}block.
Returns DeliveryVerdict — arriving true when the property is on its way; otherwise reason names the cause and property carries the registry's record of it when one exists.
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getDiagnostics
multiplayer.getDiagnostics() -> SyncDiagnostics
Get sync diagnostics — traffic counts, bandwidth, link quality,
peer count. The counts — bytesSent/Received,
datagramsSent/Received, rpcsSent/Received, ownershipChanges,
refusals — 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. refusals 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 lastRefusal names the latest in the relay's
words once there is one. bytesSentPerSec / bytesReceivedPerSec are
averages over the last completed ~1 second window.
rttMs is the smoothed round-trip time to the relay and
packetLoss the fraction (0..1) of packets lost over the last 5
seconds; both read 0 until the transport has sampled a live
connection. messagesAwaitingEntity 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.
Returns SyncDiagnostics — Diagnostics table.
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")
typed/builtin//modules/api/engine/multiplayer/multiplayer/getPeerId
multiplayer.getPeerId() -> number?
Get the peer ID the relay issued this client's connection.
Returns number? — Local peer ID, or nil exactly when isConnected() is false: while dialing, before the relay names the connection, and after it drops.
typed/builtin//modules/api/engine/multiplayer/multiplayer/getPeers
multiplayer.getPeers() -> { PeerInfo }
Get a list of all connected peers in the current session.
Returns { PeerInfo } — Array of peer info tables.
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getRoomPeers
multiplayer.getRoomPeers(roomKey: string) -> { PeerInfo }
Get the peers this client shares the given room with, ordered by
peer id. getPeers 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 getPeers.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns { PeerInfo } — Array of peer info tables for that room.
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getRooms
multiplayer.getRooms() -> { string }
The relay rooms this client has joined, sorted. A broadcast reaches
only the peers that share one of these. They are the rooms of
observe() as of the last frame, read from the rooms the session
publishes every frame, so a component can ask every frame.
Returns { string } — Room keys.
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/getTickRate
multiplayer.getTickRate() -> number
Get the current sync tick rate (network updates per second).
Returns number — Sync ticks per second (default 20).
typed/builtin//modules/api/engine/multiplayer/multiplayer/heldMessages
multiplayer.heldMessages() -> { HeldMessage }
The sync messages this peer is holding for entities it has not
received — what getDiagnostics().messagesAwaitingEntity counts, one
entry each, with the entity sync id it names, its age and the grace it
is held against.
Returns { HeldMessage } — Held messages.
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/isConnected
multiplayer.isConnected() -> boolean
Check if a multiplayer session is connected to a relay and the
relay has named this client. True exactly when getPeerId() answers
an id.
Returns boolean — True if connected under the peer id the relay issued.
typed/builtin//modules/api/engine/multiplayer/multiplayer/isHost
multiplayer.isHost() -> boolean
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
entity.spawnSynced or a scene's onHostLoad) 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'.
Returns boolean — True on the host / offline / single-player; false on a confirmed joiner. Defaults to true when the role isn't known yet (degrade to host so single-player and pre-join code still run) — pair with a scene's onHostLoad hook when exact one-shot timing matters.
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
typed/builtin//modules/api/engine/multiplayer/multiplayer/isOperationOpen
multiplayer.isOperationOpen() -> boolean
Whether an operation is open: true between beginOperation and
its commitOperation or cancelOperation. A caller that records file
writes reads the file's prior contents only while one is open.
Returns boolean — True while an operation is open.
if multiplayer.isOperationOpen() then before = vfs.read(p) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/isOwner
multiplayer.isOwner(entityId: (string | entityRef)?) -> boolean
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.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to check (defaults toself.entityIdin component context).
Returns boolean — True if the local client is the owner.
typed/builtin//modules/api/engine/multiplayer/multiplayer/isRoomCreator
multiplayer.isRoomCreator(roomKey: string) -> boolean?
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.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns boolean? — True if this client created the room, false if it joined an existing one, nil if the relay hasn't reported a role yet (offline / not joined).
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/joinRoom
multiplayer.joinRoom(roomKey: string)
Join a relay room. Room keys are built as
{worldGuid}/{profile}/{mode}/{sceneGuid} — four segments, the
{profile} 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. getRooms() reports the
keys this client is already in and roomFor(entity) 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 getDiagnostics().refusals counts the refusal and
lastRefusal names it. No-op when not connected or already joined.
Parameters
roomKeystring— Fully-qualified room key.
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
typed/builtin//modules/api/engine/multiplayer/multiplayer/leaveRoom
multiplayer.leaveRoom(roomKey: string)
Leave a relay room. The key is reported under
observe().withdrawnRooms until joinRoom names it again. No-op
when not connected or not joined.
Parameters
roomKeystring— Fully-qualified room key.
typed/builtin//modules/api/engine/multiplayer/multiplayer/loopback
multiplayer.loopback() -> { [string]: any }
Loopback testing harness. Returns a table with enable(),
disable(), flush(), receive() methods for testing sync
without a relay server.
Returns { [string]: any } — Loopback API table.
typed/builtin//modules/api/engine/multiplayer/multiplayer/observe
multiplayer.observe() -> ReplicationObservation
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 notArriving: one
name from reasons, 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 entityNotSynced 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.
Returns ReplicationObservation — The engine's current replication observation.
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
typed/builtin//modules/api/engine/multiplayer/multiplayer/observeComponent
multiplayer.observeComponent(entityId: (string | entityRef), componentType: string) -> SyncedComponent?
The registered synced component of the named type on an entity's
record. Matches a fully-qualified type (@builtin::components.Model)
and the leaf name it ends in (Model) alike.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name or its leaf.
Returns SyncedComponent? — The registered component instance, or nil when none of that type is registered on the entity.
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
typed/builtin//modules/api/engine/multiplayer/multiplayer/observeEntity
multiplayer.observeEntity(entityId: (string | entityRef)) -> EntityReplication?
The replication record for one entity — its sync id, owner, room, and the synced components registered on it.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns EntityReplication? — The entity's record, or nil when the engine holds no sync record for it.
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/on
multiplayer.on(channel: string, callback: (number, ...any) -> ())
Subscribe to a custom message channel. The callback runs as
callback(fromPeerId, ...args) whenever another peer calls
multiplayer.send(channel, ...). Multiple callbacks per channel fire
in registration order.
Parameters
channelstring— Channel name to listen on.callback(number, ...any) -> ()—function(fromPeerId: number, ...)— the sender's peer id then the sent args.
typed/builtin//modules/api/engine/multiplayer/multiplayer/recordFileWrite
multiplayer.recordFileWrite(path: string, before: string?, after: string?) -> boolean
Record a file write into the open operation. Undoing the
operation writes before back through the VFS (removing the file when
before is nil), which runs the owning asset type's onChange as the
original write did; redoing writes after. The path is resolved and held
to the rules vfs.write applies, so a sidecar (.meta, .refs,
.pull) 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 before. Records nothing and returns false
when no operation is open.
Parameters
pathstring— VFS path of the written file.beforestring(optional) — The file's contents before the write, nil when it did not exist.afterstring(optional) — The file's contents after the write, nil when the write removed it.
Returns boolean — True when the write was recorded.
local before = vfs.read(p); vfs.write(p, body); multiplayer.recordFileWrite(p, before, body)
typed/builtin//modules/api/engine/multiplayer/multiplayer/recordSpawn
multiplayer.recordSpawn(entityId: string)
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.
Parameters
entityIdstring— Entity id to record as spawned by this operation.
multiplayer.recordSpawn(id)
typed/builtin//modules/api/engine/multiplayer/multiplayer/redo
multiplayer.redo() -> boolean
Redo this client's last undone operation.
Returns boolean — True if an operation was redone.
typed/builtin//modules/api/engine/multiplayer/multiplayer/releaseOwnership
multiplayer.releaseOwnership(entityId: (string | entityRef)?) -> boolean
Release ownership of an entity, leaving it unowned so another peer's claim takes it. Only the owner releases an entity.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to release.
Returns boolean — True if ownership was released.
typed/builtin//modules/api/engine/multiplayer/multiplayer/roomFor
multiplayer.roomFor(entityId: (string | entityRef)) -> string?
The room key an entity's spawns and property deltas broadcast into:
the room of its observeEntity record as of the last frame, read from
the rooms the session publishes every frame, so a component can ask
every frame.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns string? — The room key, or nil when the engine has established no scene context for the entity.
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
typed/builtin//modules/api/engine/multiplayer/multiplayer/send
multiplayer.send(channel: string, ...: any?)
Broadcast a message on a named channel to every OTHER peer in the
room. The relay forwards it transparently; peers receive it via
multiplayer.on. 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:
nil, a boolean, a number and a string arrive as themselves.- A
vectorarrives as avector. - 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.
- A synced component (its proxy, or the
componentRefenvelope naming it) arrives as that component on the receiving peer; the proxy of a component that holds no sync id, as the envelope{ __ref, entity, componentType }naming it. - A transform vector an entity hands out (
e.position,e.rotation,e.localScale, ...) arrives as the table of its components,{ x, y, z }or{ x, y, z, w }. - Every other table is data and arrives as a new table, each value
carried by these same rules:
{ x = 5, kind = "coin" }arrives as{ x = 5, kind = "coin" }, and{ who = e, n = 1 }witht.whothe receiver's proxy fore. A table whose length#tis above 0 arrives as the list of its values at1..#talone; any other arrives keyed by its string keys and by each number key as the string of its whole part ({ [5] = "a" }arrives as{ ["5"] = "a" }). - A value no peer can hold (a function, a thread, a buffer) arrives as
nil.
The same rules carry the arguments of a
declare.syncedFunctionsmethod, the payload of aSyncevent and a synced table-valued field. No-op when not connected.
Parameters
channelstring— Channel name listeners subscribe to viamultiplayer.on....any(optional) — Zero or more values delivered to each listener after the sender's peer id.
typed/builtin//modules/api/engine/multiplayer/multiplayer/syncTotals
multiplayer.syncTotals() -> SyncTotals
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.
Returns SyncTotals — The registry totals.
print(multiplayer.syncTotals().properties .. " synced properties registered")
typed/builtin//modules/api/engine/multiplayer/multiplayer/undo
multiplayer.undo() -> boolean
Undo this client's last edit-mode operation.
Returns boolean — True if an operation was undone.
typed/builtin//modules/api/engine/notices/notices/post
notices.post(template: string, params: { [string]: any }?, opts: NoticeOpts?)
Post a notice. template is a fixed sentence used to collapse
repeats; put varying values in params. A param holding a picture — an
image value from mcp.image or a capture tool — is delivered as a
picture beside the notice's line rather than as text. opts.severity
defaults to "info"; opts.includeLocation attaches the emitting call
site. opts.actionable 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.
Parameters
templatestring— Fixed sentence identifying the notice.params{ [string]: any }(optional) — Optional table of named values rendered alongside the template; a picture in it arrives as a picture.optsNoticeOpts(optional) — Optional table: severity ("info" | "warn" | "error"), includeLocation (boolean), audience ("" | { string }?). Who sees the notice: "" addresses every caller; a list addresses those callers only, each entry an agent's name (as theagenttoolbox'srosterlists it) or a caller key<kind>:<id>such as "agent:". 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. actionable(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.
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 })
typed/builtin//modules/api/engine/notices/notices/withdraw
notices.withdraw(template: string) -> number
Withdraw the notices this code posted under template 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 notices.post under the template queues again.
Parameters
templatestring— The template the notices were posted under.
Returns number — How many emissions the withdrawn notices held.
notices.withdraw("save slot corrupted, using defaults")
typed/builtin//modules/api/engine/packages/packages/list
packages.list() -> { PackageEntry }
List every registered package across scopes.
Returns { PackageEntry } — Array of package entries.
for _, p in ipairs(packages.list()) do print(p.name, p.scope) end
typed/builtin//modules/api/engine/packages/packages/lookup
packages.lookup(name_or_scope: string, name: string?) -> PackageEntry?
Look up a single package by name (any scope) or by exact
(scope, name).
Parameters
name_or_scopestring— Package name, or scope if a second arg is given.namestring(optional) — Package name when the first arg is a scope.
Returns PackageEntry? — Package entry or nil.
local p = packages.lookup("@builtin", "audio")
typed/builtin//modules/api/engine/physics/P/addCollider
P.addCollider(entityId: string | entityRef, component: string, config: table?)
Add a collider component to an entity, naming the shape you want.
Parameters
entityIdstring | entityRef— Target entity id.componentstring— One ofPhysics.COLLIDER_COMPONENTS.configtable(optional) — The component's own fields, e.g.{ radius = 0.5 }for a sphere.
Physics.addCollider(id, "SphereCollider", { radius = 0.5 })
typed/builtin//modules/api/engine/physics/P/addConstraint
P.addConstraint(entityId: string | entityRef, opts: table?)
Add a transform constraint to an entity.
Parameters
entityIdstring | entityRef— Target entity id.optstable(optional) — Optional constraint description (targetEntityId, position, rotation, scale, lookAt, targetPosition, axes, weight).
Physics.addConstraint(id, { targetEntityId = parent, position = true })
typed/builtin//modules/api/engine/physics/P/addJoint
P.addJoint(entityIdA: string | entityRef, entityIdB: string | entityRef, opts: table?)
Add a Joint component connecting two entities. Accepts either
vec3-style anchor inputs (localAnchor = {x,y,z}) or pre-split
scalar keys (localAnchorX/Y/Z).
Parameters
entityIdAstring | entityRef— Entity that hosts the Joint component.entityIdBstring | entityRef— Connected entity.optstable(optional) — Optional joint description (kind, anchors, axis, stiffness, damping, restLength, maxDistance, breakForce, breakTorque, region, remoteRegion). Either end may be a soft body (an entity with aSoftBodycomponent):regionanchors this end on a region of entity A's soft body andremoteRegionthe other end on a region of B's, each asPhysics.softRegiontakes it ({ box = { min, max } }or{ nearest = point }); an end with no region is anchored on the soft body as a whole. The anchor of a region end is measured from the centre of its particles.
Physics.addJoint(a, b, { kind = "fixed" })
Physics.addJoint(a, b, { kind = "hinge", axis = {x=0,y=1,z=0} })
Physics.addJoint(a, b, { kind = "rope", maxDistance = 8 })
Physics.addJoint(a, b, { kind = "fixed", breakForce = 1200, breakTorque = 800 })
Physics.addJoint(bag, hook, { kind = "ball", region = { nearest = {0, 0.5, 0} } })
Physics.addJoint(jellyA, jellyB, { kind = "fixed", region = { box = { min = {0.4, -0.5, -0.5}, max = {0.5, 0.5, 0.5} } }, remoteRegion = { box = { min = {-0.5, -0.5, -0.5}, max = {-0.4, 0.5, 0.5} } } })
typed/builtin//modules/api/engine/physics/P/addVelocity
P.addVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Add to the linear velocity of an entity. Same call shapes as
setVelocity.
Parameters
astring | entityRef | number | vec3— dx, a{x, y, z}delta vector, or an entity id (explicit target).b(number | vec3)(optional) — dy, dx, or the delta vector depending on call form.cnumber(optional) — dz or dy depending on call form.dnumber(optional) — Optional dz when targeting an explicit entity.
Physics.addVelocity(0, 5, 0)
Physics.addVelocity(entityId, 0, 5, 0)
Physics.addVelocity(entityId, {x=0, y=5, z=0})
typed/builtin//modules/api/engine/physics/P/addWheelCollider
P.addWheelCollider(entityId: string | entityRef, config: table?)
Add a WheelCollider to an entity. The entity must be a child (or descendant) of a rigid body — the system walks up the hierarchy to find the Physics component. Every wheel under one body is a wheel of that body's ray-cast vehicle.
Parameters
entityIdstring | entityRef— Target entity id.configtable(optional) — Optional wheel configuration (radius?,suspensionDistance?,springRate?,damperRate?,targetPosition?,maxSuspensionForce?,motorTorque?,brakeTorque?,steerAngle?,forwardFriction?,sidewaysFriction?,frictionSlip?,sideFrictionStiffness?,rollingResistance?,width?,is2D?,visual?).
Physics.addWheelCollider(id, { radius = 0.35, motorTorque = 500 })
typed/builtin//modules/api/engine/physics/P/applyForce
P.applyForce(entityIdOrForce: string | entityRef | vec3, force: vec3?, region: PhysicsSoftRegion?)
Apply a force to an entity's rigid or soft body for the next physics
step — call every frame for continuous thrust. With one argument the
script-context entity is targeted; with two args the explicit entity
id wins. A soft body takes the force spread over its particles by mass,
so it accelerates as a whole the way a rigid body of its mass would;
region puts it on part of the body instead ({ box = { min, max } }
or { nearest = point }, as Physics.softRegion takes it), which
accelerates that part alike and leaves the rest to follow.
Parameters
entityIdOrForcestring | entityRef | vec3— Entity id (when paired withforce) OR a force vector for the script-context entity.forcevec3(optional) — Optional force vector when targeting an explicit entity.regionPhysicsSoftRegion(optional) — Part of a soft body to push, asPhysics.softRegiontakes it; omit for the whole body.
Physics.applyForce({x=0, y=10, z=0})
Physics.applyForce(entityId, {x=0, y=10, z=0})
Physics.applyForce(cloth, {x=0, y=0, z=4}, { box = { min = {-0.5, -1, 0.4}, max = {0.5, 1, 0.5} } })
typed/builtin//modules/api/engine/physics/P/applyForceAtPoint
P.applyForceAtPoint(entityId: string | entityRef, force: vec3, point: vec3, opts: PhysicsPushOptions?)
Apply a force at a specific world-space point for the next physics
step. On a rigid body it generates the matching torque from the lever
arm. On a soft body it pushes the particles around the point, weighted
from the whole at the point to nothing at opts.radius (by default
twice the spacing of the body's particles), so the body gives way there
first. It always reaches at least the four particles nearest the point,
the smallest patch a body can dent, so a radius finer than the particles
are spaced spreads over that patch.
Parameters
entityIdstring | entityRef— Target entity id.forcevec3— Force vector, in newtons.pointvec3— World-space application point.optsPhysicsPushOptions(optional) —{ radius: number? }: how far into a soft body the force reaches, in metres.
Physics.applyForceAtPoint(id, {x=0,y=10,z=0}, {x=1,y=0,z=0})
Physics.applyForceAtPoint(jelly, {x=0,y=-40,z=0}, {x=0,y=1.5,z=0}, { radius = 0.3 })
typed/builtin//modules/api/engine/physics/P/applyImpulse
P.applyImpulse(entityIdOrImpulse: string | entityRef | vec3, impulse: vec3?, region: PhysicsSoftRegion?)
Apply an instantaneous impulse (one-shot velocity change). With
one argument the script-context entity is targeted; with two args
the explicit entity id wins. A soft body takes the impulse spread over
its particles by mass, so it moves off as a whole the way a rigid body
of its mass would; region puts it on part of the body instead (as
Physics.softRegion takes it), which sets that part moving and leaves
the rest to follow. applyImpulseAtPoint aims one at a point.
Parameters
entityIdOrImpulsestring | entityRef | vec3— Entity id (withimpulse) OR an impulse vector for the script-context entity.impulsevec3(optional) — Optional impulse vector when targeting an explicit entity.regionPhysicsSoftRegion(optional) — Part of a soft body to push, asPhysics.softRegiontakes it; omit for the whole body.
Physics.applyImpulse({x=0, y=5, z=0})
Physics.applyImpulse(entityId, {x=0, y=5, z=0})
Physics.applyImpulse(jelly, {x=0, y=-2, z=0}, { box = { min = {-0.5, 0.4, -0.5}, max = {0.5, 0.5, 0.5} } })
typed/builtin//modules/api/engine/physics/P/applyImpulseAtPoint
P.applyImpulseAtPoint(entityId: string | entityRef, impulse: vec3, point: vec3, opts: PhysicsPushOptions?)
Apply an instantaneous impulse at a world-space point: a hit, a kick,
a crash. On a rigid body it changes the body's velocity and spin as an
impulse at that point of it would. On a soft body it pushes the particles
around the point, weighted from the whole at the point to nothing at
opts.radius (by default twice the spacing of the body's particles), so
the body dents there and the rest of it is carried along through its
elastic elements. It always reaches at least the four particles nearest
the point, the smallest patch a body can dent, so a radius finer than
the particles are spaced spreads over that patch; a finer dent takes a
finer body (a higher resolution). The impulse is the momentum the body
gains, in newton-seconds, wherever it lands.
Parameters
entityIdstring | entityRef— The body's entity id or proxy.impulsevec3— The impulse, in newton-seconds.pointvec3— Where it lands, in world space: a contact point.optsPhysicsPushOptions(optional) —{ radius: number? }: how far into a soft body the impulse reaches, in metres.
Physics.applyImpulseAtPoint(crate, {x=0, y=0, z=-30}, hit.point)
local contact = Physics.contacts(shell)[1]; if contact then Physics.applyImpulseAtPoint(shell, {x=-60, y=0, z=0}, contact.points[1].point, { radius = 0.4 }) end
typed/builtin//modules/api/engine/physics/P/applyRadialImpulse
P.applyRadialImpulse(entityId: string | entityRef, center: vec3, magnitude: number, opts: PhysicsPushOptions?)
Push a body straight away from a world point, like a blast: the
push is magnitude newton-seconds at the centre, weighted to nothing at
opts.radius (omitted, it reaches any distance at full strength). A
rigid body is pushed by the distance to its centre of mass; a soft body
particle by particle, each away from the centre by its own distance, so
a blast beside a balloon pushes it off and one inside a body spreads it
out. Each moves off at the speed magnitude gives a body of its mass.
Parameters
entityIdstring | entityRef— The body's entity id or proxy.centervec3— The blast's world position.magnitudenumber— The impulse at the centre, in newton-seconds.optsPhysicsPushOptions(optional) —{ radius: number? }: how far the push reaches, in metres.
Physics.applyRadialImpulse(balloon, {x=0, y=1, z=-1}, 8, { radius = 3 })
for _, id in pile do Physics.applyRadialImpulse(id, blast, 20, { radius = 4 }) end
typed/builtin//modules/api/engine/physics/P/applyTorque
P.applyTorque(entityIdOrTorque: string | entityRef | vec3, torque: vec3?)
Apply a torque to an entity's rigid body for the next physics step — call every frame for continuous spin-up. With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrTorquestring | entityRef | vec3— Entity id (withtorque) OR a torque vector for the script-context entity.torquevec3(optional) — Optional torque vector when targeting an explicit entity.
Physics.applyTorque({x=0, y=1, z=0})
Physics.applyTorque(entityId, {x=0, y=1, z=0})
typed/builtin//modules/api/engine/physics/P/attachSoftRegion
P.attachSoftRegion(softId: string | entityRef, targetId: string | entityRef, region: PhysicsSoftRegion?) -> number
Hold a region of a soft body to a rigid body, two-way: the region
follows the rigid body wherever it goes, and the rigid body carries the
region's weight and pull. Each particle in the region is tied to the
point of the rigid body it stands at when the attachment is made. The
attachment is an entry of the soft body's SoftBody component
(attachments), so it is saved with the scene, reaches every peer, and is
made again on a rebuilt body; it is released when either side is
removed. Its load reads back from Physics.softBodyState(softId).attachments.
A cloth's edge on a flagpole, a soft panel on a car's chassis, a jelly
glued to a moving platform. Where the rigid body's colliders touch the
soft body (a shell resting on its chassis), the contact and the hold push
on each other: keep them clear, or Physics.ignoreCollision(softId, targetId).
Parameters
softIdstring | entityRef— The soft body's entity (it carries aSoftBodycomponent).targetIdstring | entityRef— The rigid body's entity (it carriesPhysics), by id, name or proxy.regionPhysicsSoftRegion(optional) — Which particles to hold, asPhysics.softRegiontakes it:{ box = { min, max } },{ nearest = point }, withspace = "world"for world coordinates; nil holds the whole body.
Returns number — The attachment's index in the component's attachments.
Physics.attachSoftRegion(flag, pole, { box = { min = {-0.5, -1, -1}, max = {-0.45, 1, 1} } })
Physics.attachSoftRegion(shell, chassis, { box = { min = {-0.5, -0.5, -0.5}, max = {0.5, -0.4, 0.5} } })
typed/builtin//modules/api/engine/physics/P/bodyState
P.bodyState(entityId: string | entityRef) -> PhysicsBodyState?
Everything the solver holds for one body — its type, mass, centre of mass, inertia, gravity scale, damping, lock flags, CCD, collision groups, sleep state, velocities, the force and torque queued for the next step, its colliders, contacts, joints and transform constraints, and why it is not moving.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns PhysicsBodyState? — A PhysicsBodyState (with exists = false for an entity that carries no rigid body, and waitingFor naming what its collider needs while that collider cannot be built), or nil when nothing in the scene answers to that id.
local b = Physics.bodyState(id); print(b.bodyType, b.mass, b.stillness)
local b = Physics.bodyState(id); if not b.exists then print(b.waitingFor or "no body was built") end
typed/builtin//modules/api/engine/physics/P/boxCast
P.boxCast(origin: vec3, halfExtents: vec3, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a box along a direction and return the first hit.
A trigger (isTrigger) holds nothing up, so the sweep passes through it.
Parameters
originvec3— Box center at the start of the cast.halfExtentsvec3— Half the size of the box on each axis.directionvec3— Cast direction, of any length: the engine normalises it, somaxDistanceanddistanceare world units.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside, softBody? }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. softBody is set when what was hit is a soft body, whose surface the query meets as it is deformed now: a PhysicsSoftHit naming the surface triangle, the particles carrying it with the hit's weight on each, the particle nearest the hit, and region, the hit as a soft region to hand to Physics.applyImpulse, Physics.pinSoftRegion or Physics.softParticles. Physics.applyImpulseAtPoint takes the hit's point as it is.
local hit = Physics.boxCast(o, {x=0.5,y=0.5,z=0.5}, dir, 10)
local hit = Physics.boxCast(o, {x=0.5,y=0.5,z=0.5}, dir, 10, { selfId, carriedId })
typed/builtin//modules/api/engine/physics/P/capsuleCast
P.capsuleCast(origin: vec3, radius: number, halfHeight: number, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast an upright capsule along a direction and return the first hit.
This is the sweep that answers whether a body of that shape fits through
a passage: a capsule of radius r reports a hit on anything that leaves
it less than 2 * r of clearance.
A trigger (isTrigger) holds nothing up, so the sweep passes through it.
Parameters
originvec3— Capsule centre at the start of the cast.radiusnumber— Capsule radius.halfHeightnumber— Distance from the centre to either cap centre. The capsule standshalfHeight + radiustall in each direction.directionvec3— Cast direction, of any length: the engine normalises it, somaxDistanceanddistanceare world units.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside, softBody? }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. softBody is set when what was hit is a soft body, whose surface the query meets as it is deformed now: a PhysicsSoftHit naming the surface triangle, the particles carrying it with the hit's weight on each, the particle nearest the hit, and region, the hit as a soft region to hand to Physics.applyImpulse, Physics.pinSoftRegion or Physics.softParticles. Physics.applyImpulseAtPoint takes the hit's point as it is.
local hit = Physics.capsuleCast(o, 0.3, 0.6, dir, 0.5)
local hit = Physics.capsuleCast(o, 0.3, 0.6, dir, 0.5, selfId)
typed/builtin//modules/api/engine/physics/P/colliderCount
P.colliderCount() -> number
How many colliders the physics world holds. Zero means no ray, cast or overlap fired into this world can hit anything, so it is what separates a query that MISSED from a query fired into a world that holds nothing to hit. Read off the collider set itself, so it costs the same whatever the world holds.
Returns number — colliders across the whole physics world.
if Physics.colliderCount() == 0 then print("nothing here is solid") end
typed/builtin//modules/api/engine/physics/P/colliderGeometry
P.colliderGeometry(options: table?) -> table?
Read the physics world as drawable triangles: every collider triangulated in world space into one indexed mesh, in GPU buffers ready to draw.
Box, sphere, capsule, cylinder, cone, convex, triangle-mesh and heightfield
colliders return their real surface, and a compound returns its children
folded together; a shape with no triangulation returns its bounding box and
reports exact = false.
options.colors is POSITIONAL over colliderManifest() — entry i colours
collider i — so you can colour by role, shape, entity or anything else you
read there. A position you leave out takes options.defaultColor.
The returned buffers are yours: destroy them when you replace them.
Parameters
optionstable(optional) —{ tessellation = "low"|"medium"|"high", colors = { {r,g,b,a}, ... }, defaultColor = {r,g,b,a} }.
Returns table? — { vertices, indices, vertexCount, indexCount, colliders } where each entry of colliders is { entity, colliderName?, shapeType, role, exact, firstIndex, indexCount }.
local geo = Physics.colliderGeometry({ tessellation = "high" })
typed/builtin//modules/api/engine/physics/P/colliderManifest
P.colliderManifest() -> table
List every physics collider in the world with what it is and what it takes part in — no geometry, so it is the cheap read to make before deciding what to do with each one.
role is one of static, dynamic, kinematic, sensor. A sensor
is a collider the simulation holds as one, reported ahead of the body type
behind it, and a collider with no rigid body is static. exact says whether colliderGeometry would return this
collider's true surface or its bounding box.
Every collider of one entity shares its entity, so this is what to key
per-object decisions on. The order is stable across calls over an unchanged
world, which is what makes colliderGeometry's positional colours usable.
Returns table — Array of { entity, colliderName?, shapeType, role, exact }.
for _, c in ipairs(Physics.colliderManifest()) do print(c.entity, c.role) end
typed/builtin//modules/api/engine/physics/P/colliderOn
P.colliderOn(entityId: string | entityRef) -> string?
Which collider component an entity carries, or nil when it carries none.
Parameters
entityIdstring | entityRef— Target entity id.
Returns string? The component name, e.g. "SphereCollider".
local which = Physics.colliderOn(id)
typed/builtin//modules/api/engine/physics/P/colliderShapes
P.colliderShapes(entityId: string | entityRef) -> table
Read an entity's resolved physics collider shape(s) as the physics engine sees them, including auto-sized colliders.
shapeType is one of box, sphere, capsule, convex, mesh,
heightfield, compound, other — the shape the simulation is
running, so a mesh collider reads mesh.
params carries half-extents for a box, radius for a sphere, radius
and half-height for a capsule, and the collider's bounding half-extents
for the shapes that have no parametric description. A convex collider
reports its outline in linePoints instead.
Parameters
entityIdstring | entityRef— Target entity id.
Returns table — Array of resolved collider shapes (empty if none): { shapeType, position, rotation, params, linePoints, name? }.
local shapes = Physics.colliderShapes(id)
typed/builtin//modules/api/engine/physics/P/contacts
P.contacts(entityId: string | entityRef, otherId: (string | entityRef)?) -> { PhysicsContact }
Every contact one body's colliders are in right now, with the other
entity, the normal, how deeply the two interpenetrate, the impulse the
last step applied, and each contact point. A pair is listed while its
surfaces touch: a pair kept apart (Physics.ignoreCollision, collision
groups) or no longer touching is not. For a soft body these are the
contacts of its surface with every other collider, another soft body's
surface included. A contact with a soft body on either side names the
particles of it that are touching (particles for this side,
otherParticles for the other), so a hit can be answered where it
landed. Name otherId to read only the contacts with that one entity:
a body resting on the ground holds a contact point per surface element
that touches it, and a read of one pair costs that pair alone.
Parameters
entityIdstring | entityRef— Entity id or proxy.otherId(string | entityRef)(optional) — Optional entity id or proxy: report only the contacts with it.
Returns { PhysicsContact } — An array of PhysicsContact — empty when the body touches nothing, or when the entity carries neither a rigid body nor a soft body.
for _, c in Physics.contacts(id) do print(c.other, c.deepestPenetration) end
for _, c in Physics.contacts(truck, pole) do if c.totalImpulse > 5 then print("dented at", #c.particles, "particles") end end
typed/builtin//modules/api/engine/physics/P/cutSoftBody
P.cutSoftBody(softId: string | entityRef, blade: { vec3 } | { point: vec3, normal: vec3 })
Cut a soft body along a blade, every element of the body the blade
meets opening along it: { point = p, normal = n } cuts along the whole
plane through p facing n, splitting the body in two wherever it lies on
both sides; { a, b, c } cuts along that world-space triangle, which cuts
only as far as the triangle reaches. Applied straight after the next physics
step; a piece the cut separates becomes an entity of its own (see
onSoftBodyTear), and the cut reports as a tear of kind "cut". A plane is
measured against the body where that step leaves it, so it cuts a moving
body wherever it has gone; a cut that opens nothing is reported in the log.
Raises when the entity carries no SoftBody, and in edit mode, where
nothing simulates.
Parameters
softIdstring | entityRef— The soft body's entity.blade{ vec3 } | { point: vec3, normal: vec3 }—{ point = vec3, normal = vec3 }for a plane, or the triangle's three world-space corners{ a, b, c }.
Physics.cutSoftBody(jelly, { point = vector.create(0, 1, 0), normal = vector.create(1, 0, 0) }) -- in two along x = 0
Physics.cutSoftBody(jelly, { vector.create(0, -5, -5), vector.create(0, 5, -5), vector.create(0, 0, 10) })
-- slice off the part of a body 0.2 m above its centre
local c = Physics.softBodyState(jelly).centerOfMass
Physics.cutSoftBody(jelly, { point = vector.create(c.x, c.y + 0.2, c.z), normal = vector.create(0, 1, 0) })
typed/builtin//modules/api/engine/physics/P/debugLines
P.debugLines(opts: table?) -> (PhysicsDebugLines?, string?)
Draw what the physics world simulates beyond collider surfaces as
world-space lines, with what each object's lines picture as data. This is
the read the physics debug views draw from: capture { pass = "physics" }
and the edit-mode collider overlay (debugViz category colliders) both
show these lines.
Layers:
cage draws a soft body's elements coloured by load: slack blue, yellow
half way, red at the load it tears (see PhysicsSoftBodyDiagnostics for
how a load is read);
particles draws its particles: free ones as small grey crosses, pinned
ones as green octahedra, ones held to a rigid body as magenta octahedra;
contacts draws the contacts soft surfaces hold against soft surfaces
(their own included): the segment joining the two witness points in
orange, the normal in cyan;
wheels draws each WheelCollider wheel: its suspension ray as far as
it reaches, its whole travel and the tyre's radius below the mount (green
on ground, red in the air, as the wheel's isGrounded reads), the travel
from its mount to the tyre's centre coloured by how much of it is taken
up on the same ramp, its tyre circle in white across its steered axle,
and its ground point (yellow) and normal (cyan).
Answers in edit mode as well as play mode; in edit mode a vehicle stands
at rest with its wheels where they are authored.
Parameters
optstable(optional) —{ layers: {string}?, entities: {(string | entityRef)}? }:layersnames the layers to draw (every layer when omitted; an unknown name is an error naming the layers),entitieskeeps to those entities' objects (a wheel is kept by its own entity or its chassis').
Returns (PhysicsDebugLines?, string?) — A PhysicsDebugLines, or nil and the reason on a VM that holds no physics world (a worker).
local d = Physics.debugLines(); print(d.lineCount, #d.objects)
for _, o in Physics.debugLines({ layers = { "particles" } }).objects do print(o.entity, o.pinned, o.attached) end
local w = Physics.debugLines({ layers = { "wheels" }, entities = { truck } }); for _, o in w.objects do print(o.index, o.grounded, o.compression) end
typed/builtin//modules/api/engine/physics/P/detachSoftRegion
P.detachSoftRegion(softId: string | entityRef, targetId: (string | entityRef)?) -> number
Release regions of a soft body held with attachSoftRegion (or
authored in its SoftBody component's attachments).
Parameters
softIdstring | entityRef— The soft body's entity.targetId(string | entityRef)(optional) — Release only the regions held to this rigid body; omit to release every one.
Returns number — How many attachments were released.
Physics.detachSoftRegion(flag, pole)
Physics.detachSoftRegion(shell)
typed/builtin//modules/api/engine/physics/P/drawnPose
P.drawnPose(entityId: string | entityRef) -> PhysicsDrawnPose?
Where an entity is drawn this frame, and where its transform puts
it. The simulation advances in fixed ticks (physics.tick_rate, 30 a
second by default) while frames are drawn at the display's rate, so a
body with Physics.interpolation on is drawn blended between its last two
ticks, and everything parented under it is drawn moved with it.
entity.position reports the simulated pose, which moves only when a
tick runs; this read reports the drawn one as well. Read it from a
component's update to place anything that has to stay with a moving
body on screen: a camera following it, a label over it, a measurement of
how smoothly it moves.
Parameters
entityIdstring | entityRef— Entity id or proxy. Any entity answers; one the simulation does not blend is drawn at its simulated pose.
Returns PhysicsDrawnPose? — A PhysicsDrawnPose, or nil when nothing in the scene answers to that id.
local d = Physics.drawnPose(car); print(d.position.z, d.simulatedPosition.z, d.interpolated)
local d = Physics.drawnPose(tyre); print(d.interpolated, d.interpolatedBy)
typed/builtin//modules/api/engine/physics/P/getAngularVelocity
P.getAngularVelocity(entityId: (string | entityRef)?) -> vec3?
Read the angular velocity of an entity's rigid body.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns vec3? — Angular velocity in rad/s, or nil if the entity has no rigid body.
local w = Physics.getAngularVelocity(id)
typed/builtin//modules/api/engine/physics/P/getGravity
P.getGravity() -> vec3
Read the current world gravity vector.
Returns vec3 — Gravity vector in m/s² (negative y is "down" in the default world).
local g = Physics.getGravity()
typed/builtin//modules/api/engine/physics/P/getVelocity
P.getVelocity(entityId: (string | entityRef)?) -> vec3?
Read the linear velocity of an entity's rigid body, or of its soft body as a whole (its particles' velocities averaged by mass).
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns vec3? — Velocity in m/s, or nil if the entity has no rigid body.
local v = Physics.getVelocity(id)
typed/builtin//modules/api/engine/physics/P/getWheelState
P.getWheelState(entityId: string | entityRef) -> table?
Read a wheel collider's runtime state. Reads the native component the wheel system writes after each physics step.
Parameters
entityIdstring | entityRef— Target entity id (must carry a WheelCollider component).
Returns table? — { isGrounded, compression, angularVelocity, rotation, skid, sideImpulse, forwardImpulse, suspensionForce, stopImpulse, center, hitPoint, hitNormal }, or nil if the component is absent. compression runs from 0 at the spring's rest up to suspensionDistance, where the bump stop is; rotation is the wheel's angle about its axle in radians, growing as it rolls forward; skid is how fast the tyre slides sideways (m/s) while its grip is used up, 0 while it holds; the impulses are what the tyre applied to the chassis this tick (N·s), and stopImpulse what the bump stop applied once the suspension was used up; center is the wheel centre in world space.
local state = Physics.getWheelState(id)
typed/builtin//modules/api/engine/physics/P/hasLineOfSight
P.hasLineOfSight(fromId: string, toId: string) -> boolean
Check whether two entities have line-of-sight between their origins.
Parameters
fromIdstring— Viewer entity id.toIdstring— Target entity id.
Returns boolean — true when no collider sits between them (including coincident origins), false otherwise.
if Physics.hasLineOfSight(a, b) then ... end
typed/builtin//modules/api/engine/physics/P/ignoreCollision
P.ignoreCollision(entityIdA: string | entityRef, entityIdB: string | entityRef, ignore: boolean?)
Keep two entities from colliding, or let them collide again. Covers
every collider each one owns (its rigid body with the shapes composed
onto it, its standalone collider, its ragdoll's bones, its soft body's
surface), so it keeps a soft shell off the chassis it is held to as well
as two rigid bodies apart. An ignored pair holds no contact: the solver
pushes neither one and Physics.contacts lists neither to the other.
It applies from the next physics step, to bodies already touching as
well, and lasts until it is turned off, either entity is despawned, or
the mode changes: entering or leaving play starts without it, so make
the call where the bodies are set up for play (a component's onLoad).
It stands beside collision groups: a pair either one excludes does not
collide.
Parameters
entityIdAstring | entityRef— First entity id.entityIdBstring | entityRef— Second entity id.ignoreboolean(optional) — Whentrue(default) the pair is kept apart;falselets it collide again.
Physics.ignoreCollision(a, b, true)
typed/builtin//modules/api/engine/physics/P/isSleeping
P.isSleeping(entityId: (string | entityRef)?) -> boolean?
Whether an entity's rigid or soft body is currently asleep (at rest and not simulating). A body sleeps once it stops moving, to save simulation cost.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns boolean? — true if asleep, false if awake, or nil if the entity has neither a rigid nor a soft body.
if Physics.isSleeping(id) then Physics.wakeUp(id) end
typed/builtin//modules/api/engine/physics/P/jointBreaks
P.jointBreaks() -> table
Every joint that has broken since the last call to this function.
A joint breaks when the reaction it carries exceeds the breakForce
(newtons of linear reaction) or breakTorque (the angular row of the same
reaction) its joint was given; each joint
reports once and its constraint is already released when the record
arrives. The 256 most recent are kept: a structure that comes apart while
nothing reads them drops the oldest beyond that, as the engine's own queue
does beyond 1024.
Returns table — Array of { entityId, connectedEntityId, kind, impulse, angularImpulse, force, torque, position }, oldest first.
for _, e in ipairs(Physics.jointBreaks()) do print(e.entityId, e.force) end
typed/builtin//modules/api/engine/physics/P/jointReaction
P.jointReaction(entityId: string | entityRef) -> table?
The load an entity's joint is carrying right now, as the constraint
solver resolved it on the last physics step. This is the same quantity a
break threshold is measured against, so it is what to size breakForce
and breakTorque from. A joint with a soft body at either end reads the
same way.
Parameters
entityIdstring | entityRef— Entity carrying the Joint component.
Returns table? — { impulse, angularImpulse, force, torque, position }, or nil when the entity owns no joint.
local r = Physics.jointReaction(id); print(r and r.force)
typed/builtin//modules/api/engine/physics/P/observe
P.observe(entityId: (string | entityRef)?, opts: table?) -> PhysicsObservation?
Read the solver's own state — the world's accounting, and what it
holds for each body plus why it is not moving one. Every value comes off
the simulation rather than the Physics component, so a write the solver
refused or clamped reads back as what it kept. Answers in edit mode as
well as play mode.
Parameters
entityId(string | entityRef)(optional) — Report on this one entity. Omit for every body in the world.optstable(optional) —{ bodies: boolean?, contactPoints: boolean? }—bodies = falsebuilds the world accounting alone, andcontactPoints = falsekeeps each contact pair's normal, depth, impulse and point count while leaving out the individual points. Both default to true.
Returns PhysicsObservation? — A PhysicsObservation, or nil when entityId names nothing in the scene. bodies is an array, not a table keyed by entity id — each entry names its own entity in entity.
local o = Physics.observe(); for _, b in o.bodies do print(b.entity, b.stillness) end
local o = Physics.observe(id); print(o.bodies[1].stillness, o.bodies[1].stillnessDetail)
typed/builtin//modules/api/engine/physics/P/onJointBreak
P.onJointBreak(fn: (table) -> ()) -> () -> ()
Call fn for every joint that breaks from now on, with the same record
jointBreaks returns.
Parameters
fn(table) -> ()— Receives one break record per broken joint.
Returns () -> () — A function that removes this listener.
local off = Physics.onJointBreak(function(e) print(e.kind, e.force, e.position) end)
typed/builtin//modules/api/engine/physics/P/onSoftBodyTear
P.onSoftBodyTear(fn: (PhysicsSoftBodyTear) -> ()) -> () -> ()
Call fn for every tear of a soft body from now on: a body loaded past
its tearStrain or tearForce (fields of its SoftBody component), cut
by Physics.cutSoftBody or torn by Physics.tearSoftBody. The torn entity
keeps the largest piece; every piece split off is an entity of its own by
the time fn runs, standing at the root of the scene and carrying a
SoftBody with the torn body's fields, its share of the model and the
material, the velocity, dents and pins that fell on its side. Its body
joins it on the next frame, and an impulse applied to it in fn reaches
the body then. A piece is play state: it is left out of every save, and
leaving play takes it away and gives the torn entity back its whole body.
Parameters
fn(PhysicsSoftBodyTear) -> ()— Receives onePhysicsSoftBodyTearper tear:{ entity, pieces, position, kind, particles }.
Returns () -> () — A function that removes this listener.
local off = Physics.onSoftBodyTear(function(e) print(e.entity, #e.pieces, e.position) end)
Physics.onSoftBodyTear(function(e) for _, id in e.pieces do Physics.applyImpulse(id, { x = 0, y = 2, z = 0 }) end end)
typed/builtin//modules/api/engine/physics/P/overlapSphere
P.overlapSphere(center: vec3, radius: number) -> table
Find every entity id whose colliders overlap a sphere.
A trigger (isTrigger) holds nothing up, so it is left out.
Parameters
centervec3— Sphere center in world space.radiusnumber— Sphere radius.
Returns table — Array of overlapping entity ids.
local ids = Physics.overlapSphere({x=0,y=0,z=0}, 5)
typed/builtin//modules/api/engine/physics/P/pinSoftRegion
P.pinSoftRegion(softId: string | entityRef, region: PhysicsSoftRegion?, opts: { name: string?, target: vec3? }?) -> string
Pin a region of a soft body: its particles are held kinematic, where
they stand now, and the rest of the body hangs from them, rests on them
or swings about them through its elastic elements. A cloth pinned by two
corners hangs; a jelly pinned along its base stays put while its top
wobbles. Give the pin a target (or set one later with
Physics.setPinTarget) and the pinned particles move together so their
centre stands there, dragging the body after them: a hand grabbing a
corner, a press driven down into a block. The pin is an entry of the soft
body's SoftBody component (pins), so it is saved with the scene,
reaches every peer and is made again on a rebuilt body; it reads back
from Physics.softBodyState(softId).pins.
Parameters
softIdstring | entityRef— The soft body's entity (it carries aSoftBodycomponent).regionPhysicsSoftRegion(optional) — Which particles to pin, asPhysics.softRegiontakes it:{ box = { min, max } }or{ nearest = point }in the entity's unit box (-0.5 to 0.5 on each axis),space = "world"for world coordinates; nil pins the whole body.opts{ name: string?, target: vec3? }(optional) —{ name: string?, target: vec3? }: the name the pin is addressed by (by defaultpin1,pin2, ...), and a world position to move the pinned particles' centre to.
Returns string — The pin's name.
local corner = Physics.pinSoftRegion(cloth, { nearest = {-0.5, 0, 0.5} })
Physics.pinSoftRegion(cloth, { box = { min = {-0.5, -1, 0.45}, max = {0.5, 1, 0.5} } }, { name = "top" })
local grab = Physics.pinSoftRegion(jelly, { nearest = hit.point, space = "world" }, { target = hit.point })
typed/builtin//modules/api/engine/physics/P/pumpJointBreaks
P.pumpJointBreaks()
Deliver every joint break the simulation has recorded to the registered
listeners. An enabled Joint component calls this each tick, so listeners
fire on their own wherever joints come from that component. A joint made by
writing ecs.PhysicsJoint directly has no such tick behind it — call this
each frame, or poll jointBreaks, to deliver its breaks.
Physics.pumpJointBreaks()
typed/builtin//modules/api/engine/physics/P/pumpSoftBodyTears
P.pumpSoftBodyTears()
Give every piece the soft bodies tore off since the last call its
entity, and deliver each tear to the onSoftBodyTear listeners. Every
SoftBody component calls this each frame, so tears are handled on their
own; the work is done once per frame however many call it.
Physics.pumpSoftBodyTears()
typed/builtin//modules/api/engine/physics/P/raycast
P.raycast(origin: vec3, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a ray and return the first hit. Answers from COLLIDERS ALONE: a
mesh that renders but carries no collider is not in the physics world, so a
ray fired through it reports the same nil a ray through open air does.
renderer.raycast answers the same ray against the geometry the renderer
DRAWS, which is what reads the surface of a terrain, a procedurally
generated mesh, or any plain Model.
A trigger (isTrigger) holds nothing up, so the ray passes through it.
Parameters
originvec3— Ray origin in world space.directionvec3— Ray direction, of any length: the engine normalises it, somaxDistanceanddistanceare world units.maxDistancenumber(optional) — Maximum distance along the ray (defaults to 1000).exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table? — Hit table { entityId, point, normal, distance, startedInside, softBody? }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. softBody is set when what was hit is a soft body, whose surface the query meets as it is deformed now: a PhysicsSoftHit naming the surface triangle, the particles carrying it with the hit's weight on each, the particle nearest the hit, and region, the hit as a soft region to hand to Physics.applyImpulse, Physics.pinSoftRegion or Physics.softParticles. Physics.applyImpulseAtPoint takes the hit's point as it is. A nil says the ray met no COLLIDER, which Physics.colliderCount() separates from a world that holds none for it to meet.
local hit = Physics.raycast({x=0,y=2,z=0}, {x=0,y=-1,z=0})
local hit = Physics.raycast(origin, dir, 50, { selfId, carriedId })
local hit = Physics.raycast(muzzle, aim, 100); if hit and hit.softBody then Physics.applyImpulseAtPoint(hit.entityId, {x=0, y=0, z=-20}, hit.point, { radius = 0.3 }) end
if Physics.colliderCount() == 0 then hit = renderer.raycast(eye, down, 200) end
typed/builtin//modules/api/engine/physics/P/raycastAll
P.raycastAll(origin: vec3, direction: vec3, maxDistance: number?, maxHits: number?, exclude: (string | {string})?) -> table
Cast a ray and return every hit up to maxHits. Answers from COLLIDERS
ALONE, so a rendered mesh with no collider is absent from the result;
renderer.raycastAll answers the same ray against the geometry the
renderer draws.
A trigger (isTrigger) holds nothing up, so the ray passes through it.
Parameters
originvec3— Ray origin in world space.directionvec3— Ray direction, of any length: the engine normalises it, somaxDistanceanddistanceare world units.maxDistancenumber(optional) — Optional distance limit along the ray.maxHitsnumber(optional) — Optional cap on the number of hits returned.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table — Array of hit tables { entityId, point, normal, distance, startedInside, softBody? } — empty when nothing was hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. softBody is set when what was hit is a soft body, whose surface the query meets as it is deformed now: a PhysicsSoftHit naming the surface triangle, the particles carrying it with the hit's weight on each, the particle nearest the hit, and region, the hit as a soft region to hand to Physics.applyImpulse, Physics.pinSoftRegion or Physics.softParticles. Physics.applyImpulseAtPoint takes the hit's point as it is.
local hits = Physics.raycastAll(origin, dir, 50, 4)
typed/builtin//modules/api/engine/physics/P/raycastBetween
P.raycastBetween(fromId: string, toId: string, maxDistance: number?) -> table?
Cast a ray from one entity toward another and return the first hit.
Parameters
fromIdstring— Origin entity id.toIdstring— Target entity id.maxDistancenumber(optional) — Optional distance cap (default 1000).
Returns table? — Hit table, or nil if the entities are coincident or nothing was hit.
local hit = Physics.raycastBetween(a, b)
typed/builtin//modules/api/engine/physics/P/raycastScreen
P.raycastScreen(sx: number, sy: number, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a ray from a screen pixel into the scene and return the first hit. Unprojects the pixel with screenToRay, then casts with raycast.
Parameters
sxnumber— Screen X in viewport-local pixels (the space ofinput.mouse_positionandscreenToRay).synumber— Screen Y in viewport-local pixels.maxDistancenumber(optional) — Maximum distance along the ray (defaults to 1000, matchingraycast).exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table? — Hit table { entityId, point, normal, distance, startedInside, softBody? }, or nil on a miss or when no camera has rendered yet. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. softBody is set when what was hit is a soft body, whose surface the query meets as it is deformed now: a PhysicsSoftHit naming the surface triangle, the particles carrying it with the hit's weight on each, the particle nearest the hit, and region, the hit as a soft region to hand to Physics.applyImpulse, Physics.pinSoftRegion or Physics.softParticles. Physics.applyImpulseAtPoint takes the hit's point as it is.
local Zin = require("@builtin::modules.zinput")
local x, y = Zin.state.mousePosition(); local hit = Physics.raycastScreen(x, y)
typed/builtin//modules/api/engine/physics/P/releasePin
P.releasePin(softId: string | entityRef, name: string?) -> number
Release a pin of a soft body, or every pin: its particles take their mass back, keep the velocity they had, and move with the body again.
Parameters
softIdstring | entityRef— The soft body's entity.namestring(optional) — The pin to release; omit to release every one.
Returns number — How many pins were released.
Physics.releasePin(cloth, corner)
Physics.releasePin(cloth)
typed/builtin//modules/api/engine/physics/P/removeCollider
P.removeCollider(entityId: string | entityRef) -> string?
Remove whichever collider component an entity carries.
Parameters
entityIdstring | entityRef— Target entity id.
Returns string? The component that was removed, or nil when there was none.
Physics.removeCollider(id)
typed/builtin//modules/api/engine/physics/P/removeConstraint
P.removeConstraint(entityId: string | entityRef, index: number?)
Remove transform constraints from an entity (if any are present).
Parameters
entityIdstring | entityRef— Target entity id.indexnumber(optional) — Optional constraint index (currently ignored — the whole component is removed).
Physics.removeConstraint(id)
typed/builtin//modules/api/engine/physics/P/removeJoint
P.removeJoint(entityId: string | entityRef)
Remove the Joint component from an entity (if present).
Parameters
entityIdstring | entityRef— Target entity id.
Physics.removeJoint(id)
typed/builtin//modules/api/engine/physics/P/removeWheelCollider
P.removeWheelCollider(entityId: string | entityRef)
Remove the WheelCollider component from an entity (if present).
Parameters
entityIdstring | entityRef— Target entity id.
Physics.removeWheelCollider(id)
typed/builtin//modules/api/engine/physics/P/repairSoftBody
P.repairSoftBody(softId: string | entityRef) -> number
Restore the shape a soft body was built in: every permanent dent,
crumple and crease it took past its plasticYield is undone, and the body
is put back in that shape where it stands, keeping its motion as a whole. The repair is the
SoftBody component's repairs count raised by one, so it reaches every
peer. Physics.softBodyState(softId).plastic reads 0 once it is applied
(at the start of the next frame).
Parameters
softIdstring | entityRef— The soft body's entity (it carries aSoftBodycomponent).
Returns number — The component's new repairs count.
Physics.repairSoftBody(shell)
typed/builtin//modules/api/engine/physics/P/setAngularDamping
P.setAngularDamping(entityIdOrDamping: string | entityRef | number, damping: number?)
Set angular damping on an entity's rigid body. One-arg form targets the script-context entity.
Parameters
entityIdOrDampingstring | entityRef | number— Entity id (withdamping) OR damping value (script-context entity).dampingnumber(optional) — Optional explicit damping when targeting another entity.
Physics.setAngularDamping(0.1)
Physics.setAngularDamping(entityId, 0.1)
typed/builtin//modules/api/engine/physics/P/setAngularVelocity
P.setAngularVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Set the angular velocity of an entity (radians/sec). Same call
shapes as setVelocity.
Parameters
astring | entityRef | number | vec3— x-component, a{x, y, z}vector, or an entity id (explicit target).b(number | vec3)(optional) — y-component, x-component, or the vector depending on call form.cnumber(optional) — z-component or y-component depending on call form.dnumber(optional) — Optional z-component when targeting an explicit entity.
Physics.setAngularVelocity(0, 0, 1)
Physics.setAngularVelocity(entityId, 0, 0, 1)
Physics.setAngularVelocity(entityId, {x=0, y=0, z=1})
typed/builtin//modules/api/engine/physics/P/setBodyType
P.setBodyType(entityId: string | entityRef, bodyType: string)
Change a rigid body's type at runtime. Mass, colliders, and joints are preserved — only the body's response to forces and position writes changes.
Parameters
entityIdstring | entityRef— Target entity id.bodyTypestring— One of"dynamic","kinematic","static".
Physics.setBodyType(entityId, "kinematic")
typed/builtin//modules/api/engine/physics/P/setCcdEnabled
P.setCcdEnabled(entityIdOrEnabled: string | entityRef | boolean, enabled: boolean?)
Enable or disable continuous collision detection on an entity's rigid body. One-arg form targets the script-context entity.
Parameters
entityIdOrEnabledstring | entityRef | boolean— Entity id (withenabled) OR boolean (script-context entity).enabledboolean(optional) — Optional explicit boolean when targeting another entity.
Physics.setCcdEnabled(true)
Physics.setCcdEnabled(entityId, true)
typed/builtin//modules/api/engine/physics/P/setCollisionGroups
P.setCollisionGroups(entityId: string | entityRef, membership: number, filter: number)
Set the collision-group membership and filter bitmasks on an
entity's colliders. Adds a CollisionGroup component if missing; the
component holds the groups, so they apply to the colliders of the
entity's rigid body or standalone collider by the next physics step, and
a body built or rebuilt later takes them as it is built, in edit mode as
well as play. A soft body's groups are its SoftBody component's
collisionMemberships and collisionFilter.
Parameters
entityIdstring | entityRef— Target entity id.membershipnumber— Bitmask: which groups this collider belongs to.filternumber— Bitmask: which groups this collider can collide with.
Physics.setCollisionGroups(id, 0x0001, 0xFFFF)
typed/builtin//modules/api/engine/physics/P/setGravity
P.setGravity(gravity: vec3)
Replace the world gravity vector.
Parameters
gravityvec3— New gravity vector in m/s².
Physics.setGravity({x=0, y=-9.81, z=0})
typed/builtin//modules/api/engine/physics/P/setGravityScale
P.setGravityScale(entityIdOrScale: string | entityRef | number, scale: number?)
Set the per-entity gravity scale (1.0 = normal, 0.0 = no gravity). One-arg form targets the script-context entity.
Parameters
entityIdOrScalestring | entityRef | number— Entity id (withscale) OR scale value (script-context entity).scalenumber(optional) — Optional explicit scale when targeting another entity.
Physics.setGravityScale(0.5)
Physics.setGravityScale(entityId, 0.5)
typed/builtin//modules/api/engine/physics/P/setJointMotor
P.setJointMotor(entityId: string | entityRef, targetVelocity: number, maxForce: number)
Set a motor on an entity's joint.
Parameters
entityIdstring | entityRef— Target entity id (must carry a Joint component).targetVelocitynumber— Desired joint velocity.maxForcenumber— Maximum force the motor can apply.
Physics.setJointMotor(id, 5.0, 1000)
typed/builtin//modules/api/engine/physics/P/setLinearDamping
P.setLinearDamping(entityIdOrDamping: string | entityRef | number, damping: number?)
Set linear damping on an entity's rigid body (0 = no damping). One-arg form targets the script-context entity.
Parameters
entityIdOrDampingstring | entityRef | number— Entity id (withdamping) OR damping value (script-context entity).dampingnumber(optional) — Optional explicit damping when targeting another entity.
Physics.setLinearDamping(0.05)
Physics.setLinearDamping(entityId, 0.05)
typed/builtin//modules/api/engine/physics/P/setMass
P.setMass(entityIdOrMass: string | entityRef | number, mass: number?)
Set the mass of an entity's rigid body (kg). One-arg form targets the script-context entity.
Parameters
entityIdOrMassstring | entityRef | number— Entity id (withmass) OR mass value (script-context entity).massnumber(optional) — Optional explicit mass when targeting another entity.
Physics.setMass(10)
Physics.setMass(entityId, 10)
typed/builtin//modules/api/engine/physics/P/setPinTarget
P.setPinTarget(softId: string | entityRef, name: string, position: vec3?)
Drive a pin of a soft body to a world position: its particles move
together over the next step so the centre they had when the pin was made
stands at position, with the velocity that carries them there, and the
rest of the body follows. Set it every frame to drag the region along a
path. nil stops driving it and holds it where it stands. The target is
the pin's entry in the SoftBody component's pins, so it reaches every
peer.
Parameters
softIdstring | entityRef— The soft body's entity.namestring— The pin's name, aspinSoftRegionreturned it.positionvec3(optional) — The world position for the pinned particles' centre; nil holds them where they stand.
Physics.setPinTarget(cloth, corner, {x=2, y=3, z=0})
typed/builtin//modules/api/engine/physics/P/setRotationLocks
P.setRotationLocks(entityId: string | entityRef, x: boolean, y: boolean, z: boolean)
Lock or unlock rotation on specific axes.
Parameters
entityIdstring | entityRef— Target entity id.xboolean— Lock rotation about the world X axis.yboolean— Lock rotation about the world Y axis.zboolean— Lock rotation about the world Z axis.
Physics.setRotationLocks(id, false, true, false)
typed/builtin//modules/api/engine/physics/P/setTranslationLocks
P.setTranslationLocks(entityId: string | entityRef, x: boolean, y: boolean, z: boolean)
Lock or unlock translation on specific axes.
Parameters
entityIdstring | entityRef— Target entity id.xboolean— Lock translation along the world X axis.yboolean— Lock translation along the world Y axis.zboolean— Lock translation along the world Z axis.
Physics.setTranslationLocks(id, false, false, true)
typed/builtin//modules/api/engine/physics/P/setVelocity
P.setVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Set the linear velocity of an entity. Accepts (x, y, z) or a
{x, y, z} vector for the script-context entity, or the same
prefixed with an explicit entityId.
Parameters
astring | entityRef | number | vec3— x-component, a{x, y, z}vector, or an entity id (explicit target).b(number | vec3)(optional) — y-component, x-component, or the vector depending on call form.cnumber(optional) — z-component or y-component depending on call form.dnumber(optional) — Optional z-component when targeting an explicit entity.
Physics.setVelocity(0, 10, 0)
Physics.setVelocity(entityId, 0, 10, 0)
Physics.setVelocity(entityId, {x=0, y=10, z=0})
typed/builtin//modules/api/engine/physics/P/softBodySettings
P.softBodySettings(changes: table?) -> PhysicsSoftBodySettings
The settings every soft body of the world is simulated under, and the
way to change them: how many extra substeps a hard hit may run, how stiff
soft contacts are, which tangle and overlap recovery runs, and how far
the FEM solver (SoftBody.solver = "fem") solves each step. Each one
trades simulation quality for cost; Physics.stepCost().softBodies shows
what each body costs under them. They are world settings
(physics.soft_body_* in .world_settings) saved with the world: a
change reads back at once and the solver runs under it from the next
physics tick. Written during play, they land on the play shadow like any
other /source write: the solver runs under them at once, and they are
gone when the play edits are discarded. Write them in edit mode (or keep
them with vfs.promotePlayShadow("/zero/source/.world_settings")) for the
world to keep them.
Parameters
changestable(optional) — OptionalPhysicsSoftBodySettingsfields to change; the fields left out keep their values. A field the settings do not have, or a value of the wrong type or out of range, is refused by name.
Returns PhysicsSoftBodySettings — The settings with changes applied: what the next tick runs under.
print(Physics.softBodySettings().maxExtraSubsteps)
Physics.softBodySettings({ maxExtraSubsteps = 2 }) -- cheaper hard hits
Physics.softBodySettings({ tangleDetection = false, overlapConstraints = false })
Physics.softBodySettings({ femMaxIterations = 60 }) -- FEM bodies converge further each substep
typed/builtin//modules/api/engine/physics/P/softBodyState
P.softBodyState(entityId: string | entityRef, opts: table?) -> PhysicsSoftBodyState?
What the solver holds for one soft body (an entity with a SoftBody
component): its particle, cell and edge counts, the volume it encloses
against its rest volume, its centre of mass and frame, the box around its
particles, how fast they move, whether it sleeps, and what it touches,
and in render how the entity's model is drawn carried by the body.
plastic is how deep the deformation that stayed is, in metres (a dent
past the component's plasticYield; plasticMean the mean over the
particles), 0 for a body that only deformed elastically.
diverged is true while a step has carried the body into a state no
material reaches (its particles spread, or its volume grown, past ten
times the shape it is held at, or a particle carried to a value that is
not a number), and divergence says what was measured; the engine log
names the body the step it diverges.
tearStrain and tearForce are the thresholds it tears past (0 tears
never), and piece names the torn piece a body split off another
continues (nil for a body built from its fields).
attachments lists the regions held to rigid bodies (attachSoftRegion)
with the force each pulls with, attachedParticles counts the particles
tied, and clusters counts the body's frame plus one per joint end
anchored on a region of it. pins lists the regions pinned
(pinSoftRegion) with how many particles each holds, its target and
where its particles' centre stands, and pinnedParticles counts the
particles held kinematic. diagnostics is what the physics debug views
picture of the body as numbers: inverted cells, self-crossings, element
loads (maxLoad, loadedElements), pinned and attached particles (see
PhysicsSoftBodyDiagnostics; Physics.debugLines draws them).
replication, on a synced entity in a multiplayer session, says which
peer simulates the body (authority "local" or "remote", owner)
and what it costs on the wire here (lastSync in seconds,
bytesPerSecond).
status says where the body stands: "built", "pending" (the
component stands and the body is not built yet, waitingFor naming what
the build waits on), "refused" (refusal says why the component was
not built) or "none" (no SoftBody component). A body is built at the
start of the frame after its component is added, so a read in the call
that added it answers "pending"; wait on status rather than reading
exists = false as a refusal. Answers in edit mode as well as play mode.
Parameters
entityIdstring | entityRef— Entity id or proxy.optstable(optional) —{ positions: boolean?, contactPoints: boolean?, contactsWith: (string | entityRef)? }:positions = trueadds every particle's world position;contactPoints = trueadds each contact's individual points (each contact names its count, normal, depth, impulse and particles either way);contactsWithreports only the contacts with that one entity, so a read about one pair costs that pair alone.
Returns PhysicsSoftBodyState? — A PhysicsSoftBodyState, or nil when nothing in the scene answers to that id.
local s = Physics.softBodyState(jelly); print(s.particleCount, s.volumeRatio, s.sleeping)
print(Physics.softBodyState(shell).plastic) -- metres of dent the shell kept
local s = Physics.softBodyState(shell); if s.diverged then print(s.divergence) end
for _, p in Physics.softBodyState(jelly, { positions = true }).positions do print(p.y) end
local r = Physics.softBodyState(jelly).render; print(r.drawn, r.vertexCount, r.uploadedThisFrame)
local s = Physics.softBodyState(jelly); if s.status == "pending" then print("waiting for", s.waitingFor) end
typed/builtin//modules/api/engine/physics/P/softBodyTears
P.softBodyTears() -> { PhysicsSoftBodyTear }
Every soft body tear since the last call to this function, oldest
first, with the entities of the pieces each split off: the same records
onSoftBodyTear delivers. The 256 most recent are kept.
Returns { PhysicsSoftBodyTear } — Array of PhysicsSoftBodyTear.
for _, t in Physics.softBodyTears() do print(t.kind, t.entity, #t.pieces) end
typed/builtin//modules/api/engine/physics/P/softParticles
P.softParticles(softId: string | entityRef, region: PhysicsSoftRegion?) -> PhysicsSoftParticles?
Read the particles of a soft body that a region selects: where each
stands, how fast it moves and whether it is pinned, with the selection's
centre of mass and mean velocity. The way to see a dent where a hit
landed, how far a pinned corner was dragged, or how a region moves.
indices name the particles for comparing two reads of the same body.
The region is resolved at each call against where the particles stand
then (a local-space region in the body's frame as it stands now), so a
read after the body moved can select other particles than an earlier
read of the same region; a pin's own particles read back through
Physics.softBodyState(softId).pins (center) and the pinned flags.
Parameters
softIdstring | entityRef— The soft body's entity.regionPhysicsSoftRegion(optional) — Which particles to read, asPhysics.softRegiontakes it; nil reads the whole body.
Returns PhysicsSoftParticles? — A PhysicsSoftParticles report, or nil when nothing in the scene answers to that id.
local top = Physics.softParticles(jelly, { box = { min = {-0.5, 0.4, -0.5}, max = {0.5, 0.5, 0.5} } }); print(top.count, top.center.y)
local near = Physics.softParticles(shell, { box = { min = {1.6, 0, -0.3}, max = {2.2, 1, 0.3} }, space = "world" })
typed/builtin//modules/api/engine/physics/P/softRegion
P.softRegion(region: PhysicsSoftRegion?) -> { [string]: any }
The engine form of a soft body region: the record the SoftBody
component's attachments and a Joint's soft body ends carry. A region
names part of a soft body by where its particles stand: { box = { min, max } } is every particle between the two corners, { nearest = point }
the one particle nearest the point, and nil (or {}) the whole body.
Coordinates are in the soft body entity's local space, the unit box it
is built in (-0.5 to 0.5 on each axis, before its scale), so a region
stays the same part of the body wherever the body is; space = "world"
reads them in world space instead. A box counts a particle whose contact
skin reaches into it: a generated body's outer particles stand that skin
inside the unit box, so a box flat on one of its faces
(min = {-0.5, -1, -1}, max = {-0.5, 1, 1}) holds that face's particles.
Errors naming the key that is wrong.
Parameters
regionPhysicsSoftRegion(optional) —{ box = { min = v, max = v } | { v, v }, nearest = v, space = "local" | "world" }, or nil for the whole body.
Returns { [string]: any } — { kind = "Whole" | "Box" | "Nearest", min, max, point, world }.
Physics.softRegion({ box = { min = {-0.5, 0.45, -0.5}, max = {0.5, 0.5, 0.5} } })
Physics.softRegion({ nearest = {0, 0.5, 0} })
typed/builtin//modules/api/engine/physics/P/sphereCast
P.sphereCast(origin: vec3, radius: number, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a sphere along a direction and return the first hit.
A trigger (isTrigger) holds nothing up, so the sweep passes through it.
Parameters
originvec3— Sphere center at the start of the cast.radiusnumber— Sphere radius.directionvec3— Cast direction, of any length: the engine normalises it, somaxDistanceanddistanceare world units.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside, softBody? }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. softBody is set when what was hit is a soft body, whose surface the query meets as it is deformed now: a PhysicsSoftHit naming the surface triangle, the particles carrying it with the hit's weight on each, the particle nearest the hit, and region, the hit as a soft region to hand to Physics.applyImpulse, Physics.pinSoftRegion or Physics.softParticles. Physics.applyImpulseAtPoint takes the hit's point as it is.
local hit = Physics.sphereCast(o, 0.5, dir, 10)
local hit = Physics.sphereCast(o, 0.5, dir, 10, selfId)
typed/builtin//modules/api/engine/physics/P/stepCost
P.stepCost() -> PhysicsStepCost?
What the last physics step cost, stage by stage — the same figures
worldState().step carries, for a caller that wants only these. Each
covers that one step rather than a window of them, and consecutive steps
over the same resting scene vary by tens of percent, so several samples
averaged is the honest read of what a step costs. softBodyMs is the
soft body part of the step and softBodies each soft body's part of it.
A frame can run several steps; recent lists the last 64 with their
ordinals (step), so a loop reading once a frame counts every step.
Returns PhysicsStepCost? — A PhysicsStepCost, or nil on a frame where the pipeline did not step — a paused simulation, or a world still bootstrapping.
local c = Physics.stepCost(); if c then print(c.stepMs, c.narrowPhaseMs) end
local c = Physics.stepCost(); if c then for _, b in c.softBodies do print(b.entity, b.ms, b.particles) end end
local seen, total = 0, 0
-- once a frame: every step since the last read, however many a frame ran
local c = Physics.stepCost(); if c then for _, s in c.recent do if s.step > seen then total += s.softBodyMs; seen = s.step end end end
typed/builtin//modules/api/engine/physics/P/stillnessReasons
P.stillnessReasons() -> { string }
Every reason whyStill can answer with, in the order the engine
considers them. Read from the engine, so the list is the one the answers
come from.
Returns { string } — An array of reason names.
for _, reason in Physics.stillnessReasons() do print(reason) end
typed/builtin//modules/api/engine/physics/P/tearSoftBody
P.tearSoftBody(softId: string | entityRef, region: PhysicsSoftRegion)
Tear a soft body at a region: every edge and cell holding a particle of
the region comes apart there, opening a hole or, when the region runs
across the body, splitting it. Applied straight after the next physics
step; a piece the tear separates becomes an entity of its own (see
onSoftBodyTear), and it reports as a tear of kind "tear". Raises when the
entity carries no SoftBody, for a region naming no box and no point (the
whole body), and in edit mode, where nothing simulates.
Parameters
softIdstring | entityRef— The soft body's entity.regionPhysicsSoftRegion— Which particles to tear through, asPhysics.softRegiontakes it:{ box = { min, max } }or{ nearest = point }in the entity's unit box (-0.5 to 0.5 on each axis),space = "world"for world coordinates.
Physics.tearSoftBody(cloth, { nearest = hit.point, space = "world" })
Physics.tearSoftBody(sheet, { box = { min = {-0.02, -1, -0.5}, max = {0.02, 1, 0.5} } })
typed/builtin//modules/api/engine/physics/P/touching
P.touching(entityId: string | entityRef, otherId: string | entityRef) -> (boolean, number, { PhysicsContactPoint })
Whether two entities are touching, and how deeply.
Parameters
entityIdstring | entityRef— Entity id or proxy.otherIdstring | entityRef— The other entity id or proxy.
Returns (boolean, number, { PhysicsContactPoint }) — (touching, deepestPenetration, points) — deepestPenetration is in metres and 0 for surfaces that meet without overlapping.
local hit, depth = Physics.touching(a, b); print(hit, depth)
typed/builtin//modules/api/engine/physics/P/vehicleState
P.vehicleState(chassisId: string | entityRef) -> table?
Read a vehicle: the chassis body's forward speed and the state of every wheel under it (the wheels whose nearest rigid-body ancestor is this chassis).
Parameters
chassisIdstring | entityRef— The chassis entity (the rigid body the wheels sit under).
Returns table? — { speed, groundedWheels, wheels }, or nil when the chassis has no WheelCollider under it. speed is metres per second along the chassis' forward (-Z), negative in reverse; wheels lists getWheelState tables, each with its entityId, in hierarchy order.
local car = Physics.vehicleState(carId); print(car.speed, car.groundedWheels)
typed/builtin//modules/api/engine/physics/P/wakeUp
P.wakeUp(entityId: (string | entityRef)?)
Wake an entity's sleeping rigid or soft body so it resumes
simulating. The motion setters (applyImpulse, setVelocity,
setAngularVelocity) wake the body for you; call this to wake one
explicitly.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Physics.wakeUp(id)
typed/builtin//modules/api/engine/physics/P/whyStill
P.whyStill(entityId: string | entityRef) -> (string?, string?)
Why the solver is not moving a body. Returns nil when it IS moving
it, and otherwise one of noBody, simulationNotStepping, disabled,
static, kinematic, infiniteMass, translationLocked,
gravityDisabled, asleep, outsideIsland, resting, aboutToMove —
the nearest cause, so the answer names the thing to change. A second return
carries the detail: which collider it rests on and how deeply, what its
effective gravity works out to, and so on.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns (string?, string?) — (reason, detail).
local why, detail = Physics.whyStill(id); if why then print(why, detail) end
typed/builtin//modules/api/engine/physics/P/worldState
P.worldState() -> PhysicsWorldState
How many bodies, colliders, joints and contacts the simulation holds
right now, with world gravity, the timestep, whether the pipeline is
stepping at all, and what the last step cost. Counted off the solver, so
a body that failed to build is absent here while its Physics component
still exists.
Returns PhysicsWorldState — A PhysicsWorldState.
local w = Physics.worldState(); print(w.bodies.awake .. "/" .. w.bodies.total .. " awake")
print(Physics.worldState().contacts.touchingPairs .. " pairs touching")
typed/builtin//modules/api/engine/plugins/plugins/awaitingPerson
plugins.awaitingPerson(plugin: string) -> boolean
Whether a call to this plugin is waiting on a person to answer the engine's consent dialog.
Parameters
pluginstring— The plugin asset's library identity, written as a LITERAL.
Returns boolean — True while a dialog for this plugin is unanswered.
if plugins.awaitingPerson(PLUGIN) then print("waiting for you") end
typed/builtin//modules/api/engine/plugins/plugins/call
plugins.call(plugin: string, method: string, ...: any?) -> any
Call a method the plugin at plugin exposes.
Parameters
pluginstring— The plugin asset's identity, written as a LITERAL: the same stringasset.identityanswers for it (motionfor a plugin in this world,@publisher::path.namefor 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.methodstring— A method the plugin'splugin.yamlexposes. One it does not is not callable, whatever the component carries....any(optional) — The call's arguments: numbers, strings, booleans, nil and tables of them. A whole number arrives as an integer.
Returns any — A promise of the plugin's answer, as a value: a table as a table, a number as a number. A refusal raises where it is awaited.
local agents = await(plugins.call("@builtin/plugins/dotbot.plugin", "agents"))
typed/builtin//modules/api/engine/plugins/plugins/isLoaded
plugins.isLoaded(plugin: string) -> boolean
Whether the plugin is loaded and answering calls.
Parameters
pluginstring— The plugin asset's identity, written as a LITERAL.
Returns boolean — True while it is loaded. Never raises: a caller asking is usually waiting for a plugin that is still loading.
while not plugins.isLoaded("motion") do task.wait(0.1) end
typed/builtin//modules/api/engine/plugins/plugins/off
plugins.off(plugin: string, id: number) -> boolean
Stop a listener plugins.on started.
Parameters
pluginstring— The same identity the listener was started with.idnumber— The idplugins.onanswered.
Returns boolean — Whether a listener was removed.
typed/builtin//modules/api/engine/plugins/plugins/on
plugins.on(plugin: string, listener: (string, any) -> ()) -> number
Listen to the events a plugin raises.
Parameters
pluginstring— The plugin asset's library identity, written as a LITERAL.listener(string, any) -> ()—function(eventType, payload)—payloadis the event's decoded JSON.
Returns number — The subscription id, for plugins.off.
local id = plugins.on("@builtin/plugins/dotbot.plugin", function(kind, body) end)
typed/builtin//modules/api/engine/plugins/plugins/openUrl
plugins.openUrl(plugin: string, url: string) -> any
Ask the person to open one of the plugin's own pages.
Parameters
pluginstring— The plugin asset's library identity, written as a LITERAL.urlstring— An https URL whose host the plugin declared undernetwork.hosts. Anywhere else is refused — otherwise this would be an unbounded way to put an address in front of somebody.
Returns any — A promise: true when it opened, or the refusal.
await(plugins.openUrl("@builtin/plugins/dotbot.plugin", "https://claude.ai/login"))
typed/builtin//modules/api/engine/plugins/plugins/status
plugins.status() -> { [string]: any }
What the plugin host loaded, and what refused to load.
Returns { [string]: any } — started is whether the host has run; plugins lists each one with loaded, and the error that refused it where there is one. On the web build it also carries what the browser offers of JavaScript Promise Integration, which every plugin's host calls are lowered onto.
local report = plugins.status()
print(report.started, #report.plugins)
typed/builtin//modules/api/engine/postprocess/postprocess/add
postprocess.add(name: string, shader: string | AssetRef, opts: PostprocessOpts?) -> (boolean, string?)
Register a fullscreen post-process effect. This call is what
puts a pass into the frame — a post-process .shader 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 setProperty or setTexture naming the effect in the same
call finds it. shader is a .shader asset reference whose shader.wgsl
provides fn fragment(in: PostInput) -> vec4<f32> and whose
properties.yaml 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 opts.properties is the whole
schema — this form is deprecated and will be removed, author the effect
as a .shader 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 engine.view_proj / engine.prev_view_proj /
engine.inv_view_proj are the camera THAT render was drawn from and
engine.resolution is that target's own size, so a pass reconstructing
world space from zero_scene_depth(uv) 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
postProcessing = false is the one control that takes the chain off the
frame it returns. An offscreen render keeps no view history of its own, so
engine.prev_view_proj there holds that same matrix rather than the frame
before it, and a pass taking camera motion from the two reads none.
Parameters
namestring— Unique effect name.shaderstring | AssetRef— A resolvedshaderasset reference, or author WGSL (fn fragment(in: PostInput)only).optsPostprocessOpts(optional) —{ 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?}} }— with a shader asset,propertieslayers over the asset's own schema.layerpicks which composited image the effect grades:"scene"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"all"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 itsspacebelongs to: a"light"effect grades"scene"and a"display"effect grades"all", so a bloom reads the scene's highlights where they still stand above 1.0 and a lookup table reads code values. Naming"all"is what an effect that must grade the interface says — and on a world several authors share, it grades everyone else's screens too.spacenames whatfragment()reads and returns:"light"(the default) for a threshold/blur/grade on the scene,"display"for a lookup table, dither ladder or authored overlay working on code values. With a shader asset, defaults to what that asset'sgetSpace()declares.passesnames 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'sgetPasses()declares; a single-pass effect (the common case) declares none.textureDefaultis what atype = "texture"property samples while nothing is bound to it:"white"(1,1,1,1 — the default),"black"(0,0,0,1),"normal"(0.5,0.5,1,1) or"transparent"(0,0,0,0). An effect that lays its texture over the scene wants"transparent", so the frame is untouched untilsetTexturebinds a texture that exists.
Returns (boolean, string?) — True when the chain took the effect; false and the compiler's message when it refused it. A shader that does not compile draws nothing at any property value, so it is not registered and postprocess.list() never names it. __display__ and __blit__ are the renderer's own effects, and a registration under either name is refused with a message saying so. A call made from inside queue() or batch(), where the engine has not run the registration by the time the call returns, answers true for the queued request.
postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader"))
postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} })
typed/builtin//modules/api/engine/postprocess/postprocess/describe
postprocess.describe(name: string) -> PostprocessDescription?
One effect by name, read in full: the chain state
postprocess.status() lists for it, and on top of that properties — the
schema the effect declared, each entry { name, type, default?, min?, max?, textureDefault? } in the shape add takes — and values, what each of
those properties currently holds. A property's value is the one the last
setProperty 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 setProperty takes, so a property read here is
written straight back.
This is the read-back for a property write. setProperty 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 /runtime/fx/<name>/meta.json
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.
Before an effect is registered its schema lives on the .shader asset it
will render: asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties() names what that shader declares.
Parameters
namestring— Effect name.
Returns PostprocessDescription? — The effect's state, schema and live values, or nil when nothing is registered under the name. An effect the renderer registers itself declares no properties of its own, and its properties and values are empty.
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
typed/builtin//modules/api/engine/postprocess/postprocess/list
postprocess.list() -> { string }
List all registered post-process effect names in renderer priority order (lower priority runs first).
Returns { string } — Array of effect names.
for _, n in ipairs(postprocess.list()) do print(n) end
typed/builtin//modules/api/engine/postprocess/postprocess/remove
postprocess.remove(name: string) -> boolean
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.
Parameters
namestring— Effect name to remove.
Returns boolean — True when the mutation was queued. False, named in a WARN, for the two effects the renderer registers itself: __display__, the chain's display transform, which stays in every frame and is tuned through its curve and exposure properties, and __blit__, the copy that carries every render into its target, every capture and the viewport included.
postprocess.remove("vignette")
typed/builtin//modules/api/engine/postprocess/postprocess/setEnabled
postprocess.setEnabled(name: string, enabled: boolean) -> boolean
Queue an enable/disable toggle on a registered post-process effect. Targeting an unknown name is a silent no-op.
Parameters
namestring— Effect name.enabledboolean— True to enable, false to disable.
Returns boolean — True when the mutation was queued. False, named in a WARN, for a __display__ disable (the chain's display transform stays in every frame, and curve = 0 is its linear curve) and for a __blit__ enable (the copy that carries every render into its target joins no chain).
postprocess.setEnabled("bloom", false)
typed/builtin//modules/api/engine/postprocess/postprocess/setProperty
postprocess.setProperty(name: string, prop: string, value: (number | { number })) -> boolean
Set a named material property on a registered post-process
effect. The property must be declared in the effect's properties
schema; read in WGSL as material.<prop>. value is a number or a
number array (vec/color).
Parameters
namestring— Effect name.propstring— Declared property name.value(number | { number })— Number or array of numbers.
Returns boolean — True when the effect's uniform took the value; false when it did not — an effect that is not registered, or one that declares no property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the write by the time the call returns, answers true for the queued request.
postprocess.setProperty("vignette", "intensity", 0.6)
typed/builtin//modules/api/engine/postprocess/postprocess/setSampler
postprocess.setSampler(name: string, opts: { [string]: any }) -> boolean
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.
Parameters
namestring— Effect name.opts{ [string]: any }—{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }.
Returns boolean — True — the mutation was queued.
postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" })
typed/builtin//modules/api/engine/postprocess/postprocess/setTexture
postprocess.setTexture(name: string, prop: string, path: string) -> boolean
Bind a texture to one of an effect's declared texture properties.
Declare it in properties ({ name = "noise", type = "texture" }) and
sample in WGSL as textureSample(noise, noise_sampler, in.uv). path
is any TextureCache-resolvable spec (@builtin::textures.foo,
color:1,0,0, default:white, 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; postprocess.status() reports it under
pendingTextures until then.
Parameters
namestring— Effect name.propstring— Declared texture-property name.pathstring— Texture path / spec.
Returns boolean — True when the slot took the binding, including one held until its texture reaches the GPU; false when it did not — an effect that is not registered, or one that declares no texture property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the binding by the time the call returns, answers true for the queued request.
postprocess.setTexture("__vsky_composite", "cloud", "cloud_target")
typed/builtin//modules/api/engine/postprocess/postprocess/status
postprocess.status() -> { PostprocessStatus }
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 .shader asset it renders
when it was registered from one, the texture each declared slot is bound
to (textures) and the bindings still waiting for their texture
(pendingTextures). 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.
An effect this script has just registered is listed with pending = true until the renderer publishes it, since a registration is queued
for the next frame.
Returns { PostprocessStatus } — Array of per-effect state, in the order the chain runs them.
for _, e in ipairs(postprocess.status()) do
print(e.name, e.enabled, e.layer, e.error)
end
typed/builtin//modules/api/engine/profiler/profiler/begin
profiler.begin(name: string)
Start a named profiling block. Call profiler.finish(name) to
record the duration. Blocks appear in profiler.stats() under
"script.<name>" and inside captures.
Parameters
namestring— Block name (e.g. "MyComponent.update").
profiler.begin("MyComponent.update"); ...; profiler.finish()
typed/builtin//modules/api/engine/profiler/profiler/counters
profiler.counters(pattern: string?) -> { ProfilerCount }
Get the profiler's counts: unitless per-frame tallies (for example
the waits the engine thread made on the process-wide lock, under
system.schedule.frame.process_local_waits), kept apart from the
millisecond timings of stats. frame is the tally of the frame in
progress, prev_frame the last complete frame's, max_frame the
highest complete-frame tally and total the tally since the profiler
started. Optional glob pattern filters by name (supports * and ?).
Parameters
patternstring(optional) — Filter pattern (e.g. "system.schedule.frame.*").
Returns { ProfilerCount } — Array of profiler counts.
for _, c in ipairs(profiler.counters("system.schedule.frame.*")) do print(c.name, c.prev_frame) end
typed/builtin//modules/api/engine/profiler/profiler/disableRing
profiler.disableRing()
Disable the ring buffer and clear its history.
profiler.disableRing()
typed/builtin//modules/api/engine/profiler/profiler/enableRing
profiler.enableRing(seconds: number?) -> boolean
Enable the always-recording ring buffer, retaining the last
seconds of per-frame data (default 20). Query it AFTER the fact
with profiler.retro() — 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.
Parameters
secondsnumber(optional) — Seconds of history to retain (default 20).
Returns boolean — True if enabled, false if refused (runtime profile).
if profiler.enableRing(30) then ... end
typed/builtin//modules/api/engine/profiler/profiler/finish
profiler.finish(name: string?) -> number?
Finish a profiling block and record the elapsed duration as
"script.<name>". 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.
Parameters
namestring(optional) — Block name to finish. Omit to pop the top of the stack.
Returns number? — Elapsed milliseconds, or nil if no matching block was active.
local ms = profiler.finish("MyComponent.update")
typed/builtin//modules/api/engine/profiler/profiler/gpuFrame
profiler.gpuFrame() -> GpuFrameReport
Label-aggregated GPU pass timings over the last window_frames
resolved frames, measured with GPU timestamp queries. supported
is false when the device lacks timestamp queries — spans stays
empty. Each span covers every render/compute pass recorded under
one label — compute.<shader> per compute dispatch, scene.* for
the scene passes, post.<effect> per post-process effect,
feature.* for render-feature passes: ms is the median of its
per-frame totals, min_ms/max_ms the range that median sits in,
count the passes per frame and frames how much of the window
carried it. at_floor marks a label whose every sample landed
within a few ticks of the device's timestamp counter (tick_ms) —
those passes ran and the device resolved no duration for them,
which is not the same as a measured zero. ran is whether the
label recorded a measured pass in the newest resolved frame, and
last_frame 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 window_frames frames carrying the median of
the frames it did run in: read ran to answer whether a pass is
running, frame - last_frame for how many resolved frames ago it
last did, and ms as the cost of the frames it ran in.
frame_span_ms (first pass begin to last pass end) and
total_ms are medians too, so
rows do not sum to total_ms, and the GPU may overlap passes so
total_ms can exceed frame_span_ms. The readback is
asynchronous: the window lags the live frame by a few frames.
Returns GpuFrameReport — GPU timing window, spans ranked by median ms descending.
local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label)
typed/builtin//modules/api/engine/profiler/profiler/hits
profiler.hits(label: string?) -> string?
Drain the watchdog's recorded hit frames into a capture stored
under label (default "watch_hits") and clear the buffer.
Returns the capture JSON (same shape as stopCapture), or nil if
there were no hits.
Parameters
labelstring(optional) — Capture label to store under (default "watch_hits").
Returns string? — Capture JSON of the hit frames, or nil if none.
local json = profiler.hits()
typed/builtin//modules/api/engine/profiler/profiler/isCapturing
profiler.isCapturing() -> boolean
Check if a profiler capture is currently active.
Returns boolean — True if a capture is in progress.
if profiler.isCapturing() then ... end
typed/builtin//modules/api/engine/profiler/profiler/lastCapture
profiler.lastCapture() -> string?
Get the most recent completed capture result as a JSON string.
Same shape as profiler.stopCapture(). Returns nil if no capture
has been completed yet.
Returns string? — JSON string of the last capture, or nil.
local last = profiler.lastCapture()
typed/builtin//modules/api/engine/profiler/profiler/perfLine
profiler.perfLine() -> string?
The engine's own one-line performance summary, exactly as it
rides a tool response: the headline frame time, and when the frame
is slow or CPU moved since the last edit, the severity marker, the
delta and the systems the time went to. A DIFFERENT measurement
from everything below: this is the published smoothed frame, while
stats 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.
Returns string? — The summary line, or nil before the first frame is published.
local line = profiler.perfLine()
typed/builtin//modules/api/engine/profiler/profiler/recordingHold
profiler.recordingHold() -> string?
Who holds the one windowed recording, as a JSON string
{ label, owner, started_at, held_seconds }, or nil when nothing is
recording. owner is the caller key of whoever called
startCapture, the same value vfs.currentAuthor().id 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.
Returns string? — JSON hold description, or nil when no recording is active.
local held = profiler.recordingHold()
typed/builtin//modules/api/engine/profiler/profiler/retro
profiler.retro(seconds: number?, label: string?) -> { [string]: any }?
Retroactively aggregate the last seconds of the ring (default:
the whole ring). The full per-frame capture is retained under label
(default "retro") for in-engine drill-down (the frame / hotspots
tools);
this RETURNS a compact structured aggregate table (frame-time distribution
- per-system summary), never the raw per-frame array — bounded, so it is
safe over the ZeroMind bridge. Code-facing primitive; the
retrotool renders the agent-facing report. Latency-immune: the data is historical.
Parameters
secondsnumber(optional) — How many seconds back to include (default: whole ring).labelstring(optional) — Capture label to store under (default "retro").
Returns { [string]: any }? — A compact aggregate { label, source, frames, seconds, exclude_agent, agent_frames, dt = { avg, p50, p90, p99, max, min }, summary = {...} }, or nil if the ring holds nothing.
local agg = profiler.retro(8, "collapse")
typed/builtin//modules/api/engine/profiler/profiler/ringStatus
profiler.ringStatus() -> string
Ring buffer status as a JSON string:
{ enabled, frames, capacity, span_seconds }.
Returns string — JSON status string.
local s = profiler.ringStatus()
typed/builtin//modules/api/engine/profiler/profiler/startCapture
profiler.startCapture(label: string?) -> boolean
Start recording per-frame profiler data, held by the calling
caller. Each frame's system timings are captured until
stopCapture() is called. Results are accessible via
profiler.lastCapture() and VFS at
/zero/runtime/profiler/<label>.json. The recording is one
engine-wide slot, so a start while another recording holds it
returns false and profiler.recordingHold() names the label and the
caller holding it.
Parameters
labelstring(optional) — Capture label (default"capture").
Returns boolean — True if capture started, false when another recording holds the slot.
if profiler.startCapture("frame-spike") then ... end
typed/builtin//modules/api/engine/profiler/profiler/stats
profiler.stats(pattern: string?) -> { ProfilerStat }
Get current EMA profiling statistics from the SystemProfiler.
Optional glob pattern filters by metric name (supports * and ?
wildcards). Each entry carries two averages: avg_ms 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; avg_frame_ms
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.
Parameters
patternstring(optional) — Filter pattern (e.g. "schedule.", "system.schedule.render.").
Returns { ProfilerStat } — Array of profiler block stats.
for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end
typed/builtin//modules/api/engine/profiler/profiler/stopCapture
profiler.stopCapture() -> string?
Stop the active profiler capture and return its result as a
JSON string. The capture is also saved to VFS at
/zero/runtime/profiler/<label>.json. Top-level fields: label,
frame_count, started_at, ended_at, frames, summary,
callers. Compute duration as ended_at - started_at. callers
holds { caller, total_ms, max_ms, frames } per caller whose queued
Luau ran in the window, heaviest first, and each frame carries its
own split as agent_by_caller.
Returns string? — JSON capture result, or nil if no capture was active.
local json = profiler.stopCapture()
typed/builtin//modules/api/engine/profiler/profiler/unwatch
profiler.unwatch()
Disarm the watchdog. Recorded hits are kept for a final
profiler.hits().
profiler.unwatch()
typed/builtin//modules/api/engine/profiler/profiler/watch
profiler.watch(ceilingMs: number, mode: string?, excludeAgent: boolean?, maxHits: number?) -> boolean
Arm the frame-time watchdog. When a frame's EFFECTIVE time
(total minus agent-injected execute cost) crosses ceilingMs,
mode "record" logs every offending frame (read with
profiler.hits()), and mode "pause" pauses gameplay ONCE to
freeze the bad state, then disarms. Editor profile only: returns
false in the runtime profile. excludeAgent (default true) keeps
the agent's own calls from tripping it.
Parameters
ceilingMsnumber— Effective frame-time ceiling in ms.modestring(optional) — "record" (default) or "pause".excludeAgentboolean(optional) — Subtract agent cost before comparing (default true).maxHitsnumber(optional) — Max frames retained in record mode (default 240).
Returns boolean — True if armed, false if refused (runtime profile).
if profiler.watch(50, "pause") then ... end
typed/builtin//modules/api/engine/profiler/profiler/watchStatus
profiler.watchStatus() -> string
Watchdog status as a JSON string: { armed, ceiling_ms, mode, exclude_agent, hits, dropped_hits, tripped }.
Returns string — JSON status string.
local s = profiler.watchStatus()
typed/builtin//modules/api/engine/prototype_lifecycle/M/activatePrototypes
M.activatePrototypes()
Reactivate and unhide every prototype root and EditorOnly entity.
typed/builtin//modules/api/engine/prototype_lifecycle/M/applyCurrentMode
M.applyCurrentMode()
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 enterPlay 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.
PrototypeLifecycle.applyCurrentMode()
typed/builtin//modules/api/engine/prototype_lifecycle/M/deactivatePrototypes
M.deactivatePrototypes()
Deactivate and hide every prototype root and EditorOnly entity so play mode neither simulates nor renders them.
typed/builtin//modules/api/engine/prototype_lifecycle/M/despawnClones
M.despawnClones()
Despawn every still-existing tracked clone and clear the tracking list.
typed/builtin//modules/api/engine/prototype_lifecycle/M/enterEdit
M.enterEdit()
Enter edit mode: despawn runtime clones, then reactivate prototypes and EditorOnly entities.
typed/builtin//modules/api/engine/prototype_lifecycle/M/enterPlay
M.enterPlay()
Enter play mode: deactivate and hide prototypes and EditorOnly entities.
typed/builtin//modules/api/engine/prototype_lifecycle/M/hideEditorSurface
M.hideEditorSurface()
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.
typed/builtin//modules/api/engine/prototype_lifecycle/M/install
M.install()
Keep the play-mode invariant applied for every flip, in every world.
enterPlay / enterEdit are what make PrototypeOnly and EditorOnly
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.
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.
PrototypeLifecycle.install()
typed/builtin//modules/api/engine/prototype_lifecycle/M/prototypeRoots
M.prototypeRoots() -> { string }
Ids of every active-layer entity carrying the PlayerPrototype component.
Returns { string } — An array of entity ids.
typed/builtin//modules/api/engine/prototype_lifecycle/M/registerClone
M.registerClone(id: string)
Track a runtime clone root so it can be despawned on the return to edit.
Parameters
idstring— The clone's root entity id.
typed/builtin//modules/api/engine/prototype_lifecycle/M/showEditorSurface
M.showEditorSurface()
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.
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/add
reflectionProbe.add(x: number, y: number, z: number, opts: { [string]: any }?) -> string
Add a reflection probe at (x, y, z) 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 (bakeAll(), or bake(id)). The probe is an editor gizmo,
invisible in play mode. Returns the probe entity id.
Parameters
xnumber— World X.ynumber— World Y.znumber— World Z.opts{ [string]: any }(optional) — Optional{ radius = 12, probeId = "...", name = "..." }.probeIdis 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 — callbakeAll()once the scene is built (baking is an authoring step).
Returns string — The probe entity id.
reflectionProbe.add(0, 3, 0, { radius = 15, probeId = "lobby" })
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/apply
reflectionProbe.apply() -> number
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.
Returns number — The number of probes applied that reflect (registered, live and baked).
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/bake
reflectionProbe.bake(id: string) -> (string?, string?)
Bake the scene into probe id's cube slot from its current position AND
persist it to a faces6 .texture 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, bakeAll, or execute).
Parameters
idstring— Probe entity id.
Returns (string?, string?) — The asset path on success, or (nil, errorMessage) on failure.
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/bakeAll
reflectionProbe.bakeAll() -> { baked: number, failed: number, errors: { string } }
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 (execute, a tool, or
task.spawn).
Returns { baked: number, failed: number, errors: { string } } — { baked = N, failed = M, errors = { ... } }.
reflectionProbe.bakeAll()
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/baking
reflectionProbe.baking(id: string?) -> boolean
Whether a bake under way will reach probe id: its own bake, or a
bakeAll that has it still to reach. With no id, whether any bake is
under way.
Parameters
idstring(optional) — Optional probe entity id.
Returns boolean — True while a bake that reaches the probe (or any bake) runs.
reflectionProbe.baking(id)
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/count
reflectionProbe.count() -> number
Number of registered probes.
Returns number
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/ensureSkyFallback
reflectionProbe.ensureSkyFallback() -> boolean
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.
Returns boolean — True if a capture was queued, false if the sky slot already holds one.
reflectionProbe.ensureSkyFallback()
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/isBaked
reflectionProbe.isBaked(id: string) -> boolean
Whether probe id holds a cube it reflects: baked by bake / bakeAll,
or loaded from its persisted bake by loadBaked. An unbaked probe adds
nothing to the frame.
Parameters
idstring— Probe entity id.
Returns boolean — True once the probe holds a cube; false for an unbaked or unregistered probe.
reflectionProbe.isBaked(id)
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/list
reflectionProbe.list() -> { any }
List every registered probe: { { id, slot, radius, priority, baked, asset, position }, ... }.
baked is whether the probe holds a cube it reflects; an unbaked probe adds
nothing to the frame. asset names the persisted bake once there is one.
Returns { any } — The probe list.
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/loadBaked
reflectionProbe.loadBaked(id: string) -> boolean
Load probe id's PERSISTED baked cube (probe_<key>.texture) 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.
Parameters
idstring— Probe entity id.
Returns boolean — True if a baked asset was loaded, false if none exists / load failed.
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/register
reflectionProbe.register(id: string, radius: number, key: string?) -> number?
Register a reflection probe for entity id with influence radius.
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.
Parameters
idstring— Probe entity id.radiusnumber— Influence radius (world units) — surfaces within blend it.keystring(optional) — Optional STABLE asset identity (the probe's probeId). Defaults toid. The baked cube persists atprobe_<key>.textureso an authored probe keeps the same asset across reloads even though its runtime entity id changes.
Returns number? — The assigned cube slot, or nil if all MAX_PROBES slots are taken.
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/setPriority
reflectionProbe.setPriority(id: string, priority: number)
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.
Parameters
idstring— Probe entity id.prioritynumber— Blend rank. Defaults to 0 on every probe.
reflectionProbe.setPriority(interiorId, 1)
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/setProxy
reflectionProbe.setProxy(id: string, kind: string, x: number, y: number, z: number)
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.
Parameters
idstring— Probe entity id.kindstring— "box" (sized by all three half-extents), "sphere" (sized byx), or "none" to sample along the raw reflection vector.xnumber— Half-extent along X, in world units — the sphere radius for "sphere".ynumber— Half-extent along Y.znumber— Half-extent along Z.
reflectionProbe.setProxy(id, "box", 5, 3, 4) -- a 10x6x8 room
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/setRadius
reflectionProbe.setRadius(id: string, radius: number)
Update a probe's influence radius and re-apply.
Parameters
idstring— Probe entity id.radiusnumber— New influence radius.
typed/builtin//modules/api/engine/reflectionProbe/reflectionProbe/unregister
reflectionProbe.unregister(id: string)
Unregister entity id's probe, freeing its cube slot, and re-apply.
Parameters
idstring— Probe entity id.
typed/builtin//modules/api/engine/renderer/renderer/anisotropy
renderer.anisotropy() -> number
The maximum anisotropy material textures are sampled with right now — the requested level clamped to what this device honours.
Returns number — The effective level, 1 through 16.
if renderer.anisotropy() < 4 then ... end
typed/builtin//modules/api/engine/renderer/renderer/atmospherics/held
renderer.atmospherics.held() -> boolean
Whether a hold is standing on the air right now.
Returns boolean — True while at least one renderer.atmospherics.hold stands.
if renderer.atmospherics.held() then print("clear air") end
typed/builtin//modules/api/engine/renderer/renderer/atmospherics/hold
renderer.atmospherics.hold(share: number?, options: AtmosphericsHoldOptions?) -> () -> ()
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.
exclusive takes the air 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 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
renderer.atmospherics.release hands the air back by.
Parameters
sharenumber(optional) — How much of the authored air reaches the image, in [0, 1]. Defaults to 0 -- no air at all.optionsAtmosphericsHoldOptions(optional) —owneris 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.exclusivetakes the air for the stated key until the hold is released.
Returns () -> () — A function that releases this hold. Calling it twice releases once.
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" })
typed/builtin//modules/api/engine/renderer/renderer/atmospherics/onChange
renderer.atmospherics.onChange(listener: (number) -> ()) -> () -> ()
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.
Parameters
listener(number) -> ()— Called with the share now in force, in [0, 1].
Returns () -> () — A function that removes this listener.
local stop = renderer.atmospherics.onChange(function(share) print("atmosphere share", share) end)
typed/builtin//modules/api/engine/renderer/renderer/atmospherics/owner
renderer.atmospherics.owner() -> { id: string?, name: string?, share: number, exclusive: boolean }?
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 exclusive is true,
id is the key a hold of its own states to be admitted, and the key
renderer.atmospherics.release hands the air back by. id and name are
nil for a hold that stated no key and that the engine attributes to no
agent.
Returns { id: string?, name: string?, share: number, exclusive: boolean }? — { id, name, share, exclusive } for the standing hold, or nil when the authored air is what reaches the image.
local who = renderer.atmospherics.owner()
if who ~= nil and who.exclusive then print(who.name, "holds the air at", who.share) end
typed/builtin//modules/api/engine/renderer/renderer/atmospherics/release
renderer.atmospherics.release(owner: string) -> number
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.
Parameters
ownerstring— The key the holds to release were taken under -- whatownerstated when they were taken, whichrenderer.atmospherics.ownerreports.
Returns number — How many holds came off the stack.
renderer.atmospherics.release("albedo-read")
typed/builtin//modules/api/engine/renderer/renderer/atmospherics/share
renderer.atmospherics.share() -> number
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 — aerial, a fog density — by this,
and a hold then reaches that medium however it is being driven.
Returns number — A number in [0, 1]. 1 when nothing holds.
local density = state.density * renderer.atmospherics.share()
typed/builtin//modules/api/engine/renderer/renderer/blendedBatching
renderer.blendedBatching() -> boolean
Whether blended neighbours sharing a draw key draw together.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/bounds/clear
renderer.bounds.clear(id: string) -> boolean
Withdraw the box an entity published, so it stops contributing to the entity's reported extent.
Parameters
idstring— Entity id.
Returns boolean — True when there was a published box to withdraw.
renderer.bounds.clear(id)
typed/builtin//modules/api/engine/renderer/renderer/bounds/set
renderer.bounds.set(id: string, min: any?, max: any?) -> boolean
Publish the local-space box an entity's content-drawn geometry occupies.
entity:bounds() and entity:hierarchyBounds() 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.
Parameters
idstring— Entity id.minany(optional) — Local-space minimum corner — a Vec3 table or a 3-element array.maxany(optional) — Local-space maximum corner, same shape asmin.
Returns boolean — True when the box was stored; false for a non-finite or inverted box.
renderer.bounds.set(id, cloud.boundsMin, cloud.boundsMax)
typed/builtin//modules/api/engine/renderer/renderer/captureView/channelId
renderer.captureView.channelId(name: string) -> number?
The debug channel a registered view draws on — what a feature passes as
its pass debugChannel. Nil when no view is registered under name.
Parameters
namestring— The view name.
Returns number? — The channel number or nil.
local debugChannel = renderer.captureView.channelId("lightmap") -- a render feature passes it as an enqueued pass's `debugChannel`
typed/builtin//modules/api/engine/renderer/renderer/captureView/list
renderer.captureView.list() -> { any }
Every registered capture view as { name, channel, description } records
— what backs the discoverability of capture pass=<name> and the
unknown-view error's suggestion list.
Returns { any } — An array of view records.
for _, v in ipairs(renderer.captureView.list()) do ... end
typed/builtin//modules/api/engine/renderer/renderer/captureView/ready
renderer.captureView.ready(name: string) -> boolean
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. A view that registered
a ready 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.
Parameters
namestring— The view name.
Returns boolean — Whether this view's passes have resolved everything drawing needs.
repeat task.wait() until renderer.captureView.ready("zfighting")
typed/builtin//modules/api/engine/renderer/renderer/captureView/register
renderer.captureView.register(name: string, config: any?) -> number
Register (or update) a content capture view under name and return the
debug CHANNEL number assigned to it. A render feature gates its pass to this
channel (debugChannel = channel) so the pass draws only when a capture
selects the view. Idempotent: re-registering the same name keeps its channel.
Parameters
namestring— The view name, selected viacapture pass=<name>.configany(optional) —{ description?, ensure?, ready?, warmup?, renderLayers? }.ensureis called before a capture of this view so the feature that draws it is live (e.g. create it on demand).readyis the view's own statement of whether what it draws is in place: afunction(): (boolean, string?)answering true, or false and the reason. A capture of the view waits on it afterensureand before it renders, so a view whose draw lands over the frames afterensure(an entity it spawns, a feature that reads the stateensurebuilt 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.warmupis 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.renderLayersis 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.
Returns number — The channel number assigned to the view.
local ch = renderer.captureView.register("lightmap", { description = "...", ensure = fn })
typed/builtin//modules/api/engine/renderer/renderer/captureView/resolve
renderer.captureView.resolve(name: string) -> any
Resolve a capture view by name to its { channel, ensure, ready, description, warmup, renderLayers } record, or nil when no view is
registered under name.
Parameters
namestring— The view name.
Returns any — The view record or nil.
local v = renderer.captureView.resolve("lightmap")
typed/builtin//modules/api/engine/renderer/renderer/captureView/unregister
renderer.captureView.unregister(name: string) -> boolean
Withdraw a capture view. A subsequent capture pass=<name> no longer
resolves to it (falls through to the unknown-view error).
Parameters
namestring— The view name.
Returns boolean — True when a view was registered under name.
renderer.captureView.unregister("lightmap")
typed/builtin//modules/api/engine/renderer/renderer/clearShadowHero
renderer.clearShadowHero() -> boolean
Release the hero caster, so every receiver samples the level its footprint selects again.
Returns boolean — Whether a caster was registered.
renderer.clearShadowHero()
typed/builtin//modules/api/engine/renderer/renderer/clearShadowProxy
renderer.clearShadowProxy(mesh: string?) -> number
Stop proxying mesh, so it rasterizes its own geometry into shadow
pages again. Called with no argument, drops every registration.
Parameters
meshstring(optional) — The mesh to stop proxying. Omit to clear all of them.
Returns number — How many registrations were removed.
renderer.clearShadowProxy(statueMesh)
print(renderer.clearShadowProxy(), "proxies dropped")
typed/builtin//modules/api/engine/renderer/renderer/clusterErrorPx
renderer.clusterErrorPx(px: number?) -> number
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.
Parameters
pxnumber(optional) — A finite number above 0. Omit to read the threshold.
Returns number — The threshold in effect.
renderer.clusterErrorPx(0.5) -- finer geometry on this machine
typed/builtin//modules/api/engine/renderer/renderer/collect
renderer.collect() -> RuntimeCollection
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
renderer.texture.list, renderer.mesh.list, renderer.material.list,
renderer.textureMemory() and renderer.gpuMemory() read after it in the
same script no longer count anything it reports released. landed 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 renderer.references explains. Code that cannot yield (a
metamethod, onDestroy, a module's top level while require runs it)
is answered at once, without the wait: there landed is false while a
release is still on its way, and the listings and totals stop counting it
over the drawn frames that follow.
Returns RuntimeCollection — { released = { texture, material, mesh, feature }, kept, entries, landed }: the counts released per kind, how many stayed, every resource's status with action = "released" | "kept", and whether every release had left the device when the call returned.
local c = renderer.collect() print(c.released.texture, c.kept)
local c = renderer.collect() print(c.landed, renderer.gpuMemory().meshes)
typed/builtin//modules/api/engine/renderer/renderer/compiledShaders
renderer.compiledShaders() -> { string }
Every key renderer.compiledSource answers for — one per shader key
(guid@checksum) 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 .shader takes its keys with it. What makes the
composed-source surface enumerable rather than something to guess a key
for.
Returns { string } — An array of keys, sorted.
for _, name in renderer.compiledShaders() do print(name) end
typed/builtin//modules/api/engine/renderer/renderer/compiledSource
renderer.compiledSource(shader: string) -> string?
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
(guid@checksum), a program from renderer.shaderVariants(), 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 shaderRef:compileStatus() reports that failure.
Parameters
shaderstring— A shader key, ashaderVariants()program name, or a reference to a shader asset.
Returns string? — The composed WGSL, or nil when no compile has run for it or its shader was removed.
local status = asset.resolve("myShader", "shader"):compileStatus()
if status.status == "failed" then
print(status.error)
print(renderer.compiledSource("myShader"))
end
typed/builtin//modules/api/engine/renderer/renderer/compositeSize
renderer.compositeSize() -> { width: number, height: number }
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 @scene.color and writes into, and which a
screenSpace = "composite" 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 0 before a frame has drawn.
Returns { width: number, height: number } in pixels.
local c = renderer.compositeSize()
typed/builtin//modules/api/engine/renderer/renderer/cullStats
renderer.cullStats() -> {
What the last completed frame decided to draw. total renderables went
into the frustum test, culled fell outside it and visible survived. Of
those, occlusion culling measured occlusionTested against the depth
pyramid and proved occlusionCulled were entirely behind other geometry —
both 0 while renderer.occlusionCulling() 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 visible and
occlusionTested reads how much of the frame the test could speak for.
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
renderer.shadowViews().
views is the engine cull's own count, one row per camera view that
rendered: view is "main" for the main camera and the camera's entity
id for an offscreen one, kind says which, and frame is the frame the
counts describe. The main view's row is first. drawn 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.
instancesTested renderables were tested;
instancesCulled breaks the rejections down by reason (hidden, layer, range,
frustum, screenSize, and occludedPass1/occludedPass2 for occlusion
against the depth of the frame that last drew the view and then this
frame's); instancesRecovered
came back in the second test. Then the clusters of the survivors:
clustersTested, rejected by the level-of-detail cut
(clustersLodRejected), the frustum (clustersFrustumCulled), facing away
(clustersConeCulled) or occlusion (clustersOccludedPass1, of which
clustersRecovered came back), and clustersDrawn / trianglesDrawn.
binsPending counts material pipelines still compiling, whose clusters wait
a frame; overflow counts clusters that did not fit the view's lists.
total/culled/visible and occlusionTested/occlusionCulled above are
the main view's count for the renderables drawn outside the cluster lists.
Returns { total: number, culled: number, visible: number, occlusionTested: number, occlusionCulled: number, views: { { [string]: any } } }
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
typed/builtin//modules/api/engine/renderer/renderer/debugPass/builtins
renderer.debugPass.builtins() -> { string }
The built-in debug-pass names, one per channel in channel order — the engine's built-in pass vocabulary (final, albedo, normal, depth, …).
Returns { string } — An array of built-in pass names.
for _, n in ipairs(renderer.debugPass.builtins()) do ... end
typed/builtin//modules/api/engine/renderer/renderer/debugPass/channel
renderer.debugPass.channel(name: string) -> number?
The channel a debug-pass NAME renders on: a built-in pass, else a content
capture view registered via renderer.captureView. Nil when the name is
neither — the signal a selector uses to reject an unknown pass.
Parameters
namestring— A debug-pass name (e.g. "normal", "depth", "lightmap").
Returns number? — The channel number, or nil for an unknown name.
local ch = renderer.debugPass.channel("normal") -- 7
typed/builtin//modules/api/engine/renderer/renderer/debugPass/list
renderer.debugPass.list() -> { string }
Every selectable debug-pass name: the built-in passes plus every registered content capture view. What a debug-pass selector offers.
Returns { string } — An array of pass names.
local passes = renderer.debugPass.list()
typed/builtin//modules/api/engine/renderer/renderer/debugPass/name
renderer.debugPass.name(channel: number) -> string?
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.
Parameters
channelnumber— The channel number.
Returns string? — The pass name, or nil.
local name = renderer.debugPass.name(7) -- "normal"
typed/builtin//modules/api/engine/renderer/renderer/destroy
renderer.destroy(handleOrKind: any?, id: string?) -> boolean
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 category 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 renderer.hold and
renderer.references 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
:unload() frees the CPU copy separately.
Parameters
handleOrKindany(optional) — AMeshHandle,TextureHandle,MaterialHandleor feature handle; the id itself; or the kind ("texture","material","mesh","feature") with the id as the second argument.idstring(optional) — The guid or registry key, when the first argument is a kind.
Returns boolean true if a GPU resource was known under the id.
renderer.destroy(meshHandle); renderer.destroy(materialHandle)
renderer.destroy(renderer.texture.list()[1].guid)
renderer.destroy("mesh", guid)
typed/builtin//modules/api/engine/renderer/renderer/deviceGeneration
renderer.deviceGeneration() -> number
Which render device this process is on, counted from the first.
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; engine.onDeviceRebuilt is the hook that fires when it moves.
Returns number — The current device generation, counting from 1.
local generation = renderer.deviceGeneration()
engine.onDeviceRebuilt(function(g) print("device " .. g) end)
typed/builtin//modules/api/engine/renderer/renderer/deviceState
renderer.deviceState() -> string
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.
"ready" is a live device. "rebuilding" 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. "abandoned" 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 "ready" on its own once the platform hands
a device over. Restarting the engine (reloading the page on the web)
builds one from scratch.
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
renderer.deviceGeneration() 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.
Returns string — "ready" | "rebuilding" | "abandoned".
if renderer.deviceState() == "rebuilding" then return end
typed/builtin//modules/api/engine/renderer/renderer/drawDiagnostics
renderer.drawDiagnostics() -> { DrawDiagnostic }
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 (substituted), one the
renderer could bind nothing for at all (outcome = "skipped"), and one
drawing a program whose most recent compile FAILED (stale), 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, programStatus — the compile gate's word about the program
the material NAMED — and the one cause
from shaderCompileFailed / shaderNotRegistered / shaderNotCompiledYet
/ noGbufferEntry / renderStateKeyNotBuilt / noPipelineForTarget /
unshaded, with the compiler's own message in detail or programError.
Covers every renderable the renderer holds, whether or not a camera reached
it: a row with observed = false and outcome = "notDrawn" 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.
Returns { DrawDiagnostic }
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
typed/builtin//modules/api/engine/renderer/renderer/drawStats
renderer.drawStats() -> {
What the last completed frame actually submitted. draws 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 instances 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.
compacted 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. compactedDrawn 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.
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 setOcclusionCulling armed the frame packs the same plan a
second time once the test has answered, and compactedDrawn then counts
what came through occlusion as well.
compactedDrawn comes back from the buffer the GPU wrote, so it describes
a frame that has finished while compacted describes the most recent
plan, and it holds the last count the GPU wrote until another arrives — a
frame that compacts nothing reads compacted 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.
materialBinds is how many times the frame's geometry passes set a
material group's textures (group 1), and materialBindsElided 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 renderer.materialTable().groups). Their sum is how many times the
decision was reached — once per unit of geometry submitted, which sits at
or below draws, since a mesh of several primitives draws once per
primitive under one set of binds. materialExtraBinds and
materialExtraBindsElided 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.
pipelineBinds and pipelineBindsElided 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.
Every figure here is the whole frame's. renderer.shadowViews() carries
the main camera's own share of compacted and compactedDrawn.
clusters and clusterTriangles are the clusters and triangles the
camera views drew from the cluster lists, from the GPU's own count.
drawsByPass splits draws by the part of the frame that submitted
them — visibility (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), resolve (one draw per resolve draw in each
camera view: one per shader, and on the array-bucket backend one per
shader and material group), legacy (geometry drawn outside cluster
storage: skinned, morphed and compute-written meshes, line topology,
shaders that write their own vertex stage), blended, exception
(opaque materials whose depth state the visibility buffer cannot
express), featureGeometry, featureDraw, shadow and splat (one
draw per splat buffer a render feature draws) — and the nine sum to
draws.
instances counts instances named by draws whose size the CPU decided;
the visibility raster's work is clusters and clusterTriangles, which
the GPU decided. pixelsResolved is how many pixels the resolve shaded,
summed over camera views, and pixelsByShader the same split by shader.
clusters, clusterTriangles, pixelsResolved and pixelsByShader
come back from the GPU and describe the frame numbered frame, which is
earlier than the frame draws describes. frame 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
drawStats().frame.
Returns { draws: number, instances: number, compacted: number, compactedDrawn: number, materialBinds: number, materialBindsElided: number, materialExtraBinds: number, materialExtraBindsElided: number, pipelineBinds: number, pipelineBindsElided: number, clusters: number, clusterTriangles: number, drawsByPass: { visibility: number, resolve: number, legacy: number, blended: number, exception: number, featureGeometry: number, featureDraw: number, shadow: number, splat: number }, pixelsResolved: number, pixelsByShader: { [string]: number }, frame: number }
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
typed/builtin//modules/api/engine/renderer/renderer/feature/configure
renderer.feature.configure(handleOrRef: any?, settings: { [string]: any }) -> { [string]: any }
Lay settings 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 ctx.settings from its
next frame.
Parameters
handleOrRefany(optional) — ARenderFeatureHandle, its guid, or the feature's ref.settings{ [string]: any }— The knob values to apply.
Returns { [string]: any } — The feature's full settings now in force.
renderer.feature.configure(h, { intensity = 0.5, quality = "medium" })
typed/builtin//modules/api/engine/renderer/renderer/feature/create
renderer.feature.create(ref: any?, opts: (string | { guid: string?, settings: { [string]: any }? })?) -> any
Instantiate a render feature so the engine calls its render(ctx) hook
every frame. ref is an AssetRef<renderFeature> whose init.luau returns
{ setup?, render, teardown? }. Returns a live RenderFeatureHandle (its
guid is the stable id, same as mesh/texture handles); tear it down with
renderer:destroy(handle). Pass guid to assign a specific id.
Parameters
refany(optional) — AnAssetRef<renderFeature>, or a string identity/guid resolved viaasset.resolve(ref, "renderFeature").opts(string | { guid: string?, settings: { [string]: any }? })(optional)
Returns any — A RenderFeatureHandle.
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 } })
typed/builtin//modules/api/engine/renderer/renderer/feature/destroy
renderer.feature.destroy(handleOrGuid: any?) -> boolean
Tear down a live render feature by its RenderFeatureHandle OR its guid
string — the by-id path for when the handle was lost (e.g. across execute
calls). Same effect as renderer.destroy(handle). Returns true if a feature
was live under that id.
Parameters
handleOrGuidany(optional) — ARenderFeatureHandleor itsguidstring.
Returns boolean
renderer.feature.destroy("eb623975-d8bd-47a1-a0a4-5c0e56238e2f")
typed/builtin//modules/api/engine/renderer/renderer/feature/find
renderer.feature.find(ref: any?) -> any
The live handle of the feature running ref — an AssetRef
Parameters
refany(optional) — An AssetRef, a renderFeature identity, or a guid.
Returns any — The RenderFeatureHandle, or nil.
local h = renderer.feature.find("@builtin::systems.reflections.ssr")
typed/builtin//modules/api/engine/renderer/renderer/feature/list
renderer.feature.list() -> { { guid: string, identity: string, settings: { [string]: any } } }
List every render feature currently live (running its render(ctx) each
frame). Each entry is { guid, identity, settings } — the guid is the same
id a RenderFeatureHandle carries, so you can tear a feature down by guid
even after losing its handle (e.g. across separate execute calls), and
settings is every knob the feature declares at the value in force.
Returns { { guid: string, identity: string, settings: { [string]: any } } }
for _, f in renderer.feature.list() do print(f.identity, f.guid) end
typed/builtin//modules/api/engine/renderer/renderer/feature/schema
renderer.feature.schema(ref: any?) -> { [string]: any }
The settings a .renderFeature declares, with each knob's descriptor,
read without running it: what a caller may pass to create / configure.
Parameters
refany(optional) — An AssetRefor a renderFeature identity.
Returns { [string]: any } — { [name]: descriptor }, empty for a feature that declares none.
for name, desc in pairs(renderer.feature.schema("@builtin::systems.reflections.ssr")) do print(name, desc.default) end
typed/builtin//modules/api/engine/renderer/renderer/feature/settings
renderer.feature.settings(handleOrRef: any?) -> { [string]: any }?
A live feature's settings as they stand: every declared knob at its
default or at what create / configure set. A copy — writing to it
changes nothing; configure does.
Parameters
handleOrRefany(optional) — ARenderFeatureHandle, its guid, or the feature's ref.
Returns { [string]: any }? — The settings table, or nil when no such feature is live.
local s = renderer.feature.settings(h); print(s.intensity)
typed/builtin//modules/api/engine/renderer/renderer/feature/shaded
renderer.feature.shaded() -> { [string]: number }
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 bounds 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.
Returns { [string]: number } — { [shader: string]: number } — pixels shaded, last drawn frame.
local px = renderer.feature.shaded(); for name, n in pairs(px) do print(name, n) end
typed/builtin//modules/api/engine/renderer/renderer/featureTexture/configure
renderer.featureTexture.configure(width: number, height: number, layers: number)
Size the shared feature-texture array — the layers a surface shader reads
through zero_feature_texture(uv, layer), and the layers a SpotLight
projects through its cone via cookieLayer. Layers are rgba16f.
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. renderer.featureTexture.state() reports the extent and the layers
holding content, which is how a feature re-fills the layer a resize took
from it.
Parameters
widthnumber— Layer width in pixels.heightnumber— Layer height in pixels.layersnumber— How many layers the array holds.
renderer.featureTexture.configure(512, 512, 4)
typed/builtin//modules/api/engine/renderer/renderer/featureTexture/setLayer
renderer.featureTexture.setLayer(layer: number, textureKey: string, x: number, y: number)
Copy a texture already on the GPU into one layer of the shared array,
its top-left corner at (x, y) — 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 rgba16f and fit at that offset.
Parameters
layernumber— Which layer of the array to write into.textureKeystring— The source texture's name — the one it was created under. Acompute.createStorageTexture2Dtarget, acompute.createTextureHistorypair (its current side), and a texture acompute.copyBufferToTexturewrote all answer to the name they were given.xnumber— Left edge of the destination rectangle, in pixels.ynumber— Top edge of the destination rectangle, in pixels.
compute.createStorageTexture2D("gobo", { width = 256, height = 256, format = "rgba16f" })
renderer.featureTexture.setLayer(0, "gobo", 0, 0)
typed/builtin//modules/api/engine/renderer/renderer/featureTexture/state
renderer.featureTexture.state() -> {
What the shared feature-texture array is right now: the extent every
layer carries, and filled, the ascending 0-based indices of the layers a
setLayer 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 configure that
changed the size reallocates and zeroes every layer, and the layer it
emptied leaves filled without it. Measured off the renderer at the end of
the last rendered frame, so a configure or setLayer issued this frame
reads back on a later one.
What this describes is the array a shader samples. The source texture a
setLayer copied FROM is a GPU resource of its own and keeps the bytes it
was written with for as long as it lives, so filled is the reading that
answers whether the layer behind a cookieLayer is live right now.
fills counts, per layer, the setLayers that have landed in it since the
array was last sized: fills[layer + 1] for a 0-based layer. A fill of a
layer that already holds content leaves filled as it was, so the count
moving is how a feature re-filling its own layer reads its fill arriving.
Returns { width, height, layers, filled, fills }
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
typed/builtin//modules/api/engine/renderer/renderer/framePacing
renderer.framePacing() -> FramePacing?
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.
framesInFlight is how many submitted frames have not reported done
through the queue's completion signal, held under maxFramesInFlight: 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.
mechanism names how that bound is enforced
here: submission-wait waits for the frame that many frames back and
reports the wait in waitMs, so a paced frame costs latency and still
draws; submitted-work-done counts outstanding frames off the queue's
completion signal and declines to start a frame while the bound is met,
counting those in pacedFrames and leaving the last presented image up.
submittedFrames 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.
stalled 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 stalled answers
for a GPU that went silent. standDowns counts those over the session,
which is how a session that lost the signal and got it back
(stalled = false with standDowns above zero) reads apart from one that
never lost it.
producing is whether the renderer is drawing frames at all. 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, counting them in idleSkippedFrames. Every other
renderer stat answers with the last frame that drew, so producing is what
separates a live reading from a frozen one. A windowed renderer presents
every frame it draws and reads producing = true throughout.
presentMode is what the surface presents with and presentModes what it
offers; both are empty of meaning on a headless renderer, which never
presents. A desktop presents mailbox unless told otherwise, and Android
presents fifo, which shows each frame on a vsync of the panel and makes
no frame the panel never shows.
The rest describe the rate frames run at. displayRefreshHz 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. frameRateCap is the cap
renderer.setFrameRateCap set, targetFps the rate that cap delivers
and refreshesPerFrame the refreshes each frame stays on screen under it,
all three absent while no cap is set. deliveredFps is the rate the frame
loop has run frames at over about the last twenty frames.
Returns FramePacing? — { framesInFlight, maxFramesInFlight, pacedFrames, submittedFrames, waitMs, mechanism, stalled, standDowns, producing, idleSkippedFrames, presentMode, presentModes, displayRefreshHz?, frameRateCap?, refreshesPerFrame?, targetFps?, deliveredFps? }, or nil before the renderer has drawn a frame
local p = renderer.framePacing()
print(("%d of %d frames in flight, paced by %s"):format(p.framesInFlight, p.maxFramesInFlight, p.mechanism))
typed/builtin//modules/api/engine/renderer/renderer/getRaytrace
renderer.getRaytrace() -> boolean
Whether ray tracing is currently enabled.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/gpuMemory
renderer.gpuMemory() -> GpuMemory
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.
Three figures answer three different questions, and they are meant to be read against each other:
- The categories —
shadow,textures,meshes,instances,compute, summing tocategorised— are the renderer's own accounting of what it asked for on purpose. Always present, on every backend.texturesis what the resident textures occupy, and on the texture-array backend also the slack its shared arrays hold beyond them (renderer.materialTable().bucketSlackBytes). allocatoris 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, orreleased_scratch[<width>x<height>].colorwhile it waits for the next target of its size. It exceedscategorisedby 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, soreservedBytesruns aboveallocatedBytesby what those blocks hold unused;blockslists them emptiest first with the labels that keep each one alive, andemptyBytesplusslackBytesis that distance exactly — the pool held in empty blocks, and the room pinned inside blocks something still sits in.driver.deviceLocalBytesis 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 reportsavailable = falseand the reason.driver.outsideAllocatorBytesis 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 andrenderer.collect()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.
compute is what the compute subsystem holds; compute.observe() names
each of those resources and what it costs. arenas states the part of it
the pages runs stand in hold, and what those pages' bytes are spent on. renderTargets counts the
offscreen render targets the renderer holds at that frame, which is what
says a renderer.destroy has been applied rather than queued.
textureBudgetBytes is the texture budget in force and pressure the
memory pressure level that shapes it: under "critical" the budget reads
half the device's, and renderer.memoryPressure() says what the newest
report released.
Returns GpuMemory — The accounting — see GpuMemory. The category figures are zeroed until the renderer has published its first frame; driver is read as the call runs and answers from the first.
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
typed/builtin//modules/api/engine/renderer/renderer/hold
renderer.hold(handleOrKind: any?, id: string?) -> boolean
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 renderer.collect() alike — until renderer.release
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 renderer.mesh.isResident(guid)
is the separate question about the buffers.
Parameters
handleOrKindany(optional) — The resource's handle, or its kind ("texture","material","mesh","feature") with the guid or key as the second argument.idstring(optional) — The guid or key, when the first argument is a kind.
Returns boolean true when the registry knows the resource.
renderer.hold(tex)
renderer.hold("material", "swatch")
typed/builtin//modules/api/engine/renderer/renderer/instanceData/clear
renderer.instanceData.clear(target: string | entityRef)
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.
Parameters
targetstring | entityRef— The entity — a proxy fromentity(...)/entity.spawn(...), or an entity-id string.
renderer.instanceData.clear(subject)
typed/builtin//modules/api/engine/renderer/renderer/instanceData/laneCount
renderer.instanceData.laneCount() -> number
How many vec4 lanes each entity's per-instance block holds, so a lane
index runs 0 .. laneCount() - 1. The same count a surface shader indexes
input.shader_data against.
Returns number — lanes per entity.
for lane = 0, renderer.instanceData.laneCount() - 1 do
renderer.instanceData.set(subject, lane, 0)
end
typed/builtin//modules/api/engine/renderer/renderer/instanceData/set
renderer.instanceData.set(target: string | entityRef, lane: number, x: number, y: number?, z: number?, w: number?)
Write one vec4 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 input.shader_data[lane], 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.
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.
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.
Parameters
targetstring | entityRef— The entity — a proxy fromentity(...)/entity.spawn(...), or an entity-id string. It must name a live entity.lanenumber— Whichvec4lane to write,0 .. laneCount() - 1.xnumber— The lane's.x.ynumber(optional) — The lane's.y. Defaults to 0.znumber(optional) — The lane's.z. Defaults to 0.wnumber(optional) — The lane's.w. Defaults to 0.
-- 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
typed/builtin//modules/api/engine/renderer/renderer/lighting
renderer.lighting() -> boolean
Whether the scene renders lit.
Returns boolean — true while lighting is on.
typed/builtin//modules/api/engine/renderer/renderer/loseDevice
renderer.loseDevice()
Destroy the render device on the next frame, so the engine meets a real device loss.
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,
GET /engine/status reports the renderer as recovering while it does,
engine.onDeviceRebuilt fires afterwards, and renderer.deviceGeneration()
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.
renderer.loseDevice()
-- some frames later:
print(renderer.deviceGeneration()) -- one higher than before
typed/builtin//modules/api/engine/renderer/renderer/mainCameraView
renderer.mainCameraView() -> { number }?
The main camera's inverse view-projection (column-major, 16 numbers)
followed by its world position (3 numbers) — {m0..m15, px,py,pz} — for
reconstructing world positions from the depth buffer in a ray-tracing pass.
Nil before the first render.
Returns { number }? 19 numbers, or nil.
typed/builtin//modules/api/engine/renderer/renderer/markBaseline
renderer.markBaseline() -> number
Record which render features are live and how the feature-texture array
is configured, and make that the point renderer.resetToBaseline returns
to.
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.
Returns number — The mark's generation, counting from 1 — what renderer.resetToBaseline takes to restore to this mark.
local mark = renderer.markBaseline() -- then run, then renderer.resetToBaseline(mark)
typed/builtin//modules/api/engine/renderer/renderer/material/animatedTexture
renderer.material.animatedTexture(texture: string | AssetRef, opts: { [string]: any }?) -> MaterialHandle
Build a material that PLAYS a layered texture: its layers bound as the
frames, its timing bound beside them, and the engine's animatedTexture
shader turning the clock into the layer showing now. One call from an
imported animated image to a material an entity can wear.
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. speed scales the clock (2 plays twice as
fast, 0 holds the frame startTime lands in) and startTime offsets into
the sequence, so two surfaces sharing one texture can run out of phase.
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. speed = 0 holds one frame for as long as it
is set, which is the state to compare two screenshots in.
The returned handle is what a surface wears — Model:applySessionMaterial
takes it, and so does a Model's material field. The handle's guid is
this material's REGISTRY KEY, the currency of setProperty, describe and
destroy; a component field resolves an asset, so a bare key in one leaves
the component waiting for an asset to register under that name.
The builtin plane mesh emits uv = (u, v) with v along its own +Z, so
a quad pitched +90° about X (Transform.eulerToQuat(0, math.pi / 2)) shows
the image upright to a camera on +Z, and -90° shows it first-row-last.
A texture whose layers carry no timing is rejected — there is nothing to
play. renderer.texture.info(bytes).animated is the test.
Parameters
texturestring | AssetRef— The texture — a guid, an identity, a name, a path, or a textureAssetRef.opts{ [string]: any }(optional) —{ key?, speed?, startTime?, alphaCutoff?, baseColor?, uvScale?, uvOffset? }.
Returns MaterialHandle
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)
typed/builtin//modules/api/engine/renderer/renderer/material/create
renderer.material.create(content: MaterialContent, key: string) -> MaterialHandle
Parameters
contentMaterialContentkeystring
Returns MaterialHandle
typed/builtin//modules/api/engine/renderer/renderer/material/describe
renderer.material.describe(key: string | { [string]: any } | AssetRef) -> any
The recoverable definition ({ shader, properties, textures, name, render, features, bindings })
this module registered under key via renderer.material.create, or nil
for keys registered elsewhere (e.g. material assets resolved by the
assetType). properties and textures carry the material's current values:
each setProperty / setTexture 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. renderer 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 (renderer.textures,
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 (renderer.materialCost(),
renderer.observe() and renderer.drawDiagnostics() each keep it
observing for the next 120 drawn frames, and describe does not), so
renderer.observed states whether they were counted: true with the
counts, false with draws, instances, placeholderDraws, binds and
bindsElided all nil. renderer is nil when the renderer holds no
material under this key at all, and resident states the same fact as a
boolean. Writes reach the screen through both halves: resident = false
says the renderer holds nothing to put them in, and renderer.draws = 0
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,
renderer.drawDiagnostics() names the renderable and the cause. A
material that is not resident carries shaderStatus and reason, where
its shader stands: "absent" when the shader it names resolves to no
shader asset (nothing will compile it, so it stays unprepared until one
arrives), "pending" while that shader has not compiled, "failed" with
the compiler's message, or "compiled" when the shader is ready and the
material has not been prepared yet. A resident material carries neither.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.
Returns any — MaterialContent? with resident: boolean, renderer: MaterialObservation?, shaderStatus: string?, reason: string? and, for a key a .material asset filed, asset (the asset it is) fields
typed/builtin//modules/api/engine/renderer/renderer/material/destroy
renderer.material.destroy(key: string | { [string]: any } | AssetRef) -> boolean
Drop a runtime material registered via renderer.material.create: 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.
The reach is the registry: after this, describe and list stop answering
for the key. A surface already wearing the handle goes on drawing what it
was given — Model:restoreSessionMaterial is what puts a Model back on its
authored material.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key (the one passed tocreate), theMaterialHandlecreatereturned, or anAssetReffromasset.resolve.
Returns boolean true when a definition was known under key.
renderer.material.destroy("__preview_swatch_" .. texGuid)
typed/builtin//modules/api/engine/renderer/renderer/material/list
renderer.material.list() -> { any }
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 .material asset filed (guid@checksum) the asset it is.
renderer.references("material", key) says what is still holding a row,
and renderer.collect() releases the rows nothing holds.
Returns { any } — Array of { guid, origin, owner?, shader?, asset? }.
for _, m in ipairs(renderer.material.list()) do print(m.asset or m.guid, m.shader) end
typed/builtin//modules/api/engine/renderer/renderer/material/renderState
renderer.material.renderState(key: string | { [string]: any } | AssetRef) -> MaterialObservation?
What the renderer holds for a material, which is a different document
from the values written to it. index is the material index its row in
the material table is filed under, which an edit rewrites in place, and
textures 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 setTexture the renderer has taken up reads here
as the key it bound. shader is the program its prepared bind
group was built against, renderState the blend / cull / topology / queue /
depth key its draws are looked up under, keyBuilt whether a pipeline
exists for that key, and draws / instances / placeholderDraws /
binds / bindsElided what it cost in the frame the renderer last
published. A frame counts those only while something observes the
renderer, which renderer.materialCost() and renderer.drawDiagnostics()
start, so observed states whether they were counted: true with the
five counts, false with all five nil. nil means the renderer holds no material
under this key at all — the writes landed on a record nothing is drawing
with.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.
Returns MaterialObservation?
local r = renderer.material.renderState("water"); print(r.renderState.blend, r.keyBuilt)
typed/builtin//modules/api/engine/renderer/renderer/material/sessionKeyFor
renderer.material.sessionKeyFor(entityId: string) -> string
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 Model:applySessionMaterial, and the component re-adopts it across
VM reloads by probing this key with describe. 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.
Parameters
entityIdstring— The entity carrying the material.
Returns string — The registry key string.
local key = renderer.material.sessionKeyFor(entityId)
typed/builtin//modules/api/engine/renderer/renderer/material/setClock
renderer.material.setClock(key: string | { [string]: any } | AssetRef, seconds: number?, rate: number?) -> ()
Drive a material's surfaces from a clock of its own: the time its
shader's vertex(), vertex_clip() and fragment() read. The clock reads
seconds now and runs on by itself, rate 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 seconds, slow motion is a rate
below 1. Every surface drawing the material follows it, motion vectors
included. seconds = nil 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.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.secondsnumber(optional) — The time the material's surfaces read now, or nil to release it.ratenumber(optional) — Seconds of the clock per second of gameplay time (default 1).
Returns ()
renderer.material.setClock(asset.resolve("frost_ice", "material"), 12.5, 0)
typed/builtin//modules/api/engine/renderer/renderer/material/setProperty
renderer.material.setProperty(key: string | { [string]: any } | AssetRef, name: string, value: any?) -> ()
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 describe reports, and — for a property the material's shader
declares, which is what the uniform buffer is packed by — what a material
AssetRef reads back through getProperty / getProperties and what the
surface is drawn with. A write under any other name reaches the record
describe reports, which is where it reads back.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.namestring— Property name.valueany(optional) — New value.
Returns ()
renderer.material.setProperty(asset.resolve("wall", "material"), "roughness", 0.2)
typed/builtin//modules/api/engine/renderer/renderer/material/setTexture
renderer.material.setTexture(key: string | { [string]: any } | AssetRef, slot: string, ref: string | { [string]: any } | AssetRef) -> boolean
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.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.slotstring— Texture slot name ("base_color_texture", …).refstring | { [string]: any } | AssetRef— Texture reference — a.textureguid / identity / name / path, the image path it was imported from, acolor:/default:form, a live GPU handle, or a textureAssetRefcarrying one. An asset reference is materialised (Disk→CPU→GPU) and bound by the key the upload lands under.
Returns boolean — true when the slot was bound, false when the shader declares no texture slot under that name.
renderer.material.setTexture("sky", "sky_texture", "panorama.texture")
typed/builtin//modules/api/engine/renderer/renderer/material/vocabulary
renderer.material.vocabulary(key: string | { [string]: any } | AssetRef) -> MaterialVocabulary?
A material's property names under props and its shader-declared and
assigned texture slots under textureSlots, each a { [name] = true } set —
with byRole 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 MaterialSchema.route 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; describe 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.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.
Returns MaterialVocabulary? — nil for a key registered elsewhere, and for a record carrying no property, assigned texture, or declared texture slot.
local MaterialSchema = require("@builtin::modules.material_schema")
local route, name = MaterialSchema.route(assert(renderer.material.vocabulary(handle)), "color", { 1, 0, 0, 1 })
typed/builtin//modules/api/engine/renderer/renderer/materialCost
renderer.materialCost() -> { MaterialObservation }
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". draws and instances cover that one frame; placeholderDraws
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.
group 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. binds 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 bindsElided 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 placeholderDraws and in neither bind count.
uniformBytes 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. renderer.materialTable().rowBytes is the space the table
reserves for all rows — its row capacity, free rows included — plus its
directories. renderer.drawDiagnostics() names WHICH
renderable is not drawing what its material says, and why.
material is the key the renderer holds the material under; asset
names the .material asset a guid@checksum key is a version of.
Returns { MaterialObservation }
for _, m in renderer.materialCost() do print(m.asset or m.material, m.draws, m.binds, m.bindsElided) end
typed/builtin//modules/api/engine/renderer/renderer/materialIdentity
renderer.materialIdentity() -> MaterialIdentity
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. slots 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. renderables 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; populations 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
zero_instance(slot).material_index, and the row a ray hit resolves
through zeroMaterial(). A renderable draws with the material its entity
references, so one whose entity names none carries index 0.
Returns MaterialIdentity — { slots: { [string]: number }, renderables: { { entity: string, slot: number, index: number, material: string } }, populations: { { slot: number, count: number, index: number, material: string } } }
local id = renderer.materialIdentity()
for _, r in id.renderables do print(r.entity, r.slot, r.index, r.material) end
typed/builtin//modules/api/engine/renderer/renderer/materialIndex
renderer.materialIndex(name: string) -> number?
The index standing for a material, or nil 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 zero_instance(slot).material_index) to tell which material a
drawing instance carries.
Parameters
namestring—stringMaterial name, asrenderer.material.createfiled it.
Returns number?
local red = renderer.materialIndex("brick_red")
typed/builtin//modules/api/engine/renderer/renderer/materialTable
renderer.materialTable() -> MaterialTable
How this device resolves material textures, and what that storage
costs. backend is "bindless" (a descriptor array indexed by each
material's texture handles) or "buckets" (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.
materials is how many materials have a row, rowBytes the space the
table reserves in the scene buffer (its row capacity, free rows included,
plus the material and texture directories), textures how many distinct
textures the rows name, descriptorCapacity the length of each bindless
array (0 on buckets), and groups 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.
buckets 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). bucketBytes is what the shared buckets occupy
together, and bucketSlackBytes the slack they hold together, which
renderer.setTextureBucketSlack bounds; an external bucket counts in
neither. slackBudgetBytes is the budget the buckets bound growth by. On
bindless buckets is empty, both totals are 0, and slackBudgetBytes is
the budget recorded, which bounds nothing.
Returns MaterialTable
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
typed/builtin//modules/api/engine/renderer/renderer/maxAnisotropy
renderer.maxAnisotropy() -> number
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 —
renderer.setAnisotropy clamps for you, so a request never needs guarding.
Returns number — The device ceiling, 1 or 16.
local best = renderer.maxAnisotropy()
typed/builtin//modules/api/engine/renderer/renderer/maxViewportExtent
renderer.maxViewportExtent() -> number
The largest number of pixels this device draws a surface at, per axis
— the bound renderer.setViewportSize refuses past. 0 before the first
frame has drawn.
Returns number — pixels per axis.
local bound = renderer.maxViewportExtent()
typed/builtin//modules/api/engine/renderer/renderer/memoryPressure
renderer.memoryPressure() -> MemoryPressureReport
What memory pressure is doing to the engine. level is the level in
force: "normal", "moderate", "low" or "critical". A report puts
its level in force, whatever stood before, for holdSeconds after the
last report, and then the level returns to normal. Reports come from the
system (on Android, onTrimMemory: 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
renderer.signalMemoryPressure. signals
counts the reports taken; lastLevel, lastSource and lastAgeSeconds
describe the newest (absent before the first).
Each report releases at once what its level releases, and released says
what the newest one freed: the textures taken to the level the frame
samples and the textureBytes they gave up, and the render targets
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 textureBudgetBytes is the
budget the level leaves in force. engine.onMemoryPressure(cb) fires with
each report once its release is done.
Returns MemoryPressureReport — { level, holdSeconds, signals, lastLevel?, lastSource?, lastAgeSeconds?, textureBudgetBytes, released }
local mp = renderer.memoryPressure()
print(mp.level, mp.released.textureBytes / 1048576, "MiB freed")
typed/builtin//modules/api/engine/renderer/renderer/mesh/bounds
renderer.mesh.bounds(mesh: string | { [string]: any } | AssetRef) -> Aabb?
The mesh's extent in its own space: the box its vertices span, or the
one its author stated (aabbMin / aabbMax) 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 instanceBounds for an instanced population.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns Aabb? — The box, or nil for a mesh nothing is registered under or whose extent nobody knows.
local box = renderer.mesh.bounds(mesh)
typed/builtin//modules/api/engine/renderer/renderer/mesh/boundsSource
renderer.mesh.boundsSource(mesh: string | { [string]: any } | AssetRef) -> string
Where this mesh's culling bounds come from. "compute" 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. "geometry" otherwise: the AABB of the geometry the
mesh was created with.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns string — "compute" or "geometry".
print(renderer.mesh.boundsSource(mesh))
typed/builtin//modules/api/engine/renderer/renderer/mesh/buildClusters
renderer.mesh.buildClusters(mesh: string | { [string]: any } | AssetRef) -> string?
Build a cluster-LOD DAG (Nanite-style virtualized geometry) for the
static CPU mesh held under guid and return its serialized data.clusters
bytes. Returns nil when the mesh is degenerate, and on an engine whose
renderer.mesh.canBuildClusters reports false. Pair with
renderer.mesh.uploadClusters.
Parameters
meshstring | { [string]: any } | AssetRef— A mesh loaded into the CPU store (renderer.mesh.loadCpu) — theMeshCpuHandle, aMeshHandle, a guid, or a meshAssetRef.
Returns string? — Serialized cluster bytes, or nil.
local cb = renderer.mesh.buildClusters(cpu)
typed/builtin//modules/api/engine/renderer/renderer/mesh/canBuildClusters
renderer.mesh.canBuildClusters() -> boolean
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 renderer.mesh.buildClusters.
Returns boolean — True if renderer.mesh.buildClusters can bake on this platform.
if renderer.mesh.canBuildClusters() then ... end
typed/builtin//modules/api/engine/renderer/renderer/mesh/clusterBakeBudget
renderer.mesh.clusterBakeBudget(ms: number?) -> number
The wall time one frame may spend advancing scheduled cluster bakes, in
milliseconds — set first when ms 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.
Parameters
msnumber(optional) — New per-frame budget in milliseconds, capped at 1000. A value that is not a positive, finite number raises.
Returns number — The budget in force after the call.
renderer.mesh.clusterBakeBudget(2)
typed/builtin//modules/api/engine/renderer/renderer/mesh/clusterBakes
renderer.mesh.clusterBakes() -> { [string]: any }
What the scheduled cluster bakes are costing. budgetMs is the slice a
frame may spend, pending how many bakes are queued, completed how many
have finished since the engine started, dropped how many left the queue
because the geometry they were scheduled over stopped being readable or
cannot be drawn as clusters, and
heldBytes the source geometry the queue is holding across all of them —
the vertex pool and index run the bake at the head is reading.
inFlight is one row per queued bake —
{ guid, cpuMs, frames, slices, bytes, state, levels }: 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, "baking" for the one being
advanced against "queued" for the ones waiting their turn, and levels
the depth the bake builds, "dag" or "single".
Returns { [string]: any } — { budgetMs, pending, completed, dropped, heldBytes, inFlight }.
print(renderer.mesh.clusterBakes().heldBytes)
typed/builtin//modules/api/engine/renderer/renderer/mesh/clusterComponents
renderer.mesh.clusterComponents(clusterBytes: buffer | string) -> (ClusterComponents?, string?)
Split a cluster blob (from renderer.mesh.buildClusters) 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 vertexOffset indexes the geometry
pool in u32 elements while its indexOffset 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 (shaderRef:createBuffer +
buf:writeBytes) to drive a cluster draw from Luau.
Parameters
clusterBytesbuffer | string— Serialized cluster bytes (binary-safe).
Returns (ClusterComponents?, string?) — { vertices, geometry, records, vertexCount, vertexRefCount, triangleBytes, indexCount, clusterCount }, or (nil, err).
local c = renderer.mesh.clusterComponents(cb)
typed/builtin//modules/api/engine/renderer/renderer/mesh/clusterStore
renderer.mesh.clusterStore() -> {
What the geometry heap holds for cluster drawing. heapCapacity and
heapLive 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. vertexBytes and
lightmapBytes are the vertex pools and lightmap-UV runs of the meshes
drawn through clusters. bakedBytes is the cluster sets of meshes drawing a
baked set; unbakedBytes is the sets of meshes still drawing lod0 plus
those meshes' vertex pools — geometry drawn at full detail while its bake is
outstanding. meshes counts meshes drawing any set, unbakedMeshes those
on lod0. scriptSetBytes is CPU memory beside the heap: the sets
scripts installed with renderer.mesh.uploadClusters, held so each mesh
draws its set again after it returns to the device.
Returns { heapCapacity: number, heapLive: number, vertexBytes: number, lightmapBytes: number, bakedBytes: number, unbakedBytes: number, meshes: number, unbakedMeshes: number, scriptSetBytes: number }
local s = renderer.mesh.clusterStore(); print(s.unbakedBytes / 1048576, "MiB drawn at full detail while bakes are outstanding")
typed/builtin//modules/api/engine/renderer/renderer/mesh/clusters
renderer.mesh.clusters(mesh: string | { [string]: any } | AssetRef) -> { [string]: any }?
The cluster set a mesh draws. Every static mesh the geometry heap holds
draws through clusters: kind is "lod0" while it draws the full-detail
set built from its geometry as it uploaded, "single" for one baked level
(clusterLod = false), and "dag" for the baked LOD hierarchy.
clusterCount, levelCount and triangleCount describe that set, nodes
how many node transforms its clusters lie in, and bytes its size in the
heap. A mesh that is not drawn through clusters reads resident = false
with reason: "heapFull" (the heap cannot grow to hold it),
"spansNodes" (a triangle joins two independently moving parts) or
"deformable" (a pass deforms its vertices). Nil for a mesh the renderer
holds no GPU geometry for.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to read — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns { [string]: any }? — { resident: boolean, reason: string?, kind: string?, clusterCount: number, levelCount: number, triangleCount: number, nodes: number, bytes: number }?
local c = renderer.mesh.clusters(gpu) ; if c and c.kind == "lod0" then print("still baking") end
typed/builtin//modules/api/engine/renderer/renderer/mesh/create
renderer.mesh.create(src: any?, guid: string?) -> MeshHandle
Create (or fetch) a GPU mesh resource and return its MeshHandle. src:
a MeshCpuHandle from meshRef:load() (CPU→GPU upload under the asset's
guid, idempotent — returns the resident handle if already uploaded); raw
geometry {positions, indices, normals?, uvs?, colors?, uvs1?, unwrapUvs?, tangents?, skinning?, skins?} (a new runtime mesh — uvs1 is the lightmap
UV set, unwrapUvs generates one, skinning/skins bind a skeleton); GPU
compute buffers {vertexBuffer, indexBuffer, vertexCount, indexCount, aabbMin?, aabbMax?, prevVertexBuffer?} (size the vertex buffer at
vertexCount * engine.vertexStride bytes, the engine's standard Vertex
layout); or a MeshHandle (returned as-is). NEVER takes an AssetRef —
load the CPU first.
prevVertexBuffer 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.
morphTargets are the shapes the mesh can blend towards: a list of
{ name?, positions, normals? } records, each holding one offset per vertex
from the base geometry, in the mesh's own vertex order. An entity blends
them with ecs.MorphWeights, weight i scaling target i. A name makes
the shape addressable as itself — renderer.mesh.morphTargets reads the
names back and renderer.mesh.morphWeights drives them by name.
Raw geometry is read against the mesh type's conventions: indices count
vertices from 0, and a triangle's FRONT face is the one whose vertices turn
counter-clockwise as the viewer sees them — cross(v1 - v0, v2 - v0) 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 render = { cull = "none" }, to draw that side. normals
give the surface its outward direction and shade the face; the side that
draws comes from the index order alone. uvs sample (0,0) at the image's
top-left. Model space carries the world's basis: +X right, +Y up, -Z the
direction transform.forward points. guides { path = "types/mesh" } has
the whole table.
A geometry src carrying keepCpu = true also keeps its geometry in the
guid-keyed CPU store, so renderer.mesh.getVertices reads it and
renderer.mesh.setVertices rewrites its positions in place — the per-frame
deformation path, which sends positions alone where renderer.mesh.update
re-sends the whole geometry. renderer.mesh.unloadCpu(mesh) releases that
copy. Without it the geometry lives on the GPU alone and
renderer.mesh.readback(mesh) is what brings it back.
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
renderer.hold). 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 renderer.mesh.list 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.
A handle a script keeps names its mesh for the rest of the session: a
module table or _SCRATCH holding it carries it across play/edit flips,
scene loads and hot reloads of the components that use it, and a Model
handed it after any of them draws the mesh. While nothing displays the
mesh, the engine holds it off the device (renderer.mesh.heldOffDevice)
and the next display puts it back. Only a destroy ends a mesh a handle
still reaches (renderer.destroy, renderer.mesh.destroy): every handle
a create returned over it then carries released, the verb that released
it, and a point of use (ecs.Mesh, a Model's model field) refuses
such a handle by name until a mesh stands under its guid again, which the
handle then names and holds.
Parameters
srcany(optional) — A MeshCpuHandle, geometry, compute buffers, or a MeshHandle.guidstring(optional) — 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).
Returns MeshHandle
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 } } })
typed/builtin//modules/api/engine/renderer/renderer/mesh/decode
renderer.mesh.decode(zmsh: buffer | string) -> (MeshGeometry?, string?)
Decode engine-native ZMSH bytes back into a MeshGeometry. Inverse of
renderer.mesh.encode; 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 renderer.mesh.geometry(mesh).
Parameters
zmshbuffer | string— Engine-native ZMSH bytes (binary-safe).
Returns (MeshGeometry?, string?) the geometry, or (nil, errmsg).
local geom = renderer.mesh.decode(meshRef:getBytes())
typed/builtin//modules/api/engine/renderer/renderer/mesh/destroy
renderer.mesh.destroy(mesh: string | { [string]: any } | AssetRef) -> boolean
Release the GPU mesh mesh names, the release that pairs with
renderer.mesh.create. Takes every form that names a mesh — the
MeshHandle create returned, the guid renderer.mesh.list hands out, a
MeshCpuHandle or a mesh AssetRef — and routes through
renderer.destroy, 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 :unload().
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to release — aMeshHandle, a guid, aMeshCpuHandleor a meshAssetRef.
Returns boolean true if a GPU mesh was known under the guid.
local m = renderer.mesh.create(cpu); renderer.mesh.destroy(m)
renderer.mesh.destroy(renderer.mesh.list()[1].guid)
typed/builtin//modules/api/engine/renderer/renderer/mesh/drawInstanced
renderer.mesh.drawInstanced(mesh: string | { [string]: any } | AssetRef, opts: any?) -> InstancedDraw
Draw one mesh instanceCount 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.
The buffer holds instanceCount column-major 4x4 matrices, 64 bytes
each, tightly packed — the layout a vertex shader reads as
array<mat4x4<f32>>, 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.
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 :write, a compute
pass whose shader binds it read_write, and every frame a material whose
shader binds it read_write is live), so a population whose buffer
nothing writes costs no copy per frame however many instances it holds;
renderer.uploadStats() counts the copies a frame made.
material is what the population draws with, and it is required: a
MaterialHandle (matRef:handle()), an AssetRef, or a registry key.
instanceDataBuffer names a second buffer, holding 64 bytes per instance —
four vec4 lanes, tightly packed, in instance order. Those lanes arrive in
the fragment stage as zero_object_data(in.instance_id, lane), the same
read a per-entity __instancedata 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.
reserveCount sizes the reservation above instanceCount so
renderer.mesh.setInstanceCount can raise the drawn count later without
re-registering; both buffers must back the reservation, not just the count.
mobility states whether the copies stand still — "static", or
"movable" when it is left out. It is what a scene gather collecting
geometry for precomputed lighting admits a population on, the same
declaration Model.mobility 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.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh the population draws — aMeshHandle, the guidrenderer.mesh.listhands out, aMeshCpuHandleor a meshAssetRef. 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.optsany(optional) —{ transformBuffer, instanceCount, material, instanceDataBuffer?, reserveCount?, renderLayer?, castsShadows?, receivesShadows?, mobility?, parent? }.castsShadowsandreceivesShadows, both true when left out, state whether the copies cast shadows and whether shadows darken them.parentstands the copies in an entity's space, assetInstanceParentdoes.
Returns InstancedDraw — An InstancedDraw handle for instanceInfo / setInstanceCount / dropInstanced.
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 })
typed/builtin//modules/api/engine/renderer/renderer/mesh/dropClusters
renderer.mesh.dropClusters(mesh: string | { [string]: any } | AssetRef) -> boolean
Return a mesh to its lod0 set and cancel a bake still in flight for
it. The inverse of renderer.mesh.uploadClusters.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to return — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean — True if the mesh drew a baked set or a bake was in flight.
renderer.mesh.dropClusters(gpu)
typed/builtin//modules/api/engine/renderer/renderer/mesh/dropInstanced
renderer.mesh.dropInstanced(draw: InstancedDraw) -> boolean
Release an instanced-draw registration and the transform slots it
reserved. The mesh and the transform buffer outlive it — destroy those
through renderer.destroy and the buffer handle's :destroy().
Parameters
drawInstancedDraw— TheInstancedDrawto release.
Returns boolean — True if a registration was live under the handle.
renderer.mesh.dropInstanced(draw)
typed/builtin//modules/api/engine/renderer/renderer/mesh/encode
renderer.mesh.encode(geom: MeshGeometry) -> (string?, string?)
Encode raw geometry into engine-native ZMSH bytes (the on-disk mesh
payload). The CPU codec behind the mesh assetType's onCreate. Every stream
the format carries — including tangents, per-vertex skinning, and the
skeleton — round-trips back through renderer.mesh.decode. This pair moves
DATA the caller is holding; the geometry of a mesh the ENGINE is holding
comes from renderer.mesh.geometry(mesh).
Parameters
geomMeshGeometry—MeshGeometry— flat per-vertex float / u32 arrays plus optionalskinningandskins.
Returns (string?, string?) engine-native ZMSH bytes (binary-safe), or (nil, errmsg) naming what the geometry could not describe.
local bytes = renderer.mesh.encode({ positions = {...}, indices = {...} })
local bytes = renderer.mesh.encode(renderer.mesh.geometry(meshHandle))
typed/builtin//modules/api/engine/renderer/renderer/mesh/encodeCpu
renderer.mesh.encodeCpu(mesh: string | { [string]: any } | AssetRef) -> string
Encode a mesh's resident CPU copy into ZMSH bytes. Reads the ONE
guid-keyed CPU store — meshRef:load() populates it for assets, and
renderer.mesh.readback(mesh) populates it for a runtime mesh. Errors
loudly when the mesh has no resident CPU copy.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns string ZMSH bytes.
local bytes = renderer.mesh.encodeCpu(handle)
typed/builtin//modules/api/engine/renderer/renderer/mesh/geometry
renderer.mesh.geometry(mesh: string | { [string]: any } | AssetRef) -> MeshGeometry
The complete geometry of a mesh the engine is holding, as a
MeshGeometry — the same shape renderer.mesh.create and
renderer.mesh.encode take, carrying every stream the mesh has
(positions, indices, and whichever of normals, uvs, colors,
uvs1, tangents, skinning, skins it was built with). The read that
pairs with create: hand it the MeshHandle create 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 uvs1 == nil 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 .mesh written without a
tangent stream reads back tangents == nil for as long as a CPU copy of it
is resident.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns MeshGeometry
local geom = renderer.mesh.geometry(renderer.mesh.create({ positions = P, indices = I }))
local tangents = renderer.mesh.geometry(handle).tangents
typed/builtin//modules/api/engine/renderer/renderer/mesh/getVertices
renderer.mesh.getVertices(mesh: string | { [string]: any } | AssetRef) -> { any }
Read the vertices of a mesh's resident CPU copy — one entry per vertex,
{ pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }. Reads the resident CPU
store directly (no re-decode). Errors when the mesh has no resident CPU copy
— renderer.mesh.geometry(mesh) is the read that works wherever the mesh
lives, and returns the tangent, colour and skinning streams too.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns { any } — { { pos: {x,y,z}, normal: {x,y,z}, uv: {u,v} }, ... } Each record carries pos, normal, uv and uv1 as named channels ({ x = , y = , z = }). Geometry going the other way — into renderer.mesh.create — is parallel flat arrays (positions, normals, uvs), and create accepts this record list under vertices so a mesh read back here can go straight into a new one.
typed/builtin//modules/api/engine/renderer/renderer/mesh/heldOffDevice
renderer.mesh.heldOffDevice(mesh: string | { [string]: any } | AssetRef) -> boolean
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 Model or instanced draw that names the mesh puts it back
on the device, where renderer.mesh.isResident 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 isResident answers true for
names nothing the engine can draw.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh: aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean
local named = renderer.mesh.isResident(h) or renderer.mesh.heldOffDevice(h)
typed/builtin//modules/api/engine/renderer/renderer/mesh/instanceBounds
renderer.mesh.instanceBounds(draw: InstancedDraw, opts: { keep: number? }?) -> (Aabb?, string?)
The world box a registration's drawn copies occupy: every copy of the
mesh's extent (renderer.mesh.bounds) where the engine composed it for the
last frame drawn, carried through the entity the copies stand in when
setInstanceParent 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.
Parameters
drawInstancedDraw— TheInstancedDrawto measure.opts{ keep: number? }(optional) —{ keep? }— 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:keep = 0.9is the box around where the body of a spray stands, without the few copies thrown far from it.
Returns (Aabb?, string?) — The box, or nil and the reason there is none; a box around origins alone comes with the note that says so.
local box, note = renderer.mesh.instanceBounds(draw)
local body = renderer.mesh.instanceBounds(draw, { keep = 0.9 })
typed/builtin//modules/api/engine/renderer/renderer/mesh/instanceInfo
renderer.mesh.instanceInfo(draw: InstancedDraw) -> InstancedDrawInfo?
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.
status is what the renderer did with it. The fields above it are the
request, made a stage before the renderer sees it; status is the answer:
"drawing" for a registration the renderer is drawing, "refused" for one
it turned away — error carries its reason — and "pending" for the frame
between the call and the renderer answering. So a registration whose copies
are not being drawn says so here.
Parameters
drawInstancedDraw— TheInstancedDrawto report on.
Returns InstancedDrawInfo? — The registration record, or nil.
print(renderer.mesh.instanceInfo(draw).instanceCount)
local info = renderer.mesh.instanceInfo(draw)
if info.status == "refused" then error(info.error) end
typed/builtin//modules/api/engine/renderer/renderer/mesh/instanceTransforms
renderer.mesh.instanceTransforms(draw: InstancedDraw) -> any
Read back the world matrices a registration's drawn copies are placed
by: instanceCount 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.
Parameters
drawInstancedDraw— TheInstancedDrawwhose copies to locate.
Returns any — A Readback to poll — :ready() then :result() — or nil for a registration that is no longer live.
local pending = renderer.mesh.instanceTransforms(draw)
while not pending:ready() do task.wait() end
local floats = pending:result()
typed/builtin//modules/api/engine/renderer/renderer/mesh/instanceWorlds
renderer.mesh.instanceWorlds(draw: InstancedDraw) -> any
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 (setInstanceParent). instanceCount
column-major 4x4 matrices, 16 floats each. Async — poll the handle it
returns the same way as instanceTransforms.
Parameters
drawInstancedDraw— TheInstancedDrawto read.
Returns any — A pending readback: :ready(), then :result() for the floats.
local pending = renderer.mesh.instanceWorlds(draw)
while not pending:ready() do task.wait() end
local x = pending:result()[13]
typed/builtin//modules/api/engine/renderer/renderer/mesh/isCpuResident
renderer.mesh.isCpuResident(mesh: string | { [string]: any } | AssetRef) -> boolean
True if this mesh has a resident CPU copy in the guid-keyed CPU store.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean
if renderer.mesh.isCpuResident(handle) then ... end
typed/builtin//modules/api/engine/renderer/renderer/mesh/isResident
renderer.mesh.isResident(mesh: string | { [string]: any } | AssetRef) -> boolean
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 renderer.mesh.drawInstanced and a Model 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.
renderer.mesh.onDevice is the answer that waits for that commit.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean
print(renderer.mesh.isResident(handle))
typed/builtin//modules/api/engine/renderer/renderer/mesh/list
renderer.mesh.list() -> { any }
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 (origin is "asset" for a mesh the asset path uploaded), whether the
GPU still holds it, and where its culling bounds come from (boundsFrom is
"compute" for a mesh a compute pass writes). A resident entry also carries
the bytes its buffers cost. bytes is the mesh's whole VRAM footprint and
is the sum of the THREE buffer columns beside it — vertexBytes + vertexStorageBytes + indexBytes, 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 bytes column is what sums to the meshes category of
renderer.gpuMemory().
renderer.references("mesh", guid) says what is still holding a row, and
renderer.collect() releases the rows nothing holds.
Returns { any } — Array of { guid, vertexCount?, indexCount?, origin, owner?, resident, boundsFrom, bytes?, vertexBytes?, vertexStorageBytes?, indexBytes?, primitives?, revision? }.
for _, m in ipairs(renderer.mesh.list()) do print(m.guid, m.bytes) end
typed/builtin//modules/api/engine/renderer/renderer/mesh/listInstanced
renderer.mesh.listInstanced() -> { InstancedDrawInfo }
Every instanced-draw registration this engine is drawing, in
registration order. Each record is what instanceInfo answers with, and
carries a draw 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.
Returns { InstancedDrawInfo } — An array of registration records; empty when nothing is registered.
for _, pop in ipairs(renderer.mesh.listInstanced()) do
renderer.mesh.dropInstanced(pop.draw)
end
typed/builtin//modules/api/engine/renderer/renderer/mesh/loadCpu
renderer.mesh.loadCpu(ref: string | AssetRef) -> MeshCpuHandle
Load a .mesh 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 meshRef:load(). DEFAULT lifecycle: upload
to the GPU then handle:unload(); the store is populated only by this call.
Parameters
refstring | AssetRef— A meshAssetRef(carries.guidand reads its primary via getBytes), or any stringasset.refresolves to one — the guidencodeCputakes, an identity, a name or a source path.
Returns MeshCpuHandle
local cpu = meshRef:load(); local gpu = renderer.mesh.create(cpu); cpu:unload()
local cpu = renderer.mesh.loadCpu(gpuMesh.guid)
typed/builtin//modules/api/engine/renderer/renderer/mesh/morphTargets
renderer.mesh.morphTargets(mesh: string | { [string]: any } | AssetRef) -> { 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 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.
Empty for a mesh with no morph targets. Errors when the mesh is neither
GPU- nor CPU-resident — materialise it first (meshRef:handle()).
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns { string } one name per morph target, in target order.
for i, name in renderer.mesh.morphTargets(meshRef) do print(i, name) end
typed/builtin//modules/api/engine/renderer/renderer/mesh/morphWeights
renderer.mesh.morphWeights(mesh: string | { [string]: any } | AssetRef, weights: { [string]: number }) -> { number }
Turn weights named by shape into the ordered weight array
ecs.MorphWeights takes — the drive-a-face-by-name call. Every target the
mesh carries gets a slot; the ones weights 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.
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.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.weights{ [string]: number }—{ [string]: number }— how strongly to blend each named shape.
Returns { number } one weight per morph target, in target order.
local w = renderer.mesh.morphWeights(meshRef, { Eyes_Blink = 1, Mouth_Smile = 0.4 })
ecs.set(face, ecs.MorphWeights { weights = w })
typed/builtin//modules/api/engine/renderer/renderer/mesh/onDevice
renderer.mesh.onDevice(mesh: string | { [string]: any } | AssetRef) -> boolean
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.
renderer.mesh.isResident 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.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean
while not renderer.mesh.onDevice(guid) do task.wait() end
typed/builtin//modules/api/engine/renderer/renderer/mesh/readback
renderer.mesh.readback(mesh: string | { [string]: any } | AssetRef, into: string?) -> MeshCpuHandle
Read a runtime GPU mesh's geometry back to CPU and return a
MeshCpuHandle for it — the GPU→CPU half of the runtime-mesh freeze path. A
mesh made with renderer.mesh.create keeps no CPU copy, so persisting it
(:encode() → asset.create("mesh", …)) 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: :getTriangles, :getVertices,
:getBounds, :geometry, :encode, :unload all work. Errors if the mesh
never becomes resident in the vertex pool.
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:
renderer.mesh.update from compute buffers unloads it, which an ocean
surface does every frame it steps. into 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.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — theMeshHandlerenderer.mesh.createreturned, a guid, or a meshAssetRef.intostring(optional) — The CPU-store key the geometry lands under. Defaults to the mesh's own guid.
Returns MeshCpuHandle
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()
typed/builtin//modules/api/engine/renderer/renderer/mesh/readbackPosed
renderer.mesh.readbackPosed(requests: { { entity: string, mesh: any } }) -> { [string]: MeshCpuHandle }
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.
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
compute.buildBvh, meshcpu.* and every other guid-keyed reader takes it
like any other mesh. Call handle:unload() when done with it.
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.
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.
Parameters
requests{ { entity: string, mesh: any } }—{ { entity = <id>, mesh = <mesh> } }— the entity to read, and the mesh it is posed from (a guid,MeshHandleor meshAssetRef).
Returns { [string]: MeshCpuHandle } — A map from entity id to the MeshCpuHandle holding that entity's posed geometry.
local posed = renderer.mesh.readbackPosed({ { entity = id, mesh = ecs.get(id, ecs.Mesh).mesh } })
local tris = posed[id]:getTriangles()
typed/builtin//modules/api/engine/renderer/renderer/mesh/scheduleClusters
renderer.mesh.scheduleClusters(mesh: string | { [string]: any } | AssetRef, opts: { levels: string? }?) -> boolean
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. opts.levels is "dag" for
the LOD hierarchy or "single" for one level of clusters at full detail;
when omitted, a mesh AssetRef bakes the depth its clusterLod setting asks
for and any other argument bakes "dag". 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
renderer.mesh.clusters reports as "deformable" or "heapFull" leaves
it unbaked without one.
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. renderer.mesh.clusterBakes().heldBytes reports what the queue
is holding, and its inFlight rows report which bakes it is holding for.
This is what the .mesh assetType materialisation path uses; reach for
renderer.mesh.buildClusters when you want the bytes in hand instead.
Scheduling the same mesh again replaces the bake already in flight for it.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to bake — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef, loaded into the CPU store (renderer.mesh.loadCpu).opts{ levels: string? }(optional) —{ levels: "dag" | "single" }?
Returns boolean — True if a bake was queued; false for a mesh with no GPU mesh registered or being created under its guid.
renderer.mesh.scheduleClusters(cpu, { levels = "single" }) ; cpu:unload()
typed/builtin//modules/api/engine/renderer/renderer/mesh/setInstanceCount
renderer.mesh.setInstanceCount(draw: InstancedDraw, count: number) -> InstancedDraw
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 drawInstanced was given.
Parameters
drawInstancedDraw— TheInstancedDrawto reconfigure.countnumber— Instances to draw, at least 1 and within the reservation.
Returns InstancedDraw — The same InstancedDraw.
renderer.mesh.setInstanceCount(draw, visibleCount)
typed/builtin//modules/api/engine/renderer/renderer/mesh/setInstanceParent
renderer.mesh.setInstanceParent(draw: InstancedDraw, parent: any?) -> InstancedDraw
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. nil
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.
Parameters
drawInstancedDraw— TheInstancedDrawto reconfigure.parentany(optional) — The entity the copies stand in (a proxy or its id), or nil for world space.
Returns InstancedDraw — The same InstancedDraw.
renderer.mesh.setInstanceParent(draw, entity.find("Forest"))
typed/builtin//modules/api/engine/renderer/renderer/mesh/setInstanceRenderLayer
renderer.mesh.setInstanceRenderLayer(draw: InstancedDraw, renderLayer: number) -> InstancedDraw
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.
Parameters
drawInstancedDraw— TheInstancedDrawto reconfigure.renderLayernumber— The membership bitmask, the same valuedrawInstancedtakes asrenderLayer. At least one bit must be set.
Returns InstancedDraw — The same InstancedDraw.
renderer.mesh.setInstanceRenderLayer(draw, mask)
typed/builtin//modules/api/engine/renderer/renderer/mesh/setVertices
renderer.mesh.setVertices(mesh: string | { [string]: any } | AssetRef, positions: { number })
Replace a mesh's resident CPU vertex positions (flat { x,y,z, ... })
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
renderer.mesh.update re-sends the whole geometry. The mesh must be
CPU-resident: renderer.mesh.create({ ..., keepCpu = true }) keeps a copy
from the start, renderer.mesh.readback(mesh) recovers one from the GPU,
and meshRef:load() loads one for a .mesh asset. Errors with the reason
otherwise, or when the vertex count doesn't match.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.positions{ number }— Flat{ x,y,z, ... }— one xyz per vertex; count must match the mesh.
local m = renderer.mesh.create({ positions = P, indices = I, keepCpu = true })
renderer.mesh.setVertices(m, P) -- P mutated in place each frame
typed/builtin//modules/api/engine/renderer/renderer/mesh/unloadCpu
renderer.mesh.unloadCpu(mesh: string | { [string]: any } | AssetRef)
Drop a mesh's resident CPU copy from the guid-keyed CPU store. The explicit release for a runtime geometry mesh's recoverable definition.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
typed/builtin//modules/api/engine/renderer/renderer/mesh/update
renderer.mesh.update(mesh: string | { [string]: any } | AssetRef, src: any?) -> MeshHandle
Overwrite the GPU resource mesh names IN PLACE, under the same guid,
from new geometry or compute buffers. Never writes a .mesh 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 MeshHandle create
returned, the guid renderer.mesh.list hands out, a MeshCpuHandle or a
mesh AssetRef. Returns a handle carrying the bounds the new geometry has:
the handle it was given, refreshed, and a handle over the guid otherwise.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to update — aMeshHandle, a guid, aMeshCpuHandleor a meshAssetRef.srcany(optional) — New geometry{positions, indices, ...}or compute buffers{vertexBuffer, indexBuffer, vertexCount, indexCount, prevVertexBuffer?}.
Returns MeshHandle — A MeshHandle for the updated mesh.
typed/builtin//modules/api/engine/renderer/renderer/mesh/uploadClusters
renderer.mesh.uploadClusters(mesh: string | { [string]: any } | AssetRef, clusters: string) -> boolean
Replace the cluster set a mesh draws with a serialized set (bytes from
renderer.mesh.buildClusters), 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
(renderer.mesh.clusterStore().scriptSetBytes) 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 renderer.mesh.dropClusters returns it to lod0, and when the mesh
is destroyed.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh the clusters belong to — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.clustersstring— Serialized cluster bytes (binary-safe).
Returns boolean — True if the upload was queued; false while the mesh's GPU upload has not landed yet.
renderer.mesh.uploadClusters(gpu, cb)
typed/builtin//modules/api/engine/renderer/renderer/minScreenSize
renderer.minScreenSize() -> number
The on-screen radius, in pixels, an object must reach to be drawn. 0
while the cutoff is off.
Returns number
local px = renderer.minScreenSize()
typed/builtin//modules/api/engine/renderer/renderer/morphStats
renderer.morphStats() -> {
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
(ecs.MorphWeights); where both are present, the vertex stage adds the
weighted deltas to the base geometry.
instances is how many render slots that happened at, and blends 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. meshes is how
many meshes hold a delta block and targets how many targets those blocks
cover between them; deltaBytes is what the shared buffer they are
appended into holds, and deltaBufferBytes the size of that buffer, which
the vertex stage binds whole.
deltaBudgetBytes 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 refusedMeshes 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 meshes above zero beside an instances and blends of zero.
Returns { instances: number, blends: number, meshes: number, targets: number, deltaBytes: number, deltaBufferBytes: number, deltaBudgetBytes: number, refusedMeshes: number }
local m = renderer.morphStats()
print(("%d instances carrying %d blends over %d meshes"):format(m.instances, m.blends, m.meshes))
typed/builtin//modules/api/engine/renderer/renderer/observe
renderer.observe() -> RenderObservation
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.
renderables is one row per renderable in the renderer's draw list,
carrying the program its material named (requestedProgram) beside the one
that was bound (boundProgram) — __error__ wherever the lookup missed
and the draw went ahead on the magenta placeholder — plus substituted,
the outcome (drew / drewPlaceholder / skipped / notDrawn), the
reason that forced it and the compiler's own detail for a failed
compile. observed says which of two answers a row is: true for a
resolution a geometry pass took as it drew, false 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.
materials is one row per
material the renderer holds a prepared bind group for; shaders is one row
per program pipelines have been built for. frame names the frame every
per-frame count covers; retainedFrames how many frames a resolution a
pass took is kept for after the last frame that drew it; window and
costWindow 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.
Returns RenderObservation
local o = renderer.observe(); print(o.placeholderDraws, "renderables drew the placeholder in frame", o.frame)
typed/builtin//modules/api/engine/renderer/renderer/occlusionCulling
renderer.occlusionCulling() -> boolean
Whether occlusion culling is currently enabled.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/passSchedule
renderer.passSchedule() -> {
The schedule check over this frame's enqueued render passes. Passes
declare what they read (inputs) and what they write (output /
outputs / storage), and the frame runs them in phase order and, inside
a phase, in order order. violations 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 @scene.* buffer — and the pass renders
either way. The frame's own buffers are checked on the same terms as a
render target: bind @scene.motion 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 readsPrevious and drops out of the list;
unboundPrevious 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 compute.dispatch both fill targets outside the pass queue,
and the scene draw fills the @scene.* 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.
unreachable holds passes at a phase that does not run their kind: every
phase drains its fragment and compute passes, while afterLighting 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.
Returns { violations, unboundPrevious, unreachable }
local s = renderer.passSchedule()
for _, v in s.violations do print(v.message) end
typed/builtin//modules/api/engine/renderer/renderer/pendingWork
renderer.pendingWork() -> PendingWork?
What the renderer still owes the picture — the work it has taken on and has not yet put in the frame.
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.
pipelineBuilds, textureUploads, textureReleases and
textureResidency are those four. textureTransfers 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 textureResidency alone reads zero on
them. outstanding is the sum of every count.
settled is true when outstanding 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.
epoch 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 settled and leaves epoch higher, so settled at both ends with
the same epoch at both ends is the statement that nothing arrived across
the whole span, rather than at its two endpoints alone. Test.settleReading
is built on exactly that pair of conditions.
frame 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.
deviceRebuild 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. computeCommands 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. clusterSets 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. materialRows 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.
skippedDraws 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. shaderCompiles 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.
Returns PendingWork? — { settled, outstanding, pipelineBuilds, textureUploads, textureReleases, textureTransfers, textureResidency, deviceRebuild, computeCommands, clusterSets, materialRows, skippedDraws, shaderCompiles, epoch, frame }, or nil on a build with no renderer attached
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")
typed/builtin//modules/api/engine/renderer/renderer/pipelineCache
renderer.pipelineCache() -> PipelineCache?
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.
restoredBytes is what a previous run left for this GPU and this launch
read; pipelinesBuilt counts the pipelines built since startup and
buildMs is what they cost together, which is the number the store lowers.
saves and savedBytes describe writing it back — deferred until a burst
of builds settles, so one launch is one write — and dirty is true while
pipelines have been built that the file does not hold, including after a
write that failed, which lastError then names. path is the file, named
after the GPU it belongs to.
supported 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. reason says which, and the build count and timing still read
true there. lastError 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.
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 buildWorkers worker threads on native, behind the browser's
asynchronous entry point on the web, where buildWorkers 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.
offFrameBuilt and offFrameBuildMs are the pipelines and the
milliseconds that never held a frame; pipelinesBuilt - offFrameBuilt
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. buildsSubmitted, buildsFinished,
buildsFailed and buildsInstalled count build requests (shader modules
and pipelines) made, completed, refused and taken back by a frame.
buildsAbandoned counts the requests a renderer still held when a device
rebuild let it go, which no frame will take back, and buildsPending 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 buildsPending to reach zero reads pipelinesBuilt with every
build it waited for counted, and can tell a queue draining from one that
is stuck. modulesBuilt and moduleBuildMs are
the shader modules compiled off the frame thread, parse and validation
included.
pipelinesBuilt and buildMs are engine-wide totals; renderer.shaderCost()
is the same cost broken down per program, with each one's permutation count.
Returns PipelineCache? — { supported, reason, path, restoredBytes, pipelinesBuilt, buildMs, offFrameBuilt, offFrameBuildMs, buildsSubmitted, buildsFinished, buildsFailed, buildsInstalled, buildsAbandoned, buildsPending, modulesBuilt, moduleBuildMs, buildWorkers, saves, savedBytes, dirty, lastError }, or nil before the renderer has drawn a frame
local c = renderer.pipelineCache()
print(("%d pipelines in %.1f ms, %d bytes restored"):format(c.pipelinesBuilt, c.buildMs, c.restoredBytes))
typed/builtin//modules/api/engine/renderer/renderer/presentedFrames
renderer.presentedFrames(opts: { scope: string?, frames: number?, interval: number?, velocity: boolean? }?) -> { PresentedFrame }
Parameters
opts{ scope: string?, frames: number?, interval: number?, velocity: boolean? }(optional)
Returns { PresentedFrame }
typed/builtin//modules/api/engine/renderer/renderer/projectionJitter
renderer.projectionJitter() -> { { number } }
The offsets renderer.setProjectionJitter stated, each {x, y} in
pixels from the pixel centre.
Returns { { number } } — The offsets; empty while every render samples pixel centres.
local offsets = renderer.projectionJitter()
typed/builtin//modules/api/engine/renderer/renderer/projectionOffset
renderer.projectionOffset() -> (number, number)
The sub-pixel projection offset in force for the main camera, in NDC.
Returns (number, number) — The x and y offset, both 0 when the projection samples pixel centres.
local ox, oy = renderer.projectionOffset()
typed/builtin//modules/api/engine/renderer/renderer/qualityTier
renderer.qualityTier() -> QualityTier
The device's quality tier and the renderer defaults it chose. The
engine assigns every device one of "low", "medium" and "high" 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 "high": 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 renderer.setQualityTier alone is a whole
graphics setting on every platform.
Every default the tier states stands wherever the world's settings name no
value of their own (render.anisotropy), and the matching renderer call
overrides any one of them: renderer.setResolutionScale,
renderer.setTextureBudget, renderer.setAnisotropy.
renderer.setQualityTier chooses another tier altogether.
Returns QualityTier: { tier, deviceTier, overridden, reason, tiers, device, defaults }
local q = renderer.qualityTier()
print(q.tier, q.reason, q.defaults.resolutionScale)
typed/builtin//modules/api/engine/renderer/renderer/raycast
renderer.raycast(origin: vec3, direction: vec3, maxDistance: number?, exclude: (string | { string })?) -> RenderRayHit?
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 Model 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.
distance is measured from origin 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 physics.raycast distance along the same ray.
normal is a unit vector turned to face back along the ray. exact 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.
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 entityId, exclude steps over the ones you do not want, and
renderer.raycastAll 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 entityId before trusting a number.
Parameters
originvec3—vec3ray start in world spacedirectionvec3—vec3ray direction; any length, the engine normalisesmaxDistancenumber(optional) —number?how far the ray reaches, in world units. Default 1000exclude(string | { string })(optional) —(string | { string })?entity id, or ids, to step over
Returns RenderRayHit?
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
typed/builtin//modules/api/engine/renderer/renderer/raycastAll
renderer.raycastAll(origin: vec3, direction: vec3, maxDistance: number?, maxHits: number?, exclude: (string | { string })?) -> { RenderRayHit }
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
entityId rather than hoping it is the nearest. Each entry carries the
fields renderer.raycast returns.
Parameters
originvec3—vec3ray start in world spacedirectionvec3—vec3ray direction; any length, the engine normalisesmaxDistancenumber(optional) —number?how far the ray reaches, in world units. Default 1000maxHitsnumber(optional) —number?how many surfaces to return. Default 32exclude(string | { string })(optional) —(string | { string })?entity id, or ids, to step over
Returns { RenderRayHit }
for _, hit in renderer.raycastAll(eye, look, 500) do print(hit.entityId, hit.distance) end
typed/builtin//modules/api/engine/renderer/renderer/raytraceCapability
renderer.raytraceCapability() -> string
The active ray-tracing backend: "hardware" (GPU ray query) or
"compute" (software traversal — the path on devices without hardware ray
query, e.g. the web). The same ray-tracing features work on both.
Returns string "hardware" | "compute"
if renderer.raytraceCapability() == "hardware" then ... end
typed/builtin//modules/api/engine/renderer/renderer/raytraceStats
renderer.raytraceStats() -> { [string]: any }
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 "compute" backend geometry that has stood still
long enough is filed under a static partition the frames after it leave
alone: staticTriangles + dynamicTriangles = triangles, nodes is the
hierarchy over them, fullRebuilds / partialRebuilds / reusedFrames
count what the session's frames did, and trianglesRebuilt is what those
rebuilds re-emitted, summed. On the "hardware" backend blas is the
bottom-level structures cached, blasBuilt how many the last frame built,
and tlasInstances what the top-level structure names. The counters are
cumulative — sample, run the scene, sample again.
Returns { [string]: any } — table {backend, triangles, staticTriangles, dynamicTriangles, nodes, fullRebuilds, partialRebuilds, reusedFrames, trianglesRebuilt, blas, blasBuilt, tlasInstances}
local before = renderer.raytraceStats().trianglesRebuilt
typed/builtin//modules/api/engine/renderer/renderer/references
renderer.references(handleOrKind: any?, id: string?) -> RuntimeResourceStatus?
What holds a runtime resource right now — the answer a root scene load
reads before releasing it. references names each live consumer the engine
found: { by = "entity", id } for an entity wearing the material or mesh,
"material" for a material whose slot names the texture, "instancedDraw",
"camera", "sky", "lightmap", "ui" (a screen drawing it) and
"postProcess" (an effect sampling it). handleHeld says whether a script
still reaches a handle to it, assetBacked whether an asset stands behind
it, ownerLive whether the component instance, scene load or feature that
created it still stands, and held whether a hold pins it. origin reads
"device" 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 renderer.collect 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.
Parameters
handleOrKindany(optional) — The resource's handle, or its kind with the id second.idstring(optional) — The guid or key, when the first argument is a kind.
Returns RuntimeResourceStatus? — The resource's status, or nil for a key the registry does not record and the device holds no texture under.
local s = renderer.references(mat) for _, r in s.references do print(r.by, r.id) end
typed/builtin//modules/api/engine/renderer/renderer/reflectionEnvironment
renderer.reflectionEnvironment() -> {
What a reflective surface is reflecting. probes is how many reflection
probes the shading blends; they are gathered highest priority 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. ranks is the priority each of those probe slots was
published with, in slot order. sky 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. skyCaptured is whether the sky slot holds a capture — arming is
refused until it does, since an uncaptured slot reflects black. skyStale
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 environment.captureSky(), 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. skyPosition is where the capture was taken
from, absent before one. skyCaptures counts the sky
captures that have landed since the renderer was built, so one retaken from
the same position reads as new, and skyMaterial names the material the
sky pass draws from, absent when no sky is drawn.
slots is how many cube slots the environment array holds right now: the
sky's alone, at index skySlot, until a probe is captured into it, then
that one, the slot a camera's stated environment is held in, and one per
probe. maxProbes is how many of them probes may
take, and resident whether the array has grown past the sky's single
slot. Capture the sky with environment.captureSky().
Returns { probes, ranks, sky, skyCaptured, skyStale, skyPosition?, skyCaptures, skyMaterial?, resident, slots, skySlot, maxProbes }
local env = renderer.reflectionEnvironment()
print(("%d probes, sky fallback %s"):format(env.probes, tostring(env.sky)))
typed/builtin//modules/api/engine/renderer/renderer/release
renderer.release(handleOrKind: any?, id: string?) -> boolean
Let go of the hold renderer.hold placed. The resource stays until
nothing else holds it and a collection releases it — the one a root scene
load runs, or a direct renderer.collect(). 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.
Parameters
handleOrKindany(optional) — The resource's handle, or its kind with the id second.idstring(optional) — The guid or key, when the first argument is a kind.
Returns boolean true when the registry knows the resource.
renderer.release(tex)
typed/builtin//modules/api/engine/renderer/renderer/releaseBaseline
renderer.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
generationnumber— The mark to drop, asrenderer.markBaselinereturned it.
Returns boolean — True when the mark stood.
renderer.releaseBaseline(mark) -- the run is over
typed/builtin//modules/api/engine/renderer/renderer/renderTargetLimits
renderer.renderTargetLimits() -> {
The size a render target may be on this device. maxDimension is the
device's own maximum 2D texture dimension — the largest either side of a
render target may take. maxPixels 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 maxSquare is the largest square
that budget buys. A capture, a renderer.texture.create({ width, height })
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.
Returns { maxDimension, maxPixels, maxSquare }
local lim = renderer.renderTargetLimits()
local w = math.min(want, lim.maxSquare)
typed/builtin//modules/api/engine/renderer/renderer/renderTargets
renderer.renderTargets() -> {
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
name, whether it is resident, the bytes it holds while it is, its
width/height/layers/mipLevels, and onDemand.
An onDemand target exists only while something needs it: a target nothing
writes into reads resident = false and bytes = 0 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 (scene_occlusion,
scene_reflection, scene_color_history) are resident at the scene's
extent while a render-feature pass writes or reads them, and
shadow_page_pool while a light casts into the virtual shadow map, with
shadow_transmittance_pool beside it while a translucent caster casts
into it.
The scratch the draws into
a render target have needed is reported as camera[<handle>].* 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
released_scratch[<width>x<height>].* 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
released_scratch[<width>x<height>].color row for the next target made at
that size and format, which starts from it cleared to transparent black.
totalBytes is what the resident targets hold together. allocations
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.
Returns { targets, totalBytes, residentCount, allocations }
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
typed/builtin//modules/api/engine/renderer/renderer/resetToBaseline
renderer.resetToBaseline(generation: number?) -> number
Parameters
generationnumber(optional)
Returns number
typed/builtin//modules/api/engine/renderer/renderer/resolutionScale
renderer.resolutionScale() -> number
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 (renderer.qualityTier().defaults.resolutionScale): 1 on a
desktop at the high tier, below 1 on a dense handheld display and on a
desktop at a lower tier.
Returns number
local s = renderer.resolutionScale()
typed/builtin//modules/api/engine/renderer/renderer/resolutionScaleMode
renderer.resolutionScaleMode() -> string
Where the render scale resolves: "present" or "reconstruct".
"present" until something sets it.
Returns string
local m = renderer.resolutionScaleMode()
typed/builtin//modules/api/engine/renderer/renderer/setAnisotropy
renderer.setAnisotropy(level: number) -> number
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.
Parameters
levelnumber— One of 1, 2, 4, 8, 16. Any other value is an error.
Returns number — The EFFECTIVE level after clamping to renderer.maxAnisotropy(), so asking for more than the device offers reports what was actually applied.
renderer.setAnisotropy(16)
typed/builtin//modules/api/engine/renderer/renderer/setBlendedBatching
renderer.setBlendedBatching(enabled: boolean) -> ()
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; renderer.drawStats().draws counts the
difference.
Parameters
enabledboolean—boolean
Returns ()
renderer.setBlendedBatching(false) -- a draw per blended renderable
typed/builtin//modules/api/engine/renderer/renderer/setFrameRateCap
renderer.setFrameRateCap(fps: number?) -> number?
Hold the engine to at most fps 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.
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; with no cap it is told the panel's highest rate.
Read renderer.framePacing() for the effect: targetFps is the rate the
cap delivers, refreshesPerFrame the refreshes each frame holds,
displayRefreshHz the display it was worked out against, and
deliveredFps the rate frames actually ran at. Takes effect on the next
frame. frame_rate_cap under [render] in the world's .world_settings
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.
Parameters
fpsnumber(optional) — number? Frames per second, 1 through 1000; 0 or nil lifts the cap.
Returns number? — The cap in force, or nil when frames are not capped.
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
typed/builtin//modules/api/engine/renderer/renderer/setGpuMemoryTracking
renderer.setGpuMemoryTracking(frames: number?) -> number
Set how often the GPU allocator sampler reads — one reading every
frames frames — or turn it off with 0. It starts at 60, a reading a
second at 60 Hz, so renderer.gpuMemory().allocator 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.
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.
Parameters
framesnumber(optional) —number?Frames between readings; 0 turns the sampler off. Omit to read the interval without changing it.
Returns number — The interval now in force.
renderer.setGpuMemoryTracking(60)
local was = renderer.setGpuMemoryTracking()
renderer.setGpuMemoryTracking(1)
-- ... take a tight reading ...
renderer.setGpuMemoryTracking(was)
typed/builtin//modules/api/engine/renderer/renderer/setLighting
renderer.setLighting(enabled: boolean) -> ()
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.
Parameters
enabledboolean—truerenders the scene lit,falseunlit.
Returns ()
renderer.setLighting(false); print(renderer.lighting())
typed/builtin//modules/api/engine/renderer/renderer/setMaxFramesInFlight
renderer.setMaxFramesInFlight(frames: number) -> number
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.
Answers the bound after clamping to [1, 8], so asking for more than the renderer honours reports what you actually got.
Parameters
framesnumber— number Frames of GPU work that may be outstanding, 1 through 8.
Returns number — The bound that took effect, after clamping.
renderer.setMaxFramesInFlight(1) -- lowest latency
print(renderer.setMaxFramesInFlight(99), "was clamped")
typed/builtin//modules/api/engine/renderer/renderer/setMaxTextureSize
renderer.setMaxTextureSize(px: number?) -> { px: number, devicePx: number }
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.
Parameters
pxnumber(optional) —number?— texels, a whole number of at least 1; nil for the device's own limit
Returns { px: number, devicePx: number } — { px, devicePx } — the cap in force and the device's limit
renderer.setMaxTextureSize(2048)
renderer.setMaxTextureSize() -- the device's own limit
typed/builtin//modules/api/engine/renderer/renderer/setMinScreenSize
renderer.setMinScreenSize(pixels: number) -> ()
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 — 0, 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 renderer.setShadowCasterCutoff.
Parameters
pixelsnumber—number— smallest on-screen radius still drawn; 0 disables.
Returns ()
renderer.setMinScreenSize(4) -- drop anything under 4 px of radius
print(renderer.drawStats().compactedDrawn, "instances survived it")
typed/builtin//modules/api/engine/renderer/renderer/setOcclusionCulling
renderer.setOcclusionCulling(enabled: boolean) -> ()
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.
Parameters
enabledboolean—boolean
Returns ()
renderer.setOcclusionCulling(true); print(renderer.cullStats().occlusionCulled)
typed/builtin//modules/api/engine/renderer/renderer/setPresentMode
renderer.setPresentMode(mode: string) -> string
Set how a presented frame reaches the display. fifo queues every frame
and shows it on a vertical blank, which never tears and never drops one;
mailbox replaces the queued frame with the newest, which does not tear
and does not hold the renderer to the refresh rate; immediate presents as
soon as a frame is ready and can tear; fifo_relaxed is fifo that tears
rather than stall when a frame misses its blank; auto_vsync and
auto_no_vsync leave the choice to the backend.
A surface that does not offer the mode presents fifo instead, so read
renderer.framePacing().presentMode for what took effect and
.presentModes for what this surface offers. Takes effect on the next
frame.
Parameters
modestring— string One of "fifo", "fifo_relaxed", "mailbox", "immediate", "auto_vsync", "auto_no_vsync".
Returns string — The canonical spelling of the request — renderer.framePacing().presentMode is what the surface presents with, and differs when the surface does not offer the request.
renderer.setPresentMode("mailbox")
print(renderer.framePacing().presentMode, "is what the surface took")
typed/builtin//modules/api/engine/renderer/renderer/setProjectionJitter
renderer.setProjectionJitter(offsets: { { number } })
State where inside the pixel consecutive renders of a view sample, for
the frames until it is stated again: a list of {x, y} 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 (@scene.motion) is measured against the
offset-free projection, and a history target's state buffer carries the
offset each render took. renderer.setProjectionOffset, while it is not
(0, 0), 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.
Parameters
offsets{ { number } }— The offsets, each{x, y}in pixels from the pixel centre.
renderer.setProjectionJitter({ { 0.25, -0.25 }, { -0.25, 0.25 } })
typed/builtin//modules/api/engine/renderer/renderer/setProjectionOffset
renderer.setProjectionOffset(x: number, y: number)
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 2.0 / width across, so half a pixel
is 1.0 / width. Velocity (@scene.motion) 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. (0, 0) samples pixel centres.
Parameters
xnumber— Horizontal offset in NDC. One pixel is2.0 / width.ynumber— Vertical offset in NDC. One pixel is2.0 / height.
renderer.setProjectionOffset(0.5 * 2 / w, -0.25 * 2 / h)
typed/builtin//modules/api/engine/renderer/renderer/setQualityTier
renderer.setQualityTier(tier: string?) -> QualityTier
Render at another quality tier than the one the device was assigned:
"low", "medium" or "high", or nil / "default" 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 renderer.setTextureBudget 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.
Parameters
tierstring(optional) —string?:"low","medium","high", ornil/"default"for the device's own.
Returns QualityTier: the reading renderer.qualityTier() answers, for the tier now in force.
renderer.setQualityTier("low")
renderer.setQualityTier() -- back to the device's own
typed/builtin//modules/api/engine/renderer/renderer/setRaytrace
renderer.setRaytrace(enabled: boolean) -> ()
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.
Parameters
enabledboolean—boolean
Returns ()
renderer.setRaytrace(true); renderer.feature.create("rt_shadows")
typed/builtin//modules/api/engine/renderer/renderer/setResolutionScale
renderer.setResolutionScale(scale: number) -> number
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 0.5 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 renderer.setResolutionScaleMode. The scene rows in
profiler.gpuFrame() are what move.
Parameters
scalenumber—number— fraction of the display resolution, clamped to [0.25, 1].
Returns number — the scale in force after clamping.
renderer.setResolutionScale(0.7)
typed/builtin//modules/api/engine/renderer/renderer/setResolutionScaleMode
renderer.setResolutionScaleMode(mode: string) -> string
Where in the frame the render scale is resolved — what a smaller raster
MEANS. "present" (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.
"reconstruct" 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.
Parameters
modestring—string—"present"or"reconstruct".
Returns string — the mode in force.
renderer.setResolutionScaleMode("present")
typed/builtin//modules/api/engine/renderer/renderer/setShadowCaching
renderer.setShadowCaching(enabled: boolean) -> ()
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.
Parameters
enabledboolean—boolean
Returns ()
renderer.setShadowCaching(false) -- draw every requested shadow page, every frame
typed/builtin//modules/api/engine/renderer/renderer/setShadowCasterCutoff
renderer.setShadowCasterCutoff(cfg: {
minRadiusPx: number?,
maxDistance: number?,
}) -> ShadowCasterCutoff
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. maxDistance 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.
Parameters
cfg{ minRadiusPx: number?, maxDistance: number?, }— The fields to change —minRadiusPxand/ormaxDistance.
Returns ShadowCasterCutoff — The cutoff now in force.
renderer.setShadowCasterCutoff({ minRadiusPx = 2 })
renderer.setShadowCasterCutoff({ minRadiusPx = 1.5, maxDistance = 120 })
typed/builtin//modules/api/engine/renderer/renderer/setShadowConfig
renderer.setShadowConfig(cfg: {
distance: number?,
fadeFraction: number?,
softness: number?,
punctualMipFloor: number?,
detailBias: (number | string)?,
budgetMegabytes: (number | string)?,
}) -> ShadowConfig
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 distance
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. punctualMipFloor (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. detailBias ([-8, 8] levels) trades
shadow detail for pages: each level coarser asks a quarter as many.
budgetMegabytes (>= 1) bounds the depth the pages hold. Each takes
"default" 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.
Parameters
cfg{ distance: number?, fadeFraction: number?, softness: number?, punctualMipFloor: number?, detailBias: (number | string)?, budgetMegabytes: (number | string)?, }— The fields to change — seeShadowConfig.
Returns ShadowConfig — The full config now in force.
renderer.setShadowConfig({ softness = 0.65, fadeFraction = 0.28 })
renderer.setShadowConfig({ distance = 240 })
renderer.setShadowConfig({ detailBias = 1, budgetMegabytes = 8 })
renderer.setShadowConfig({ detailBias = "default", budgetMegabytes = "default" })
typed/builtin//modules/api/engine/renderer/renderer/setShadowHero
renderer.setShadowHero(entity: string, padding: number?) -> ()
Give one caster a finer sun shadow, in a box around its world bounds.
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 padding into a box, and every receiver inside it requests
and samples a level renderer.shadowHero().levelBias 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.
Parameters
entitystring— The entity whose renderables the box is made around.paddingnumber(optional) — 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.
Returns ()
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))
typed/builtin//modules/api/engine/renderer/renderer/setShadowProxy
renderer.setShadowProxy(mesh: string, proxy: string) -> ()
Rasterize proxy in place of mesh 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.
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.
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 renderer.shadowProxies().
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.
Parameters
meshstring— The mesh a caster draws, as a guid or any mesh reference.proxystring— The mesh it rasterizes into shadow pages instead.
Returns ()
renderer.setShadowProxy(statueMesh, statueHullMesh)
print(renderer.shadowProxies().triangles, "vs", renderer.shadowProxies().sourceTriangles)
typed/builtin//modules/api/engine/renderer/renderer/setSkinnedBatching
renderer.setSkinnedBatching(enabled: boolean) -> ()
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 — renderer.drawStats().draws counts the difference and
renderer.skinningStats().poses says how many distinct poses it holds.
Parameters
enabledboolean—boolean
Returns ()
renderer.setSkinnedBatching(false) -- a draw per skinned instance
typed/builtin//modules/api/engine/renderer/renderer/setSkinningPoseHold
renderer.setSkinningPoseHold(enabled: boolean) -> ()
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
renderer.skinningStats() counts the difference as dispatches against
held. A mesh whose vertices a compute pass writes is dispatched every
frame however this stands.
Parameters
enabledboolean—boolean
Returns ()
renderer.setSkinningPoseHold(false) -- dispatch every pose, every frame
typed/builtin//modules/api/engine/renderer/renderer/setTextureBucketSlack
renderer.setTextureBucketSlack(bytes: number?) -> TextureBucketSlack
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); nil restores it. bytes 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 renderer.setTextureBudget; read what
the buckets hold with renderer.materialTable().
Parameters
bytesnumber(optional) —number?— the slack budget in bytes, or nil for the device's
Returns TextureBucketSlack — { slackBudgetBytes, deviceSlackBudgetBytes } — the budget now in force, and the device's own
renderer.setTextureBucketSlack(16 * 1024 * 1024)
renderer.setTextureBucketSlack(nil) -- the device's budget
typed/builtin//modules/api/engine/renderer/renderer/setTextureBudget
renderer.setTextureBudget(opts: TextureBudgetOpts?) -> TextureBudget
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 (renderer.qualityTier()).
renderer.textureStreaming() names the rule (budgetRule) and the RAM it
read (deviceMemoryBytes), and memory pressure narrows what the budget
keeps (renderer.memoryPressure()). Pass
{ megabytes = 256 } to override the budget for the process; megabytes
is a positive number, 1 to 65536, and 0 or a negative number is an error.
A field set to the string "default" 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. nil restores the device's budget and bias
together. The budget belongs to the device the world is played on.
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 uvScale = 8 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 // @uv_space: world 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.
bias 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].
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.
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
(renderer.textureMemory().streamSourceBytes) 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.
renderer.textureMemory().pinnedTextures 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.
Parameters
optsTextureBudgetOpts(optional) —{ megabytes: (number | "default")?, bias: (number | "default")? }?— nil for the device's budget and bias
Returns TextureBudget — { megabytes, bias, deviceMegabytes, deviceBias } — the budget and bias now in force, and the device's own
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
typed/builtin//modules/api/engine/renderer/renderer/setTransmissionShadows
renderer.setTransmissionShadows(enabled: boolean) -> ()
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
transmission 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 1 - alpha of
the light through untouched and its transmission 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.
Parameters
enabledboolean—boolean
Returns ()
renderer.setTransmissionShadows(true) -- stained glass tints the floor
typed/builtin//modules/api/engine/renderer/renderer/setUpscaleBackend
renderer.setUpscaleBackend(backend: string, quality: string?) -> string
Request a backend for the upscale seam — the step that brings a scene
rendered below display resolution back up to it. "blit" stretches;
"dlss" 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
renderer.upscaleBackend() — a request for "dlss" on an adapter
renderer.upscaleCapabilities() omits it from resolves to "blit"
there, answered truthfully rather than echoed back here. quality names
the reconstruction quality mode a reconstructing backend derives its
render scale from; the blit keeps whatever renderer.setResolutionScale()
set and reads no value from quality.
@builtin::systems.temporalUpscale.temporalUpscale is the content-facing
surface built on this primitive — it carries the policy this raw setter
carries none of: refusing "dlss" where unavailable, refusing quality
alongside an explicit render scale, and keeping its own reconstruction
from stacking with a reconstructing backend.
Parameters
backendstring—string—"blit"or"dlss".qualitystring(optional) —string?—"quality" | "balanced" | "performance" | "ultraPerformance", read only by a reconstructing backend.
Returns string — the backend now requested, echoed back before the renderer resolves it against this adapter's capability.
renderer.setUpscaleBackend("dlss", "balanced")
typed/builtin//modules/api/engine/renderer/renderer/setViewportSize
renderer.setViewportSize(width: number, height: number) -> { width: number, height: number }
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: getViewportSize(),
ui.screenSize(), 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 ui.screenSize() 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.
The size is held for the task that asked for it (an execute 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.
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 renderer.surfaceSize() on a later frame for what was
realized.
The new size is in force from the NEXT frame, so wait a frame before
reading it back: renderer.surfaceSize() read straight after the call
still reports the size the surface had.
The logical UI space is normalised to about 1280 points wide, so a resize
to a size of the same aspect moves ui.pixelRatio() and leaves
ui.screenSize() where it was, while a resize that changes the aspect
changes that shape too. Both resize the surface.
A size larger than the device draws is refused naming the bound, which
renderer.maxViewportExtent() reports.
Parameters
widthnumber—number— width in pixels, at least 1.heightnumber—number— height in pixels, at least 1.
Returns { width: number, height: number } — the size asked for, in whole pixels.
renderer.setViewportSize(390, 844); task.waitFrames(2); print(ui.screenSize()) -- the phone shape, then the surface goes back when the call ends
typed/builtin//modules/api/engine/renderer/renderer/shaderCache
renderer.shaderCache() -> ShaderCache?
What the shader compile gate's store of baked WGSL held, answered and
wrote back. Compiling a .shader wraps the author's body in its framework,
expands every #include, 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.
restoredEntries and restoredBytes are what a previous launch left that
this one read back. hits counts the compiles answered out of the store
and misses those that ran in full; savedMs sums what each hit's own
recorded compile had cost, against compileMs, what the misses spent.
stale counts the misses whose key was held but whose #included 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: verdictHits counts the verdicts answered out of
it and verdictMisses the ones whose gate ran, apart from the compile
counts above.
entries and bytes are what the store now holds, evictions how many a
write dropped to stay inside its bounds, and saves / savedBytes /
dirty describe writing it back, deferred until a burst of compiles
settles. persistent is false where a launch has nowhere to keep
artifacts and reason says why; location is the file, or the browser
store, they are kept in. restoreState is how the read of what a previous
launch left has gone — pending while it is still out (a browser answers
through a promise, so a launch reaches its first frames before it lands),
restored once entries came back, empty when there were none to come
back, failed when what was there could not be read, and none where a
launch keeps nothing. A cold, missing or corrupt store leaves every
shader compiling from source with identical output, and lastError then
names what went wrong.
Returns ShaderCache? — { persistent, reason, restoreState, location, restoredEntries, restoredBytes, hits, misses, stale, verdictHits, verdictMisses, entries, bytes, compileMs, savedMs, saves, savedBytes, dirty, evictions, lastError }, or nil on a build with no renderer
local c = renderer.shaderCache()
print(("%d hits, %d misses, %.1f ms saved"):format(c.hits, c.misses, c.savedMs))
typed/builtin//modules/api/engine/renderer/renderer/shaderCost
renderer.shaderCost() -> { ShaderCost }
What each program has cost in pipeline builds, beside the compile
gate's most recent word about it. variants is how many pipelines this
engine has built for it — one per (target format, vertex layout,
render-state key) permutation reached — and buildMs 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. status is compiled, failed or
pending, and error carries the compiler's message for a failure.
Ordered by cost, most expensive first. shader is the key the program
is registered under; asset names the .shader asset a guid@checksum
key is a version of.
Returns { ShaderCost }
local top = renderer.shaderCost()[1]; print(top.asset or top.shader, top.variants, top.buildMs)
typed/builtin//modules/api/engine/renderer/renderer/shaderVariants
renderer.shaderVariants() -> { ShaderVariants }
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 budget compiles no more; the
materials asking for further feature sets draw with the base program.
Returns { ShaderVariants } — An array of ShaderVariants, one per feature-declaring shader.
for _, s in renderer.shaderVariants() do print(s.shader, #s.variants, s.budget) end
typed/builtin//modules/api/engine/renderer/renderer/shadingOf
renderer.shadingOf(subject: string | { [string]: any }) -> ShadingReading
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. subject is
an entity that draws or the registry key of a material. state reads
itsMaterial where the renderer bound the program the material names,
errorMaterial where it bound the placeholder instead, stalePipeline
where the pipeline drawing it was built before that program's most recent
compile, nothingBound where the renderer resolved no pipeline for it,
pending where this call is the one that armed per-draw recording and the
frame after it publishes, and unknown where the renderer holds a
resolution under no such subject. A fault state carries the renderer's own
reason from the closed set renderer.drawDiagnostics() names — plus
materialNotPrepared, which a material subject reads where the renderer
prepared nothing under that key — the compiler's detail, the program
the material asked for and the bound one; means 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 itsMaterial from the frame the renderer
draws it with again.
Parameters
subjectstring | { [string]: any }— The entity — a proxy fromentity(...)or an entity-id string — or the material, as its registry key or theMaterialHandlerenderer.material.createreturned.
Returns ShadingReading
local s = renderer.shadingOf(emitter); if s.state ~= "itsMaterial" then print(s.means) end
typed/builtin//modules/api/engine/renderer/renderer/shadowCacheStats
renderer.shadowCacheStats() -> {
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 rendered 0 while its pages stay cached. 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.
spot counts spot and area lights.
These are totals over every light of a kind. renderer.shadowViews() is
the same run one light at a time.
Returns { directionalRendered, directionalCached, spotRendered, spotCached, pointRendered, pointCached, rendered, cached }
local s = renderer.shadowCacheStats()
print(("shadow pages: %d drawn, %d cached"):format(s.rendered, s.cached))
typed/builtin//modules/api/engine/renderer/renderer/shadowCaching
renderer.shadowCaching() -> boolean
Whether a shadow page may keep the depth it already holds.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/shadowCasterCutoff
renderer.shadowCasterCutoff() -> ShadowCasterCutoff
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.
Returns ShadowCasterCutoff — The cutoff in force — see ShadowCasterCutoff.
local c = renderer.shadowCasterCutoff(); print(c.minRadiusPx, c.maxDistance)
typed/builtin//modules/api/engine/renderer/renderer/shadowConfig
renderer.shadowConfig() -> ShadowConfig
The sun's shadow settings now in force. distance bounds how far from
the camera the sun shadows (the world's render.shadow_distance until
renderer.setShadowConfig sets it); fadeFraction and softness shape
how the shadow is sampled. The sun's virtual shadow map follows each
receiver's screen footprint, so its resolution follows the view.
punctualMipFloor is the finest mip a spot, area or point light's map
renders. detailBias and budgetMegabytes are what a caller stated, or
"default"; what the renderer sized the pages with is
renderer.shadowSizing(). The table is one renderer.setShadowConfig
takes back.
Returns ShadowConfig — The full config — see ShadowConfig.
print(renderer.shadowConfig().distance)
typed/builtin//modules/api/engine/renderer/renderer/shadowHero
renderer.shadowHero() -> ShadowHeroReport
The registered hero caster and what the last drawn frame's box was. A
frame that made no box says why in decline.
entity is the live registration; everything else is the box of the
drawn frame frame names, and stands until the renderer draws another.
So a reading taken straight after setShadowHero or clearShadowHero
describes the registration before the call: read the drawn frame count as
the change is made, and take the report once its frame is past it.
Returns ShadowHeroReport — See ShadowHeroReport.
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)
typed/builtin//modules/api/engine/renderer/renderer/shadowMemory
renderer.shadowMemory() -> {
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. pool is the depth page pool with the sun's static
cache laid out as it, held while any light casts; transmittance is the
transmittance pool, held while a translucent caster casts into it
(renderer.renderTargets() names them shadow_page_pool,
shadow_static_cache and shadow_transmittance_pool). Both read 0 while
released.
Returns { pool, transmittance, total } in bytes
local m = renderer.shadowMemory()
print(("shadows: %.1f MiB"):format(m.total / 1024 / 1024))
typed/builtin//modules/api/engine/renderer/renderer/shadowPageBreakdown
renderer.shadowPageBreakdown() -> { { [string]: any } }
The breakdowns renderer.watchShadowPages gathered since the last call,
oldest first, one per run of the main camera. levels has a row per
clipmap level: texelWorld (world units per texel), the pages it
requested, held (resident), drew (rendered) and had dirtied by the
frame's caster changes (invalidated), the pages those changes reach in its
window (reached) and the smallest size a change projects to there
(smallestRecordTexels). records lists each caster change with its
light-space sizeWorld and, per level, the texels it covers and the
pages it reaches. cull is what the page cull walked (slotsWalked
caster slots, ncMembers casters drawn from their own vertex buffers) and
kept (pairs, casters tested, admitted and cutOff, chunks,
visible clusters, ncPairs, batchPages). pages has a row per batch
page: its level, the casters it drew and how many changed this frame
(dynamicCasters), its chunks, clusters and triangles with the
changed casters' shares, and its pairs of casters drawn from their own
vertex buffers (ncPairs, dynamicNcPairs).
Returns { { [string]: any } } — An array of breakdowns, empty when none was read since the last call.
renderer.watchShadowPages(30)
task.wait(1)
for _, run in renderer.shadowPageBreakdown() do print(run.frame, #run.pages) end
typed/builtin//modules/api/engine/renderer/renderer/shadowPageStats
renderer.shadowPageStats() -> {
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 ran reads false and runs stands still while
frames climbs. The counts come back from the GPU a few frames after the
run, so frame names the frame they describe.
proxyPairs counts the (caster, page) pairs a shadow proxy stood in for,
proxyTriangles the triangles those pairs drew from the proxy and
sourceTriangles what the casters' own meshes would have drawn there.
castersDropped 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.
Returns { frame, ran, runs, frames, requested, rendered, pending, starved, resident, clusters, batchPages, proxyPairs, proxyTriangles, sourceTriangles, invalidated, invalidateOverflow, castersDropped }, or nil before the first run is read back
local s = renderer.shadowPageStats()
if s then print(("%d pages asked for, %d drawn"):format(s.requested, s.rendered)) end
typed/builtin//modules/api/engine/renderer/renderer/shadowProxies
renderer.shadowProxies() -> ShadowProxyReport
The shadow proxies in force and what the shadow pages did with them.
triangles and sourceTriangles 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.
Returns ShadowProxyReport — See ShadowProxyReport.
local p = renderer.shadowProxies()
print(("shadow triangles: %d of %d"):format(p.triangles, p.sourceTriangles))
typed/builtin//modules/api/engine/renderer/renderer/shadowSizing
renderer.shadowSizing() -> ShadowSizing
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 renderer.setShadowConfig 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
overflowBias by a level, up to 4, until the pool is sized again.
Returns ShadowSizing — See ShadowSizing.
print(renderer.shadowSizing().depthMegabytes)
typed/builtin//modules/api/engine/renderer/renderer/shadowViews
renderer.shadowViews() -> ShadowViewReport?
Every shadow-casting spot, area and point light's virtual shadow map on the main camera's newest counted run, one row each.
renderer.shadowCacheStats() and renderer.shadowPageStats() sum these
counts over every light; this is the same run one light at a time. The
sun's pages are reported by renderer.shadowPageStats().
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. camera carries the main camera's share
of the instanced-compaction counters.
The counts come back from the GPU a few frames after the run, and frame
names the frame they belong to. A light that stops casting leaves views.
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.
Returns ShadowViewReport? — See ShadowViewReport.
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
typed/builtin//modules/api/engine/renderer/renderer/signalMemoryPressure
renderer.signalMemoryPressure(level: string) -> ()
Report memory pressure the way the system does: the engine releases
what level releases on its next frame, and engine.onMemoryPressure
subscribers hear it with source "script". Content about to need a lot of
memory (a large scene load, a big decode) frees room ahead of it with
this, and "normal" lifts a level a script put in force. level is
"normal", "moderate", "low" or "critical"; anything else is an
error naming the four. Read the outcome with renderer.memoryPressure()
once a frame has passed.
Parameters
levelstring—"normal","moderate","low"or"critical"
Returns ()
renderer.signalMemoryPressure("moderate") -- free unsampled mips before a big load
typed/builtin//modules/api/engine/renderer/renderer/skinnedBatching
renderer.skinnedBatching() -> boolean
Whether skinned instances holding one pose draw together.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/skinningPoseHold
renderer.skinningPoseHold() -> boolean
Whether a pose already written into its slice skips its dispatch.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/skinningStats
renderer.skinningStats() -> {
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. instances is how many were posed, poses how
many distinct poses they held, and dispatches 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.
held 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 dispatches 0 beside a held
equal to its poses, and the two add up to poses 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 dispatches on each of them and
reads as held from the frame after the first one that records it. A
wait for a crowd to settle is therefore counted in drawn frames
(task.waitDrawnFrames).
reusedSlices 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 poolBytes that stays where it was.
liveBytes is what the slices holding this frame's poses occupy, against
unsharedBytes — what the same instances would occupy with a slice each.
poolBytes is what the pool holds; a previous-position buffer of the same
size rides alongside it so skinned deformation reaches motion vectors.
The same pass draws a soft body's model through the body's elements (its
cage). cages is how many soft bodies it draws that way, cageBytes what
their buffers occupy, and cageUploads 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.
Returns { instances: number, poses: number, dispatches: number, held: number, reusedSlices: number, liveBytes: number, unsharedBytes: number, poolBytes: number, cages: number, cageBytes: number, cageUploads: number }
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))
typed/builtin//modules/api/engine/renderer/renderer/splat/components
renderer.splat.components(bytes: any?, convention: string?) -> (SplatComponents?, string?)
Decode a Gaussian splat capture — a Niantic .spz (gzipped or raw) or a
3DGS .ply — into the GPU-ready byte pools a render feature uploads.
records is the packed splat array at recordBytes per splat (position,
log scale, quaternion, DC colour + opacity); sh is the quantized
higher-order spherical-harmonics pool at shStrideWords u32 words per
splat, empty at degree 0. A pure decode (no GPU work): upload the pools with
shaderRef:createBuffer + buf:writeBytes and draw them with a
kind = "splat", channel = "gaussian" pass.
Parameters
bytesany(optional) — Capture bytes —.spzor.ply, as abufferor a binary string.conventionstring(optional) — Source axis convention:"rightDownFront"(the default, what COLMAP-trained captures use) or"engineNative"for a capture already in engine space.
Returns (SplatComponents?, string?) — { records, sh, count, shDegree, shStrideWords, recordBytes, boundsMin?, boundsMax?, antialiased, format }, or (nil, err).
local c = renderer.splat.components(vfs.read("/source/captures/ceramic.spz"))
typed/builtin//modules/api/engine/renderer/renderer/surfaceSize
renderer.surfaceSize() -> { width: number, height: number }
The whole drawing surface in pixels — the window, the browser canvas
or the headless framebuffer. This is the size renderer.setViewportSize
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.
{ width = 0, height = 0 } before the first frame has drawn.
Returns { width: number, height: number } in pixels.
local s = renderer.surfaceSize()
typed/builtin//modules/api/engine/renderer/renderer/temporal/held
renderer.temporal.held() -> boolean
Whether a hold is pinning the per-frame clock right now.
Returns boolean — True while at least one renderer.temporal.hold stands.
if renderer.temporal.held() then print("frame is pinned") end
typed/builtin//modules/api/engine/renderer/renderer/temporal/hold
renderer.temporal.hold(at: number?, options: TemporalHoldOptions?) -> () -> ()
Pin the clock every per-frame effect draws itself against, and return
the release. While the hold stands, renderer.temporal.now answers at
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.
exclusive takes the clock 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 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 renderer.temporal.release hands the clock back by. A
capture taken while the hold stands renders at the held instant, and a
deterministic capture states that key as its own owner option to be
admitted under the hold and photograph the instant it pinned.
Parameters
atnumber(optional) — 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.optionsTemporalHoldOptions(optional) —owneris 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.exclusivetakes the clock for the stated key until the hold is released.
Returns () -> () — A function that releases this hold. Calling it twice releases once.
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" })
typed/builtin//modules/api/engine/renderer/renderer/temporal/now
renderer.temporal.now() -> number
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.
Returns number — Seconds — pinned while a hold stands, running otherwise.
local params = { grainTime = renderer.temporal.now() }
typed/builtin//modules/api/engine/renderer/renderer/temporal/onChange
renderer.temporal.onChange(listener: (number) -> ()) -> () -> ()
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.
Parameters
listener(number) -> ()— Called with the instant now in force, in seconds.
Returns () -> () — A function that removes this listener.
local stop = renderer.temporal.onChange(function(t) print("temporal clock", t) end)
typed/builtin//modules/api/engine/renderer/renderer/temporal/owner
renderer.temporal.owner() -> { id: string?, name: string?, at: number, exclusive: boolean }?
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 exclusive is true, id is the key a hold of its
own states to be admitted, and the key renderer.temporal.release hands
the clock back by. id and name are nil for a hold that stated no key
and that the engine attributes to no agent.
Returns { id: string?, name: string?, at: number, exclusive: boolean }? — { id, name, at, exclusive } for the standing hold, or nil when the clock is running.
local who = renderer.temporal.owner()
if who ~= nil and who.exclusive then print(who.name, "holds the clock at", who.at) end
typed/builtin//modules/api/engine/renderer/renderer/temporal/release
renderer.temporal.release(owner: string) -> number
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.
Parameters
ownerstring— The key the holds to release were taken under — whatownerstated when they were taken, whichrenderer.temporal.ownerreports.
Returns number — How many holds came off the stack.
renderer.temporal.release("stage-air")
typed/builtin//modules/api/engine/renderer/renderer/texture/capture
renderer.texture.capture(texture: string | { [string]: any } | AssetRef) -> string
Request a CPU readback of the GPU texture texture names (e.g. a
camera's rendered output). Returns a result key to pass to a
TextureCpuHandle's :encode() once the readback completes. Takes every form
that names a texture — the TextureHandle create returned, the guid
renderer.texture.list hands out, a TextureCpuHandle or a texture
AssetRef.
Parameters
texturestring | { [string]: any } | AssetRef— The texture to read back — aTextureHandle, a guid, aTextureCpuHandleor a textureAssetRef.
Returns string The capture result key.
local key = renderer.texture.capture(cameraTarget)
typed/builtin//modules/api/engine/renderer/renderer/texture/cpuCreate
renderer.texture.cpuCreate(width: number, height: number, fill: any?) -> TextureCpuHandle
Allocate a blank CPU image (RGBA8) filled with a solid colour and return a
TextureCpuHandle. Compose into it with canvas:blit(src, x, y, w, h), then
canvas:encodeJpeg() / :encodePng() for the bytes; :unload() drops it.
Parameters
widthnumber— number Canvas width in pixels.heightnumber— number Canvas height in pixels.fillany(optional) — Optional{ r, g, b, a }(0-255) solid fill; defaults to opaque white.
Returns TextureCpuHandle
local c = renderer.texture.cpuCreate(1024, 576, { 255, 255, 255, 255 })
typed/builtin//modules/api/engine/renderer/renderer/texture/cpuFromBytes
renderer.texture.cpuFromBytes(bytes: buffer | string, encodeOpts: any?) -> TextureCpuHandle
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 data.ztex
read as a file, a payload held in memory. The pixels stay at the format
they were encoded in. DEFAULT: handle:unload() once done with them.
Parameters
bytesbuffer | string— The ZTEX or image bytes.encodeOptsany(optional) —{ colorSpace?, compression?, generateMipmaps?, maxDimension?, filter?, textureType?, alpha?, alphaIsTransparency? }— the optsrenderer.texture.encodeFromImagetakes — applied when the bytes are an encoded image and need the engine-native encode.maxDimensionbounds 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.
Returns TextureCpuHandle
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 })
typed/builtin//modules/api/engine/renderer/renderer/texture/create
renderer.texture.create(src: any?, guid: string?) -> TextureHandle
Create (or fetch) a GPU texture resource and return its TextureHandle.
src: a TextureCpuHandle from texRef:load() (CPU→GPU under the asset's
guid, idempotent); raw pixels {rgba, width, height, srgb?, format?} (a
flat widthheight4 byte payload, 0-255, row-major, top-to-bottom, RGBA —
a buffer, a binary string, or a number array; format = "rgba16f"
uploads an HDR texture instead, where rgba carries float channel
values); a TextureHandle (returned as-is);
or render-target dimensions {width, height, name?, format?} with no pixel
source — an empty GPU texture a render pass writes into (camera output,
UI surface) and that samples like any other texture. format names the
colour format the target is allocated in, and the passes drawing into it
are built for that format: "rgba8unorm" / "bgra8unorm" (the two
eight-bit channel orders, either of which a surface may carry),
"rgba16f" / "rgba32f", "rg16f" / "rg32f", "r16f" / "r32f".
Each also answers to its spelled-out width ("rgba16float", "r32float",
and so on), in any case. Omit it and a screen target takes the scene's
own format ("rgba16f") — 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. "rgba8" is what a mask, a weight or an occlusion term asks
for, where 0..1 is the whole range; hand one back over @scene.color 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
format 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 filter the way raw pixels do: "nearest" 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; "linear" (the default) smooths between them. It also
takes screen (the engine keeps it the size of the image being drawn),
screenScale (the fraction of that size it takes) and screenSpace
("scene", the default, or "composite" — 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 screen 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 screen 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 screenSpace = "composite", or from a
target created without screen. history = true 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 guid names the image that render
writes and previous.guid 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 state (one vec4<f32>, bound through a pass's
buffers): x is 1 when previous 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 previous reads transparent black), y how many renders in a row
have carried the history, zw the render's sub-pixel projection offset in
pixels (renderer.setProjectionJitter). 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.
Parameters
srcany(optional) — A TextureCpuHandle, raw pixels, a TextureHandle, or render-target dimensions.guidstring(optional) — 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.
Returns TextureHandle
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" })
typed/builtin//modules/api/engine/renderer/renderer/texture/createFromAsset
renderer.texture.createFromAsset(ref: string | AssetRef, encodeOpts: any?, keepCpu: boolean?) -> TextureHandle
Put a .texture 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
renderer.texture.isResident reports the arrival. The decoded pixels are
dropped once uploaded unless keepCpu holds them in the CPU store for
textureRef:load()-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.
Parameters
refstring | AssetRef— A textureAssetRef, or a string naming one (guid, identity, name or source path).encodeOptsany(optional) —{ colorSpace?, compression?, generateMipmaps?, maxDimension?, filter?, textureType?, alpha?, alphaIsTransparency? }— the optsrenderer.texture.encodeFromImagetakes — applied when the primary is an encoded source image and needs the engine-native encode (a.ztexprimary is decoded as-is).keepCpuboolean(optional) — Keep the decoded pixels in the CPU store after the upload.
Returns TextureHandle
local h = renderer.texture.createFromAsset(texRef) -- bind h.guid on a material; it draws once resident
typed/builtin//modules/api/engine/renderer/renderer/texture/decode
renderer.texture.decode(bytes: buffer | string, opts: { straightAlpha: boolean? }?) -> (any, any, any, any)
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:
- an engine-native
ZTEXpayload — 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. AZTEXholding block-compressed or verbatim source-image levels decodes to"rgba8". - source image bytes — png, jpeg, gif or webp, straight off disk or out of
a
capture— decoded to"rgba8"at whatever colour type, bit depth or interlacing the file was written with. This is the call that reads the pixels of a screenshot.
The fourth return names the format the buffer came back in: "rgba8" (4
bytes/texel, channels 0-255), "rgba16" (8 bytes/texel, 16-bit unsigned
normalized channels 0-65535) or "rgba32f" (16 bytes/texel, float
channels).
The pixels are the texels the payload stores. With opts.straightAlpha = true, a payload encoded with alpha = "premultiplied" has its colour
divided back by alpha, in the colour space it was premultiplied in — the
straight-alpha pixels renderer.texture.encode takes. A texel whose alpha
is 0 keeps the black premultiplying left in it.
Parameters
bytesbuffer | string— AZTEXpayload or source image bytes.opts{ straightAlpha: boolean? }(optional) —{ straightAlpha? }
Returns (any, any, any, any) — (string?, number?, number?, string?) pixels, width, height, format — or (nil, errmsg) where errmsg is in the 2nd slot.
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 })
typed/builtin//modules/api/engine/renderer/renderer/texture/destroy
renderer.texture.destroy(texture: string | { [string]: any } | AssetRef) -> boolean
Release the GPU texture texture 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, renderer.texture.list stops answering for the guid. Takes
every form that names a texture — the TextureHandle create returned, the
guid the listing hands out, a TextureCpuHandle or a texture AssetRef.
Parameters
texturestring | { [string]: any } | AssetRef— The texture to release — aTextureHandle, a guid, aTextureCpuHandleor a textureAssetRef.
Returns boolean true when a texture was known under the guid.
renderer.texture.destroy(rt)
renderer.texture.destroy(renderer.texture.list()[1].guid)
typed/builtin//modules/api/engine/renderer/renderer/texture/encode
renderer.texture.encode(rgba: any?, width: number, height: number, opts: any?) -> (string?, string?)
Encode raw pixels into an engine-native ZTEX payload (the on-disk
texture content). The CPU codec behind the texture assetType's onCreate.
Called from a task (an execute chunk, a component hook the engine lets
wait, task.spawn), a "uastc" or "bc7" 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 (__texture.encodeBudget, reported by
__texture.encodeJobs). The call returns on the frame the payload lands.
Code that cannot yield, and a coroutine the script resumes itself
(coroutine.create / coroutine.wrap), encode in place, and every other
compression is encoded on the call. The payload is the same bytes either
way.
opts.compression selects what the texels are stored as: "rgba8"
(default, 8 bits/channel, rgba is widthheight4 bytes), "uastc"
(UASTC 4x4 blocks, a quarter of the RGBA8 size, transcoded on each device
to the block format it holds; rgba is widthheight4 bytes and each
side is resampled to the nearest power of two, halfway rounding up),
"bc7" (BC7 blocks, a quarter of the RGBA8 size; rgba is still
widthheight4 bytes and both sides a multiple of 4) or the high-precision data compressions
"rgba16" (16-bit unsigned normalized, widthheight8 bytes) /
"rgba32f" (32-bit float, widthheight16 bytes) — for height/displacement
fields, baked lightmaps, and other data rasters 8 bits quantize visibly.
The two high-precision compressions store rgba verbatim, reject
opts.generateMipmaps / opts.maxDimension, and take opts.colorSpace = "linear" only. opts.colorSpace is "srgb" (default, colour data) or
"linear" (normal maps, masks, heights). opts.input ("rgba8" /
"rgba16" / "rgba32f") names the texel format rgba arrives in when it
is not the one opts.compression stores — the pixels
renderer.texture.decode hands back at their own width, say — and the
encoder carries each channel across as the same normalized value,
natively, before storing it.
opts.textureType records what the texture holds — "color"
(default), "normalMap" (when the encode generates mips, every level
of the chain, the base included, is renormalised to unit length),
"mask" or "sprite".
opts.alpha is what the encode does to alpha: "input" (default, kept
as given), "premultiplied" (RGB multiplied by alpha — in linear light
under srgb, as stored under linear — the source a material with the
premultiplied blend mode composites) or "none" (every texel opaque, RGB
kept). opts.alphaIsTransparency = true 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 renderer.texture.info.
The sampler opts are recorded the same way: opts.filter ("linear",
the default, or "nearest"), opts.wrapU / opts.wrapV ("repeat", the
default, "clamp" or "mirror" — how the texture is sampled past its
edges) and opts.anisotropy ("world", the default, following the
world's render.anisotropy, or a fixed "1" / "2" / "4" / "8" /
"16", also taken as the number). opts.effort ("fastest", the default,
"fast" or "thorough") 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. opts.uastcRdo ("none", the
default, "low", "medium" or "high") 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.
Parameters
rgbaany(optional) — Pixel payload atopts.input's (elseopts.compression's) native byte width — abuffer, a binary string, or a number array.widthnumber— numberheightnumber— numberoptsany(optional) —{ colorSpace?, compression?, uastcRdo?, generateMipmaps?, maxDimension?, filter?, wrapU?, wrapV?, anisotropy?, textureType?, alpha?, alphaIsTransparency?, input? }
Returns (string?, string?) ZTEX bytes, or (nil, errmsg).
typed/builtin//modules/api/engine/renderer/renderer/texture/encodeFromImage
renderer.texture.encodeFromImage(bytes: buffer | string, opts: any?) -> (string?, string?)
Encode source image bytes (png/jpg/webp/…) into an engine-native ZTEX
payload. Used by the texture importer / assetType onChange. A "uastc"
or "bc7" encode called from code that can yield runs where the frame
loop keeps stepping, as renderer.texture.encode describes.
opts.compression is "rgba8" (default), "uastc", "bc7", "rgba16"
or "rgba32f", opts.colorSpace "srgb" (default) or "linear", and
opts.uastcRdo as renderer.texture.encode takes it. "rgba8" with
generateMipmaps = false, a maxDimension the image fits within (or
none), alpha = "input" and no alphaIsTransparency stores the source
image bytes verbatim.
"rgba16" and "rgba32f" 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 v
is stored as v * 257 (rgba16) or v / 255 (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 v * 257 of their 8-bit values. Like
encode, they
reject opts.generateMipmaps and a maxDimension the image exceeds.
opts.textureType records what the texture holds — "color"
(default), "normalMap" (when the encode generates mips, every level
of the chain, the base included, is renormalised to unit length),
"mask" or "sprite".
opts.alpha is what the encode does to alpha: "input" (default, kept
as given), "premultiplied" (RGB multiplied by alpha — in linear light
under srgb, as stored under linear — the source a material with the
premultiplied blend mode composites) or "none" (every texel opaque, RGB
kept). opts.alphaIsTransparency = true 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 renderer.texture.info, as are the sampler
opts filter, wrapU, wrapV and anisotropy, which take the values
renderer.texture.encode takes.
Parameters
bytesbuffer | string— source image bytes.optsany(optional) —{ colorSpace?, compression?, uastcRdo?, generateMipmaps?, maxDimension?, filter?, wrapU?, wrapV?, anisotropy?, textureType?, alpha?, alphaIsTransparency? }
Returns (string?, string?) ZTEX bytes, or (nil, errmsg).
typed/builtin//modules/api/engine/renderer/renderer/texture/frameSchedule
renderer.texture.frameSchedule(texture: string | AssetRef) -> { number }?
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.
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 schedule slot of the builtin
animatedTexture shader holds, one entry per layer.
A texture whose layers carry no timing — a still image, a sprite sheet, a LUT stack — has no schedule and answers nil.
Parameters
texturestring | AssetRef— The texture — a guid, an identity, a name, a path, or a textureAssetRef.
Returns { number }? one cumulative end time per layer, or nil.
local ends = renderer.texture.frameSchedule(texRef) -- {0.1, 0.15, 0.35}
typed/builtin//modules/api/engine/renderer/renderer/texture/info
renderer.texture.info(ztex: buffer | string) -> (any, any)
Read the header of an engine-native ZTEX payload without copying the
pixels. Returns its format, its compression ("rgba8", "bc7", "rgba16"
or "rgba32f" — what the texels are stored as) and colorSpace ("srgb" or
"linear" — how they are read when sampled), dimensions, mip count,
the sampler recorded in the payload from the asset's settings — filter
("nearest" or "linear"), wrapU and wrapV ("repeat", "clamp" or
"mirror") and anisotropy ("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 —
textureType ("color", "normalMap", "mask" or "sprite"), alpha
("input", "premultiplied" or "none") and alphaIsTransparency (boolean);
a payload written before these were recorded reads color / input /
false — and the payload's layer shape.
layers counts the array layers the payload carries and isArray is true
past one — the answer to "am I about to sample a texture_2d_array?",
available before anything samples it. animated is true when those layers
are a sequence in time; then frameDelaysMs lists each layer's display
time in milliseconds in display order, and durationMs totals one pass.
An animated image imports as one layer per frame, so layers is its frame
count. A still texture reports layers = 1, isArray = false.
Parameters
ztexbuffer | string— ZTEX bytes.
Returns (any, any) — (table?, string?) { format, compression, colorSpace, textureType, alpha, alphaIsTransparency, width, height, mipCount, filter, wrapU, wrapV, anisotropy, layers, isArray, animated, frameDelaysMs?, durationMs? }, or (nil, errmsg).
local i = renderer.texture.info(bytes); if i.animated then print(i.layers, "frames", i.durationMs, "ms") end
typed/builtin//modules/api/engine/renderer/renderer/texture/isCpuResident
renderer.texture.isCpuResident(texture: string | { [string]: any } | AssetRef) -> boolean
True if this texture has a resident CPU copy in the guid-keyed CPU store.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — aTextureHandle, aTextureCpuHandle, a guid, or a textureAssetRef.
Returns boolean
if renderer.texture.isCpuResident(handle) then ... end
typed/builtin//modules/api/engine/renderer/renderer/texture/isResident
renderer.texture.isResident(texture: string | { [string]: any } | AssetRef) -> boolean
True if a GPU texture is resident under this texture's guid.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — aTextureHandle, aTextureCpuHandle, a guid, or a textureAssetRef.
Returns boolean
print(renderer.texture.isResident(handle))
typed/builtin//modules/api/engine/renderer/renderer/texture/keepCpuOnLanding
renderer.texture.keepCpuOnLanding(texture: string | { [string]: any } | AssetRef, keep: boolean) -> boolean
Set whether the decode on its way to this texture — scheduled by
textureRef:handle() or renderer.texture.createFromAsset and not yet on
the device — keeps its decoded pixels in the CPU store once it lands. A
texture's keepCpu setting changing while its decode is in flight reaches
the texture through this.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — aTextureHandle, aTextureCpuHandle, a guid, or a textureAssetRef.keepboolean— Keep the CPU copy once the decode lands.
Returns boolean — true when a decode was on its way; false changes nothing.
renderer.texture.keepCpuOnLanding(texRef, true)
typed/builtin//modules/api/engine/renderer/renderer/texture/list
renderer.texture.list() -> { any }
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 (origin is "asset" 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 (mipLevels), so the
listing sums to renderer.textureMemory(). A streamable one carries
streamOrigin — "asset" when a level change reads the levels it needs
back from the asset, "device" when no asset answers for it and its whole
chain on the device is the only copy, "retained" 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: filter ("linear" / "nearest"), wrapU and
wrapV ("repeat" / "clamp" / "mirror") and anisotropy
("world" or a fixed "1".."16"), and samplerAnisotropy, the level
the texture's sampler takes: anisotropy resolved against
renderer.anisotropy() 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 held — whether renderer.hold pins
it for the session — and scene, the load that created it.
renderer.references("texture", guid) says what is still holding a row,
and renderer.collect() releases the rows nothing holds.
Returns { any } — Array of { guid, origin, owner?, scene?, held?, resident, bytes?, width?, height?, mipLevels?, format?, filter?, wrapU?, wrapV?, anisotropy?, samplerAnisotropy?, compressed?, streamable?, streamOrigin? }.
for _, t in ipairs(renderer.texture.list()) do print(t.guid, t.bytes) end
typed/builtin//modules/api/engine/renderer/renderer/texture/loadCpu
renderer.texture.loadCpu(ref: string | AssetRef, encodeOpts: any?) -> TextureCpuHandle
Load a .texture 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: handle.format is
"rgba8", "rgba16" or "rgba32f", and :readPixel reports channels in
that format's own units. Called by texRef:load(). DEFAULT: upload to the
GPU then handle:unload().
Parameters
refstring | AssetRef— A textureAssetRef(carries.guid, reads its primary via getBytes), or any stringasset.refresolves to one — a guid, an identity, a name or a source path.encodeOptsany(optional) —{ colorSpace?, compression?, generateMipmaps?, maxDimension?, filter?, textureType?, alpha?, alphaIsTransparency? }— the optsrenderer.texture.encodeFromImagetakes — applied when the primary is an encoded source image and needs the engine-native encode (a.ztexprimary is loaded as-is).
Returns TextureCpuHandle
local cpu = texRef:load(); local r,g,b,a = cpu:readPixel(3, 4); cpu:unload()
typed/builtin//modules/api/engine/renderer/renderer/texture/readback
renderer.texture.readback(texture: string | { [string]: any } | AssetRef) -> TextureCpuHandle
Read a runtime GPU texture's pixels back to CPU and return a
TextureCpuHandle for them — the GPU→CPU half of the runtime-texture freeze
path. A texture made with renderer.texture.create keeps no CPU copy, so
persisting it (:encode() → asset.create("texture", …)) 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: :readPixel,
:writePixel, :getInfo, :encode, :unload all work. Errors if the
texture never becomes GPU-resident.
A SCENE-space screen-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.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — theTextureHandlerenderer.texture.createreturned, a guid, or a textureAssetRef.
Returns TextureCpuHandle
local cpu = renderer.texture.readback(handle); local ztex = cpu:encode(); cpu:unload()
typed/builtin//modules/api/engine/renderer/renderer/texture/reencode
renderer.texture.reencode(ztex: buffer | string, opts: any?) -> (string?, string?)
Re-encode an engine-native ZTEX payload against new opts. A "uastc"
or "bc7" re-encode called from code that can yield runs where the frame
loop keeps stepping, as renderer.texture.encode describes. opts is
the complete encode spec, exactly as for renderer.texture.encode: every
field it leaves out takes its default, so textureType, alpha,
alphaIsTransparency, filter, wrapU, wrapV and anisotropy come
from opts — not from the payload's header — and a payload re-encoded
with { compression = "uastc" } alone loses a nearest filter or a
premultiplied alpha it carried. renderer.texture.withSampler 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 alpha = "premultiplied" has its colour divided back by
alpha), carried to the width the new compression stores as the same
normalized value, and encoded exactly as renderer.texture.encode 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 alpha = "none" 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.
Parameters
ztexbuffer | string— AZTEXpayload — abufferor a binary string.optsany(optional) —{ colorSpace?, compression?, uastcRdo?, generateMipmaps?, maxDimension?, filter?, wrapU?, wrapV?, anisotropy?, textureType?, alpha?, alphaIsTransparency? }
Returns (string?, string?) ZTEX bytes, or (nil, errmsg).
local wide = renderer.texture.reencode(vfs.read(texPath .. "/data.ztex"), { compression = "rgba16", colorSpace = "linear", generateMipmaps = false })
typed/builtin//modules/api/engine/renderer/renderer/texture/tone
renderer.texture.tone(histogram: any?) -> TextureTone
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.
span (max - min) is the whole range including a single stray pixel;
spread (p95 - p5) 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
mean and a tiny spread, and no mean alone can tell that apart from a
frame with a subject in it.
crushed and clipped 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.
Parameters
histogramany(optional) — A histogram fromcpu:histogram().
Returns TextureTone
local t = cpu:tone(); if t.spread < 24 then error("the shot is flat") end
typed/builtin//modules/api/engine/renderer/renderer/texture/unloadCpu
renderer.texture.unloadCpu(texture: string | { [string]: any } | AssetRef)
Drop this texture's resident CPU copy from the guid-keyed CPU store.
The explicit release for a texture asset's keepCpu setting turning off.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — aTextureHandle, aTextureCpuHandle, a guid, or a textureAssetRef.
renderer.texture.unloadCpu(texRef)
typed/builtin//modules/api/engine/renderer/renderer/texture/update
renderer.texture.update(texture: string | { [string]: any } | AssetRef, src: any?) -> TextureHandle
Overwrite the GPU texture texture names IN PLACE, under the same guid,
from new pixels. Never writes a .texture file — the play-mode mutate
path. Takes every form that names a texture — the TextureHandle create
returned, the guid renderer.texture.list hands out, a TextureCpuHandle
or a texture AssetRef. src is raw pixels, or a TextureCpuHandle
(texRef:load(), renderer.texture.loadCpu, renderer.texture.readback),
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.
Parameters
texturestring | { [string]: any } | AssetRef— The texture to update — aTextureHandle, a guid, aTextureCpuHandleor a textureAssetRef.srcany(optional) — New raw pixels{rgba, width, height, srgb?, format?}—rgbaas abuffer, a binary string, or a number array — or aTextureCpuHandle.
Returns TextureHandle — A TextureHandle for the updated texture.
typed/builtin//modules/api/engine/renderer/renderer/texture/withSampler
renderer.texture.withSampler(ztex: buffer | string, opts: { filter: string?, wrapU: string?, wrapV: string?, anisotropy: (string | number)? }) -> (string?, string?)
Change how an engine-native ZTEX payload is sampled without
re-encoding it. opts names any of filter ("linear" / "nearest"),
wrapU and wrapV ("repeat" / "clamp" / "mirror") and anisotropy
("world", or "1" / "2" / "4" / "8" / "16", 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.
Parameters
ztexbuffer | string— AZTEXpayload — abufferor a binary string.opts{ filter: string?, wrapU: string?, wrapV: string?, anisotropy: (string | number)? }—{ filter?, wrapU?, wrapV?, anisotropy? }
Returns (string?, string?) ZTEX bytes, or (nil, errmsg).
local clamped = renderer.texture.withSampler(vfs.read(texPath .. "/data.ztex"), { wrapU = "clamp", wrapV = "clamp" })
typed/builtin//modules/api/engine/renderer/renderer/textureMemory
renderer.textureMemory() -> {
What the GPU texture cache holds, split by whether the texture is
block-compressed. compressedBytes and uncompressedBytes are what those
textures cost in VRAM, measured from each texture's own format and mip
chain — so a .texture whose settings name compression = "uastc" appears in the
compressed columns at a quarter of what the same image costs as RGBA8.
blockCompressionSupported 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.
blockFormat names the format block-compressed textures are resident in
on this adapter ("bc7" on desktop, "astc4x4" on a phone, "etc2" on
a phone without ASTC) and is nil where it samples none. A texture stored
as UASTC is transcoded to blockFormat; one stored as BC7 keeps its
blocks where the adapter samples BC7 and is re-encoded into blockFormat
where it does not, each at the same one byte per texel.
Measured at the end of the last rendered frame.
streamableTextures 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: assetStreamedTextures 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. retainedTextures counts the ones
holding a copy, and streamSourceBytes is what those copies occupy in
system memory — bytes that are not VRAM. pinnedTextures 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.
byFormat names every GPU format a cached texture is held in, keyed by
the format's name (Bc7RgbaUnormSrgb, Astc { block: B4x4, .. },
Rgba8UnormSrgb, ...), with how many textures it holds and their bytes.
heldAboveCap lists the textures resident larger than
renderer.setMaxTextureSize 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. decodedFallback lists the
UASTC textures this device holds as texels because it samples no block
format.
Returns { blockCompressionSupported, blockFormat?, compressedTextures, compressedBytes, uncompressedTextures, uncompressedBytes, streamableTextures, assetStreamedTextures, retainedTextures, pinnedTextures, streamSourceBytes, byFormat, heldAboveCap, decodedFallback }
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))
typed/builtin//modules/api/engine/renderer/renderer/textureStreaming
renderer.textureStreaming() -> TextureStreaming
What the last frame's texture-residency plan decided. budgetBytes is
the budget in force: the device's own, or the one setTextureBudget
names, as much of it as the memory pressure in force leaves (half at
"critical"). deviceBudgetBytes is the device's own budget (what
setTextureBudget names deviceMegabytes): memoryBudgetBytes, held to
the quality tier's cap where the tier states one. memoryBudgetBytes is
what the device's memory affords and budgetRule the rule it came from:
"systemMemory", an eighth of the RAM the system states
(deviceMemoryBytes, nil where it states none), on a GPU that draws from
that RAM, or "bufferSize", half the adapter's max_buffer_size; between
256 MiB and 2 GiB either way. pressure is the memory pressure level in force; from
"moderate" up the plan holds every texture at the level its surfaces
sample, below the budget as well as past it (renderer.memoryPressure()).
streamable is how many textures the plan can move.
residentBytes is what those textures occupy now, measured from the
textures that are allocated; demandedBytes 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. starved counts the textures left
coarser than the frame asked for, promoted the ones that climbed a level
this frame, and changed the ones whose GPU texture was replaced. A camera
approaching a surface reads promoted above zero for a few frames and then
zero once it settles.
textures 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.
Returns TextureStreaming — { budgetBytes, deviceBudgetBytes, memoryBudgetBytes, budgetRule, deviceMemoryBytes?, pressure, streamable, residentBytes, plannedBytes, demandedBytes, starved, promoted, changed, textures }
local ts = renderer.textureStreaming()
print(("textures: %.1f MiB resident of %.1f MiB demanded, %d starved"):format(
ts.residentBytes / 1048576, ts.demandedBytes / 1048576, ts.starved))
typed/builtin//modules/api/engine/renderer/renderer/transmissionShadows
renderer.transmissionShadows() -> boolean
Whether translucent casters tint the directional light they block.
Returns boolean
typed/builtin//modules/api/engine/renderer/renderer/uploadStats
renderer.uploadStats() -> {
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 task.waitDrawnFrames 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 renderer.mesh.drawInstanced
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. bytes is what those
uploads and copies carried, fullBytes what re-sending every slot would
have cost, and writes how many buffer writes carried it. The three numbers
cover that per-renderable data alone, so a scene standing still reads
bytes = 0 against a fullBytes 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.
Returns { writes: number, bytes: number, fullBytes: number }
local u = renderer.uploadStats()
print(("instance upload: %d B of %d B in %d writes"):format(u.bytes, u.fullBytes, u.writes))
typed/builtin//modules/api/engine/renderer/renderer/upscaleBackend
renderer.upscaleBackend() -> string
The backend actually filling the upscale seam this frame — "blit"
or "dlss". Reports what the renderer resolved the last
renderer.setUpscaleBackend() request into, which reads "blit" after a
request for "dlss" on an adapter renderer.upscaleCapabilities() omits
it from.
Returns string — "blit" or "dlss".
local backend = renderer.upscaleBackend()
typed/builtin//modules/api/engine/renderer/renderer/upscaleCapabilities
renderer.upscaleCapabilities() -> { string }
The backend names this engine can actually run for the upscale seam,
from a boot-time probe of the adapter. "blit" always answers, since the
stretch needs no adapter capability; "dlss" joins it only where NGX
DLSS Super Resolution is available. Read this before
renderer.setUpscaleBackend("dlss", ...) to know ahead of the request
what it resolves to.
Returns { string } — at least { "blit" }.
local caps = renderer.upscaleCapabilities()
typed/builtin//modules/api/engine/renderer/renderer/variantSource
renderer.variantSource(program: string) -> string?
The WGSL one of the programs renderer.shaderVariants() lists holds,
exactly as the shader compiler received it. program is the program
field of a row's base or of one of its variants. 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
renderer.compiledSource, which answers the same for every other shader.
Parameters
programstring— Aprogramname fromrenderer.shaderVariants().
Returns string? — The compiled WGSL, or nil for a name no compile has run under.
local row = renderer.shaderVariants()[1]
local base = renderer.variantSource(row.base.program)
typed/builtin//modules/api/engine/renderer/renderer/viewportSize
renderer.viewportSize() -> { width: number, height: number }
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 renderer.setViewportSize, a
window drag and a browser page resize alike, so it reports what is
realized rather than what was asked for.
Returns { width: number, height: number } in pixels.
local v = renderer.viewportSize()
typed/builtin//modules/api/engine/renderer/renderer/watchShadowPages
renderer.watchShadowPages(runs: number)
Gather a breakdown of what the sun's virtual shadow map does on each of
the main camera's next runs runs. Each run's counts come back from the
GPU a few frames after it, and renderer.shadowPageBreakdown() hands them
over. Nothing is gathered while no reader has asked. 0 stops a
gathering under way.
Parameters
runsnumber— How many of the main camera's runs to gather.
renderer.watchShadowPages(120)
typed/builtin//modules/api/engine/service/service/authenticated
service.authenticated() -> boolean
Whether a platform identity (JWT) is available to attach to service calls. Returns only a boolean — never the token.
Returns boolean — True if a caller identity is available.
if not service.authenticated() then error("link ZeroMind") end
typed/builtin//modules/api/engine/service/service/balance
service.balance() -> PromiseHandle?
Read the caller's credit balance from ZeroMind. Returns a
promise handle for task.await() resolving the balance JSON, or nil
when the gateway is unconfigured or no caller identity is available.
Returns PromiseHandle? — Promise handle for task.await(), or nil if not ready.
local h = service.balance(); local raw = h and task.await(h)
typed/builtin//modules/api/engine/service/service/gatewayConfigured
service.gatewayConfigured() -> boolean
Whether the ZeroMind service gateway has been configured.
Service handlers use this to distinguish "gateway not configured"
from "not signed in" when invoke returns nil.
Returns boolean — True if the gateway base URL is set.
if not service.gatewayConfigured() then error("no gateway") end
typed/builtin//modules/api/engine/service/service/invoke
service.invoke(offering: string, endpoint: string, opts: InvokeOpts?) -> PromiseHandle?
Invoke a provider offering's logical endpoint through ZeroMind.
Returns a promise handle for task.await() 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.
Parameters
offeringstring— Fully-qualified offering identityprovider/name(e.g. "origozero/mesh_gen").endpointstring— Logical endpoint name (e.g. "create_preview").optsInvokeOpts(optional) —{ params?, headers?, body?, idempotency_key? }.
Returns PromiseHandle? — Promise handle for task.await(), or nil if not ready.
local h = service.invoke("origozero/mesh_gen", "create_preview", { body = { prompt = p } })
typed/builtin//modules/api/engine/service/service/invokeStream
service.invokeStream(offering: string, endpoint: string, opts: StreamOpts) -> ServiceStream?
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 zeromind.settled event whose data is {"charged":N,"balance":M}, or
a zeromind.failed event whose data is {"reason":...} when the relay
stopped early. A call it refuses before the stream starts arrives as one
zeromind.refused event carrying the HTTP status and the JSON answer
as data; a call that could not be made, or a stream that broke or went
quiet, arrives as zeromind.failed. Returns a stream at once, or nil
when the gateway is unconfigured or no caller identity is available,
exactly as invoke. Take events with stream:poll() (never waits) or
stream:read() (waits for the next event; nil at the end), and release
the connection with stream:close(). opts takes what invoke takes,
plus a required idempotency_key that is unique to this call (ZeroMind
refuses a key a streamed call already used) and an optional timeout_ms
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.
Parameters
offeringstring— Fully-qualified offering identityprovider/name(e.g. "origozero/llm").endpointstring— Logical endpoint name (e.g. "chat").optsStreamOpts—{ params?, headers?, body?, idempotency_key, timeout_ms? }.
Returns ServiceStream? — A stream, or nil if not ready.
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
typed/builtin//modules/api/engine/service/service/jobStatus
service.jobStatus(jobId: string) -> PromiseHandle?
Poll a submitted service job. Returns a promise handle for
task.await() resolving the JobStatusResponse JSON { job_id, status, result?, error? }: status walks pending/running -> succeeded
(with result, the same InvokeResponse invoke returns) or failed
(with error). nil when the gateway is unconfigured or no caller
identity is available.
Parameters
jobIdstring— Job id returned bysubmitJob.
Returns PromiseHandle? — Promise handle resolving the job status JSON, or nil if not ready.
local h = service.jobStatus(jobId); local raw = h and task.await(h)
typed/builtin//modules/api/engine/service/service/submitJob
service.submitJob(offering: string, endpoint: string, opts: InvokeOpts?) -> PromiseHandle?
Submit a durable async invocation of an offering endpoint. Same
arguments as invoke, 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
task.await() resolving { job_id, status }; poll it with
jobStatus. nil when the gateway is unconfigured or no caller
identity is available.
Parameters
offeringstring— Fully-qualified offering identityprovider/name(e.g. "origozero/mesh_gen").endpointstring— Logical endpoint name (e.g. "create_preview").optsInvokeOpts(optional) —{ params?, headers?, body?, idempotency_key? }.
Returns PromiseHandle? — Promise handle resolving { job_id, status }, or nil if not ready.
local h = service.submitJob("origozero/mesh_gen", "create_preview", { body = { prompt = p } })
typed/builtin//modules/api/engine/shader/shader/compile
shader.compile(keys: string | { string }, opts: { [string]: any }) -> boolean
Compile a zero-scaffolding SURFACE shader: the author wrote only
vertex() / fragment() 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.
Parameters
keysstring | { string }— The shader key (guid@checksumof the version compiled), alone or as a one-element array.opts{ [string]: any }—{ source, domain?, properties?, replaces? }— the author's WGSL, its@domain, the declared property schema, and the key this shader compiled under before its source changed, whose materials move to the new key.
Returns boolean — True when the compile was queued.
local m = asset.meta(ref); shader.compile(m.guid .. "@" .. m.checksum, { source = wgsl, properties = props })
typed/builtin//modules/api/engine/shader/shader/forget
shader.forget(keys: string | { string }) -> boolean
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.
Parameters
keysstring | { string }— The shader key (guid@checksum), alone or in an array.
Returns boolean — True when the removal was queued.
local m = asset.meta(ref); shader.forget(m.guid .. "@" .. m.checksum)
typed/builtin//modules/api/engine/shader/shader/registerModule
shader.registerModule(keys: string | { string }, source: string) -> boolean
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.
Parameters
keysstring | { string }— One key, or the array of keys this module answers to.sourcestring— The module's WGSL.
Returns boolean — True when the registration was queued.
shader.registerModule({ ref.guid, ref.identity }, wgsl)
typed/builtin//modules/api/engine/shader/shader/status
shader.status(name: string) -> (string, string?)
A shader's latest compile outcome, without reading the engine log:
"compiled", "failed" (with the compiler error second), or "pending".
Compilation is async, so a "pending" straight after a write means ask
again next frame.
Parameters
namestring— The key it compiled under (guid@checksum), or a reference to a shader asset (identity, path, guid), which reaches the key of the version that reference resolves to now.
Returns (string, string?) — Status, and the compiler error when it failed.
local status, err = shader.status("myShader")
typed/builtin//modules/api/engine/shell/shell/run
shell.run(command: string, sessionKey: string?) -> ShellResult
Execute a command in the engine's emulated Unix shell and
return once it has completed. This is the same shell as the MCP
bash tool — 60+ builtins (ls, cat, grep, find, echo, ...)
operating on the virtual scene filesystem. A command that runs
Luau (run, luau, zm, zero) needs the engine's frame loop,
and a command whose work is a wait (sleep) 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 stderr and shell.runAsync
as the way to run it whole.
Parameters
commandstring— Shell command to execute.sessionKeystring(optional) — Which caller's shell session the command runs in. A session holds what a command leaves behind: the directorycdmoved to, exports, shell functions,set -e. 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'sbashcommands also use; code with no attested caller behind it runs in the session every caller that names none shares.
Returns ShellResult — Command result { stdout, stderr, exitCode, ok }.
local r = shell.run("ls /zero/source")
typed/builtin//modules/api/engine/shell/shell/runAsync
shell.runAsync(command: string, sessionKey: string?) -> string
Asynchronous version of shell.run. Returns a promise ID that
resolves to a JSON-encoded result string. Use with
task.await().
Parameters
commandstring— Shell command to execute.sessionKeystring(optional) — Which caller's shell session the command runs in. A session holds what a command leaves behind: the directorycdmoved to, exports, shell functions,set -e. 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'sbashcommands also use; code with no attested caller behind it runs in the session every caller that names none shares.
Returns string — Promise ID — pass to task.await() to get the JSON result.
local json = task.await(shell.runAsync("find /zero -name '*.luau'"))
typed/builtin//modules/api/engine/skeleton/skeleton/applyPose
skeleton.applyPose(sinkHandle: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Snapshot the buffer's first layout.total_floats 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.
Parameters
sinkHandlenumber— Sink handle frombindPose.poseBufferSubstrate.TypedBuffer— The pose buffer to apply.
Returns boolean — True on success.
typed/builtin//modules/api/engine/skeleton/skeleton/bindClip
skeleton.bindClip(zanimBytes: buffer | string, boneOrder: { string }) -> ClipBindInfo?
Decode a zanim payload and bind it to boneOrder,
precomputing which of the clip's channels feed each bone so
per-frame sampleClip is allocation-free. Returns
{ handle, matched, total, duration }, or nil on a malformed
payload / empty bone order. Check matched: 0 means the clip
drives none of these bones.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).boneOrder{ string }— Output bone names — one stride-10 record per bone.
Returns ClipBindInfo? — { handle, matched, total, duration }, or nil.
typed/builtin//modules/api/engine/skeleton/skeleton/bindPose
skeleton.bindPose(entityId: (string | entityRef)?, opts: SkeletonLayout) -> number?
Register a pose sink targeting entityId. The opts table
carries the layout: boneOrder is the bone-name array
({"hip", "spine", ...}), stride defaults to 10
(translation.xyz + rotation.xyzw + scale.xyz). Pass entityId
as nil to use the current component's owning entity.
Parameters
entityId(string | entityRef)(optional) — Engine entity id or proxy, or nil for the current entity.optsSkeletonLayout—{ boneOrder, stride }.
Returns number? — Sink handle, or nil.
local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 })
typed/builtin//modules/api/engine/skeleton/skeleton/clipBones
skeleton.clipBones(zanimBytes: buffer | string) -> { string }?
Decode a zanim 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.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { string }? — Bone names referenced by the clip, or nil.
local names = skeleton.clipBones(assert(vfs.read(path .. "/data.zanim")))
typed/builtin//modules/api/engine/skeleton/skeleton/clipDecode
skeleton.clipDecode(zanimBytes: buffer | string) -> string?
Decode a zanim payload to its readable JSON form
({ name, duration, channels, bone_names }), 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 clipToTable, which skips the text.
Returns nil on bytes that aren't a valid zanim payload. Inverse of
clipEncode.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns string? — The clip as a JSON string, or nil.
local json = require("@builtin::modules.json")
local clipJson = skeleton.clipDecode(bytes)
local clip = if clipJson then json.decode(clipJson) else nil
typed/builtin//modules/api/engine/skeleton/skeleton/clipEncode
skeleton.clipEncode(jsonString: string) -> string?
Encode a clip's JSON form (the shape clipDecode returns) back
to a zanim payload — the bytes a .animation stores and
bindClip/sampleClip consume. Inverse of clipDecode. A clip
held as a table encodes with clipFromTable, which skips the text.
Returns nil on invalid JSON.
Parameters
jsonStringstring— A clip JSON document.
Returns string? — The clip's zanim payload bytes, or nil.
local json = require("@builtin::modules.json")
local bytes = skeleton.clipEncode(json.encode(clip))
typed/builtin//modules/api/engine/skeleton/skeleton/clipFromTable
skeleton.clipFromTable(clip: { [string]: any }) -> (string?, string?)
Encode a clip table (the shape clipToTable returns) to a zanim
payload, the bytes a .animation stores and bindClip/sampleClip
consume. Every field is required and every number finite. Inverse of
clipToTable. Returns nil and the reason, naming the field, when the
table is not a clip.
Parameters
clip{ [string]: any }— A clip table:{ name, duration, channels, bone_names }.
Returns (string?, string?) — The clip's zanim payload bytes, or nil and the reason.
local bytes = skeleton.clipFromTable(clip)
typed/builtin//modules/api/engine/skeleton/skeleton/clipPack
skeleton.clipPack(packed: PackedClip) -> (string?, string?)
Encode a clip in packed form (the shape clipUnpack returns) into
zanim bytes. Channels may share one run of times by naming the same
keyOffset; interpolation defaults to Linear; with no boneNames the
clip names its bones in the order its channels first drive them.
times and values take a compute readback's resultBytes()
directly. Returns nil and the reason, naming the channel, when a
channel's keys fall outside the runs or its times run backwards.
Parameters
packedPackedClip— The packed clip.
Returns (string?, string?) — The clip's zanim payload bytes, or nil and the reason.
local bytes = skeleton.clipPack(packed)
typed/builtin//modules/api/engine/skeleton/skeleton/clipToTable
skeleton.clipToTable(zanimBytes: buffer | string) -> { [string]: any }?
Decode a zanim payload to a table: { name, duration, channels = { { target_bone, path, keyframes_in, keyframes_out, interpolation } }, bone_names }, the shape clipDecode writes as JSON, built by the engine
in place. path is Translation, Rotation or Scale; interpolation
is Linear, Step or CubicSpline. Inverse of clipFromTable.
Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { [string]: any }? — The clip as a table, or nil.
local clip = skeleton.clipToTable(bytes)
typed/builtin//modules/api/engine/skeleton/skeleton/clipUnpack
skeleton.clipUnpack(zanimBytes: buffer | string) -> PackedClip?
Decode a zanim payload into its packed form: a channel table
({ bone, path, interpolation, keyOffset, keyCount, valueOffset },
offsets 0-based element indices) and two buffers of little-endian
f32s, times and values, 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
(buf:writeBytes(packed.values)) for a compute pass. Returns nil on
bytes that aren't a valid zanim payload. Inverse of clipPack.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns PackedClip? — The packed clip, or nil.
local packed = skeleton.clipUnpack(bytes)
typed/builtin//modules/api/engine/skeleton/skeleton/jointTransforms
skeleton.jointTransforms(entityId: string | entityRef) -> table
Read a skinned entity's per-joint world transforms for the current animated pose.
Parameters
entityIdstring | entityRef— Engine entity id or proxy of a skinned entity.
Returns table — Array of joint transforms: { position, matrix, parent, name }.
local joints = skeleton.jointTransforms(meshId)
typed/builtin//modules/api/engine/skeleton/skeleton/sampleClip
skeleton.sampleClip(handle: number, time: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Sample the bound clip at time (clamped to [0, duration])
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.
Parameters
handlenumber— Sampler handle frombindClip.timenumber— Sample time in seconds.poseBufferSubstrate.TypedBuffer— The stride-10 pose buffer written into.
Returns boolean — True on success.
typed/builtin//modules/api/engine/skeleton/skeleton/unbindClip
skeleton.unbindClip(handle: number) -> boolean
Drop the bound clip sampler from the registry.
Parameters
handlenumber— Sampler handle to remove.
Returns boolean — True if the sampler existed.
typed/builtin//modules/api/engine/skeleton/skeleton/unbindPose
skeleton.unbindPose(sinkHandle: number) -> boolean
Remove the sink from the registry.
Parameters
sinkHandlenumber— Sink handle to remove.
Returns boolean — True if the sink was present.
typed/builtin//modules/api/engine/sky/sky/get
sky.get() -> { [string]: any }
Get all current sky configuration as a table. Returns the
same fields as sky.set accepts, plus read-only fields like
material_name and type. Color values are returned as
positional arrays [r, g, b].
Returns { [string]: any } — Full sky configuration table.
local cfg = sky.get(); print(cfg.time_of_day)
typed/builtin//modules/api/engine/sky/sky/getTimeOfDay
sky.getTimeOfDay() -> number
Get the current time of day in hours (0-24).
Returns number — Current time of day.
local t = sky.getTimeOfDay()
typed/builtin//modules/api/engine/sky/sky/installFallback
sky.installFallback()
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.
sky.installFallback()
typed/builtin//modules/api/engine/sky/sky/preset
sky.preset(name: string)
Apply a named sky preset. Available: clear_day, sunset,
sunrise, overcast, night, studio, none. Raises a Luau
error for unrecognized names — wrap in pcall if uncertain.
Parameters
namestring— Preset name (case-sensitive).
sky.preset("sunset")
typed/builtin//modules/api/engine/sky/sky/set
sky.set(opts: SkyOpts)
Configure the sky system. All fields are optional — only
provided fields are updated. Color fields accept both named
{x=r, y=g, z=b} and positional {r, g, b} forms. color is
an alias for solid_color.
Parameters
optsSkyOpts— Sky configuration properties.
sky.set({ type = "procedural", time_of_day = 14, sync_sun_to_light = true })
typed/builtin//modules/api/engine/sky/sky/setSunDirection
sky.setSunDirection(dir: SkyColor)
Set an explicit sun direction and disable time-based sun positioning. The directional light is updated to match.
Parameters
dirSkyColor— Normalized sun direction vector.
sky.setSunDirection({ 0.5, -1, 0.3 })
typed/builtin//modules/api/engine/sky/sky/setTimeOfDay
sky.setTimeOfDay(time: number)
Set the time of day (0-24 hours). 0 = midnight, 6 = sunrise, 12 = noon, 18 = sunset.
Parameters
timenumber— Time of day in hours.
sky.setTimeOfDay(18.5)
typed/builtin//modules/api/engine/stream/stream/accept
stream.accept(listener: string) -> string?
Take the connection that has waited longest on the listener, as a
stream handle that reads, writes, and closes exactly like one
stream.open returned. Returns nil when nothing is waiting, so call
it in a loop each tick to take every peer that arrived.
stream.listenerStatus(listener).pending is how many are still
waiting.
Parameters
listenerstring— Listener handle from stream.listen.
Returns string? — The connection's stream handle, or nil when none is waiting.
while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end
typed/builtin//modules/api/engine/stream/stream/close
stream.close(handle: string) -> boolean
Finish whatever handle names — a stream or a listener — and
drop it from the registry.
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 stream.status(handle).pending reach zero
before it closes.
Closing a listener stops it answering new peers and closes the
connections nobody took; the connections stream.accept already
handed over keep running until they are closed themselves.
Parameters
handlestring— Stream handle from stream.open or stream.accept, or listener handle from stream.listen.
Returns boolean — True if a stream or listener was closed, false if handle already named none.
stream.write(peer, response); stream.close(peer)
typed/builtin//modules/api/engine/stream/stream/listen
stream.listen(url: string, opts: StreamListenOpts?) -> PromiseHandle
Hold the address url names and answer the peers that dial it —
the other direction from stream.open, for when the thing you are
talking to starts the conversation and restarts on its own schedule.
Returns a promise handle: task.await() 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 stream.accept.
The host in the url is the interface bound, and the whole of what
decides who can reach it. tcp://127.0.0.1:9000 answers only
programs on this same machine. tcp://0.0.0.0:9000 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 stream.listenerStatus reports
which of the two you got. A port of 0 asks the operating system to
choose one, which that same status then reports.
opts.inboundCapacity and opts.outboundCapacity bound each
answered connection (65536 bytes each by default);
opts.backlog bounds the connections held for stream.accept
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).
The listener belongs to the chunk that opened it — the chunk whose
own code called stream.listen, 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.
stream.listeners() names that chunk as each entry's owner.
Parameters
urlstring— Listen URL — scheme://host:port.optsStreamListenOpts(optional) — Per-connection capacities and the accept backlog (optional).
Returns PromiseHandle — Promise handle for task.await().
local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending)
typed/builtin//modules/api/engine/stream/stream/listenerStatus
stream.listenerStatus(listener: string) -> ListenerStatus?
Report what the listener holds and has handed over. address is
the address the operating system resolved the bind to, port
included — the one to hand a peer. reach says who can connect to
it: "thisMachine" when it is a loopback address and only programs
on this machine can, "network" when any host that can route here
can. accepted counts the connections stream.accept handed over,
pending the ones still waiting, and capacity the value pending
may reach before the listener stops taking connections from the
operating system. nil when handle names no open listener.
Parameters
listenerstring— Listener handle from stream.listen.
Returns ListenerStatus? — Listener status, or nil when handle names no open listener.
local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending)
typed/builtin//modules/api/engine/stream/stream/listeners
stream.listeners() -> { OpenListener }
Every listener this engine currently holds an address for, in the
order they were opened. Each entry is what stream.listenerStatus
reports about it, plus the handle it is addressed by and the owner
chunk its life follows.
This is how an address is reached again once nothing holds its handle:
filter on address for the port you want and close the entry by its
handle, rather than guessing at handles.
Returns { OpenListener } — An array of open listeners.
for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end
typed/builtin//modules/api/engine/stream/stream/open
stream.open(url: string, opts: StreamOpenOpts?) -> PromiseHandle
Open a byte stream at url (scheme://target[?k=v]). loopback
carries written bytes back out of the same stream and works on
every platform; tcp dials host:port; tty opens a serial
device node — /dev/ttyACM0 or /dev/ttyUSB0 for a USB CDC board
such as an ESP32, /dev/rfcomm0 for a Bluetooth controller paired
over classic SPP (both present as a tty on Linux, so one transport
serves either peer), COM5 on Windows. tty query parameters:
baud (default 115200), dataBits (5-8, default 8), parity
(none | odd | even, default none), stopBits (1 or 2,
default 1).
ble 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:
ble://<device>?service=<uuid>&write=<uuid>¬ify=<uuid>. The
device is the name it advertises, * any device offering the
service, a trailing * a name prefix (Paw*). write is the
characteristic this engine writes to and notify 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 (ffe0),
32-bit, or full. Optional: chunk (bytes per packet, 1-512 —
otherwise what the connection carries), writeMode
(withResponse | withoutResponse, default withResponse),
timeout (seconds to find and connect to the device, default
15).
opts bounds the stream's undrained inbound buffer
and in-flight outbound bytes (default 65536 each). Returns a
promise handle: task.await() 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 ble stream resolves as soon as
it exists and reports the rest as state — watch
stream.status(handle).state go opening, permissionPending
while the browser asks the person at the machine to pick a device,
then open; writes made meanwhile are queued and go out when it
connects. Check stream.transports() first
to tell a mistyped scheme from one this build does not carry.
Parameters
urlstring— Stream URL — scheme://target[?k=v&k=v].optsStreamOpenOpts(optional) — Buffer capacities (optional).
Returns PromiseHandle — Promise handle for task.await().
local pending = stream.open("loopback://echo"); local handle = task.await(pending)
local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1¬ify=ffe1"))
typed/builtin//modules/api/engine/stream/stream/read
stream.read(handle: string, max: number?) -> string
Drain up to max buffered inbound bytes from the stream.
Parameters
handlestring— Stream handle from stream.open.maxnumber(optional) — Maximum bytes to drain (optional). Omit to drain everything buffered.
Returns string — Drained bytes, byte-safe. "" when none buffered or handle names no open stream.
local chunk = stream.read(handle)
typed/builtin//modules/api/engine/stream/stream/serialPorts
stream.serialPorts() -> SerialPorts
Every serial device this machine has, for picking the one to open.
ports is an array ordered by path. Each entry carries the path the
device is at (/dev/ttyACM0 on Linux, COM3 on Windows), the url
that opens it, the kind of bus it attaches by, and — for a USB
device — the vendorId, productId, serialNumber, manufacturer
and product it advertises.
A device's path moves with enumeration order: a board that came up at
/dev/ttyACM0 is at /dev/ttyACM1 once something else is plugged in
first, and moves across COM3-COM5 on Windows. What the device
advertises holds still across those moves, so match on
vendorId/productId — or on serialNumber to tell two of the same
board apart — and open the url that entry carries, appending the
port settings stream.open documents.
Three answers are distinct. supported false with a reason means
this platform has no serial bus to enumerate at all. error set means
it has one and the operating system refused this enumeration, so a
later call may answer. An empty ports with neither means the machine
has no serial device attached, which is an ordinary result.
Returns SerialPorts — { supported, reason, error, ports } — the platform's answer, this enumeration's, and the devices it found.
for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end
typed/builtin//modules/api/engine/stream/stream/status
stream.status(handle: string) -> StreamStatus?
Report what the stream has carried and lost. state is where
the stream is in its life: opening, permissionPending while the
platform asks the person at the machine to allow the connection,
open, denied when that permission was refused, and closed
when it is finished. pending is bytes
accepted and not yet handed to the peer; capacity is the value
pending may reach before a write is refused. error holds the
most recent transport failure and the refusal a denied stream
carries, retained for the life of the
stream. nil when handle names no open stream.
Parameters
handlestring
Returns StreamStatus? — Stream status, or nil when handle names no open stream.
local s = stream.status(handle); print(s.pending, s.capacity)
typed/builtin//modules/api/engine/stream/stream/streams
stream.streams() -> { OpenStream }
Every open stream, dialled or answered, in the order they were
opened. Each entry is what stream.status reports about it, plus the
handle it is addressed by and the owner chunk its life follows —
a connection stream.accept handed over carries the owner of the
listener that answered it.
Returns { OpenStream } — An array of open streams.
for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end
typed/builtin//modules/api/engine/stream/stream/transports
stream.transports() -> { [string]: TransportSupport }
Every stream scheme this build knows about — a capability
probe, in both directions. supported answers stream.open and
listen answers stream.listen, 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 (tcp and tty on wasm;
ble 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.
Returns { [string]: TransportSupport } — Map of scheme name to { supported, reason, listen, listenReason }.
local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end
typed/builtin//modules/api/engine/stream/stream/write
stream.write(handle: string, bytes: string) -> WriteOutcome
Queue bytes for the stream's peer. Never blocks. "accepted"
means the bytes were queued. "full" 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 pending against capacity on
stream.status() to see it coming before a write is refused.
"closed" means the stream is finished, or handle names no open
stream — reopen to continue, retrying never succeeds. "tooLarge"
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.
Parameters
handlestring— Stream handle from stream.open.bytesstring— Bytes to queue, byte-safe.
Returns WriteOutcome — "accepted" | "full" | "closed" | "tooLarge"
local outcome = stream.write(handle, data)
typed/builtin//modules/api/engine/streaming/streaming/cells
streaming.cells() -> { [string]: any }
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.
Returns { [string]: any } — { config, stats, sources, cells, proxies }.
local s = streaming.cells()
typed/builtin//modules/api/engine/streaming/streaming/levels
streaming.levels(scene: any?) -> { [string]: any }
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
standing = false: the chain is registered and there is nothing left for
it to draw.
Parameters
sceneany(optional) — The scene walk to read against. Omitted, the call takes its own.
Returns { [string]: any } — { count, receivers }.
local l = streaming.levels()
typed/builtin//modules/api/engine/streaming/streaming/observe
streaming.observe() -> { [string]: any }
The whole reading in one document: terrain, voxel, streaming cells and mesh LOD, plus the totals those rows sum to.
Built by this call and published as its last act, so
/zero/runtime/observations/streaming serves the same document rather
than a second derivation of it.
Returns { [string]: any } — { terrain, voxel, cells, levels, totals }.
local r = streaming.observe()
typed/builtin//modules/api/engine/streaming/streaming/reasons
streaming.reasons() -> { string }
Every reason whyNotDrawn can answer with, so a caller can enumerate
the set rather than meeting it one failure at a time.
Returns { string } — Sorted array of reason names.
local r = streaming.reasons()
typed/builtin//modules/api/engine/streaming/streaming/terrain
streaming.terrain(scene: any?) -> { [string]: any }
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.
Parameters
sceneany(optional) — The scene walk to read against. Omitted, the call takes its own, which is what makes a whole reading one walk rather than four.
Returns { [string]: any } — { count, entities } — one row per entity carrying a Terrain.
local t = streaming.terrain()
typed/builtin//modules/api/engine/streaming/streaming/voxel
streaming.voxel(scene: any?) -> { [string]: any }
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.
Parameters
sceneany(optional) — The scene walk to read against. Omitted, the call takes its own.
Returns { [string]: any } — { count, worlds } — one entry per entity carrying a VoxelWorld.
local v = streaming.voxel()
typed/builtin//modules/api/engine/streaming/streaming/whyNotDrawn
streaming.whyNotDrawn(subject: any?) -> { [string]: any }
Why a piece of a world's detail is not on screen, as one reason from
the closed set streaming.reasons() enumerates, with a detail line naming
what that reason is about.
The subject picks which system answers:
- an entity ref, id or name — whichever of the four systems holds it
{ entity = ..., chunk = "cx_cy_cz" }— one chunk of a voxel world{ entity = ..., level = n }— one level of a mesh-LOD chain{ cell = "x_z" }— one cell of the spatial-streaming store
Parameters
subjectany(optional) — The entity, chunk, level or cell to answer about.
Returns { [string]: any } — { kind, reason, detail }.
local w = streaming.whyNotDrawn({ entity = "Vox", chunk = "0_0_0" })
typed/builtin//modules/api/engine/stringx/stringx/scanNumbers
stringx.scanNumbers(s: string, pos: number?) -> ({ number }, number)
Read the run of numbers starting at pos — separated by commas and/or
whitespace — and report where the run ended.
The run stops at the first character that neither continues a number nor
separates two of them (], }, a quote, a letter), and nextPos 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 nextPos left ON
it rather than past it, so nothing is skipped without the caller seeing it.
Parameters
sstring— The text to read.posnumber(optional) — 1-based index to start at. Defaults to 1.
Returns ({ number }, number) — The numbers found, and the 1-based position just past them.
-- 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)
typed/builtin//modules/api/engine/subscriptions/subscriptions/cancel
subscriptions.cancel(id: string) -> boolean
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.
Parameters
idstring— Subscription id to cancel.
Returns boolean — True when a live subscription was disconnected.
subscriptions.cancel(conn.id)
typed/builtin//modules/api/engine/subscriptions/subscriptions/get
subscriptions.get(id: string) -> SubscriptionRow?
One subscription row by id, or nil when the id is unknown (never tracked, or evicted after its publisher was destroyed).
Parameters
idstring— Subscription id (conn.id, or a/zero/runtime/events/subscriptions/entry).
Returns SubscriptionRow? — The subscription row, or nil.
local row = subscriptions.get(conn.id); print(row and row.deliveries)
typed/builtin//modules/api/engine/subscriptions/subscriptions/list
subscriptions.list(filter: SubscriptionFilter?) -> { SubscriptionRow }
Every tracked subscription row, optionally filtered by publisher instance id, publisher entity id, event name, and/or connected state.
Parameters
filterSubscriptionFilter(optional) — Optional filter table.
Returns { SubscriptionRow } — Array of subscription rows.
for _, s in ipairs(subscriptions.list({ connected = true })) do print(s.id, s.event, s.deliveries) end
typed/builtin//modules/api/engine/subscriptions/subscriptions/publishers
subscriptions.publishers() -> { PublisherRow }
Every live event publisher: component instance, entity, and per-event fire stats (fires happen whether or not anyone subscribes) plus current subscriber ids.
Returns { PublisherRow } — Array of publisher rows.
for _, p in ipairs(subscriptions.publishers()) do print(p.component, p.entityName) end
typed/builtin//modules/api/engine/substrate/substrate/bindFrames
substrate.bindFrames(key: string, id: string, slots: { any }) -> (boolean, string?)
Bind the play key to the sequence id through slots, so
showFrame(key, f) places its elements from frame f. slots[s] is
{ kind = "instances", key, transforms, data?, capacity, draw, interval? }
for elements an instanced draw copies from transforms — the scatter
binding key places up to capacity of them there and the draw draw
draws the frame's count — or { kind = "entities", key, binding?, interval? } for elements standing in entities: the scatter binding key,
whose entities scatterTransforms has already set, places them, and the
frame's fields are written across the entity binding binding.
Parameters
keystring— The play's name.idstring— The sequence the play shows.slots{ any }— What each numbered slot places, for this play.
Returns (boolean, string?) — True when the binding was queued, and why it was refused when not.
substrate.bindFrames("spark#1", "spark", { { kind = "instances", key = "spark#1", transforms = "spark#1.xf", capacity = 64, draw = drawId } })
typed/builtin//modules/api/engine/substrate/substrate/bufferSource
substrate.bufferSource(name: string) -> string?
The chunk that created the GPU buffer name holds, as debug.info
names a chunk — a module's identity, a test suite's, an execute body's.
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.
Parameters
namestring— The name the buffer was created under.
Returns string? — The creating chunk's name, or nil.
if substrate.bufferSource("debug_viz.vtx") then print("made through the substrate") end
typed/builtin//modules/api/engine/substrate/substrate/clearScatterTransforms
substrate.clearScatterTransforms(key: string) -> boolean
Drop the device transform binding under key. Its entities go back to
being placed by whatever writes their Transform. Queued.
Parameters
keystring— The key the binding was made under.
Returns boolean — True when the drop was queued.
substrate.clearScatterTransforms("wave")
typed/builtin//modules/api/engine/substrate/substrate/createBuffer
substrate.createBuffer(opts: BufferOpts) -> TypedBuffer?
Allocate a typed buffer and return its handle.
A "gpu" buffer is storage a compute shader binds; usage adds
"vertex", "index", "indirect" or "readback" on top of the storage
it always has. A "cpu" buffer lives in the scripting heap and reads back
as a flat array of floats.
The handle's write 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. writeU32 and
writeBytes answer the same way, against the same extent.
A "cpu" buffer holds at most 1 GiB of storage: 268435456 f32 words,
16777216 mat4 records. A len past that raises an error naming the
bytes it needs and the limit, and a len within it whose storage the heap
cannot supply raises an error naming the bytes. Neither allocates
anything.
A "gpu" buffer holds at most what the device creates in one buffer, its
max_buffer_size. A len 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 name stays free.
class makes a "gpu" 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. "effect" is for runs made and let go of
as an effect plays, "persistent" for runs held until let go of. A run
binds, writes, reads and frees like any "gpu" buffer under the name its
handle carries, which the engine mints, so a run takes no name; a
compute program declared with ranges = true binds any number of runs of
one page through one bind group. A run carries "readback" usage and no
other: a vertex, index or indirect buffer is one of its own. owner names
whom a run is held for, and substrate.releaseOwner(owner) lets go of
every run created with it at once.
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;
substrate.listBuffers 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 len 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.
substrate.setOwner and substrate.releaseOwner answer in a job as they
do on the engine thread, counting the job's own changes at once.
Parameters
optsBufferOpts—{ type, len, kind?, usage?, name?, class?, owner? }—typeis"f32","vec3","vec4","quat"or"mat4";kindis"cpu"(the default) or"gpu".nameis the name a dispatch binds a"gpu"buffer by, and the namesubstrate.getBufferandsubstrate.destroyBufferreach it under.class("effect"or"persistent") makes a"gpu"buffer a run;ownernames whom the run is held for.
Returns TypedBuffer? — The buffer handle, or nil when the allocation failed — an unknown type or kind, a zero length, or a name that already holds a GPU buffer of another shape. A name holding a buffer of the SAME type and length hands that buffer back, contents and all; substrate.destroyBuffer frees a name whose buffer is the wrong shape.
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()
typed/builtin//modules/api/engine/substrate/substrate/defineFrames
substrate.defineFrames(id: string, frames: { any }) -> (boolean, string?)
Hold a sequence of frames on the engine under id, for plays bound to
it (bindFrames) to show by index (showFrame) with no script running
per frame. frames[f] is { scatters = {...}, fields = {...} }. A
scatter { slot, positions, rotations?, scales?, lanes?, identities?, count } names the buffers the slot's elements are placed from in that
frame and how many elements they hold; lanes[k] is a buffer name or
false. A field { slot, component, field, values } 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 id again replaces it.
Parameters
idstring— The sequence's name.frames{ any }— The frames, in order.
Returns (boolean, string?) — True when the sequence was queued, and why it was refused when not.
substrate.defineFrames("spark", { { scatters = { { slot = 1, positions = P:bufferName(), count = 64 } } } })
typed/builtin//modules/api/engine/substrate/substrate/destroyBuffer
substrate.destroyBuffer(name: string) -> boolean
Free the GPU buffer name denotes, whatever else still holds a handle
to it.
The allocation goes and the name is free to be created again at any type
and length; every handle that pointed at it answers :alive() 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.
Parameters
namestring— The name the buffer was created under.
Returns boolean — True when a GPU buffer under that name was freed.
substrate.destroyBuffer("env.town.xf")
typed/builtin//modules/api/engine/substrate/substrate/forgetFrames
substrate.forgetFrames(id: string) -> boolean
Drop the sequence held under id. A play still bound to it shows
nothing further.
Parameters
idstring— The sequence's name.
Returns boolean — True when the drop was queued.
substrate.forgetFrames("spark")
typed/builtin//modules/api/engine/substrate/substrate/getBuffer
substrate.getBuffer(name: string) -> TypedBuffer?
The GPU buffer name denotes, as a handle you now hold.
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 .module
that hot-reloaded, a build that ran in an earlier execute. The handle
carries everything createBuffer's does and releases its reference with
:destroy().
Parameters
namestring— The name the buffer was created under.
Returns TypedBuffer? — The buffer handle, or nil when no GPU buffer holds that name.
local xf = substrate.getBuffer("env.town.xf")
local shape = xf and { xf:type(), xf:length() }
typed/builtin//modules/api/engine/substrate/substrate/gpuReadback
substrate.gpuReadback(key: string?) -> Readback?
Wrap the key an FFI read handed back as the Readback 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.
Parameters
keystring(optional) — The key the read returned.
Returns Readback? — The Readback, or nil when the read did not start.
local pending = substrate.gpuReadback(compute.readTexture3D(handle))
typed/builtin//modules/api/engine/substrate/substrate/listBuffers
substrate.listBuffers() -> { NamedBuffer }
Every named GPU buffer the engine holds, in name order.
Each record states id, name, type ("F32", "Vec3", "Vec4",
"Quat", "Mat4"), len in records, refs — how many holders it
has — holder, the owner a run is held for when one was named
(createBuffer's owner), class, the lifetime class of a run
("effect" or "persistent"; absent for a buffer of its own), and
bytes, 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.
Returns { NamedBuffer } — Array of { id, name, type, len, refs, holder?, class?, bytes }.
for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end
typed/builtin//modules/api/engine/substrate/substrate/losses
substrate.losses() -> number
How many times a buffer has been freed by name or by owner: destroyBuffer, releaseOwner 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.
Returns number — The count, which only grows.
local at = substrate.losses()
typed/builtin//modules/api/engine/substrate/substrate/markBaseline
substrate.markBaseline() -> number
Record which named GPU buffers the engine holds right now, and make that
the point substrate.resetToBaseline returns to.
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.
Returns number — The mark's generation, counting from 1 — what substrate.resetToBaseline takes to restore to this mark.
local mark = substrate.markBaseline() -- then run, then substrate.resetToBaseline(mark)
typed/builtin//modules/api/engine/substrate/substrate/releaseBaseline
substrate.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
generationnumber— The mark to drop, assubstrate.markBaselinereturned it.
Returns boolean — True when the mark stood.
substrate.releaseBaseline(mark) -- the run is over
typed/builtin//modules/api/engine/substrate/substrate/releaseOwner
substrate.releaseOwner(owner: string) -> number
Let go of every run created with owner (createBuffer's class and
owner), whatever else still holds a handle to it.
Each run's range goes back to its arena and every handle to it answers
:alive() 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.
Parameters
ownerstring— The owner the runs were created with.
Returns number — How many runs were let go of.
substrate.releaseOwner("burst:" .. id)
typed/builtin//modules/api/engine/substrate/substrate/resetToBaseline
substrate.resetToBaseline(generation: number?) -> number
Free every named GPU buffer created since substrate.markBaseline,
leaving the ones it recorded where they are.
A module that created one of these holds its own handle to it. Put that
module's state back alongside this (engine.restoreModuleState) or it
goes on addressing storage this freed.
is touched.
Parameters
generationnumber(optional) — The mark to restore to, assubstrate.markBaselinereturned it; the newest mark when omitted. The marks taken after it go with the reset.
Returns number — How many buffers were freed.
local freed = substrate.resetToBaseline(mark)
typed/builtin//modules/api/engine/substrate/substrate/scatterTransforms
substrate.scatterTransforms(key: string, entityIds: { string }?, positions: string, rotations: string?, scales: string?, lanes: { string? }?, interval: number?, identities: string?) -> boolean
Place entities from the buffers that already hold their transform, on
the device, instead of from a Transform written per entity.
entityIds[i] is placed from row i of every named buffer, so the order
is the buffers' own. Pass nil for entityIds 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.
Re-binding the same key 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.
Parameters
keystring— Stable identity of the binding.entityIds{ string }(optional) — The entities in buffer order, or nil to rebind the buffers alone.positionsstring— Name of a buffer holding three f32 per element.rotationsstring(optional) — Name of a buffer holding four f32 per element.scalesstring(optional) — Name of a buffer holding three f32 per element.lanes{ string? }(optional) — Per-instance shader data, by lane:lanes[k + 1]names a buffer holding one vec4 per element, written into lanekof each element's block — what its material reads back asinput.shader_data[k]. A lane left out is left to whatever else writes it.intervalnumber(optional) — 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.identitiesstring(optional) — 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.
Returns boolean — True when the binding was queued.
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())
typed/builtin//modules/api/engine/substrate/substrate/scatterTransformsInto
substrate.scatterTransformsInto(key: string, into: { transforms: string, data: string?, count: number }, positions: string, rotations: string?, scales: string?, lanes: { string? }?, interval: number?, identities: string?) -> boolean
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 into.transforms, and lane k of block i of into.data — the
two buffers an instanced draw (renderer.mesh.drawInstanced with
transformBuffer and instanceDataBuffer) copies into its slots every
frame. It is scatterTransforms for a population: the same columns, the
same blend between presents when interval is given, the same
identities. Binding the same key 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.
Parameters
keystring— Stable identity of the binding.into{ transforms: string, data: string?, count: number }—{ transforms, data?, count }: a buffer ofcountcolumn-major mat4, a buffer ofcount64-byte blocks of four vec4 lanes when lanes are bound, and how many elements there are.positionsstring— Name of a buffer holding three f32 per element.rotationsstring(optional) — Name of a buffer holding four f32 per element.scalesstring(optional) — Name of a buffer holding three f32 per element.lanes{ string? }(optional) —lanes[k + 1]names a buffer holding one vec4 per element, written into lanekof each element's block ininto.data.intervalnumber(optional) — Seconds between the domain's presents; given, each element is drawn between the last two presents.identitiesstring(optional) — 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.
Returns boolean — True when the binding was queued.
substrate.scatterTransformsInto("sparks", { transforms = "sparks.xf", count = 4096 }, P:bufferName())
typed/builtin//modules/api/engine/substrate/substrate/scatterWorlds
substrate.scatterWorlds(key: string) -> any
Read back where the device drew the elements of the transform binding
under key 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.
Parameters
keystring— The key the binding was made under.
Returns any — A pending readback: :ready(), then :result() for the floats.
local pending = substrate.scatterWorlds("sparks")
while not pending:ready() do task.wait() end
local x = pending:result()[13]
typed/builtin//modules/api/engine/substrate/substrate/setOwner
substrate.setOwner(name: string, owner: string?) -> boolean
Hold the run name names for owner from now on, in place of the
owner it was created with, or for no one when owner is nil.
substrate.releaseOwner(owner) then lets go of it with the rest of what
owner 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.
Parameters
namestring— The name the run binds under (its handle'sname).ownerstring(optional) — The owner to hold it for, or nil.
Returns boolean — Whether name names a run (a buffer created with a class).
substrate.setOwner(column.name, "cache#3")
typed/builtin//modules/api/engine/substrate/substrate/showFrame
substrate.showFrame(key: string, frame: number) -> boolean
Show frame frame (from 1) of the sequence the play key 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.
Parameters
keystring— The play's name.framenumber— The frame, from 1.
Returns boolean — True when the frame was queued.
substrate.showFrame("spark#1", 12)
typed/builtin//modules/api/engine/substrate/substrate/unbindFrames
substrate.unbindFrames(key: string) -> boolean
Drop the play key's binding to its sequence. What it placed stays as
the last frame shown left it.
Parameters
keystring— The play's name.
Returns boolean — True when the drop was queued.
substrate.unbindFrames("spark#1")
typed/builtin//modules/api/engine/text/text/alive
text.alive(handle: any?) -> boolean
Whether the text system still holds this handle — true between
text.create and the text.destroy that released it.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns boolean — True while the handle is live.
if not text.alive(h) then h = text.create({ content = "again" }) end
typed/builtin//modules/api/engine/text/text/count
text.count() -> number
How many text objects the text system is holding — the number that
moves when text.create and text.destroy are called.
Returns number — The live text-object count.
local before = text.count()
typed/builtin//modules/api/engine/text/text/create
text.create(options: table) -> any
Create a text handle from an initial content + style table. The handle
owns a runtime GPU texture (see text.textureGuid); pass it to every other
call.
Parameters
optionstable— Table ofcontentplus style fields (fontSize, color, alignment, richText, maxWidth, ...).
Returns any — An opaque text handle, or nil if the text system is unavailable.
local h = text.create({ content = "Hello", fontSize = 48 })
typed/builtin//modules/api/engine/text/text/destroy
text.destroy(handle: any?) -> boolean
Destroy a text handle and release its raster + glyph layout.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns boolean — True when the text system held the handle and released it; false for a handle it did not have.
text.destroy(h)
typed/builtin//modules/api/engine/text/text/face
text.face(handle: any?) -> any
Which font face one handle actually shaped with, and whether that is
the family its style selects. requested is the name the style wrote,
selected 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 — resolved is the face that answered, matched
says whether the face is the family the name selected and reason
says why when it is not — one of text.faceReasons(). A style that named
no family reports noFamilyRequested: it got the default because it asked
for nothing, so reason rather than matched is what an alert switches
on. faces 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.
missing 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 (font.onDemand()).
Parameters
handleany(optional) — Text handle fromtext.create.
Returns any — { requested, selected, resolved, postScriptName, matched, reason, faces, glyphCount, missing }, or nil for a handle the text system does not hold.
local r = text.face(h).reason; if r == "familyUnknown" or r == "familyNotSelectable" then print(r) end
typed/builtin//modules/api/engine/text/text/faceReasons
text.faceReasons() -> { string }
Every reason the face readings give for a label or a family not being
in the family a style named, nearest cause first. text.face gives them
for one label; font.reconcile() also gives familyCoveredNoGlyph, which
it can only reach by laying the family out under its own weights and over
several scripts.
Returns { string } — Array of reason strings.
for _, r in ipairs(text.faceReasons()) do print(r) end
typed/builtin//modules/api/engine/text/text/isDirty
text.isDirty(handle: any?) -> boolean
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.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns boolean — True when the layout changed since the last text.rasterize.
if text.isDirty(h) then text.rasterize(h, text.textureGuid(h)) end
typed/builtin//modules/api/engine/text/text/listFonts
text.listFonts() -> { string }
List the font families currently available to the text system.
Returns { string } — Array of font-family name strings.
local fonts = text.listFonts()
typed/builtin//modules/api/engine/text/text/loadFont
text.loadFont(ref: any?) -> any
Load a font from an asset reference so it becomes available to
setStyle's fontFamily.
Parameters
refany(optional) — Font asset reference or path.
Returns any — The loaded font-family name, or nil on failure.
text.loadFont(asset.ref("fonts.inter", "font"))
typed/builtin//modules/api/engine/text/text/measure
text.measure(handle: any?) -> any
Measure the rasterised text in pixels without producing a texture.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns any — Table with width and height in pixels.
local size = text.measure(h)
typed/builtin//modules/api/engine/text/text/observe
text.observe() -> any
Everything the text system is holding right now. count is the live
text objects; objects is one row each, carrying its content, the style
it was laid out with, its measured extent, whether it is dirty, the face
the shaper actually used, the owner entity whose component created it
with whether that entity is still there, and the raster texture its last
rasterisation landed in with the bytes it costs. orphans is the subset
whose owning entity is gone, fonts the families the shaper can resolve,
and raster 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.
Returns any — { count, objects, orphans, fonts, dirty, raster }.
local live = text.observe().count
typed/builtin//modules/api/engine/text/text/orphans
text.orphans() -> { any }
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 text.observe().objects carries.
Returns { any } — Array of text-object rows with a dead owner.
print(#text.orphans() .. " labels outlived their entity")
typed/builtin//modules/api/engine/text/text/rasterMemory
text.rasterMemory() -> any
The glyph-raster bytes, broken out of the runtime GPU texture pool.
bytes is summed off the same map renderer.gpuMemory().textures is
totalled from, so shareOfPool is a share of that number rather than a
second count of the same memory.
Returns any — { pool, bytes, textures, poolBytes, shareOfPool }.
local r = text.rasterMemory(); print(r.bytes .. " of " .. r.poolBytes)
typed/builtin//modules/api/engine/text/text/rasterize
text.rasterize(handle: any?, texture: any?, scale: number?) -> any
Rasterise the handle's current text + style into the given runtime GPU
texture. Bind that texture's guid as a material's base_color_texture to
display the text; re-rasterising the same texture overwrites it in place.
Parameters
handleany(optional) — Text handle fromtext.create.textureany(optional) — Destination GPU texture handle (renderer.texture.create) or its guid string — WHERE the raster lands.scalenumber(optional) — World/pixel scale factor for the raster (default 1.0).
Returns any — Table with width and height (in pixels), or nil if nothing rasterised.
local tex = renderer.texture.create({ width = 256, height = 64 })
local r = text.rasterize(h, tex, 1.0)
typed/builtin//modules/api/engine/text/text/setStyle
text.setStyle(handle: any?, style: table) -> boolean
Replace the handle's style. Fields not present keep their current value.
Parameters
handleany(optional) — Text handle fromtext.create.styletable— Style table (fontSize, color, alignment, outline, ...).
Returns boolean — True when the text system held the handle and took the style; false when it did not.
text.setStyle(h, { fontSize = 64, color = "yellow" })
typed/builtin//modules/api/engine/text/text/setText
text.setText(handle: any?, content: string) -> boolean
Replace the handle's text content.
Parameters
handleany(optional) — Text handle fromtext.create.contentstring— New text string.
Returns boolean — True when the text system held the handle and took the content; false when it did not, which is how a caller learns its handle went away.
if not text.setText(h, "HP: 100") then h = text.create({ content = "HP: 100" }) end
typed/builtin//modules/api/engine/text/text/textureGuid
text.textureGuid(handle: any?) -> string?
The runtime GPU texture guid this handle rasterises into — bind it as a
material texture (base_color_texture) to display the text.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns string? — The texture guid string, or nil for a handle the text system does not hold.
entity(id).component.get("Material"):setTexture("base_color_texture", text.textureGuid(h))
typed/builtin//modules/api/engine/ui/ui/blur
ui.blur()
Surrender keyboard focus from whichever widget currently holds it, in the editor's UI and the game's alike.
typed/builtin//modules/api/engine/ui/ui/bringAreaToFront
ui.bringAreaToFront(id: string)
Raise a movable area 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 layer band raises
it over the bands below.
Parameters
idstring— Area widget id.
typed/builtin//modules/api/engine/ui/ui/captureWindow
ui.captureWindow(screen: string, window: string, opts: CaptureOpts?) -> CaptureResult?
Render a single Window widget to its own offscreen texture
and write the result as PNG at
/runtime/render_surfaces/<rtHandle>.png. The screen does NOT
need to be visible. Returns { rtHandle, texturePath } or nil
on invalid inputs (width/height clamped to [1, 8192],
defaults 600x400).
Parameters
screenstring— Screen id containing the target Window.windowstring— Widget id of the Window.optsCaptureOpts(optional) —{ width, height }(optional).
Returns CaptureResult? — { rtHandle, texturePath } or nil.
typed/builtin//modules/api/engine/ui/ui/click
ui.click(callbackId: string, value: any?) -> boolean
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 onClick / onChange / other on* prop
names, or its element id where it leaves that prop unnamed (a button
clicked without an onClick, a slider changed without an onChange).
Given the element id of a widget whose interactions emit other ids, it
raises an error naming those ids, and clicks nothing.
Parameters
callbackIdstring— Callback id the widget emits (itsonClick/onChange/on*prop).valueany(optional) — Optional value the callback receives asdata.value: a number, a string, a boolean, or a table with the fields a structured interaction delivers (a code editor's{ token, line, col }, a canvas drag's deltas).
Returns boolean — true when a widget on a registered screen emits the id; false when none does, and the click then still reaches every ui.registerCallbackEnv env and every component and scene entrypoint that receives callbacks no screen claims.
typed/builtin//modules/api/engine/ui/ui/copyText
ui.copyText(text: string)
Put text 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.
Parameters
textstring— The text to copy.
ui.copyText("/zero/source/scenes/main.scene")
typed/builtin//modules/api/engine/ui/ui/defineStyle
ui.defineStyle(name: string, style: StyleProps)
Define a named style. Style keys follow
<widgetType>.<className> (e.g. "label.h1", "button.primary")
or bare <className> to apply across widget types. Widgets
reference styles via the classes (or class) prop.
Parameters
namestring— Style name.styleStyleProps— Style properties table.
typed/builtin//modules/api/engine/ui/ui/defineStyles
ui.defineStyles(styles: { [string]: StyleProps })
Define multiple named styles at once.
Parameters
styles{ [string]: StyleProps }— Map of style name to style properties.
typed/builtin//modules/api/engine/ui/ui/defineWidget
ui.defineWidget(name: string, builderFn: (WidgetTree, { WidgetTree }) -> WidgetTree)
Register a custom widget kind. When a tree contains
{ type = name, props = ..., children = ... }, the decoder
calls builderFn(props, children) at register / update time and
substitutes the returned widget table in place. Errors surface
through ui.lastValidation() with codes widget-builder-error
/ widget-builder-bad-return / decode-recursion-depth-exceeded.
Parameters
namestring— Custom widget kind name.builderFn(WidgetTree, { WidgetTree }) -> WidgetTree— Builder closure(props, children) -> widgetTable.
typed/builtin//modules/api/engine/ui/ui/diagnose
ui.diagnose(widgetId: string) -> WidgetPaint?
Why one widget did or did not reach the last frame. Returns that
widget's row from ui.observe() — the same fields, resolved against the
same reading. An id no registered screen carries reads noSuchWidget,
which is how a misspelling separates from a widget whose screen is
hidden and from one the frame laid out no box for.
Parameters
widgetIdstring— The id the widget records layout under.
Returns WidgetPaint? — The widget's row, or nil before the UI has published a frame.
local row = ui.diagnose("hud-healthbar")
typed/builtin//modules/api/engine/ui/ui/displayScale
ui.displayScale() -> DisplayScale
The physical scale the UI is sized at. density 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. textScale 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). touchTarget is the smallest target, in points, a touch
reaches an interactive widget within: 48 on a handheld display, 0 on a
desktop. pixelRatio is ui.pixelRatio() of the same frame.
ui.simulateDisplayScale previews a phone's on any engine.
Returns DisplayScale — { density?, textScale, touchTarget, pixelRatio }.
local scale = ui.displayScale()
local buttonHeight = math.max(32, scale.touchTarget)
typed/builtin//modules/api/engine/ui/ui/dragState
ui.dragState() -> { payload: string, x: number, y: number, label: string?, ghost: boolean }?
The in-flight drag-and-drop payload while a dragPayload widget is
being dragged, else nil. x/y are the pointer's position in the
logical space ui.getLayoutInfo rects live in, so the reading resolves
directly against widget rects. label is the text the drag ghost beside
the pointer reads (dragLabel, else the source's text; nil for an
outline ghost), and ghost whether the engine draws one (dragGhost) —
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 onDrop. Snapshotted each frame.
Returns { payload: string, x: number, y: number, label: string?, ghost: boolean }? — { payload, x, y, label?, ghost } during a drag, nil otherwise.
typed/builtin//modules/api/engine/ui/ui/elementTree
ui.elementTree(screenName: string) -> ElementNode?
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 id set on the node when the author gave it one, otherwise
<screen>/<type>@<path>. bounds is that element's rect — the same
table ui.getLayoutInfo(id) returns — and appears once the element has
been measured. Kinds registered through ui.defineWidget appear
expanded into the primitives they build. Feeds the gui.captureElement
tool: list the tree, pick the ids to frame, capture their region.
Parameters
screenNamestring— Screen id passed toui.registerScreen.
Returns ElementNode? — An ElementNode tree, or nil when no screen is registered under that name.
typed/builtin//modules/api/engine/ui/ui/focus
ui.focus(widgetId: string)
Programmatically request keyboard focus on a widget. Queued
as a one-shot; the next render of the matching widget calls
response.request_focus().
Parameters
widgetIdstring— Widget id to focus.
typed/builtin//modules/api/engine/ui/ui/focusedWidget
ui.focusedWidget() -> (string?, string?)
Return the widget id of whichever widget currently holds
keyboard focus, or nil, and the space it draws in: "window" for the
editor's UI around the game view, "camera" for the game's own UI in
the game view. Snapshotted post-render each frame.
Returns (string?, string?) — Focused widget id or nil, and its space.
local id, space = ui.focusedWidget()
typed/builtin//modules/api/engine/ui/ui/getAreaPos
ui.getAreaPos(id: string) -> AreaPos?
Read the current pivot position of an area widget,
including any user drag deltas. Returns { x, y } or nil if
the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaPos? — { x, y } or nil.
typed/builtin//modules/api/engine/ui/ui/getAreaSize
ui.getAreaSize(id: string) -> AreaSize?
Read the measured size of an area widget, including any user
resize-grip drags if the area is resizable. Returns { w, h }
or nil if the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaSize? — { w, h } or nil.
typed/builtin//modules/api/engine/ui/ui/getDockLayout
ui.getDockLayout(id: string) -> string?
Read the current serialized layout (split/tab arrangement) of
a dockArea 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 layout prop to restore the arrangement.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string, or nil.
typed/builtin//modules/api/engine/ui/ui/getLayoutInfo
ui.getLayoutInfo(widgetId: string?) -> LayoutInfo?
Get layout info (position, size, content bounds) for UI
containers. If widgetId is given, returns info for that
widget only; otherwise returns all.
Parameters
widgetIdstring(optional) — Optional widget id to query.
Returns LayoutInfo? — Layout info table or nil.
typed/builtin//modules/api/engine/ui/ui/getPersistentDockLayout
ui.getPersistentDockLayout(id: string) -> string?
Read a dockArea's arrangement as a JSON string with every tab
whose dockPanel is transient left out — the form to save and
restore across sessions. Returns nil if the dockArea didn't render
this frame.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string without transient tabs, or nil.
typed/builtin//modules/api/engine/ui/ui/getScreenTree
ui.getScreenTree(screenName: string) -> WidgetTree?
Return the last widget tree table passed to
registerScreen / updateScreen for screenName.
Parameters
screenNamestring— Screen name to query.
Returns WidgetTree? — Widget tree or nil.
typed/builtin//modules/api/engine/ui/ui/getTheme
ui.getTheme() -> string
Get the name of the currently active theme.
Returns string — Active theme name.
typed/builtin//modules/api/engine/ui/ui/getToken
ui.getToken(name: string) -> string?
Look up a single design token value from the active theme.
Parameters
namestring— Token name (without$prefix).
Returns string? — Token value or nil.
typed/builtin//modules/api/engine/ui/ui/getTokens
ui.getTokens() -> { [string]: string }
Get all design tokens from the active theme as a key-value map.
Returns { [string]: string } — Token map.
typed/builtin//modules/api/engine/ui/ui/getWidgetProps
ui.getWidgetProps(typeName: string) -> { WidgetPropDescriptor }?
Get the property definitions for a widget type.
Parameters
typeNamestring— Widget type name.
Returns { WidgetPropDescriptor }? — Array of property descriptors, or nil if type not found.
typed/builtin//modules/api/engine/ui/ui/getWidgetTypes
ui.getWidgetTypes() -> { string }
Get all available widget type names that can be used in widget trees.
Returns { string } — Array of widget type names.
typed/builtin//modules/api/engine/ui/ui/hideScreen
ui.hideScreen(name: string) -> boolean
Hide a registered screen, and report whether a screen by that name
is registered. The engine applies the hide later in the frame;
listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to hide.
Returns boolean — True when a screen by this name is registered.
typed/builtin//modules/api/engine/ui/ui/hitTest
ui.hitTest(x: number, y: number) -> PaintHitTest?
Which widget a pointer at (x, y) reaches, and the stack beneath it.
Coordinates are logical points on the window, the space getLayoutInfo
and gui.bounds 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 pointerEvents = "none", is passed over: the
answer is the widget beneath it.
Parameters
xnumber— Logical X.ynumber— Logical Y.
Returns PaintHitTest? — { widget, screen, stack, x, y } — widget nil when the point is over no UI — or nil before the UI has published a frame.
local hit = ui.hitTest(640, 360)
typed/builtin//modules/api/engine/ui/ui/idCollisions
ui.idCollisions() -> { [string]: { string } }
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 ui.getLayoutInfo and the gui element tools
answering for whichever painted last, and the gui tools refuse such
an id. Empty while every id is unique.
Returns { [string]: { string } } — Colliding id -> the screens that recorded it.
for id, screens in ui.idCollisions() do print(id, table.concat(screens, ", ")) end
typed/builtin//modules/api/engine/ui/ui/invisibilityReasons
ui.invisibilityReasons() -> { string }
Every verdict ui.diagnose can report, as a closed list.
Returns { string } — The reason names.
typed/builtin//modules/api/engine/ui/ui/lastRegistration
ui.lastRegistration() -> { name: string, layer: number? }?
The name and layer passed to the most recent ui.registerScreen
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 ui table.
Returns { name: string, layer: number? }? — { name, layer } for the last registration, or nil if none yet.
typed/builtin//modules/api/engine/ui/ui/lastValidation
ui.lastValidation(screenName: string?) -> any
Validation diagnostics produced at the most recent
registerScreen / updateScreen, plus what the render stage
found while painting — unknown-font-family reports a
style.fontFamily that named no registered font family, once
per family per screen, wrap-label-no-width reports a
label with props.wrap whose box came out narrower than its
own longest word, once per label per screen, and
svg-not-as-declared reports an svg widget whose document
declared something the picture does not hold — a <text>
whose font-family selected no registered family, a <text>
the picture holds no glyph of, or a shape whose url(#id)
paint resolved to nothing — once per finding per widget per
screen. With no args
returns a
{ [screen] = entry } map; with a name returns that screen's
entry or nil. Validation gated by world setting
ui.validation = "off" | "warn" | "strict" (default "warn").
Parameters
screenNamestring(optional) — Optional screen name.
Returns any — Validation entry, full map, or nil.
typed/builtin//modules/api/engine/ui/ui/listFonts
ui.listFonts() -> { FontFamilyInfo }
Every font family a style.fontFamily 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.
family and every name in aliases are accepted as a
fontFamily, case-insensitively; aliases carries the
web-font names, CSS generic families and face names that
select the same group. faces names the concrete face in each
weight/style slot, so a fontWeight = 700 against a family
with no bold face gets a synthesised heavy. more 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 (font.onDemand() lists them all). system = true
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
system = false. A fontFamily naming nothing in this list
is reported as an unknown-font-family warning through
ui.lastValidation(screen) once the screen paints, and the
text renders in the default proportional face.
Returns { FontFamilyInfo } — Array of { family, aliases, faces, more, system }, by family.
for _, f in ui.listFonts() do print(f.family) end
typed/builtin//modules/api/engine/ui/ui/listScreens
ui.listScreens() -> { ScreenSummary }
List every registered screen with its current visibility,
z-order layer, renderLayer (the render-layer membership mask it
draws under: the mask setScreenRenderLayer stated, else the one the
surface setScreenSpace named implies, else the ui bit),
editorChrome (whether the mask setScreenRenderLayer stated carries
the EditorUI 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
("window" for the editor's UI, "camera" for the game's),
including the register / show / hide / unregister calls the running
script has already made. Sorted by layer ascending, then name.
Returns { ScreenSummary } — Array of screen summaries.
typed/builtin//modules/api/engine/ui/ui/listThemes
ui.listThemes() -> { string }
List all registered theme names.
Returns { string } — Array of theme names.
typed/builtin//modules/api/engine/ui/ui/lockOrientation
ui.lockOrientation(lock: OrientationLock)
Hold the display to the orientations lock allows: "landscape"
(both landscape directions, so turning the device over turns the app
with it), "portrait" (both portrait directions) or "any" (the display
follows the device). A world states the orientation it plays in as
world.orientation, 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. ui.orientation() reports the lock and whether the platform
holds it from the next frame. Any other value is refused by name.
Parameters
lockOrientationLock—"any","landscape"or"portrait".
ui.lockOrientation("portrait")
ui.lockOrientation("any")
typed/builtin//modules/api/engine/ui/ui/missingGlyphs
ui.missingGlyphs() -> { string }
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 (font.onDemand()) requests that face the frame
it is met, and leaves this list once the face is installed and the next
frame draws it.
Returns { string } — Sorted array of single-character strings; empty when every character drawn had a glyph.
if #ui.missingGlyphs() > 0 then print(table.concat(ui.missingGlyphs(), " ")) end
typed/builtin//modules/api/engine/ui/ui/observe
ui.observe(screenName: string?) -> PaintObservation?
What the last UI frame painted. Returns
{ generation, viewport, pointer, pointerOverUi, pointerWidget, widgets }
with one widgets 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 (visible), the order it painted in (paintIndex),
and its reason from the closed set ui.invisibilityReasons() lists.
Every rect, the reading's viewport (the window) and pointer are in
logical points on the window; a row's viewport 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 offViewport is judged against it.
generation advances once per re-rendered frame, so two calls reporting
the same number describe the same frame.
Parameters
screenNamestring(optional) — Narrow the rows to one screen. Omit for every screen.
Returns PaintObservation? — The reading; nil before the UI has published a frame, and nil for a screenName no registered screen answers to.
local reading = ui.observe("hud")
typed/builtin//modules/api/engine/ui/ui/onOrientationChange
ui.onOrientationChange(callback: (reading: OrientationReading) -> ()) -> () -> ()
Call callback with the new reading every time ui.orientation()
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.
Parameters
callback(reading: OrientationReading) -> ()— Receives the reading, asui.orientation()returns it.
Returns () -> () — A function that removes this listener.
local off = ui.onOrientationChange(function(o) print(o.current, o.rotation) end)
typed/builtin//modules/api/engine/ui/ui/orientation
ui.orientation() -> OrientationReading
The orientation the display presents the app in, and the lock it is
held to. current is "landscape" (wider than tall) or "portrait":
the shape of the window the app presents in. lock is what
ui.lockOrientation last asked, "any" when the display follows the
device. enforced 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; reason names why when it does not. rotation 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.
revision advances every time any of these changes; ui.onOrientationChange
calls back on each change.
Returns OrientationReading — { current, lock, enforced, reason?, rotation?, revision }.
if ui.orientation().current == "portrait" then print("lay the HUD out for a tall screen") end
typed/builtin//modules/api/engine/ui/ui/paintOrder
ui.paintOrder(a: string, b: string) -> number?
Which of two widgets paints later: -1 when a paints before b,
1 when after, 0 when level. This is what separates two widgets whose
rects are identical.
Parameters
astring— First widget id.bstring— Second widget id.
Returns number? — -1, 0, 1, or nil when the reading holds no row for one of them.
local order = ui.paintOrder("panel-a", "panel-b")
typed/builtin//modules/api/engine/ui/ui/pixelRatio
ui.pixelRatio() -> number
Physical pixels per logical point — the factor between the logical
space ui.screenSize() / getLayoutInfo rects live in and the physical
space input.mousePosition, the camera viewport rect and
input.simulateMouse* coordinates live in. Multiply a layout coordinate
by this to aim a simulated pointer at a widget.
Returns number — Physical pixels per logical point (1.0 when unscaled).
typed/builtin//modules/api/engine/ui/ui/pointerWidget
ui.pointerWidget() -> PointerRead?
Whether the UI is consuming the pointer, and which widget holds it —
the pointer counterpart of ui.focusedWidget(). x, y are in logical
points on the window, the space getLayoutInfo rects are in.
Returns PointerRead? — { x, y, overUi, widget, screen }, or nil before the UI has published a frame.
typed/builtin//modules/api/engine/ui/ui/registerBackgroundShader
ui.registerBackgroundShader(shaderHandle: any?, width: number?, height: number?)
Register a screen-domain .shader as a UI background, drawn
via the backgroundShader style. Takes the shader's asset handle
from asset.resolve. A shader whose compile failed
(shaderHandle:compileStatus() reads failed), 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
compileStatus().error reads. A shader whose compileStatus()
still reads pending raises too: register it once it reads
compiled.
Parameters
shaderHandleany(optional) — The screen.shader's asset handle, fromasset.resolve.widthnumber(optional) — Render target width (default 1280).heightnumber(optional) — Render target height (default 720).
typed/builtin//modules/api/engine/ui/ui/registerCallbackEnv
ui.registerCallbackEnv(key: string, env: { [string]: any })
Register an environment table to receive widget-callback
broadcasts: its global onCallback(id, value) fires for any widget
callback not owned by a specific component instance — the same
broadcast a component's onCallback receives. Keyed by key;
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
ui.unregisterCallbackEnv(key) so the ref is released.
Parameters
keystring— Stable identifier for this registration (re-register replaces).env{ [string]: any }— Environment table whoseonCallbackreceives the broadcasts.
typed/builtin//modules/api/engine/ui/ui/registerScreen
ui.registerScreen(name: string, widgetTree: WidgetTree, layer: number?)
Register a named UI screen with a widget tree. Optional
layer 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 area or window root sits over ordinary content, and
a modal root sits over the whole stack. Tag-based
grouping lives in Z.tags (Z.tags.set(name, { "editor" })
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
onCallback 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. topics/ui has the rule.
Parameters
namestring— Unique screen identifier.widgetTreeWidgetTree— Root widget table.layernumber(optional) — Z-order layer (optional).
ui.registerScreen("hud", tree)
typed/builtin//modules/api/engine/ui/ui/registerTheme
ui.registerTheme(name: string, theme: ThemeDefinition)
Register a theme from a flat Luau table. Most callers
should use Z.theme.register(name, table) which runs the
cascade for them.
Parameters
namestring— Theme name to register.themeThemeDefinition— Flat-resolved theme table.
typed/builtin//modules/api/engine/ui/ui/removeScreen
ui.removeScreen(name: string) -> boolean
Alias for ui.unregisterScreen.
Parameters
namestring— Screen identifier to remove.
Returns boolean — True when a screen by this name was registered.
typed/builtin//modules/api/engine/ui/ui/resetAreaSize
ui.resetAreaSize(id: string)
Clear a resizable area's remembered size (from a grip drag or
ui.setAreaSize) so its declared — or content — size takes over again.
Parameters
idstring— Area widget id.
typed/builtin//modules/api/engine/ui/ui/response
ui.response(widgetId: string) -> WidgetResponse?
Per-widget interaction snapshot for the most recent frame.
Returns { clicked, hovered, focused, changed, value } where
clicked / changed mark transitions and hovered / focused
mark current state.
Parameters
widgetIdstring— The widget id (NOT the onClick / onChange callback id).
Returns WidgetResponse? — WidgetResponse or nil.
typed/builtin//modules/api/engine/ui/ui/safeArea
ui.safeArea() -> SafeArea
The part of the screen no system bar, display cutout or on-screen
keyboard covers, in the logical points ui.screenSize() reports.
top / right / bottom / left are how far in from each edge the
covered band reaches; x / y / width / height are the rect they
leave; cutouts lists each display cutout (a camera hole or a notch) as
{ x, y, width, height }. Screens lay out inside this rect, so anchors,
top-level % sizes and a window's default position already keep clear
of it; read it to place something by absolute pos, or to run a
background under the bars while its controls stay inside. revision
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;
ui.simulateSafeArea lays the UI out as a phone would on any engine.
Returns SafeArea — { top, right, bottom, left, x, y, width, height, cutouts, revision } in logical UI points.
local safe = ui.safeArea()
ui.setAreaPos("hud", safe.x + 8, safe.y + 8)
typed/builtin//modules/api/engine/ui/ui/screen
ui.screen(name: string) -> { [string]: any }?
Get a screen proxy with methods like setResolution and
rasterize.
Parameters
namestring— Screen name.
Returns { [string]: any }? — Screen proxy table, or nil.
typed/builtin//modules/api/engine/ui/ui/screenSize
ui.screenSize() -> { width: number, height: number }
The UI coordinate space as { width, height } (logical points). This is
the space area pos, anchors, and getLayoutInfo rects use — and it is
NOT the pixel size of a capture screenshot, which may be downscaled. Use
this for absolute area positioning (e.g. pinning a menu above a bottom
taskbar) instead of guessing the size from a capture image.
It follows the size the engine draws at, and renderer.setViewportSize
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 renderer.surfaceSize() — the whole drawing
surface a screen is laid out over, which an editor layout makes larger
than the rect renderer.viewportSize() draws the scene into — and
ui.pixelRatio() is the factor between the two spaces.
Returns { width: number, height: number } — { width, height } in logical UI points.
typed/builtin//modules/api/engine/ui/ui/scroll
ui.scroll(deltaX: number, deltaY: number)
Simulate a mouse-wheel scroll event on the UI.
Parameters
deltaXnumber— Horizontal scroll delta.deltaYnumber— Vertical scroll delta.
typed/builtin//modules/api/engine/ui/ui/setAreaPos
ui.setAreaPos(id: string, x: number, y: number)
Programmatically move a movable area widget to (x, y).
Applied for one frame; subsequent frames let drag tracking
take over.
Parameters
idstring— Area widget id.xnumber— Target pivot x (screen coords).ynumber— Target pivot y (screen coords).
typed/builtin//modules/api/engine/ui/ui/setAreaSize
ui.setAreaSize(id: string, w: number, h: number)
Programmatically set a resizable area's size (the user-size
override) — for maximize / restore / tile. Persists until the area's
declared width/height changes or ui.resetAreaSize(id) clears it.
Parameters
idstring— Area widget id.wnumber— Target width (screen coords).hnumber— Target height (screen coords).
typed/builtin//modules/api/engine/ui/ui/setDockWindowRect
ui.setDockWindowRect(dockId: string, panelId: string, x: number, y: number, width: number, height: number)
Place the floating window of a dockArea panel at (x, y) with
size (width, height). Applies once the panel occupies a window —
a request made before then waits for it.
Parameters
dockIdstring— DockArea widget id.panelIdstring— Id of the panel held by the window to place.xnumber— Window left edge (screen coords).ynumber— Window top edge (screen coords).widthnumber— Window width (screen coords).heightnumber— Window height (screen coords).
typed/builtin//modules/api/engine/ui/ui/setScreenRenderLayer
ui.setScreenRenderLayer(name: string, mask: number)
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 ui bit;
the editor places its chrome on EditorUI 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, EditorUI
for the window and ui for the camera's picture. __renderLayers.bit(name)
answers with a layer's bit INDEX, so a mask sets the bit at that index and
__renderLayers.names(mask) reads it back as the names it holds.
Parameters
namestring— Screen identifier.masknumber— Render-layer membership bitmask — the OR ofbit32.lshift(1, __renderLayers.bit(name))over the layers the screen is on.
ui.setScreenRenderLayer("hud", bit32.lshift(1, __renderLayers.bit("ui")))
typed/builtin//modules/api/engine/ui/ui/setScreenSpace
ui.setScreenSpace(name: string, space: string)
Set which surface a screen is part of. "camera" 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. "window" is the window itself, whatever presents the scene
— where the editor's chrome lives, and the default for a screen on the
EditorUI layer. The two are one statement made from either end, so a
screen placed on the window takes the EditorUI membership unless
setScreenRenderLayer 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.
Parameters
namestring— Screen identifier.spacestring—"camera"|"window".
ui.setScreenSpace("agent-banner", "window")
typed/builtin//modules/api/engine/ui/ui/setScrollPosition
ui.setScrollPosition(widgetId: string, offsetY: number)
Set the scroll offset of a scrollArea widget.
Parameters
widgetIdstring— Scroll area widget id.offsetYnumber— Vertical scroll offset in pixels.
typed/builtin//modules/api/engine/ui/ui/setShaderUniforms
ui.setShaderUniforms(name: string, uniforms: { [string]: number })
Set uniform values on a registered background shader.
Parameters
namestring— Shader name identifier.uniforms{ [string]: number }— Map of uniform name to number value.
typed/builtin//modules/api/engine/ui/ui/setTheme
ui.setTheme(name: string)
Switch the active global theme by name.
Parameters
namestring— Theme name to activate.
typed/builtin//modules/api/engine/ui/ui/showScreen
ui.showScreen(name: string) -> boolean
Make a registered screen visible, and report whether a screen by
that name is registered. The engine applies the show later in the
frame; listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to show.
Returns boolean — True when a screen by this name is registered.
typed/builtin//modules/api/engine/ui/ui/simulateDisplayScale
ui.simulateDisplayScale(scale: DisplayScaleSpec?)
Size the UI as a display stating scale would, in place of what the
platform states. A density sizes it as a handheld display of that
many physical pixels per point (omit it for a desktop display sized by
its width), and textScale 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.
ui.displayScale(), ui.screenSize() and ui.pixelRatio() report it
from the next frame. Call it with no argument (or nil) 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.
Parameters
scaleDisplayScaleSpec(optional) —{ density?, textScale? }; nil returns to the platform's scale.
ui.simulateDisplayScale({ density = 2.625, textScale = 1.3 })
ui.simulateDisplayScale()
typed/builtin//modules/api/engine/ui/ui/simulateSafeArea
ui.simulateSafeArea(area: SafeAreaSpec?)
Lay the UI out as if the system covered the screen by area
(logical points) in place of what the platform states: each inset
defaults to 0 and cutouts is a list of { x, y, width, height }.
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. ui.safeArea() reports it from the next frame. Call
it with no argument (or nil) to return to the platform's own safe
area. A negative or non-number inset is refused by name.
Parameters
areaSafeAreaSpec(optional) —{ top?, right?, bottom?, left?, cutouts? }in logical points; nil returns to the platform's safe area.
ui.simulateSafeArea({ top = 40, bottom = 30, cutouts = { { x = 600, y = 0, width = 80, height = 40 } } })
ui.simulateSafeArea()
typed/builtin//modules/api/engine/ui/ui/unregisterCallbackEnv
ui.unregisterCallbackEnv(key: string)
Remove an environment registered with ui.registerCallbackEnv. Its
onCallback stops receiving broadcasts. No-op if key isn't registered.
Parameters
keystring— The key passed toui.registerCallbackEnv.
typed/builtin//modules/api/engine/ui/ui/unregisterScreen
ui.unregisterScreen(name: string) -> boolean
Remove a screen from the registry entirely. Unlike
hideScreen, this deletes the entry so it no longer appears in
listScreens or render iteration.
Parameters
namestring— Screen identifier to unregister.
Returns boolean — True when a screen by this name was registered.
typed/builtin//modules/api/engine/ui/ui/unregisterWidget
ui.unregisterWidget(name: string)
Drop a registered custom widget kind. Subsequent references
produce an unknown-widget-type diagnostic.
Parameters
namestring— Custom widget kind name.
typed/builtin//modules/api/engine/ui/ui/updateScreen
ui.updateScreen(name: string, widgetTree: WidgetTree)
Replace the widget tree of an already-registered screen.
Parameters
namestring— Screen identifier to update.widgetTreeWidgetTree— New root widget table.
typed/builtin//modules/api/engine/ui/ui/useStyles
ui.useStyles(themeName: string)
Apply a registered style file's classes additively without changing the active theme.
Parameters
themeNamestring— Name of the registered style / theme asset.
typed/builtin//modules/api/engine/ui/ui/widgetState
ui.widgetState(widgetId: string, key: string, default: any?) -> any
Read per-widget cross-frame state. Returns the value
previously written via widgetStateSet, or default (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.
Parameters
widgetIdstring— Widget id whose state to read.keystring— State key.defaultany(optional) — Value to return when nothing has been written.
Returns any — Stored value, default, or nil.
typed/builtin//modules/api/engine/ui/ui/widgetStateClear
ui.widgetStateClear(widgetId: string, key: string)
Remove a per-widget state entry.
Parameters
widgetIdstring— Widget id whose state to clear.keystring— State key.
typed/builtin//modules/api/engine/ui/ui/widgetStateSet
ui.widgetStateSet(widgetId: string, key: string, value: any?)
Write per-widget cross-frame state. Replaces any existing
value under (widgetId, key). Tables are stored by reference.
Parameters
widgetIdstring— Widget id to scope the state under.keystring— State key.valueany(optional) — Value to store (must be non-nil).
typed/builtin//modules/api/engine/userfile/userfile/pick
userfile.pick(opts: PickOpts?) -> PickResult
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 task.await) and returns
{ cancelled, files = {{ name, mime, size, bytes?, vfsPath? }} }.
Without writeTo each file carries bytes (a binary-safe string);
with writeTo each carries vfsPath (read it with vfs.read).
Cancelling returns { cancelled = true, files = {} }; a genuine failure
(e.g. a lost browser user-activation gesture) raises an error.
Parameters
optsPickOpts(optional) — Picker options (optional): multiple, folder, title, filters, writeTo.
Returns PickResult — The decoded result table.
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
typed/builtin//modules/api/engine/userfile/userfile/pickFolder
userfile.pickFolder(opts: PickOpts?) -> PickResult
Convenience for userfile.pick({ folder = true }) — pick a whole
directory tree. Yields until the user finishes and returns the same
result table as pick. On the web this degrades to a multi-file selection.
Parameters
optsPickOpts(optional) — Picker options (optional);folderis forced true.
Returns PickResult — The decoded result table.
local r = userfile.pickFolder({ writeTo = "/source/imported/" })
typed/builtin//modules/api/engine/userfile/userfile/save
userfile.save(path: string, opts: SaveOpts?) -> (boolean, string, SaveReport?)
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 (task.spawn from a
callback). Returns true, a sentence saying where the file went, and the
report { state, detail, name, size, path } whose state
is "saved" (the file is on the device) or "offered" (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).
Parameters
pathstring— VFS path of the file to save.optsSaveOpts(optional) —{ name }, the file name the device gets (the file's own when omitted).
Returns (boolean, string, SaveReport?) — Whether the device took the file. Where it went, or why it was refused. The save report, when the device took the file.
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" })
typed/builtin//modules/api/engine/vfs/vfs/claimWalks
vfs.claimWalks() -> number
Lifetime count of walks over every asset the world offers: the part of
an enumeration's cost that grows with how many assets exist. asset.list
reads a snapshot of every registered claim, built once for each change to
what the world offers and shared by every list after it;
asset.categories 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.
Returns number — Count of claim walks this session. Monotonic.
local at = vfs.claimWalks()
vfs.write("/zero/source/probe.module/init.luau", "return {}")
asset.list("module")
print(vfs.claimWalks() - at)
typed/builtin//modules/api/engine/vfs/vfs/clearPlayShadow
vfs.clearPlayShadow() -> boolean
Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.
Returns boolean — Always true.
vfs.clearPlayShadow()
typed/builtin//modules/api/engine/vfs/vfs/contentAddress
vfs.contentAddress(path: string, opts: VfsOpts?) -> string?
The content address (sha-256 hex) of the bytes vfs.read(path)
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 vfs.read 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.
Parameters
pathstring— VFS path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns string? — The sha-256 hex of the blob-held bytes at path, or nil.
local address = vfs.contentAddress("/zero/source/scenes/main.scene/scene.json")
typed/builtin//modules/api/engine/vfs/vfs/contentVersion
vfs.contentVersion(path: string) -> number
A number that moves every time the file at path 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 path (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.
Parameters
pathstring— The file's path, in any formvfs.readtakes.
Returns number — The path's content version.
local seen = vfs.contentVersion("/zero/source/.world_settings")
typed/builtin//modules/api/engine/vfs/vfs/copy
vfs.copy(src: string, dst: string) -> (boolean, (WriteReport | string)?)
Copy a file OR directory from src to dst, cp -r style. A
directory recurses — every descendant is replicated at the same
relative path under dst, .refs sidecars included. .meta
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.
Parameters
srcstring— Source absolute VFS path (file or directory).dststring— Destination absolute VFS path.
Returns (boolean, (WriteReport | string)?) — True and a WriteReport on success. A copy is a write at its destination, so it reports what vfs.write reports for the same bytes at that path: where they went, and what the static passes read in the Luau among them, per landed leaf when the copy took a folder. A module that is advisory where it sat reads as a script error once it sits on a component path, and the copy is what put it there. (false, errmsg) on failure.
vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")
local ok, report = vfs.copy(mod, "/zero/source/game/Vent.component")
typed/builtin//modules/api/engine/vfs/vfs/currentAuthor
vfs.currentAuthor() -> { id: string, stable: string, connection: string, agent: string?, name: string?, account: string? }?
The caller this call is attributed to: the author a /source write
made right now would be recorded under in the play shadow. id 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. stable 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
as argument every tool takes, has a stable of its own. connection
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. agent is the name the world's roster holds for the caller now;
name is its account's username, which every agent of the account
shares; account 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 id against vfs.playShadowAuthors() to
separate your own pending edits from a co-author's.
Returns { id: string, stable: string, connection: string, agent: string?, name: string?, account: string? }? — { id, stable, connection, agent, name, account } for the acting caller, or nil when unattributed.
local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")
typed/builtin//modules/api/engine/vfs/vfs/dirSink
vfs.dirSink(path: string, opts: DirSinkOpts) -> PluginHandle
A sink for new files under the folder at path, subfolders
included, to pass a plugin in a call. The plugin makes whatever files it
needs within opts.maxFiles files and opts.maxBytes bytes in all.
Only the world's own files take a sink.
Parameters
pathstring— The folder to write under.optsDirSinkOpts—{ maxFiles = <n>, maxBytes = <bytes>, extensions = { "png" }? }.
Returns PluginHandle — The folder sink.
local frames = vfs.dirSink("/source/frames", { maxFiles = 240, maxBytes = 200 * 1024 * 1024, extensions = { "png" } })
typed/builtin//modules/api/engine/vfs/vfs/discardPlayShadow
vfs.discardPlayShadow() -> { discarded: { string }, owed: { { path: string, reason: string } } }
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 discarded, 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 owed, posted as a notice and logged by name. Calling it again asks
for every owed restore again, pulling a copy whose pull failed.
Returns { discarded: { string }, owed: { { path: string, reason: string } } } — discarded: every path the discard took off the play shadow, sorted, owed ones included. owed: every path it left owed, sorted by path, with why its restore has not landed; empty when every path reverted.
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
typed/builtin//modules/api/engine/vfs/vfs/durability
vfs.durability(paths: string | { string }, opts: VfsOpts?) -> Durability
Did that write save to disk, and if not how is it kept? What became of
the bytes just written at paths — one path, or an array of them
answered as ONE write. durable 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 /source/tmp/ 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).
/source/local/ is durable: the machine keeps it. durable is
present whatever the answer is, so its absence is never a reading. A
non-durable answer carries survivesRestart (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
warning (the state and what a restart does with it, for a reader
scanning values rather than checking a field; it opens with PLAY SHADOW
or SCRATCH). When the shadow holds a path the answer also carries
playShadow (the routes to disk and what each costs a session other
people are running in) and shadowed (which of the given paths the shadow
has, or under pending would take); when a path is in scratch it carries
scratch (which of them are in scratch).
This is the answer, off the same shadow set and in the same words, that the
write_file / edit_file / capture tools attach to their own results and
that asset.create 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.
Parameters
pathsstring | { string }— One VFS path, or an array of paths answered together as one write. A path resolves the wayvfs.writeresolves its own — absolute or@-rooted as it stands, a bare one under/source/— 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.optsVfsOpts(optional) —{ root = "/source/", pending = false }.pending = trueasks 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 —av.recordopens 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.
Returns Durability — { durable, survivesRestart?, warning?, playShadow?, shadowed?, scratch? }: survivesRestart and warning present exactly when durable is false, playShadow and shadowed when the play shadow holds a path, scratch when a path is in /source/tmp/, which reads back while the engine runs and is gone when it stops or restarts.
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 })
typed/builtin//modules/api/engine/vfs/vfs/evict
vfs.evict(path: string, opts: VfsOpts?) -> boolean
Drop the in-memory bytes for path from the writable
MemFs layer without removing the asset. Use after processing
large binaries to reclaim RAM.
Parameters
pathstring— VFS path whose bytes should be evicted.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if MemFs bytes were dropped; false otherwise.
vfs.evict("/zero/source/textures/imported_big.png")
typed/builtin//modules/api/engine/vfs/vfs/exists
vfs.exists(path: string, opts: VfsOpts?) -> boolean
Is the path known to the VFS? Checks the Stage-1 metadata
(.meta sidecar / ManifestView) — NOT "are the bytes locally
cached?". Use vfs.read(path) ~= nil to confirm bytes are
reachable.
Parameters
pathstring— VFS path to check.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if the path is known regardless of byte cache state.
assert(vfs.exists("@builtin/models/Cube"))
typed/builtin//modules/api/engine/vfs/vfs/folderHashWalks
vfs.folderHashWalks() -> number
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.
Returns number — Count of folder checksum walks this session. Monotonic.
local at = vfs.folderHashWalks()
vfs.write("/zero/source/scenes/probe.scene/scene_dirty/entities/e1.json", "{}")
print(vfs.folderHashWalks() - at)
typed/builtin//modules/api/engine/vfs/vfs/holdContentSync
vfs.holdContentSync(held: boolean)
Hold (true) or release (false) 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:
world.syncStatus() reads content_synced = false and
content_sync_held = true, 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
--hold-content-sync (holdContentSync=1 in a browser launch) raises the
hold before the world binds, which is how the initial sync of a fresh
engine is held.
Parameters
heldboolean— True to hold the content sync, false to release it.
vfs.holdContentSync(true)
vfs.holdContentSync(false)
typed/builtin//modules/api/engine/vfs/vfs/installedFrom
vfs.installedFrom(path: string, opts: VfsOpts?) -> InstalledFrom?
Where the asset holding path 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.
path is the asset's own folder or anything inside it. Nil for a
path whose bytes the library or the world holds itself.
Parameters
pathstring— VFS path of the asset or of anything inside it.optsVfsOpts(optional) —{ root = "/source/" }.
Returns InstalledFrom? — The origin, or nil when the path is not inside an asset carried by reference.
local from = vfs.installedFrom(ref.path) if from then print(from.world, from.commit) end
typed/builtin//modules/api/engine/vfs/vfs/isDirectory
vfs.isDirectory(path: string, opts: VfsOpts?) -> boolean
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 (notes.json) reads as the file it is while
a real <name>.<type>/ folder reads as a folder. Costs the same
whatever the containing folder holds; use vfs.list when you want
every entry's kind, this when you hold one path.
Parameters
pathstring— VFS path to classify.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if the path is a directory.
if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end
typed/builtin//modules/api/engine/vfs/vfs/isSaveExcluded
vfs.isSaveExcluded(path: string, opts: VfsOpts?) -> boolean
Does this path hold content the machine keeps to itself?
/source/tmp/ is session scratch and /source/local/ 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 /source/; 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
/source/tmpfoo/ is ordinary content. Ask here whenever your
code has to agree with what a world can hold.
Parameters
pathstring— VFS path to classify.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True when the path is held back from world saves and sync.
if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end
typed/builtin//modules/api/engine/vfs/vfs/list
vfs.list(path: string?) -> { VfsListEntry }
List entries in a VFS directory. A directory whose current
listing the next frame assembles (/zero/runtime/sync/entities after a
stretch nothing read the replication surface) is waited for where the
calling code can yield (coroutine.isyieldable()), as vfs.read waits;
where it cannot, the listing raises VFS_READ_CANNOT_WAIT, naming the
path.
Parameters
pathstring(optional) — Directory path (defaults to/zero).
Returns { VfsListEntry } — Array of { name, isDirectory, path } tables; vfs.stat(entry.path) gives an entry's size and modification time.
for _, e in ipairs(vfs.list("/zero/source")) do print(e.name) end
typed/builtin//modules/api/engine/vfs/vfs/memResident
vfs.memResident() -> { { path: string, bytes: number, kind: string } }
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 vfs.evict 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 vfs.evict and to reading with { keep = true } —
use it to see what encoded bytes are held in RAM, and why.
Returns { { path: string, bytes: number, kind: string } } — Array of { path, bytes, kind }; kind is "binary" for non-text content and "large-text" for oversized text. Empty when the VFS isn't up.
for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end
typed/builtin//modules/api/engine/vfs/vfs/mkdir
vfs.mkdir(path: string, opts: VfsOpts?) -> boolean
Create a directory. mkdir -p semantics — idempotent.
Errors if a file already exists at the same path. While play is
running an authored /source 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
/source/tmp/ creates as in edit mode.
Parameters
pathstring— Directory path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if the directory exists after the call. A creation the running play session refuses raises with the whole reason.
vfs.mkdir("/zero/source/scenes/")
typed/builtin//modules/api/engine/vfs/vfs/move
vfs.move(src: string, dst: string, opts: { quiet: boolean? }?) -> (boolean, (WriteReport | string)?)
Move a file from src to dst. By default fires the destination's
write side effects; pass opts.quiet = true 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 vfs.write 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
/source 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.
Parameters
srcstring— Source absolute VFS path.dststring— Destination absolute VFS path.opts{ quiet: boolean? }(optional) — Optional{ quiet: boolean? }.
Returns (boolean, (WriteReport | string)?) — True and a WriteReport on success. A move IS a write at its destination, so it reports what vfs.write reports for the same bytes at that path: where they went, and what the static passes read in the Luau among them, per landed leaf when the move took a folder. (false, errmsg) when the paths themselves refuse it. A move the running play session refuses raises with the whole reason instead.
vfs.move("/zero/source/a.luau", "/zero/source/b.luau")
local ok, report = vfs.move(mod .. "/init.luau", comp .. "/init.luau")
typed/builtin//modules/api/engine/vfs/vfs/movedReferenceNames
vfs.movedReferenceNames(path: string?, opts: VfsOpts?) -> { MovedReferenceSource }
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 currentText
back with vfs.write(source, currentText) brings the text up to date
without changing what any reference resolves to.
Only sources the world authors are considered: the engine library and
/source/deps/ keep the text they were published with.
Parameters
pathstring(optional) — One source to ask about; every source the world authors when absent.optsVfsOpts(optional) —{ root = "/source/" }.
Returns { MovedReferenceSource } — One entry per source holding moved names, in path order; empty when none does.
for _, s in ipairs(vfs.movedReferenceNames()) do print(s.source, #s.names) end
typed/builtin//modules/api/engine/vfs/vfs/mutationSeq
vfs.mutationSeq() -> number
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 onChange, 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; asset.reloadSeq(ref) is the per-asset reading.
Returns number — Count of applied VFS mutations this session. Monotonic.
local at = vfs.mutationSeq()
vfs.write("/zero/source/tmp/note.txt", "hi")
repeat task.wait() until vfs.mutationSeq() > at
typed/builtin//modules/api/engine/vfs/vfs/open
vfs.open(path: string, opts: VfsOpts?) -> PluginHandle
A reader of the file at path, 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.
Parameters
pathstring— The file to read.optsVfsOpts(optional) —{ root = "/source/" }.
Returns PluginHandle — The reader.
local weights = vfs.open(asset.containing(__FILE__).path .. "/weights.gguf")
typed/builtin//modules/api/engine/vfs/vfs/openDir
vfs.openDir(path: string, opts: VfsOpts?) -> PluginHandle
A reader of the folder at path 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.
Parameters
pathstring— The folder to read.optsVfsOpts(optional) —{ root = "/source/" }.
Returns PluginHandle — The folder reader.
local model = vfs.openDir(asset.containing(__FILE__).path .. "/model")
typed/builtin//modules/api/engine/vfs/vfs/pendingWrites
vfs.pendingWrites() -> { string }
List the /source 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;
world.vcsStatus unions these so a fresh edit reads back as dirty
immediately, and world.syncStatus().owed_paths reports the same list.
Empty when fully synced.
Returns { string } — Array of VFS paths with a pending (unconfirmed) local write or removal.
for _, p in ipairs(vfs.pendingWrites()) do print(p) end
typed/builtin//modules/api/engine/vfs/vfs/playDiscardsOwed
vfs.playDiscardsOwed() -> { { path: string, reason: string } }
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.
Returns { { path: string, reason: string } } — Every owed path, sorted by path, with why its restore has not landed.
for _, owed in ipairs(vfs.playDiscardsOwed()) do
print(owed.path, owed.reason)
end
typed/builtin//modules/api/engine/vfs/vfs/playHoldsInPlace
vfs.playHoldsInPlace(path: string, opts: VfsOpts?) -> boolean
Whether the running play session holds path where it stands:
vfs.move of it, and vfs.remove of it without durable = true, 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
/source/tmp/ or /source/local/, and a path outside authored source.
Ask it before the first step of a change that ends by taking path
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.
Parameters
pathstring— The VFS path: absolute,@-rooted, or bare under/source/.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True while a move or ambient removal of path would be refused.
if vfs.playHoldsInPlace(src) then log.warn(src .. " moves once play ends") end
typed/builtin//modules/api/engine/vfs/vfs/playShadowAuthors
vfs.playShadowAuthors() -> { [string]: { id: string, stable: string, connection: string, agent: string?, name: string?, account: string? } }
The caller behind each currently-shadowed /source path, in the
shape vfs.currentAuthor() answers in: the id the write was attributed
to, the name the world's roster holds for that caller now (agent, 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. id 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.
Returns { [string]: { id: string, stable: string, connection: string, agent: string?, name: string?, account: string? } } — Map of shadowed path to { id, stable, connection, agent, name, account }.
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
typed/builtin//modules/api/engine/vfs/vfs/playShadowPaths
vfs.playShadowPaths() -> { string }
Every source write made during play that is not durable yet — the set
to keep or discard before leaving play. Lists the /source 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.
Returns { string } — Array of normalized VFS paths currently shadowed.
for _, p in ipairs(vfs.playShadowPaths()) do print(p) end
typed/builtin//modules/api/engine/vfs/vfs/promotePlayShadow
vfs.promotePlayShadow(path: string | { string }) -> ((string | { string })?, { string }, { string })
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.
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 (Gold.material, controls.inputMap) 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
vfs.playShadowPaths() snapshot one path at a time settles every path
in it.
Parameters
pathstring | { string }— A shadowed VFS path to promote, or an array of them (from vfs.playShadowPaths()).
Returns ((string | { string })?, { string }, { string }) — The path the call settled (nil when nothing was on the play shadow there), or the array of settled paths when an array went in; then the array of every path the call settled, a folder asset's carried files included; then the array of named paths that held nothing on the play shadow. None of the named paths are shadowed any more.
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)
typed/builtin//modules/api/engine/vfs/vfs/read
vfs.read(path: string, opts: VfsOpts?) -> (string?, string?)
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 read_file tool and the VFS HTTP route give for the
same path (/zero/source/a.txt: not found, a directory's refusal,
a /runtime 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, Json.decode((vfs.read(path))), to
pass the bytes alone. Relative paths resolve under opts.root
(default /source/). When the bytes aren't locally cached and the
calling code can yield (coroutine.isyieldable(): a task,
awake, start, update, onEnable, onOwnerChanged,
onSyncReceived, a replicated event's
handler), the read waits while the lazy fetch runs and answers
the bytes. Code that cannot yield (onDisable, onDestroy,
onPropertyChanged, a received synced-function call, a vfs.watch callback, a
module's top level while require runs it, a metamethod, a
table.sort comparator) cannot wait: a read there of bytes still on their
way (a file vfs.exists knows, a render surface being captured, a
/runtime/sync node whose current reading the next frame assembles)
raises VFS_READ_CANNOT_WAIT, naming the path and where to read it.
Parameters
pathstring— VFS path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns (string?, string?) — File contents (binary-safe); nil and the reason (the path and why it holds no bytes to read) for a read that finds none.
local src, why = vfs.read("/zero/source/notes.txt"); if src == nil then print(why) end
typed/builtin//modules/api/engine/vfs/vfs/readAsync
vfs.readAsync(path: string, opts: VfsOpts?) -> string
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.
Parameters
pathstring— VFS path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns string — Promise ID — pass to task.await().
local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png"))
typed/builtin//modules/api/engine/vfs/vfs/readBounded
vfs.readBounded(path: string, maxBytes: number) -> (string?, string?)
Read a file from the virtual filesystem, at most maxBytes
long. Binary-safe, and read the way vfs.read reads it. A file
longer than maxBytes answers nil, "too_large"; 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
/source/. 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
VFS_READ_CANNOT_WAIT, as vfs.read does. A read that finds no
bytes answers nil and the reason vfs.read gives.
Parameters
pathstring— VFS path.maxBytesnumber— The most bytes the read answers, a whole number 0 or more.
Returns (string?, string?) — File contents (binary-safe); nil and "too_large" for a file longer than maxBytes; nil and the reason vfs.read gives for a read that finds no bytes.
local raw, why = vfs.readBounded("/zero/source/notes.json", 65536)
typed/builtin//modules/api/engine/vfs/vfs/reload
vfs.reload(modulePath: string?) -> boolean
Clear entries from the require() cache so the next
require(name) 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 require runs the source the module's file holds
when the reload is called, whichever write brought those bytes
there, a quiet one included.
Parameters
modulePathstring(optional) — Module identity to reload (omit to reload every module of the world's own).
Returns boolean — For a single identity, whether a module was cached under that name and has now been dropped — false says the name matched nothing. The no-arg form returns true.
vfs.reload("@mylib/utils.helpers")
typed/builtin//modules/api/engine/vfs/vfs/remove
vfs.remove(path: string, opts: VfsOpts?) -> (boolean, string?)
Remove a file. Refuses to remove directories unless
opts.recursive = true. Refuses protected system roots. While
play is running, authored /source content that predates play is
refused and the refusal RAISES with the reason; content this play
session created is removable, a folder included. Pass
opts.durable = true 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
vfs.write(path, bytes, { durable = true }) 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 recursive.
Parameters
pathstring— VFS path to remove.optsVfsOpts(optional) —{ root = "/source/", recursive = false, durable = false }.
Returns (boolean, string?) — True on success; (false, errmsg) when the path itself refuses the removal. A removal the running play session refuses raises with the whole reason instead, so a pcall around the call reads it — and the lock answers ahead of whether the path is there, so a locked /source path raises whether or not it holds anything.
vfs.remove("/zero/source/scratch.luau")
typed/builtin//modules/api/engine/vfs/vfs/revertPlayShadow
vfs.revertPlayShadow(path: string | { string }) -> ((string | { string })?, { string }, { string })
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.
Parameters
pathstring | { string }— A shadowed VFS path to revert, or an array of them (from vfs.playShadowPaths()).
Returns ((string | { string })?, { string }, { string }) — The path that is now reverted (nil when nothing was on the play shadow there), or the array of reverted paths when an array went in; then the array of every path the call reverted, a folder asset's carried files included; then the array of named paths that held nothing on the play shadow. None of the named paths are shadowed any more.
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
local dropped = vfs.revertPlayShadow({ "/zero/source/a.md", "/zero/source/b.md" })
typed/builtin//modules/api/engine/vfs/vfs/sessionLost
vfs.sessionLost() -> boolean
Whether the live world session this engine established is lost and
reconnecting. While it is, every write this engine makes to authored
source (/source outside /source/tmp/) 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 vfs.simulateSessionLoss
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.
Returns boolean — True while writes to authored source are refused because the live session is lost.
if vfs.sessionLost() then log.warn("writes wait for the live session") end
typed/builtin//modules/api/engine/vfs/vfs/simulateSessionLoss
vfs.simulateSessionLoss(lost: boolean)
Put the VFS in the state a lost live session puts it in (true), or
take it out of it (false), without touching the connection:
vfs.sessionLost() 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.
Parameters
lostboolean— True to refuse writes to authored source as a lost session does, false to accept them again.
vfs.simulateSessionLoss(true)
vfs.simulateSessionLoss(false)
typed/builtin//modules/api/engine/vfs/vfs/sink
vfs.sink(path: string, opts: SinkOpts) -> PluginHandle
A sink for the file at path, to pass a plugin in a call. The
plugin writes it from empty, up to opts.maxBytes, and the file lands
when the plugin closes the sink or drops it. Only the world's own files
take a sink.
Parameters
pathstring— The file to write.optsSinkOpts—{ maxBytes = <bytes> }, required.
Returns PluginHandle — The sink.
local out = vfs.sink("/source/imported/sphere.zvol", { maxBytes = 64 * 1024 * 1024 })
typed/builtin//modules/api/engine/vfs/vfs/stat
vfs.stat(path: string, opts: VfsOpts?) -> (VfsStat?, string?)
What is at path, how big it is and when it last changed, without
reading it. size is how many bytes vfs.read(path) 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. modified is when this engine's copy last changed, in
seconds since the Unix epoch (the scale os.time() 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 stat and ls -l print
the same size and time. A path nothing is at answers nil and the reason, the
sentence a vfs.read of it gives.
To tell whether a file changed without reading it: vfs.contentAddress(path)
names the bytes the blob store holds, and vfs.contentVersion(path)
moves on every change this session to a file the memory layer holds.
Parameters
pathstring— VFS path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns (VfsStat?, string?) — The entry's description, or nil and the reason nothing is at the path.
local s = vfs.stat("/zero/source/scenes/main.scene/scene.json"); if s then print(s.size, s.modified) end
typed/builtin//modules/api/engine/vfs/vfs/unmarkPlayShadow
vfs.unmarkPlayShadow(path: string) -> boolean
Forget a single play-shadow path after it has been promoted (saved over source) or discarded. Tracking only — never touches the bytes.
Parameters
pathstring— VFS path to unmark.
Returns boolean — Always true.
vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau")
typed/builtin//modules/api/engine/vfs/vfs/unwatch
vfs.unwatch(watcherId: number) -> boolean
Remove a previously registered VFS watcher.
Parameters
watcherIdnumber— Watcher id returned byvfs.watch.
Returns boolean — True if the watcher was found and removed.
vfs.unwatch(id)
typed/builtin//modules/api/engine/vfs/vfs/watch
vfs.watch(path: string, callback: (string, string, string) -> ()) -> number
Register a callback that fires when a VFS path is written or
removed. Two match modes: exact, or folder/prefix (key ends with
/, and fires for any descendant). The callback runs in the VM
that registered it. Returns a watcher id for vfs.unwatch.
Parameters
pathstring— Exact path, or folder path ending in/.callback(string, string, string) -> ()—(mutated_path, kind, origin) -> (), kind"write"or"remove"; origin"local"for a change this engine made (throughvfs.*, the shell or any other surface) and"remote"for one that arrived from another participant in the world.
Returns number — Watcher id.
local id = vfs.watch("/zero/source/", function(path, kind, origin) print(kind, origin, path) end)
typed/builtin//modules/api/engine/vfs/vfs/write
vfs.write(path: string, content: string, opts: VfsOpts?) -> (boolean, (WriteReport | string)?)
Write content to a file. Binary-safe. Overwrites existing
files by default — pass opts.overwrite = false to refuse to
clobber. While play is running a /source 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
survivesRestart says whether a restart keeps it. Scratch under /source/tmp/ writes through untouched,
in edit mode and in play, and is gone when the engine stops or restarts, so
its report says durable = false, survivesRestart = false and a warning
opening SCRATCH.
Pass opts.durable = true to say these bytes ARE the source: the
write reaches canonical /source 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.
Parameters
pathstring— VFS path to write to.contentstring— File content (binary-safe).optsVfsOpts(optional) —{ root = "/source/", overwrite = true, quiet = false, durable = false }.
Returns (boolean, (WriteReport | string)?) — True and a WriteReport on success: durable says where the bytes went, with survivesRestart / warning / playShadow / shadowed beside it when the play shadow took them and survivesRestart / warning / scratch when they landed in /source/tmp/ scratch, diagnostics carries what the static passes read in the Luau it landed, and contentRequirements names what the asset it landed in must still say before the world publishes. It is the answer the MCP write_file tool gives for the same bytes at the same path. On failure returns false + error message; a durable write, and content past 256 MiB, raise instead of returning false.
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 })
typed/builtin//modules/api/engine/video/video/create
video.create(url: string, options: VideoOptions?) -> string
Create a video player. Returns a texture handle (e.g.
"video_0") usable directly in material.setTexture() — its
frames sample like any other texture.
Parameters
urlstring— URL or asset path to an MP4 video file.optionsVideoOptions(optional) — Playback options:loop(default false),autoplay(default false),rate(default 1.0).
Returns string — Texture handle.
local tex = video.create("http://example.com/clip.mp4", { autoplay = true })
typed/builtin//modules/api/engine/video/video/destroy
video.destroy(handle: string) -> boolean
Destroy a video player and free the render target and all resources.
Parameters
handlestring— Video handle fromvideo.create.
Returns boolean — True if the player was found and destroyed.
video.destroy(rt)
typed/builtin//modules/api/engine/video/video/getInfo
video.getInfo(handle: string) -> VideoInfo?
Get video information and current playback state. codec is the
configuration the stream declares, such as "avc1.64002a", and
codecSupported 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. state reads "unsupported" for a stream this
host has no decoder for, with error naming the codec and what refused
it, and no frame is produced for it. The frame counts describe
playback since create: framesDecoded is how many frames the decoder
produced, framesPresented how many the player handed to its texture,
framesExpected how many frames the playback clock reached (one per
frame interval a tick landed in), and frameThreadDecodes how many
samples were decoded on the engine's frame thread rather than on the
decoder's own.
Parameters
handlestring— Video handle.
Returns VideoInfo? — { width, height, duration, currentTime, state, rate, loop, codec, codecSupported, error, framesDecoded, framesPresented, framesExpected, frameThreadDecodes } or nil if the handle is invalid.
local i = video.getInfo(rt); print(i.currentTime, "/", i.duration)
typed/builtin//modules/api/engine/video/video/pause
video.pause(handle: string) -> boolean
Pause video playback. Can be resumed with video.play.
Parameters
handlestring— Video handle.
Returns boolean — True if the video was playing and is now paused, or was still loading with a play carried and that play is withdrawn. False for a player that was not playing (ready, paused, ended, error, unsupported) and for a handle that names no player.
video.pause(rt)
typed/builtin//modules/api/engine/video/video/play
video.play(handle: string) -> boolean
Start or resume video playback.
Parameters
handlestring— Video handle fromvideo.create.
Returns boolean — True if the command was accepted: the player is loading (the play is carried until the load lands), ready, playing, paused or ended. False for a handle that names no player and for one in error or unsupported, which never plays.
video.play(rt)
typed/builtin//modules/api/engine/video/video/seek
video.seek(handle: string, time: number) -> boolean
Seek to a specific time (seconds) in the video.
Parameters
handlestring— Video handle.timenumber— Target time in seconds.
Returns boolean — True if the seek was accepted; false for a handle that names no player and for one in error or unsupported.
video.seek(rt, 30.5)
typed/builtin//modules/api/engine/video/video/setLoop
video.setLoop(handle: string, loop: boolean) -> boolean
Enable or disable looping.
Parameters
handlestring— Video handle.loopboolean— Whether to loop playback.
Returns boolean — True if the setting was applied.
video.setLoop(rt, true)
typed/builtin//modules/api/engine/video/video/setRate
video.setRate(handle: string, rate: number) -> boolean
Set the playback speed multiplier. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.
Parameters
handlestring— Video handle.ratenumber— Playback rate.
Returns boolean — True if the rate was set.
video.setRate(rt, 2.0)
typed/builtin//modules/api/engine/video/video/stop
video.stop(handle: string) -> boolean
Stop video playback and reset to the beginning.
Parameters
handlestring— Video handle.
Returns boolean — True if the command was accepted; false for a handle that names no player and for one in error or unsupported.
video.stop(rt)
typed/builtin//modules/bundle_update/M/installInto
M.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
bundleBundleNamespace— The target namespace table. No-op when given a non-table value.
require("@builtin::modules.bundle_update").installInto(bundle)
typed/builtin//modules/bundle_update/bundle/update
bundle.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
entityIdstring— The entity whose hierarchy is captured into the bundle.bundleRefBundleRef(optional) — Optional. When omitted, the ref is inferred from the entity'sAsset.sourcefield. When given, the explicit ref wins.
Returns any — True on success (forwarded from the bundle assetType's :update).
bundle.update(entityId) -- infer from Asset
bundle.update(entityId, { guid = "..." }) -- explicit ref
typed/builtin//modules/colorSequence/M/deserialize
M.deserialize(data: any?) -> ColorSequenceObj
Rebuild a ColorSequence from a {kind = "ColorSequence", keypoints = {...}} payload produced by :serialize(). Used by scene save/load.
Parameters
dataany(optional) — The serialized payload.
Returns ColorSequenceObj — A fresh ColorSequenceObj with the deserialized keypoints.
local c = ColorSequence.deserialize(savedData)
typed/builtin//modules/colorSequence/M/new
M.new(...: any?) -> ColorSequenceObj
Construct a ColorSequence from a constant color (3-array {r,g,b} or {r=,g=,b=} record), a two-point lerp from c0 to c1, or a keypoints array. An entry of that array is a named { time =, value =, envelope? = } record, a { time, {r,g,b}, envelope? } pair, or a bare {r,g,b} colour whose time is its place in the list — so a list of colours is a ramp through them. envelope is optional and may be a single number (broadcast across channels) or a 3-array. Up to 64 keypoints; the first must anchor at time = 0, the last at time = 1. NaN / Inf rejected.
Parameters
...any(optional) —(color),(c0, c1), or({ keypoint, ... })where a keypoint is{time =, value =, envelope? =},{time, {r,g,b}, envelope?}, or{r,g,b}.
Returns ColorSequenceObj — A ColorSequenceObj with :evaluate, :sample, :keypoints, :duration, :serialize, :destroy.
local solid = ColorSequence.new({ 1, 0.5, 0.25 })
local fade = ColorSequence.new({ 1, 1, 1 }, { 0, 0, 0 })
local bow = ColorSequence.new({ { time = 0, value = {1,0,0} }, { time = 0.5, value = {0,1,0} }, { time = 1, value = {0,0,1} } })
local stops = ColorSequence.new({ { 0, {1,0,0} }, { 1, {0,0,1} } })
local ramp = ColorSequence.new({ { 1, 0.85, 0.35 }, { 1, 0.35, 0.05 } })
typed/builtin//modules/component_field_route/M/componentRef
M.componentRef(entityId: string, component: string) -> ComponentRef?
The component ref serving component on entity, or nil when the
entity is gone or does not carry it.
Parameters
entityIdstring— The entity id.componentstring— The component name.
Returns ComponentRef? — The component ref, or nil.
local ref = Route.componentRef(id, "Model")
typed/builtin//modules/component_field_route/M/declaredFields
M.declaredFields(ref: ComponentRef) -> { [string]: boolean }
The public and private field names ref declares, as a set.
Parameters
refComponentRef— A component ref.
Returns { [string]: boolean } — { [fieldName] = true }.
local names = Route.declaredFields(ref)
typed/builtin//modules/component_field_route/M/isReflected
M.isReflected(component: string) -> boolean
Whether component is backed by an engine struct the reflect registry
resolves. A component declared in Luau answers false and is served by
the component ref instead.
Parameters
componentstring— The component name.
Returns boolean — true when the reflect registry holds this component.
if Route.isReflected("Transform") then ... end
typed/builtin//modules/component_field_route/M/readField
M.readField(api: string, entityId: string, component: string, field: string) -> any
Read one component field, through whichever route serves it.
Parameters
apistring— The calling API, named in any diagnostic.entityIdstring— The entity id.componentstring— The component name.fieldstring— The field name.
Returns any — The field value, or nil when the entity, the component or the field is missing.
local blend = Route.readField("Entity.getField", id, "Model", "tintBlend")
typed/builtin//modules/component_field_route/M/reportEntitiesWithoutComponent
M.reportEntitiesWithoutComponent(api: string, component: string, missing: number, total: number, outcome: string)
Report the entities of one call that do not carry the named component, as a single line for the call rather than one per entity.
Parameters
apistring— The calling API, named in the line.componentstring— The component name.missingnumber— How many of the named entities do not carry it.totalnumber— How many entities the call named.outcomestring— What the call did for them, e.g."nothing written".
Route.reportEntitiesWithoutComponent("entity.batchWrite", "Model", 3, 8, "nothing written")
typed/builtin//modules/component_field_route/M/reportEntityWithoutComponent
M.reportEntityWithoutComponent(api: string, entityId: string, component: string, outcome: string)
Report one entity that does not carry the named component, at most
once per interval per (api, component).
Parameters
apistring— The calling API, named in the line.entityIdstring— The entity that does not carry it.componentstring— The component name.outcomestring— What the call did instead, e.g."nothing written".
Route.reportEntityWithoutComponent("Entity.setField", id, "Model", "nothing written")
typed/builtin//modules/component_field_route/M/reportMissingField
M.reportMissingField(api: string, component: string, field: string, outcome: string)
Report a field the component does not declare, at most once per
interval per (api, component, field).
Parameters
apistring— The calling API, named in the line.componentstring— The component name.fieldstring— The field name that resolved to nothing.outcomestring— What the call did instead, e.g."nothing written".
Route.reportMissingField("entity.batchWrite", "Model", "tintBlnd", "nothing written")
typed/builtin//modules/component_field_route/M/requireComponentType
M.requireComponentType(api: string, component: string)
Raise unless component names a component this engine knows — either
an engine struct in the reflect registry or a declared .component.
Parameters
apistring— The calling API, named in the error so the message points at the call.componentstring— The component name to check.
Route.requireComponentType("entity.batchWrite", "Model")
typed/builtin//modules/component_field_route/M/writeField
M.writeField(api: string, entityId: string, component: string, field: string, value: any?) -> boolean
Write one component field, through whichever route serves it.
Parameters
apistring— The calling API, named in any diagnostic.entityIdstring— The entity id.componentstring— The component name.fieldstring— The field name.valueany(optional) — The value to write.
Returns boolean — true when the write landed.
Route.writeField("Entity.setField", id, "Model", "tintBlend", 0.5)
typed/builtin//modules/component_proxy/M/computed
M.computed(fn: (any) -> any) -> string
Mark a function as a computed property. The function takes
self (the proxy) and returns the computed value. Returns a string
sentinel that the Rust-side public_index dispatches through the
registry on every read.
Parameters
fn(any) -> any— The getter — receives the proxy and returns the computed value.
Returns string — The sentinel string to store in the proxy's public table.
public.area = computed(function(self) return self.w * self.h end)
typed/builtin//modules/component_proxy/M/installGlobal
M.installGlobal()
Install computed as a global so component modules can write
public.X = computed(fn) without an explicit require. Called by the
prelude.
require("@builtin::modules.component_proxy").installGlobal()
typed/builtin//modules/component_proxy/M/isComputedSentinel
M.isComputedSentinel(v: any?) -> boolean
True iff v is a computed-property sentinel — the string a
computed(fn) declaration stores in a proxy's public table. Reads
of the property resolve the sentinel to the getter's value, but a raw
pairs() over the backing table yields the sentinel itself. Serializers
call this to skip computed (derived) fields, which are re-derived on load.
Parameters
vany(optional) — The value to test.
Returns boolean — true for a computed sentinel, false otherwise.
typed/builtin//modules/connected_users/connectedUser/agents
connectedUser.agents -> { { name: string, harness: string, since: number, lastCall: number, isLocal: boolean } }
The agents working for this user, since / lastCall in ms since the Unix epoch.
Returns { { name: string, harness: string, since: number, lastCall: number, isLocal: boolean } }
typed/builtin//modules/connected_users/connectedUser/data
connectedUser.data -> { [string]: any }
This user's per-player runtime-data store, bound to their identity.
Returns { [string]: any }
typed/builtin//modules/connected_users/connectedUser/displayName
connectedUser.displayName -> string
This user's human-readable name, falling back to their identity.
Returns string
typed/builtin//modules/connected_users/connectedUser/engines
connectedUser.engines -> { { sessionId: string, profile: string, branch: string?, since: number } }
The engines this user is connected through, since in ms since the Unix epoch.
Returns { { sessionId: string, profile: string, branch: string?, since: number } }
typed/builtin//modules/connected_users/connectedUser/entity
connectedUser.entity -> entityRef?
This user's world-presence body: the avatar bound to them in the active scene, resolved on access.
Returns entityRef?
typed/builtin//modules/connected_users/connectedUser/identity
connectedUser.identity -> string
This user's account id, the sub claim of their session JWT.
Returns string
typed/builtin//modules/connected_users/connectedUser/isLocal
connectedUser.isLocal -> boolean
Whether this record is the user signed in on this engine.
Returns boolean
typed/builtin//modules/connected_users/connectedUsers/agents
connectedUsers.agents() -> { { name: string, harness: string, person: string, lastCall: number } }
Every agent on the world, sorted by person then name.
Returns { { name: string, harness: string, person: string, lastCall: number } }
typed/builtin//modules/connected_users/connectedUsers/count
connectedUsers.count() -> number
How many users are connected.
Returns number
typed/builtin//modules/connected_users/connectedUsers/exists
connectedUsers.exists(identity: string) -> boolean
Whether a user carrying an identity is connected.
Parameters
identitystring
Returns boolean
typed/builtin//modules/connected_users/connectedUsers/get
connectedUsers.get(identity: string) -> connectedUser?
The user carrying an identity, or nil when nobody connected carries it.
Parameters
identitystring
Returns connectedUser?
typed/builtin//modules/connected_users/connectedUsers/list
connectedUsers.list() -> { connectedUser }
Every connected user, ordered by identity.
Returns { connectedUser }
typed/builtin//modules/connected_users/connectedUsers/localUser
connectedUsers.localUser -> connectedUser?
The user signed in on this engine, re-read on each access; nil for an anonymous session.
Returns connectedUser?
typed/builtin//modules/connected_users/connectedUsers/offConnect
connectedUsers.offConnect(handle: number) -> boolean
Drop a connect subscription by its handle. True when a live subscription carried it.
Parameters
handlenumber
Returns boolean
typed/builtin//modules/connected_users/connectedUsers/offDisconnect
connectedUsers.offDisconnect(handle: number) -> boolean
Drop a disconnect subscription by its handle. True when a live subscription carried it.
Parameters
handlenumber
Returns boolean
typed/builtin//modules/connected_users/connectedUsers/offLocalConnect
connectedUsers.offLocalConnect(handle: number) -> boolean
Drop a local-connect subscription by its handle. True when a live subscription carried it.
Parameters
handlenumber
Returns boolean
typed/builtin//modules/connected_users/connectedUsers/onConnect
connectedUsers.onConnect(callback: (connectedUser) -> ()) -> number
Run a callback each time a user connects. Answers the handle offConnect takes.
Parameters
callback(connectedUser) -> ()
Returns number
typed/builtin//modules/connected_users/connectedUsers/onDisconnect
connectedUsers.onDisconnect(callback: (connectedUser) -> ()) -> number
Run a callback each time a user disconnects. Answers the handle offDisconnect takes.
Parameters
callback(connectedUser) -> ()
Returns number
typed/builtin//modules/connected_users/connectedUsers/onLocalConnect
connectedUsers.onLocalConnect(callback: (connectedUser) -> ()) -> number
Run a callback once the local user's session is established, firing immediately when it already is.
Parameters
callback(connectedUser) -> ()
Returns number
typed/builtin//modules/content_version/M/bump
M.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
pathstring— VFS path whose content changed.
require("@builtin::modules.content_version").bump(p)
typed/builtin//modules/content_version/M/get
M.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
pathstring— Normalized VFS path.
Returns number — The path's current version counter.
local v = require("@builtin::modules.content_version").get(p)
typed/builtin//modules/debris/M/add
M.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
idany(optional) — Entity id, name, or proxy table.lifetimenumber(optional) — Seconds before despawn — defaults to 10 when nil.
Returns DebrisHandle — Cancel handle (0 if entity not found).
local bullet = entity.spawn("Bullet"); debris.add(bullet.id, 2.0)
local h = debris.add(target.id, 5); debris.cancel(h)
typed/builtin//modules/debris/M/cancel
M.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
handleOrIdany(optional) — Cancel handle, or entity id / name / proxy.
Returns boolean — Whether a pending record was removed.
debris.cancel(handle); debris.cancel(target.id)
typed/builtin//modules/debris/M/clear
M.clear() -> boolean
Drop every pending entry. Used by the test suite to isolate cases — not part of the user-facing surface.
Returns boolean — Always true.
typed/builtin//modules/debris/M/count
M.count() -> number
Number of currently pending debris entries — handy for diagnostics overlays.
Returns number — Pending count.
print(debris.count(), "pending despawns")
typed/builtin//modules/debris/M/list
M.list() -> { DebrisEntry }
Snapshot every pending entry as a flat array of {id, remainingSecs, handle} records. Order is not stable — don't rely on it.
Returns { DebrisEntry } — Array of pending entries.
for _, e in debris.list() do print(e.id, e.remainingSecs) end
typed/builtin//modules/debris/M/pending
M.pending(id: any?) -> number?
Return the number of seconds remaining before the entity is despawned, or nil if it isn't scheduled.
Parameters
idany(optional) — Entity id, name, or proxy.
Returns number? — Seconds remaining, or nil.
local s = debris.pending(bullet.id); if s then print("dies in", s) end
typed/builtin//modules/deprecated/zui/widget/canvas/canvas
canvas(id: string?, opts: CanvasOpts?) -> any
Build a 2D-paint canvas widget. The widget body is a list of paint commands (line, bezier, polyline, rect, circle, text) drawn in widget-local coords. Pointer/keyboard/scroll handlers are wired through as engine props.
Parameters
idstring(optional) — Widget id used by the engine for event routing and DOM mirror.optsCanvasOpts(optional) — Options table — commands array plus optional width/height, interaction handlers, role/tag overrides, ARIA passthroughs (anyaria*key), and a style table.
Returns any — A widget node consumable by the renderer.
canvas("my-canvas", { commands = { { kind = "circle", center = {50,50}, radius = 20, fill = "#FF8855" } } })
canvas("plot", { commands = {}, onDrag = "plot:drag", style = { width = 400, height = 200 } })
typed/builtin//modules/deprecated/zui/widget/node/node
node(widgetType: string, opts: Opts?, children: any?) -> any
Build a generic widget table with optional id, classes, props,
style, and children. The constructor every zui widget composes on.
Accepts either a single child or an array of children — single
children get wrapped automatically, matching the scroll.module
precedent.
Parameters
widgetTypestring— The widget kind string (e.g."label","panel","canvas").optsOpts(optional) — Optional.id,classes(orclass),props,style.childrenany(optional) — Optional widget table or array of widget tables.
Returns any — The widget table — interoperable with hand-written trees.
local n = node("label", { props = { text = "Hi" } })
local n = node("panel", { id = "p1" }, { childWidget })
typed/builtin//modules/entity_hierarchy/M/swap
M.swap(entityId: string, assetPath: string, opts: SwapOpts?) -> (string?, string?)
Replace a blockout entity with a generated/imported asset, fitting the asset to the source's bounds and inheriting the source's world rotation. The new entity is reparented to the source's parent, optionally inherits the source name, and the original is despawned.
Parameters
entityIdstring— The entity to replace (must exist). Must be a non-empty string.assetPathstring— The asset path to spawn. Must be a non-empty string.optsSwapOpts(optional) — OptionalSwapOpts. Fields:fit("bounds"default |"bounds_xy"|"none"),source_origin("bottom"default |"center"|"top"),asset_origin(same set),keep(e.g.{"name"}),timeout(optional seconds override for the async asset-bounds wait; default is a generous load-scaled poll budget — leave unset).
Returns (string?, string?) — (newEntityId, nil) on success; (nil, errmsg) on failure (invalid args, missing source, asset bounds timeout).
local id, err = EntityHierarchy.swap("blockout", "@builtin::meshes.cube")
local id, err = EntityHierarchy.swap("blockout", asset, { fit = "bounds_xy", keep = { "name" } })
typed/builtin//modules/entity_records/M/addData
M.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
dataany(optional) — The entry's field values.instanceNamestring(optional) — The entry's instance name, or nil for the anonymous instance.
Returns any — The table to pass to component.add.
entity(id).component.add("Pool", EntityRecords.addData({ amount = 120 }, "mana"))
typed/builtin//modules/entity_records/M/authoredComponentData
M.authoredComponentData(rid: string, componentType: string, instanceName: string?, data: any?) -> any
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.
Parameters
ridstring— Runtime entity id carrying the instance.componentTypestring— Component type name as the entity reports it.instanceNamestring(optional) — Name of the instance, or nil for an anonymous one.dataany(optional) — Snapshot of the instance's public data.
Returns any — The snapshot without the instance's own runtime bookkeeping.
local d = EntityRecords.authoredComponentData(id, ty, nil, snapshot)
typed/builtin//modules/entity_records/M/captureRecord
M.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
ridstring— Runtime entity id to capture.parentOriginalIdstring(optional) — Parent's original_id, or nil for a root record.
Returns any — One record table.
local rec = EntityRecords.captureRecord(id, nil)
typed/builtin//modules/entity_records/M/carriesInstance
M.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
ridstring— Runtime entity id.typeNamestring— Component type the entry names.instanceNamestring(optional) — The entry's instance name, or nil for the anonymous instance.
Returns boolean — True when that instance is on the entity.
if not EntityRecords.carriesInstance(id, "Pool", "mana") then entity(id).component.add("Pool", { name = "mana" }) end
typed/builtin//modules/entity_records/M/componentIsCodeAttached
M.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
ridstring— Runtime entity id carrying the instance.componentTypestring— Component type name as the entity reports it.
Returns boolean — True when a component's lifecycle attached it.
if EntityRecords.componentIsCodeAttached(id, "Humanoid") then continue end
typed/builtin//modules/entity_records/M/compose
M.compose(rootId: string, rank: { [string]: number }?, skip: { [string]: boolean }?) -> { any }
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.
Parameters
rootIdstring— Runtime entity id to walk from.rank{ [string]: number }(optional) — Optional map of entity id → integer. Each node's children are visited in ascending rank, so a caller holding the order its entities were created in gets that order back. Omitted, the walk orders siblings by name and then by id.skip{ [string]: boolean }(optional) — Optional set of entity ids, keyed by id. An id it names is left out along with everything under it — what another owner produces and puts back itself, which a record here would describe a second time.
Returns { any } — Flat array of records, root first, parents before children.
local records = EntityRecords.compose(rootId)
typed/builtin//modules/entity_records/M/composeMany
M.composeMany(rootIds: { string }, rank: { [string]: number }?, skip: { [string]: boolean }?) -> { any }
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.
Parameters
rootIds{ string }— Array of runtime entity ids.rank{ [string]: number }(optional) — Optional map of entity id → integer, applied to the roots and to every node's children alike.skip{ [string]: boolean }(optional) — Optional set of entity ids, keyed by id, pruned wherever the walk reaches one — same rule ascompose.
Returns { any } — Flat array of records covering every root's hierarchy.
local records = EntityRecords.composeMany({ idA, idB })
typed/builtin//modules/entity_records/M/configuredComponentData
M.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
refany(optional) — The live component instance (a component proxy).dataany(optional) — Snapshot of the instance's data.
Returns any — The snapshot without the fields nothing configured.
local ComponentSnapshot = require("@builtin::modules.component_snapshot")
local d = EntityRecords.configuredComponentData(ref, ComponentSnapshot.snapshot(ref))
typed/builtin//modules/entity_records/M/configuredFields
M.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
refany(optional) — The live component instance (a component proxy).
Returns { [string]: boolean }? — Set of configured field names, or nil.
local set = EntityRecords.configuredFields(entity(id).component("Light"))
typed/builtin//modules/entity_records/M/instanceNameOf
M.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
entryany(optional) — One entry of a record'scomponentslist.
Returns string? — The instance name, or nil when the entry names the anonymous instance.
local name = EntityRecords.instanceNameOf({ type = "Pool", instance_name = "mana" }) -- "mana"
typed/builtin//modules/entity_records/M/instancesOf
M.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
ridstring— Runtime entity id.typeNamestring(optional) — Component type to narrow to, or nil for every instance.
Returns { ComponentInstance } — The entity's component instances in the order it reports them.
for _, inst in ipairs(EntityRecords.instancesOf(id, "Pool")) do print(inst.type, inst.instance) end
typed/builtin//modules/entity_reflect/E/allTypes
E.allTypes() -> { string }
List all registered reflectable component types in this engine instance.
Returns { string } — Array of component name strings.
local types = Entity.allTypes()
typed/builtin//modules/entity_reflect/E/distance
E.distance(entityIdA: string, entityIdB: string) -> number?
Compute the straight-line distance between two entities' Transform.position fields.
Parameters
entityIdAstring— First entity id.entityIdBstring— Second entity id.
Returns number? — The distance, or nil when either entity is missing a position.
local d = Entity.distance("player", "enemy")
typed/builtin//modules/entity_reflect/E/getComponents
E.getComponents(entityId: string) -> { string }?
List all reflected component types on an entity.
Parameters
entityIdstring— The entity id.
Returns { string }? — Array of component name strings, or nil when the entity has no reflected components.
local comps = Entity.getComponents("player")
typed/builtin//modules/entity_reflect/E/getField
E.getField(entityId: string, component: string, field: string) -> any
Get a specific component field value. An engine-struct component reads through reflection; a component declared in Luau reads through its component ref, so one call serves both.
Parameters
entityIdstring— The entity id.componentstring— The component name (e.g."Transform","Model").fieldstring— The field name (e.g."position","tintBlend").
Returns any — The field value, or nil when the entity/component/field is missing. A component name this engine does not declare raises.
local pos = Entity.getField("player", "Transform", "position")
typed/builtin//modules/entity_reflect/E/getName
E.getName(entityId: string) -> string?
Get the name of an entity from its Name component.
Parameters
entityIdstring— The entity id.
Returns string? — The name string, or nil when the entity has no Name component.
local name = Entity.getName("player")
typed/builtin//modules/entity_reflect/E/getPosition
E.getPosition(entityId: string) -> Vec3?
Get the position of an entity — shortcut for getField(id, "Transform", "position").
Parameters
entityIdstring— The entity id.
Returns Vec3? — The position { x, y, z }, or nil when the entity has no Transform.
local pos = Entity.getPosition("player")
typed/builtin//modules/entity_reflect/E/getRotation
E.getRotation(entityId: string) -> Quat?
Get the rotation of an entity — shortcut for getField(id, "Transform", "rotation").
Parameters
entityIdstring— The entity id.
Returns Quat? — The rotation quaternion { x, y, z, w }, or nil when the entity has no Transform.
local rot = Entity.getRotation("player")
typed/builtin//modules/entity_reflect/E/getScale
E.getScale(entityId: string) -> Vec3?
Get the scale of an entity — shortcut for getField(id, "Transform", "scale").
Parameters
entityIdstring— The entity id.
Returns Vec3? — The scale { x, y, z }, or nil when the entity has no Transform.
local scale = Entity.getScale("player")
typed/builtin//modules/entity_reflect/E/getSchema
E.getSchema(componentName: string) -> { ComponentFieldSchema }?
Get the full schema of a component type — field names + types.
Parameters
componentNamestring— The component name.
Returns { ComponentFieldSchema }? — Array of { name, type } schema entries, or nil when the component is not registered.
local schema = Entity.getSchema("Transform")
typed/builtin//modules/entity_reflect/E/isVisible
E.isVisible(entityId: string) -> boolean
Check if an entity is visible — reads the Visible component. Missing component is treated as visible.
Parameters
entityIdstring— The entity id.
Returns boolean — true when visible (or no Visible component is present), false when explicitly hidden.
local visible = Entity.isVisible("player")
typed/builtin//modules/entity_reflect/E/patch
E.patch(entityId: string, component: string, fields: { [string]: any }) -> boolean
Patch multiple fields on a single component at once. Serves an engine-struct component and a component declared in Luau alike.
Parameters
entityIdstring— The entity id.componentstring— The component name.fields{ [string]: any }—{ fieldName = value, ... }map of fields to write.
Returns boolean — true when every named field was written. A component name this engine does not declare raises.
Entity.patch("player", "Transform", { position = pos, scale = scl })
typed/builtin//modules/entity_reflect/E/setField
E.setField(entityId: string, component: string, field: string, value: any?) -> boolean
Set a specific component field value. An engine-struct component writes through reflection; a component declared in Luau writes through its component ref, so one call serves both.
Parameters
entityIdstring— The entity id.componentstring— The component name.fieldstring— The field name.valueany(optional) — The new value.
Returns boolean — true when the write succeeded, false otherwise. A component name this engine does not declare raises.
Entity.setField("player", "Transform", "position", { x = 0, y = 1, z = 0 })
typed/builtin//modules/entity_reflect/E/setPosition
E.setPosition(entityId: string, pos: Vec3)
Set the position of an entity — shortcut for setField(id, "Transform", "position", pos).
Parameters
entityIdstring— The entity id.posVec3— The new position{ x, y, z }.
Entity.setPosition("player", { x = 0, y = 1, z = 0 })
typed/builtin//modules/entity_reflect/E/setScale
E.setScale(entityId: string, scale: Vec3)
Set the scale of an entity — shortcut for setField(id, "Transform", "scale", scale).
Parameters
entityIdstring— The entity id.scaleVec3— The new scale{ x, y, z }.
Entity.setScale("player", { x = 1, y = 1, z = 1 })
typed/builtin//modules/entity_reflect/E/snapshot
E.snapshot(entityId: string) -> any
Snapshot all reflected components on an entity into a { ComponentName = { field = value, ... }, ... } map.
Parameters
entityIdstring— The entity id.
Returns any — The snapshot table, or nil when the entity does not exist.
local snap = Entity.snapshot("player")
typed/builtin//modules/entity_signals/M/entityKeyed
M.entityKeyed(records: { [string]: any }) -> { [string]: any }
Make records a table keyed by entity id that keeps only entities
in the world: when an entity is despawned, whatever despawned it (an
entity.despawn, a parent's cascade, a scene rebuild or reload, a mode
flip), its id leaves records. An id a newer entity carries by then stays.
A module holding per-entity state that something reads every frame
declares its table this way, so the reader never meets an entity that is
gone. The table is one of the engine's own records, so a module-state
restore leaves it as it stands and an id that left it stays out.
Returns records.
Parameters
records{ [string]: any }— The table to maintain, keyed by entity id.
Returns { [string]: any } — The same table.
local entries = EntitySignals.entityKeyed({})
typed/builtin//modules/entity_signals/M/get
M.get(entityId: string, phase: string) -> any
Return the onDestroying/onDestroyed signal for an entity,
creating it on first access. phase is "destroying" or "destroyed".
Parameters
entityIdstring— Entity whose signal to fetch.phasestring— Either "destroying" or "destroyed".
Returns any
typed/builtin//modules/field/Field/alias
Field.alias(target: string | { string }, description: string?) -> FieldDesc<any>
Alias for one or more existing fields. An alias is an ACCEPTED key that is not stored itself — it routes the written value to the real field(s) it points at, so a component answers to a caller's natural key without hand-rolling translation code, and both the runtime and the LSP recognise the key.
The key works everywhere the fields it targets do: as a
component.add init key, and as a read and a write on the live
component. Reading it returns what the target(s) hold right now.
Two forms:
Field.alias("radius")— rename. The value is written verbatim to the single target field (running that field's normal coercion, so an alias onto an assetRef field resolves the ref), and reads back as that field's value.Field.alias({ "colorR", "colorG", "colorB" })— fan-out. The value is destructured across the targets: an array{a, b, c}positionally, or a named{r=, g=, b=}/{x=, y=, z=}table by the target's position (r/x, g/y, b/z, a/w). It reads back as an array in target order, soc.color = c.colorround-trips.
An alias never replicates and is never persisted — the fields it targets own their own Sync/NoSync, so no mode argument is taken.
Parameters
targetstring | { string }— A single target field name, or an array of target field names.descriptionstring(optional) — Documents the alias for the LSP; nil to leave it undocumented.
Returns FieldDesc<any> — FieldDesc descriptor with kind = "alias".
type = Field.alias("kind")
color = Field.alias({ "colorR", "colorG", "colorB" })
typed/builtin//modules/field/Field/assetRef
Field.assetRef(category: (C & string) | { string }, default: AssetRef<C> | I | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<C, I>>
Typed asset reference field. C is the asset category — a
singleton string type inferred from the category argument
("material", "mesh", "@user/customCategory", etc.). One
constructor handles every category, including user-registered ones.
Default accepts a resolved AssetRef<C> handle, an identity string
(full @library::path form OR a bare leaf name resolved
category-locally via asset.resolve(identity, category)), or nil.
At registration the engine resolves any string default through the
same category-aware resolver public_newindex uses for runtime
writes, so the first read of public.<field> already returns a
resolved envelope — not a raw string.
Parameters
category(C & string) | { string }— Asset category as a string literal ("material","mesh", etc.),"*"for any category, or a list of categories the field takes any of ({ "importer", "component" }). A string is inferred intoC.defaultAssetRef<C> | I | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef<C, I>> descriptor.
material = Field.assetRef("material", "@my-library::materials.gold", Sync)
source = Field.assetRef("bundle", nil, Sync)
declarer = Field.assetRef({ "importer", "component" }, nil, NoSync)
typed/builtin//modules/field/Field/bitmask
Field.bitmask(bits: number, default: number?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<number>
Bit-mask number field. bits declares the width the consumer can
address; a default or a write that is not a whole number in
0 .. 2^bits - 1 is rejected, naming the width. Use it wherever a numeric
field is read as a set of bits rather than as a quantity — what the field
reads back is then a mask, so a read-back is evidence the value took.
Parameters
bitsnumber— How many bits wide the mask is, 1..53.defaultnumber(optional) — Numeric default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDescbitmask constraint.
lightChannels = Field.bitmask(32, 0, Sync)
typed/builtin//modules/field/Field/bool
Field.bool(default: boolean?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<boolean>
Boolean field. nil leaves the field unset.
Parameters
defaultboolean(optional) — Boolean default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
enabled = Field.bool(true, Sync)
typed/builtin//modules/field/Field/check
Field.check(desc: FieldDesc<any>, value: any?) -> (boolean, { { path: string, message: string } })
Check one value against one Field descriptor: its base kind, its constraint, and every nested subfield or element. Returns whether the value is accepted plus every violation found, each naming the path it sits at and what that field takes. A nil value is accepted — a field the values do not set takes its declared default.
Parameters
descFieldDesc<any>— The Field descriptor to check against.valueany(optional) — The value to check.
Returns (boolean, { { path: string, message: string } }) — (ok, violations) — violations is empty when ok.
local ok, violations = Field.check(Field.number(0, NoSync), "x")
typed/builtin//modules/field/Field/checkSchema
Field.checkSchema(schema: FieldSchema, values: { [string]: any }) -> (boolean, { { path: string, message: string } })
Check a table of values against a schema of Field descriptors. A key the schema does not declare is a violation naming every declared field.
Parameters
schemaFieldSchema— Map of field name to Field descriptor.values{ [string]: any }— The values to check.
Returns (boolean, { { path: string, message: string } }) — (ok, violations) — violations is empty when ok.
local ok, violations = Field.checkSchema(schema, { filter = "nearest" })
typed/builtin//modules/field/Field/color
Field.color(default: color?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<color>
Color field. Default is a color — either
{r = .., g = .., b = .., a = ..?} or {r, g, b, a?}.
Parameters
defaultcolor(optional) — color default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
tint = Field.color({ 1, 1, 1, 1 }, Sync)
typed/builtin//modules/field/Field/componentRef
Field.componentRef(componentType: T & string, default: ComponentRef<T> | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<ComponentRef<T>>
Typed component reference field. T is the component-type
name — a singleton string type inferred from the componentType
argument ("Camera", "Transform", "@user/Inventory"). The
engine validates the referent exists and is of the declared type
at every write.
Parameters
componentTypeT & string— Component type name as a string literal. Inferred intoT.defaultComponentRef<T> | nil(optional) — ComponentRef envelope or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<ComponentRef
aimCam = Field.componentRef("Camera", nil, NoSync)
typed/builtin//modules/field/Field/dataRef
Field.dataRef(contract: C & string, default: AssetRef<"data"> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<"data">>
Contract-constrained typed-data reference field. Accepts only
.data assets whose dataType contract chain includes contract.
Rides the assetRef machinery (category "data") — dependency graph,
sync, and rehydration behave exactly like Field.assetRef — with the
contract gate enforced through the generic field-constraint hook on
every write and on the registration-time default.
Parameters
contractC & string— The required dataType contract identity. Inferred intoC.defaultAssetRef<"data"> | string | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef<"data">> descriptor.
weapon = Field.dataRef("weapon", nil, Sync)
typed/builtin//modules/field/Field/entityRef
Field.entityRef(default: EntityRef | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<EntityRef>
Entity reference field. Accepts a live entity proxy (EntityRef),
a raw entity-id string, or nil (no target). Writes are normalised to
the plain id string for storage/replication; reads return a live
EntityRef proxy (or nil), so public.<field>:method() and
public.<field>.id work directly without re-resolving. As a
Field.struct subfield or a Field.list element it stores the same
id string and reads back as that id string.
Parameters
defaultEntityRef | string | nil(optional) — Live entity proxy, entity-id string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
target = Field.entityRef(nil, Sync)
typed/builtin//modules/field/Field/enum
Field.enum(values: { string }, default: string?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<string>
Closed-set string field. values declares every member; a default or a
write outside the set is rejected with the whole set named. The editor
renders the members as a choice and the LSP completes them.
Parameters
values{ string }— The members, as an array of non-empty, distinct strings.defaultstring(optional) — The default member, or nil to leave the field unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDescenum constraint.
fit = Field.enum({ "exact", "hull" }, "hull", Sync)
typed/builtin//modules/field/Field/instantiableRef
Field.instantiableRef(default: AssetRef<any> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<any>>
Scene-instantiable asset reference field — accepts ANY asset whose
type can be instantiated into a scene, gated by CAPABILITY rather than a
hardcoded type list. Rides the assetRef machinery with no category filter
(any asset type resolves), and the generic field-constraint hook rejects,
on every write and on the registration-time default, any asset whose type
defines no instantiate method (ref:canInstantiate() is false). A
new scene-instantiable asset type is accepted here the moment it defines
the hook — no edit to this field or its consumers. The uniform
ref:instantiate(target?, opts?) is how a consumer then instantiates the
assigned asset (Asset.component, a viewport drop, a tool argument).
Parameters
defaultAssetRef<any> | string | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef
source = Field.instantiableRef(nil, Sync)
typed/builtin//modules/field/Field/list
Field.list(element: FieldDesc<any>, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<ListValue>
List field — an array of one repeated element type. The element is the Field constructor descriptor every item conforms to, often a Field.struct for a list of records. The list value is an array of the element's value type. Like Field.struct, the engine descends the element schema to resolve nested asset refs into envelopes, so a stack of structs each holding an asset ref has every ref appear in the asset dependency graph, validates each item, and the LSP type-checks the array. An entity reference element is stored as the entity's id string, whether a proxy or an id was written, so the list saves into a scene and replicates. The default value is an empty list. The element declares its own Sync or NoSync for typing; the list's own mode governs replication of the whole array as a unit.
Parameters
elementFieldDesc<any>— The Field constructor descriptor each item conforms to.modeSyncMode— Sync or NoSync — required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<ListValue> — FieldDesc whose value is an array of the element's values.
typed/builtin//modules/field/Field/number
Field.number(default: number?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<number>
Number field. nil leaves the field unset, so a component can treat an
absent value as "derive this from somewhere else" without a second field
recording whether the first one was authored.
Parameters
defaultnumber(optional) — Numeric default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
positionX = Field.number(0, Sync)
typed/builtin//modules/field/Field/quat
Field.quat(default: quat?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<quat>
Quaternion field. Default is a quat — either
{x = .., y = .., z = .., w = ..} or {x, y, z, w}.
Parameters
defaultquat(optional) — quat default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
rotation = Field.quat({ 0, 0, 0, 1 }, Sync)
typed/builtin//modules/field/Field/range
Field.range(min: number?, max: number?, default: number?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<number>
Bounded number field. min and max declare the interval the value
means something in; a default or a write outside it is rejected with the
interval named. Either bound may be nil, leaving that side open. The value
the field reads back is one the system consuming it can use, and a number
that lands outside is reported where it was written.
Parameters
minnumber(optional) — Lowest accepted value, or nil to leave the low side open.maxnumber(optional) — Highest accepted value, or nil to leave the high side open.defaultnumber(optional) — Numeric default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDescrange constraint.
volume = Field.range(0, 1, 1, Sync)
typed/builtin//modules/field/Field/resource
Field.resource(category: C & string, default: AssetRef<C> | Handle<C> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<C> | Handle<C>>
Category-gated RESOURCE field — accepts EITHER a persistent
AssetRef<C> OR a live GPU Handle<C>, gated by category. This is the
renderer-facing field type (e.g. Model.model, Model.material, material
texture slots): content can author a persistent asset OR pass a runtime
handle (renderer.<resource>.create(...)); the component bridges either to
the GPU resource. The category gate still holds — an AssetRef<audio> or a
wrong-category handle (a TextureHandle on a "mesh" slot) is a type error
AND a runtime rejection. Use Field.assetRef instead when the field MUST be
a persistent asset (handles rejected).
With Sync, persistent-asset values replicate to peers; a live GPU
handle value is local by construction and stays local — peers keep the
last replicated asset value.
Parameters
categoryC & string— Resource category string literal ("mesh","texture","material", ...). Inferred intoC.defaultAssetRef<C> | Handle<C> | string | nil(optional) —AssetRef<C>/Handle<C>/ identity string / nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef
model = Field.resource("mesh", nil, Sync)
typed/builtin//modules/field/Field/settingsOf
Field.settingsOf(declarer: string, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?, default: StructValue?) -> FieldDesc<StructValue>
A struct field whose schema is another declarer's settings schema — an assetType, an importer, a component. The declarer is named by its asset identity and resolved by guid, so a same-named type in another package is never mistaken for it. The subfields are the declarer's own, so a field embedding them never restates them.
Parameters
declarerstring— The declarer's asset identity, e.g. "@builtin::assetTypes.texture".modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.defaultStructValue(optional) — The group's declared default: a table of the declarer's fields, or nil for none.
Returns FieldDesc<StructValue> — FieldDesc whose value is a table of the declarer's settings.
textureSettings = Field.settingsOf("@builtin::assetTypes.texture", NoSync, "Stamped onto every texture.", { filter = "nearest" })
typed/builtin//modules/field/Field/string
Field.string(default: string?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<string>
String field. nil leaves the field unset.
Parameters
defaultstring(optional) — String default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
label = Field.string("hello", Sync)
typed/builtin//modules/field/Field/struct
Field.struct(schema: FieldSchema, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<StructValue>
Struct field — a table whose keys are themselves typed fields. The schema maps each subfield name to its Field constructor descriptor; the struct value is a table holding one value per subfield. Use this instead of Field.table when the table carries asset references or other typed data: the engine descends the schema to resolve nested asset refs into envelopes at registration and at write time, so they appear in the asset dependency graph, validates writes per subfield, and the LSP type-checks the shape. Each subfield declares its own Sync or NoSync for typing; the struct's own mode governs replication of the whole value as a unit.
Parameters
schemaFieldSchema— Map of subfield name to a Field constructor descriptor.modeSyncMode— Sync or NoSync — required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<StructValue> — FieldDesc whose value is a table of the subfields' values.
typed/builtin//modules/field/Field/table
Field.table(default: T, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<T>
Generic table field. T is the table's shape — usually
inferred from the default value, or supplied explicitly via
an explicit ascription Field.table({} :: MyShape, mode) when the default doesn't cover every
key the runtime will write. The engine accepts any Luau table as
a value at write time; per-shape enforcement is opt-in static
typing only.
Parameters
defaultT— Table value to use as the default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
idMap = Field.table({} :: { [string]: string }, NoSync)
typed/builtin//modules/field/Field/taggedRef
Field.taggedRef(tag: string, default: AssetRef<any> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<any>>
Tag-constrained asset reference field — accepts any asset carrying
tag in its .metadata.tags, whatever its type. This is how a slot
states the KIND of asset it takes (a camera behavior, a player visual)
without naming the assets themselves: a new asset becomes assignable the
moment it is tagged, with no edit here or in the consumer. Rides the
assetRef machinery with no category filter — dependency graph, sync and
rehydration behave exactly like Field.assetRef — and the generic
field-constraint hook rejects an untagged asset on every write and on the
registration-time default. asset.list({ fields = { tags = tag } })
enumerates what fits the slot.
Parameters
tagstring— The tag an assigned asset must carry.defaultAssetRef<any> | string | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef
behavior = Field.taggedRef("cameraBehavior", nil, Sync)
typed/builtin//modules/field/Field/vec2
Field.vec2(default: vec2?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<vec2>
Parameters
defaultvec2(optional)modeSyncModemarker(SerializedMode | string | FieldOptions)(optional)
Returns FieldDesc<vec2>
typed/builtin//modules/field/Field/vec3
Field.vec3(default: vec3?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<vec3>
Vec3 field. Default is a vec3 — either {x = .., y = .., z = ..}
or the 3-element array form {x, y, z}.
Parameters
defaultvec3(optional) — vec3 default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
offset = Field.vec3({ 0, 0, 0 }, Sync)
typed/builtin//modules/frame_bounds/F/cameraAxes
F.cameraAxes(dx: number, dy: number, dz: number) -> CameraAxes
The camera's own forward / right / up for a view along an orbit direction, with right and up taken from the world up.
Parameters
dxnumber— Orbit direction X (subject toward camera, unit length).dynumber— Orbit direction Y.dznumber— Orbit direction Z.
Returns CameraAxes — { forward = {x,y,z}, right = {x,y,z}, up = {x,y,z} }.
local axes = FrameBounds.cameraAxes(0, 0.34, 0.94)
typed/builtin//modules/frame_bounds/F/fit
F.fit(box: Aabb, opts: FitOpts?) -> Framed
Solve for a camera pose that frames box at the given orbit angle.
Parameters
boxAabb— The AABB to frame.optsFitOpts(optional) —{ fov, aspect, margin, angle = { yawDeg, pitchDeg } }.
Returns Framed — { px, py, pz, cx, cy, cz, distance, radius, fov, near, far, center, size }.
local f = FrameBounds.fit(b, { fov = 60, angle = { 0, 20 } })
typed/builtin//modules/frame_bounds/F/fitDistance
F.fitDistance(hx: number, hy: number, hz: number, dx: number, dy: number, dz: number, tanH: number, tanV: number) -> number
Smallest distance along the orbit direction that keeps all eight corners of a half-extent box inside the frustum.
Parameters
hxnumber— Half-extent on X.hynumber— Half-extent on Y.hznumber— Half-extent on Z.dxnumber— Orbit direction X (subject toward camera, unit length).dynumber— Orbit direction Y.dznumber— Orbit direction Z.tanHnumber— Tangent of the half horizontal FOV.tanVnumber— Tangent of the half vertical FOV.
Returns number — The fitting distance.
local d = FrameBounds.fitDistance(1, 1, 1, 0, 0.34, 0.94, 1.03, 0.58)
typed/builtin//modules/frame_bounds/F/fitDistanceAxes
F.fitDistanceAxes(hx: number, hy: number, hz: number, axes: CameraAxes, tanH: number, tanV: number) -> number
Smallest distance that keeps all eight corners of a half-extent box
inside the frustum, projected onto explicitly-given camera axes. Callers
that aim by a named station rather than an orbit angle pass their own axes;
fitDistance derives them from a direction and calls this.
Parameters
hxnumber— Half-extent on the first extent axis.hynumber— Half-extent on the second.hznumber— Half-extent on the third.axesCameraAxes—{ forward, right, up }, in the SAME frame the half-extents are measured in.tanHnumber— Tangent of the half horizontal FOV.tanVnumber— Tangent of the half vertical FOV.
Returns number — The fitting distance, before any margin.
local d = FrameBounds.fitDistanceAxes(7, 0.25, 7, axes, 1.03, 0.58)
typed/builtin//modules/frame_bounds/F/ofEntity
F.ofEntity(target: any?) -> Aabb?
World-space bounds of an entity and its descendants, falling back to the entity's own mesh bounds.
Parameters
targetany(optional) — An entity proxy.
Returns Aabb? — AABB or nil when the target has no renderable geometry.
local b = FrameBounds.ofEntity(entity.find("player"))
typed/builtin//modules/frame_bounds/F/union
F.union(boxes: { Aabb }) -> Aabb?
Union a list of AABBs into one.
Parameters
boxes{ Aabb }— Array of{ min = vec3, max = vec3 }.
Returns Aabb? — The enclosing AABB, or nil when the list is empty.
local u = FrameBounds.union({ a:hierarchyBounds(), b:hierarchyBounds() })
typed/builtin//modules/highlight/M/dispatch
M.dispatch(language: string, text: string) -> { Segment }
Dispatch a highlighter by language name, with yml and md
aliases matching the engine's pre-Phase-3 parse_editor_language.
Unknown languages return a single default-colored segment so callers
can pass arbitrary user input without crashing.
Parameters
languagestring— Language name ("lua","json","yaml","wgsl","markdown", or aliases"yml"/"md").textstring— Source text to tokenize.
Returns { Segment } — An array of Segment records.
local segs = Z.highlight.dispatch("lua", source)
local segs = Z.highlight.dispatch("md", readme)
typed/builtin//modules/highlight/M/languageOf
M.languageOf(path: string) -> string?
The language a file at path is coloured as, read from its
extension (what follows the last dot that has a name before it, so a
dotfile such as .zmignore has none), or nil for text shown uncoloured.
Parameters
pathstring— A file path or name.
Returns string? — A language name dispatch takes, or nil.
Z.highlight.languageOf("/zero/source/a/init.luau") -- "lua"
typed/builtin//modules/highlight/json/highlightJson
highlightJson(text: string) -> { Segment }
Tokenize a JSON string into colored text segments for the zui
code renderer. Distinguishes object keys (via lookahead for :)
from string values, colors numbers (including scientific notation),
bool/null literals, and structural punctuation.
Parameters
textstring— The JSON source to highlight. Coerced viatostringand defaults to""when nil.
Returns { Segment } — An array of { text, color, monospace } segments suitable for the zui code renderer.
local segments = highlight('{"a": 1, "b": "two"}')
typed/builtin//modules/highlight/lua/highlightLua
highlightLua(text: string) -> { Segment }
Tokenize a Luau source string into colored text segments for the zui code renderer. One walk over the bytes: comments, directives, strings of every shape, numbers, and identifiers read in context — a keyword, a literal, a builtin, a called function, a member, a declared function name, or a type.
Parameters
textstring— The Luau source to highlight. Coerced viatostringand defaults to""when nil.
Returns { Segment } — An array of { text, color, monospace } segments whose texts concatenate back to the source byte for byte.
local segments = highlight("local x = 1 -- pi-ish")
typed/builtin//modules/highlight/markdown/highlightMarkdown
highlightMarkdown(text: string) -> { Segment }
Tokenize a Markdown source string into colored text segments for the zui code renderer. Block-level pass recognises fenced code (```), headings (#), blockquotes (>), bullet and ordered lists; inline pass within each non-block line recognises inline code, bold, and links.
Parameters
textstring— The Markdown source to highlight. Coerced viatostringand defaults to""when nil.
Returns { Segment } — An array of { text, color, monospace } segments suitable for the zui code renderer.
local segments = highlight("# Title\n**bold**\n")
typed/builtin//modules/highlight/wgsl/highlightWgsl
highlightWgsl(text: string) -> { Segment }
Tokenize a WGSL source string into colored text segments for the
zui code renderer. Handles // and /* */ comments, "..."
strings with backslash escapes, @attribute tokens, numeric
literals with suffixes (1u, 0xFF, 1.0_f32), and identifiers
dispatched against the WGSL keyword and built-in type tables.
Parameters
textstring— The WGSL source to highlight. Coerced viatostringand defaults to""when nil.
Returns { Segment } — An array of { text, color, monospace } segments suitable for the zui code renderer.
local segments = highlight("@vertex fn main() -> vec4<f32> {}")
typed/builtin//modules/highlight/yaml/highlightYaml
highlightYaml(text: string) -> { Segment }
Tokenize a YAML source string into colored text segments for
the zui code renderer. Splits each line on a # comment first,
then on the first : to extract a key from its value; the value
is then classified as quoted string / bool / null / numeric / plain.
Parameters
textstring— The YAML source to highlight. Coerced viatostringand defaults to""when nil.
Returns { Segment } — An array of { text, color, monospace } segments suitable for the zui code renderer.
local segments = highlight("name: zero\n# comment\n")
typed/builtin//modules/json/Json/decode
Json.decode(str: string) -> any
Decode a JSON string to a Lua value. Returns the decoded value, or nil + error message on failure.
Parameters
strstring— The JSON string to decode.
Returns any — The decoded Lua value. On failure, returns nil (and the error message in the second return value).
local v = Json.decode('{"a":1,"b":"hi"}') -- → { a = 1, b = "hi" }
local v, err = Json.decode("bad") -- → nil, "Invalid literal..."
typed/builtin//modules/json/Json/encode
Json.encode(value: any?, indent: string?, currentIndent: string?) -> string
Encode a Lua value to a compact JSON string. Functions and unknown types serialise to null; NaN/Inf serialise to null (JSON has no representation for them). A table that contains itself raises an error naming where the walk reached it again.
Parameters
valueany(optional) — Any Lua value (nil, boolean, number, string, table).indentstring(optional) — Optional indent string. Reserved: the compact encoder ignores it; useencodePrettyfor indented output.currentIndentstring(optional) — Optional current-depth indent string. Reserved.
Returns string — The encoded JSON string.
local s = Json.encode({ type = "button", text = "Click Me" })
typed/builtin//modules/json/Json/encodeArgs
Json.encodeArgs(...: any?) -> string
Encode a list of arguments as a JSON array string. Useful when forwarding varargs to a JSON-based bridge.
Parameters
...any(optional) — Any number of values to encode.
Returns string — The encoded JSON array string.
local s = Json.encodeArgs("foo", 1, true) -- '["foo",1,true]'
typed/builtin//modules/json/Json/encodePretty
Json.encodePretty(value: any?, indentStr: string?) -> string
Encode a Lua value to a pretty-printed JSON string. Indents nested values and sorts object keys for diff-friendly output. A table that contains itself raises an error naming where the walk reached it again.
Parameters
valueany(optional) — Any Lua value.indentStrstring(optional) — Indent string per level (default" ").
Returns string — The pretty-printed JSON string.
local s = Json.encodePretty({ a = 1, b = { c = 2 } })
typed/builtin//modules/json_utils/M/decodeOrEmptyTable
M.decodeOrEmptyTable(jsonStr: string?) -> { [any]: any }
Decode a JSON string safely, returning an empty table on any failure (instead of nil). Returns the table when the decode succeeds AND the result is a table; otherwise {}.
Parameters
jsonStrstring(optional) — The JSON input.
Returns { [any]: any } — A table — never nil.
local t = JsonUtils.decodeOrEmptyTable(engineJson)
typed/builtin//modules/json_utils/M/decodeOrNil
M.decodeOrNil(jsonStr: string?) -> any
Decode a JSON string safely. Returns the decoded value or nil on any failure (empty input, literal "null", parse error).
Parameters
jsonStrstring(optional) — The JSON input.nil, empty, or"null"short-circuit tonil.
Returns any — The decoded value, or nil.
local v = JsonUtils.decodeOrNil('{"a":1}') -- → { a = 1 }
local v = JsonUtils.decodeOrNil("null") -- → nil
typed/builtin//modules/json_utils/M/formatNumber
M.formatNumber(n: number, decimals: number?) -> string
Truncate a number to N decimal places using only string operations. WASM-safe — does NOT use string.format numeric specifiers.
Parameters
nnumber— The number to format.decimalsnumber(optional) — Decimal places to keep (default 2). Use 0 to drop the fractional part entirely; a count below 0 reads the same as 0, and a fractional count counts the whole places in it.
Returns string — The truncated number rendered as a string.
local label = JsonUtils.formatNumber(3.14159, 2) -- "3.14"
local whole = JsonUtils.formatNumber(3.14159, 0) -- "3"
typed/builtin//modules/json_utils/M/safeDecode
M.safeDecode(jsonStr: string?) -> any
Historical alias for decodeOrNil. Kept for back-compat with callers that used the older name.
Parameters
jsonStrstring(optional) — The JSON input.
Returns any — The decoded value, or nil.
local v = JsonUtils.safeDecode(jsonStr)
typed/builtin//modules/luau_introspect/M/docstrings
M.docstrings(src: string) -> DocMap
Scans src for --!desc / --!arg / --!return / --!example
/ --!deprecated / --!hidden doc-comment runs and binds each run to
the name of the declaration on the next code line (the identifier after
function, public:, local function, M., public.<name>, or a
<name> = Field.<...> /
<name> = Event(...) field/event declaration). A continuation line
("--! " with no tag word) extends whichever field the run's last tag
opened. Blank and plain -- comment lines between the run and the
declaration are skipped without terminating the run.
Parameters
srcstring— Luau/asset source text.
Returns DocMap — Map of declaration name to its parsed doc entry.
local d = I.docstrings(src); print(d.takeDamage.desc)
typed/builtin//modules/luau_introspect/M/events
M.events(src: string) -> { EventEntry }
Parses events = { name = Event(payloadSchema?, syncMode?), ... }
table-literal entries. The payload schema — a { k = Field.<kind>(...) }
table — is parsed like publicFields and reduced to {name, type}
pairs; a payloadless Event() yields an empty payload. sync is true
only when the SECOND positional Event argument is the literal
identifier Sync. Comments and string literals never register. Folds in
desc from docstrings(src).
Parameters
srcstring— Luau/asset source text.
Returns { EventEntry } — Ordered array of event entries, source order preserved.
local e = I.events(src); print(e[1].name, e[1].sync)
typed/builtin//modules/luau_introspect/M/forSource
M.forSource(src: string, computeFn: (string) -> any) -> any
Memoises computeFn(src) keyed on src itself in a bounded
module-local table. The same source text returns the cached result
without re-invoking computeFn; different source text recomputes. The
cache is capped (oldest key evicted first), so an evicted key
recomputes on its next request.
Parameters
srcstring— Source text — both the memo key and the argument passed tocomputeFn.computeFn(string) -> any— Called ascomputeFn(src)on a cache miss.
Returns any — The (possibly cached) result of computeFn(src).
local detail = I.forSource(src, buildDetail)
typed/builtin//modules/luau_introspect/M/lifecycleHooks
M.lifecycleHooks(src: string, catalog: { string }) -> { string }
Finds top-level function <name>( declarations whose name is in
the caller-supplied catalog. Excludes local function, public:
methods, and M. exports — only a bare top-level function NAME(
declaration matches. Never hardcodes the lifecycle-callback catalog;
the caller passes the list of names to match against. Commented-out
declarations never match.
Parameters
srcstring— Luau/asset source text.catalog{ string }— Array of lifecycle-callback names to match against.
Returns { string } — Array of matched hook names, source order preserved.
local hooks = I.lifecycleHooks(src, { "awake", "update" })
typed/builtin//modules/luau_introspect/M/maskNonCode
M.maskNonCode(src: string) -> string
A same-length copy of src where comment bodies and string-literal
contents are replaced by spaces (delimiters and newlines preserved),
so structural pattern matching over the result never registers a
comment or string body. Index-aligned with src — a match position in
the mask is the same position in src.
Parameters
srcstring— Luau/asset source text.
Returns string — The comment/string-masked copy.
local mask = I.maskNonCode(src); string.find(mask, "operations%s*=%s*{")
typed/builtin//modules/luau_introspect/M/methods
M.methods(src: string) -> { MethodEntry }
Parses function public:<name>(<params>) and typed function public:<name>(<params>) declarations — with an optional : <ret>
return-type annotation immediately after the closing paren — into
{name, params, returns}. Each param is split on top-level commas
into {name, type?}; a ... variadic is captured with name = "...".
Folds in desc from docstrings(src).
Parameters
srcstring— Luau/asset source text.
Returns { MethodEntry } — Ordered array of method entries, source order preserved.
local m = I.methods(src); print(m[1].name, m[1].returns)
typed/builtin//modules/luau_introspect/M/moduleExports
M.moduleExports(src: string) -> { ExportEntry }
Parses M.<name> = function(...) assignments, function M.<name>(...) declarations, and a trailing return { foo = foo, ... }
export table into {name, kind, signature?, desc?}. kind is
"function" for a function assignment/declaration, "value" for any
other M.<name> = <expr> assignment (a == comparison is not an
assignment). A name that appears only in the return { ... } table
infers its kind from whether a same-named function <name>( /
local function <name>( declaration exists. Folds in desc from
docstrings(src).
Parameters
srcstring— Luau/asset source text.
Returns { ExportEntry } — Array of export entries, in source-scan order.
local exports = I.moduleExports(src); print(exports[1].name)
typed/builtin//modules/luau_introspect/M/privateFields
M.privateFields(src: string) -> { FieldEntry }
Parses the private = { name = Field.<kind>(...), ... } table
literal the way publicFields parses public: each entry's kind, its
default and the arguments ahead of it, and its sync. A private field
a component marks Serialized is saved with the scene like a public
one, so a reader comparing saved state against declared defaults needs
both.
Parameters
srcstring— Luau/asset source text.
Returns { FieldEntry } — Ordered array of field entries, source order preserved.
local f = I.privateFields(src); print(f[1].name, f[1].default)
typed/builtin//modules/luau_introspect/M/publicFields
M.publicFields(src: string) -> { FieldEntry }
Parameters
srcstring
Returns { FieldEntry }
typed/builtin//modules/luau_introspect/M/refMethods
M.refMethods(src: string) -> { MethodEntry }
Parses the methods a behavior.luau-style module exposes on a
M.ref = { name = fn, ... } table — the assetType's per-asset behavior
surface (ref:method(...)). Each entry's key maps to its backing
local function <fn>(self, <params>): <ret> (or function <fn>(...))
declaration: the leading self parameter is dropped (it is the ref the
method is called on), the remaining params + return annotation + the
declaration's --!desc are captured, with hidden and deprecated
from its --!hidden / --!deprecated lines, so a listing can leave a
hidden method out while a type shape still carries it. A key whose
value is a table literal or an expression other than a bare function
name is skipped.
Runs over the comment/string-aware mask, so a commented M.ref never
registers.
Parameters
srcstring— Luau/asset source text.
Returns { MethodEntry } — Ordered array of method entries (source order of the M.ref keys).
local api = I.refMethods(behaviorSrc); print(api[1].name, api[1].returns)
typed/builtin//modules/luau_introspect/M/tableLiteral
M.tableLiteral(src: string, assignmentName: string) -> TableNode?
Parses a named table literal assignmentName = { ... } into a nested
TableNode — each top-level key = value becomes an entry whose value is
either a scalar (the trimmed source text of a non-table value, quotes
included) or a nested table (TableNode) when the value is itself a
{ ... }. Runs over the comment/string-aware mask, so a key, {, or
} inside a comment or string never registers. Computed ([expr] =) and
positional entries are skipped. Returns nil when the assignment is absent.
Parameters
srcstring— Luau/asset source text.assignmentNamestring— The table's assignment name (a Lua pattern, e.g. "operations" or "M%.tokens").
Returns TableNode? — The parsed TableNode, or nil.
local ops = I.tableLiteral(src, "operations")
typed/builtin//modules/luau_introspect/M/tableLiteralKeys
M.tableLiteralKeys(src: string, assignmentName: string) -> { string }
Finds <assignmentName> = { ... } in src and returns the top-level
literal-identifier keys of that table (e.g. tableLiteralKeys(src, "operations") on operations = { generate = {...}, from_image = {...} } returns {"generate","from_image"}). Runs over the
comment/string-aware mask, so a -- comment or string literal
mentioning the assignment name never registers. Comment/string/nesting
inside a value never breaks the scan (balanced brace matching, same as
publicFields/events). A computed ["key"] entry is omitted — only
bare identifier keys are recognised. Returns {} when the assignment
is absent.
Parameters
srcstring— Luau/asset source text.assignmentNamestring— The table's assignment name (e.g. "operations").
Returns { string } — Array of top-level key names, source order preserved.
local ops = I.tableLiteralKeys(src, "operations")
typed/builtin//modules/material_utils/M/Apply
M.Apply(entityId: string, materialRef: MaterialRefOrName) -> boolean
PascalCase back-compat alias for apply.
Parameters
entityIdstring— Target entity id.materialRefMaterialRefOrName— Either an AssetRef envelope or a material-name string.
Returns boolean — True on success.
typed/builtin//modules/material_utils/M/Create
M.Create(name: string, opts_or_shader: MaterialOpts | string | nil?, props: MaterialOpts?) -> MaterialRef?
PascalCase back-compat alias for create. Accepts the legacy 3-arg form (name, shader_string, opts_table) by folding shader into opts, and the canonical 2-arg form (name, opts).
Parameters
namestring— Material name.opts_or_shaderMaterialOpts | string | nil(optional) — Either an options table OR a legacy shader string.propsMaterialOpts(optional) — Optional extra options table — only used whenopts_or_shaderis a shader string.
Returns MaterialRef? — The AssetRef envelope, or nil on failure.
Material.Create("gold", "pbr", { color = { 1, 0.85, 0.2 } }) -- legacy 3-arg
Material.Create("gold", { shader = "pbr", color = { 1, 0.85, 0.2 } }) -- canonical
Material.Create("neon", { shader = "pbr", emissive = { 0.1, 0.9, 1 }, emissive_intensity = 5 }) -- cyan glow at 5× intensity
typed/builtin//modules/material_utils/M/Exists
M.Exists(name: MaterialRefOrName) -> boolean
PascalCase back-compat alias for exists.
Parameters
nameMaterialRefOrName— AssetRef envelope or material-name string.
Returns boolean — True when the material is registered.
typed/builtin//modules/material_utils/M/GetProperty
M.GetProperty(materialName: MaterialRefOrName, propertyName: string) -> any
PascalCase back-compat alias for getProperty.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.propertyNamestring— Property name.
Returns any — The property value, or nil when not found.
typed/builtin//modules/material_utils/M/GetPropertyNames
M.GetPropertyNames(materialName: MaterialRefOrName) -> { string }?
PascalCase back-compat alias for getPropertyNames.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.
Returns { string }? — Array of property names, or nil when the material is not found.
typed/builtin//modules/material_utils/M/SetProperty
M.SetProperty(materialName: MaterialRefOrName, property: string, value: any?) -> any
PascalCase alias. Writes the property on the material ASSET by name/ref (a runtime change every entity using it takes; matRef:saveDefinition() writes it into mat.yaml) — distinct from M.setProperty, which targets the material on one entity's model.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.propertystring— Property name.valueany(optional) — New value.
Returns any — True on success.
Material.SetProperty("gold", "roughness", 0.1)
typed/builtin//modules/material_utils/M/SetTexture
M.SetTexture(materialName: MaterialRefOrName, slot: string, textureRef: string) -> boolean
PascalCase back-compat alias for setTexture.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.slotstring— Texture slot name ("albedo","normal", etc.).textureRefstring— Texture reference string.
Returns boolean — True on success.
typed/builtin//modules/material_utils/M/Update
M.Update(target: any?, props: { [string]: any }) -> number
PascalCase alias for update.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.props{ [string]: any }— Table of{ [propertyName] = value }pairs.
Returns number — Number of properties applied.
Material.Update("gold", { base_color = { 1, 0, 0 }, roughness = 0.15 })
typed/builtin//modules/material_utils/M/apply
M.apply(entity: any?, materialRef: MaterialRefOrName) -> boolean
Apply a material to an entity's Model / SkinnedModel component by setting its material field. Accepts either an AssetRef envelope (from Material.create) or a bare material-name string. Errors when the entity has no Model or SkinnedModel — a material only renders where there is a mesh.
Parameters
entityany(optional) — The entity to apply to — an entity proxy (recommended: a validated handle to a real entity), an entity-id string, or the display name the entity carries.materialRefMaterialRefOrName— Either an AssetRef envelope or a material-name string.
Returns boolean — True on success.
Material.apply(entityId, "gold")
local mat = assert(Material.create("gold", { color = { 1, 0.8, 0.3, 1 }, metallic = 1 })); Material.apply(entityId, mat)
typed/builtin//modules/material_utils/M/create
M.create(name: string, opts: MaterialOpts?) -> MaterialRef?
Create a named material in the MaterialRegistry. Returns the canonical AssetRef envelope ({ __ref, type="material", name, guid }) — pass directly to Material.apply, the Model / SkinnedModel material field, or any AssetRef<material> consumer.
Parameters
namestring— Material name. Must be a non-empty string.optsMaterialOpts(optional) — Optional material options.shader?,color?,roughness?,metallic?,emissive?(the glow colour),emissive_intensity?(the HDR multiplier over it),textures?.
Returns MaterialRef? — The AssetRef envelope on success, or nil on failure.
local gold = Material.create("gold", { color = { 1, 0.85, 0.2 }, metallic = 1 })
typed/builtin//modules/material_utils/M/exists
M.exists(name: MaterialRefOrName) -> boolean
Check whether a material exists in the registry. Accepts an AssetRef envelope or a bare material name.
Parameters
nameMaterialRefOrName— AssetRef envelope or material-name string.
Returns boolean — True when the material is registered.
if Material.exists("gold") then ... end
typed/builtin//modules/material_utils/M/getProperty
M.getProperty(materialName: MaterialRefOrName, propertyName: string) -> any
Read the current value of a material property.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.propertyNamestring— Property name ("roughness","metallic","base_color", etc.).
Returns any — The property value, or nil when not found.
local r = Material.getProperty("gold", "roughness")
typed/builtin//modules/material_utils/M/getPropertyNames
M.getPropertyNames(materialName: MaterialRefOrName) -> { string }?
List the property names exposed by a registered material.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.
Returns { string }? — Array of property names, or nil when the material is not found.
local props = Material.getPropertyNames("gold")
typed/builtin//modules/material_utils/M/setProperties
M.setProperties(target: any?, props: { [string]: any }) -> number
Set many material properties in one call. Addresses the target the same
way setProperty does: a material name / AssetRef writes the material ASSET
(affecting every entity using it), an entity proxy / entity-id / entity name writes the
material bound to that entity's Model / SkinnedModel. Each key resolves
against the shader's declared vocabulary, so the spellings create accepts
reach the same uniforms; a key the shader does not expose is skipped, which
lets one patch table serve materials built on different shaders.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.props{ [string]: any }— Table of{ [propertyName] = value }pairs.
Returns number — Number of properties applied.
Material.setProperties("gold", { roughness = 0.2, metallic = 0.9 })
Material.setProperties(entityId, { base_color = { 1, 0, 0 } })
typed/builtin//modules/material_utils/M/setProperty
M.setProperty(target: any?, property: string, value: any?)
Set a material property. Addresses the target the same way getProperty does: pass a material name / AssetRef to write the material ASSET (affecting every entity using it), or an entity — a proxy, an id, or a display name — to write the material bound to that entity's Model / SkinnedModel. Errors when an entity target has no Model or SkinnedModel.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.propertystring— Property name.valueany(optional) — New value (type depends on the property).
Material.setProperty("gold", "roughness", 0.4) -- by material name
Material.setProperty(entityId, "roughness", 0.4) -- by entity
typed/builtin//modules/material_utils/M/setTexture
M.setTexture(materialName: MaterialRefOrName, slot: string, textureRef: any?) -> boolean
Set a texture slot on a named material.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.slotstring— Texture slot name ("albedo","normal", etc.).textureRefany(optional) — Texture reference. Formats:"color:r,g,b,a","@builtin::textures.foo", or a render-output guid (camera target, video handle).
Returns boolean — True on success.
Material.setTexture("gold", "albedo", "@builtin::textures.gold")
typed/builtin//modules/material_utils/M/update
M.update(target: any?, props: { [string]: any }) -> number
Change an existing material from a property table — the counterpart to
create, taking the same table shape. Addresses its target and counts its
writes the way setProperties does.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.props{ [string]: any }— Table of{ [propertyName] = value }pairs.
Returns number — Number of properties applied.
Material.update("gold", { color = { 1, 0.85, 0.2 }, roughness = 0.15 })
typed/builtin//modules/numberSequence/M/deserialize
M.deserialize(data: any?) -> NumberSequenceObj
Rebuild a NumberSequence from a {kind = "NumberSequence", keypoints = {...}} payload produced by :serialize(). Used by scene save/load.
Parameters
dataany(optional) — The serialized payload.
Returns NumberSequenceObj — A fresh NumberSequenceObj with the deserialized keypoints.
local s = NumberSequence.deserialize(savedData)
typed/builtin//modules/numberSequence/M/new
M.new(...: any?) -> NumberSequenceObj
Construct a NumberSequence from one of three signatures: a constant value, a two-point lerp from v0 to v1, or a keypoints array of { time, value, envelope? } records. Envelope defaults to 0 when omitted. Up to 64 keypoints; the first must anchor at time = 0, the last at time = 1. NaN / Inf in time / value / envelope is rejected.
Parameters
...any(optional) —(v),(v0, v1), or({ {time, value, envelope?}, ... }).
Returns NumberSequenceObj — A NumberSequenceObj with :evaluate, :sample, :keypoints, :duration, :serialize, :destroy.
local fade = NumberSequence.new(1.0)
local fadeOut = NumberSequence.new(1.0, 0.0)
local size = NumberSequence.new({{time=0,value=0.5,envelope=0.1},{time=0.5,value=1.5},{time=1,value=0}})
typed/builtin//modules/number_range/M/new
M.new(min: number, max: number?) -> any
Construct a NumberRange. Pass one number for a constant range
(min == max); pass two for a uniform random range. Reversed
arguments are normalized to ascending order.
Parameters
minnumber— Lower bound.maxnumber(optional) — Upper bound; defaults tomin.
Returns any
local lifetime = NumberRange.new(1.0) -- always 1.0
local speed = NumberRange.new(0.5, 2.0) -- random
typed/builtin//modules/nx/gpu/M/fft1d
M.fft1d(opts: Fft1dOpts) -> boolean
1D complex FFT over a power-of-two-sized interleaved complex
GPU buffer. size is the number of complex samples (must be
a power of two ≥ 2). Pass the same input / output handle for
in-place.
Parameters
optsFft1dOpts—{ input, output?, size, inverse? }.input/outputare GPU buffer handles;outputdefaults toinput.inverse=trueruns the inverse transform (scaled by 1/N).
Returns boolean — true on success, false on validation failure (non-pow2 size, missing buffers).
M.fft1d({ input = specIn, output = specOut, size = 1024 })
typed/builtin//modules/nx/gpu/M/fft2d
M.fft2d(opts: Fft2dOpts) -> boolean
2D complex FFT over a power-of-two-sized row-major interleaved complex GPU buffer. Width and height must both be powers of two ≥ 2.
Parameters
optsFft2dOpts—{ input, output?, width, height, inverse? }.input/outputare GPU buffer handles;outputdefaults toinput.inverse=trueruns the inverse (scaled by 1/(width*height)).
Returns boolean — true on success, false on validation failure (non-pow2 dims, missing buffers).
M.fft2d({ input = imgIn, output = imgOut, width = 256, height = 256 })
typed/builtin//modules/nx/gpu/M/ifft1d
M.ifft1d(opts: Fft1dOpts) -> boolean
Convenience: forward 1D FFT with inverse=true — equivalent
to M.fft1d(opts) after stamping opts.inverse = true.
Parameters
optsFft1dOpts— Same asM.fft1d. Mutatesopts.inverse.
Returns boolean — true on success, false on validation failure.
M.ifft1d({ input = specIn, output = specOut, size = 1024 })
typed/builtin//modules/nx/gpu/M/ifft2d
M.ifft2d(opts: Fft2dOpts) -> boolean
Convenience: forward 2D FFT with inverse=true — equivalent
to M.fft2d(opts) after stamping opts.inverse = true.
Parameters
optsFft2dOpts— Same asM.fft2d. Mutatesopts.inverse.
Returns boolean — true on success, false on validation failure.
M.ifft2d({ input = imgIn, output = imgOut, width = 256, height = 256 })
typed/builtin//modules/nx/nx/add
nx.add(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] += x (x scalar) or b[i] += x[i] (x buffer).
Dispatches on type(x). For interleaved-stride writes use
nx.addStrided.
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.add(b, 1.5)
nx.add(dst, src)
typed/builtin//modules/nx/nx/addStrided
nx.addStrided(b: NxBuffer, scalar: number, stride: number, offset: number?) -> boolean
Strided add: buf[k * stride + offset] += scalar for every
valid k.
Parameters
bNxBuffer— Target buffer (mutated).scalarnumber— Per-element addend.stridenumber— Element stride.offsetnumber(optional) — Optional element offset (default 0).
Returns boolean — true on success, false on unknown handle.
nx.addStrided(buf, 1.0, 3, 1)
typed/builtin//modules/nx/nx/addStridedFrom
nx.addStridedFrom(dst: NxBuffer, src: NxBuffer, scale: number?, dst_stride: number, dst_off: number?, src_stride: number, src_off: number?) -> boolean
Strided BLAS-axpy from src into dst:
dst[k * dst_stride + dst_off] += scale * src[k * src_stride + src_off].
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.scalenumber(optional) — Optionalsrcscale factor (default 1.0).dst_stridenumber— Destination element stride.dst_offnumber(optional) — Optional destination offset (default 0).src_stridenumber— Source element stride.src_offnumber(optional) — Optional source offset (default 0).
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.addStridedFrom(dst, src, 1, 3, 0, 3, 0)
typed/builtin//modules/nx/nx/applyAbs
nx.applyAbs(b: NxBuffer) -> boolean
In-place b[i] = abs(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyAbs(b)
typed/builtin//modules/nx/nx/applyCeil
nx.applyCeil(b: NxBuffer) -> boolean
In-place b[i] = ceil(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyCeil(b)
typed/builtin//modules/nx/nx/applyCos
nx.applyCos(b: NxBuffer) -> boolean
In-place b[i] = cos(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyCos(b)
typed/builtin//modules/nx/nx/applyExp
nx.applyExp(b: NxBuffer) -> boolean
In-place b[i] = exp(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyExp(b)
typed/builtin//modules/nx/nx/applyFloor
nx.applyFloor(b: NxBuffer) -> boolean
In-place b[i] = floor(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyFloor(b)
typed/builtin//modules/nx/nx/applyFract
nx.applyFract(b: NxBuffer) -> boolean
In-place b[i] = fract(b[i]) (fractional part).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyFract(b)
typed/builtin//modules/nx/nx/applyLog
nx.applyLog(b: NxBuffer) -> boolean
In-place b[i] = ln(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyLog(b)
typed/builtin//modules/nx/nx/applyLog2
nx.applyLog2(b: NxBuffer) -> boolean
In-place b[i] = log2(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyLog2(b)
typed/builtin//modules/nx/nx/applyNeg
nx.applyNeg(b: NxBuffer) -> boolean
In-place b[i] = -b[i].
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyNeg(b)
typed/builtin//modules/nx/nx/applyRecip
nx.applyRecip(b: NxBuffer) -> boolean
In-place b[i] = 1 / b[i].
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyRecip(b)
typed/builtin//modules/nx/nx/applyRecipSqrt
nx.applyRecipSqrt(b: NxBuffer) -> boolean
In-place b[i] = 1 / sqrt(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyRecipSqrt(b)
typed/builtin//modules/nx/nx/applyRound
nx.applyRound(b: NxBuffer) -> boolean
In-place b[i] = round(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyRound(b)
typed/builtin//modules/nx/nx/applySign
nx.applySign(b: NxBuffer) -> boolean
In-place b[i] = sign(b[i]) (returns -1, 0, or +1).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySign(b)
typed/builtin//modules/nx/nx/applySin
nx.applySin(b: NxBuffer) -> boolean
In-place b[i] = sin(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySin(b)
typed/builtin//modules/nx/nx/applySqrt
nx.applySqrt(b: NxBuffer) -> boolean
In-place b[i] = sqrt(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySqrt(b)
typed/builtin//modules/nx/nx/applySquare
nx.applySquare(b: NxBuffer) -> boolean
In-place b[i] = b[i] * b[i].
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySquare(b)
typed/builtin//modules/nx/nx/applyTan
nx.applyTan(b: NxBuffer) -> boolean
In-place b[i] = tan(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyTan(b)
typed/builtin//modules/nx/nx/applyTrunc
nx.applyTrunc(b: NxBuffer) -> boolean
In-place b[i] = trunc(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyTrunc(b)
typed/builtin//modules/nx/nx/applyWindow
nx.applyWindow(signal: NxBuffer, window: NxBuffer) -> boolean
Element-wise signal[i] *= window[i] in place. Operates over
the shorter of the two — passing a longer window to window a
shorter clip is intentional, not an error.
Parameters
signalNxBuffer— Signal buffer (mutated).windowNxBuffer— Window buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyWindow(signal, hann)
typed/builtin//modules/nx/nx/axpby
nx.axpby(dst: NxBuffer, a: number, src: NxBuffer, b: number) -> boolean
BLAS axpby: dst[i] = a*dst[i] + b*src[i].
Parameters
dstNxBuffer— Target buffer (mutated).anumber— Scale applied todst.srcNxBuffer— Source buffer.bnumber— Scale applied tosrc.
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.axpby(y, 0.5, x, 2.0)
typed/builtin//modules/nx/nx/clamp
nx.clamp(b: NxBuffer, min_v: number, max_v: number) -> boolean
In-place clamp: b[i] = clamp(b[i], min_v, max_v).
Parameters
bNxBuffer— Target buffer (mutated).min_vnumber— Lower bound.max_vnumber— Upper bound.
Returns boolean — true on success, false on unknown handle.
nx.clamp(b, 0.0, 1.0)
typed/builtin//modules/nx/nx/copy
nx.copy(dst: NxBuffer, src: NxBuffer) -> boolean
Copy every record from src into dst (memcpy fast path).
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.
Returns boolean — true on success, false on shape mismatch or unknown handle.
nx.copy(dst, src)
typed/builtin//modules/nx/nx/copyStridedFrom
nx.copyStridedFrom(dst: NxBuffer, src: NxBuffer, scale: number?, dst_stride: number, dst_off: number?, src_stride: number, src_off: number?) -> boolean
Strided copy from src into dst with optional scaling:
dst[k * dst_stride + dst_off] = scale * src[k * src_stride + src_off].
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.scalenumber(optional) — Optionalsrcscale factor (default 1.0).dst_stridenumber— Destination element stride.dst_offnumber(optional) — Optional destination offset (default 0).src_stridenumber— Source element stride.src_offnumber(optional) — Optional source offset (default 0).
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.copyStridedFrom(dst, src, 1, 3, 0, 3, 0)
typed/builtin//modules/nx/nx/create
nx.create(type_: NxType, n: number) -> NxBuffer?
Allocate a CPU buffer of type × len records. Thin alias for
substrate.createBuffer({type=type_, len=n, kind="cpu"}) — kept here so the
public nx library is the canonical entry point and users never
need to import buffer separately.
Parameters
type_NxType— Element layout ("f32","vec3","vec4","quat","mat4").nnumber— Record count.
Returns NxBuffer? — Freshly allocated buffer handle, or nil on failure.
local b = nx.create("vec3", 1024)
typed/builtin//modules/nx/nx/div
nx.div(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] /= x (x scalar) or b[i] /= x[i] (x buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.div(b, 2)
nx.div(dst, src)
typed/builtin//modules/nx/nx/dot
nx.dot(a: NxBuffer, b: NxBuffer) -> number?
Reduction: dot product of two same-shaped buffers.
Parameters
aNxBuffer— First buffer.bNxBuffer— Second buffer.
Returns number? — Scalar dot product, or nil on shape mismatch / unknown handle.
local d = nx.dot(a, b)
typed/builtin//modules/nx/nx/fft1d
nx.fft1d(re: NxBuffer, im: NxBuffer, inverse: boolean?) -> boolean
In-place 1D FFT over parallel re / im CPU buffers.
inverse=true runs the inverse transform scaled by 1/N (so
ifft(fft(x)) ≈ x).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).inverseboolean(optional) — Whentrueruns the inverse transform.
Returns boolean — true on success, false on length mismatch / invalid handle. Raises an error naming the bytes when the transform's plan or working memory cannot be held.
nx.fft1d(re, im)
nx.fft1d(re, im, true)
typed/builtin//modules/nx/nx/fft2d
nx.fft2d(re: NxBuffer, im: NxBuffer, width: number, height: number, inverse: boolean?) -> boolean
In-place 2D FFT over row-major parallel re / im buffers
of length width*height. inverse=true is scaled by
1 / (width * height).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).widthnumber— 2D width in samples.heightnumber— 2D height in samples.inverseboolean(optional) — Whentrueruns the inverse transform.
Returns boolean — true on success, false on length mismatch / invalid handle. Raises an error naming the bytes when the transform's plan or working memory cannot be held.
nx.fft2d(re, im, w, h)
typed/builtin//modules/nx/nx/fill
nx.fill(b: NxBuffer, value: number?) -> boolean
Fill the buffer with value (default 0.0). Equivalent to the
scalar form of nx.add against a zeroed buffer, but skips the
type-dispatch.
Parameters
bNxBuffer— Target buffer (mutated).valuenumber(optional) — Fill value (default 0.0).
Returns boolean — true on success, false on unknown handle.
nx.fill(b, 3.5)
typed/builtin//modules/nx/nx/fillRandomNormal
nx.fillRandomNormal(b: NxBuffer, mean: number?, stddev: number?, seed: NxSeed) -> boolean
Fill the buffer with Gaussian samples (Box-Muller), with the given mean and standard deviation, using a splitmix-keyed PRNG.
Parameters
bNxBuffer— Target buffer (mutated).meannumber(optional) — Optional mean (default 0.0).stddevnumber(optional) — Optional standard deviation (default 1.0).seedNxSeed— Optional seed — number,"frame", ornil(0).
Returns boolean — true on success, false on unknown handle.
nx.fillRandomNormal(b, 0, 1, 42)
typed/builtin//modules/nx/nx/fillRandomUniform
nx.fillRandomUniform(b: NxBuffer, min_v: number?, max_v: number?, seed: NxSeed) -> boolean
Fill the buffer with uniform-random samples in [min, max),
using a splitmix-keyed deterministic PRNG.
Parameters
bNxBuffer— Target buffer (mutated).min_vnumber(optional) — Optional lower bound (default 0.0).max_vnumber(optional) — Optional upper bound (default 1.0).seedNxSeed— Optional seed — number,"frame"(per-frame value), ornil(0).
Returns boolean — true on success, false on unknown handle.
nx.fillRandomUniform(b, -1, 1, "frame")
typed/builtin//modules/nx/nx/fillStrided
nx.fillStrided(b: NxBuffer, value: number, stride: number, offset: number?) -> boolean
Strided fill: buf[k * stride + offset] = value for every
valid k.
Parameters
bNxBuffer— Target buffer (mutated).valuenumber— Per-element value.stridenumber— Element stride.offsetnumber(optional) — Optional element offset (default 0).
Returns boolean — true on success, false on unknown handle.
nx.fillStrided(buf, 0, 3, 2)
typed/builtin//modules/nx/nx/fromTable
nx.fromTable(arr: { number }, type_: NxType?) -> NxBuffer?
Build a CPU buffer from a Lua table of numbers. The table is
written through buf:write in a single FFI crossing — no
per-element Lua loop. For large arrays, prefer one of the
nx.* constructors + a kernel pass over building a Lua array
first.
Parameters
arr{ number }— Lua array of numbers.type_NxType(optional) — Optional element layout (default"f32").
Returns NxBuffer? — Newly allocated buffer holding arr, or nil on failure.
local b = nx.fromTable({ 0.1, 0.2, 0.3 })
typed/builtin//modules/nx/nx/full
nx.full(n: number, type_: NxType?, value: number?) -> NxBuffer?
Allocate a buffer of type × len records and fill with value.
Parameters
nnumber— Record count.type_NxType(optional) — Optional element layout (default"f32").valuenumber(optional) — Fill value (default 0.0).
Returns NxBuffer? — Buffer initialised to value.
local b = nx.full(1024, "f32", -1.0)
typed/builtin//modules/nx/nx/ifft1d
nx.ifft1d(re: NxBuffer, im: NxBuffer) -> boolean
Convenience: nx.fft1d(re, im, true).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).
Returns boolean — true on success, false on length mismatch / invalid handle. Raises an error naming the bytes when the transform's plan or working memory cannot be held.
nx.ifft1d(re, im)
typed/builtin//modules/nx/nx/ifft2d
nx.ifft2d(re: NxBuffer, im: NxBuffer, width: number, height: number) -> boolean
Convenience: nx.fft2d(re, im, w, h, true).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).widthnumber— 2D width.heightnumber— 2D height.
Returns boolean — true on success, false on length mismatch / invalid handle. Raises an error naming the bytes when the transform's plan or working memory cannot be held.
nx.ifft2d(re, im, w, h)
typed/builtin//modules/nx/nx/integratePosition
nx.integratePosition(pos: NxBuffer, vel: NxBuffer, dt: number) -> boolean
Per-vec3: pos[i] += vel[i] * dt — the position half of an
Euler step. Both buffers must be vec3 (stride 3). The velocity step
is the caller's: update vel BEFORE this call for semi-implicit
Euler; updating it after gives forward Euler, which gains energy on
stiff systems.
Parameters
posNxBuffer— Position buffer (mutated).velNxBuffer— Velocity buffer.dtnumber— Time step.
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.integratePosition(pos, vel, dt)
typed/builtin//modules/nx/nx/irfft1d
nx.irfft1d(re: NxBuffer, im: NxBuffer) -> NxBuffer?
Real-output inverse 1D FFT. Input re / im are length
N/2 + 1. Returns a fresh CPU f32 buffer of length
2 * (N/2 + 1 - 1) = N real samples.
Parameters
reNxBuffer— Real-component buffer.imNxBuffer— Imaginary-component buffer.
Returns NxBuffer? — Real-valued output buffer on success, nil on failure. Raises an error naming the bytes when the transform's plan or working memory cannot be held. A call that answers nil or raises leaves none of the buffers it created allocated.
local out = nx.irfft1d(re, im)
typed/builtin//modules/nx/nx/irfft2d
nx.irfft2d(re: NxBuffer, im: NxBuffer, width: number, height: number) -> NxBuffer?
Real-output inverse 2D FFT. Input re / im are
(width/2 + 1) * height row-major. Returns a fresh f32 buffer
of length width * height.
Parameters
reNxBuffer— Real-component buffer.imNxBuffer— Imaginary-component buffer.widthnumber— 2D width.heightnumber— 2D height.
Returns NxBuffer? — Real-valued output buffer on success, nil on failure. Raises an error naming the bytes when the transform's plan or working memory cannot be held. A call that answers nil or raises leaves none of the buffers it created allocated.
local out = nx.irfft2d(re, im, w, h)
typed/builtin//modules/nx/nx/lerpTo
nx.lerpTo(dst: NxBuffer, src: NxBuffer, t: number) -> boolean
dst[i] += t * (src[i] - dst[i]) — element-wise lerp toward
src by t.
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.tnumber— Interpolation factor.
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.lerpTo(current, target, 0.1)
typed/builtin//modules/nx/nx/max
nx.max(b: NxBuffer) -> number?
Reduction: maximum of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar max, or nil on unknown handle.
local m = nx.max(b)
typed/builtin//modules/nx/nx/maxOp
nx.maxOp(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
Element-wise b[i] = max(b[i], x) (scalar) or
b[i] = max(b[i], x[i]) (buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Scalar or same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.maxOp(b, 0.0)
typed/builtin//modules/nx/nx/mean
nx.mean(b: NxBuffer) -> number?
Reduction: arithmetic mean of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar mean, or nil on unknown handle.
local m = nx.mean(b)
typed/builtin//modules/nx/nx/min
nx.min(b: NxBuffer) -> number?
Reduction: minimum of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar min, or nil on unknown handle.
local m = nx.min(b)
typed/builtin//modules/nx/nx/minOp
nx.minOp(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
Element-wise b[i] = min(b[i], x) (scalar) or
b[i] = min(b[i], x[i]) (buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Scalar or same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.minOp(b, 1.0)
typed/builtin//modules/nx/nx/mul
nx.mul(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] *= x (x scalar) or b[i] *= x[i] (x buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.mul(b, 2)
nx.mul(dst, src)
typed/builtin//modules/nx/nx/mulStrided
nx.mulStrided(b: NxBuffer, scalar: number, stride: number, offset: number?) -> boolean
Strided multiply: buf[k * stride + offset] *= scalar for
every valid k.
Parameters
bNxBuffer— Target buffer (mutated).scalarnumber— Per-element multiplier.stridenumber— Element stride.offsetnumber(optional) — Optional element offset (default 0).
Returns boolean — true on success, false on unknown handle.
nx.mulStrided(buf, 2, 4, 0)
typed/builtin//modules/nx/nx/normL1
nx.normL1(b: NxBuffer) -> number?
Reduction: L1 norm — sum(|b[i]|).
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar L1 norm, or nil on unknown handle.
local n = nx.normL1(b)
typed/builtin//modules/nx/nx/normL2
nx.normL2(b: NxBuffer) -> number?
Reduction: L2 norm — sqrt(sum(b[i]^2)).
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar L2 norm, or nil on unknown handle.
local n = nx.normL2(b)
typed/builtin//modules/nx/nx/normalizeVec3
nx.normalizeVec3(b: NxBuffer) -> boolean
Normalise each vec3 in-place. Vectors below 1e-8 are left untouched.
Parameters
bNxBuffer— Vec3 buffer (mutated).
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.normalizeVec3(directions)
typed/builtin//modules/nx/nx/ones
nx.ones(n: number, type_: NxType?) -> NxBuffer?
Allocate a buffer of type × len records and fill with 1.0.
Parameters
nnumber— Record count.type_NxType(optional) — Optional element layout (default"f32").
Returns NxBuffer? — Buffer initialised to one.
local b = nx.ones(1024)
typed/builtin//modules/nx/nx/pow
nx.pow(b: NxBuffer, p: number) -> boolean
In-place b[i] = b[i] ^ p (scalar exponent).
Parameters
bNxBuffer— Target buffer (mutated).pnumber— Scalar exponent.
Returns boolean — true on success, false on unknown handle.
nx.pow(b, 2.2)
typed/builtin//modules/nx/nx/quatFromYaw
nx.quatFromYaw(dst: NxBuffer, yaw: NxBuffer) -> boolean
Per-quat: dst[i] = (0, sin(yaw[i]/2), 0, cos(yaw[i]/2)) —
the pure-Y axis-angle quaternion for each yaw value. dst must
be stride-4 (quat); yaw must be stride-1 (f32).
Parameters
dstNxBuffer— Quaternion buffer (mutated).yawNxBuffer— Source yaw scalars buffer.
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.quatFromYaw(quats, yaws)
typed/builtin//modules/nx/nx/rfft1d
nx.rfft1d(signal: NxBuffer?) -> (NxBuffer?, NxBuffer?)
Real-input forward 1D FFT. Allocates two new CPU f32 buffers
of length N/2 + 1 holding the (re, im) parts of the
Hermitian-symmetric spectrum (same convention as NumPy
np.fft.rfft).
Parameters
signalNxBuffer(optional) — Real-valued input buffer.
Returns (NxBuffer?, NxBuffer?) — (re_buf, im_buf) on success, nil otherwise. Raises an error naming the bytes when the transform's plan or working memory cannot be held. A call that answers nil or raises leaves none of the buffers it created allocated.
local re, im = nx.rfft1d(signal)
typed/builtin//modules/nx/nx/rfft2d
nx.rfft2d(signal: NxBuffer, width: number, height: number) -> (NxBuffer?, NxBuffer?)
Real-input forward 2D FFT. Input signal is row-major
width*height. Returns (re_buf, im_buf) of length
(width/2 + 1) * height each (matches np.fft.rfft2 layout).
Parameters
signalNxBuffer— Real-valued input buffer (row-major).widthnumber— 2D width.heightnumber— 2D height.
Returns (NxBuffer?, NxBuffer?) — (re_buf, im_buf) on success, nil on failure. Raises an error naming the bytes when the transform's plan or working memory cannot be held. A call that answers nil or raises leaves none of the buffers it created allocated.
local re, im = nx.rfft2d(image, w, h)
typed/builtin//modules/nx/nx/scale
nx.scale(b: NxBuffer, s: number) -> boolean
In-place b[i] *= s.
Parameters
bNxBuffer— Target buffer (mutated).snumber— Scalar multiplier.
Returns boolean — true on success, false on unknown handle.
nx.scale(b, 0.5)
typed/builtin//modules/nx/nx/sinCosTo
nx.sinCosTo(src: NxBuffer, sin_dst: NxBuffer, cos_dst: NxBuffer) -> boolean
Compute sin_dst[i] = sin(src[i]) and cos_dst[i] = cos(src[i])
in one pass using cheaper paired-trig argument reduction.
Parameters
srcNxBuffer— Source angles buffer.sin_dstNxBuffer— Destination buffer for the sine values.cos_dstNxBuffer— Destination buffer for the cosine values.
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.sinCosTo(angles, s, c)
typed/builtin//modules/nx/nx/sub
nx.sub(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] -= x (x scalar) or b[i] -= x[i] (x buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.sub(b, 0.5)
nx.sub(dst, src)
typed/builtin//modules/nx/nx/sum
nx.sum(b: NxBuffer) -> number?
Reduction: sum of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar sum, or nil on unknown handle.
local s = nx.sum(b)
typed/builtin//modules/nx/nx/wanderYaw
nx.wanderYaw(args: NxWanderArgs) -> boolean
Fused per-entity wander step. Each entity's yaw[i] walks by
a uniform random delta in [-yawDelta, +yawDelta], then pos[i]
advances forward in the (sin yaw, cos yaw) direction by step.
Optional rot quat output writes a pure-Y axis-angle rotation.
Replaces the per-entity Luau loop pattern (~12 ms / 5000 in
interpreter) with a single Rust pass (~0.2 ms / 5000).
Buffer requirements: pos vec3 (stride 3), yaw f32 (stride 1),
rot optional quat (stride 4). All counts should match (kernel
walks min(count_i)).
Parameters
argsNxWanderArgs— Table with{ pos, yaw, rot?, step?, yawDelta?, seed }.
Returns boolean — true on success, false on stride / shape failures.
nx.wanderYaw({ pos = pos, yaw = yaw, step = 0.5, yawDelta = 0.2, seed = "frame" })
typed/builtin//modules/nx/nx/window/blackman
nx.window.blackman(n: number) -> NxBuffer?
Allocate a fresh CPU f32 buffer of length n filled with a
symmetric Blackman window (NumPy np.blackman convention).
Parameters
nnumber— Number of samples in the window.
Returns NxBuffer? — Buffer holding the window samples, or nil on failure.
local w = nx.window.blackman(1024)
typed/builtin//modules/nx/nx/window/hamming
nx.window.hamming(n: number) -> NxBuffer?
Allocate a fresh CPU f32 buffer of length n filled with a
symmetric Hamming window (NumPy np.hamming convention).
Parameters
nnumber— Number of samples in the window.
Returns NxBuffer? — Buffer holding the window samples, or nil on failure.
local w = nx.window.hamming(1024)
typed/builtin//modules/nx/nx/window/hann
nx.window.hann(n: number) -> NxBuffer?
Allocate a fresh CPU f32 buffer of length n filled with a
symmetric Hann window (NumPy np.hanning convention).
Parameters
nnumber— Number of samples in the window.
Returns NxBuffer? — Buffer holding the window samples, or nil on failure.
local w = nx.window.hann(1024)
typed/builtin//modules/nx/nx/zeros
nx.zeros(n: number, type_: NxType?) -> NxBuffer?
Allocate a buffer of type × len records and fill with 0.0.
Parameters
nnumber— Record count.type_NxType(optional) — Optional element layout (default"f32").
Returns NxBuffer? — Buffer initialised to zero.
local b = nx.zeros(2048)
typed/builtin//modules/preset/M/addDefault
M.addDefault(source: AssetRef<preset>, opts: { filter: string?, at: number? }?) -> number
Add one Preset Manager row for source, governing the declarer the
preset targets. opts.filter limits it to new things whose path
matches; opts.at places it (1 = first); it goes last otherwise.
Parameters
sourceAssetRef<preset>— The preset.opts{ filter: string?, at: number? }(optional) —{ filter?, at? }.
Returns number — The new row's index.
preset.addDefault("pixel_art")
typed/builtin//modules/preset/M/apply
M.apply(source: AssetRef<preset>, into: AssetRef | ComponentTarget) -> ApplyResult
Write a preset's values into something its target governs: an asset
whose settings the target declares, or a component on an entity, named
{ entity = <id>, component = <name> }, whose component the target is.
Struct groups merge field by field. Only valid fields are written; the
rest are returned in rejected.
Parameters
sourceAssetRef<preset>— The preset.intoAssetRef | ComponentTarget— The asset, or the component on an entity, to write into.
Returns ApplyResult — { applied, rejected, targetChanged }.
preset.apply("pixel_art", "hero_sprite")
preset.apply("red_tint", { entity = id, component = "Model" })
typed/builtin//modules/preset/M/capture
M.capture(from: AssetRef | ComponentTarget, name: string, opts: CaptureOpts?) -> AssetRef<preset>
Save the settings something has now as a new preset targeting the
declarer that governs them: an asset's settings, or the live values of a
component on an entity, named { entity = <id>, component = <name> }.
Parameters
fromAssetRef | ComponentTarget— The asset, or the component on an entity, whose values are captured.namestring— A bare name (lands in/source/presets/) or an absolute.presetpath.optsCaptureOpts(optional) —{ overwrite = true }re-authors an existing preset of that name, keeping its guid.
Returns AssetRef<preset> — The new preset.
local p = preset.capture("hero_sprite", "pixel_art")
local tint = preset.capture({ entity = id, component = "Model" }, "red_tint")
typed/builtin//modules/preset/M/create
M.create(name: string, entityId: string, componentType: string, opts: PresetCreateOpts?) -> PresetCreateResult
Capture a component's live values on an entity as a preset targeting
that component; forwards to preset.capture, overwriting a preset of the
same name.
Parameters
namestring— Either a bare preset name (lands under/source/presets/) or an absolute path to a.presetasset or its/preset.yaml.entityIdstring— Source entity id.componentTypestring— Component to capture (e.g."CharacterController").optsPresetCreateOpts(optional) —{ name = <display name> }sets the preset's.metadatatitle.
Returns PresetCreateResult — { path, entityId, component, properties }: the written preset.yaml, and the values it holds.
typed/builtin//modules/preset/M/defaultFor
M.defaultFor(declarer: AssetRef | string, path: string) -> ({ [string]: any }?, any?)
The values a new thing declarer governs would take from the
Preset Manager at path, and the row that gives them — the layer
itself, before the creating importer's and the caller's settings. A
path is a /source/... path for an asset or an import, and an entity's
hierarchy path for a component.
Parameters
declarerAssetRef | string— The declarer: an assetType, an importer or a component.pathstring— The new thing's path.
Returns ({ [string]: any }?, any?) — (values, match), or nil when no row matches.
local values, match = preset.defaultFor("@builtin::importers.texture", "/source/sprites/hero.png")
typed/builtin//modules/preset/M/defaults
M.defaults() -> { any }
The world's Preset Manager rows, in order. Each row names the
declarer it governs (target), the preset a new thing takes (preset)
and a glob over the new thing's path (filter), resolved, with every
problem that keeps it from applying.
Returns { any } — { { index, target, preset, filter, targetGuid, presetGuid, targetName, presetName, problems } }.
for _, row in ipairs(preset.defaults()) do print(row.index, row.targetName, row.presetName, row.filter) end
typed/builtin//modules/preset/M/load
M.load(source: AssetRef<preset>, overrides: table?) -> { [string]: any }
Read a preset as a plain table: a typed preset's valid values, or a
component-only preset's properties as authored, with overrides laid
over the top. Forwards to preset.values.
Parameters
sourceAssetRef<preset>— The preset.overridestable(optional) — Optional table merged onto the values.
Returns { [string]: any } — Plain table.
typed/builtin//modules/preset/M/removeDefault
M.removeDefault(index: number)
Remove the Preset Manager row at index. The other rows are kept
exactly as written, so removing succeeds while another row is broken.
Parameters
indexnumber— The row's index, aspreset.defaults()reports it.
preset.removeDefault(1)
typed/builtin//modules/preset/M/setDefaults
M.setDefaults(rows: { DefaultRowInput })
Replace the world's Preset Manager rows. Each row names a preset, optionally its declarer (the preset's own target when omitted) and a filter (empty matches everything). Every row is checked first; any problem refuses the whole write, naming every one. Rows change only what is created afterwards.
Parameters
rows{ DefaultRowInput }— The complete ordered rows.
preset.setDefaults({ { preset = "pixel_art", filter = "sprites/**" } })
typed/builtin//modules/preset/M/setValues
M.setValues(source: AssetRef<preset>, values: { [string]: any })
Replace a preset's values. The whole table is checked against the
target's schema first; any violation refuses the write, naming every one.
A preset in the component-only form is written in the typed form, its
target the component it names and its legacy name moved to the
.metadata title, as a heal moves it.
Parameters
sourceAssetRef<preset>— The preset.values{ [string]: any }— The complete values table the preset holds from now on.
preset.setValues("pixel_art", { filter = "nearest", generateMipmaps = false })
typed/builtin//modules/preset/M/values
M.values(source: AssetRef<preset>) -> ({ [string]: any }, Report)
A preset's values and a report on them. Every value is checked
against the target declarer's current schema: the fields that pass are
returned, every field that fails is listed by path in report.rejected,
report.pinned says the preset's .refs pins the target by guid, and
report.targetChanged says the target differs from the version pinned.
A preset still in the component-only form reads as authored, and
report.rejected lists what its component's schema would refuse.
Parameters
sourceAssetRef<preset>— The preset.
Returns ({ [string]: any }, Report) — (values, report), where report is { format, target, targetGuid, pinned, targetChanged, rejected }.
local values, report = preset.values("pixel_art")
typed/builtin//modules/random/M/fromState
M.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
statestring— The 32-character hex stringrng:state()returned.
Returns RandomGenerator — A generator at that position.
local resumed = Random.fromState(Random.new(7):state())
typed/builtin//modules/random/M/new
M.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
seednumber(optional) — Any finite number; every distinct number is a distinct stream. Omitted, the clock seeds it.
Returns RandomGenerator — A generator answering :nextNumber(), :nextNumber(min, max), :nextInteger(min, max), :clone() and :state().
local roll = Random.new(42):nextInteger(1, 6)
local jitter = Random.new(42):nextNumber(-0.5, 0.5)
local clockSeeded = Random.new()
typed/builtin//modules/render_visibility/Vis/showing
Vis.showing(e: any?) -> boolean?
Whether the entity's own renderable is showing, or nil when the entity
carries no renderable for a frame to reach.
Parameters
eany(optional) — Entity proxy.
Returns boolean? — True while the renderable is drawn, false while it is switched off, nil when the entity holds no renderable.
local showing = Vis.showing(entity.find("chunk_0_0_0"))
typed/builtin//modules/render_visibility/Vis/switchedOff
Vis.switchedOff(e: any?) -> boolean
Whether the frame reaches nothing this entity draws — the entity or an ancestor is inactive, or the mesh it draws through is switched off.
Parameters
eany(optional) — Entity proxy.
Returns boolean — True when nothing this entity draws reaches the frame.
if Vis.switchedOff(entity.find("rock")) then return end
typed/builtin//modules/rigmath/M/axisAngle
M.axisAngle(axis: { number }, angle: number) -> { number }
Quaternion from an axis and an angle in radians. The axis is normalized internally; a degenerate axis yields identity.
Parameters
axis{ number }— Rotation axis.anglenumber— Rotation angle in radians.
Returns { number } — The unit quaternion.
local q = rigmath.axisAngle({ 0, 1, 0 }, math.pi / 2)
typed/builtin//modules/rigmath/M/clamp
M.clamp(v: number, lo: number, hi: number) -> number
Clamp v into [lo, hi].
Parameters
vnumber— The value.lonumber— Lower bound.hinumber— Upper bound.
Returns number — The clamped value.
local w = rigmath.clamp(weight, 0, 1)
typed/builtin//modules/rigmath/M/computeBoneLengths
M.computeBoneLengths(bones: { any }, gPos: { { number } }) -> { number }
Each bone's length, measured as the distance to its farthest child. Retargeting uses this to scale root translation by rig proportion.
Parameters
bones{ any }— Array of{ parent };parentis 0-based, -1 for a root.gPos{ { number } }— Global positions parallel tobones, as returned bycomputeGlobals.
Returns { number } — Bone lengths, 1-based and parallel to bones. A leaf measures zero.
local lengths = rigmath.computeBoneLengths(rig.bones, gPos)
typed/builtin//modules/rigmath/M/computeGlobals
M.computeGlobals(bones: { any }) -> ({ { number } }, { { number } })
Resolve every bone's global rest rotation and position by forward kinematics over the local rest transforms. Handles any bone ordering — a parent may be listed after its child — by iterating until all resolve. A malformed cyclic parent falls back to the bone's local transform.
Parameters
bones{ any }— Array of{ parent, rest = { t, r } };parentis 0-based, -1 for a root.
Returns ({ { number } }, { { number } }) — Global rotations followed by global positions, both 1-based and parallel to bones.
local gRot, gPos = rigmath.computeGlobals(rig.bones)
typed/builtin//modules/rigmath/M/isFinite
M.isFinite(n: number) -> boolean
Whether a number is finite — neither NaN nor an infinity.
Parameters
nnumber— The number to test.
Returns boolean — True when n is finite.
if not rigmath.isFinite(x) then return end
typed/builtin//modules/rigmath/M/qangle
M.qangle(q: { number }) -> number
The rotation angle of a quaternion in radians, in [0, pi].
Parameters
q{ number }— The quaternion.
Returns number — Its rotation magnitude.
local a = rigmath.qangle(delta)
typed/builtin//modules/rigmath/M/qinverse
M.qinverse(q: { number }) -> { number }
Inverse of a unit quaternion, which is its conjugate. Normalizes first so a bind rotation that drifted slightly off unit still inverts cleanly.
Parameters
q{ number }— The quaternion to invert.
Returns { number } — The inverse quaternion.
local inv = rigmath.qinverse(parentGlobalRotation)
typed/builtin//modules/rigmath/M/qmul
M.qmul(a: { number }, b: { number }) -> { number }
Hamilton product a * b — apply b, then a. Matches glam's Quat
multiplication so results agree with the engine's own rig math.
Parameters
a{ number }— Outer rotation.b{ number }— Inner rotation.
Returns { number } — The composed quaternion.
local q = rigmath.qmul(parentGlobal, boneLocal)
typed/builtin//modules/rigmath/M/qnormalize
M.qnormalize(q: { number }) -> { number }
Normalize a quaternion to unit length. A degenerate quaternion returns identity rather than NaN.
Parameters
q{ number }— The quaternion.
Returns { number } — The unit quaternion.
local q = rigmath.qnormalize(accumulated)
typed/builtin//modules/rigmath/M/qrotvec
M.qrotvec(q: { number }, v: { number }) -> { number }
Rotate a 3-vector by a quaternion.
Parameters
q{ number }— The rotation.v{ number }— The vector to rotate.
Returns { number } — The rotated vector.
local forward = rigmath.qrotvec(boneRotation, { 0, 0, 1 })
typed/builtin//modules/rigmath/M/qslerp
M.qslerp(a: { number }, b: { number }, t: number) -> { number }
Spherical linear interpolation along the shortest arc. t = 0 returns
a, t = 1 returns b. Falls back to normalized lerp for nearly parallel
inputs, where the arc formulation loses precision.
Parameters
a{ number }— Start rotation.b{ number }— End rotation.tnumber— Interpolation factor.
Returns { number } — The interpolated unit quaternion.
local blended = rigmath.qslerp(animatedRotation, solvedRotation, weight)
typed/builtin//modules/rigmath/M/shortestArc
M.shortestArc(a: { number }, b: { number }) -> { number }
Shortest-arc quaternion rotating unit vector a onto unit vector b.
The antiparallel case resolves to a half turn about an arbitrary
perpendicular axis instead of producing NaN.
Arbitrarily small rotations are represented rather than rounded away. An iterative solver refines a pose in ever-smaller steps, so a near-parallel cutoff would stall it at whatever residual the cutoff angle spans — the bones needing the finest corrections would be exactly the ones ignored.
Parameters
a{ number }— Source unit vector.b{ number }— Destination unit vector.
Returns { number } — The rotation carrying a to b.
local q = rigmath.shortestArc(currentDir, wantedDir)
typed/builtin//modules/rigmath/M/signedAngle
M.signedAngle(a: { number }, b: { number }, axis: { number }) -> number
Signed angle in radians from a to b measured about axis. Both
vectors are projected onto the plane perpendicular to axis first, so the
result is the roll about that axis.
Parameters
a{ number }— Source vector.b{ number }— Destination vector.axis{ number }— The axis to measure about; normalized internally.
Returns number — The signed angle in radians.
local roll = rigmath.signedAngle(midOffset, poleOffset, chainDirection)
typed/builtin//modules/rigmath/M/swingTwist
M.swingTwist(q: { number }, axis: { number }) -> ({ number }, { number })
Split a rotation into its twist about axis and the remaining swing.
Rotation limits clamp the twist and rebuild, which is what keeps a hinge
joint on its axis.
Parameters
q{ number }— The rotation to decompose.axis{ number }— The twist axis; normalized internally.
Returns ({ number }, { number }) — The twist quaternion followed by the swing quaternion.
local twist, swing = rigmath.swingTwist(localRotation, hingeAxis)
typed/builtin//modules/rigmath/M/vadd
M.vadd(a: { number }, b: { number }) -> { number }
Component-wise sum a + b.
Parameters
a{ number }— First addend.b{ number }— Second addend.
Returns { number } — A new vector.
local p = rigmath.vadd(rootPos, offset)
typed/builtin//modules/rigmath/M/vcross
M.vcross(a: { number }, b: { number }) -> { number }
Cross product of two 3-vectors.
Parameters
a{ number }— First vector.b{ number }— Second vector.
Returns { number } — a cross b as a new vector.
local n = rigmath.vcross({ 1, 0, 0 }, { 0, 1, 0 })
typed/builtin//modules/rigmath/M/vdot
M.vdot(a: { number }, b: { number }) -> number
Dot product of two 3-vectors.
Parameters
a{ number }— First vector.b{ number }— Second vector.
Returns number — The scalar dot product.
local d = rigmath.vdot({ 1, 0, 0 }, { 0, 1, 0 })
typed/builtin//modules/rigmath/M/vlen
M.vlen(v: { number }) -> number
Length of a 3-vector.
Parameters
v{ number }— The vector.
Returns number — Its Euclidean length.
local l = rigmath.vlen({ 3, 4, 0 })
typed/builtin//modules/rigmath/M/vnormalize
M.vnormalize(v: { number }) -> { number }
Normalize a 3-vector. A zero-length vector returns zero rather than NaN.
Parameters
v{ number }— The vector to normalize.
Returns { number } — The unit vector, or { 0, 0, 0 } when v is degenerate.
local dir = rigmath.vnormalize(rigmath.vsub(target, root))
typed/builtin//modules/rigmath/M/vperpendicular
M.vperpendicular(v: { number }) -> { number }
A unit vector perpendicular to v, chosen deterministically. Used as a
bend axis when a chain is perfectly straight and carries no pole target.
Parameters
v{ number }— The reference vector.
Returns { number } — A unit vector at right angles to v.
local axis = rigmath.vperpendicular(chainDirection)
typed/builtin//modules/rigmath/M/vscale
M.vscale(v: { number }, s: number) -> { number }
Scale a 3-vector by a scalar.
Parameters
v{ number }— The vector.snumber— The scalar.
Returns { number } — A new scaled vector.
local half = rigmath.vscale(dir, 0.5)
typed/builtin//modules/rigmath/M/vsub
M.vsub(a: { number }, b: { number }) -> { number }
Component-wise difference a - b.
Parameters
a{ number }— Minuend.b{ number }— Subtrahend.
Returns { number } — A new vector.
local d = rigmath.vsub(tipPos, rootPos)
typed/builtin//modules/scopes/M/current
M.current() -> string?
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.
Returns string? — The scope tag, or nil.
print("resources I register follow", scopes.current())
typed/builtin//modules/scopes/M/list
M.list() -> { LiveResource }
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.
Returns { LiveResource } — Array of live resources.
for _, r in scopes.list() do print(r.scope, r.kind, r.detail) end
typed/builtin//modules/scopes/M/open
M.open(options: ScopeOptions?) -> Scope
Open a task scope without waiting for it: start work in it with
s:spawn, end it with s:cancel(), or let what it is lived inside end it
({ entity = id } ties it to an entity). Wait for its work with
s:join() from a task.
Parameters
optionsScopeOptions(optional) —{ entity = id }ties the scope to an entity;{ name = "..." }names it.
Returns Scope — The scope.
local s = scopes.open({ entity = npc }); s:spawn("patrol", patrol)
typed/builtin//modules/scopes/M/release
M.release(scope: string) -> { Released }
End every resource registered under scope, across every subsystem:
tasks are cancelled, jobs dropped. Reaches contexts no seam does, such as
the chunk of an execute call that started something and ended without
ending it. An execute call runs as a task listed under its own exec:
scope, so releasing that scope from the same call cancels the call too:
release it from a later call.
Parameters
scopestring— A scope tag, as thescopefield of alist()row carries it.
Returns { Released } — One row per subsystem that ended something, with how many it ended.
local ended = scopes.release("exec:__exec_12")
typed/builtin//modules/scopes/M/run
M.run(fn: (Scope) -> ...any, options: ScopeOptions?) -> ...any
Run fn(s) in a new task scope and wait until every piece of work
started in it is finished or cancelled. Returns what fn returned. The
first error raised by fn 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.
Parameters
fn(Scope) -> ...any— The function to run; it receives the scope, and starts work withs:spawn.optionsScopeOptions(optional) —{ entity = id }ties the scope to an entity;{ name = "..." }names it.
Returns ...any — What fn returned.
scopes.run(function(s) s:spawn("a", fa); s:spawn("b", fb) end)
typed/builtin//modules/session/Session/get
Session.get(key: string) -> any
The value stored under key this session, or nil.
Parameters
keystring— The session key.
Returns any — The stored value, or nil.
local handle = Session.get("session_material_" .. entityId)
typed/builtin//modules/session/Session/set
Session.set(key: string, value: any?)
Store value under key for the rest of the engine session.
Pass nil to clear the key.
Parameters
keystring— The session key.valueany(optional) — The value to store, or nil to clear.
Session.set("session_material_" .. entityId, handle)
typed/builtin//modules/settings/settings/all
settings.all() -> { [string]: any }
Snapshot of the entire settings document (parsed). Modifying
the returned table does NOT propagate — call set or setMany
to persist. Useful for editors/inspectors that render every
section.
Returns { [string]: any } — A nested table mirroring the TOML document.
for section, keys in pairs(settings.all()) do
typed/builtin//modules/settings/settings/get
settings.get(key: string) -> any
Look up a value by dotted key. Returns whatever the file holds at that path — string / number / boolean / array / table — or nil if missing.
Parameters
keystring— Dotted-path key, e.g."render.culling_mode".
Returns any — The raw value at that key, or nil.
local mode = settings.get("render.culling_mode")
typed/builtin//modules/settings/settings/getBool
settings.getBool(key: string, default: boolean?) -> boolean
Boolean-typed accessor. Returns the value when present and
boolean-typed; falls back to default (or false) on missing key
or type mismatch.
Parameters
keystring— Dotted-path key.defaultboolean(optional) — Optional fallback boolean.
Returns boolean — The boolean value or the fallback.
if settings.getBool("render.shadows", true) then ... end
typed/builtin//modules/settings/settings/getNumber
settings.getNumber(key: string, default: number?) -> number
Number-typed accessor. Returns the value when present and
number-typed; falls back to default (or 0) on missing key or
type mismatch.
Parameters
keystring— Dotted-path key.defaultnumber(optional) — Optional fallback number.
Returns number — The number value or the fallback.
local g = settings.getNumber("physics.gravity", -9.81)
typed/builtin//modules/settings/settings/getString
settings.getString(key: string, default: string?) -> string
String-typed accessor. Returns the value when present and
string-typed; falls back to default (or "" if omitted) on
missing key or type mismatch.
Parameters
keystring— Dotted-path key.defaultstring(optional) — Optional fallback string.
Returns string — The string value or the fallback.
local mode = settings.getString("render.culling_mode", "gpu")
typed/builtin//modules/settings/settings/set
settings.set(key: string, value: any?)
Set a value by dotted key, then serialize and write the
file. In play mode the write fails like any other source-file
write — call wld.edit() first to unlock.
Parameters
keystring— Dotted-path key.valueany(optional) — Replacement value (string / number / boolean / table).
settings.set("render.culling_mode", "cpu")
typed/builtin//modules/settings/settings/setMany
settings.setMany(updates: { [string]: any })
Apply many key/value updates in one batched write — fewer
serialize+write round-trips than calling set per-key. Same
lock semantics as set.
Parameters
updates{ [string]: any }— A table of dotted-key → value pairs.
settings.setMany({
typed/builtin//modules/signal/M/disconnectAllFromEntity
M.disconnectAllFromEntity(entityId: string) -> number
Disconnect every connection that was sourced from entityId
(connections made while that entity's script was on the call stack).
Returns the number disconnected. Called by the entity-destroy
dispatch so a destroyed entity's connections never leak.
Parameters
entityIdstring— Entity whose sourced connections to drop.
Returns number
typed/builtin//modules/signal/M/disconnectAllFromInstance
M.disconnectAllFromInstance(instanceId: string) -> number
Disconnect every connection sourced from a specific component instance (connections made while that instance's script was on the call stack). Returns the number disconnected. The engine calls it when the instance is removed from an entity that stays in the world, and before a hot reload or a re-wake starts the instance's next life.
Parameters
instanceIdstring— Component instance whose sourced connections to drop.
Returns number
typed/builtin//modules/signal/M/markBaseline
M.markBaseline() -> number
Record the connections standing right now as the mark
resetToBaseline takes back to. A caller running work in an engine it
shares, such as a sweep of test suites, marks before the work and resets after
it.
Returns number — The mark.
local mark = Signal.markBaseline()
typed/builtin//modules/signal/M/new
M.new() -> any
Construct a new Signal.
Returns any
local hit = Signal.new()
hit:Connect(function(dmg) print("hit for", dmg) end)
hit:Fire(10)
typed/builtin//modules/signal/M/resetToBaseline
M.resetToBaseline(mark: number) -> number
Disconnect every connection made since mark with no component on
the call stack. A component's connections answer to the component and
end with it, or with its entity.
Parameters
marknumber— WhatmarkBaselinereturned.
Returns number — How many connections were disconnected.
Signal.resetToBaseline(mark)
typed/builtin//modules/substrate_batch/M/installInto
M.installInto(entity: EntityNamespace)
Install the polymorphic batchWrite / batchRead wrappers
onto an entity-shaped namespace. The prelude calls this once at
boot with the engine's entity global; users shouldn't call it
directly. Errors if the namespace is missing any of the five
required FFI primitives.
Parameters
entityEntityNamespace— The target entity namespace. Must already carrybatchWrite,batchWriteBound,batchWriteFromBuffer,batchRead, andbatchReadToBufferas functions.
require("@builtin::modules.substrate_batch").installInto(entity)
typed/builtin//modules/text_diff/M/added
M.added(newText: string?, opts: DiffOpts?) -> FileDiff
Pure-add convenience: build a diff representing the full content
of newText as added. Equivalent to M.diff("", newText, opts).
Parameters
newTextstring(optional) — The full added content.optsDiffOpts(optional) — Same shape asM.diff's opts.
Returns FileDiff — The structured FileDiff describing the addition.
local d = TextDiff.added(newBytes, { path = "newfile.luau" })
typed/builtin//modules/text_diff/M/diff
M.diff(oldText: string?, newText: string?, opts: DiffOpts?) -> FileDiff
Diff two strings and return the structured per-file shape. LCS-based;
produces op-tagged hunks rather than unified-diff text so consumers
can pattern-match on op instead of parsing prefixes. Files larger
than M.SIZE_CAP_BYTES return a size-only summary with is_text=false.
Parameters
oldTextstring(optional) — The previous text content.nilis treated as"".newTextstring(optional) — The new text content.nilis treated as"".optsDiffOpts(optional) — Optional.contextoverrides the default 3-line context window;pathandactionare folded into the returned table for caller convenience.
Returns FileDiff — The structured FileDiff table (see module header for shape).
local d = TextDiff.diff(oldBytes, newBytes)
local d = TextDiff.diff(oldBytes, newBytes, { path = "foo.luau", context = 5 })
typed/builtin//modules/text_diff/M/removed
M.removed(oldText: string?, opts: DiffOpts?) -> FileDiff
Pure-remove convenience: full removal of oldText. Equivalent
to M.diff(oldText, "", opts).
Parameters
oldTextstring(optional) — The full removed content.optsDiffOpts(optional) — Same shape asM.diff's opts.
Returns FileDiff — The structured FileDiff describing the removal.
local d = TextDiff.removed(oldBytes, { path = "gone.luau" })
typed/builtin//modules/text_diff/M/toStatLine
M.toStatLine(fileDiff: FileDiff) -> string
Compact --stat-style summary line for a single file. Mirrors
git diff --stat's per-file row.
Parameters
fileDiffFileDiff— The structuredFileDifftable.
Returns string — One-line summary string. Binary/oversize files report <action> <path> (binary) instead of +/- counts.
print(TextDiff.toStatLine(d))
typed/builtin//modules/text_diff/M/toUnifiedText
M.toUnifiedText(fileDiff: FileDiff) -> string
Render a single file's structured diff back into unified-diff
text. Used by zm diff / zm show shell commands when the user
wants the conventional +/-/ text output instead of structured
hunks. Pure derivation from the structured shape — no second LCS pass.
Parameters
fileDiffFileDiff— The structuredFileDifftable. Binary / oversize files (is_text == false) render as a single suppression line including the size delta when available.
Returns string — The unified-diff body as a single string (no trailing newline).
print(TextDiff.toUnifiedText(d))
typed/builtin//modules/toml/toml/encode
toml.encode(root: { [string]: any }) -> string
Encode a Luau table as canonical TOML bytes. Top-level string-keyed sub-tables become section headers ([name]); deeper string-keyed tables become dotted sections ([a.b]). A sequence whose every element is a non-empty string-keyed table becomes one [[a.b]] block per element, after the section's scalars. Other sequence tables are emitted as inline arrays, and string-keyed tables in value position (inside an array or a block) as inline tables ({ k = v }). Section + key order is alphabetical so the same input always produces the same bytes.
Parameters
root{ [string]: any }— The table to encode. Must be string-keyed at the root.
Returns string — A TOML-formatted string suitable for vfs.write.
local body = toml.encode({ render = { culling_mode = "gpu" } })
typed/builtin//modules/toml/toml/parse
toml.parse(src: string) -> { [string]: any }
Parse a TOML document into a nested Luau table. Sections ([a.b]) become nested tables; key/value pairs become entries on the current section (or root if before any section header). Arrays of tables ([[a.b]]) become sequences of tables, one per block, and a section after a block ([a.b.c]) is a sub-table of that block's row. Throws with the line number on syntax errors.
Parameters
srcstring— TOML source bytes as a string.
Returns { [string]: any } — The parsed root table. Sub-tables are plain Luau tables; arrays are 1-indexed sequence tables.
local t = toml.parse('[a]\nx = 1\ny = "hi"\n')
typed/builtin//modules/toml/toml/replaceArrayTables
toml.replaceArrayTables(src: string, name: string, rows: { { [string]: any } }) -> string
Replace every [[name]] block in a TOML document with rows. A block
is its [[name]] header and the lines after it up to the next header
that is neither [name.…] nor [[name.…]] — those sub-tables go with
their row. Comment lines between a block's last key and the header that
ends it stay, with that header; a line inside a multi-line array value
belongs to that value. Every line outside the blocks is kept byte for
byte, including its line ending. When nothing is removed and rows is
empty the document is returned unchanged; otherwise blank lines at the
end are dropped, and each row is appended as a [[name]] block after
one blank line, in the document's own line ending (CRLF when it uses
CRLF).
Parameters
srcstring— The document's text.namestring— The top-level array-of-tables name, e.g. "presets".rows{ { [string]: any } }— The rows, each a string-keyed table.
Returns string — The new document text.
local path = "/zero/source/.world_settings"
typed/builtin//modules/tools/M/__useVia
M.__useVia(via: string, toolbox: string, tool: string, ...: any?) -> ...any
tools.use for a call site written as editorTools("<toolbox>").<tool>(...):
the toolbox the site recorded under via is the one reached.
Parameters
viastring— The reference kind the call site recorded ("editorTools"or"tools.use").toolboxstring— The toolbox name the site wrote.toolstring— The tool name....any(optional)
Returns ...any — Everything the tool returned. Raises on an unknown toolbox / tool or a tool-reported failure.
__useVia("editorTools", "entityOps", "spawn", { Model = { model = "cube" } })
typed/builtin//modules/tools/M/bind
M.bind(identity: string, positional: { any }?, named: { [string]: any }?, opts: BindOpts?) -> BindResult
Resolve a call's arguments against a tool's declared parameters,
turning named arguments into the positional call the tool actually
takes. This is the binder behind zero <toolbox> <tool> --name value
and the named args an MCP caller passes, so a name resolves the
same way whichever surface the caller reached for. Reads the schema
from tools.get, matches each name to a parameter (exactly first,
then case-insensitively), and reports the first unresolvable name,
a name that a positional argument already filled, a call longer than
the signature, and a required parameter skipped over while a later
one is filled. Resolves the call only — running it is tools.use.
Parameters
identitystring— Tool identity ("<toolbox>.<name>", with or without the leadingtools.).positional{ any }(optional) — Arguments already given by position, filling slots from 1.named{ [string]: any }(optional) — Arguments given by name,{ [parameterName]: value }.optsBindOpts(optional) —order— the order to visitnamedin, so the first problem reported is the caller's first (defaults to sorted, for a stable answer).prefix— written in front of every argument name in the failure message,"--"for a shell flag.whole— readnamedas ONE table argument when not one of its keys names a parameter and the first parameter can hold a table, as the first argument written inline when only some of them do, and as arguments named against the parameter list when no parameter could hold the table — where an emptynamedthen resolves to a call with no arguments at all.positionalCount— how many slotspositionalfills, for a caller that passed an explicitniland so cannot be measured by length.
Returns BindResult — { ok, call, count, failure? }. Call the tool with table.unpack(call, 1, count); on failure call is empty and failure carries the reason.
tools.bind("camera.lookAt", {}, { target = { 0, 5, 0 } })
tools.bind("MaterialAuthor.fromColor", {}, { color = { 1, 0, 0 } }, { whole = true })
typed/builtin//modules/tools/M/callable
M.callable(toolbox: string, tool: string) -> string?
The identity a registered tool is called by — <toolbox>.<tool>, the
pair tools.use takes — or nil when no toolbox registered under
toolbox carries tool. Reads the bound toolbox proxies, so it answers
for what this engine can call rather than for what the asset tree holds:
a .tool folder whose source never registered a function answers nil.
A name several toolboxes share reaches no one tool and answers nil too;
tools.toolboxesNamed names the toolboxes it reaches.
Parameters
toolboxstring— The toolbox's registered name ("zm", a library toolbox's scoped identity"@lib::ns", or a toolbox's canonical identity"pkg.blender").toolstring— The tool's leaf name ("status").
Returns string? — "<toolbox>.<tool>", or nil when the registry carries no such tool.
local callable = tools.callable("zm", "status") -- "zm.status"
typed/builtin//modules/tools/M/create
M.create(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, signature: string?, error: string? }
Create a new tool on disk inside an EXISTING toolbox.
Scaffolds the .tool/ folder via asset.create("tool", …),
then writes the supplied code wrapped in a documented typed
function into init.luau (the --!desc/--!arg/--!return/
--!example doc block + signature built from the structured
metadata), the tags into .metadata, and a brief README.md
— so the authored tool is indistinguishable from a builtin.
Errors cleanly if the parent toolbox doesn't exist — call
tools.createToolbox first so the toolbox starts out with a
real description instead of the placeholder template body.
The engine's normal hot-reload pipeline picks up the new
files and binds the tool's global on the next pass.
Every toolbox created via tools.createToolbox ships a
shared.module/ (the template default has ok/fail
result-envelope constructors; users can extend or replace it
via sharedCode at create time or by editing the module
later). The generated init.luau automatically declares
local shared = require(".shared") before your function
body, so the code you pass can reference shared.ok(...)
/ shared.fail(...) (or whatever the toolbox's custom
helpers expose) directly. If a toolbox doesn't have a
shared.module/ for some reason, the require is skipped so
there's no dangling import to fail.
Parameters
args{ [string]: any }— Structured tool definition. Required:name(leaf, e.g."hello"),toolbox(parent toolbox, e.g."mytools"),code(the Luau function body — not a full module, just the statements that will become the function's body),description(one or more sentences describing what the tool does). Optional but recommended:args(array of{name, type, description}argument records),returns({type, description}for the return value),examples(array of call-site code strings),tags(array of strings for search).
Returns { ok: boolean, path: string?, identity: string?, signature: string?, error: string? } — { ok, path?, identity?, signature?, error? }. path — the new .tool/ folder's VFS path. identity — the tool's <toolbox>.<name> identity. signature — the derived name(args) -> ret signature.
tools.create({ name = "hello", toolbox = "mytools", description = "Greet.", code = "return 'hi'" })
typed/builtin//modules/tools/M/createToolbox
M.createToolbox(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, error: string? }
Create a new toolbox folder. A toolbox is the namespace
container for tools — .toolbox/ on disk; once tools are authored
inside it they are invoked as tools.use("<name>", "<toolName>", …).
This call scaffolds the .toolbox/ folder via
asset.create("toolbox", …) and overwrites the placeholder
README.md with a real description so the toolbox doesn't ship
the template stub. Optionally seeds shared.module/ if the
caller supplies cross-tool helper code. Authoring a tool inside
this toolbox is the separate tools.create call.
Parameters
args{ [string]: any }— Structured toolbox definition. Required:name(the toolbox's leaf name, e.g."mytools"— becomes<name>.toolbox/on disk and the namespace for tool identities),description(one or more sentences describing the surface this toolbox exposes — what problem space the tools cover and who calls them). Optional:sharedCode— replacement body for the toolbox'sshared.module/init.luau. The toolbox template ALWAYS ships ashared.module/with defaultok/failresult-envelope constructors so tools in this toolbox canrequire(".shared")from day one. SupplysharedCodeonly when you want to override that default with custom cross-tool helpers (parsers, registries, …); the module body is left untouched if you omit it.path— explicit VFS destination (/zero/source/...form). Defaults to/zero/source/tools/<name>if omitted; pass an explicit path to author a library-scoped or package-internal toolbox.
Returns { ok: boolean, path: string?, identity: string?, error: string? } — { ok, path?, identity?, error? }. path — the new .toolbox/ folder's VFS path. identity — the toolbox's registered identity (<name>).
tools.createToolbox({ name = "mytools", description = "Custom tooling for my workflow." })
typed/builtin//modules/tools/M/delete
M.delete(identity: string) -> { ok: boolean, path: string?, error: string? }
Remove a previously-authored tool by identity. Deletes the
.tool/ folder and its contents from the VFS via
vfs.remove(..., { recursive = true }). The engine drops the
tool's global on the next hot-reload pass. Refuses to operate
on a path that doesn't exist (returns { ok = false } with
an explanatory error).
Parameters
identitystring— Tool identity ("<toolbox>.<name>").
Returns { ok: boolean, path: string?, error: string? } — Table shaped { ok, path?, error? }.
tools.delete("mytools.hello")
typed/builtin//modules/tools/M/get
M.get(identity: string) -> ToolMeta?
Read back a tool's assembled metadata — description, typed
signature, per-argument docs, return, examples, and tags — gathered
from the four canonical sources in its .tool/ folder. Individual
tools aren't entries in the asset index (only their parent toolboxes
are), so this resolves the toolbox via asset.resolve(toolbox, "toolbox") and reads the tool relative to it. Returns nil when the
toolbox or tool is missing.
Parameters
identitystring— Tool identity ("<toolbox>.<name>", with or without the leadingtools.).
Returns ToolMeta? — The assembled metadata { name, signature?, description, args, varargs, returns?, examples, typeDefs, tags }, or nil if not found. Each entry in args is { name, type, description, optional } in signature order, so a caller can tell a required parameter from an optional one without parsing the rendered signature. varargs is true when the tool accepts trailing arguments beyond the named parameters. typeDefs carries { name, definition } for every named type the signature refers to, resolved transitively, so a signature reading spawn(opts: SpawnOpts) can be called without opening the tool's source.
local meta = tools.get("entityOps.modify"); print(meta.signature)
typed/builtin//modules/tools/M/list
M.list(tier: (number | string)?, toolbox: string?) -> { stdout: string, value: any }
Discover registered code-mode tools, grouped by toolbox.
Default tier returns just { <toolbox> = { name, name, … } }
plus a formatted stdout listing every toolbox on one line
with its tools — compact enough that listing the whole
catalogue doesn't flood agent context. Higher tiers enrich
each entry with its one-line description (tier 2) or its full
assembled metadata — signature, args, returns, examples (tier 3).
Pass a toolbox name to restrict the output to one toolbox.
Parameters
tier(number | string)(optional) — Verbosity level:1(default) toolbox summary,2adds one-line descriptions,3adds the full assembled metadata. Pass a string instead to restrict to that toolbox at tier 1.toolboxstring(optional) — Optional toolbox name to restrict the output to (e.g."entityOps"). Passing a string as the first arg also works.
Returns { stdout: string, value: any } — Table shaped { stdout: string, value: <grouped> }. For tier 1, value is { [toolbox] = { toolName, … } }. For tier ≥ 2, value is { [toolbox] = { { name, identity, signature?, description, … }, … } }.
tools.list() -- default: every toolbox, names only
tools.list(2) -- include first-line descriptions
tools.list("entityOps") -- only the `entityOps` toolbox at tier 1
tools.list(2, "entityOps") -- only `entityOps`, with descriptions
typed/builtin//modules/tools/M/miss
M.miss(toolbox: string, tool: string?) -> string
What every tool surface says when a toolbox and tool reach no single
tool: a toolbox name several toolboxes share names each toolbox it
reaches, a toolbox with no tool of that name lists the tools it holds with
the nearest one, a tool's name written as the toolbox names the
toolboxes holding that tool, and any other name reaching no toolbox lists
the nearest toolboxes. tools.use, the describe_tool MCP tool and
zero in the engine shell all refuse with this reason, so the same name
is refused the same way wherever it is written.
Parameters
toolboxstring— The toolbox name the caller wrote.toolstring(optional) — The tool name the caller wrote, when there is one.
Returns string — The reason, as one sentence the caller can act on.
tools.miss("blender", "probe")
typed/builtin//modules/tools/M/registeredToolbox
M.registeredToolbox(name: string) -> string?
The name a drill-in lists a registered toolbox by, or nil when the
registry holds no toolbox under name. Reads the bound toolbox proxies,
the same ones tools.callable reads, so it answers for what this engine
can call rather than for what the asset tree holds. A name several
toolboxes share reaches no one toolbox and answers nil;
tools.toolboxesNamed names the toolboxes it reaches.
Parameters
namestring— The toolbox's registered name ("zm", a library toolbox's scoped identity"@lib::ns", or a toolbox's canonical identity"pkg.blender").
Returns string? — name, or nil when the registry holds no such toolbox.
local box = tools.registeredToolbox("zm") -- "zm"
typed/builtin//modules/tools/M/search
M.search(query: string?, opts: { toolbox: string?, limit: number?, mode: string?, from: string? }?) -> { stdout: string, value: any, total: number?, containers: { any }? }
Search registered code-mode tools, the canonical in-engine
tool-discovery entry point — callable from execute Luau so an agent
can find the tool it needs without leaving the engine. A query is
answered by @builtin::modules.discovery (searchTools), the question
search_tools { query } asks. In semantic mode on an engine bound to
a world, the search core ranks the tools, toolboxes and workflows of the
whole published library, then this world's own matches follow; a hit
falls by how closely it means what was asked, so a tool is found by what
it does rather than by the words it happens to use. In keyword mode,
and on an engine bound to no world, the corpus is the files this engine
holds. In keyword mode a hit falls by what the query's words the
asset carried as words of their own weigh, a word weighing more the fewer
of the assets read carry it, then by the most of them it
carried side by side in the query's order, then by how many it carried
anywhere, then by the order its type declares the role that matched, then
by name (discovery.keyword). With an empty query the toolbox
named is listed, or the whole catalogue when none is. Each row's
identity is the tool's <toolbox>.<tool> identity; invoke it with
tools.use("<toolbox>", "<tool>", ...), from Luau or inside a tool_exec
script, the form each row's examples are rendered in, or in the engine
shell as zero <toolbox> <tool> [args].
Parameters
querystring(optional) — Free-text search string, matched against every file each type's indexing recipe names — for a tool, its README and its source. Empty or nil lists the catalogue, or the toolboxopts.toolboxnames.opts{ toolbox: string?, limit: number?, mode: string?, from: string? }(optional) — Optional filters table.toolbox— keep only rows whose owning toolbox exactly matches.limit— maximum rows to return (default10; pass a larger value to return more).mode—"semantic"ranks by what a tool means,"keyword"by the words it carries. Omit it fordiscovery.defaultMode():semanticwherever a world is bound,keywordwhere none is and the only corpus is the files this engine holds.from— WHERE results may come from. 'all' (default) is everything published. 'bound' is the current world and its installed libraries, answered by this engine with no published-library leg;modestill picks how the query is matched, and mode='keyword' reads this engine's own files, unpushed ones and a private world's included.
Returns { stdout: string, value: any, total: number?, containers: { any }? } — Table shaped { stdout: string, value: { SearchRow } }. Each row is { identity, name, assetType, description, toolbox?, registered?, signature?, tags?, examples?, matched?, guid?, installed?, use?, install?, p?, container_key?, system_key?, from? }. A query row says whether this world holds the hit (installed) and the one call that acts on it (use: the tool_exec call that runs it through tools.use, a search_tools { toolbox } drill-in, the workflow toolbox's start through tool_exec, or for a hit this world does not hold bash { command: "zm ls-tree @<guid>" }, the shell read of its implementation, with install the zeromind_install call for the candidate the reading shows fits); from = "world" marks a match from this world's own content after a library ranking. A tool row's identity is the <toolbox>.<tool> a call names; a toolbox row's is the toolbox's name, which toolbox repeats and a drill-in takes. registered says whether the registry holds what identity names: the function a call would run, or the toolbox a drill-in lists. matched is the role and file a query hit matched in and what it ranked on (weight, terms, words, together); a listing carries none. stdout is a one-line summary followed by one line per row. total, on a query naming no toolbox, is how many matched in all, of which value holds the best limit. containers, beside rows the search core ranked, is the tier their container_key and system_key point into: each entry { key, guid, name, assetType, parent_key?, description? } is a container a row ships in, naming the one above it by parent_key, so a tool's container_key is its toolbox and its system_key the package that toolbox ships in.
tools.search("spawn camera")
tools.search("material", { limit = 5 })
tools.search("", { toolbox = "physics" }) -- list a toolbox
tools.search("") -- list everything
tools.search("spawn camera", { mode = "keyword" })
typed/builtin//modules/tools/M/toolboxes
M.toolboxes() -> { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } }
Discover the registered toolboxes as a grouped overview — one
row per toolbox with its one-line purpose and tool count. The
toolbox-first entry point to tool discovery: an agent navigates by
DOMAIN (which toolbox), then drills into a toolbox's tools with
tools.search("", { toolbox = "<name>" }). purpose is sourced from
the toolbox's README.md (its first descriptive line), falling back
to the .metadata description.
Returns { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } } — Table shaped { stdout: string, value: { { toolbox, purpose, toolCount } } }. value is toolbox-name-sorted; each entry is { toolbox = "<name>", purpose = "<one line>", toolCount = <n> }.
tools.toolboxes()
typed/builtin//modules/tools/M/toolboxesNamed
M.toolboxesNamed(name: string) -> { string }
The toolboxes a toolbox name reaches, each spelled the way a call
reaches it alone. A toolbox is bound under its folder's name, so two
toolbox folders with the same name — one in each of two packages, say —
share it: that name reaches both, and each is then reached by its own
canonical identity (pkgA.blender, pkgB.blender) instead. Every tool
surface reads this answer: the listing names each toolbox by it, and a
call or lookup through a name reaching more than one is refused naming
each (tools.miss states the refusal).
Parameters
namestring— A toolbox name ("entityOps"), a library toolbox's scoped key ("@lib::ns"), or any other name the toolbox asset answers to — its canonical identity or its path.
Returns { string } — The names a call reaches each toolbox by, sorted: one entry when the name reaches one toolbox, several when it reaches more, none when it reaches no toolbox holding a tool.
tools.toolboxesNamed("blender") -- { "pkgA.blender", "pkgB.blender" }
typed/builtin//modules/tools/M/tryUse
M.tryUse(toolbox: string, tool: string, ...: any?) -> ToolCall
Invoke one code-mode tool and report the outcome as a value. Takes the
same arguments as tools.use and resolves the toolbox the same way, and
returns { ok, value, error, toolbox, tool } for every outcome — an
unknown toolbox, an unknown tool, a tool that reported failure, and a tool
that raised all arrive as ok = false with the reason in error. The
ok / value / error fields are the ones a tool_exec answer
reports, so a script comparing several calls in one pass reads the same
names it would over MCP, and reads them without wrapping each call in pcall.
Parameters
toolboxstring— The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").toolstring— The tool's leaf name within that toolbox ("spawn")....any(optional)
Returns ToolCall — { ok, value, error, toolbox, tool }. value carries the tool's own value when ok is true; error carries the reason when it is false.
tools.tryUse("entityOps", "spawn", { Model = { model = "cube" } })
typed/builtin//modules/tools/M/use
M.use(toolbox: string, tool: string, ...: any?) -> ...any
Invoke one code-mode tool by naming its toolbox and tool explicitly.
The same call runs from engine Luau (editor panels, shipped modules,
execute) and inside an MCP tool_exec script
(return await tools.use("<toolbox>", "<tool>", ...)), and
zero <toolbox> <tool> [args] is its engine-shell spelling. Resolves the toolbox
from the runtime store (builtin, library, and user-authored runtime
toolboxes all work), calls the named tool with the remaining args, and
returns the tool's value directly — unwrapping the ZmToolResult
envelope and RAISING a Luau error when the tool fails. Because every call
names both toolbox and tool, it can never read as a tools.<box>
namespace. tools.tryUse takes the same arguments and reports the
outcome as a value instead of raising.
Parameters
toolboxstring— The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").toolstring— The tool's leaf name within that toolbox ("spawn")....any(optional)
Returns ...any — Everything the tool returned, in the order it returned it — a tool declaring (path, reason, frame) hands back all three, so a second and third value the tool states are read the way the tool's own signature says. Raises on an unknown toolbox / tool or a tool-reported failure. These are the tool's OWN values, not an { ok, value } envelope; reach for tools.tryUse when the call's outcome is what you want.
tools.use("entityOps", "spawn", { Model = { model = "cube" } }, { position = {0, 2, 0} })
typed/builtin//modules/transform/T/direction
T.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
fromXnumber— From x.fromYnumber— From y.fromZnumber— From z.toXnumber— To x.toYnumber— To y.toZnumber— To z.
Returns (number, number, number) — Three numbers dx, dy, dz — the unit direction.
local dx, dy, dz = Transform.direction(0, 0, 0, 1, 0, 0)
typed/builtin//modules/transform/T/directionBetween
T.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
entityAstring | EntityRef— Source entity (id string or proxy).entityBstring | EntityRef— Target entity (id string or proxy).
Returns (number, number, number) — Three numbers dx, dy, dz — the unit direction.
local dx, dy, dz = Transform.directionBetween("cam", "target")
typed/builtin//modules/transform/T/distance
T.distance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number) -> number
Euclidean distance between two world-space positions.
Parameters
x1number— First point x.y1number— First point y.z1number— First point z.x2number— Second point x.y2number— Second point y.z2number— Second point z.
Returns number — The Euclidean distance.
local d = Transform.distance(0, 0, 0, 1, 1, 1)
typed/builtin//modules/transform/T/distanceBetween
T.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
entityAstring | EntityRef— First entity (id string or proxy).entityBstring | EntityRef— Second entity (id string or proxy).
Returns number? — The Euclidean distance, or nil when either entity can't be resolved.
local d = Transform.distanceBetween("cam", "box")
typed/builtin//modules/transform/T/euler
T.euler(qx: number, qy: number, qz: number, qw: number) -> (number, number, number)
Convert quaternion to euler angles (yaw, pitch, roll) in radians.
Parameters
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.
Returns (number, number, number) — Three numbers yaw, pitch, roll (Y, X, Z rotations).
local yaw, pitch, roll = Transform.euler(0, 0, 0, 1)
typed/builtin//modules/transform/T/eulerToQuat
T.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
yawnumber— Y-axis rotation in radians.pitchnumber(optional) — X-axis rotation in radians. Defaults to 0.rollnumber(optional) — Z-axis rotation in radians. Defaults to 0.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.eulerToQuat(math.pi / 2)
typed/builtin//modules/transform/T/lerp
T.lerp(ax: number, ay: number, az: number, bx: number, by: number, bz: number, t: number) -> (number, number, number)
Linearly interpolate between two positions.
Parameters
axnumber— Start x.aynumber— Start y.aznumber— Start z.bxnumber— End x.bynumber— End y.bznumber— End z.tnumber— Interpolation factor[0, 1].
Returns (number, number, number) — Three numbers — the interpolated position.
local x, y, z = Transform.lerp(0, 0, 0, 1, 1, 1, 0.5)
typed/builtin//modules/transform/T/lerp1
T.lerp1(a: number, b: number, t: number) -> number
Linearly interpolate two scalars.
Parameters
anumber— Start value.bnumber— End value.tnumber— Interpolation factor[0, 1].
Returns number — The interpolated scalar.
local v = Transform.lerp1(0, 10, 0.5)
typed/builtin//modules/transform/T/lerpAngle
T.lerpAngle(a: number, b: number, t: number) -> number
Lerp between two angles via the shortest arc; returns a value in [-pi, pi].
Parameters
anumber— Start angle in radians.bnumber— End angle in radians.tnumber— Interpolation factor[0, 1].
Returns number — The interpolated angle, normalized to [-pi, pi].
local a = Transform.lerpAngle(0, math.pi, 0.5)
typed/builtin//modules/transform/T/localToWorld
T.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
pxnumber— Parent position x.pynumber— Parent position y.pznumber— Parent position z.pqxnumber— Parent rotation x.pqynumber— Parent rotation y.pqznumber— Parent rotation z.pqwnumber— Parent rotation w.lxnumber— Local x.lynumber— Local y.lznumber— Local z.
Returns (number, number, number) — Three numbers wx, wy, wz — the world position.
local wx, wy, wz = Transform.localToWorld(px, py, pz, pqx, pqy, pqz, pqw, lx, ly, lz)
typed/builtin//modules/transform/T/lookAt
T.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
entityOrIdstring | EntityRef— Entity id, name, or proxy for the entity to rotate.txOrTargetany(optional) — A number (world x), a point table, or an entity id / name / proxy whose world position is resolved as the look-at target.tyany(optional) — World y of the target. Omitted whentxOrTargetis a point or an entity.tznumber(optional) — World z of the target. Omitted whentxOrTargetis a point or an entity.upany(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.
Returns (boolean, string?) — True when the entity's world rotation was written, and nil for the second value. The target and up slots take any value, because naming which of the shapes arrived is this call's own job: a value that is none of them comes back as a reason rather than as an error raised out of the argument check. False plus a reason otherwise: "unresolved" when a reference names no entity, "no-transform" when one carries no transform, "incomplete-target" when the target spells no point — coordinates with a y or z missing, or a table carrying neither three numbers nor x/y/z, "incomplete-up" when the up hint spells none either, "degenerate" when the two points coincide so no facing direction exists.
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
typed/builtin//modules/transform/T/lookAtQuat
T.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
fxnumber— Origin x.fynumber— Origin y.fznumber— Origin z.txnumber— Target x.tynumber— Target y.tznumber— Target z.
Returns (number?, number?, number?, number?) — Four numbers qx, qy, qz, qw — the look-at quaternion. Nil when degenerate.
local qx, qy, qz, qw = Transform.lookAtQuat(0, 0, 0, 1, 0, 1)
typed/builtin//modules/transform/T/lookRotation
T.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
fxnumber— Eye x — where the entity stands.fynumber— Eye y.fznumber— Eye z.txnumber— Target x — the world point it faces.tynumber— Target y.tznumber— Target z.uxnumber(optional) — Up hint x. World +Y when the hint is omitted.uynumber(optional) — Up hint y.uznumber(optional) — Up hint z.
Returns (number?, number?, number?, number?) — Four numbers qx, qy, qz, qw. Nil when the eye and the target coincide, so no facing direction exists.
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) }
typed/builtin//modules/transform/T/normalizeAngle
T.normalizeAngle(a: number) -> number
Normalize an angle into [-pi, pi].
Parameters
anumber— The angle in radians.
Returns number — The same angle wrapped into [-pi, pi].
local a = Transform.normalizeAngle(3 * math.pi)
typed/builtin//modules/transform/T/orbit
T.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
centerXnumber— Center x.centerYnumber— Center y.centerZnumber— Center z.radiusnumber— Horizontal distance from the center.heightnumber— Vertical offset fromcenterY.anglenumber— Orbital angle in radians.
Returns (number, number, number, number, number, number, number) — Seven numbers x, y, z, qx, qy, qz, qw.
local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t)
typed/builtin//modules/transform/T/quatFromAxisAngle
T.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
axnumber— Axis x.aynumber— Axis y.aznumber— Axis z.anglenumber— Rotation angle in radians.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.pi)
typed/builtin//modules/transform/T/quatFromBasis
T.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
rxnumber— Right axis x.rynumber— Right axis y.rznumber— Right axis z.uxnumber— Up axis x.uynumber— Up axis y.uznumber— Up axis z.fxnumber— Forward axis x.fynumber— Forward axis y.fznumber— Forward axis z.
Returns (number, number, number, number) — x, y, z, w of the rotation quaternion.
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)
typed/builtin//modules/transform/T/quatFromYaw
T.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
yawnumber— Rotation in radians around the Y axis.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.quatFromYaw(math.pi / 2)
typed/builtin//modules/transform/T/quatFromYawPitch
T.quatFromYawPitch(yaw: number, pitch: number) -> (number, number, number, number)
Create quaternion from yaw and pitch in radians.
Parameters
yawnumber— Y-axis rotation in radians.pitchnumber— X-axis rotation in radians.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.quatFromYawPitch(0, math.pi / 4)
typed/builtin//modules/transform/T/quatIdentity
T.quatIdentity() -> (number, number, number, number)
Identity quaternion (0, 0, 0, 1).
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the identity.
local qx, qy, qz, qw = Transform.quatIdentity()
typed/builtin//modules/transform/T/quatInverse
T.quatInverse(qx: number, qy: number, qz: number, qw: number) -> (number, number, number, number)
Quaternion inverse. Equal to the conjugate for unit quaternions.
Parameters
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the inverse.
local ix, iy, iz, iw = Transform.quatInverse(qx, qy, qz, qw)
typed/builtin//modules/transform/T/quatMul
T.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
axnumber— Left quat x.aynumber— Left quat y.aznumber— Left quat z.awnumber— Left quat w.bxnumber— Right quat x.bynumber— Right quat y.bznumber— Right quat z.bwnumber— Right quat w.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the composed quaternion.
local qx, qy, qz, qw = Transform.quatMul(ax, ay, az, aw, bx, by, bz, bw)
typed/builtin//modules/transform/T/quatRotateVec
T.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
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.vxnumber— Vector x.vynumber— Vector y.vznumber— Vector z.
Returns (number, number, number) — Three numbers — the rotated vector.
local rx, ry, rz = Transform.quatRotateVec(qx, qy, qz, qw, 1, 0, 0)
typed/builtin//modules/transform/T/quatToEuler
T.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
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.
Returns (number, number, number) — Three numbers yaw, pitch, roll (Y, X, Z rotations).
local yaw, pitch, roll = Transform.quatToEuler(qx, qy, qz, qw)
typed/builtin//modules/transform/T/readVec3
T.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
valueVec3Input— The vector to normalize.labelstring(optional) — Name reported in the error when the value is not a vector. Defaults to "Transform".
Returns { number } — A three-element array { x, y, z }.
local v = Transform.readVec3({ x = 1, y = 2, z = 3 })
typed/builtin//modules/transform/T/slerp
T.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
axnumber— Start quaternion x.aynumber— Start quaternion y.aznumber— Start quaternion z.awnumber— Start quaternion w.bxnumber— End quaternion x.bynumber— End quaternion y.bznumber— End quaternion z.bwnumber— End quaternion w.tnumber— Interpolation factor[0, 1].
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the interpolated unit quaternion.
local qx, qy, qz, qw = Transform.slerp(0, 0, 0, 1, 1, 0, 0, 0, 0.5)
typed/builtin//modules/transform/T/snapVec3
T.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 }.stepnumber | Vec3Input— Uniform step size, or a per-axis vector of step sizes.
Returns { number } — A three-element array { x, y, z } snapped to the step grid.
local v = Transform.snapVec3({ 1.4, 2.6, -0.4 }, 1)
typed/builtin//modules/transform/T/toQuaternion
T.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
rotationany(optional) — The rotation to normalize, in any form of theRotationInputunion.labelstring(optional) — Name reported in the error when the value is not a rotation. Defaults to "Transform".
Returns { number } — A four-element array { qx, qy, qz, qw }.
local q = Transform.toQuaternion({ pitch = 0, yaw = 90, roll = 0 })
typed/builtin//modules/transform/T/tryQuaternion
T.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
rotationany(optional) — The value to read as a rotation.labelstring(optional) — Name reported in the message. Defaults to "Transform".
Returns ({ number }?, string?) — The quaternion { qx, qy, qz, qw }, or nil and the message.
local q, why = Transform.tryQuaternion(value, "myTool")
typed/builtin//modules/transform/T/vec/add
T.vec.add(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> (number, number, number)
Component-wise vec3 addition.
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns (number, number, number) — Three numbers — the sum.
local x, y, z = Transform.vec.add(1, 2, 3, 4, 5, 6)
typed/builtin//modules/transform/T/vec/cross
T.vec.cross(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> (number, number, number)
Cross product a x b.
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns (number, number, number) — Three numbers cx, cy, cz — the cross product.
local cx, cy, cz = Transform.vec.cross(1, 0, 0, 0, 1, 0)
typed/builtin//modules/transform/T/vec/dot
T.vec.dot(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> number
Dot product of two vec3s.
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns number — The scalar dot product.
local d = Transform.vec.dot(1, 0, 0, 0, 1, 0)
typed/builtin//modules/transform/T/vec/length
T.vec.length(x: number, y: number, z: number) -> number
Euclidean length of a vec3.
Parameters
xnumber— Vector x.ynumber— Vector y.znumber— Vector z.
Returns number — The length.
local len = Transform.vec.length(1, 2, 3)
typed/builtin//modules/transform/T/vec/normalize
T.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
xnumber— Vector x.ynumber— Vector y.znumber— Vector z.
Returns (number, number, number) — Three numbers — the unit-length vec3.
local nx, ny, nz = Transform.vec.normalize(0, 5, 0)
typed/builtin//modules/transform/T/vec/scale
T.vec.scale(x: number, y: number, z: number, s: number) -> (number, number, number)
Component-wise scalar multiplication of a vec3.
Parameters
xnumber— Vector x.ynumber— Vector y.znumber— Vector z.snumber— Scalar factor.
Returns (number, number, number) — Three numbers — the scaled vec3.
local x, y, z = Transform.vec.scale(1, 2, 3, 2)
typed/builtin//modules/transform/T/vec/sub
T.vec.sub(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> (number, number, number)
Component-wise vec3 subtraction (a - b).
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns (number, number, number) — Three numbers — the difference.
local x, y, z = Transform.vec.sub(4, 5, 6, 1, 2, 3)
typed/builtin//modules/transform/T/worldToLocal
T.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
pxnumber— Parent position x.pynumber— Parent position y.pznumber— Parent position z.pqxnumber— Parent rotation x.pqynumber— Parent rotation y.pqznumber— Parent rotation z.pqwnumber— Parent rotation w.wxnumber— World x.wynumber— World y.wznumber— World z.
Returns (number, number, number) — Three numbers lx, ly, lz — the local position.
local lx, ly, lz = Transform.worldToLocal(px, py, pz, pqx, pqy, pqz, pqw, wx, wy, wz)
typed/builtin//modules/tween/easing/E/list
E.list() -> { string }
List every canonical easing name (camelCase form). Useful for pickers / UI.
Returns { string } — Array of names, e.g. { "linear", "inQuad", "outQuad", ... }.
for _, n in ipairs(Easing.list()) do print(n) end
typed/builtin//modules/tween/easing/E/resolve
E.resolve(easing: string | EasingFn) -> EasingFn
Resolve an easing name (string) or function to an easing
function. Pass-through if easing is already a function. Raises
on unknown name or unsupported type.
Parameters
easingstring | EasingFn— Either an easing name (case-insensitive, optionaleaseprefix), or anEasingFn(returned unchanged).
Returns EasingFn — The resolved EasingFn.
local fn = Easing.resolve("easeInOutQuad")
local fn = Easing.resolve(function(t) return t * t end)
typed/builtin//modules/ui_motion_capture/M/register
M.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()
typed/builtin//modules/vfs_async_read/M/installInto
M.installInto(vfs: VfsNamespace)
Install the yielding read wrapper onto the supplied vfs-shaped
namespace. The vfs API module calls this once on itself as it
loads; users shouldn't call it directly. No-op if the target
doesn't expose both read and readAsync as functions. A miss the
target's read answers nil, "pending" for raises
VFS_READ_CANNOT_WAIT where the reading code cannot yield, and is
waited for where it can. Every read that ends without bytes answers
nil and the reason the target's read gave beside its nil. The
target's list is wrapped the same way: a listing it answers
nil, "pending" for is waited for where the code can yield and
raises VFS_READ_CANNOT_WAIT where it cannot.
Parameters
vfsVfsNamespace— The target namespace table. Must carryreadandreadAsyncfunction fields (the engine'svfsglobal does); otherwise the install is a no-op.
require("@builtin::modules.vfs_async_read").installInto(vfs)
typed/builtin//modules/world_defaults/M/offLoaded
M.offLoaded(handle: number) -> boolean
Stop a callback registered with world.onLoaded from running.
Parameters
handlenumber— The handleworld.onLoadedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offLoaded(h)
typed/builtin//modules/world_defaults/M/offSaved
M.offSaved(handle: number) -> boolean
Stop a callback registered with world.onSaved from running.
Parameters
handlenumber— The handleworld.onSavedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offSaved(h)
typed/builtin//modules/world_defaults/M/offUnloaded
M.offUnloaded(handle: number) -> boolean
Stop a callback registered with world.onUnloaded from running.
Parameters
handlenumber— The handleworld.onUnloadedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offUnloaded(h)
typed/builtin//modules/world_defaults/M/onLoaded
M.onLoaded(cb: (...any) -> ()) -> number
Register a callback to run after a world finishes loading.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offLoaded.
local h = world.onLoaded(function() log.info("loaded") end)
typed/builtin//modules/world_defaults/M/onSaved
M.onSaved(cb: (...any) -> ()) -> number
Register a callback to run after a world is saved.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offSaved.
local h = world.onSaved(function() log.info("saved") end)
typed/builtin//modules/world_defaults/M/onUnloaded
M.onUnloaded(cb: (...any) -> ()) -> number
Register a callback to run after a world is unloaded.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offUnloaded.
local h = world.onUnloaded(function() log.info("unloaded") end)
typed/builtin//modules/world_defaults/M/reload
M.reload() -> { started: boolean?, guid: string?, branch: string?, error: string? }
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 { error }.
Returns { started: boolean?, guid: string?, branch: string?, error: string? } — { started = true, guid, branch } or { error = string }.
world.reload()
typed/builtin//modules/world_sync/M/installInto
M.installInto(world: WorldNamespace)
Graft syncStatus / resolveConflict onto the supplied world
namespace. The prelude calls this once at boot.
Parameters
worldWorldNamespace— The target namespace — typically the engine'sworldglobal.
require("@builtin::modules.world_sync").installInto(world)
typed/builtin//modules/world_sync/world/dropOwedWrites
world.dropOwedWrites(path: string?) -> { string }
Give up the changes this session still owes the world at path
and beneath it: the writes and removals vfs.pendingWrites() lists
there (a write the world never took included), the ones
world.syncStatus() names as conflicts or losses, and every stored
record of them on this machine, so no later boot re-applies one. Each
path then reads as the world's row holds it: the row's content, or
nothing where the world has no row. Returns the paths given up. The
bytes of a given-up change are not kept.
Parameters
pathstring(optional) — Canonical/sourcepath; defaults to the whole source tree.
Returns { string } — The paths given up, sorted.
world.dropOwedWrites()
world.dropOwedWrites("/zero/source/hud")
typed/builtin//modules/world_sync/world/resolveConflict
world.resolveConflict(path: string, mode: string, content: string?) -> ResolveResult
Resolve one /source record this session owes the world, one
path at a time: a conflicted one, or one whose write or removal the
server refused or the queue gave up on (unsaved_writes in
world.syncStatus()), which waiting never carries and which holds a
world.swap or world.checkout away from the world until it is
settled here.
mode is merge | apply | take-local | take-backend |
discard. merge returns { merged, clean } and mutates nothing
— a clean merge can be finalized with apply, and a conflicted one
carries <<<<<<< / ======= / >>>>>>> markers to edit first. apply
writes the finalized content to /source; take-local writes the
retained local bytes; take-backend / discard keep the backend
head. For a removal (deleted in world.syncStatus().conflicts),
take-local removes the path again and merge errors, since a
removal holds no content to merge. Errors when the record is absent,
a merge has no common ancestor or hits binary content, or a write
fails.
Parameters
pathstring— Canonical/sourcepath of the conflicted record.modestring— One of merge | apply | take-local | take-backend | discard.contentstring(optional) — Finalized bytes forapplymode.
Returns ResolveResult
world.resolveConflict("/zero/source/foo.luau", "take-local")
local r = world.resolveConflict(p, "merge"); if r.clean then world.resolveConflict(p, "apply", r.merged) end
typed/builtin//modules/world_sync/world/syncStatus
world.syncStatus() -> SyncStatus
Read the durable-sync status: { subscribed, content_synced, content_sync_held, world_entry_held, outgoing_synced, uploads_outstanding, uploads_in_flight, progress, pending_writes, unrecorded_writes, owed_paths, unsaved_writes, held_uploads, uploads_abandoned, conflicts, binding }. Content sync runs in two
directions and each has its own answer: content_synced is incoming,
true once the world's content has been applied to this client, and it
says nothing about this session's own changes. outgoing_synced is
outgoing, true once every /source change this session made has
reached the world: nothing queued, uploading, held for another
attempt, unreflected by the manifest, kept in a durable record or a
conflict, or refused. Read outgoing_synced to know whether your
work is saved; uploads_outstanding counts the upload jobs it still
waits on, and uploads_in_flight the ones among them a flush has
taken and is carrying to the world. Both booleans read false with no
world subscribed.
content_sync_held is true while vfs.holdContentSync(true) holds
the content sync, which keeps content_synced false until released.
world_entry_held is true while world.holdEntry(true) holds the
world entry: a world load whose content is ready waits before the
world's entrypoint runs and its startup scene loads, with
binding.step reading entryHeld, until it is released.
owed_paths lists the /source paths this session wrote or removed
that the server has not taken yet, the same list vfs.pendingWrites()
returns.
conflicts maps each conflicted /source path to { base_sha, local_sha, backend_sha, isBinary, deleted };
deleted is true when the author's side is a removal of the path.
unsaved_writes lists the /source paths this session wrote or
removed whose change the server never took, and uploads_abandoned counts the uploads
the queue stopped carrying — read those two to tell a queue working
through a backlog from one that gave content up, which
pending_writes alone reads the same for. held_uploads lists the
owed changes whose upload failed and is being tried again, as
{ path, reason }: a stage or commit of other content goes ahead
past them, one naming such a path waits on it and reports the
reason, and removing the path ends the hold. unrecorded_writes lists
the owed paths whose durable record this machine could not store,
which a restart loses; pending_writes counts the owed records it
did store. binding names which
world holds /source — bound, unbound, binding,
session_only, or unclassified before the boot has decided — and,
when a load attempt is on record,
which attempt is running, the step it is held at and for how long,
what the last one answered, and whether the load stopped. Read
binding to tell a world that is still coming from one that was
never asked for: subscribed answers false for both. Synchronous.
Returns SyncStatus
local s = world.syncStatus(); print(s.outgoing_synced, s.uploads_outstanding)
local s = world.syncStatus(); print(s.pending_writes)
local s = world.syncStatus(); for _, p in ipairs(s.unsaved_writes) do print(p) end
local s = world.syncStatus(); if s.binding.awaiting_world then print(s.binding.state, s.binding.attempt) end
typed/builtin//modules/world_vcs/M/__addAllReport
M.__addAllReport(stagedAfter: { string }, heldBefore: { [string]: boolean }, conflicted: { string }, released: { ReleasedPath }, written: { WrittenPath }) -> AddAllReport
The report world.add_all returns beside its held-back list: the
paths the call added to the staging area, every path it left out with
the reason, the paths it took from other staging areas, and the staged
paths a caller of another account is writing now.
Parameters
stagedAfter{ string }— The paths the staging area holds after the call.heldBefore{ [string]: boolean }— The paths it held before the call, as a set.conflicted{ string }— Paths left out for an unresolved conflict.released{ ReleasedPath }— The paths taken from other staging areas.written{ WrittenPath }— The staged paths a caller of another account is writing now.
Returns AddAllReport — { staged, held = { { path, reason } }, released, written }.
worldVcs.__addAllReport({ "/zero/source/a.luau" }, {}, {}, {}, {})
typed/builtin//modules/world_vcs/M/__answerChangedPathsFrom
M.__answerChangedPathsFrom(paths: any?)
Answer the changed-path read from paths in place of the world's
history and working tree, so a case can state which paths a push carries
on an engine that holds no history. false states that the history
cannot be read, and nil reads the history again.
Parameters
pathsany(optional) — The paths a push carries,false, or nil.
worldVcs.__answerChangedPathsFrom({ "/zero/source/a.module/README.md" })
typed/builtin//modules/world_vcs/M/__answerCommittedAreasFrom
M.__answerCommittedAreasFrom(rows: { CommittedArea }?)
Answer the committed-area read from rows in place of the world's
history, so a case can state which staging area committed an asset
on an engine that holds no history. nil reads the history again.
Parameters
rows{ CommittedArea }(optional) — The rows the read answers with, shaped as__spacetime_vcs.committedAreasreports them, or nil.
worldVcs.__answerCommittedAreasFrom({ { path = p, depth = 0, stage_name = "fauna", answers = true } })
typed/builtin//modules/world_vcs/M/__applyUpdates
M.__applyUpdates(units: any?, opts: UpdateOpts?, ops: ApplyUpdateOps) -> UpdateResult
Apply updates one unit at a time: libraries through the pin move,
installed assets through the install pull. A unit that raises is reported
in its entry with the reason and the next unit runs. Stages, commits and
pushes nothing; the files with uncommitted changes it touched are named in
warnings.
Parameters
unitsany(optional) — Nil for every unit the check lists, a name, or a list of names.optsUpdateOpts(optional) —{ ref }, the ref every unit is updated to, or nil.opsApplyUpdateOps— AnApplyUpdateOps.
Returns UpdateResult — { results, to_commit, skipped, warnings, note }.
worldVcs.__applyUpdates({}, nil, { checkUpdates = function() return {}, {} end, libraries = function() return {} end, roots = function() return {} end, updateLibrary = function() return {} end, pullAsset = function() return {} end, rootVersion = function() return nil end, modifiedPaths = function() return {} end })
typed/builtin//modules/world_vcs/M/__bulkStage
M.__bulkStage(io: BulkStageIo, heldBefore: { [string]: boolean }, before: { [string]: any }) -> BulkStageOutcome
Complete a bulk stage after its one sweep of the dirty set: stage
the rows the sweep left, carry each staged path's asset family, and take
each staged path out of any other staging area holding it. Staging is
advisory, so the stage takes every dirty row whoever wrote it: a row
another caller is writing while its session is open is staged like any
other, and a staged path a caller of another account is writing now is
reported in written. A path is staged in one area at a time, so each
path the stage holds that another area held is taken out of that area,
whichever account opened it and whether or not its session is open, and
reported in released. The backend moves a path out of the area holding
it as it stages it, so the areas holding each path are read before the
sweep (before) as well as after it, and a path an area still holds
after the stage is taken out of it here.
An interior file staged on its own leaves the asset containing it
pinned to what the asset derived before the file changed, so each staged
path brings its asset family: the outermost asset it lies in and every
changed row beneath that asset.
The work is proportional to the dirty set: at most two status reads,
one batch of paths staged and one released per area.
Parameters
ioBulkStageIo— The reads and batches the stage runs on.heldBefore{ [string]: boolean }— The paths the calling area held before the sweep, as a set.before{ [string]: any }— The calling area's status read before the sweep: the areas holding each path, and who wrote each dirty row.
Returns BulkStageOutcome — What the stage took from other areas and from other accounts.
worldVcs.__bulkStage(io, heldBefore, before)
typed/builtin//modules/world_vcs/M/__callerStageNameFrom
M.__callerStageNameFrom(authorId: string?) -> string
The staging area a caller with this identity owns. authorId is the
connection key vfs.currentAuthor().connection answers with: the calling
session where the transport attested one, the ZeroMind account where it
attested only that, whatever name the world's roster gives the caller.
The agents sharing one connection share its area. A caller the transport named has an area of its own under that key;
a caller it did not name acts on the one shared area, because there is
nothing there to tell two such callers apart by.
Parameters
authorIdstring(optional) — The caller key, or nil for a caller the transport did not name.
Returns string — The staging area a call from that caller acts on.
worldVcs.__callerStageNameFrom("client:01M1VW") --> "client:01M1VW"
typed/builtin//modules/world_vcs/M/__carriedContentReport
M.__carriedContentReport(carried: { ContentCarried }) -> string?
Compose what a push published that another staging area answers for: one line per area, naming the assets it holds, and how a push gates on an area's own content. A caller who staged under a name and pushed under none reads their own area's name here, so the line says where an asset gets described rather than who is reading it. Answers nil when a push carried nothing.
Parameters
carried{ ContentCarried }— The rows the content gate attributed to another area.
Returns string? — The report, or nil when there is nothing to say.
worldVcs.__carriedContentReport({ { violation = v, stage = "fauna" } })
typed/builtin//modules/world_vcs/M/__carriedLines
M.__carriedLines(verb: string, carried: { DependencyPath }) -> { string }
The lines a staging call writes to its caller about the referenced assets it staged beside what was named.
Parameters
verbstring— The calling API's name.carried{ DependencyPath }— The referenced assets the call staged beside the named paths.
Returns { string } — One line per asset.
worldVcs.__carriedLines("world.add", report.carried)
typed/builtin//modules/world_vcs/M/__changedPathSet
M.__changedPathSet(workingPaths: () -> { string }) -> { [string]: boolean }?
The assets a push changes, as the set the description gate counts: every folder a changed path lies in, and the path itself. A path is changed when a commit the remote does not hold yet changed it, and when the working tree or the staging area holds it changed. Answers nil when the world's history cannot be read, which counts every asset as changed.
Parameters
workingPaths() -> { string }— Reads the paths the working tree and the staging area hold.
Returns { [string]: boolean }? — A set keyed by path, or nil.
local changed = worldVcs.__changedPathSet(function() return { "/zero/source/a.module/init.luau" } end)
typed/builtin//modules/world_vcs/M/__claimsOf
M.__claimsOf(reads: { any }) -> { any }
The rows several status reads list in claimed_by_other_stages,
one list. A staging call reads the areas holding its paths before it
stages them, since the backend moves a path out of the area holding it
as it stages it, and after, for an area that still holds one.
Parameters
reads{ any }— Status reads, each{ claimed_by_other_stages }, nil entries skipped.
Returns { any } — The rows, in the order the reads list them.
worldVcs.__claimsOf({ before, after })
typed/builtin//modules/world_vcs/M/__closureCurrentRecords
M.__closureCurrentRecords(assets: { any }) -> ({ any }, { any })
The entries of a closure that name records their paths hold now. A
closure can list two records of one world at one path: the asset an
author published there under one guid, and the asset published there
later under another. A dependency entry names a record its path no longer
holds when the closure lists a dependency of another guid at the same
path of the same world at a later version_id (a version stating none
ranks below every version that states one), and no reference recorded by
an entry outside its own tree names its guid. Such an entry and every
tree_child landing in it are left out, so one dependency path resolves to
one record; every other entry is kept, in the order the closure lists it.
Parameters
assets{ any }— The closure's entries.
Returns ({ any }, { any }) — The entries kept, and the entries left out.
local kept, left = worldVcs.__closureCurrentRecords(closure.assets)
typed/builtin//modules/world_vcs/M/__closureFileChanges
M.__closureFileChanges(fromAssets: { any }, toAssets: { any }) -> { any }
The leaf files of an install that change between two of its closures, matched by asset guid: a file only the new closure holds is added, one only the old holds is removed, and one both hold at different checksums is changed. Folder assets are left out; their files are listed.
Parameters
fromAssets{ any }— The closure entries at the version the world holds.toAssets{ any }— The closure entries at the version it updates to.
Returns { any } — { guid, change, old, new } per file, sorted by origin path.
worldVcs.__closureFileChanges({}, {})
typed/builtin//modules/world_vcs/M/__closurePlacementKeys
M.__closurePlacementKeys(assets: { any }) -> ((any) -> string, (any) -> string, (any) -> string)
The key each entry of a closure's assets list is placed under, and
the key of the folder each tree_child entry lands in. A closure can name
one asset at more than one version (a file the installed content depends
on at one version, and the same file inside a folder version it depends on
at another), and one file at one version inside more than one folder
version. Each lands at a place of its own, so each is keyed apart: the
first one of an asset (its root or dependency entries first) is keyed by
the asset's guid, and each other by its version and the folder holding it.
A tree_child entry belongs to the entry whose identity its via_identity
names; without one, to the first entry of its via asset whose version
states this file at this version among its children, else to the first
entry of that asset. An entry's identity is its asset at its version, and
for a tree_child the identity of the entry it belongs to.
Parameters
assets{ any }— The closure's entries.
Returns ((any) -> string, (any) -> string, (any) -> string) — keyOf(entry), the key of an entry of assets; parentOf(entry), the key of the folder a tree_child entry lands in; identityOf(entry), the entry's identity.
local keyOf, parentOf = worldVcs.__closurePlacementKeys(closure.assets)
typed/builtin//modules/world_vcs/M/__collectUpdates
M.__collectUpdates(ops: CollectUpdatesOps) -> ({ UpdateReport }, { UpdateSkip }, number)
The units of the world that have an update available. A library is one
unit: its marker's requested_ref (default main) is resolved in the
library's world and reported when it names a commit other than the marker's
resolved_commit. An installed asset is one unit per marked root: the
root's recorded version is compared with the newest version of that asset at
its origin, one lookup per root. A dependency is never compared and never
reported, a root whose origin world is a library's world is part of that
library, and a pulled row no table marks as a root (an install older than the
marks, content forked from another world) is not reported. A root is
compared with the head of the ref its install was asked for (the root's
install_ref: a branch name; resolved in the origin's world, the default
being main, the ref world.installLibrary resolves); a root whose ref
shaped like a fixed version (a commit id, version checksum or version id) the
origin does not hold as a branch is never behind, a branch named like one is
compared like any branch, and one whose row records no version is listed in
skipped as no recorded version. A unit whose origin cannot be reached, or does not answer
within the module's wait bound, is listed in skipped with the reason.
Parameters
opsCollectUpdatesOps— ACollectUpdatesOps.
Returns ({ UpdateReport }, { UpdateSkip }, number) — reports (libraries first, each group by name or path), skipped, and checked, the number of units the check could read.
worldVcs.__collectUpdates({ listMarkers = function() return {}, {} end, resolveRef = function() return "" end, markedRoots = function() return {} end, rootVersionAtOrigin = function() return "" end, refKindAtOrigin = function() return "branch" end })
typed/builtin//modules/world_vcs/M/__commitChain
M.__commitChain(start: string, stopAt: string?, fetchPage: (cursor: string?) -> any, cap: number, maxPages: number?) -> ({ any }, boolean)
The commits from start back along each commit's first parent, read
from an origin's log a page at a time: up to and excluding stopAt when it
is given, else to the end of the history. Holds at most cap commits and
reads at most maxPages pages; past either it stops and says so.
Parameters
startstring— The commit to start from.stopAtstring(optional) — The commit to stop before, or nil.fetchPage(cursor: string?) -> any— Reads the page aftercursor(nil for the newest):{ commits, next_cursor }.capnumber— The most commits listed.maxPagesnumber(optional) — The most pages read, nil forUPDATE_LOG_PAGES.
Returns ({ any }, boolean) — The commits, newest first, and whether the range holds more.
worldVcs.__commitChain("c", nil, function() return { commits = {} } end, 200)
typed/builtin//modules/world_vcs/M/__commitOnceWith
M.__commitOnceWith(held: () -> { string }, rebase: ({ string }) -> (), commit: () -> (boolean, string)) -> (boolean, string)
Materialise a staging area, bringing it onto the branch head there is now if a commit landed under it. An area a caller keeps across another caller's commit was opened against the head of the moment it was opened, and the backend refuses to build a commit on a head that has moved. Rebasing is dropping that area and opening one against the current head, then staging the paths the old one held — so the commit carries the caller's own paths and no one else's. Re-staging reads each path's content as the working tree holds it now, which is what a caller asking to commit them is asking to freeze. Any other refusal travels back as it came, and the rebase is attempted once: a second moved head is another caller committing again, which the caller hears about rather than the call looping on.
Parameters
held() -> { string }— Reads the paths the staging area currently holds.rebase({ string }) -> ()— Drops the area and stagesheld's paths onto a fresh one.commit() -> (boolean, string)— Materialises the area, answering the backend's(ok, message).
Returns (boolean, string) — The (ok, message) pair the surviving attempt produced.
worldVcs.__commitOnceWith(held, rebase, function() return true, "01J" end)
typed/builtin//modules/world_vcs/M/__committedAreas
M.__committedAreas(paths: { string }) -> { CommittedArea }
Which staging area committed each path at or under one of
paths, read from the world's history: every commit records the area
it materialised, so the answer is the same from every engine and every
session bound to the world, and survives the engine that made the
commit. Answers no rows when the engine holds no world session, which
leaves every asset the caller's own.
Parameters
paths{ string }— The asset paths to report on.
Returns { CommittedArea } — One row per commit that froze a path inside one of them through a named area, nearest the branch head first.
worldVcs.__committedAreas({ "/zero/source/a.component" })
typed/builtin//modules/world_vcs/M/__completeFolderVersions
M.__completeFolderVersions(assets: { any }, readVersions: ({ { guid: string, checksum: string } }) -> { [string]: any }) -> number
Complete the folder versions of a closure from their own reads. A
folder version states its children (content.children); a closure that
lists the folder without an entry for one of them gets that child's entry
from a read of the folder at its version, naming the folder's entry as its
via_identity, and a folder child brought that way is completed the same
way. The folders are completed a level at a time: readVersions is handed
every version a level needs at once and answers each one's decoded closure
(read out to one edge) by <guid>@<checksum>. Each version is read once.
Entries the reads answer that are not a missing child of the folder read
are left out.
Parameters
assets{ any }— The closure's entries; the brought entries are appended to it.readVersions({ { guid: string, checksum: string } }) -> { [string]: any }— Reads a list of{ guid, checksum }versions, answering each decoded closure by<guid>@<checksum>.
Returns number — How many versions it read.
worldVcs.__completeFolderVersions(assets, readVersions)
typed/builtin//modules/world_vcs/M/__convertInstallRecord
M.__convertInstallRecord(entry: InstallEntry, rowByPath: { [string]: any }, pace: (() -> ())?) -> { record: { [string]: any }, drop: { string }, kept: { string } }
The record of an install that stands as pulled rows: built from the rows themselves, with no network read. The root's row names the asset, its version and its origin; the rows beneath the root are its listing; every other row of the install belongs to the dependency whose root it lies in. A file row the user changed (its bytes are no longer the version's) is not listed and stays a row of the world; folder rows and unchanged files are listed and are the rows the conversion takes out.
Parameters
entryInstallEntry— The install, as__groupInstallsstates it.rowByPath{ [string]: any }— The pulled manifest rows by path.pace(() -> ())(optional) — Called between rows; a caller that must not hold a frame yields from it.
Returns { record: { [string]: any }, drop: { string }, kept: { string } } — { record, drop, kept }: the record document, the paths of the rows to take out and the paths of the changed files that stay.
worldVcs.__convertInstallRecord(entry, rowByPath)
typed/builtin//modules/world_vcs/M/__decodeClosure
M.__decodeClosure(read: boolean, body: any?) -> any?
A closure read as the backfill uses it: the decoded { assets },
narrowed to the records its paths hold now (__keepCurrentRecords), or nil
when the read failed or its body does not decode to one. Never raises.
Parameters
readboolean— Whether the read was answered.bodyany(optional) — The answer's text.
Returns any? — The closure, or nil.
worldVcs.__decodeClosure(true, "{\"assets\":[]}")
typed/builtin//modules/world_vcs/M/__dependencyPlacementPaths
M.__dependencyPlacementPaths(entry: any?) -> { plain: string, versioned: string }
The two paths an install can place one dependency of a closure at, under
the dependency tree beside the world it belongs to: its install path, where
it lands when the world holds nothing of it and no other version in the
closure holds that path first, and the version's own folder, where it lands
otherwise. computeClosurePlacement places by these and the install-root
backfill looks for a placed dependency there.
Parameters
entryany(optional) — The closure entry of the dependency.
Returns { plain: string, versioned: string } — { plain, versioned }.
worldVcs.__dependencyPlacementPaths({ owning_world = "w", path = "a/b.module", checksum = "c" })
typed/builtin//modules/world_vcs/M/__dependencyPullRefusal
M.__dependencyPullRefusal(rows: { any }, path: string?, guid: string?) -> string?
The refusal for a pull aimed at a pulled row that belongs to an
install without being its root: <path> is a dependency of the install <display name> (<root path>); update the root. A root, a row no install
holds, and an asset guid that has a root row answer nil.
Parameters
rows{ any }— The pulled manifest rows, each with itsrefs_jsontable.pathstring(optional) — The row's path, or nil.guidstring(optional) — The asset guid the pull names, or nil.
Returns string? — The refusal text, nil when the pull may run.
worldVcs.__dependencyPullRefusal({}, "/zero/source/deps/w/a.module", nil)
typed/builtin//modules/world_vcs/M/__describeReferenceMove
M.__describeReferenceMove(move: ReferenceMove) -> string
The text naming one change that moved references: the pin of a library with its marker, or the asset's path.
Parameters
moveReferenceMove— The change.
Returns string — The text.
worldVcs.__describeReferenceMove({ library = "@builtin" }) --> "the pin of `@builtin` (/source/libs/@builtin)"
typed/builtin//modules/world_vcs/M/__discardAllPlan
M.__discardAllPlan(status: any?, scope: string) -> DiscardAllPlan
What discarding every change under scope does, decided from one
status read (read with the rows of installs left in): which rows are
taken back, which are left where they are, and which staged paths need
their staged copy written back first. The paths a staging area holds
stay (a staged file is not a change in the Changes list), as do the
ignored rows (a scene's autosave overlay aside: it goes with the rest),
the unmerged paths, the rows another account is changing,
and a dependency another install records too. An install whose root lies
in the scope goes back whole; an install whose root lies outside it
keeps its rows inside the scope.
Parameters
statusany(optional) — The status read, withinstallsand the install rows listed.scopestring— The engine-canonical path to discard beneath.
Returns DiscardAllPlan — A DiscardAllPlan.
worldVcs.__discardAllPlan(world.vcsStatus(), "/zero/source")
typed/builtin//modules/world_vcs/M/__discardInstallWith
M.__discardInstallWith(install: InstallEntry, installs: { InstallEntry }, ops: any?)
Put an install back at the version the branch head holds, through
ops: every row of it that the staging area holds leaves the area, then
every row goes back to its committed version, or away when no commit
holds it. Rows of the install the area does not hold are put back as
they are, so the install goes back whether its rows are unstaged, staged
or both. A row another install records as well stays.
Parameters
installInstallEntry— The install being discarded.installs{ InstallEntry }— Every install the working tree lists.opsany(optional) —{ staged, unstage, discardRows, changed }: the paths the area holds, taking one path out of the area, putting one path back, and the paths the working tree holds a change for.
worldVcs.__discardInstallWith(install, installs, ops)
typed/builtin//modules/world_vcs/M/__enclosingAssetPath
M.__enclosingAssetPath(path: string) -> string?
The path of the asset whose folder holds the asset at path as one of
its members (a module inside a package), or nil when no asset encloses it.
Parameters
pathstring— The engine-canonical path of an asset.
Returns string? — The enclosing asset's path, or nil.
worldVcs.__enclosingAssetPath("/zero/source/Kit.package/api.module")
typed/builtin//modules/world_vcs/M/__encodeRecord
M.__encodeRecord(record: { [string]: any }, pace: () -> ()) -> string
The JSON text of an install record, encoded entry by entry so a record listing thousands of files costs the frame one small step per entry rather than one long one.
Parameters
record{ [string]: any }— The record document.pace() -> ()— Called between entries.
Returns string — The JSON text.
worldVcs.__encodeRecord({ ["$schema"] = "zero/asset-pull/v2", listing = {}, deps = {} }, function() end)
typed/builtin//modules/world_vcs/M/__fetchedCommitHeld
M.__fetchedCommitHeld(ofSession: boolean, behind: number, walk: () -> boolean) -> boolean
Whether the session branch already holds a fetched commit. For the
session branch's own upstream the fetch's behind count answers it: 0
holds it, more does not. For another branch walk reads the session
branch's history.
Parameters
ofSessionboolean— Whether the branch pulled is the session branch.behindnumber— The commits origin holds that the branch does not, as the fetch counted them.walk() -> boolean— Whether the fetched commit is in the session branch's history.
Returns boolean — True when the session branch holds the fetched commit.
worldVcs.__fetchedCommitHeld(true, 0, function() return false end)
typed/builtin//modules/world_vcs/M/__footprintOf
M.__footprintOf(summaries: any?, rootGuid: string) -> { string }
The paths the files of an earlier install of rootGuid stand at: its
destination and the root of each dependency it placed.
Parameters
summariesany(optional) — Whatinstall.summariesanswers.rootGuidstring— The root asset's guid.
Returns { string } — The paths, empty when the world holds no install of the root.
worldVcs.__footprintOf({}, "g")
typed/builtin//modules/world_vcs/M/__groupInstalls
M.__groupInstalls(input: InstallGroupingInput) -> { InstallEntry }
The installs among the pulled rows whose rows differ from the branch
head. An install is its root, the row whose reference table marks it
install_root; its rows are the root, every row nested beneath a row of
the install, and the rows under /zero/source/deps/ that the tables of
its rows name as dependencies, followed to the end of the chain. A
dependency row holding no table of its own brings the rows beside it, under
the same /zero/source/deps/<world>/versions/<checksum>/ folder. A
dependency two installs share belongs to both, and a root another install
reaches as a dependency belongs to that install and keeps its own entry. An
install none of whose rows has a change is not listed. The rows are indexed
once by sorted path, so the rows nested beneath a path are found by a
binary search.
Parameters
inputInstallGroupingInput— AnInstallGroupingInput.
Returns { InstallEntry } — One InstallEntry per changed install, ordered by root path.
worldVcs.__groupInstalls({ rows = {}, dirty = {}, staged = {}, headChecksum = function() return nil end })
typed/builtin//modules/world_vcs/M/__hasConflictMarkers
M.__hasConflictMarkers(text: string) -> boolean
True if text contains a git-style conflict-marker line
(<<<<<<<, =======, or >>>>>>>) anchored at the start of a
line. The gate's resolution validator: a conflicted row can only
clear once its file has no marker lines left. Anchoring at line
start (not a bare substring search) avoids false-flagging prose
that merely mentions the marker text mid-line.
Parameters
textstring— The file content to scan.
Returns boolean — true when a marker line is present.
worldVcs.__hasConflictMarkers("<<<<<<< ours\nx\n") --> true
typed/builtin//modules/world_vcs/M/__headPin
M.__headPin(library: string) -> string?
The commit the branch head's pin marker of a library pins it at.
Parameters
librarystring— The library, with its@.
Returns string? — The commit, nil when the head holds no marker for the library.
worldVcs.__headPin("@builtin")
typed/builtin//modules/world_vcs/M/__heldMoves
M.__heldMoves(row: DifferingTable, libraries: { string }, holds: (string) -> boolean, ops: ReferenceOps) -> { HeldMove }
What moved the references of row among a set of changes, each change
with the asset and world pins it moved; the rule __referenceMoveCauses
states.
Parameters
rowDifferingTable— The file, with its live and head tables.libraries{ string }— The libraries whose pin marker is among the changes, with their@.holds(string) -> boolean— Whether an asset path is among the changes.opsReferenceOps— What the check reads beyond the working tree.
Returns { HeldMove } — One entry per change, in the order the tables name them.
worldVcs.__heldMoves(row, { "@builtin" }, holds, ops)
typed/builtin//modules/world_vcs/M/__heldOutMarkers
M.__heldOutMarkers(scope: { string }, status: LiveStageStatus) -> { string }
The libraries whose marker (/zero/source/libs/@<name>) is changed in
the working tree, dirty or untracked, and is not part of the commit.
Parameters
scope{ string }— The rows the commit freezes, engine-canonical.statusLiveStageStatus— The working tree and the calling area, read once.
Returns { string } — The library names, with their @, sorted.
worldVcs.__heldOutMarkers(scope, status)
typed/builtin//modules/world_vcs/M/__holdsMarkedRoot
M.__holdsMarkedRoot() -> boolean
Whether the world holds a pulled row marked as the root of an install: one read of the marked rows, which answers without any grouping work.
Returns boolean — True when a root is marked.
worldVcs.__holdsMarkedRoot()
typed/builtin//modules/world_vcs/M/__inRecordedVersionFolder
M.__inRecordedVersionFolder(path: string) -> boolean
Whether path lies in the folder of one recorded version of another
world's asset, /zero/source/deps/<world>/versions/<checksum>/. Such a
folder holds that version and no other.
Parameters
pathstring— A VFS path.
Returns boolean — True when the path lies in a recorded version's folder.
worldVcs.__inRecordedVersionFolder("/zero/source/deps/w/versions/c/a.module")
typed/builtin//modules/world_vcs/M/__installConflicts
M.__installConflicts(rows: { any }, rootPath: string?, writes: { [string]: string }) -> { string }
The conflicted rows one install's pull writes: a conflicted row at or beneath the install's root, or the row a closure entry of the pull lands in (its asset's guid at the path the pull writes it to). Another install's held version of a shared dependency sits at a path of its own and is another install's row. A row flagged conflicted that holds the bytes it installed carries no local content; the pull clears its flag and lands it, and it does not block.
Parameters
rows{ any }— The world's pulled rows.rootPathstring(optional) — The install root's path, or nil when the install holds no root row.writes{ [string]: string }— The path the pull writes each closure asset to, by asset guid.
Returns { string } — The conflicted paths, sorted.
worldVcs.__installConflicts({}, "/zero/source/Fire.module", {})
typed/builtin//modules/world_vcs/M/__installDiscardPlan
M.__installDiscardPlan(install: InstallEntry, installs: { InstallEntry }) -> { discard: { string }, kept: { string } }
The rows discarding an install puts back: every changed row of the install except one another install records too, deepest path first so a file goes before the folder that holds it.
Parameters
installInstallEntry— The install being discarded.installs{ InstallEntry }— Every install the working tree lists.
Returns { discard: { string }, kept: { string } } — { discard, kept }: the paths to put back, and the shared paths left in place.
worldVcs.__installDiscardPlan({ root_path = "/zero/source/A", changed = {}, rows = {} }, {})
typed/builtin//modules/world_vcs/M/__installRecordDocument
M.__installRecordDocument(assets: { any }, rootGuid: string, placement: { [string]: string }, worlds: { any }, installRef: string?) -> ({ [string]: any }, number)
The record of an install: the one document the world holds for it and
every engine materializes the install from. It names the asset and the
version installed, and lists every file and folder of that version with its
guid, checksum, size, kind and published reference table, the listing of
each dependency the install placed beside it, and the versions the commits
its reference sites name hold (worlds). An entry's path in the listing is
relative to where its asset stands.
Parameters
assets{ any }— The closure's entries, the world pulls ofWorldPlanincluded.rootGuidstring— The root asset's guid.placement{ [string]: string }— Where each placed entry stands, keyed the waycomputeClosurePlacementkeys them (__closurePlacementKeys).worlds{ any }— The commit records the install's reference sites resolve against.installRefstring(optional) — The ref the install was asked for, or nil.
Returns ({ [string]: any }, number) — The record document and the number of rows it states.
worldVcs.__installRecordDocument(assets, guid, placement, {}, nil)
typed/builtin//modules/world_vcs/M/__installRedirect
M.__installRedirect(installs: { InstallEntry }, path: string) -> (InstallEntry?, string?)
What acting on path means when the working tree holds installs: the
install whose root path names, which the call then acts on as a whole,
or the sentence refusing an act on a single row of an install.
Parameters
installs{ InstallEntry }— TheInstallEntrylist of the status read.pathstring— The engine-canonical path the call names.
Returns (InstallEntry?, string?) — (install, nil) for a root, (nil, refusal) for another row of an install, (nil, nil) for a path that belongs to no install.
worldVcs.__installRedirect({}, "/zero/source/Foo.module")
typed/builtin//modules/world_vcs/M/__installRefOf
M.__installRefOf(row: any?) -> string?
The ref a pulled row's table records its install as asked for.
Parameters
rowany(optional) — A pulled manifest row with itsrefs_jsontable.
Returns string? — The ref, nil when the table records none.
worldVcs.__installRefOf({ refs_json = '{"install_ref":"release"}' })
typed/builtin//modules/world_vcs/M/__installRootBackfill
M.__installRootBackfill(ops: BackfillOps) -> { marked: { string }, rejected: { string }, skipped: { string } }
Decide, for the pulled rows no table decides yet, which are the roots of an
install, on the evidence an install leaves. A world copied from another
holds every one of that world's assets as a pulled row, so an origin world
whose bootstrap files (.world_settings, .world_entrypoint.luau) this
world holds as pulled rows is a fork source and none of its rows is looked
at. An install places its dependencies under /zero/source/deps/, so a
world holding no pulled row there is looked up for nothing, and a candidate
(a pulled row outside the dependency tree and the libraries, beneath no
other candidate and no marked root) is looked up only then. One closure per candidate, at the commit its
row records, read a few at a time. A candidate is a root when its closure
answers it as the root entry, no other candidate's closure holds it as a
dependency, and the closure holds a dependency the world holds as a pulled
row at the path an install places it (__dependencyPlacementPaths); a root
whose dependencies the world satisfied from its own content leaves no such
row and cannot be told from forked content. A candidate whose closure was
read and is not proven is recorded as decided not a root, so it is never
looked up again; one whose closure could not be read records nothing, is
logged once, and is tried again at a later open.
Parameters
opsBackfillOps— ABackfillOps.
Returns { marked: { string }, rejected: { string }, skipped: { string } } — { marked, rejected, skipped }: the paths marked as roots, the paths recorded as no root, and the paths whose closure could not be read.
worldVcs.__installRootBackfill({ pulledRows = function() return {} end, closuresOf = function() return {} end, decide = function() return true end, log = print })
typed/builtin//modules/world_vcs/M/__installRootRow
M.__installRootRow(rows: { any }, guid: string, path: string?) -> any
The pulled row that is the root of an install, among the rows the world
holds for the root's asset. A world can hold one asset at several paths: its
install, and recorded versions of it another install brought under
/zero/source/deps/<world>/versions/<checksum>/. The row at path is the
root when the world holds one there; otherwise the row marked as an
install's root, and then any row outside a recorded version's folder.
Parameters
rows{ any }— The world's pulled rows.guidstring— The root asset's guid.pathstring(optional) — The install root's path, or nil.
Returns any — The root's row, or nil when the world holds no install of the asset.
worldVcs.__installRootRow({}, "g", "/zero/source/Fire.module")
typed/builtin//modules/world_vcs/M/__installRowsByGuid
M.__installRowsByGuid(rows: { any }, rootPath: string?, placement: { [string]: string }, satisfied: { [string]: string }) -> { [string]: any }
The pulled row a pull of one install acts on for each asset of its
closure, by asset guid. A world can hold one asset at several paths: the
install's own copy, the copies other installs hold, and recorded versions
placed under /zero/source/deps/<world>/versions/<checksum>/, each holding
exactly its version. An asset the pull places is acted on at the path it
places it at, else at a row at or beneath the install's root. An asset the
world already holds at the recorded version is acted on where it holds it:
the row at that path, or for a file of a held folder, the row beneath that
folder.
Parameters
rows{ any }— The world's pulled rows.rootPathstring(optional) — The install root's path, or nil.placement{ [string]: string }— The path the pull places each asset at, by guid.satisfied{ [string]: string }— The path the world already holds each asset at, by guid: for a file of a held folder, that folder's path.
Returns { [string]: any } — The row for each guid that has one.
worldVcs.__installRowsByGuid({}, "/zero/source/Fire.module", {}, {})
typed/builtin//modules/world_vcs/M/__installVerdict
M.__installVerdict(ok: boolean, payload: string, isEngineAsset: ((string) -> boolean)?) -> AssetInstallableReport
Read an installability verdict out of one closure fetch. Pure —
it decides from the fetch outcome and the response body alone, so
the classification is exercisable without a live backend.
world.assetInstallable is this function over a real fetch.
Parameters
okboolean— Whether the closure fetch returned successfully.payloadstring— The response body whenok, the error message otherwise.isEngineAsset((string) -> boolean)(optional) — Whether the engine running the install ships an asset under a guid in its own library; that asset's references are not the installed content's to answer for.
Returns AssetInstallableReport — An AssetInstallableReport with guid left empty for the caller to fill: installable (would installing this guid alone land content), verdict, a one-line detail, and the closure's shape as nodes / tree_children / deps.
worldVcs.__installVerdict(false, "HTTP 404: not found").verdict --> "unpublished"
typed/builtin//modules/world_vcs/M/__installedEntriesOf
M.__installedEntriesOf(rows: { any }, rootPath: string, toAssets: { any }) -> { any }
The entries an install holds as the world installed them, read from its
pulled rows: every row at or beneath the root, and the row of every asset
the new closure names, one row per asset (a row at or beneath the root
first, a recorded version's row under /zero/source/deps/<world>/versions/
last). Each entry states the asset, its origin path, the
version the world installed (origin_checksum) and whether it is a folder
asset (a row holding no blob of its own).
Parameters
rows{ any }— The world's pulled rows.rootPathstring— The install root's path.toAssets{ any }— The closure entries at the version the install updates to.
Returns { any } — Closure-shaped entries { asset_guid, path, checksum, is_composite }.
worldVcs.__installedEntriesOf({}, "/zero/source/Fire.module", {})
typed/builtin//modules/world_vcs/M/__isEngineSurface
M.__isEngineSurface(via: string, path: string?, kind: string?) -> boolean
Whether a site into another world reaches engine surface: what the
running engine provides, which content binds in the engine it runs in, so
a pin never versions it and an install never brings it
(docs/specs/library-versioning.md, "Engine surface is not pinned"). It
is when the site's via is asset_type, editorTools, tools.use,
plugins.call, settings_of or data_contract, or when the asset it
reaches lies under modules/api/ of its world, or is an assetType,
toolbox, tool or plugin (the leaf of its path, or its manifest kind).
The engine's reference tables and ZeroMind classify by the same rule,
held by the shared vectors tests/fixtures/engine_surface.json states.
Parameters
viastring— The site's provenance.pathstring(optional) — The asset the site reaches, relative to its world's root, when known.kindstring(optional) — That asset's manifest kind, when known.
Returns boolean — True when the site reaches engine surface.
worldVcs.__isEngineSurface("require", "modules/api/scene.module", nil)
typed/builtin//modules/world_vcs/M/__keepCurrentRecords
M.__keepCurrentRecords(closure: any?) -> { any }
A decoded closure narrowed to the records its paths hold now
(__closureCurrentRecords): assets holds the entries kept, and worlds
the sites recorded by an asset some kept entry is, so a record left out
reaches into no other world. Changes closure in place.
Parameters
closureany(optional) — The decoded closure,{ assets, worlds? }.
Returns { any } — The entries left out.
worldVcs.__keepCurrentRecords(closure)
typed/builtin//modules/world_vcs/M/__libraryChangedFiles
M.__libraryChangedFiles(diff: any?) -> { { path: string, change: string } }
The files of a library that change between two of its commits, read
from the diff of the two (zeromind.world diff): every path the diff
names except a folder whose files it names too.
Parameters
diffany(optional) —{ added, modified, removed }, each a list of paths.
Returns { { path: string, change: string } } — { path, change } per file, change being added | changed | removed, sorted by path.
worldVcs.__libraryChangedFiles({ modified = { "a.module", "a.module/init.luau" } })
typed/builtin//modules/world_vcs/M/__libraryMarkersIn
M.__libraryMarkersIn(entries: { any }, read: (string) -> any) -> ({ LibraryMarker }, { UpdateSkip })
The library markers among the entries of /zero/source/libs. A library
is listed as a directory because the folder of its files shares the path of
its marker, so an entry's marker is read from its own path whether the
listing calls it a file or a directory. A directory whose path holds no
marker is a mounted library with none and is left out; a file that is no
zero/world-import/v1 marker is skipped with its reason.
Parameters
entries{ any }— The listingvfs.list("/zero/source/libs")answers.read(string) -> any— Reads the bytes at a path, nil when none are held.
Returns ({ LibraryMarker }, { UpdateSkip }) — The markers, and the entries skipped with the reason.
worldVcs.__libraryMarkersIn({ { name = "@lib", isDirectory = true } }, function(path) return nil end)
typed/builtin//modules/world_vcs/M/__listLibraryMarkers
M.__listLibraryMarkers() -> ({ LibraryMarker }, { UpdateSkip })
The library markers the working tree holds under /zero/source/libs:
what the update check and world.installedLibraries read.
Returns ({ LibraryMarker }, { UpdateSkip }) — The markers, and the entries skipped with the reason.
local markers, skipped = worldVcs.__listLibraryMarkers()
typed/builtin//modules/world_vcs/M/__listReferencesMoved
M.__listReferencesMoved(dirty: { any }, staged: { any }, differing: { DifferingTable })
List each file whose reference table differs from the branch head's
while its bytes do not, as a status read reports it: a staged entry for it
gains a reason, and one the area does not hold is added to dirty with
it. The reason is "references moved", or "install recorded" for a
table that differs only in the install record the engine writes. A row
whose bytes changed is listed for them and gains no reason, and a staged
entry holding the row's references alone (references_only) reads
"references moved" whatever its bytes.
Parameters
dirty{ any }— The dirty entries, appended to.staged{ any }— The calling area's staged entries, marked in place.differing{ DifferingTable }— The rows whose live table differs from the head's.
worldVcs.__listReferencesMoved(dirty, staged, differing)
typed/builtin//modules/world_vcs/M/__liveReferenceOps
M.__liveReferenceOps() -> ReferenceOps
The reference checks' reads over the live world: the trusted pin library answers whether a table names a library, and the asset index answers the path of a guid.
Returns ReferenceOps — The ReferenceOps.
worldVcs.__liveReferenceOps()
typed/builtin//modules/world_vcs/M/__liveReferencesToRecord
M.__liveReferencesToRecord(stageName: string, caller: string) -> ({ ReferencesRecord }, LiveStageStatus)
The files the calling area's commit records for their references
alone, over the live world, as __referencesToRecord states them, with
the status read they were decided on. Reads nothing more when the area
holds nothing with its bytes.
Parameters
stageNamestring— The calling staging area.callerstring— The verb asking, named when a read fails.
Returns ({ ReferencesRecord }, LiveStageStatus) — The files, and the status read.
worldVcs.__liveReferencesToRecord("default", "world.commit")
typed/builtin//modules/world_vcs/M/__liveStaleReferences
M.__liveStaleReferences(stageName: string, caller: string) -> { StaleReference }
The stale files of the calling area's staged set over the live world,
as __staleReferences states them. Reads nothing more when the area
holds nothing.
Parameters
stageNamestring— The calling staging area.callerstring— The verb asking, named when a read fails.
Returns { StaleReference } — The stale files.
worldVcs.__liveStaleReferences("default", "world.commit")
typed/builtin//modules/world_vcs/M/__localEditVerdict
M.__localEditVerdict(change: string, row: any?, mergeConflicts: boolean?) -> string
Whether the world's edit of one installed file meets the incoming
change: none when the file is added or the world holds it as installed,
conflicts when an edited file is removed or its edit does not merge, and
merges when it merges cleanly.
Parameters
changestring— added | changed | removed.rowany(optional) — The pulled row the world holds for the file, or nil.mergeConflictsboolean(optional) — For an edited changed file: whether a three-way merge of it conflicts, nil when no text merge is possible.
Returns string — none | merges | conflicts.
worldVcs.__localEditVerdict("changed", { checksum = "a", origin_checksum = "a" }, nil)
typed/builtin//modules/world_vcs/M/__markInstallRoot
M.__markInstallRoot(refsJson: string?, decision: boolean?) -> string
A reference table as its row carries it, with the engine's decision about
its asset being the root of an install recorded: true for the root of an
install, false for an asset the engine looked at and found to be none. The
rest of the table stays as its publisher wrote it.
Parameters
refsJsonstring(optional) — The table's JSON text, or nil for an asset that has none.decisionboolean(optional) —true(the default) orfalse.
Returns string — The table's JSON text with "install_root" set to the decision.
worldVcs.__markInstallRoot('{"deps":[]}')
typed/builtin//modules/world_vcs/M/__markedRootsOf
M.__markedRootsOf(rows: { any }) -> { InstalledRoot }
The marked install roots among pulled rows, each with the version its
row records (origin_checksum). A row whose table
carries no mark, or marks it false, is not a root.
Parameters
rows{ any }— The pulled manifest rows, each with itsrefs_jsontable.
Returns { InstalledRoot } — One InstalledRoot per marked root row.
worldVcs.__markedRootsOf({})
typed/builtin//modules/world_vcs/M/__memoizedWorldProbe
M.__memoizedWorldProbe(probe: (world: string) -> (boolean, string)) -> (world: string) -> (boolean, string)
A probe of whether an origin world answers, asked at most once per world however many roots ask: the first caller runs it, a caller that arrives while it runs waits for that answer, and a later caller reads the kept one.
Parameters
probe(world: string) -> (boolean, string)— Asks whether a world answers: ok, or not ok and the failure text.
Returns (world: string) -> (boolean, string) — A function from a world guid to the kept answer.
worldVcs.__memoizedWorldProbe(function(world) return true, "" end)("w")
typed/builtin//modules/world_vcs/M/__modifiedFiles
M.__modifiedFiles(files: { UpdateFile }?, moving: { string }?, modified: { string }) -> { ModifiedFile }
The files with uncommitted changes an update touches, from a review's
files (an install) or its moving sources (a library): a modified file the
update changes reads conflicts when its edit conflicts or it is removed
after the world edited it, and merges otherwise; a file the world holds as
installed carries no edit and is left out; a modified moving source reads
merges.
Parameters
files{ UpdateFile }(optional) — The review's files, or nil.moving{ string }(optional) — The review's moving sources, or nil.modified{ string }— The paths holding uncommitted changes.
Returns { ModifiedFile } — The ModifiedFile list, sorted by path.
worldVcs.__modifiedFiles({}, nil, {})
typed/builtin//modules/world_vcs/M/__movedDependencies
M.__movedDependencies(live: string?, head: string?) -> { string }
The dependencies two reference tables, each as a manifest row carries it on the wire, both pin and pin at different versions: every asset guid both tables name whose set of checksums differs between them. A table names an asset once per version it records, so an asset reached at two versions is one guid with two checksums. A dependency only one table names was added or removed by an edit of the file and is not a moved pin.
Parameters
livestring(optional) — The live row's table.headstring(optional) — The branch head's table.
Returns { string } — The asset guids, sorted.
worldVcs.__movedDependencies('{"deps":[{"asset_guid":"g","checksum":"b"}]}', '{"deps":[{"asset_guid":"g","checksum":"a"}]}')
typed/builtin//modules/world_vcs/M/__movedRowsToStage
M.__movedRowsToStage(rows: { DifferingTable }) -> { string }
The rows a bulk stage takes for the files whose references moved: each row whose bytes did not change. A row whose bytes changed is a dirty row the sweep itself stages.
Parameters
rows{ DifferingTable }— The rows whose live table differs from the head's.
Returns { string } — The paths to stage, in the order rows lists them.
worldVcs.__movedRowsToStage(rows)
typed/builtin//modules/world_vcs/M/__movedWorldPins
M.__movedWorldPins(live: string?, head: string?) -> { { guid: string, name: string } }
The world entries two reference tables, each as a manifest row carries it on the wire, both hold for one world guid at different commits: each world's guid and library name.
Parameters
livestring(optional) — The live row's table.headstring(optional) — The branch head's table.
Returns { { guid: string, name: string } } — The worlds, sorted by guid.
worldVcs.__movedWorldPins(live, head)
typed/builtin//modules/world_vcs/M/__movedWorlds
M.__movedWorlds(live: string?, head: string?) -> { string }
The other worlds two reference tables, each as a manifest row carries it on the wire, both name at different commits: the name of every world entry both tables hold for one world guid with different commits. A world only one table names was added or removed by an edit of the file.
Parameters
livestring(optional) — The live row's table.headstring(optional) — The branch head's table.
Returns { string } — The library names (@builtin), sorted.
worldVcs.__movedWorlds('{"worlds":[{"world_guid":"w","name":"@builtin","commit":"c2"}]}', '{"worlds":[{"world_guid":"w","name":"@builtin","commit":"c1"}]}')
typed/builtin//modules/world_vcs/M/__openUpdateCheck
M.__openUpdateCheck(ops: OpenUpdateCheckOps) -> string?
The check an editor open makes once the world is ready: raises one notice and writes one log line when units have updates, nothing when they are all current, and one log line (no notice) when no unit could be read. Does nothing outside an editor session whose content is confirmed.
Parameters
opsOpenUpdateCheckOps— AnOpenUpdateCheckOps.
Returns string? — The notice text, nil when none was raised.
worldVcs.__openUpdateCheck({ confirmedEditorOpen = function() return false end, collect = function() return {}, {}, 0 end, notify = print, log = print })
typed/builtin//modules/world_vcs/M/__otherAreasHolding
M.__otherAreasHolding(claims: { any }) -> { [string]: { ReleasedPath } }
The other staging areas holding each path, by path, as status reads'
claimed_by_other_stages name them. A path is staged in one area at a
time, so a staging call that stages one of these paths takes it out of
each area listed here, by that area's row id, whichever account opened
the area and whether or not the session holding it is open.
Parameters
claims{ any }— The rows other staging areas hold,{ path, stage_name, stage_row_id, owner_account, same_account, live, session, ... }.
Returns { [string]: { ReleasedPath } } — One ReleasedPath row per area holding each path, in the order claims lists them, each area once per path.
worldVcs.__otherAreasHolding(status.claimed_by_other_stages)["/zero/source/a.luau"][1].stage --> "client:b"
typed/builtin//modules/world_vcs/M/__otherStageHolding
M.__otherStageHolding(assetPath: string, callerStage: string, claims: { any }, committed: { CommittedArea }) -> string?
The staging area holding an asset, for a caller acting on
callerStage. An area holds the asset when it holds the asset's own
path or any path inside it. A staged row another area holds names that
area. Otherwise the commit nearest the branch head that froze a path
inside the asset through a named area decides: that area holds it
while it still answers for what it committed (answers), and a commit
through the one shared area claims nothing. Answers nil when the asset
is the caller's own or when no area holds it, which is the answer a
lone author gets for everything.
Parameters
assetPathstring— The asset's path.callerStagestring— The staging area the calling verb acts on.claims{ any }— Theclaimed_by_other_stagesrows from a status read.committed{ CommittedArea }— The rows__committedAreasreports for the asset.
Returns string? — The name of the other area holding it, or nil.
worldVcs.__otherStageHolding("/zero/source/a.component", "beta", claims, committed)
typed/builtin//modules/world_vcs/M/__owedRowsToStage
M.__owedRowsToStage(owed: { string }, ignored: (string) -> boolean) -> { string }
The rows a bulk stage takes beyond the dirty rows it swept: each path
of owed that the call stages, where owed names the sources whose
manifest row states fewer dependencies than the table the engine derives
from its bytes, or the files whose references moved. A path the live
ignore set matches publishes nothing, so it stays out. A path another
staging area holds is staged here like any other, and the staging call
takes it out of that area.
Parameters
owed{ string }— The paths to take: what__spacetime_vcs.owedTablesreports, or the files whose references moved.ignored(string) -> boolean— Whether the live ignore set matches a path.
Returns { string } — The rows to stage, in the order owed lists them.
worldVcs.__owedRowsToStage({ "/zero/source/a.module/init.luau" }, function() return false end)
typed/builtin//modules/world_vcs/M/__ownChangePaths
M.__ownChangePaths(dirty: { any }, staged: { any }) -> { string }
The paths a status read lists as holding changes of their own: every
dirty and staged entry except one listed for its reference table alone
(a reason of references moved or install recorded), which the
engine recorded and nobody edited.
Parameters
dirty{ any }— The dirty entries, each a path or{ path, reason? }.staged{ any }— The staged entries, each{ path, reason? }.
Returns { string } — The paths, in the order listed.
worldVcs.__ownChangePaths({ { path = "/zero/source/a.luau" } }, {})
typed/builtin//modules/world_vcs/M/__ownEditPaths
M.__ownEditPaths(paths: { string }, rows: { any }) -> { string }
The paths among paths that hold an edit of the world's own: a pulled
file whose bytes differ from the version it installed, or any path no
pulled row holds. A pulled folder, and a pulled file holding the bytes it
installed, carry no edit of the world's, whether or not their install has
been committed.
Parameters
paths{ string }— The paths holding uncommitted changes.rows{ any }— The world's pulled rows.
Returns { string } — The paths that hold an edit, in the order given.
worldVcs.__ownEditPaths({ "/zero/source/own.luau" }, {})
typed/builtin//modules/world_vcs/M/__pacer
M.__pacer(yieldTurn: (() -> ())?) -> () -> ()
A function that gives the frame back once a slice of work has spent its share: called between small steps of a long piece of work, it yields to the frame loop when the steps since its last yield have taken a few milliseconds, so the work as a whole never holds a frame.
Parameters
yieldTurn(() -> ())(optional) — What gives the frame back;task.waitwhen omitted.
Returns () -> () — The function to call between steps.
local pace = worldVcs.__pacer(); pace()
typed/builtin//modules/world_vcs/M/__pathIsUnder
M.__pathIsUnder(path: any?, root: string) -> boolean
Whether path is root or lies inside it. A path a neighbouring
folder shares a prefix with is a different path.
Parameters
pathany(optional) — The path to place.rootstring— The asset path it is measured against.
Returns boolean — True when path is root or continues it past a /.
worldVcs.__pathIsUnder("/zero/source/a.component/init.luau", "/zero/source/a.component") --> true
typed/builtin//modules/world_vcs/M/__pathsTouch
M.__pathsTouch(paths: { string }, watched: { string }, rootPath: string) -> (boolean, boolean)
Whether the paths one commit changed touch an install: a path that is
one of watched (the origin paths of the install's closure) or lies
beneath one touches it, and one that is the root's path or lies beneath it
touches the root.
Parameters
paths{ string }— The paths the commit changed.watched{ string }— The origin paths of the closure.rootPathstring— The origin path of the root.
Returns (boolean, boolean) — Whether the commit touches the install, and whether it touches the root.
worldVcs.__pathsTouch({ "Fire.module/init.luau" }, { "Fire.module" }, "Fire.module")
typed/builtin//modules/world_vcs/M/__pendingWriter
M.__pendingWriter(rowCaller: string?, caller: string?) -> string
What a write this engine made and the manifest has not reflected
yet is to the calling caller: "you" when the calling caller made it,
"other" when another caller did, "unattributed" when no caller was
attested for it and the calling caller is a named one. A caller that is
itself unattributed reads an unattributed write as its own. The rule a
status read applies to a manifest row this engine wrote, applied to a
write still on its way to one.
Parameters
rowCallerstring(optional) — The caller key the write was made under, nil when none.callerstring(optional) — The calling caller's key, nil when none.
Returns string — "you", "other" or "unattributed".
worldVcs.__pendingWriter("client:a", "client:b") -- "other"
typed/builtin//modules/world_vcs/M/__placedBesideRow
M.__placedBesideRow(rowPath: any?, placement: string?) -> boolean
Whether a closure entry placed at placement lands beside the pulled row
the world holds for the same asset, rather than updating it in place: the
row sits under /zero/source/deps/ and the placement is another path.
Parameters
rowPathany(optional) — The path of the pulled row the world holds for the asset.placementstring(optional) — The path the closure's placement gives the entry, nil when the world already holds it.
Returns boolean — True when the entry lands beside the row.
worldVcs.__placedBesideRow("/zero/source/deps/w/a.module", "/zero/source/deps/w/versions/c/a.module")
typed/builtin//modules/world_vcs/M/__planBoundedPulls
M.__planBoundedPulls(worlds: { ClosureWorld }, libraries: { LibraryHere }, resolveSites: (string, string, { any }) -> { any }, holds: (string, string, string, string?) -> boolean, readVersion: (string, string, number) -> any) -> { entries: { any }, worlds: { WorldPlanEntry }, reads: number }
What an install brings from the other worlds its closure reaches into
(docs/specs/library-versioning.md, "Installing content whose pin
differs"). Each world entry is planned by __planWorldPulls. Each asset
it brings is read at its version on its own (readVersion, out to as few
edges as hold its own files, __versionOwnEntries) and lands with its own
files alone. Each dependency those files' reference tables record that is
not engine surface and whose version this world does not hold is brought
the same way, recursively, and each world entry of those files is planned
the same way; nothing else of the asset's world is read or brought.
Parameters
worlds{ ClosureWorld }— The closure's world entries.libraries{ LibraryHere }— The libraries this world reaches (libraryPin.worlds).resolveSites(string, string, { any }) -> { any }— Answers, for a world, a commit and a list of{ via, literal, names? }, one{ assets }per site in order.holds(string, string, string, string?) -> boolean— Whether this world holds an asset (guid,checksum, world guid, path in its world).readVersion(string, string, number) -> any— Reads one asset (guid) at one version (checksum) out to a number of edges, answering the decoded closure.
Returns { entries: { any }, worlds: { WorldPlanEntry }, reads: number } — { entries, worlds, reads }: the entries to land (each asset's own root standing as a dependency), one plan entry per world, and how many version reads it made.
worldVcs.__planBoundedPulls(closure.worlds, libs, resolve, holds, read)
typed/builtin//modules/world_vcs/M/__planWorldPulls
M.__planWorldPulls(worlds: { ClosureWorld }, libraries: { LibraryHere }, resolveSites: (string, string, { any }) -> { any }, holds: (string, string, string, string?) -> boolean) -> { pulls: { WorldPull }, worlds: { WorldPlanEntry } }
What an install brings for the other worlds its closure reaches into
(closure.worlds). A world this world uses at the commit the installed
content recorded brings nothing: every site resolves in this world's own
copy. The commit a world uses is the one it pins, or for a library it pins
none of, the commit of that library the engine opens it with
(libraryPin.worlds, mounted). A world entry that recorded no commit
brings nothing either: its
sites resolve against this world's own library. For any other world, each
recorded site is resolved at the recorded commit — a naming site to the
asset it names, an enumeration site to the members its selector matches
there — and each asset whose version there this world does not hold is
brought, once. A site that reaches engine surface (__isEngineSurface) is
never asked about, and a resolved asset that is engine surface is never
brought. The library itself is never brought whole.
Parameters
worlds{ ClosureWorld }— The closure's world entries.libraries{ LibraryHere }— The libraries this world reaches (libraryPin.worlds).resolveSites(string, string, { any }) -> { any }— Answers, for a world, a commit and a list of{ via, literal, names? }, one{ assets }per site in order.holds(string, string, string, string?) -> boolean— Whether this world holds an asset (guid,checksum, world guid, path in its world).
Returns { pulls: { WorldPull }, worlds: { WorldPlanEntry } } — { pulls, worlds }: the assets to bring, sorted by guid then version, and one entry per world.
worldVcs.__planWorldPulls(closure.worlds, libs, resolve, holds)
typed/builtin//modules/world_vcs/M/__pullMerge
M.__pullMerge(ops: PullMergeOps) -> (string, string)
How git pull merges a fetched commit into the session branch:
nothing when the session branch already holds it (read before anything
is moved, so a tree with uncommitted changes is not refused for it), else
fast-forward when the branch is strictly behind (unless noFf), else a
merge commit (refused under ffOnly).
Parameters
opsPullMergeOps— APullMergeOps.
Returns (string, string) — up_to_date, fast_forward, merged (with the merge commit id) or refused (with the reason).
worldVcs.__pullMerge({ ffOnly = false, noFf = false, fastForward = function() return true, "ok" end, inHistory = function() return false end, merge = function() return true, "ok", "M" end })
typed/builtin//modules/world_vcs/M/__pullPlan
M.__pullPlan(rows: { any }, assets: { any }, rootGuid: string, rootPath: string?) -> PullPlan
How a pull of one install lands its closure: the install's root row
(__installRootRow), where each closure asset is placed and where the world
already holds one (computeClosurePlacement, with the root kept at its
row's path), and the pulled row the pull acts on for each asset
(__installRowsByGuid).
Parameters
rows{ any }— The world's pulled rows.assets{ any }— The closure entries at the version the pull lands.rootGuidstring— The root asset's guid.rootPathstring(optional) — The install root's path the pull was asked for, or nil.
Returns PullPlan — The PullPlan.
worldVcs.__pullPlan({}, {}, "g", nil)
typed/builtin//modules/world_vcs/M/__readDifferingTables
M.__readDifferingTables(caller: string, under: string?) -> { DifferingTable }
The rows of the world's own content whose live reference table differs
from the one the branch head holds, read from the session's kept set:
every one, or the ones at and beneath under. An empty list when the
session holds no such read.
Parameters
callerstring— The verb reading them, named when the read fails.understring(optional) — An engine-canonical path to narrow the read to, nil for the whole branch.
Returns { DifferingTable } — The rows, sorted by path.
worldVcs.__readDifferingTables("world.commit")
typed/builtin//modules/world_vcs/M/__readLiveStageStatus
M.__readLiveStageStatus(stageName: string) -> LiveStageStatus
The working tree and the calling area, read once: the rows the working
tree holds an edit for, the rows the area holds with their bytes, the rows
it holds the references of alone (referencesOnly), and the live ignore
set.
Parameters
stageNamestring— The calling staging area.
Returns LiveStageStatus — The LiveStageStatus the commit's staging decisions read.
worldVcs.__readLiveStageStatus("default")
typed/builtin//modules/world_vcs/M/__readPulledRowsPaged
M.__readPulledRowsPaged(pace: () -> ()) -> { any }
Every pulled manifest row of the working copy, read a page at a time so no read or decode holds a frame for the whole set.
Parameters
pace() -> ()— Called between rows.
Returns { any } — The rows, in path order.
local rows = worldVcs.__readPulledRowsPaged(function() end)
typed/builtin//modules/world_vcs/M/__reconcileDecision
M.__reconcileDecision(base: string, ours: string, theirs: string) -> string
Decide the three-way reconcile action for one pulled-asset row
from its three content checksums (base = last-known origin checksum,
ours = current local checksum, theirs = latest upstream checksum).
Pure — no I/O, no VCS calls. world.pullAsset drives its reconcile
loop off this decision per row.
Parameters
basestring— The origin checksum recorded the last time this row was pulled or advanced.oursstring— This world's current local checksum for the row.theirsstring— The latest upstream checksum.
Returns string — One of "noop" (upstream unchanged, whatever the local side holds — nothing to do), "fast_forward" (untouched locally and upstream moved — take theirs), "converged" (both sides already match — advance provenance only), or "merge" (all three differ — three-way merge required).
worldVcs.__reconcileDecision("A", "A", "B") --> "fast_forward"
worldVcs.__reconcileDecision("A", "A", "A") --> "noop"
typed/builtin//modules/world_vcs/M/__recordFileCount
M.__recordFileCount(root: string) -> number
How many files and folders the install record at root lists, the
dependencies it placed included; 0 when the world holds no record there.
Parameters
rootstring— The install's destination.
Returns number — The count.
worldVcs.__recordFileCount("/zero/source/none.package")
typed/builtin//modules/world_vcs/M/__recordRoots
M.__recordRoots(summaries: any?) -> { InstalledRoot }
The installs that stand as one record row, as the update check reads an install root: the destination, the asset, the version the record states and the ref it was installed at.
Parameters
summariesany(optional) — Whatinstall.summariesanswers.
Returns { InstalledRoot } — The InstalledRoot list, sorted by path.
worldVcs.__recordRoots({})
typed/builtin//modules/world_vcs/M/__refHasVersionShape
M.__refHasVersionShape(ref: string?) -> boolean
Whether a recorded install ref has the shape of a fixed version of its
origin (a commit id, a version checksum or a version id). Only the origin
says whether a ref of that shape is a version or a branch named like one
(__refKindAtOrigin).
Parameters
refstring(optional) — The recorded ref, or nil.
Returns boolean — True for a ref shaped like a version.
worldVcs.__refHasVersionShape("0123456789abcdef")
typed/builtin//modules/world_vcs/M/__refKindAtOrigin
M.__refKindAtOrigin(resolve: (ref: string) -> (boolean, string), ref: string, worldReadable: () -> (boolean, string)) -> string
What the origin world says a ref shaped like a fixed version is: branch
when the origin resolves it as a named ref to a commit other than itself,
pin when it names exactly the commit it resolves to, or when the world
answers and holds no such ref (a version checksum or version id). The
origin's answer is read from the phrase returned HTTP 404 in the resolve
failure text; a failure whose text carries no such status, and a 404 from a
world that cannot be read either, raise with the origin's text.
Parameters
resolve(ref: string) -> (boolean, string)— Resolves a ref in the origin world: ok and the response body, or not ok and the failure text.refstring— The recorded ref.worldReadable() -> (boolean, string)— Whether the origin world answers at all: ok, or not ok and the failure text. Asked only after the ref itself came back 404.
Returns string — branch or pin.
worldVcs.__refKindAtOrigin(function(ref) return false, "ZeroMind GET u returned HTTP 404: not found" end, "0123456789abcdef", function() return true, "{}" end)
typed/builtin//modules/world_vcs/M/__referenceMoveCauses
M.__referenceMoveCauses(row: DifferingTable, libraries: { string }, holds: (string) -> boolean, ops: ReferenceOps) -> { ReferenceMove }
What moved the references of a file whose reference table differs from
the branch head's, among a set of changes. A world entry both tables hold
at different commits moved with the pin of that library, when its marker
is among the changes. Each asset entry both tables pin at different
versions is followed to its asset: one under
/zero/source/libs/@<name>/ moved with the pin of that library, when its
marker is among the changes; an asset of the world moved itself, when it
is among the changes. A dependency whose asset the world does not hold is
read through the head's table: it moved with the pin of a library among
the changes that the table names.
Parameters
rowDifferingTable— The file, with its live and head tables.libraries{ string }— The libraries whose pin marker is among the changes, with their@.holds(string) -> boolean— Whether an asset path is among the changes.opsReferenceOps— What the check reads beyond the working tree.
Returns { ReferenceMove } — The changes, each named once, libraries first.
worldVcs.__referenceMoveCauses(row, { "@builtin" }, holds, ops)
typed/builtin//modules/world_vcs/M/__referencesMovedDiscardRefusal
M.__referencesMovedDiscardRefusal(path: string, state: DiscardReferenceState, causes: (DifferingTable) -> { ReferenceMove }, headPin: (string) -> (string?)) -> string?
The sentence refusing to discard a path whose only difference from the branch head is reference tables. Its bytes are the head's, so a discard could only put back a table naming versions the world no longer runs; the sentence names what moved the references and the undo, which is undoing that change: the library's pin moved back to the commit the branch head pins, or the moved asset reverted. A path with a dirty row at or beneath it, or a staged row that is not one of the differing rows, holds an authored change and is discarded normally.
Parameters
pathstring— The engine-canonical path the call names.stateDiscardReferenceState— The rows and paths the refusal reads.causes(DifferingTable) -> { ReferenceMove }— What moved one differing row's references.headPin(string) -> (string?)— The commit the branch head's marker pins a library at, nil when it pins none.
Returns string? — The refusal text, nil when the path holds an authored change or no table difference.
worldVcs.__referencesMovedDiscardRefusal("/zero/source/a.luau", state, causes, headPin)
typed/builtin//modules/world_vcs/M/__referencesMovedDiscardRefusalFor
M.__referencesMovedDiscardRefusalFor(path: string, stageName: string) -> string?
__referencesMovedDiscardRefusal over the live world. A dirty row at or
beneath the path is an authored change, so the read of the rows whose
table differs is made only when there is none, and only for the path and
what lies beneath it. What moved a row's references is read among the
library markers and the paths the working tree holds a change of.
Parameters
pathstring— The engine-canonical path the call names.stageNamestring— The staging area the call acts on.
Returns string? — The refusal text, nil when the path holds an authored change or no table difference.
worldVcs.__referencesMovedDiscardRefusalFor("/zero/source/a.luau", "default")
typed/builtin//modules/world_vcs/M/__referencesToRecord
M.__referencesToRecord(scope: { string }, differing: { DifferingTable }, status: LiveStageStatus, ops: ReferenceOps) -> { ReferencesRecord }
The files a commit records for their references alone: each file whose
reference table moved with a change the commit holds (the pin marker of a
library, or an asset of the world it pins at another version) and that
the commit leaves out, its bytes changed or not. The commit records the
version of each that the branch head holds, its pins of what the commit
holds advanced to the versions the live table states; every other edit of
the file, another caller's work in progress among them, stays out.
Library content (everything under /zero/source/libs/) is never
recorded, nor is a file the live ignore set matches, nor a table that
differs only in its install record. Nothing is staged or changed.
Parameters
scope{ string }— The rows the commit freezes with their bytes, engine-canonical.differing{ DifferingTable }— The rows whose live table differs from the head's.statusLiveStageStatus— The working tree and the calling area, read once.opsReferenceOps— What the check reads beyond the working tree.
Returns { ReferencesRecord } — One entry per file, sorted by path.
worldVcs.__referencesToRecord(scope, differing, status, ops)
typed/builtin//modules/world_vcs/M/__refreshAndStageScope
M.__refreshAndStageScope(scope: { string }, refresh: ({ string }) -> ({ string }, { string }), drain: () -> (), liveStatus: () -> LiveStageStatus, stageRows: ({ string }) -> (), holdBack: (string) -> ())
Re-derive the dependency tables of the rows a commit is about to
freeze, and re-stage the ones whose staging carries just that delta.
scope is the whole of what the commit re-derives — the paths the
calling staging area holds, so a row outside it is neither re-derived
nor staged, whoever
else in the world is editing it and however far its own table has
drifted.
Of the candidates refresh reports, a row is staged when staging it
carries JUST the refreshed table: it is HEAD-clean in the shared
working tree, or the calling area already holds it. A row with an
unstaged byte edit whose table also moved goes to holdBack instead,
so a commit stays git add semantics rather than git commit -a.
A row the live ignore set matches is skipped — an ignored source
publishes nothing, so its table gates nothing.
Parameters
scope{ string }— The rows this commit freezes, engine-canonical.refresh({ string }) -> ({ string }, { string })— Re-derives the tables of the paths it is given, answering the candidates it re-derived and the subset whose table moved. Its answer is narrowed toscopebefore any of it is staged.drain() -> ()— Waits for the re-derived rows to reach the manifest.liveStatus() -> LiveStageStatus— Reads the working tree and the calling area, once, after the drain.stageRows({ string }) -> ()— Stages the rows it is given into the calling area, once for all of them: the staging read and settle a stage makes cost the size of the working set, so the rows go in one call rather than one call per row.holdBack(string) -> ()— Reports a row whose table moved and whose byte edit is the author's to stage.
worldVcs.__refreshAndStageScope(staged, refresh, drain, status, add, warn)
typed/builtin//modules/world_vcs/M/__resetLeavesRow
M.__resetLeavesRow(row: any?) -> boolean
Whether a hard reset or a Discard All leaves a dirty row as it is: the
row holds a local change of an account other than the calling caller's
(a row whose writer records no account, or acts for the caller's account,
is taken), or it is a bootstrap file of the world (.world_settings,
.world_entrypoint.luau) that no commit holds, which the world needs to
open and so is never removed.
Parameters
rowany(optional) — One dirty row as a status read reports it.
Returns boolean — True for a row the reset leaves in place.
worldVcs.__resetLeavesRow({ path = "/a", same_account = false, dirtied_by_account = "acct-b" }) --> true
typed/builtin//modules/world_vcs/M/__resolveUpdateUnit
M.__resolveUpdateUnit(unit: string, libraries: { string }, roots: { InstalledRoot }) -> UpdateUnit
Which unit a name stands for. @<name> and a /zero/source/libs/@<name>
marker path are libraries; a path is an installed asset, and a path inside
an install stands for its root; a bare name is a library the world pins, an
install root of that name, or else the library @<name>.
Parameters
unitstring— The name a caller gave.libraries{ string }— The library names the world pins (@<name>).roots{ InstalledRoot }— The world's install roots.
Returns UpdateUnit — { kind = "library" | "asset", unit }.
worldVcs.__resolveUpdateUnit("builtin", { "@builtin" }, {})
typed/builtin//modules/world_vcs/M/__reviewUpdate
M.__reviewUpdate(unit: string, opts: ReviewUpdateOpts) -> UpdateReview
Review one unit's update against the open world (world.reviewUpdate).
Parameters
unitstring— The unit's name.optsReviewUpdateOpts—ReviewUpdateOpts.
Returns UpdateReview — The UpdateReview.
worldVcs.__reviewUpdate("@builtin", {})
typed/builtin//modules/world_vcs/M/__rootDestination
M.__rootDestination(entry: any?, rootGuid: string, at: string?) -> string
The path an install places the root of a closure at. at is the
destination directory and the root keeps its own name (its display name,
category suffix included), so at = "/source/combat" lands
sea.material at /zero/source/combat/sea.material; with no at the
directory is /zero/source. An at that already ends with the root's own
folder name is the whole destination.
Parameters
entryany(optional) — The closure entry of the root.rootGuidstring— The root's guid, the name of a root whose entry states none.atstring(optional) — The destination directory the install was asked for, or nil.
Returns string — The root's engine-canonical destination path.
worldVcs.__rootDestination({ display_name = "sea.material" }, "g", "/source/combat")
typed/builtin//modules/world_vcs/M/__rowsUnderPrefix
M.__rowsUnderPrefix(dir: string, paths: { string }) -> { string }
The rows of paths that lie beneath the directory dir. dir is
taken in either the /source/… shorthand or the engine-canonical
/zero/source/… form and matched as a whole path segment, so a
directory selects its own contents and never a sibling whose name it is
a prefix of. This is the set a directory stands for when it is staged: a
directory outside any asset carries no manifest row of its own, and the
rows to stage are the ones under it.
Parameters
dirstring— The directory whose contents to select.paths{ string }— The candidate row paths, engine-canonical.
Returns { string } — Those of paths under dir, in the order given.
worldVcs.__rowsUnderPrefix("/source/hud", { "/zero/source/hud/a.luau" })
typed/builtin//modules/world_vcs/M/__runInstallRootBackfill
M.__runInstallRootBackfill() -> { marked: { string }, rejected: { string }, skipped: { string } }?
Run the install-root backfill against the open world: the pulled rows and closures come from the live manifest and ZeroMind, closures are read four at a time, and a decision lands on the live row at once, to ride in the next commit. Does nothing outside an editor session that may write the world.
Returns { marked: { string }, rejected: { string }, skipped: { string } }? — The backfill's result, nil when the session may not write or the pulled rows could not be read.
worldVcs.__runInstallRootBackfill()
typed/builtin//modules/world_vcs/M/__runOpenUpdateCheck
M.__runOpenUpdateCheck() -> string?
Run the editor open's update check against the open world: libraries and
marked install roots are read a few at a time from ZeroMind, and the result
is one engine notice and one log line (__openUpdateCheck). The notice
goes to every caller without waking an agent, so an agent reads it on the
response of its first tool call, and it names the ZeroMind panel's updates view, which
the editor's notification list opens from it. A read-only viewer is told
as well.
Returns string? — The notice text, nil when none was raised.
worldVcs.__runOpenUpdateCheck()
typed/builtin//modules/world_vcs/M/__sameBindings
M.__sameBindings(a: any?, b: any?) -> boolean
Whether two reference-table texts record the same bindings, leaving
out the diagnostics (unresolved_deps, problems) that describe local
resolution.
Parameters
aany(optional) — A reference-table JSON text, or nil.bany(optional) — A reference-table JSON text, or nil.
Returns boolean — true when both record the same bindings.
worldVcs.__sameBindings("{}", "{\"deps\":[]}") --> true
typed/builtin//modules/world_vcs/M/__scopeHolds
M.__scopeHolds(scope: { string }) -> ({ [string]: boolean }, (string) -> boolean, { string })
The rows a commit freezes, read three ways: the set of them, whether an asset is in the commit (the commit holds its row, a file of it, or a folder it lies in), and the libraries whose pin marker it holds.
Parameters
scope{ string }— The rows the commit freezes, engine-canonical.
Returns ({ [string]: boolean }, (string) -> boolean, { string }) — The set, the asset test, and the libraries with their @, sorted.
worldVcs.__scopeHolds({ "/zero/source/libs/@builtin" })
typed/builtin//modules/world_vcs/M/__selectAssetCommits
M.__selectAssetCommits(chain: { any }, changesOf: (index: number) -> ({ string }, { string }?), watched: { string }, rootPath: string, rootAt: (commitId: string) -> (string?), from: string) -> ({ any }, boolean, string?)
The commits of an install's update, from the chain of its origin back from the version it updates to: each commit whose change touches the install is kept, and the walk ends at the commit that moved the root from the installed version (kept), at a commit after which the root has no earlier version (a commit that adds the root's own path is where its history starts, read from the diff with no version read), and at the first commit of the history. When the version of the root at a commit cannot be read the walk stops there, short of the installed version, and says why.
Parameters
chain{ any }— The origin's commits, newest first (__commitChain).changesOf(index: number) -> ({ string }, { string }?)— The paths the commit at an index changed against its parent, and the ones among them it added.watched{ string }— The origin paths of the closure at the new version.rootPathstring— The origin path of the root.rootAt(commitId: string) -> (string?)— The version of the root at a commit, nil when it has none there; raises when it cannot be read.fromstring— The version the world holds.
Returns ({ any }, boolean, string?) — The commits kept, whether the walk reached the commit that left from, and why it stopped short when a version could not be read (nil otherwise).
worldVcs.__selectAssetCommits({}, function() return {} end, {}, "a", function() return nil end, "f")
typed/builtin//modules/world_vcs/M/__setInstallRef
M.__setInstallRef(refsJson: string?, installRef: string?) -> string?
A reference table as its row carries it, with the ref its install was
asked for recorded as "install_ref": a commit id, a version checksum or a
branch name of the origin. The rest of the table stays as written.
Parameters
refsJsonstring(optional) — The table's JSON text, or nil for an asset that has none.installRefstring(optional) — The ref the caller named, nil or empty when it named none.
Returns string? — The table's JSON text, unchanged when no ref was named.
worldVcs.__setInstallRef('{"deps":[]}', "release")
typed/builtin//modules/world_vcs/M/__settleOwedWrites
M.__settleOwedWrites(target: string, stallSeconds: number) -> { string }
Wait until no local write at or beneath target is still owed to the
world, then give up the ones that stay. Progress is a path leaving the
owed set, whatever else joins it meanwhile; once none has left for
stallSeconds what remains is a change the world is not taking, and
world.dropOwedWrites gives it up so the path reads as the world holds
it. Raises naming the paths when one is still owed after that.
Parameters
targetstring— The engine-canonical path to settle beneath.stallSecondsnumber— How long the owed set may go without a path leaving it.
Returns { string } — The paths given up, sorted.
worldVcs.__settleOwedWrites("/zero/source", 15)
typed/builtin//modules/world_vcs/M/__settleStagedPaths
M.__settleStagedPaths(asked: { string }, heldBefore: { [string]: boolean }, before: { [string]: any }, after: { [string]: any }, releaseThere: (ReleasedPath) -> ()) -> AddReport
Hold one staging call to the paths its caller staged. A path is
staged in one area at a time, so each path the call stages that another
staging area held is taken out of that area, whichever account opened
it and whether or not its session is open, and named in released. The
backend also stages, beside a named path, the referenced assets whose
current version no commit holds; each is named in carried and taken
out of any other area the same way, so the commit carries nothing its
caller is not told about. A staged path a caller of another account is
writing now is named in written. Staging is advisory: nothing another
area holds and nothing another caller is writing narrows the stage.
Parameters
asked{ string }— The paths the call named, after asset-family expansion.heldBefore{ [string]: boolean }— The paths the calling area held before the call, as a set.before{ [string]: any }— The calling area's status read before the call: itsclaimed_by_other_stagesnames the areas holding each path, and itsdirtywho wrote each.after{ [string]: any }— The calling area's status read after the call:stagedandclaimed_by_other_stages.releaseThere(ReleasedPath) -> ()— Takes one path out of the other staging area aReleasedPathrow names; called for a path that area still holds after the call.
Returns AddReport — { staged, carried, released, written }.
worldVcs.__settleStagedPaths(asked, held, before, after, release)
typed/builtin//modules/world_vcs/M/__settledStageLines
M.__settledStageLines(verb: string, report: AddReport) -> { string }
The lines a staging call writes to its caller about the paths it
settled: each dependency it carried beside what was named, then the
work of another account it took (M.__takeLines). Empty when the call
staged exactly what it named and took nothing from another account.
Parameters
verbstring— The calling API's name.reportAddReport— What__settleStagedPathsanswered.
Returns { string } — The lines, in that order.
worldVcs.__settledStageLines("world.add", report)
typed/builtin//modules/world_vcs/M/__stageBaseMoved
M.__stageBaseMoved(msg: string) -> boolean
True when the backend refused to build a commit on a stage whose branch has moved: the area was opened against the commit that was the branch head then, and another caller's commit is the head now.
Parameters
msgstring— The backend's refusal text.
Returns boolean — Whether that text is the moved-head answer.
worldVcs.__stageBaseMoved("ParentCommitMoved: branch \"main\" HEAD is ...") --> true
typed/builtin//modules/world_vcs/M/__stageNameFrom
M.__stageNameFrom(opts: any?, verb: string) -> string
The staging area a staging call acts on. opts.stage names it, and a
call that names none acts on the calling caller's own area, the one
vfs.currentAuthor identifies. Two agents driving one engine therefore
stage into different rows without naming anything, so each commits the
paths it staged and leaves the other's staged where they are. Naming an
area is deliberate and shares it: that is how two callers work one set of
paths together, and how a caller reaches an area it opened under an
earlier session of its own.
Parameters
optsany(optional) — The options table the caller passed, or nil.verbstring— The calling API's name, quoted in a refusal.
Returns string — The name of the staging area this call acts on.
worldVcs.__stageNameFrom({ stage = "fauna" }, "world.add") --> "fauna"
typed/builtin//modules/world_vcs/M/__stageOnceWith
M.__stageOnceWith(open: () -> string, stageInto: (string) -> (boolean, string)) -> (boolean, string)
Run one staging step against the implicit stage, resolving the handle
through open immediately before the step so the row cannot retire while
a barrier or a gate runs ahead of it. stageInto returns the
(ok, message) pair the backend answered with and must carry out the
WHOLE of one step: a retired row takes its entries with it, so when the
backend reports the row is gone a fresh row is opened and stageInto runs
once more, landing every path the step names in the stage that exists now
rather than only the ones after the failure. The second run resolves a
handle rather than repeating a caller's operation — staging a path is
idempotent at (stage, manifest_row), so it reaches exactly the state the
first run was asked for. Any other refusal is returned as it came.
Parameters
open() -> string— Resolves the implicit stage, returning its row id.stageInto(string) -> (boolean, string)— Carries out one whole staging step against a row id.
Returns (boolean, string) — The (ok, message) pair the surviving run produced.
worldVcs.__stageOnceWith(openStage, function(id) return true, "" end)
typed/builtin//modules/world_vcs/M/__stageReferencesRecords
M.__stageReferencesRecords(records: { ReferencesRecord }, held: { [string]: boolean }?, stage: (string, { string }, { string }) -> (boolean, string), unstage: (string) -> (boolean, string)) -> (boolean, string)
Stage, into the area stageId names, the references of every file the
commit records for them alone, and take out of it every references entry
the commit no longer records: the area then holds each file the commit
holds a change that moved, at the branch head's bytes with those pins
advanced. References are staged over the branch head, so an area opened
against an earlier head is answered with the backend's moved-head
refusal (__stageBaseMoved) and nothing more is staged: the caller
brings the area onto the head and stages them again. Any other refusal
raises, naming the file.
Parameters
records{ ReferencesRecord }— The files to record, as__referencesToRecordanswers them.held{ [string]: boolean }(optional) — The paths the area holds the references of alone now.stage(string, { string }, { string }) -> (boolean, string)— Stages one file's references:(path, assetGuids, worldGuids) -> (ok, message).unstage(string) -> (boolean, string)— Takes one path out of the area:(path) -> (ok, message).
Returns (boolean, string) — true, "" once every file is staged and every stale entry taken out; false and the refusal when the area's head moved.
worldVcs.__stageReferencesRecords(records, status.referencesOnly, stage, unstage)
typed/builtin//modules/world_vcs/M/__stageRowAbsent
M.__stageRowAbsent(msg: string) -> boolean
True when the backend's reason for refusing to take a path out of a staging area is that the area does not hold it, which is the state the caller asked for.
Parameters
msgstring— The backend's refusal text.
Returns boolean — Whether that text is the not-staged answer.
worldVcs.__stageRowAbsent("path /zero/source/a.luau is not staged in stage 41") --> true
typed/builtin//modules/world_vcs/M/__stageRowRetired
M.__stageRowRetired(msg: string) -> boolean
True when the backend's reason for refusing a stage operation is that
the row the handle names is gone. A stage row is deleted the moment a
commit materializes it, and a staging area is one row per
(world, branch, account, name), so a handle onto an area another caller
is also acting on names a row that a commit of theirs has since retired.
Parameters
msgstring— The backend's refusal text.
Returns boolean — Whether that text is the retired-row answer.
worldVcs.__stageRowRetired("stage_add: stage 41 does not exist") --> true
typed/builtin//modules/world_vcs/M/__stageableSources
M.__stageableSources(paths: { string }) -> { string }
The rows of paths a commit can stage: the candidates narrowed to
the paths the world can hold a manifest row for. A save-excluded path is
local to this machine and has no row anywhere, so stage_add names it
and refuses — which, from inside a commit, aborts over content the
author never staged and cannot unstage.
Parameters
paths{ string }— The candidate paths, engine-canonical.
Returns { string } — Those of paths the world can hold a row for, in the order given.
worldVcs.__stageableSources({ "/zero/source/a.luau" })
typed/builtin//modules/world_vcs/M/__stagedPath
M.__stagedPath(path: string) -> string
The manifest path a staging call names for path: the engine-canonical
/zero/... form, reached by the rule every VFS path argument takes — an
absolute or library path normalized as it stands, a relative one joined
under /source/. world.add("NOTES.md") therefore stages the file
vfs.write("NOTES.md", ...) wrote.
Parameters
pathstring— A VFS path, absolute, library or relative.
Returns string — The canonical path.
worldVcs.__stagedPath("NOTES.md") --> "/zero/source/NOTES.md"
typed/builtin//modules/world_vcs/M/__stagingSelection
M.__stagingSelection(path: string, root: string?, isDirectory: boolean, dirtyPaths: { string }) -> StagingSelection
The rows staging path names. A path inside an asset names that
asset's family, as __assetFamilyMembers states. A path outside any
asset names itself and every changed row beneath it, whether the path
is still on disk or not: a directory holds no row of its own, and a
deleted asset folder holds a delete row of its own with a delete row
for each of its files, so staging the folder names every one of them.
A path that names no changed row is named on its own when it is not a
directory, so the backend stages the row there or refuses it by name;
a directory with no changed row beneath it names nothing, and the
selection carries the notice saying so.
Parameters
pathstring— The engine-canonical path the call names.rootstring(optional) — The outermost asset folder that holdspath, nil when no asset holds it.isDirectoryboolean— Whetherpathis a directory on disk.dirtyPaths{ string }— The changed rows of the working set, engine-canonical.
Returns StagingSelection — { rows, notice }.
worldVcs.__stagingSelection("/zero/source/a.module", nil, false, dirty)
typed/builtin//modules/world_vcs/M/__staleReferences
M.__staleReferences(scope: { string }, differing: { DifferingTable }, status: LiveStageStatus, ops: ReferenceOps) -> { StaleReference }
The files a commit holds whose references do not resolve in what it
holds. Two cases make one rule:
a file the commit holds names a library whose marker is changed and left
out, so its table states that library at the pin the marker moves to;
a file the commit holds pins an asset of the world at bytes that asset
holds in the working tree and the commit leaves out.
Library content (everything under /zero/source/libs/) is never stale,
nor is a file the live ignore set matches, nor a table that differs only
in its install record. A file whose references moved with what the commit
holds and that the commit leaves out is recorded for its references
(__referencesToRecord), and is not stale. Nothing is staged or changed.
Parameters
scope{ string }— The rows the commit freezes with their bytes, engine-canonical.differing{ DifferingTable }— The rows whose live table differs from the head's.statusLiveStageStatus— The working tree and the calling area, read once.opsReferenceOps— What the check reads beyond the working tree.
Returns { StaleReference } — One entry per stale file, sorted by path.
worldVcs.__staleReferences(scope, differing, status, ops)
typed/builtin//modules/world_vcs/M/__staleReferencesRefusal
M.__staleReferencesRefusal(section: PublishBlockerClass) -> string
The text a commit raises over stale files: the class title, each file
with what moved its references, and the remedy. Past
STALE_REFERENCES_LISTED files the rest are counted, and
world.publishBlockers() names every one.
Parameters
sectionPublishBlockerClass— The class__staleReferencesSectionmade.
Returns string — The refusal text.
worldVcs.__staleReferencesRefusal(section)
typed/builtin//modules/world_vcs/M/__staleReferencesSection
M.__staleReferencesSection(stale: { StaleReference }) -> PublishBlockerClass?
The blocker class stale files make, in the shape world.publishBlockers
reports every class, with blocks = "commit": the class blocks the next
commit of the staged set and never a push of what is committed. One item
per file, each detail carrying the dep.stale_references category and
what its references name that the commit leaves out. world.commit
refuses with it.
Parameters
stale{ StaleReference }— The stale files, as__staleReferencesanswers them.
Returns PublishBlockerClass? — The class, or nil when no file is stale.
worldVcs.__staleReferencesSection(stale)
typed/builtin//modules/world_vcs/M/__stillMissing
M.__stillMissing(problems: { string }, holds: (string) -> boolean) -> { string }
The problems an install reports, less the files of a folder version the
closure gave no entry for that the materialized install holds anyway (a
folder that shares its content with a sibling stands whole). Each line
__unplacedFolderFiles states names its path before : the version.
Parameters
problems{ string }— The problem lines the install found.holds(string) -> boolean— Whether the world holds a file at a path.
Returns { string } — The lines that still stand, in their order.
worldVcs.__stillMissing({ "/zero/source/a.package/x: the version v of a holds this file at c, and the closure names no entry for it, so the install did not place it" }, function(path) return true end)
typed/builtin//modules/world_vcs/M/__takeLines
M.__takeLines(verb: string, released: { ReleasedPath }, written: { WrittenPath }, unstaged: boolean?) -> { string }
The lines a staging call writes to its caller about the work of another account it staged. Staging is advisory, so the call took each path; these say from whom. One line per staging area of another account it took paths from, naming the account, the area and the session holding it, and saying that account is working on them now when that session is open, or that the session has ended. One line per caller of another account writing paths the call staged. A take from an area of the caller's own account (another of its agents or engines) is in the report and writes no line.
Parameters
verbstring— The calling API's name.released{ ReleasedPath }— The paths the call took from other staging areas.written{ WrittenPath }— The staged paths a caller of another account is writing now.unstagedboolean(optional) — True when the call unstaged the paths it took rather than staging them into its own area.
Returns { string } — The lines, areas first, in the order the paths were taken.
worldVcs.__takeLines("world.add_all", report.released, report.written)
typed/builtin//modules/world_vcs/M/__takenFromOtherAreas
M.__takenFromOtherAreas(paths: { string }, claims: { any }) -> { ReleasedPath }
The ReleasedPath rows a staging call takes from other staging
areas when it stages paths: one row per path and per other area that
holds it, in the order paths lists them.
Parameters
paths{ string }— The paths the call stages.claims{ any }— The rows other staging areas hold, as status reads report them.
Returns { ReleasedPath } — The rows, each naming the path and the area holding it.
worldVcs.__takenFromOtherAreas({ "/zero/source/a.luau" }, status.claimed_by_other_stages)
typed/builtin//modules/world_vcs/M/__unlandedUnder
M.__unlandedUnder(rows: { any }) -> { [string]: number }
How many rows lie at or beneath each marked install root while flagged conflicted and holding the bytes they installed (a flag a previous update left behind, which a pull clears and lands).
Parameters
rows{ any }— The world's pulled rows.
Returns { [string]: number } — The count by root path, for the roots with at least one.
worldVcs.__unlandedUnder({})
typed/builtin//modules/world_vcs/M/__unplacedFolderFiles
M.__unplacedFolderFiles(assets: { any }, placement: { [string]: string }) -> { string }
The files a folder version an install writes states among its children and the closure gives no entry for, so the install places none of them. Each is named by the path it would land at, with the folder's version and the file's.
Parameters
assets{ any }— The closure's entries.placement{ [string]: string }— Where the install writes each entry, by the closure's keys.
Returns { string } — One line per file left out, sorted; empty when every written folder holds every file of its version.
worldVcs.__unplacedFolderFiles(assets, placement)
typed/builtin//modules/world_vcs/M/__unstageTakes
M.__unstageTakes(target: string, ownStaged: { [string]: boolean }, claims: { any }) -> { ReleasedPath }?
What unstaging target takes from other staging areas: nil when
the calling area holds the path or anything beneath it (it is unstaged
there), otherwise one ReleasedPath row per other area holding the path
or a path beneath it. A path is staged in one area at a time, so
unstaging a path another area holds takes it out of that area, whichever
account opened it; the working tree keeps its bytes.
Parameters
targetstring— The path to unstage.ownStaged{ [string]: boolean }— The paths the calling area holds, as a set.claims{ any }— The rows other staging areas hold, as a status read reports them.
Returns { ReleasedPath }? — The rows, or nil.
worldVcs.__unstageTakes("/zero/source/a.luau", {}, status.claimed_by_other_stages)
typed/builtin//modules/world_vcs/M/__updateCommitOf
M.__updateCommitOf(commit: any?) -> UpdateCommit
One commit of an origin's log (zeromind.world commits) as a review
lists it.
Parameters
commitany(optional) — A commit of the log.
Returns UpdateCommit — { id, message, author, date }.
worldVcs.__updateCommitOf({ commit_id = "c", message = "m", author_username = "a", created_at = "2026-10-08T00:00:00Z" })
typed/builtin//modules/world_vcs/M/__updateCommitPaths
M.__updateCommitPaths(results: { UpdateUnitResult }) -> { string }
The paths an update changed or moved, which the caller stages and commits next: a library's marker when its pin moved and every file whose references moved with it, and every path an install's pull merged, flagged conflicted, added or removed. A unit that failed contributes nothing.
Parameters
results{ UpdateUnitResult }— The per-unit results of an update.
Returns { string } — The paths, sorted, each once.
worldVcs.__updateCommitPaths({})
typed/builtin//modules/world_vcs/M/__updateNotice
M.__updateNotice(reports: { UpdateReport }) -> string?
The sentence an editor open raises for the units with an update:
N updates available: @<lib> <from7>→<to7>, …, <display name> <from7>→<to7>, …. zeromind.reviewUpdate("<first unit>") shows what it changes …; zeromind.update() applies every update without committing …, naming how
the zeromind tools are called and the ZeroMind panel. Libraries come first,
at most five units are listed and the rest read … and K more.
Parameters
reports{ UpdateReport }— TheUpdateReportlistworld.checkUpdatesreturned.
Returns string? — The sentence, nil when there is no update.
worldVcs.__updateNotice({})
typed/builtin//modules/world_vcs/M/__updateRecordInstall
M.__updateRecordInstall(root: InstalledRoot, ref: string?) -> PullAssetResult
Move an install that stands as one record row to the version of ref
(the ref it was installed at when nil): the record is written again, so the
one row of the install is the one entry that changes.
Parameters
rootInstalledRoot— The install, as__recordRootsstates it.refstring(optional) — The ref to install, or nil.
Returns PullAssetResult — A PullAssetResult naming the install as merged.
worldVcs.__updateRecordInstall(root, nil)
typed/builtin//modules/world_vcs/M/__updateRowPlan
M.__updateRowPlan(assets: { any }, localByGuid: { [string]: any }, placement: { [string]: string }, installRef: string?) -> UpdateRowPlan
What one pull of an install does to each asset of the closure it lands.
An asset the world holds no row for lands at its placement. A folder moves
to the closure's version, and takes its reference table, when either
differs. A file the world never edited (its bytes equal the version it
installed) lands, bytes and table together, whenever its version, its
table or a stale conflict flag says it differs from the closure, and a flag
it carries is cleared first. A file the world edited is reconciled by
__reconcileDecision: upstream unmoved leaves it alone (its reference table
is recorded when that differs), the world's bytes already equal to the
incoming ones advance the version, and a divergence merges. A row an
asset's placement lies beside, as a dependency version, is not the asset's
row. Lists are sorted by path.
Parameters
assets{ any }— The closure entries at the version the pull lands.localByGuid{ [string]: any }— The pulled row the pull acts on for each closure asset, by guid.placement{ [string]: string }— The path each closure asset is written to, by guid.installRefstring(optional) — The ref the pull was asked for, nil or empty when it named none.
Returns UpdateRowPlan — The UpdateRowPlan.
worldVcs.__updateRowPlan({}, {}, {}, nil)
typed/builtin//modules/world_vcs/M/__updateSummary
M.__updateSummary(files: { UpdateFile }) -> UpdateSummary
A review's counts over its files: per change kind, and the local edits that merge and that conflict.
Parameters
files{ UpdateFile }— The review's files.
Returns UpdateSummary — The summary.
worldVcs.__updateSummary({})
typed/builtin//modules/world_vcs/M/__updateUnitsFor
M.__updateUnitsFor(units: any?, reports: { UpdateReport }, libraries: { string }, roots: { InstalledRoot }) -> { UpdateUnit }
The units an update acts on: every unit the check reported when units
is nil, else the unit each name stands for (__resolveUpdateUnit), each
unit once, in the order given.
Parameters
unitsany(optional) — Nil, a name, or a list of names.reports{ UpdateReport }— What the update check reported.libraries{ string }— The library names the world pins.roots{ InstalledRoot }— The world's install roots.
Returns { UpdateUnit } — The units.
worldVcs.__updateUnitsFor(nil, {}, {}, {})
typed/builtin//modules/world_vcs/M/__updateWarnings
M.__updateWarnings(results: { UpdateUnitResult }, modified: { string }) -> { UpdateWarning }
The files with uncommitted changes an update touched, from what each
unit did and the paths that held such changes before it ran: a path a pull
merged reads merged and one it flagged conflicted reads conflicted; a
file whose references a library's pin move re-derived reads merged. A
folder whose files are listed is left out for them.
Parameters
results{ UpdateUnitResult }— The per-unit results of an update.modified{ string }— The paths holding uncommitted changes before the update.
Returns { UpdateWarning } — One { unit, path, outcome } per touched file, in result order.
worldVcs.__updateWarnings({}, {})
typed/builtin//modules/world_vcs/M/__versionOwnEntries
M.__versionOwnEntries(version: any?, guid: string, maxDepth: number?) -> { root: any?, entries: { any }, complete: boolean }
The files one version of an asset of another world owns, from a read
of that version's closure (GET /v1/assets/{guid}/closure?checksum=, out
to maxDepth edges): the asset itself and every entry at or beneath its
path. A read that ZeroMind cut short (truncated) is complete when every
folder among those files states its children and every child is present.
Parameters
versionany(optional) — The closure read.guidstring— The asset read.maxDepthnumber(optional) — The edge count the read was bounded at, nil for an unbounded read.
Returns { root: any?, entries: { any }, complete: boolean } — { root, entries, complete }: the asset's own entry, its files in the read's order, and whether the read holds all of them.
worldVcs.__versionOwnEntries(version, guid, 2)
typed/builtin//modules/world_vcs/M/__withoutInstallRows
M.__withoutInstallRows(status: any?) -> any
A status read without the rows of the installs it lists: an install is
one entry of installs, so none of its rows (its root, the files beneath
it, its dependencies) stands in dirty, staged or untracked as well.
Parameters
statusany(optional) — The status read, carryinginstalls.
Returns any — The status with those rows left out.
worldVcs.__withoutInstallRows({ dirty = {}, staged = {}, untracked = {}, installs = {} })
typed/builtin//modules/world_vcs/M/__writtenAmong
M.__writtenAmong(paths: { string }, dirtyReads: { { any } }) -> { WrittenPath }
The paths among paths a caller of another account is writing now,
read off the dirty rows the status reads list. A read lists a row only
while the area it asks about does not hold the row's current bytes, so
the read taken before a staging call is the one that names who wrote
what it staged.
Parameters
paths{ string }— The paths a staging call staged.dirtyReads{ { any } }— Lists of dirty rows, earliest read first.
Returns { WrittenPath } — One { path, by, account } row per such path, in the order paths lists them.
worldVcs.__writtenAmong({ "/zero/source/a.luau" }, { before.dirty })
typed/builtin//modules/world_vcs/M/__writtenByAnotherAccount
M.__writtenByAnotherAccount(row: any?) -> boolean
Whether a dirty row is the work in progress of a caller of another
account: a caller whose session is open wrote it (writer is
"other") and acts for an account other than the calling caller's. The
rows the account's other engines and agents write are the account's own.
Parameters
rowany(optional) — One dirty row as a status read reports it,{ path, writer, same_account }.
Returns boolean — True for a row a caller of another account is writing now.
worldVcs.__writtenByAnotherAccount({ writer = "other", same_account = false }) --> true
typed/builtin//modules/world_vcs/M/installInto
M.installInto(world: WorldNamespace)
Parameters
worldWorldNamespace
typed/builtin//modules/world_vcs/M/installOpenBackfill
M.installOpenBackfill()
Subscribe the install-root backfill to the world becoming ready, so an
editor open marks the roots of installs made before the engine recorded
them. The backfill runs as its own task, off the open's critical path,
and shows the person nothing. Called once from prelude.luau after the
engine global is set.
worldVcs.installOpenBackfill()
typed/builtin//modules/world_vcs/world/add
world.add(paths: string | { string }, opts: AddOpts?) -> AddReport
Stage one path's manifest row, or each of a list of paths,
expanding to the full asset family if a path lives inside a composite
asset. Idempotent at (stage, manifest_row). A list stages every row
its paths select once, so naming several files of one asset stages
that asset's family once, as git add a b c does. Pass { force = true } to bypass
the .zmignore / .gitignore gate — same intent as
git add -f. Without force, attempts to stage an ignored
path (or a path whose .refs points at an ignored dep)
error.
{ stage = "<name>" } stages into the named area, shared with
whoever else names it, instead of the caller's own, so a commit
naming that area freezes these paths and leaves every other area's
staged.
Staging is advisory, and a path is staged in one area at a time.
Naming a path another staging area holds takes it from that area,
whichever account opened it and whether or not its session is open,
and the report names the area, its account and whether that session
is working now in released. A take from an area of another account
warns, naming that account, the area and its session; a take from
another agent or engine of the caller's own account does not. A
staged path a caller of another account is writing now is named in
written and warned about the same way.
A staged source that references an asset whose current version no
commit holds needs that version committed with it. The asset is
staged beside the source, taken from any other area holding it, and
named in carried.
A .scene stages as its files on disk hold it, in edit and in play
alike; live entity edits nobody saved stay in the session, and a
notice names them with the calls that keep or drop them.
An installed asset is staged by its root path: every row of the install
(its files, its dependencies under /zero/source/deps/) is staged with
it, as world.vcsStatus().installs lists it. A path that is one of the
other rows of an install raises, naming the install and its root. A file
world.vcsStatus lists with the reason references moved stages like
any change: its row carries its live reference table and its bytes are
the ones the branch head already holds.
Parameters
pathsstring | { string }— The path to stage, or a non-empty list of paths. Each must be a non-empty string; a gate refusing any of them refuses the call before anything is staged.{ renormalize = true }first restates every folder asset's row at or beneath the named paths from the files beneath it, asgit add --renormalizedoes: a folder whose stated version is not the folder hash of its files is staged at the hash its files hold, and a folder that already states it is left out.optsAddOpts(optional) — Optional{ force: boolean?, renormalize: boolean?, stage: string? }. Defaults to{ force = false }on the calling caller's own staging area.
Returns AddReport — What the call staged: staged lists the rows the paths name (the asset family of a path inside an asset; otherwise the path and every changed row beneath it, deletions included, so a deleted asset folder stages the deletion of each of its files), leaving out with a warning naming it each path no row at or beneath which holds a change, carried the referenced assets staged beside them because no commit holds their current version, released each path taken from another staging area (stage, its account, same_account, its session and whether that session is live), and written each staged path a caller of another account is writing now (by, account). Each carried row, and each released and written row of another account, is also written to the caller as a warning.
world.add("/source/foo.luau")
world.add({ "/source/a.luau", "/source/b.luau" })
world.add("/source/scene_dirty/entities/42.json", { force = true })
world.add("/source/fauna.module", { stage = "fauna" })
world.add("/source/fauna.module", { renormalize = true })
typed/builtin//modules/world_vcs/world/add_all
world.add_all(opts: AddAllOpts?) -> ({ string }, AddAllReport)
Stage every dirty manifest row, whoever wrote it, skipping any
path that matches .zmignore / .gitignore. Paths that match an
ignore pattern are skipped; world.add(path, { force = true }) is the
explicit way to override the gate for an individual path.
Rows still flagged conflicted by world.pullAsset are held back
too: resolve them (edit + world.add(path), or
world.resolvePullConflict) and re-run.
Staging is advisory. The working tree is one per branch and staging
areas are not, and a path is staged in one area at a time: a path
another staging area holds is taken from that area, whichever account
opened it and whether or not its session is open, and named in
released with the area, its account, its session and whether that
session is live. A row another caller is writing is staged like any
other. The calling account's own agents and engines never warn each
other: their areas and their writes are the account's own. A take
from another account's area warns once per area, naming the account,
the area and its session, and saying that account is working on the
paths now when its session is open; a staged path a caller of another
account is writing now warns once per caller and is named in
written.
Each staged path brings its asset family. A file world.vcsStatus
lists with the reason references moved is staged too. The call reads the
working tree a fixed number of times and stages and releases in
batches, so its cost follows the size of the dirty set.
A scene stages as its files on disk hold it, in edit and in play
alike: live entity edits nobody saved stay in the session, and a
notice names each scene holding some, with the calls that write
them into scene.json (sceneAuthoring acceptChanges,
layers.active:save()) or drop them.
Parameters
optsAddAllOpts(optional) — Optional{ stage: string? }.stagenames the staging area to stage into; omitted, the call stages into the calling caller's own area.
Returns ({ string }, AddAllReport) — The paths that were held back: the conflicted ones. Empty when none were. The second value reports the call in full: staged lists the paths it added to the staging area, held lists every held-back path with its reason ("conflicted"), released lists each path taken from another staging area (stage, account, same_account, session, live), and written each staged path a caller of another account is writing now (by, account).
world.add_all()
world.add_all({ stage = "fauna" })
local held, report = world.add_all()
typed/builtin//modules/world_vcs/world/affirm
world.affirm(token: string)
Consume an XXX-XXX-XXX-style affirmation token returned by
a destructive op surface (e.g. vfs.remove). The destruction
commits atomically with the pending-row delete; the token is
one-shot. Errors verbatim on expiry / wrong-user.
Parameters
tokenstring— The affirmation token.
world.affirm("ABC-DEF-GHI")
typed/builtin//modules/world_vcs/world/assetInstallable
world.assetInstallable(opts: AssetInstallableOpts) -> AssetInstallableReport
Report whether one published asset can be installed on its own,
without raising. world.previewInstall plans a real install and
raises when the closure will not resolve; this answers the prior
question — does this guid name something ZeroMind will hand over by
itself — as a verdict a caller can branch on. Content that ships
inside a larger library (a module inside a package, a material
inside a system) carries its own published identity, so the answer
is per asset rather than per library. Reads only.
Parameters
optsAssetInstallableOpts—{ guid }names the asset;refpins a commit-id instead of the latest.
Returns AssetInstallableReport — An AssetInstallableReport — installable, a verdict, a one-line detail, and the closure's nodes / tree_children / deps shape when one resolved.
world.assetInstallable({ guid = asset.guid("@builtin::materials.neon") })
typed/builtin//modules/world_vcs/world/awaitOutgoingSync
world.awaitOutgoingSync()
Wait until every /source write this session made has reached
the branch it was written against. Raises naming the paths that did
not land. add / commit / push / checkout / merge / pull
already wait on their own; call this before rebinding after a burst
of writes, which world.checkout and world.swap refuse over.
Returns Nothing. Raises when a write did not land.
world.awaitOutgoingSync() ; world.checkout("main")
typed/builtin//modules/world_vcs/world/beforeCommit
world.beforeCommit(name: string, fn: ((any) -> ())?)
Open-or-resume a staging area, set the message, and materialise
the commit. Commits ONLY what's already staged via world.add /
world.add_all — git semantics, not git commit -a. The reducer
auto-deletes the stage row on success so a subsequent
world.commit opens a fresh one.
Where derive hooks compute outputs for the staged content, the commit
is sealed under its id first and lands once every required output is
computed. The id this returns can name a commit that has not landed
yet: world.vcsStatus().sealed lists the sealed commits with what each
waits on, and world.unseal returns one to the staging area. When an
older sealed commit holds a refusal, this commit queues behind it and
the call says so.
{ stage = "<name>" } materialises one of the caller's own staging
areas, so the commit carries the paths staged under that name and
leaves every other caller's staged. When a commit from another
caller has landed since the area was opened, this brings the area
onto the branch head there is now and commits it there.
A staged scene is frozen as staged. Live entity edits its overlay holds that nobody saved stay in the session, and a notice names them once the commit lands.
Pre-flight .zmignore refs gate: every staged source's
aggregated deps (via asset.deps, which recurses composite
asset folders) are checked against the live ignore set. If
any dep target's path is currently ignored AND the dep target
is not itself in the stage, the commit is refused. This
mirrors the closure invariant — a commit whose deps can't
resolve cleanly shouldn't land. Force-staging the dep
alongside (world.add(dep, { force = true })) makes the
ignored dep satisfy the gate.
Run fn at the start of every world.commit, before the commit
reads what is staged — the moment to write content that belongs in
the commit being made, and stage it. fn receives { stage }, the
staging area the commit materialises (nil for the caller's own), to
pass to world.add. Registering a name again replaces what it held,
and fn = nil removes it. A hook that raises refuses the commit,
naming the hook.
Parameters
namestring— What identifies the hook.fn((any) -> ())(optional) — The hook, or nil to remove it.
Returns The newly-allocated commit id (ULID string).
local id = world.commit("feat: ship widget")
local id = world.commit("fauna: the swallow colony", { stage = "fauna" })
world.beforeCommit("procgen.diskcache", function(ctx) ... end)
typed/builtin//modules/world_vcs/world/branches
world.branches() -> { { branch: string, commit_id: string, current: boolean } }
Every branch this world has, with the commit each one's head
names and which one this session is on — git branch --list. Sorted
by name. A branch exists for everyone in the world; which one you are
on is yours alone, so current is true for at most one row here and
says nothing about where anybody else is.
Returns { { branch: string, commit_id: string, current: boolean } } — Array of { branch, commit_id, current }.
for _, b in ipairs(world.branches()) do print(b.branch, b.commit_id) end
typed/builtin//modules/world_vcs/world/checkUpdates
world.checkUpdates() -> ({ UpdateReport }, { UpdateSkip })
Discover which units of this world have an update available. Read-only — makes no local mutation and no VCS write. A unit is one of:
- a library: a marker (
/source/libs/@<name>,@builtinincluded) whoserequested_ref(defaultmain) now resolves in the library's world to a commit other than the marker'sresolved_commit. Apply withworld.updateLibrary(name). - an installed asset: an install root (the asset that was installed;
its reference table marks it) whose recorded version differs from the
newest version of that asset at its origin, one lookup per root. A
dependency of an install is never compared or reported: only the
root is. Apply with
world.update(<root path>)(zeromind.update), which installs the root's new closure with each dependency at the version the new root records.
Installed assets whose origin world is a library's world are part of that library's unit. A pulled row that no table marks as an install root (an install older than the marks, content forked from another world) is not reported.
Returns ({ UpdateReport }, { UpdateSkip }) — Two values: an array of UpdateReport (libraries first), each { kind = "library", library, from, to } or { kind = "asset", root_path, root_guid, display_name, from, to }, empty when everything is current; and an array of { name, reason } for the units whose origin could not be reached, which never fail the whole call.
local reports, skipped = world.checkUpdates()
typed/builtin//modules/world_vcs/world/checkout
world.checkout(branch: string) -> string
Switch this session to another branch — git checkout <branch>.
The branch must already exist (create one with world.createBranch).
The tree is replaced by the branch's own content. Which branch this
session is on is this session's alone; the branch itself is shared,
so others may be on the one you move onto.
Your uncommitted changes and staged entries come with you, as git
keeps them across a switch: each lands on branch as it read
before. When the switch would replace a path you changed with a
different version, the call refuses with git's message naming the
paths and nothing moves — commit or stash them first. Another
caller's uncommitted changes stay on the branch they were made on.
The switch is all or nothing: a write of yours still on its way to
the branch you leave is waited for, and one that does not arrive
refuses the switch before anything moves; a switch refused after
your changes moved puts them back, and names each path that does not
read as it did.
Returns only once the branch's content has landed, so world.head,
world.log, world.commit and the VFS all target the new branch
immediately afterwards.
Parameters
branchstring— The branch to switch to.
Returns string — The branch now checked out.
world.checkout("feature")
typed/builtin//modules/world_vcs/world/checkoutPaths
world.checkoutPaths(rev: string, paths: { string }, opts: CheckoutPathsOpts?) -> { string }
Write paths as a revision holds them, and stage them: git checkout <rev> -- <path>.... rev is any revision world.revParse
reads. Each path comes back as the asset the revision records: its
asset guid, its bytes and its kind, so what refers to it by guid keeps
resolving, and restoring it again gives the same asset again. A folder,
a composite asset's or a plain directory, brings back every file the
revision holds beneath it, so a deleted component, module or package
comes back whole; a file beneath it the revision does not hold stays.
A file whose folder asset was deleted brings the folder back with it.
Binary files come back by their content. A path whose version is the
branch head's reads clean afterwards and leaves the staging area; any
other is staged. The paths that name nothing the
revision holds are refused together, naming each, and nothing is
written. Returns once the working copy holds every written path as the
revision records it.
Parameters
revstring— The revision to read.paths{ string }— The paths to write: files, composite asset folders or directories.optsCheckoutPathsOpts(optional) — OptionalCheckoutPathsOpts:stage = falsewrites the working tree and leaves the staging area as it was (git restore --source <rev> -- <path>).
Returns { string } — The paths written, as the revision names them.
world.checkoutPaths("HEAD~1", { "/source/foo.luau" })
world.checkoutPaths("HEAD", { "/source/Doom.component", "/source/notes.md" })
world.checkoutPaths("HEAD~2", { "/source/levels" }, { stage = false })
typed/builtin//modules/world_vcs/world/checkoutSide
world.checkoutSide(path: string, side: string)
Take one side of an unmerged path — git checkout --ours|--theirs <path>. The path holds the side's whole content afterwards; a merge
conflict's side is the session branch (ours) or the branch merged
in (theirs), an installed asset's update conflict's side is your
local version (ours) or the upstream one (theirs). Record the
resolution with world.add and commit.
A merge conflict's side is that branch's committed version: its
bytes, its asset guid, its kind and its references, so what refers
to the asset by guid keeps resolving. Text, binary and composite
assets alike: a composite asset (a .scene, .package,
.component … folder) is taken whole — name its folder and the
folder and every file in it come from that side. A side that deleted
the path leaves it a pending delete. Taking a side again replaces
the earlier choice.
Parameters
pathstring— The unmerged path, or the composite asset folder holding one.sidestring—oursortheirs.
world.checkoutSide("/source/foo.luau", "theirs")
typed/builtin//modules/world_vcs/world/commit
world.commit(message: string, opts: CommitOpts?) -> string
Parameters
messagestringoptsCommitOpts(optional)
Returns string
typed/builtin//modules/world_vcs/world/conflicts
world.conflicts() -> { ConflictEntry }
List every locally-pulled row currently flagged conflicted —
the findable surface world.pullAsset leaves behind on an
unresolved merge. Read-only.
Returns { ConflictEntry } — An array of ConflictEntry, one per conflicted row. Empty when nothing is conflicted.
local list = world.conflicts()
typed/builtin//modules/world_vcs/world/contentRequirements
world.contentRequirements(scope: { string }?) -> { ContentRequirement }
List the world's unmet content requirements: user-authored assets
whose type-declared content constraints are not yet satisfied (an
empty README, a .metadata with no description or tags — the
empty-skeleton state a fresh create emits for the author to fill, and
a description the type asks for that still starts with the
[[describe]] placeholder, which describes = true marks on the
row; world.push asks for a description only of the assets the push
changes).
The same walk world.push gates on, over the whole world: the
push refuses over the rows whose asset the calling staging area
holds or committed, and publishes the rest with the shortfall
recorded on the asset that carries it. world.publishBlockers
reports the caller's own share as the content class beside the
other two. Empty list = every checked asset meets its type's
contract. A write into an asset reports the rows its type's
type.yaml requirements give for that asset as it lands:
contentRequirements on the vfs.write report and on the MCP
write_file / edit_file result.
Parameters
scope{ string }(optional) — Asset paths to restrict the check to — pass a status read's dirty + staged paths to check only content that would actually publish (a per-file path matches its containing asset; each path resolves directly, with no world enumeration). Omit for the full-world walk the push gate performs.
Returns { ContentRequirement } — Array of ContentRequirement rows — { asset, typeName, detail, path, describes? }, where path is the asset the row is about.
for _, r in ipairs(world.contentRequirements()) do print(r.asset, r.detail) end
typed/builtin//modules/world_vcs/world/contribute
world.contribute(opts: ContributeOpts?) -> { ContributeOutcome }
Send improvements to installed content back upstream — git subtree push ending in a pull request. For each targeted origin
world: the diverging subtree is remapped to the origin's canonical
paths, three-way merged against the origin's CURRENT content
(regions the origin also changed become local conflicts to
resolve first), pushed as a contrib/<id> branch in the origin
world, and opened as a pull request there. With merge (the
default) the pull request is merged immediately when authorized —
a refusal leaves it open and reported, never a failure. After a
merge, the local fork re-pulls so its origin pins advance and the
asset no longer reads as ahead.
Parameters
optsContributeOpts(optional) — OptionalContributeOpts—targets(origin world guids; default all ahead),roots(install roots; default every installed asset ahead),merge(default true),title,description,dryRun.
Returns { ContributeOutcome } — Array of ContributeOutcome, one per targeted origin.
local r = world.contribute({})
typed/builtin//modules/world_vcs/world/convertInstalls
world.convertInstalls(opts: { only: string?, dryRun: boolean? }?) -> { { root_path: string, record: string, rows_removed: number, overrides_kept: number, files_added: number, completion: string? } }
Bring installs that stand as pulled rows (an earlier engine landed each
file as a row of the world) into one record row each, the form
world.installAsset writes now. The record is built from the rows
themselves, written, and once this engine has materialized the install
from it the unchanged rows are taken out of the world; a file changed
since the install stays a row of the world. Nothing is committed: rows
that were committed leave as deletions in the next commit. Returns one
entry per install, and with dryRun writes nothing. Once the rows are
taken out the record is stated again from the published version it
names, so an install the old engine cut short holds every file of the
version: files_added counts the files that added, and completion
says why the record stayed as the rows stated it (the published
version unreachable, or an asset of it that does not load as its type).
The files stay where they stand and the call yields between its steps, so
it never holds a frame: a run that outlasts the execute budget continues
as a task.
Parameters
opts{ only: string?, dryRun: boolean? }(optional) —{ only?: string, dryRun?: boolean }:onlynames one install by its root path.
Returns { { root_path: string, record: string, rows_removed: number, overrides_kept: number, files_added: number, completion: string? } } — { { root_path, record, rows_removed, overrides_kept, files_added, completion? } }.
world.convertInstalls({ dryRun = true })
typed/builtin//modules/world_vcs/world/createBranch
world.createBranch(name: string, fromCommit: string?, opts: { force: boolean? }?)
Create a branch — git branch <name> [<start>]. The branch
starts at fromCommit (defaults to the session branch's HEAD)
and gets its own working tree, materialized from that commit.
The session stays on its current branch; move onto it with
world.checkout("<branch>") (git checkout). With
{ force = true } (git branch -f) an existing branch is pointed
at fromCommit and its working tree rewritten to it, in one
transaction; that refuses only when the branch's working tree holds
uncommitted changes, which the rewrite would lose.
Parameters
namestring— The new branch name.fromCommitstring(optional) — Commit id to start at. Defaults toworld.head().opts{ force: boolean? }(optional) — Optional{ force: boolean? }.
world.createBranch("feature")
world.createBranch("feature", world.revParse("HEAD~1"), { force = true })
typed/builtin//modules/world_vcs/world/deleteBranch
world.deleteBranch(branch: string, opts: DeleteBranchOpts?)
Delete a branch — git branch -d|-D <name>. Drops the branch and
the working tree it owns; its commits are left alone, since deleting
a branch is dropping the name and the tree under it, not rewriting
history. Without opts.force (-d) it refuses a branch whose
commits the session's branch does not hold, and one whose working
tree holds uncommitted changes, naming them, since those go with the
tree; { force = true } (-D) deletes it regardless. Refuses the
branch this session is on (check out another first) and the world's
last branch.
Parameters
branchstring— The branch to delete.optsDeleteBranchOpts(optional) — OptionalDeleteBranchOpts—force.
world.deleteBranch("feature")
world.deleteBranch("experiment", { force = true })
typed/builtin//modules/world_vcs/world/derive
world.derive(name: string, spec: any?)
Register a computation whose output lands in the commit with the
content it is computed from. The hook (scope = "asset")
computes output inside each staged asset matches accepts, from the
asset as staged. Its fingerprint and compute are entries of
module, run on a worker.
Staging starts the work, and a required output that is not computed
holds the commit that carries it: world.commit seals the commit and
returns its id at once, and the commit lands with the outputs once
they are computed. When every required output is computed already,
world.commit returns after the commit landed. spec = nil removes it.
Parameters
namestring— What identifies the hook.specany(optional) — The hook, or nil to remove it.
world.derive("thumbs", { scope = "asset", output = "thumb.png", matches = function(ref) return ref.type == "texture" end, module = "@root::thumbs", fingerprint = "fingerprint", compute = "compute" })
typed/builtin//modules/world_vcs/world/diff
world.diff(...: string) -> any
Mirror git diff's CLI arg shape. Returns per-file diffs
by default; pass --stat for summary stats, --name-only for
just paths. Positional commit ids drive the two sources;
--staged pivots to staged-vs-HEAD. ---separated args
scope the diff to specific paths. --stage=<name> reads the named
staging area in place of the calling caller's own.
Parameters
...string— Variadic string args: flags, commit ids,--, path filters.
Returns any — Array of DiffFile tables (or string-list for --name-only).
local files = world.diff()
local files = world.diff("--staged")
local files = world.diff("abc", "def")
local names = world.diff("--name-only")
local files = world.diff("--staged", "--stage=fauna")
typed/builtin//modules/world_vcs/world/diffUpstream
world.diffUpstream(root: string) -> { UpdateFile }
What your copy of an installed asset changed against the version
it was installed from — the diff zm diff <root> upstream prints.
One UpdateFile per file under root whose content differs from
its origin version, with the unified diff from the origin version
to yours for a text file (diff and hunks are nil for a binary
one). Empty when the installed asset holds no change of yours.
Parameters
rootstring— The installed asset's root path (or any path inside it).
Returns { UpdateFile } — Array of UpdateFile, sorted by path.
for _, f in ipairs(world.diffUpstream("/source/Doom.component")) do print(f.diff) end
typed/builtin//modules/world_vcs/world/discard
world.discard(opts: StageOpts?)
Drop a staging area without committing. Live manifest dirty flags are preserved so the user can re-stage later. No-op if the area doesn't exist.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to drop. Omitted, the call drops the calling caller's own area.
world.discard()
world.discard({ stage = "fauna" })
typed/builtin//modules/world_vcs/world/discardAll
world.discardAll(scope: string?) -> DiscardAllResult
Discard every change under a path in one operation — the whole
working tree by default. Every file edited since its commit goes back
to the committed version, every deletion comes back, and every file
no commit holds, an uncommitted install included, leaves the tree.
Each goes to trash first, where world.restore(<handle>) brings it
back. The paths a staging area holds stay, a staged file edited since
it was staged goes back to its staged copy, and a change another
account is making and a dependency another install records stay as
well: they are named in kept. One backend operation takes the whole
scope back, and the call returns once the working copy reads as the
committed content.
Parameters
scopestring(optional) — The VFS path to discard beneath; omitted, every path.
Returns DiscardAllResult — { discarded, paths, kept } — how many rows went back, which, and which rows under the scope stayed because another account or another install holds them.
world.discardAll()
world.discardAll("/source/hud")
typed/builtin//modules/world_vcs/world/discardFile
world.discardFile(path: string)
Discard a path's unstaged working edits, taking it back to what
it was staged or committed as — the git restore <path> shape. A
folder takes every file beneath it back with it, and a deleted
path comes back as its committed version. The stage is the
baseline when the path is staged, the last commit when it is not,
and where it is neither there is nothing to come back to, so the
path goes away. Staging is left exactly as it was; world.unstage
is the verb that changes it. One shot: edited bytes that revert to
a committed version are snapshotted to trash first, so that case is
recoverable via world.restore(<handle>); a deletion taken back
throws nothing away and leaves no trash entry. Returns once the
working copy reads as the reverted content: text reads back as
committed, and binary bytes are served at the committed version on
the next read. Errors when nothing at or beneath the path is dirty
(nothing to discard).
An installed asset is discarded by its root path, which puts the whole
install back at the version the branch head holds as one unit: its rows
leave the staging area and then return to their committed version, or go
away when no commit holds them. A dependency another install records too
stays. Another row of an install raises, naming the install and its
root. A file whose only difference from the head is its reference table
(listed with the reason references moved) raises too, naming what
moved its references and the undo: moving the library pin back
(world.updateLibrary, or discarding the pin marker) or reverting the
moved asset.
Parameters
pathstring— The VFS path whose unstaged edits to discard.
world.discardFile("/source/foo.luau")
world.discardFile("/source/Doom.component")
typed/builtin//modules/world_vcs/world/dropDerived
world.dropDerived(outputPath: string, opts: { stage: string? }?) -> number
Leave the derived output at outputPath out of the commit. Only
an output whose hook is best-effort can be dropped; a required one is
refused, naming the other ways out (retry, provide, unseal).
Parameters
outputPathstring— The output's VFS path.opts{ stage: string? }(optional) — Optional{ stage: string? }naming the staging area.
Returns number — How many reservations were dropped.
world.dropDerived("/zero/source/gold.material/extra.json")
typed/builtin//modules/world_vcs/world/fetch
world.fetch(branch: string?) -> FetchResult
Update the origin/<branch> remote-tracking ref — git fetch.
Mirrors the world's ZeroMind branch head into the local commit
history (no working-tree change) and reports how the session
branch relates to it: behind origin commits to pull, ahead
local commits to push, diverged when both. A stale installed
pin or out-of-band ZeroMind change shows up here as behind —
reconcile with world.pull().
Parameters
branchstring(optional) — Remote branch to fetch. Defaults to the session branch.
Returns FetchResult — A FetchResult table.
local f = world.fetch()
typed/builtin//modules/world_vcs/world/forkLive
world.forkLive(opts: { source: string, sourceBranch: string?, maxBatches: number? }) -> number
Seed THIS (empty) world's live content from another world by
copying its whole manifest as clean Pulled rows — the
in-engine half of "fork a world". Provenance is preserved: each
row points at the content's ORIGINAL owner (a fork of a
fork-of-A still points at A), so the fork never claims to have
authored what it pulled. The copy runs server-side in bounded
batches (idempotent + resumable), looping until the source is
fully mirrored. Pair with world.add_all() + world.commit() +
world.push() to publish the fork.
Parameters
opts{ source: string, sourceBranch: string?, maxBatches: number? }—{ source, sourceBranch?, maxBatches? }—sourceis the source world GUID; branches default to"main";maxBatchescaps the batch loop (default 60 ⇒ up to ~120k entries).
Returns number — The number of pulled (dirty) rows now staged-pending on the fork.
world.forkLive({ source = "e89aa92e-4c1f-460e-acd8-73859dd3a346" })
typed/builtin//modules/world_vcs/world/forkStatus
world.forkStatus() -> { ForkStatus }
Per-asset "ahead of origin" report — the fork analogue of git
status against an upstream. Every installed (pulled) row whose
content diverges from its pinned origin is listed, partitioned by
the TRUE origin world it was pulled from (nested dependencies
carry the world that authored them, not the intermediary they
arrived through). This is information for judgment: decide
whether a change belongs upstream, then world.contribute.
Returns { ForkStatus } — Array of ForkStatus partitions.
for _, f in ipairs(world.forkStatus()) do print(f.origin_world, #f.entries) end
typed/builtin//modules/world_vcs/world/head
world.head() -> string?
Return the current branch HEAD commit id, or nil if the branch has no commits yet.
Returns string? — The commit id string, or nil.
local id = world.head()
typed/builtin//modules/world_vcs/world/installAsset
world.installAsset(opts: InstallAssetOpts) -> InstallAssetResult
Install a published asset into this world, pulling the asset and
every dependency it closes over and writing them into the source tree.
The world holds ONE row for the install, its record <dest>.pull, which
lists every file of the version and every dependency placed beside it;
every engine that holds the row materializes the install from it, so a
file of the install is a row of the world only once it is changed.
Returns once this engine has materialized the install and it runs.
Installing an asset where the world already holds it is safe to repeat:
rows at this version are left as they are, and rows of an earlier
version are moved to this one. The root lands at at, else where the
world already installed it, else at /source/<name>; a copy of it the
world holds at this version anywhere else (another install's
dependency under /source/deps/) moves there, so the world holds the
asset once and everything that depends on it resolves to the moved
copy. A path holding another asset, or local
edits to this one, refuses the install. An install that does not
materialize takes its record out again, so the world holds nothing of it
and the same install can simply be run again.
Parameters
optsInstallAssetOpts—{ guid }names the root asset to install;refpins a specific commit-id instead of the latest.
Returns InstallAssetResult — { assets_written, blobs_downloaded, root_guid, root_path, root_version, deps, pending, problems, outcome, moved_from, record, files, timings }. record is the path of the install's row, files the number of files and folders it states, timings the seconds each step took. outcome is installed, updated or already_present; moved_from names the copy the root moved from, nil when it moved from nowhere. blobs_downloaded counts the blobs the install fetched (bytes already cached cost none); pending lists the written paths still unreadable when the wait for them stalled, and is empty when every one reads back; problems names each reference site in the installed content that resolved to nothing in the world it was published from, and each file a folder version the install wrote holds at its version that ZeroMind gave no entry for, and is empty when every site resolves and every written folder holds all of its files. A folder version lands holding every file its version states: a file the closure leaves out of it is read from that version of the folder.
local r = world.installAsset({ guid = assetGuid })
typed/builtin//modules/world_vcs/world/installLibrary
world.installLibrary(opts: InstallLibraryOpts) -> InstallLibraryResult
Declarative cross-world dependency. Writes a single marker
file at /source/libs/@<name> whose body is the
zero/world-import/v1 JSON. The next commit ships it as one
regular manifest entry; ZM's import-derivation pass at
finalize-time decodes the marker and stamps the new commit's
imports[]. Unmodified library content never ships in the
importing world's tree: the engine fetches the pinned commit
from ZeroMind and mounts it as @<name>::<path>. The call
returns once the library is mounted (require("@<name>::…")
resolves and library.list() lists it), and raises naming why
when it does not mount, leaving the marker as it was before the
call. A marker already there whose library is mounted is
refused (move it with world.updateLibrary); one whose library
never mounted is replaced, so running the install again repairs it.
Parameters
optsInstallLibraryOpts— SeeInstallLibraryOpts.opts.worldis the upstream world's guid (required).opts.commitis the upstream commit_id to pin (optional; resolvesopts.reformainif omitted).opts.asis the local library name (defaults to the upstream world's slug).opts.refis the human-meaningful ref recorded in the marker.
Returns InstallLibraryResult summarising the install.
world.installLibrary({ world = "guid", as = "combat" })
typed/builtin//modules/world_vcs/world/installedAssets
world.installedAssets() -> { InstalledAsset }
Every asset this world carries from ZeroMind, keyed by the published guid it was pulled from. The read that answers "what is actually in this world" by identity rather than by path — an installed asset's local name can be chosen by the installer, so a path is not the thing to check a pull against.
Returns { InstalledAsset } — An array of InstalledAsset sorted by local path, one per pulled row carrying a published guid.
for _, a in ipairs(world.installedAssets()) do print(a.asset_guid, a.path) end
typed/builtin//modules/world_vcs/world/installedLibraries
world.installedLibraries() -> { LibraryMarker }
The libraries this world has installed: one row per library marker
under /zero/source/libs, @builtin among them, sorted by name.
Returns { LibraryMarker } — { name, world_guid, requested_ref?, resolved_commit } per library: its name with the @, the world it comes from, the ref that was asked for and the commit it is pinned at.
for _, lib in ipairs(world.installedLibraries()) do print(lib.name, lib.resolved_commit) end
typed/builtin//modules/world_vcs/world/installs
world.installs(opts: { rows: boolean? }?) -> { InstallInfo }
The installs this world holds, one entry each, whichever form each
is stored in: record (one row, every engine materializes the files
from it) or rows (an earlier engine landed one row per file, and
world.convertInstalls brings it into one record). An asset installed
at more than one place names the other places in also_at.
Parameters
opts{ rows: boolean? }(optional) —{ rows: boolean? }:rows = falseleaves the row installs out, which reads no row of the world.
Returns { InstallInfo } — An array of InstallInfo sorted by path.
for _, i in ipairs(world.installs()) do print(i.root_path, i.form, i.files) end
typed/builtin//modules/world_vcs/world/list
world.list() -> { WorldEntry }
List every world the authenticated user has access to.
Calls the spacetime list_my_worlds procedure which wraps
ZeroMind's GET /v1/me/worlds. Flattens each entry to one
record per world with the role promoted to a top-level field.
Returns { WorldEntry } — Array of WorldEntry records.
local worlds = world.list()
typed/builtin//modules/world_vcs/world/log
world.log(opts: LogOpts?) -> { CommitRow }
The history of the session branch, newest first — git log:
every commit reachable from HEAD through both parent edges, so a
merged branch's commits and a pulled commit appear along with the
commits recorded on this branch. opts.rev starts the history at
another revision (git log <rev>). Pass opts.path to get the
per-path history (git log -- <path>): only the commits that
touched that file, newest-first.
Parameters
optsLogOpts(optional) — Optional.opts.limitcaps the number of commits (default 50, 0 = all).opts.revnames where the history starts.opts.pathscopes the log to one file.
Returns { CommitRow } — Array of CommitRow tables.
local commits = world.log({ limit = 20 })
local merged = world.log({ rev = "feature" })
local touched = world.log({ path = "/source/foo.luau" })
typed/builtin//modules/world_vcs/world/merge
world.merge(sourceBranch: string, opts: MergeOpts?) -> MergeResult
Merge another branch into the session branch — git merge <source>. The merge runs locally in the world's SpacetimeDB clone
and is abortable with world.mergeAbort; nothing reaches ZeroMind
until the result is pushed. When the session branch's head is in
the source's history the head fast-forwards onto it (unless
opts.noFf); otherwise a two-parent merge commit lands
(opts.ffOnly refuses that). Local changes follow git's rule: one
on a path the merge does not rewrite stays in the working tree, and
one on a path it does rewrite refuses the merge with git's message
naming the paths. A scene's unsaved live edits are a local change
to its scene.json: a fast-forward refuses over a scene whose
scene.json it rewrites, naming the scene and how to save or
discard the edits, and a merge commit refuses over any scene holding
them. A staged entry refuses a merge commit. Conflicts →
git-style markers are projected into each conflicting text file,
the cleanly-merged remainder is applied as working-tree changes, and
world.vcsStatus().unmerged lists what needs attention: resolve
each path (edit out the markers, or take one side whole), then
world.add + world.commit — that commit records the merge
(second parent = the source head) and clears the unmerged set.
Parameters
sourceBranchstring— The branch to merge in.optsMergeOpts(optional) — OptionalMergeOpts—ffOnly,noFf.
Returns MergeResult — A MergeResult — status is fast_forward or clean (with commit), conflicts (with conflicts), or up_to_date.
local r = world.merge("feature")
local r = world.merge("feature", { noFf = true })
typed/builtin//modules/world_vcs/world/mergeAbort
world.mergeAbort()
Abort the in-progress merge — git merge --abort. Clears the
unmerged set and restores the working tree to the pre-merge state
(the target head's committed content; the branch head never moved
during a conflicted merge). Errors when no merge is in progress.
world.mergeAbort()
typed/builtin//modules/world_vcs/world/prConflicts
world.prConflicts(worldGuid: string?, number: number) -> any
Read a pull request's conflicts — what stands between it and a
merge. Returns the mergeability verdict, the merge base, both
heads, and one entry per conflicting path. A conflicting TEXT path
carries marked_text: the same <<<<<<< / ======= / >>>>>>>
rendering a merge leaves in the working tree, with the source and
target sides laid against their common ancestor. Resolve a path by
writing the settled bytes back to it and committing on the source
branch; the pull request re-analyses on the next read. A binary
path carries the two sides' hashes and no text — pick a side.
A mergeable pull request returns an empty conflict list.
Parameters
worldGuidstring(optional) — The world the pull request lives in. Defaults to the bound world.numbernumber— The pull request number.
Returns any — Decoded ZeroMind conflicts response.
local c = world.prConflicts(nil, 3)
for _, m in ipairs(world.prConflicts(originGuid, 3).markers) do print(m.path, m.marked_text) end
typed/builtin//modules/world_vcs/world/prList
world.prList(worldGuid: string?, number: number?) -> any
Parameters
worldGuidstring(optional)numbernumber(optional)
Returns any
typed/builtin//modules/world_vcs/world/prMerge
world.prMerge(worldGuid: string, number: number, strategy: string?) -> any
Merge a pull request — the agent-side merge button.
Parameters
worldGuidstring— The world the pull request lives in.numbernumber— The pull request number.strategystring(optional) —merge(default),squash, orfast_forward.
Returns any — Decoded ZeroMind merge response.
world.prMerge(originGuid, 3)
typed/builtin//modules/world_vcs/world/prOpen
world.prOpen(opts: PrOpenOpts) -> any
List a world's pull requests, or fetch one.
Open a pull request — gh pr create. Proposes the work on one
(world, branch) pair to another. Defaults make the common cases one
argument: from a fork, the target is the world it was forked from, so
world.prOpen({ title = "..." }) proposes your work upstream. In an
ordinary world the target is the same world, so you get a
branch → main pull request.
The PR lives in — and is numbered by — the world it targets, exactly
as a forge numbers pull requests on the upstream repository. That is
also where world.prList finds it.
Parameters
optsPrOpenOpts—title(required), plusdescription,sourceWorld,sourceBranch,targetWorld,targetBranchto address any leg explicitly.
Returns any — Decoded ZeroMind response. The decoded ZeroMind pull request.
local prs = world.prList()
world.prOpen({ title = "fix the door hinge" })
world.prOpen({ title = "port the fix", targetWorld = otherGuid })
typed/builtin//modules/world_vcs/world/prView
world.prView(worldGuid: string?, number: number) -> any
Read one pull request in full — gh pr view. Returns the record
plus a LIVE re-analysis against the current branch heads: mergeability
(clean / conflicts / fast_forwardable / up_to_date /
unrelated), conflict_count, and diff — every path the request
adds, modifies or deletes with its checksums. Read this before merging:
it is what tells you WHAT the request changes.
Parameters
worldGuidstring(optional) — The world the pull request lives in (its target world). Defaults to the session world.numbernumber— The pull request number.
Returns any — The decoded pull request view.
world.prView(nil, 1)
typed/builtin//modules/world_vcs/world/previewInstall
world.previewInstall(opts: InstallAssetOpts) -> PreviewResult
Preview what installing an asset WOULD write, without writing
anything. Fetches + decodes the closure and plans placement (the
same helpers world.installAsset uses), returning a flat node
list plus rollup totals. A truncated closure is reported (not
raised) so a caller can surface it and block import.
Parameters
optsInstallAssetOpts—{ guid, at?, ref? }— same shape as installAsset.
Returns PreviewResult — nodes + totals + a truncated flag.
world.previewInstall({ guid = "..." })
typed/builtin//modules/world_vcs/world/provideDerived
world.provideDerived(outputPath: string, opts: { stage: string? }?) -> number
Take the file at outputPath as the derived output: the file you
wrote is staged as it stands, and the reservation is settled done
with the fingerprint authored, which no recomputation replaces.
Parameters
outputPathstring— The output's VFS path; the file must exist.opts{ stage: string? }(optional) — Optional{ stage: string? }naming the staging area.
Returns number — How many reservations the file filled.
world.provideDerived("/zero/source/gold.material/preview.png")
typed/builtin//modules/world_vcs/world/publishBlockers
world.publishBlockers(opts: StageOpts?) -> { PublishBlockerClass }
List every reason world.push would refuse to publish this
world, as one entry per blocker class: script errors in user
content (script), assets that don't meet their type's content
requirements (content), computed asset references a component or
scene entrypoint reaches (staticPin), and the conditions the
publish gate refuses on: a component or scene entrypoint reaching a
dynamic resolve over require/tools.use (gameplayPath), a reference
pinned to an asset the world no longer holds (references), a
reference of the world's own file recorded before the world pinned
the library it names (pinPending, which the commit that takes the
file clears by deriving its table at the pin), and an error-severity
problem recorded on published content (problems).
Each class
carries the same title the refusal prints, one items entry per
offending subject (an asset identity, or a <path>:<line> site),
and the single remedy covering that class. This is the account
world.push composes its refusal from, so it names the same
blockers with no push attempted — and in full, where a refusal
bounds how many of a class it prints. zm status prints this list.
One more class blocks the next commit rather than the publish, and
carries blocks = "commit": staleReferences, the files the caller's
staged set holds or leaves out whose references do not resolve in it
(a file whose references moved with a staged change and is left out, or
a staged file stating a version the staged set leaves out). It is the
class world.commit refuses with; world.push never reads it, since
a push publishes what is committed. Empty list = the caller commits
and publishes.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming a staging area, so thecontentclass reports the assets that area answers for. Omitted, the read acts on the calling caller's own area.
Returns { PublishBlockerClass } — Array of PublishBlockerClass entries, empty when nothing blocks.
for _, c in ipairs(world.publishBlockers()) do
for _, i in ipairs(c.items) do print(c.kind, i.subject, i.detail) end
end
typed/builtin//modules/world_vcs/world/pull
world.pull(branch: string?, opts: PullOpts?) -> PullResult
git pull: fetch origin/<branch> (default the session branch)
and merge it into the session branch. When the session branch is
strictly behind, its head fast-forwards to the fetched commit; when both
sides moved, a two-parent merge commit lands with the same
conflict/marker/resolve flow as world.merge (resolve the unmerged
paths, then world.add + world.commit; abortable with
world.mergeAbort); when the fetched commit is already in the session
branch's history nothing changes. opts.ffOnly refuses anything but a
fast-forward; opts.noFf always makes a merge commit. Local changes
follow git's rule, as world.merge's do, unsaved scene edits
included. A branch origin does not
hold raises.
Parameters
branchstring(optional) — The origin branch to pull. Defaults to the session branch.optsPullOpts(optional) —{ ffOnly?, noFf? }.
Returns PullResult — A PullResult table: status is up_to_date, fast_forward (commit the new head), clean (commit the merge commit) or conflicts (conflicts).
local r = world.pull()
local r = world.pull("feature")
local r = world.pull("main", { ffOnly = true })
typed/builtin//modules/world_vcs/world/pullAsset
world.pullAsset(opts: PullAssetOpts?) -> PullAssetResult
Pull upstream changes into a previously-installed asset. Re-resolves
the root's transitive closure at opts.ref (default latest) and lands
only what differs from what the world holds (M.__updateRowPlan):
- A file the world never edited (its bytes equal the version it installed) lands whole when its version, its reference table or a stale conflict flag differs from the closure's: bytes and table are written to the pulled row in one step, text and binary alike, and the write reaches source whatever the engine's mode.
- A file the world edited is reconciled from
(row.origin_checksum, row.checksum, entry.checksum)(M.__reconcileDecision):noop(upstream unmoved) leaves it alone,convergedadvances its origin pointer, andmergeruns a three-wayvcs.merge3over (base, local, theirs): a clean result is written and the origin pointer advances, a conflicted result is written WITH markers and the row is flagged conflicted (base + theirs checksums recorded forworld.resolvePullConflict). A binary file the world edited that upstream changed too is flagged conflicted with the structured base/theirs checksums (no marker write). - A folder moves to the closure's version and takes its table.
- A row flagged conflicted that holds the bytes it installed carries no local content: it does not block the pull, and the pull clears it.
Closure drift: an entry the original install never landed is pulled fresh (added). A locally-pulled row nested under the root's own directory whose origin entry disappeared from the closure is removed when it holds the bytes it installed (pruned), or flagged conflicted when it carries edits.
Parameters
optsPullAssetOpts(optional) — SeePullAssetOpts.opts.guidoropts.pathis required;opts.refpins the re-resolve to a specific upstream commit (defaults to latest finalized).
Returns PullAssetResult summarising what merged, conflicted, was pruned, and was newly added, the version of the root it landed (root_version), and the reference sites in the pulled content that resolved to nothing where it was published (problems).
world.pullAsset({ guid = "..." })
typed/builtin//modules/world_vcs/world/push
world.push(commitId: string?, opts: PushOpts?) -> (string? | PushQueued, string)
Publish the current branch to ZeroMind. The no-argument form
is a git merge --squash push: EVERY unpushed commit on the
branch collapses into a SINGLE ZeroMind commit (latest content
per path, parented on the branch's current remote HEAD). Because
only the merged final state's bytes are uploaded, a superseded or
lost intermediate-commit blob can never break the push — this is
what makes a churn-heavy world publishable. The local commit
history is preserved as the editing journal; on success every
squashed commit shares the one remote commit id. The explicit
commitId form still pushes that single commit verbatim via
publish_commit (advanced / chain-replay use; its parent must
already be on the remote).
Parameters
commitIdstring(optional) — Optional. A single commit to push verbatim. Omit for the squash push of the whole unpushed stack (the normal path). A push only moves the remote branch forward, as git's does: a remote head no local commit holds raises git's! [rejected] <branch> -> <branch> (fetch first), and one the local head does not descend from raises(non-fast-forward);world.pull()first.opts.forceisgit push --force: a branch whose head is already published moves the remote branch there regardless (a Maintainer's call). A branch with commits not yet published is refused with force, nothing sent (pull first), and so is force withcommitId: force moves the session branch.optsPushOpts(optional) — OptionalPushOpts:stagenames a staging area, the way every other staging verb takes it, andforceforces the move. The pre-push content gate refuses over the assets that area holds or committed; an asset another area answers for publishes with its shortfall recorded on it and is named to the caller with the area that holds it. Omitted, the call acts on the calling caller's own area, so the gate refuses over what that caller staged and committed and names the rest. What an area holds is read from the staged rows a status call reports and from the world's history, where every commit records the area it came through, so the answer is the same from every engine and every session bound to the world. An area named after a caller holds what it committed while that caller's session is open; an area a caller named holds it until another area commits inside the asset.
Returns (string? | PushQueued, string) — Two values: the ZeroMind-allocated commit id, and the verdict. "published" with the new commit id when this call published; "already-published" when ZeroMind already carries what this call would have published — the state a push asks for, so it returns rather than raising. That verdict carries the commit's existing ZeroMind id when the publish names one (the single-commit form), and a nil id when it names none (the whole-stack form, which reports a chain). A squash whose merged final state carries dep.unresolved problems takes the slow path inside the same call: the engine re-resolves each pending literal against its live asset index and submits the resolutions to the publish procedure, which completes the push. A reference literal that still resolves to nothing is published with the asset holding it and stays a problem recorded on that asset, while a dep pin the squash severed raises instead; either way the engine names each one with its path, line, reference and reason. A publish ZeroMind refuses raises naming the condition and the command that clears it — a branch that moved under this push names world.pull().
local zmId = world.push()
local zmId, verdict = world.push()
local queue, verdict = world.push() -- verdict "queued" while commits are sealed
world.push("01HABC...")
typed/builtin//modules/world_vcs/world/remoteBranches
world.remoteBranches() -> { { branch: string, commit_id: string, origin_head: string } }
Origin's branches as the last fetch of each recorded them — git branch -r. One row per branch of this world that world.fetch has
fetched: branch is origin/<name>, commit_id the local commit
that names origin's head, origin_head that head's ZeroMind id.
Returns { { branch: string, commit_id: string, origin_head: string } } — Array of { branch, commit_id, origin_head }, sorted by branch.
for _, r in ipairs(world.remoteBranches()) do print(r.branch) end
typed/builtin//modules/world_vcs/world/renameBranch
world.renameBranch(oldName: string, newName: string, opts: { force: boolean? }?)
Rename a branch — git branch -m <old> <new>. The branch's
head and history, and your uncommitted changes on it, go to
newName; when oldName is the branch this session is on, the
session moves with it. Another caller's uncommitted changes on
oldName keep that branch in place beside the renamed one.
Parameters
oldNamestring— The branch to rename.newNamestring— Its new name.opts{ force: boolean? }(optional)
world.renameBranch("feature", "feature-2")
typed/builtin//modules/world_vcs/world/reset
world.reset(target: string, opts: ResetOpts?) -> ResetResult
Move the session branch's head to target — git reset [--soft|--mixed|--hard] <commit>. target is any revision
world.revParse reads (a commit id, HEAD~1, a branch, ...).
opts.mode: soft keeps the staged entries and the working tree
and stages every path whose committed version the move changed;
mixed (the default) empties your staging area and keeps the
working tree, so a path the move changed reads as a local change;
hard empties your staging area and brings the working tree to
the new head: every path the move changed, every path your account
changed, and every file of your account's that no commit holds
(an uncommitted install and a scene's autosave overlay included),
each to trash first. A write this session owes the world and the
world is not taking is given up (world.dropOwedWrites). The local
changes of another account stay and are listed in kept. A target equal to HEAD moves nothing, so
world.reset("HEAD") unstages everything and world.reset("HEAD", { mode = "hard" }) discards your local changes. Non-destructive
for history: every commit the head leaves behind becomes a trash
entry that re-attaches it (in chain order).
Parameters
targetstring— The revision to move the head to.optsResetOpts(optional) — OptionalResetOpts—mode.
Returns ResetResult — A ResetResult — the new head, its subject line, and the commits left behind.
world.reset("HEAD~1")
world.reset("01HABC...", { mode = "hard" })
typed/builtin//modules/world_vcs/world/resolvePullConflict
world.resolvePullConflict(path: string, choice: string)
Resolve a conflicted row. A TEXT conflict is one where the file
currently contains conflict markers (world.pullAsset writes
markers only for a text three-way merge that didn't resolve
cleanly); a BINARY/composite conflict has no markers — the local
bytes were left untouched.
choice = "theirs" fetches clean upstream text by content hash
(conflict_theirs_blob_sha256 — path/rename-independent, since
blobs are content-addressed) and overwrites the local file with it
before clearing the flag. It errors, refusing to guess, when the
row records no theirs blob (a binary/composite conflict — a text
blob read can't address theirs for those; keep "ours" or
re-install the asset instead).
choice = "ours" keeps the local side. When the file carries
conflict markers, the local side is reconstructed from them (the
marker writers put ours first, so dropping each block's base and
theirs sections restores your bytes exactly) and written back;
a marker-free file is kept as-is. Either way the flag clears.
Either way, staging the resolved file (world.add) is the normal
git-add path once this returns — this call only clears the
manifest-level flag and (for "theirs") the file content.
Parameters
pathstring— The conflicted row's local VFS path.choicestring—"ours"or"theirs".
world.resolvePullConflict("/source/combat/rules.luau", "theirs")
typed/builtin//modules/world_vcs/world/restore
world.restore(handle: any?)
Restore one trash entry by row_id. Errors verbatim on handler-not-yet-implemented / world-mismatch.
Parameters
handleany(optional) — The trash row id. May be a number or a numeric string.
world.restore(42)
typed/builtin//modules/world_vcs/world/retryDerived
world.retryDerived(outputPath: string, opts: { stage: string? }?) -> number
Compute the derived output at outputPath again, on the staging
area or on a sealed commit that reserves it. A reservation that
failed, was dropped or is done is reserved anew and computed from
the inputs as they stand; a sealed commit waiting on it lands when
it is done.
Parameters
outputPathstring— The output's VFS path, aszm statusnames it.opts{ stage: string? }(optional) — Optional{ stage: string? }naming the staging area.
Returns number — How many reservations were computed again.
world.retryDerived("/zero/source/gold.material/preview.png")
typed/builtin//modules/world_vcs/world/revParse
world.revParse(rev: string) -> string
The commit a revision names — git rev-parse <rev>. A revision
is HEAD, a branch, origin/<branch> (as the last fetch recorded
it), a commit id or a unique prefix of one, each optionally followed
by ~N (N first parents back), ^ (the first parent) or ^2 (a
merge commit's second parent). Raises git's message for a revision
that names nothing, HEAD on a branch with no commit included.
Parameters
revstring— The revision.
Returns string — The commit id.
local id = world.revParse("HEAD~1")
typed/builtin//modules/world_vcs/world/reviewUpdate
world.reviewUpdate(unit: string, opts: ReviewUpdateOpts?) -> UpdateReview
Review what updating one unit changes, writing nothing: the update
itself, never a preview of the asset. Answers the versions the unit
moves from (what the world holds) and to (what its origin holds at
opts.ref, default the ref world.checkUpdates compares it with), then
commits: the commits of the origin world in between, newest first, each
{ id, message, author, date } (more is true past 200), then the files.
modified names up front each file the world holds uncommitted changes
to (a dirty or staged row) that the update touches, { path, outcome }
with outcome merges or conflicts.
- An installed asset (
unitis its root path, asworld.checkUpdatesreports it; a path inside the install stands for its root): the commits that change the install's files, back to the one that moved the root from the version the world holds;fileslists each file the update adds, changes or removes,{ path, change, diff, hunks, local_edit }, wherelocal_editisnone, ormerges/conflictsfor a file the world edited since it was installed. - A library (
@builtin,@combat, with or without the@): the commits from the pinned commit to the new one andsummarycounts of the library files that change and of this world's files whose references move with the pin (they readreferences movedin status and are committed with the marker).opts.files = trueaddsfiles(each changed library file with its diff) andmoving(those paths).
opts.commits = false reads no commits, and opts.files = false for an
install leaves out files and its diffs: { commits = false, files = false } is the check before an update, answering modified and the
counts alone. When a version of the install's root cannot be read the
commit list stops there, more is true and commits_error says why.
Parameters
unitstring— A library name or an install root path.optsReviewUpdateOpts(optional) —{ ref?, files?, commits? }.
Returns UpdateReview: { kind, unit, from, to, commits, more, commits_error?, files?, moving?, modified, summary }; modified is one { path, outcome = "merges" | "conflicts" } per file holding uncommitted changes of its own that the update touches; summary counts added, changed, removed, modified, and merges / conflicts for an install or moving for a library.
local r = world.reviewUpdate("@builtin")
local r = world.reviewUpdate("/zero/source/Fire.module")
local r = world.reviewUpdate("/zero/source/Fire.module", { commits = false, files = false })
typed/builtin//modules/world_vcs/world/show
world.show(...: string) -> any
Mirror git show's CLI arg shape. Default returns commit
metadata + full diff vs parent. world.show("commit:/path")
returns just the bytes. Flags: --stat, --name-only.
Parameters
...string— Variadic string args: commit id, optional path, optional flags.
Returns any — Either a ShowResult table or a string (for commit:/path).
local r = world.show("abc123")
local r = world.show("--stat", "abc123")
local bytes = world.show("abc123:/foo.luau")
typed/builtin//modules/world_vcs/world/stageVersion
world.stageVersion(rev: string, path: string, opts: StageOpts?) -> boolean
Make the staging area's entry for path the version revision
rev holds — git reset <rev> -- <path> (zm restore --staged --source <rev>). When that version is the one the branch head
holds (both lacking the path included), the path is unstaged, as
the index then matches HEAD there; otherwise a revision that lacks
the path stages its deletion. The working tree is untouched.
Parameters
revstring— Any revisionworld.revParsereads.pathstring— The path to stage.optsStageOpts(optional) — OptionalStageOpts—stage, the staging area.
Returns boolean — true when the staging area holds an entry for the path afterwards, false when the path ended unstaged.
world.stageVersion("HEAD~1", "/source/foo.luau")
typed/builtin//modules/world_vcs/world/stash
world.stash(label: string?) -> StashPushResult
Stash the caller's uncommitted changes — git stash push. The
caller's dirty + staged state on the session branch is saved into
a stash row (label names it), and each stashed path goes back to
its committed version, unstaged. Binary and large content is saved
by its stored blob, so applying the stash brings it back
byte-identical. A path no commit holds keeps its working content in
the tree, as git leaves an untracked file, and is listed under
kept. A path that cannot be taken back raises, naming it; the
stash is saved either way.
Parameters
labelstring(optional) — Optional. Free-form text label for the stash.
Returns StashPushResult — A StashPushResult.
local r = world.stash("wip widget refactor")
typed/builtin//modules/world_vcs/world/stashApply
world.stashApply(ref: any?) -> StashApplyResult
Apply a stash and keep it — git stash apply [<stash>].
Author-only. ref is a row id, stash@{n}, or nil for the
caller's newest stash. Text content is written back and staged
entries are staged again; content recorded by checksum alone is
listed under skipped. Refuses, writing nothing, when a path the
stash restores holds a different local change.
Parameters
refany(optional) — Optional stash reference.
Returns StashApplyResult — A StashApplyResult.
local r = world.stashApply()
typed/builtin//modules/world_vcs/world/stashDrop
world.stashDrop(ref: any?) -> number
Drop a stash — git stash drop [<stash>]. Author-only. ref
is a row id, stash@{n}, or nil for the caller's newest stash.
The dropped stash goes to trash, where world.restore brings it
back under a new row id.
Parameters
refany(optional) — Optional stash reference.
Returns number — The row id of the dropped stash.
world.stashDrop()
typed/builtin//modules/world_vcs/world/stashPop
world.stashPop(ref: any?) -> StashApplyResult
Apply a stash and drop it — git stash pop [<stash>].
Author-only. ref is a row id, stash@{n}, or nil for the
caller's newest stash. Applies as world.stashApply does; a
refused apply keeps the stash. The dropped stash goes to trash.
Parameters
refany(optional) — Optional stash reference.
Returns StashApplyResult — A StashApplyResult.
local r = world.stashPop()
typed/builtin//modules/world_vcs/world/stashes
world.stashes() -> { StashRow }
List every stash row in the world. Anyone with read access
sees every stash; the per-row author_hex makes it clear
which entries the caller can pop / drop themselves.
Returns { StashRow } — Array of StashRow tables.
local rows = world.stashes()
typed/builtin//modules/world_vcs/world/trash
world.trash() -> { TrashRow }
List trash entries for the world. Anyone with read access
to the world can list trash — recovery is a shared safety net,
not a privacy boundary. The handle (row_id) feeds back into
world.restore.
Returns { TrashRow } — Array of TrashRow tables.
local rows = world.trash()
typed/builtin//modules/world_vcs/world/trashEmpty
world.trashEmpty() -> number
Permanently delete the trash entries your own operations put
there — zm trash empty — in one transaction. Entries other
callers' operations trashed stay restorable. An emptied entry cannot
be restored.
Returns number — The number of entries deleted.
local n = world.trashEmpty()
typed/builtin//modules/world_vcs/world/uninstallLibrary
world.uninstallLibrary(name: string) -> string
Delete the library marker file. name accepts "@combat"
or "combat" (the leading @ is the convention carried by
the on-disk path).
Parameters
namestring— The library name, with or without the leading@.
Returns string — The marker path that was removed.
world.uninstallLibrary("@combat")
typed/builtin//modules/world_vcs/world/unseal
world.unseal(commitId: string?) -> string
Return a sealed commit to the staging area it was sealed from, with
its message kept, so its paths can be changed and committed again. A
commit world.commit sealed lands by itself once its required derived
outputs are computed; this is the way out of one that cannot land.
Parameters
commitIdstring(optional) — The sealed commit's id. Omitted, the oldest sealed commit holding a failure, else the newest.
Returns string — The id of the commit returned.
world.unseal()
typed/builtin//modules/world_vcs/world/unstage
world.unstage(path: string, opts: StageOpts?) -> UnstageReport
Remove a path from the staging area. Live manifest dirty state is untouched. A path is staged in one area at a time: a path the calling area does not hold that another staging area does is taken out of that area, whichever account opened it, and named in the report; a take from an area of another account warns, naming that account, the area and its session. An installed asset is unstaged by its root path, which takes every row of the install out of the area; another row of an install raises, naming the install and its root.
Parameters
pathstring— The path to unstage.optsStageOpts(optional) — Optional{ stage: string? }naming the staging area the path was staged into. Omitted, the call acts on the calling caller's own area.
Returns UnstageReport — { released }: each path taken out of another staging area (stage, account, same_account, session, live). Empty when the calling area held the path.
world.unstage("/source/foo.luau")
world.unstage("/source/foo.luau", { stage = "fauna" })
typed/builtin//modules/world_vcs/world/update
world.update(units: (string | { string })?, opts: UpdateOpts?) -> UpdateResult
Apply updates. With no units, every unit world.checkUpdates
lists; a name or a list of names (library names and install root
paths, mixed) updates those. A library moves its pin
(world.updateLibrary), an installed asset pulls its new closure
(world.pullAsset), one unit at a time: a unit that fails is reported in
its entry and the rest still run, and an install whose local edits
conflict lists the conflicted paths. Nothing is staged, committed or
pushed: the updated files wait in world.status(), to_commit lists
every path the updates changed or moved, and warnings names each file
holding uncommitted changes that an update merged into or conflicted.
Parameters
units(string | { string })(optional) — Nil, a unit name, or a list of names.optsUpdateOpts(optional) —{ ref? }: a branch name or commit id every unit is updated to.
Returns UpdateResult — { results, to_commit, skipped, warnings, note }: results holds one { unit, kind, from, to, ok, error?, merged?, conflicted?, added?, pruned?, moving? } per unit, skipped the units the check could not read, warnings one { unit, path, outcome = "merged" | "conflicted" } per modified file an update touched, and note says nothing was committed.
local r = world.update()
local r = world.update("@combat")
local r = world.update({ "@builtin", "/zero/source/Fire.module" })
typed/builtin//modules/world_vcs/world/updateLibrary
world.updateLibrary(name: string, ref: string?) -> UpdateLibraryResult
Move an installed library to the newest commit of a ref. Resolves
ref (default main) in the world the library's marker at
/source/libs/@<name> names; when that is the commit the marker
already holds nothing is written, even when requested_ref differs;
an imported library the engine does not hold at that commit is
mounted and waited for, and a mount that fails raises naming why.
Otherwise the marker is rewritten with the new commit (other marker
fields are kept), the call waits until the engine holds the library at
that commit (@builtin: its pin table is read; another library: its
content is mounted), and the reference tables of the world's own
sources that name the library are derived again. Each source whose table
moved is listed by world.vcsStatus with the reason references moved
and is committed with the marker: a commit holding the marker and
leaving one of them out is refused, naming each. Discarding the marker
puts the pin and its tables back. Works for @builtin
and for every library world.installLibrary wrote. A failed wait puts
the old marker back, waits for the library at the old commit and
raises, saying where it ended. One move per library runs at a time, and
none runs while play is running or in a runtime-profile session.
Parameters
namestring— The library name, with or without the leading@.refstring(optional) — A branch name or commit id of the library's world; defaults tomain.
Returns UpdateLibraryResult — { name, from, to, changed, rederived }: the commits the pin moved between, whether it moved, and the source paths whose reference table was derived again, each one to commit with the marker.
local r = world.updateLibrary("@builtin")
typed/builtin//modules/world_vcs/world/vcsStatus
world.vcsStatus(opts: StageOpts?) -> StatusResult
Return the working-tree VCS status: dirty paths, staged
entries, ignored paths, untracked paths, and any unmerged ones.
A file whose bytes are the ones the branch head holds and whose
reference table is not (a library pin moved, or an asset it references
was renamed, moved or got new bytes) is listed like any change, with
reason = "references moved": in staged when the area holds it, in
dirty otherwise. world.add / world.add_all stage it and it is
committed with the change that moved it. A table that differs only in
the install record the engine writes (an installed asset marked as the
root of its install) is listed the same way with
reason = "install recorded".
An untracked path is one with no committed version behind it, and
it appears in dirty as well — git add . picks up new files too.
Named vcsStatus (not status) because world.status() is the
runtime-snapshot accessor owned by world_status.module; the
source-control surface keeps its own VCS-specific name so the
two never shadow each other.
Each dirty[i].dirtied_by is the identity the row was written
under, which every session of one account shares;
dirtied_by_caller is the stable caller key of the call that wrote
it, dirtied_by_name the name that caller goes by on the world's
roster now,
and writer says what the row is to the calling caller: "you",
"other" (a caller whose session is open), "ended" (a caller
whose session has ended) or "unattributed". dirty_since_micros is the
microsecond timestamp of the first write of the current dirty run.
dirtied_by_account is the account the row's writer acts for and
same_account whether that is the calling caller's account.
local_identity is this connection's identity and
local_caller the calling caller's own stable key, local_name
the name it goes by now, local_account the account it acts for.
claimed_by_other_stages names the paths some other staging area
holds and who holds each: the area, its owner_account, whether
that is the caller's (same_account), the session holding it and
whether that session is open now (live). Staging is advisory: a
staging call that stages one of them takes it out of that area.
writer_accounts_refusal is set when the world's backend module
predates the record of writers' accounts. stage names the area
staged is the set of, so a caller reads which name reaches its own
staged rows.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area whose staged set to report. Omitted, the read reports the calling caller's own area. The dirty, ignored and untracked sets are the world's working tree and read the same whichever area is named.
Returns StatusResult — A StatusResult table.
local s = world.vcsStatus()
local s = world.vcsStatus({ stage = "fauna" })
typed/builtin//modules/yaml/M/decode
M.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
textstring— The YAML document text.
Returns any — The decoded value (table / scalar / nil for an empty document).
local doc = Yaml.decode(vfs.read(path))
typed/builtin//modules/yaml/M/decodeWithComments
M.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
textstring— The YAML document text.
Returns (any, boolean) — The decoded value, and whether the text carries a comment.
local doc, commented = Yaml.decodeWithComments(vfs.read(path))
typed/builtin//modules/yaml/M/encode
M.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.
Returns string — The YAML text.
vfs.write(path, Yaml.encode({ contract = "weapon", values = v }))
typed/builtin//modules/yaml/M/flowEntries
M.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
textstring— The flow collection's text.
Returns ({ FlowEntry }, string) — An array of { key?, value }, in written order, and the text that follows the collection (a trailing comment with the spaces before it), as written; empty when nothing follows it.
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")
typed/builtin//modules/zinput/M/_resetLiveArmed
M._resetLiveArmed()
Test-only: reset the liveness auto-arm guard so a suite can exercise the arming decision from a cold state.
Zin._resetLiveArmed()
typed/builtin//modules/zinput/M/_resetTickGuard
M._resetTickGuard()
Test-only: reset the idempotency guard so the next tick()
call runs unconditionally. Suite isolation; not part of the
public contract for production code.
Zin._resetTickGuard()
typed/builtin//modules/zinput/M/disconnect
M.disconnect(handle: number) -> boolean
Disconnect any Zin handle (from Zin.input.on* or
Zin.actions.bind). Returns true if a handler/subscription was
removed; false on miss.
Parameters
handlenumber— The numeric handle.
Returns boolean — Whether a handler was actually removed.
Zin.disconnect(h)
typed/builtin//modules/zinput/M/lastTickAt
M.lastTickAt() -> number
Test/debug: wall-clock timestamp of the most recent tick,
or -1 if Zin.tick has never been called.
Returns number — The wall-clock seconds value of the most recent tick, or -1.
local t = Zin.lastTickAt()
typed/builtin//modules/zinput/M/lastTickFrameId
M.lastTickFrameId() -> number
Test/debug: engine frame id of the most recent tick, or -1 if
Zin.tick has never been called (or __zero_input.frameId() is
unavailable). Used by Zin.autoTick's worker loop to dedup
against explicit ticks.
Returns number — The engine frame id of the most recent tick, or -1.
local f = Zin.lastTickFrameId()
typed/builtin//modules/zinput/M/tick
M.tick(dt: number?)
Advance every stateful zinput subsystem by dt seconds.
dt is the frame delta. Pass the value your host already computes
for animation. If omitted, computed from elapsed wall time since the
last tick() call (first call defaults to 1/60). Order each tick:
- clear chord per-tick fire flags, Gestures per-tick deltas, Surface per-frame device-class flags, Virtual button edges + drag deltas
- route this tick's raw touches into Zin.virtual (Zin.touchControls._advance() — auto-mounts, writes stick/button/drag), then run the key-emulation floor over that fresh state (Zin.emulation._advance()). Both PRODUCE the Zin.virtual state the binding/axis/action evaluators below consume THIS tick, so they run before any of them.
- advance Axes smoothing, Chords window aging, Rebind timeouts, Gestures time-based recognition (long-press) + pinch/pan deltas
- drain
__zero_input.events()once (first tick of the frame only) - feed each event into State (held-time), Chords (state machines), Input (lastInputType tracking), Gestures (contact tracks + discrete recognition), Surface (device-class flags)
- resolve Surface's device class for this frame, then dispatch event subscribers (Zin.events.on / Zin.input.on* callbacks — priority + sink/pass + gpe)
- dispatch action handlers (Zin.actions.bind / onPressed / onReleased / onChanged / onHeld)
- dispatch chord onFire callbacks and Gestures discrete-gesture callbacks for anything that fired this tick
Parameters
dtnumber(optional) — Optional frame delta in seconds (defaults to elapsed wall time).
Zin.tick(dt)
typed/builtin//modules/zinput/actions/M/_clearHandlers
M._clearHandlers()
Test-only: clear every handler (does NOT touch action definitions).
Zin.actions._clearHandlers()
typed/builtin//modules/zinput/actions/M/_dispatchHandlers
M._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
firstTickThisFrameboolean(optional) — Whether this is the frame's first dispatch pass.
Zin.actions._dispatchHandlers()
typed/builtin//modules/zinput/actions/M/_owns
M._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
handlenumber— The numeric handle to probe.
Returns boolean — Whether this module owns the handle.
if Zin.actions._owns(h) then Zin.actions.disconnect(h) end
typed/builtin//modules/zinput/actions/M/_resetUnknownWarnings
M._resetUnknownWarnings()
Test-only: forget which names have already been reported as unregistered, so a fresh suite sees the warning again.
Zin.actions._resetUnknownWarnings()
typed/builtin//modules/zinput/actions/M/_setAllocator
M._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)
typed/builtin//modules/zinput/actions/M/_setEnsureBindingsFn
M._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)
typed/builtin//modules/zinput/actions/M/_setEnsureLiveFn
M._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)
typed/builtin//modules/zinput/actions/M/_settled
M._settled() -> boolean
Internal: whether the most recent dispatch pass delivered nothing and saw nothing held. The tick's quiescence gate reads it.
Returns boolean — true when no bound action is doing anything.
if Zin.actions._settled() then ... end
typed/builtin//modules/zinput/actions/M/active
M.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
namestring— The action name.
Returns boolean — Whether the action is currently active.
if Zin.actions.active("jump") then ... end
typed/builtin//modules/zinput/actions/M/bind
M.bind(name: string, fn: any?, 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
namestring— The action name.fnany(optional) — Handlerfn(name, state, io, gpe) -> "sink" | any.optsBindOpts(optional) — Optional{ priority, fire, once }.
Returns ActionHandle? — The numeric handle, or nil when the action isn't defined.
local h = Zin.actions.bind("jump", function() print("jump") end)
typed/builtin//modules/zinput/actions/M/clear
M.clear()
Wipe every defined action AND every registered handler. Primarily for tests.
Zin.actions.clear()
typed/builtin//modules/zinput/actions/M/define
M.define(spec: ActionSpec)
Define one or more actions. Each entry replaces any existing action under the same name; other actions are preserved.
Parameters
specActionSpec— Map ofname -> binding | { binding... } | { context, binding(s) }.
Zin.actions.define({ jump = Zin.bindings.key("Space") })
typed/builtin//modules/zinput/actions/M/disconnect
M.disconnect(handle: ActionHandle) -> boolean
Tear down a handler returned by bind. Idempotent.
Parameters
handleActionHandle— The handle frombind(oronPressed/onReleased/etc).
Returns boolean — Whether the handler was actually removed.
Zin.actions.disconnect(h)
typed/builtin//modules/zinput/actions/M/get
M.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
namestring— The action name.
Returns ActionEntry? — The internal { context, bindings } record, or nil.
local entry = Zin.actions.get("jump")
typed/builtin//modules/zinput/actions/M/handlerCount
M.handlerCount(name: string) -> number
Number of registered handlers for an action (0 if none / unknown).
Parameters
namestring— The action name.
Returns number — The handler count for this action.
assert(Zin.actions.handlerCount("jump") == 1)
typed/builtin//modules/zinput/actions/M/has
M.has(name: string) -> boolean
True if an action with this name is defined.
Parameters
namestring— The action name to test.
Returns boolean — Whether the action exists in the registry.
if Zin.actions.has("jump") then ... end
typed/builtin//modules/zinput/actions/M/held
M.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
namestring— The action name.
Returns boolean — Whether the action is held this frame.
if Zin.actions.held("attack") then print("swing") end
typed/builtin//modules/zinput/actions/M/heldTime
M.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
namestring— The action name.
Returns number? — The longest held-time (seconds) across the action's boolean bindings, or nil.
local t = Zin.actions.heldTime("interact")
typed/builtin//modules/zinput/actions/M/names
M.names() -> { string }
All defined action names, in arbitrary order.
Returns { string } — A fresh array of action names.
for _, n in ipairs(Zin.actions.names()) do print(n) end
typed/builtin//modules/zinput/actions/M/onChanged
M.onChanged(name: string, fn: any?, 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
namestring— The action name.fnany(optional) — Handlerfn(name, state, io, gpe).optsBindOpts(optional) — Optional{ priority, once }.
Returns ActionHandle? — The numeric handle, or nil when the action isn't defined.
Zin.actions.onChanged("move", function(_, _, io) print(io.value) end)
typed/builtin//modules/zinput/actions/M/onHeld
M.onHeld(name: string, fn: any?, 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
namestring— The action name.fnany(optional) — Handlerfn(name, state, io, gpe).optsBindOpts(optional) — Optional{ priority, once }.
Returns ActionHandle? — The numeric handle, or nil when the action isn't defined.
Zin.actions.onHeld("interact", function(_, _, io) print("charge", io.value) end)
typed/builtin//modules/zinput/actions/M/onPressed
M.onPressed(name: string, fn: any?, opts: BindOpts?) -> ActionHandle?
Fire on rising edge (state = "Begin"). Sugar for bind with
fire = {"Begin"}.
Parameters
namestring— The action name.fnany(optional) — Handlerfn(name, state, io, gpe).optsBindOpts(optional) — Optional{ priority, once }.
Returns ActionHandle? — The numeric handle, or nil when the action isn't defined.
Zin.actions.onPressed("jump", function() ... end)
typed/builtin//modules/zinput/actions/M/onReleased
M.onReleased(name: string, fn: any?, opts: BindOpts?) -> ActionHandle?
Fire on falling edge (state = "End"). Sugar for bind with
fire = {"End"}.
Parameters
namestring— The action name.fnany(optional) — Handlerfn(name, state, io, gpe).optsBindOpts(optional) — Optional{ priority, once }.
Returns ActionHandle? — The numeric handle, or nil when the action isn't defined.
Zin.actions.onReleased("attack", function() ... end)
typed/builtin//modules/zinput/actions/M/pressed
M.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
namestring— The action name.
Returns boolean — Whether the action's press edge fired this frame.
if Zin.actions.pressed("jump") then ... end
typed/builtin//modules/zinput/actions/M/reason
M.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
namestring— The action name.
Returns string — One of unknownControl / contextInactive / atRest / delivering.
if Zin.actions.reason("jump") == "unknownControl" then ... end
typed/builtin//modules/zinput/actions/M/released
M.released(name: string) -> boolean
True if any boolean binding on the action was just released this frame. Suppressed by context gating.
Parameters
namestring— The action name.
Returns boolean — Whether the action's release edge fired this frame.
if Zin.actions.released("attack") then ... end
typed/builtin//modules/zinput/actions/M/remove
M.remove(name: string)
Remove an action by name. Also tears down every handler that was bound to it. No-op when the action isn't defined.
Parameters
namestring— The action name to remove.
Zin.actions.remove("jump")
typed/builtin//modules/zinput/actions/M/repeated
M.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
namestring— The action name.opts{ delay: number?, period: number? }(optional) — Optional{ delay, period }override of the global repeat defaults.
Returns boolean — Whether a synthetic repeat should fire this frame.
if Zin.actions.repeated("scrollLeft") then ... end
typed/builtin//modules/zinput/actions/M/value
M.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
namestring— The action name.
Returns any — The action's current value (shape depends on first binding kind).
local mv = Zin.actions.value("move") -- { x, y }
typed/builtin//modules/zinput/arming/M/_resetPress
M._resetPress()
Test-only: forget the claim decided for the press in progress, so the next read searches again. Suite isolation for a simulated press that never had a release.
Zin.arming._resetPress()
typed/builtin//modules/zinput/arming/M/gate
M.gate(opts: Steering?) -> () -> boolean
Build the predicate a look-style control uses as its gate: the
test for "the player is steering", assembled from the gestures this
scheme means by it.
A locked pointer always arms — a cursor the scheme took is one the
player gave to the camera. Everything else is named in opts.
Parameters
optsSteering(optional) — Which gestures arm this control. Every field is optional; an empty table reads pointer lock and the right button alone.
Returns () -> boolean — A predicate to assign to a control's gate.
gate = Zin.arming.gate({ dragZone = "right", stick = "right" })
typed/builtin//modules/zinput/arming/M/rightButtonArms
M.rightButtonArms(mode: string?) -> boolean
Whether the right mouse button is arming a camera gesture right
now, on the terms mode names ("free" / "held" / "off").
Under "free" the claim search runs on the frame the button goes
down and its answer holds for that whole press.
Parameters
modestring(optional) — How the button is read;"free"when omitted.
Returns boolean — True while the button is arming.
if Zin.arming.rightButtonArms("held") then ... end
typed/builtin//modules/zinput/arming/M/rightButtonClaimants
M.rightButtonClaimants() -> { string }
Every control that declares the right mouse button and could
answer on it this frame, other than the engine's own arming gesture —
the controls a "free" gate stands down for.
Both layers a world can bind through are searched: the controls of
every live .inputMap, and the Zin.actions / Zin.axes
compatibility registry. A control whose map is standing down for a
suppressor, or whose context is not the one on top, is one the player
cannot reach, and the button is free of it for as long as that holds.
Returns { string } — Control names, scheme controls first, each prefixed by the layer it was found in (map: / action: / axis:).
if #Zin.arming.rightButtonClaimants() > 0 then ... end
typed/builtin//modules/zinput/autoTick/M/_setLastTickFrameIdFn
M._setLastTickFrameIdFn(fn: () -> number)
Internal: register the accessor that returns the engine frame
id of the most recent M.tick. Wired by init.luau so the
worker can dedup itself against explicit ticks within the same
engine frame. Optional — when nil, the worker ticks
unconditionally.
Parameters
fn() -> number— The accessor returning the engine frame id of the last tick.
AutoTick._setLastTickFrameIdFn(M.lastTickFrameId)
typed/builtin//modules/zinput/autoTick/M/_setTickFn
M._setTickFn(fn: (number?) -> ())
Internal: register the function that the auto-tick loop should
call each frame. Wired by init.luau at module load.
Parameters
fn(number?) -> ()— The tick function (typicallyZin.tick).
AutoTick._setTickFn(M.tick)
typed/builtin//modules/zinput/autoTick/M/isRunning
M.isRunning() -> boolean
True if the auto-tick loop is currently running.
Returns boolean — Whether the worker coroutine is live.
if Zin.autoTick.isRunning() then ... end
typed/builtin//modules/zinput/autoTick/M/start
M.start() -> boolean
Start the auto-tick loop. No-op if already running.
Returns boolean — true when a new loop was started, false when it was already up.
Zin.autoTick.start()
typed/builtin//modules/zinput/autoTick/M/stop
M.stop()
Stop the auto-tick loop on its next yield. No-op if not running.
Zin.autoTick.stop()
typed/builtin//modules/zinput/axes/M/_resetGateWarnings
M._resetGateWarnings()
Test-only: reset the once-per-axis gate-error warning state so a fresh suite can verify warning behavior again.
Zin.axes._resetGateWarnings()
typed/builtin//modules/zinput/axes/M/_resetUnknownWarnings
M._resetUnknownWarnings()
Test-only: forget which names have already been reported as unregistered, so a fresh suite sees the warning again.
Zin.axes._resetUnknownWarnings()
typed/builtin//modules/zinput/axes/M/_setEnsureBindingsFn
M._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)
typed/builtin//modules/zinput/axes/M/_setEnsureLiveFn
M._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)
typed/builtin//modules/zinput/axes/M/_settled
M._settled() -> boolean
Internal: whether every axis sits at zero, current and target alike. The tick's quiescence gate reads it.
Returns boolean — true when no axis is moving or being driven.
if Zin.axes._settled() then ... end
typed/builtin//modules/zinput/axes/M/advance
M.advance(dt: number)
Parameters
dtnumber
typed/builtin//modules/zinput/axes/M/clear
M.clear()
Wipe every axis and its state. Primarily for tests.
Zin.axes.clear()
typed/builtin//modules/zinput/axes/M/define
M.define(spec: AxisSpec)
Define one or more axes. Each entry replaces any existing axis of the same name; other axes are preserved.
Parameters
specAxisSpec— Map ofname -> { binding, deadzone?, smoothing?, curve?, invert?, context?, gate? }.
Zin.axes.define({ aim_x = { binding = ..., deadzone = 0.05 } })
typed/builtin//modules/zinput/axes/M/get
M.get(name: string) -> AxisDef?
Internal introspection: definition record (or nil).
Parameters
namestring— The axis name.
Returns AxisDef? — The internal AxisDef record, or nil if not defined.
local def = Zin.axes.get("aim_x")
typed/builtin//modules/zinput/axes/M/has
M.has(name: string) -> boolean
True if an axis with this name is defined.
Parameters
namestring— The axis name.
Returns boolean — Whether the axis exists in the registry.
if Zin.axes.has("aim_x") then ... end
typed/builtin//modules/zinput/axes/M/names
M.names() -> { string }
All defined axis names, in arbitrary order.
Returns { string } — A fresh array of axis names.
for _, n in ipairs(Zin.axes.names()) do print(n) end
typed/builtin//modules/zinput/axes/M/raw
M.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
namestring— The axis name.
Returns any — The raw value (scalar or vector depending on axis kind).
print(Zin.axes.raw("aim_x"))
typed/builtin//modules/zinput/axes/M/reason
M.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
namestring— The axis name.
Returns string — One of unknownControl / contextInactive / gateRefused / atRest / delivering.
if Zin.axes.reason("look") == "gateRefused" then ... end
typed/builtin//modules/zinput/axes/M/remove
M.remove(name: string)
Remove an axis. No-op if not defined.
Parameters
namestring— The axis name.
Zin.axes.remove("aim_x")
typed/builtin//modules/zinput/axes/M/value
M.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
namestring— The axis name.
Returns any — The smoothed, shaped value (scalar or {x, y}).
local mv = Zin.axes.value("move") -- { x, y }
typed/builtin//modules/zinput/back/M/_observeEvent
M._observeEvent(ev: any?)
Internal: per-event observer called by the Zin.tick coordinator. A
back event the engine routed to a claim runs the handler of the claim
that takes it; with no claim held any more, the press goes to the platform's
own default.
Parameters
evany(optional) — The raw input event record.
Back._observeEvent(ev)
typed/builtin//modules/zinput/back/M/_reset
M._reset()
Test-only: release every claim, so each case of a suite begins with none held.
Zin.back._reset()
typed/builtin//modules/zinput/back/M/_setEnsureLiveFn
M._setEnsureLiveFn(fn: () -> ())
Internal: register the function that arms the input tick, which
delivers a press to the claim that takes it. Wired by init.luau.
Parameters
fn() -> ()— Arms the tick.
Back._setEnsureLiveFn(ensureInputLive)
typed/builtin//modules/zinput/back/M/claim
M.claim(handler: () -> (), options: { priority: number? }?) -> Claim
Take Back: while the claim is held, a Back press that no dismissible
overlay takes runs handler. The claim of highest priority takes a
press, and among claims of equal priority the most recent.
Hold a claim while Back means something to the world (a menu to close,
a pause screen to bring up) and release it when Back should leave, so
the platform's own Back (the app moving to the background on Android)
answers once nothing holds it. A claim a component makes is released
when its entity is destroyed.
Parameters
handler() -> ()— Runs once per Back press this claim takes.options{ priority: number? }(optional) —{ priority: number? }. Priority 0 when omitted; the editor holds its claim that leaves play at -1, under every claim a played world makes.
Returns Claim — The claim: claim:release() gives Back up, claim:held() says whether it is still held.
local claim = Zin.back.claim(function() ui.showScreen("pause") end)
claim:release()
typed/builtin//modules/zinput/back/M/claims
M.claims() -> number
The number of claims held.
Returns number — How many claims on Back are held now.
if Zin.back.claims() == 0 then ... end
typed/builtin//modules/zinput/back/M/press
M.press()
Press Back, routed the way the platform's own Back is: the topmost dismissible overlay is dismissed, else the most recent claim's handler runs, else the platform's own default answers it (on Android the app moves to the background). Bind a world's own control to it to make Escape or a pad's button Back as well.
controls.pause:onPressed(Zin.back.press)
typed/builtin//modules/zinput/bindings/M/arrowKeys
M.arrowKeys() -> Binding
Convenience: arrow keys as a vector binding. Identical to
vector(key("ArrowRight"), key("ArrowLeft"), key("ArrowUp"), key("ArrowDown")).
Returns Binding — A vector binding for the arrow keys.
local move = Zin.bindings.arrowKeys()
typed/builtin//modules/zinput/bindings/M/axis
M.axis(plus: Binding, minus: Binding, opts: StickOpts?) -> Binding
Bind to a 1-D axis composed of two opposing bindings. Each arm contributes how far it is pushed, 0..1, so a key gives 1 while held and a trigger gives its travel — which is what separates easing a car forward from flooring it. Value = plus travel - minus travel.
Parameters
plusBinding— The binding whose travel contributes positively.minusBinding— The binding whose travel contributes negatively.optsStickOpts(optional)
Returns Binding — An axis binding descriptor.
local b = Zin.bindings.axis(B.key("KeyD"), B.key("KeyA"))
typed/builtin//modules/zinput/bindings/M/evalAxis
M.evalAxis(b: any?, surface: string?) -> number
Resolve a 1-D axis binding to a scalar.
For axis bindings the result is in [-1, 1]. For mouseDelta /
scroll bindings the result is a raw physical delta (pixels,
wheel clicks) and may exceed that range — shape it with Zin.axes.
Parameters
bany(optional) — The binding to evaluate.surfacestring(optional) — Whose keys a key binding reads:"scene"(the default) or"editor". An input map declared for the editor surface evaluates with"editor".
Returns number — The axis value this frame (0 when the binding is not scalar).
local x = Zin.bindings.evalAxis(Zin.bindings.mouseDelta("x"))
typed/builtin//modules/zinput/bindings/M/evalHeld
M.evalHeld(b: any?, surface: string?) -> boolean
Is this binding currently delivering a held signal? key/mouse/modKey/pointerLocked → boolean held. axis/vector/mouseDelta/scroll → magnitude non-zero.
Parameters
bany(optional) — The binding to evaluate.surfacestring(optional) — Whose keys a key binding reads:"scene"(the default) or"editor". An input map declared for the editor surface evaluates with"editor".
Returns boolean — Whether the binding is held this frame.
if Zin.bindings.evalHeld(b) then ... end
typed/builtin//modules/zinput/bindings/M/evalPressed
M.evalPressed(b: any?, surface: string?) -> boolean
Did this binding's press edge fire this frame? Boolean bindings only. Axis/vector return false (use evalAxis/evalVector with a held-time threshold for edge tracking).
Parameters
bany(optional) — The binding to evaluate.surfacestring(optional) — Whose keys a key binding reads:"scene"(the default) or"editor". An input map declared for the editor surface evaluates with"editor".
Returns boolean — Whether the press edge fired this frame.
if Zin.bindings.evalPressed(b) then print("jump") end
typed/builtin//modules/zinput/bindings/M/evalReleased
M.evalReleased(b: any?, surface: string?) -> boolean
Did this binding's release edge fire this frame?
Parameters
bany(optional) — The binding to evaluate.surfacestring(optional) — Whose keys a key binding reads:"scene"(the default) or"editor". An input map declared for the editor surface evaluates with"editor".
Returns boolean — Whether the release edge fired this frame.
if Zin.bindings.evalReleased(b) then ... end
typed/builtin//modules/zinput/bindings/M/evalVector
M.evalVector(b: any?, surface: string?) -> Vec2
Resolve a 2-D vector binding to { x, y }.
For vector bindings the result is in [-1, 1]. For mouseDelta
/ scroll bindings (with no axis set) the result is the raw
frame delta — mouseDelta = { x = dx, y = dy }, scroll =
{ x = 0, y = wheel_delta } (x reserved for future h-scroll).
Parameters
bany(optional) — The binding to evaluate.surfacestring(optional) — Whose keys a key binding reads:"scene"(the default) or"editor". An input map declared for the editor surface evaluates with"editor".
Returns Vec2 — A { x, y } vector.
local mv = Zin.bindings.evalVector(Zin.bindings.wasd())
typed/builtin//modules/zinput/bindings/M/format
M.format(b: any?) -> string
Render a single binding descriptor as a user-readable string.
Stable output across runs — the rebinding UI uses this verbatim.
Returns "<invalid>" for malformed input (does not throw).
Parameters
bany(optional) — The binding to format.
Returns string — A short, human-recognizable string.
local s = Zin.bindings.format(B.modKey("Ctrl+S")) -- "Ctrl+S"
typed/builtin//modules/zinput/bindings/M/formatAll
M.formatAll(bindings: any?, sep: string?) -> string
Render an array of bindings joined by sep (default ", ").
Returns "<no bindings>" if the argument isn't a table, "<empty>"
for an empty array. What a summary row listing one control's
bindings for a device class shows.
Parameters
bindingsany(optional) — The bindings to format.sepstring(optional) — Optional join separator (default", ").
Returns string — A composite string description.
Zin.bindings.formatAll({ B.key("Space"), B.modKey("Ctrl+S") })
typed/builtin//modules/zinput/bindings/M/isAxis
M.isAxis(binding: any?) -> boolean
True for the binding descriptors evalAxis resolves to a scalar:
axis, touchPinch and padAxis, plus a mouseDelta, scroll,
touchDrag or touchStick naming one axis of its two-axis surface.
Parameters
bindingany(optional) — The binding to test.
Returns boolean — Whether the binding resolves to a scalar.
assert(Zin.bindings.isAxis(Zin.bindings.mouseDelta("x")))
typed/builtin//modules/zinput/bindings/M/isBoolean
M.isBoolean(binding: any?) -> boolean
True for binding descriptors that resolve to a boolean (key, mouse, modKey, pointerLocked, touchButton).
Parameters
bindingany(optional) — The binding to test.
Returns boolean — Whether the binding resolves to a boolean.
assert(Zin.bindings.isBoolean(Zin.bindings.key("Space")))
typed/builtin//modules/zinput/bindings/M/isVector
M.isVector(binding: any?) -> boolean
True for the binding descriptors that resolve to a 2-D vector:
vector and padStick, plus a mouseDelta, scroll, touchDrag
or touchStick reporting its whole surface. Naming one axis of those
four makes the binding a scalar, which isAxis answers for, so no
binding answers both.
Parameters
bindingany(optional) — The binding to test.
Returns boolean — Whether the binding resolves to a 2-D vector.
assert(Zin.bindings.isVector(Zin.bindings.wasd()))
typed/builtin//modules/zinput/bindings/M/key
M.key(code: string) -> Binding
Bind to a single key. Code is a web-style identifier (e.g.
"KeyW", "Space", "ShiftLeft", "ArrowUp") — same vocabulary as
Zin.state.keyDown(code).
Parameters
codestring— The key code identifier.
Returns Binding — A key binding descriptor.
local b = Zin.bindings.key("Space")
typed/builtin//modules/zinput/bindings/M/modKey
M.modKey(spec: string) -> Binding
Parse a modifier-decorated key spec like "Ctrl+S", "Shift+Alt+P", or just "Escape". Recognized modifier tokens (case-insensitive): "Ctrl", "Control", "Shift", "Alt". Anything else is treated as the base key code. The last segment is the base key.
Parameters
specstring— The modifier-decorated key spec.
Returns Binding — A modKey binding descriptor.
local b = Zin.bindings.modKey("Ctrl+S")
typed/builtin//modules/zinput/bindings/M/mouse
M.mouse(button: string) -> Binding
Bind to a single mouse button by name ("left", "right", or "middle").
Parameters
buttonstring— The mouse-button name.
Returns Binding — A mouse binding descriptor.
local b = Zin.bindings.mouse("right")
typed/builtin//modules/zinput/bindings/M/mouseDelta
M.mouseDelta(axis: string?) -> Binding
Bind to the mouse delta this frame.
axis = nil→ vector binding returning{ x = dx, y = dy }axis = "x"→ scalar binding returningdxaxis = "y"→ scalar binding returningdyValues are not normalised to[-1, 1]— mouse delta is a physical pixel count. A control carrying this beside a stick declaresas = "delta"withunitsPerSecondon the stick class, so both classes reach the consumer measured in these pixels.
Parameters
axisstring(optional) — Optional."x"/"y"for a scalar binding; omit for a vector.
Returns Binding — A mouseDelta binding descriptor.
local look = Zin.bindings.mouseDelta()
local lookX = Zin.bindings.mouseDelta("x")
typed/builtin//modules/zinput/bindings/M/padAxis
M.padAxis(axis: string, opts: StickOpts?) -> Binding
Gamepad axis binding, one canonical axis as a scalar. Stick axes
run -1..1 in the frame the binding declares — forward positive by
default, matching the touch stick and B.wasd(), and screen-down
positive under as = "delta", matching the mouse and the dragging
finger it is measured against. Trigger axes run 0..1.
Parameters
axisstring— Canonical name: left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger, right_trigger.optsStickOpts(optional)
Returns Binding — The binding descriptor.
B.padAxis("right_stick_x")
typed/builtin//modules/zinput/bindings/M/padButton
M.padButton(button: string, slot: number?) -> Binding
Gamepad button binding, addressed by canonical position rather
than by any vendor's letter — south is the lower face button on
every pad, whatever it is printed with. With no slot, ANY connected
pad drives it, which is what a single-player scheme wants; name a slot
for local multiplayer.
Parameters
buttonstring— Canonical name: south, east, west, north, left_shoulder, right_shoulder, left_trigger, right_trigger, left_stick, right_stick, select, start, guide, dpad_up, dpad_down, dpad_left, dpad_right.slotnumber(optional) — Pad slot, or nil for any pad.
Returns Binding — The binding descriptor.
B.padButton("south")
typed/builtin//modules/zinput/bindings/M/padStick
M.padStick(stick: string, opts: StickOpts?) -> Binding
Gamepad stick binding: the 2-D vector of one thumbstick. The vector shape a movement or look axis wants, without naming its two components separately.
Parameters
stickstring— "left" or "right".optsStickOpts(optional)
Returns Binding — The binding descriptor.
B.padStick("left")
typed/builtin//modules/zinput/bindings/M/padTrigger
M.padTrigger(trigger: string, slot: number?) -> Binding
Gamepad trigger binding: one trigger's analog travel, 0..1. The
same trigger also latches a digital padButton at half throw, so a
scheme binds whichever of the two it means.
Parameters
triggerstring— "left" or "right".slotnumber(optional) — Pad slot, or nil for any pad.
Returns Binding — The binding descriptor.
B.padTrigger("right")
typed/builtin//modules/zinput/bindings/M/pointerLocked
M.pointerLocked() -> Binding
Bind to pointer-lock state. evalHeld is true while the
pointer is locked. evalPressed / evalReleased always return
false (use Zin.events.on for edge events).
Returns Binding — A pointerLocked binding descriptor.
local locked = Zin.bindings.pointerLocked()
typed/builtin//modules/zinput/bindings/M/scroll
M.scroll(axis: string?) -> Binding
Bind to the scrollwheel delta this frame.
axis = nil→ vector binding returning{ x = 0, y = scroll_delta }axis = "y"→ scalar binding returning the wheel deltaaxis = "x"→ scalar binding returning0(no horizontal-scroll surface today; reserved for future hardware support)
Parameters
axisstring(optional) — Optional."x"/"y"for a scalar binding; omit for a vector.
Returns Binding — A scroll binding descriptor.
local zoom = Zin.bindings.scroll("y")
typed/builtin//modules/zinput/bindings/M/surfacesOf
M.surfacesOf(binding: any?) -> { string }
Which device surface a binding reads from: pointer (mouse, wheel
or finger), keyboard (keys and composed text), or gamepad. A
composite binding (axis, vector) answers with the surfaces of every
binding it combines.
Parameters
bindingany(optional) — The binding to classify.
Returns { string } — Array of surface names, without repeats.
local s = Zin.bindings.surfacesOf(Zin.bindings.wasd())
typed/builtin//modules/zinput/bindings/M/touchButton
M.touchButton(opts: TouchButtonOpts) -> Binding
Virtual-button binding: a boolean fed by the on-screen button
identified by opts.label (else opts.zone).
priority (lower renders first/more prominent on the touch overlay;
defaults to 50 when omitted), size ("small"|"medium"|"large",
defaults to "medium" - this button's own circle radius, whatever
else the overlay is drawing), and group (buttons sharing a group
render adjacently) shape the overlay's layout - see
Zin.touchControls.
label is what the circle reads. icon is a glyph drawn in its
place - modules.icons names the phosphor set the default
iconFont selects, and any string the face holds a glyph for draws.
fill, stroke, strokeWidth, textColor, fontFamily,
fontScale, iconFont and iconScale state THIS button's
appearance over whatever Zin.touchControls.setAppearance set for
the overlay. A colour takes the CSS forms the canvas accepts; a
family takes the list style.fontFamily takes (ui.listFonts()
names them); a scale is the share of the button's radius the glyph
is set at.
Parameters
optsTouchButtonOpts— { zone: string?, label: string?, icon: string?, priority: number?, size: string?, group: string?, fill: string?, stroke: string?, strokeWidth: number?, textColor: string?, fontFamily: string?, fontScale: number?, iconFont: string?, iconScale: number? } - at least one of zone/label.
Returns Binding — The binding descriptor.
B.touchButton({ zone = "right-lower", label = "Jump", priority = 10 })
B.touchButton({ zone = "right-lower", label = "Fire", icon = Icon.Bomb, fill = "#7d1d1d", stroke = "#e8b33a" })
typed/builtin//modules/zinput/bindings/M/touchDrag
M.touchDrag(opts: { zone: string, axis: string? }) -> Binding
Touch-drag binding: a per-frame delta vector fed by drags in
opts.zone (mouseDelta semantics — px this frame).
Parameters
opts{ zone: string, axis: string? }— { zone: string } — the drag zone id (e.g. "right").
Returns Binding — The binding descriptor.
B.touchDrag({ zone = "right" })
typed/builtin//modules/zinput/bindings/M/touchPinch
M.touchPinch(opts: StickOpts?) -> Binding
Pinch binding: a scalar axis fed by the two-finger pinch delta (px this tick; positive = spreading).
Parameters
optsStickOpts(optional)
Returns Binding — The binding descriptor.
B.touchPinch()
typed/builtin//modules/zinput/bindings/M/touchStick
M.touchStick(opts: { zone: string, axis: string? }) -> Binding
Virtual-stick binding: a vector fed by the on-screen stick in
opts.zone (components -1..1). Evaluates {x=0,y=0} while no
control writes the zone.
Parameters
opts{ zone: string, axis: string? }— { zone: string } — the stick's zone id (e.g. "left").
Returns Binding — The binding descriptor.
B.touchStick({ zone = "left" })
typed/builtin//modules/zinput/bindings/M/vector
M.vector(right: Binding, left: Binding, up: Binding, down: Binding) -> Binding
Bind to a 2-D vector. Each arm is a binding that contributes to
one axis when held. The returned vector is
{ x = right - left, y = up - down }.
Parameters
rightBinding— The binding that drives+x.leftBinding— The binding that drives-x.upBinding— The binding that drives+y.downBinding— The binding that drives-y.
Returns Binding — A vector binding descriptor.
local b = Zin.bindings.vector(B.key("KeyD"), B.key("KeyA"), B.key("KeyW"), B.key("KeyS"))
typed/builtin//modules/zinput/bindings/M/wasd
M.wasd() -> Binding
Convenience: WASD as a vector binding. Identical to
vector(key("KeyD"), key("KeyA"), key("KeyW"), key("KeyS")).
Returns Binding — A vector binding for WASD.
local move = Zin.bindings.wasd()
typed/builtin//modules/zinput/chords/M/_advanceTime
M._advanceTime(_dt: number)
Internal: age out stale recognizers. Called once per tick.
Parameters
_dtnumber— Frame delta in seconds (currently unused; recognizers age off wall-clock time).
Zin.chords._advanceTime(0.016)
typed/builtin//modules/zinput/chords/M/_beginTick
M._beginTick()
Internal: clear per-frame firedThisTick flags. Called at the
start of each Zin.tick.
Zin.chords._beginTick()
typed/builtin//modules/zinput/chords/M/_dispatchFires
M._dispatchFires()
Internal: dispatch onFire callbacks for chords that fired
this tick. Called at the end of Zin.tick.
Zin.chords._dispatchFires()
typed/builtin//modules/zinput/chords/M/_observeEvent
M._observeEvent(ev: any?)
Internal: feed one event into each defined chord's state
machine. Called by Zin.tick for every event in this frame's
__zero_input.events().
Parameters
evany(optional) — One input event from the frame log.
Zin.chords._observeEvent(ev)
typed/builtin//modules/zinput/chords/M/_settled
M._settled() -> boolean
Internal: whether every recognizer sits idle -- no sequence mid-entry, no simultaneous press collecting, no fire pending dispatch. The tick's quiescence gate reads it.
Returns boolean — true when no recognizer holds in-flight state.
if Zin.chords._settled() then ... end
typed/builtin//modules/zinput/chords/M/cancel
M.cancel(name: string)
Manually reset the chord's progress (e.g. on context switch).
Parameters
namestring— The chord name.
Zin.chords.cancel("konami")
typed/builtin//modules/zinput/chords/M/clear
M.clear()
Wipe every chord, state, and subscriber. Primarily for tests.
Zin.chords.clear()
typed/builtin//modules/zinput/chords/M/define
M.define(spec: ChordSpec)
Define one or more chords. Each entry replaces any existing chord under the same name; other chords are preserved.
Parameters
specChordSpec— Map ofname -> { kind = "sequence"|"simultaneous", ... }.
Zin.chords.define({ konami = { kind = "sequence", steps = {...} } })
typed/builtin//modules/zinput/chords/M/fired
M.fired(name: string) -> boolean
True on exactly one frame: the frame the chord completed its pattern.
Parameters
namestring— The chord name.
Returns boolean — Whether the chord fired this frame.
if Zin.chords.fired("konami") then print("bonus unlocked") end
typed/builtin//modules/zinput/chords/M/get
M.get(name: string) -> ChordDef?
Internal introspection: definition record (or nil).
Parameters
namestring— The chord name.
Returns ChordDef? — The internal ChordDef record, or nil if not defined.
local def = Zin.chords.get("konami")
typed/builtin//modules/zinput/chords/M/has
M.has(name: string) -> boolean
True if a chord with this name is defined.
Parameters
namestring— The chord name.
Returns boolean — Whether the chord exists in the registry.
if Zin.chords.has("konami") then ... end
typed/builtin//modules/zinput/chords/M/names
M.names() -> { string }
All defined chord names, in arbitrary order.
Returns { string } — A fresh array of chord names.
for _, n in ipairs(Zin.chords.names()) do print(n) end
typed/builtin//modules/zinput/chords/M/off
M.off(handle: OnFireHandle)
Cancel an onFire subscription.
Parameters
handleOnFireHandle— The handle returned byonFire.
Zin.chords.off(h)
typed/builtin//modules/zinput/chords/M/onFire
M.onFire(name: string, callback: () -> (), opts: OnFireOpts?) -> OnFireHandle
Register a callback fired when the named chord completes.
Returns a handle for off(). Multiple callbacks per chord
dispatch in insertion order.
Parameters
namestring— The chord name.callback() -> ()— Invoked when the chord fires.optsOnFireOpts(optional) — Optional{ context, once }.
Returns OnFireHandle — A handle whose off() cancels the subscription.
local h = Zin.chords.onFire("konami", function() ... end)
typed/builtin//modules/zinput/chords/M/progress
M.progress(name: string) -> number
Progress through the chord, 0..1. For sequences: matched- steps / total. For simultaneous: matched / total. Always 0 when not in flight.
Parameters
namestring— The chord name.
Returns number — Progress as a number in [0, 1].
local p = Zin.chords.progress("konami")
typed/builtin//modules/zinput/chords/M/remove
M.remove(name: string)
Remove a chord. No-op if not defined. Subscribers on the same name are dropped along with it.
Parameters
namestring— The chord name.
Zin.chords.remove("konami")
typed/builtin//modules/zinput/clock/M/_setClock
M._setClock(fn: (() -> number)?)
Internal: override the input clock with fn (returns seconds), or pass
nil to restore the engine clock. Exposed for deterministic timing —
fixed-step replay and tests that need a controllable clock.
Parameters
fn(() -> number)(optional) — A clock returning seconds, ornilto restore the default source.
require("@builtin::modules.zinput.clock")._setClock(function() return t end)
typed/builtin//modules/zinput/clock/M/nowSeconds
M.nowSeconds() -> number
Current wall-clock seconds for input timing. Reads the engine's
per-frame getTime(), falling back to os.clock() where no engine time
surface exists. Honors a clock injected via _setClock.
Returns number — Seconds of wall time.
local t = require("@builtin::modules.zinput.clock").nowSeconds()
typed/builtin//modules/zinput/conflicts/M/_loadIntentionalPairs
M._loadIntentionalPairs(pairs_: any?)
Internal: seed the working set from a saved profile's
intentional_pairs field. Called on Zin.profile.activate. The
input shape is { ["a"] = "b", ... } or { {a, b}, ... } —
the JSON encoder lays it out as the array shape, but a hand-
edited profile may use either. Silently ignores malformed
entries.
Parameters
pairs_any(optional) — The raw saved pair list.
Conflicts._loadIntentionalPairs(profileJson.intentional_pairs)
typed/builtin//modules/zinput/conflicts/M/_resetIntentional
M._resetIntentional()
Test-only: wipe the working set. Suite isolation; not part of the public contract.
Zin.conflicts._resetIntentional()
typed/builtin//modules/zinput/conflicts/M/_serializeIntentional
M._serializeIntentional() -> { { string } }
Serialize the working set for Zin.profile.save / export.
Output is an array of { "a", "b" } pairs in canonical order —
matches the shape _loadIntentionalPairs consumes on activate,
so the round-trip is lossless.
Returns { { string } } — The serialized intentional-pair list.
local rows = Zin.conflicts._serializeIntentional()
typed/builtin//modules/zinput/conflicts/M/_setPersistFn
M._setPersistFn(fn: () -> ())
Internal: register the closure that writes the current
intentional-pair list back to the active profile. Wired by
zinput.init so the profile module doesn't have to depend on
conflicts (one-way edge).
Parameters
fn() -> ()— The persist closure.
local Profile = require("@builtin::modules.zinput.profile")
Conflicts._setPersistFn(function() Profile.save(...) end)
typed/builtin//modules/zinput/conflicts/M/bindingKey
M.bindingKey(b: any?) -> string?
Canonical leaf key for a single discrete binding, or nil if
the binding has no single discrete identity (axis, vector,
mouseDelta, scroll, pointerLocked).
Outputs:
key:Space — { kind = "key", code = "Space" }
modKey:Ctrl+Shift+KeyS — { kind = "modKey", code = "KeyS", mods = { ctrl, shift } }
mouse:left — { kind = "mouse", button = "left" }
Parameters
bany(optional) — The binding to canonicalise.
Returns string? — The canonical leaf-key string, or nil when the binding has no single identity.
local k = Zin.conflicts.bindingKey(B.key("Space")) -- "key:Space"
typed/builtin//modules/zinput/conflicts/M/find
M.find(binding: any?) -> { Owner }
Find every action/axis whose binding shares a leaf key with
the probe. Returns an array of { target, name, slot?, context? }
owners. Empty on no conflict. Decomposes axis/vector bindings on both
sides — a probe key:Space matches an axis whose plus = Space.
Parameters
bindingany(optional) — The binding to look up.
Returns { Owner } — An array of conflict owners.
local owners = Zin.conflicts.find(B.key("Space"))
typed/builtin//modules/zinput/conflicts/M/forActiveProfile
M.forActiveProfile() -> { ConflictRow }
Enumerate every leaf key shared by two or more owners across
the active action + axis registries. Returns an array sorted by
key for deterministic UI rendering; empty when there are no
conflicts. Each row carries a kind = "real" | "intentional"
field — see module about-block for the classification rule.
Returns { ConflictRow } — An array of ConflictRow records.
for _, row in ipairs(Zin.conflicts.forActiveProfile()) do ... end
typed/builtin//modules/zinput/conflicts/M/isIntentional
M.isIntentional(actionA: string, actionB: string) -> boolean
True when (actionA, actionB) is currently marked
intentional. Symmetric: (a, b) and (b, a) produce the same
answer.
Parameters
actionAstring— The first action name.actionBstring— The second action name.
Returns boolean — Whether the pair is currently marked.
if Zin.conflicts.isIntentional("a", "b") then ... end
typed/builtin//modules/zinput/conflicts/M/leafKeys
M.leafKeys(binding: any?) -> { string }
Every leaf key a binding decomposes into: one for a discrete
binding, one per arm for an axis or vector, and none for a
continuous source (mouseDelta, scroll) which has no discrete
identity to share.
This is the canonical form two bindings are compared in — what makes
Shift+Ctrl+KeyS and Ctrl+Shift+KeyS the same key, and what lets a
caller outside this module ask whether a control declares an input
without writing a second comparison of its own.
Parameters
bindingany(optional) — The binding to decompose.
Returns { string } — The leaf keys, possibly empty.
local keys = Zin.conflicts.leafKeys(B.mouse("right"))
typed/builtin//modules/zinput/conflicts/M/listIntentional
M.listIntentional() -> { { string } }
All currently-marked pairs, sorted lexicographically for deterministic UI rendering. Each pair is returned in canonical order (lexicographic ascending).
Returns { { string } } — An array of { a, b } pairs.
for _, p in ipairs(Zin.conflicts.listIntentional()) do ... end
typed/builtin//modules/zinput/conflicts/M/markIntentional
M.markIntentional(actionA: string, actionB: string) -> (boolean, string?)
Mark a pair of actions as intentionally sharing a binding. The
pair is suppressed from realOnly() until unmarkIntentional
reverses it. Stored in the active user profile's JSON so the mark
survives restart. Built-in profiles (Luau modules) hold marks in
memory only for the session — the natural flow is "mark → save
as user profile" if you want them to persist.
Parameters
actionAstring— The first action name.actionBstring— The second action name.
Returns (boolean, string?) — (true) on success, (false, err) when either action is unknown or both names are equal.
local ok, err = Zin.conflicts.markIntentional("crouch", "slide")
typed/builtin//modules/zinput/conflicts/M/realOnly
M.realOnly() -> { ConflictRow }
Subset of forActiveProfile() filtered to kind == "real"
rows only — the noise-filtered view the Input tab uses by
default.
Returns { ConflictRow } — The real conflicts only.
for _, row in ipairs(Zin.conflicts.realOnly()) do ... end
typed/builtin//modules/zinput/conflicts/M/unmarkIntentional
M.unmarkIntentional(actionA: string, actionB: string) -> boolean
Remove a previously-marked intentional pair. Idempotent: unmarking an absent pair is a no-op success.
Parameters
actionAstring— The first action name.actionBstring— The second action name.
Returns boolean — Whether the inputs were accepted (true unless either name was non-string).
Zin.conflicts.unmarkIntentional("crouch", "slide")
typed/builtin//modules/zinput/context/M/contains
M.contains(name: string) -> boolean
True if name is anywhere in the active stack (not just the
top). Useful for "is UI open?" checks regardless of further pushes.
Parameters
namestring— The context name to look for.
Returns boolean — Whether name appears anywhere in the stack.
if Zin.context.contains("ui") then engine.paused = true end
typed/builtin//modules/zinput/context/M/current
M.current() -> string
Current (top-of-stack) context name. Always at least default.
Returns string — The name at the top of the stack.
if Zin.context.current() == "ui" then ... end
typed/builtin//modules/zinput/context/M/default
M.default() -> string
The always-on context name. Anything declared without an explicit
context belongs here. The bottom of the stack is permanently this
value; pop cannot remove it.
Returns string — The default context name (the string "default").
local d = Zin.context.default() -- "default"
typed/builtin//modules/zinput/context/M/pop
M.pop() -> string?
Pop the top context. No-op when only default remains (so
unbalanced pop is safe and never leaves the stack empty).
Returns string? — The popped context name, or nil when the stack already held only default.
local prev = Zin.context.pop()
typed/builtin//modules/zinput/context/M/push
M.push(name: any?)
Push a context onto the stack. Becomes the new "current" context. Pushing the same context twice requires two pops to fully remove (matches typical UI nesting).
Parameters
nameany(optional) — The context name to push. Ignored when not a non-empty string.
Zin.context.push("ui")
typed/builtin//modules/zinput/context/M/reset
M.reset()
Pop everything except default. Useful for "exit all menus"
flows and for test setup/teardown.
Zin.context.reset()
typed/builtin//modules/zinput/context/M/stack
M.stack() -> { string }
Snapshot of the full stack, bottom→top. Returned table is a copy; mutating it does not affect the live stack.
Returns { string } — A copy of the active context stack.
local snap = Zin.context.stack()
typed/builtin//modules/zinput/context/M/with
M.with(name: string, fn: () -> T...) -> T...
Run fn with name pushed onto the stack; pop guaranteed on
return or error. Returns whatever fn returns.
Parameters
namestring— The context to push during the call.fn() -> T...— The thunk to run withnameon top of the stack.
Returns T... — Whatever fn returns.
Zin.context.with("ui", function() ... end)
typed/builtin//modules/zinput/controllers/M/fps
M.fps(opts: FpsOpts) -> FpsController
Construct a first-person walker controller.
Parameters
optsFpsOpts— FpsOpts table.
Returns FpsController — An FpsController object with :start/:stop/:update plus motion-intent readers (getDesiredVelocity, consumeJumpRequest, isSprinting, isCrouching).
local c = Zin.controllers.fps({ entity = playerId })
typed/builtin//modules/zinput/controllers/M/free
M.free(opts: FreeOpts) -> FreeController
Construct a free-fly designer/debug camera controller.
WASD plane motion + Q/E vertical + ShiftLeft sprint multiplier +
raw mouse-look (always reads the per-frame mouse delta — gating
is via Zin.context, not via pointer-lock). Mandatory: opts.entity
(entity ID string from entity.spawn(...).id). All other fields
default; pass actionPrefix to namespace if two free controllers
must coexist.
Parameters
optsFreeOpts— FreeOpts table.
Returns FreeController — A FreeController object with :start/:stop/:update.
local c = Zin.controllers.free({ entity = id })
typed/builtin//modules/zinput/controllers/M/orbit
M.orbit(opts: OrbitOpts) -> OrbitController
Construct an orbit camera controller around opts.target.
target is either an entity ID string (the orbit center tracks
that entity each frame) or a fixed { x, y, z } table.
Parameters
optsOrbitOpts— OrbitOpts table.
Returns OrbitController — An OrbitController object with :start/:stop/:update.
local c = Zin.controllers.orbit({ entity = cam, target = pivot })
typed/builtin//modules/zinput/controllers/M/vehicle
M.vehicle(opts: VehicleOpts?) -> VehicleController
Construct a driving controller: the throttle, steering, brake,
handbrake, boost, recover and zoom a player gives a ground vehicle,
read from an input map that binds each one on keyboard, gamepad and
touch. opts.inputMap defaults to @builtin::inputMaps.vehicle
(W/S throttle, A/D steer, Space brake, Ctrl handbrake, Shift boost,
R recover, scroll zoom). :start() takes the controls, which stands
the walking scheme down while somebody drives; :stop() gives them
back. The VehicleController component drives a car's
WheelColliders from this intent.
Parameters
optsVehicleOpts(optional) — VehicleOpts table, optional:inputMap(asset ref or path),label.
Returns VehicleController — A VehicleController object with :start/:stop/:update/:isActive and the intent readers getThrottle (-1..1, positive forward), getSteer (-1..1, positive right), getBrake (0..1), isHandbrakeHeld, isBoosting, consumeRecoverRequest and consumeZoom.
local drive = Zin.controllers.vehicle(); drive:start(); print(drive:getThrottle())
typed/builtin//modules/zinput/emulation/M/_advance
M._advance()
Internal: advance the emulation floor one tick. Releases every
emitted key when the active map or the input context changed since
the previous tick, then re-derives button / stick / drag emulation
from the current effective map's touch/kbm binding pairs — active-
context entries only. No-op when no map is active. Wired into
Zin.tick.
Zin.emulation._advance()
typed/builtin//modules/zinput/emulation/M/_reset
M._reset()
Test-only: release everything emitted and clear tracked state.
Zin.emulation._reset()
typed/builtin//modules/zinput/emulation/M/_settled
M._settled() -> boolean
Internal: whether the emulation floor is producing nothing -- no synthesized pointer motion, no emulated key held. The tick's quiescence gate reads it.
Returns boolean — true when the floor is at rest.
if Zin.emulation._settled() then ... end
typed/builtin//modules/zinput/emulation/M/getSensitivity
M.getSensitivity() -> number
The current touchDrag -> emulated mouse-delta multiplier.
Returns number — The sensitivity multiplier.
local s = Zin.emulation.getSensitivity()
typed/builtin//modules/zinput/emulation/M/setSensitivity
M.setSensitivity(n: number)
Set the touchDrag -> emulated mouse-delta multiplier (default 1.0).
Parameters
nnumber— The sensitivity multiplier.
Zin.emulation.setSensitivity(1.5)
typed/builtin//modules/zinput/emulation/M/synthesizedDelta
M.synthesizedDelta() -> (boolean, number, number)
The mouse delta this floor synthesized on the current tick, and whether it synthesized one at all. A drag the floor converted for a control the scheme does not cover is a delta the evaluators should read, where the platform's projection of the same contact is not.
Returns (boolean, number, number) — (active, dx, dy)
local ok, dx, dy = Zin.emulation.synthesizedDelta()
typed/builtin//modules/zinput/events/M/_dispatch
M._dispatch(events: any?)
Internal: dispatch a batch of events to every registered
subscriber. Called once per Zin.tick from the events stage of
the tick pipeline.
Parameters
eventsany(optional) — The frame's events as returned byM.frame().
M._dispatch(Zin.events.frame())
typed/builtin//modules/zinput/events/M/_onHeldByCaller
M._onHeldByCaller(filter: EventFilter, callback: (any) -> (), opts: SubOpts?) -> SubHandle
Internal: on for a caller that holds the subscription itself,
such as a composite handle over several subscriptions that ends with
the context which made it. Registers nothing under the calling context.
Parameters
filterEventFilter— Asontakes it.callback(any) -> ()— Asontakes it.optsSubOpts(optional) — Asontakes it.
Returns SubHandle — A handle whose off() cancels the subscription.
local h = Events._onHeldByCaller("key.down", fn)
typed/builtin//modules/zinput/events/M/clearSubscribers
M.clearSubscribers()
Cancel every subscription. Primarily for test isolation.
Zin.events.clearSubscribers()
typed/builtin//modules/zinput/events/M/frame
M.frame() -> { InputEvent }
Get this frame's events as an array of records, in dispatch
order. Returns an empty table if __zero_input.events is missing
or returns a non-table value.
Returns { InputEvent } — The current frame's events (a fresh array each call).
for _, ev in ipairs(Zin.events.frame()) do ... end
typed/builtin//modules/zinput/events/M/iter
M.iter()
Iterate this frame's events. Identical to
ipairs(Zin.events.frame()) but the explicit name reads better
at call sites.
Returns An ipairs-style iterator over this frame's events.
for i, ev in Zin.events.iter() do ... end
typed/builtin//modules/zinput/events/M/keysDown
M.keysDown() -> { InputEvent }
All key.down events this frame.
Returns { InputEvent } — The frame's key.down events in dispatch order.
for _, ev in ipairs(Zin.events.keysDown()) do print(ev.code) end
typed/builtin//modules/zinput/events/M/keysUp
M.keysUp() -> { InputEvent }
All key.up events this frame.
Returns { InputEvent } — The frame's key.up events in dispatch order.
for _, ev in ipairs(Zin.events.keysUp()) do ... end
typed/builtin//modules/zinput/events/M/mouseDowns
M.mouseDowns() -> { InputEvent }
All mouse.down events this frame.
Returns { InputEvent } — The frame's mouse.down events in dispatch order.
for _, ev in ipairs(Zin.events.mouseDowns()) do ... end
typed/builtin//modules/zinput/events/M/mouseMoves
M.mouseMoves() -> { InputEvent }
All mouse.move events this frame.
Returns { InputEvent } — The frame's mouse.move events in dispatch order.
for _, ev in ipairs(Zin.events.mouseMoves()) do ... end
typed/builtin//modules/zinput/events/M/mouseUps
M.mouseUps() -> { InputEvent }
All mouse.up events this frame.
Returns { InputEvent } — The frame's mouse.up events in dispatch order.
for _, ev in ipairs(Zin.events.mouseUps()) do ... end
typed/builtin//modules/zinput/events/M/mouseWheels
M.mouseWheels() -> { InputEvent }
All mouse.wheel events this frame.
Returns { InputEvent } — The frame's mouse.wheel events in dispatch order.
for _, ev in ipairs(Zin.events.mouseWheels()) do ... end
typed/builtin//modules/zinput/events/M/off
M.off(handle: SubHandle | number)
Cancel a subscription. Accepts a Handle returned from on or
a numeric id.
Parameters
handleSubHandle | number— Handle or numeric subscription id.
Zin.events.off(h)
typed/builtin//modules/zinput/events/M/on
M.on(filter: EventFilter, callback: (any) -> (), opts: SubOpts?) -> SubHandle
Register a callback fired when matching events arrive on
Zin.tick. Returns a handle for off(). The subscription ends with
the context that made it as well: a component's with that component
instance, a module body's with the module's next run.
Parameters
filterEventFilter— Either a kind string ("key.down"), a table of field matches ({ kind = "key.down", code = "Space" }), or a predicate functionfunction(ev) return ... end.callback(any) -> ()— Invoked ascallback(event, gpe)wheregpeis whether the UI layer took THIS event: the event's ownconsumed_by_uimarker, stamped against the focus the UI holds over the surface it arrived on. Returning"sink"halts further (lower-priority) dispatch for that event.optsSubOpts(optional) — Optional{ context, once, priority }.
Returns SubHandle — A handle whose off() cancels the subscription.
local h = Zin.events.on("key.down", function(ev) ... end)
typed/builtin//modules/zinput/events/M/subscriberCount
M.subscriberCount() -> number
Test/introspection: number of active subscribers.
Returns number — The current subscriber count.
assert(Zin.events.subscriberCount() == 0)
typed/builtin//modules/zinput/events/M/texts
M.texts() -> { InputEvent }
All text events this frame (composed text commits).
Returns { InputEvent } — The frame's text events in dispatch order.
for _, ev in ipairs(Zin.events.texts()) do print(ev.text) end
typed/builtin//modules/zinput/gamepad/M/available
M.available() -> boolean
Whether this session has ever seen a pad. Latched by the first connection, so unplugging one does not flip a scheme's prompts back to keyboard glyphs on a cable knock.
Returns boolean — True once a pad has connected.
if Zin.gamepad.available() then print("show the pad prompts") end
typed/builtin//modules/zinput/gamepad/M/count
M.count() -> number
How many pads are connected.
Returns number — The count.
if Zin.gamepad.count() >= 2 then print("two pads: start co-op") end
typed/builtin//modules/zinput/gamepad/M/family
M.family(slot: number?) -> string
Which controller family a pad belongs to, read from the device
name the platform reported: "xbox", "playstation", or
"nintendo". Xbox is the answer for anything unrecognised, because
the standard mapping every backend normalises to is the Xbox layout.
Parameters
slotnumber(optional) — Pad slot, or nil for the first connected pad.
Returns string — The family name.
if Zin.gamepad.family() == "playstation" then ... end
typed/builtin//modules/zinput/gamepad/M/get
M.get(slot: number) -> Pad?
The pad in slot, or nil when nothing holds it.
Parameters
slotnumber— The pad slot.
Returns Pad? — The pad record, or nil.
local pad = Zin.gamepad.get(0)
typed/builtin//modules/zinput/gamepad/M/label
M.label(button: string, slot: number?) -> string
What a legend should call a canonical button on the connected
pad — "south" reads as A on an Xbox pad, Cross on a
PlayStation one, B on a Nintendo one. Falls back to the canonical
name for anything the family tables do not cover.
Parameters
buttonstring— Canonical button name.slotnumber(optional) — Pad slot, or nil for the first connected pad.
Returns string — The label to draw.
print("Press " .. Zin.gamepad.label("south") .. " to jump")
typed/builtin//modules/zinput/gamepad/M/list
M.list() -> { Pad }
Every connected pad, in slot order. Each entry carries slot,
name, the three canonical button-name arrays, an axes map, and
whether it came from inputSim rather than a device.
Returns { Pad } — Array of pad records; empty when nothing is connected.
for _, pad in ipairs(Zin.gamepad.list()) do print(pad.slot, pad.name) end
typed/builtin//modules/zinput/gestures/M/_advanceTime
M._advanceTime(_dt: number)
Internal: time-based recognition (long-press) + continuous two-finger deltas (pinch, pan). Runs every tick.
Parameters
_dtnumber
Zin.gestures._advanceTime(dt)
typed/builtin//modules/zinput/gestures/M/_beginTick
M._beginTick()
Internal: clear per-tick continuous deltas. First tick of each engine frame.
Zin.gestures._beginTick()
typed/builtin//modules/zinput/gestures/M/_dispatchFires
M._dispatchFires()
Internal: deliver this tick's discrete gesture fires. Handler errors are caught and logged.
Zin.gestures._dispatchFires()
typed/builtin//modules/zinput/gestures/M/_observeEvent
M._observeEvent(ev: any?)
Internal: per-event observer — builds contact tracks and classifies discrete gestures on release.
Parameters
evany(optional) — The raw input event record.
Zin.gestures._observeEvent(ev)
typed/builtin//modules/zinput/gestures/M/_pairDown
M._pairDown() -> boolean
Internal: whether a two-finger gesture is being made right now: exactly two contacts are down that no on-screen widget is working. The overlay reads it to leave that pair to the gesture rather than routing it as one-finger drag motion, so the two agree on which gesture is happening.
Returns boolean — true while the pair is down.
if Zin.gestures._pairDown() then ... end
typed/builtin//modules/zinput/gestures/M/_reset
M._reset()
Test-only: clear all tracks, fires, deltas, and subscriptions.
Zin.gestures._reset()
typed/builtin//modules/zinput/gestures/M/_setWidgetContacts
M._setWidgetContacts(ids: { [number]: boolean })
Internal: name the contacts an on-screen widget is working, so the pair a two-finger gesture reads leaves them out. Published each tick by the overlay that routes touches, before the recognizers run.
Parameters
ids{ [number]: boolean }— A set keyed by contact id.
Zin.gestures._setWidgetContacts({ [3] = true })
typed/builtin//modules/zinput/gestures/M/_settled
M._settled() -> boolean
Internal: whether the recognizers sit idle -- no touch tracks live, no fire pending dispatch, no pinch or pan delta carried. The tick's quiescence gate reads it.
Returns boolean — true when nothing gestural is in flight.
if Zin.gestures._settled() then ... end
typed/builtin//modules/zinput/gestures/M/configure
M.configure(opts: { [string]: number })
Override recognition thresholds. Unspecified fields keep their current values.
Parameters
opts{ [string]: number }— Partial GestureConfig.
Zin.gestures.configure({ longPressDuration = 0.8 })
typed/builtin//modules/zinput/gestures/M/getConfig
M.getConfig() -> GestureConfig
Current recognition thresholds (a copy).
Returns GestureConfig — The GestureConfig table.
local c = Zin.gestures.getConfig()
typed/builtin//modules/zinput/gestures/M/off
M.off(handle: number) -> boolean
Unsubscribe a handle returned by any Zin.gestures.on* function.
Parameters
handlenumber— The numeric handle.
Returns boolean — Whether a subscription was removed.
Zin.gestures.off(h)
typed/builtin//modules/zinput/gestures/M/onDoubleTap
M.onDoubleTap(fn: (any) -> ()) -> number
Subscribe to double-taps. fn(ev) with ev = { x, y, id }. The
release that completes a double-tap also emits a tap on the same
tick (each qualifying release taps; the second one additionally
double-taps).
Parameters
fn(any) -> ()— The callback.
Returns number — A numeric handle for Zin.gestures.off.
Zin.gestures.onDoubleTap(function(ev) ... end)
typed/builtin//modules/zinput/gestures/M/onLongPress
M.onLongPress(fn: (any) -> ()) -> number
Subscribe to long-presses (fires once per contact, while the
finger is still down). fn(ev) with ev = { x, y, id }.
Parameters
fn(any) -> ()— The callback.
Returns number — A numeric handle for Zin.gestures.off.
Zin.gestures.onLongPress(function(ev) print("long press", ev.x, ev.y) end)
typed/builtin//modules/zinput/gestures/M/onSwipe
M.onSwipe(fn: (any) -> ()) -> number
Subscribe to swipes (fires on release). fn(ev) with ev =
{ direction = "left"|"right"|"up"|"down", dx, dy, velocity, id }.
Parameters
fn(any) -> ()— The callback.
Returns number — A numeric handle for Zin.gestures.off.
Zin.gestures.onSwipe(function(ev) if ev.direction == "left" then ... end end)
typed/builtin//modules/zinput/gestures/M/onTap
M.onTap(fn: (any) -> ()) -> number
Subscribe to taps. fn(ev) with ev = { x, y, id, duration }.
Parameters
fn(any) -> ()— The callback.
Returns number — A numeric handle for Zin.gestures.off.
Zin.gestures.onTap(function(ev) select(ev.x, ev.y) end)
typed/builtin//modules/zinput/gestures/M/panDelta
M.panDelta() -> (number, number)
This tick's two-finger pan delta: movement of the contacts' centroid, in px. Zeros unless exactly two contacts are down that no on-screen widget is working.
Returns (number, number) — dx, dy.
local dx, dy = Zin.gestures.panDelta()
typed/builtin//modules/zinput/gestures/M/pinchDelta
M.pinchDelta() -> number
This tick's two-finger pinch delta: change in the distance between the two contacts, in px (positive = spreading). 0 unless exactly two contacts are down that no on-screen widget is working. A thumb on a movement stick or a button is operating that control, and the gesture reads the contacts left over.
Returns number — The pinch delta.
cam.zoom += Zin.gestures.pinchDelta() * 0.01
typed/builtin//modules/zinput/input/M/_disconnectAll
M._disconnectAll()
Test-only: tear down every Zin.input subscription installed
this session.
Zin.input._disconnectAll()
typed/builtin//modules/zinput/input/M/_observeEvent
M._observeEvent(ev: any?)
Internal observer driven by Zin.tick. Receives every event in
the same drain loop as State / Chords and updates _lastInputType.
Parameters
evany(optional) — The raw input event record.
Zin.input._observeEvent(ev)
typed/builtin//modules/zinput/input/M/_owns
M._owns(handle: number) -> boolean
Internal: reports whether handle was issued by this module.
Used by the unified Zin.disconnect to route ownership.
Parameters
handlenumber— Candidate handle id.
Returns boolean — true when this module owns the handle.
if Zin.input._owns(h) then ... end
typed/builtin//modules/zinput/input/M/_reset
M._reset()
Test-only: clear lastInputType. Public so test suites can
isolate cases.
Zin.input._reset()
typed/builtin//modules/zinput/input/M/_setAllocator
M._setAllocator(fn: () -> number)
Internal: wire a shared id allocator. Called once at
module-load time from zinput/init.luau.
Parameters
fn() -> number— A() -> numberallocator that returns fresh handle ids.
Zin.input._setAllocator(zin._allocateHandle)
typed/builtin//modules/zinput/input/M/disconnect
M.disconnect(handle: Handle) -> boolean
Tear down a subscription returned by Zin.input.on*. Idempotent.
Parameters
handleHandle— The composite handle returned by anon*call.
Returns boolean — true if a subscription was disconnected, false if the handle was unknown or already disconnected.
Zin.input.disconnect(h)
typed/builtin//modules/zinput/input/M/lastInputType
M.lastInputType() -> string?
Returns the userInputType of the most recent input event this session.
"Keyboard" | "Mouse" | "Touch" | nil.
Returns string? — The userInputType string, or nil if no events seen yet.
local t = Zin.input.lastInputType()
typed/builtin//modules/zinput/input/M/onBegan
M.onBegan(fn: (any, boolean) -> any, opts: SubOpts?) -> Handle
Fires on press / mouse-button-down. Handler: fn(io, gpe) -> "sink"?.
Parameters
fn(any, boolean) -> any— Handler(io, gpe) -> "sink"?.optsSubOpts(optional) — Optional{ priority, context, once }forwarded toZin.events.on.
Returns Handle — A composite handle usable with disconnect / Zin.actions.disconnect.
Zin.input.onBegan(function(io, gpe) print(io.kind) end)
typed/builtin//modules/zinput/input/M/onChanged
M.onChanged(fn: (any, boolean) -> any, opts: SubOpts?) -> Handle
Fires on mouse motion / scroll. Handler: fn(io, gpe) -> "sink"?.
Parameters
fn(any, boolean) -> any— Handler(io, gpe) -> "sink"?.optsSubOpts(optional) — Optional{ priority, context, once }.
Returns Handle — A composite handle.
Zin.input.onChanged(function(io, gpe) ... end)
typed/builtin//modules/zinput/input/M/onEnded
M.onEnded(fn: (any, boolean) -> any, opts: SubOpts?) -> Handle
Fires on release / mouse-button-up. Handler: fn(io, gpe) -> "sink"?.
Parameters
fn(any, boolean) -> any— Handler(io, gpe) -> "sink"?.optsSubOpts(optional) — Optional{ priority, context, once }.
Returns Handle — A composite handle.
Zin.input.onEnded(function(io, gpe) ... end)
typed/builtin//modules/zinput/input/M/onTextInput
M.onTextInput(fn: (string, boolean) -> any, opts: SubOpts?) -> Handle
Fires on committed text input. Handler: fn(text, gpe) -> "sink"?.
Parameters
fn(string, boolean) -> any— Handler(text, gpe) -> "sink"?wheretextis the committed string.optsSubOpts(optional) — Optional{ priority, context, once }.
Returns Handle — A composite handle.
Zin.input.onTextInput(function(text, gpe) ... end)
typed/builtin//modules/zinput/input/M/subscriberCount
M.subscriberCount() -> number
Test/introspection: number of active Zin.input.* subscriptions
(composite handles, not the underlying per-kind Events subscribers).
Returns number — Subscriber count.
local n = Zin.input.subscriberCount()
typed/builtin//modules/zinput/map/M/_reactivate
M._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
recordany(optional) — The map record, freshly read from its source.
Returns any — The effective map that was applied.
Zin.map._reactivate(record)
typed/builtin//modules/zinput/map/M/_reset
M._reset()
Test-only: clear active-map state (the profile registry keeps whatever was applied).
Zin.map._reset()
typed/builtin//modules/zinput/map/M/activate
M.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
recordany(optional) — The map (or profile) record, or an inputMap asset ref.
Returns any — The effective map that was activated.
Zin.map.activate(require("@builtin::inputMaps.default"))
typed/builtin//modules/zinput/map/M/activeName
M.activeName() -> string?
The active map's name, or nil.
Returns string? — The name string, or nil.
if Zin.map.activeName() == "default" then ... end
typed/builtin//modules/zinput/map/M/addTouchButton
M.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
actionNamestring— The action to attach the button to.buttonOptsBindings.TouchButtonOpts— The optionsZin.bindings.touchButtontakes - the button's zone, label, icon glyph, layout and appearance.emitKeystring(optional) — Optional key code — when set and the action has no kbm class yet, seeds it withB.key(emitKey)so a synthetic action is self-contained from the moment it's created.
Returns any — The added touchButton binding descriptor.
Zin.map.addTouchButton("emit:KeyF", { label = "Cast" }, "KeyF")
typed/builtin//modules/zinput/map/M/bake
M.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
namestring— The new asset's name.
Returns any — The created asset ref.
Zin.map.bake("my_scheme")
typed/builtin//modules/zinput/map/M/bindingsFor
M.bindingsFor(eff: any?, name: string, class: string) -> any
The effective bindings for one action or axis and device class.
Parameters
effany(optional) — An effective map (from materialize/effective).namestring— The action or axis name.classstring— "kbm" | "gamepad" | "touch".
Returns any — The bindings array (actions) or binding (axes), or nil.
local touch = Zin.map.bindingsFor(eff, "jump", "touch")
typed/builtin//modules/zinput/map/M/effective
M.effective() -> any
The active map's effective form, or nil before any activation.
Returns any — The effective map, or nil.
local eff = Zin.map.effective()
typed/builtin//modules/zinput/map/M/ensureActive
M.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.
Returns any — The active effective map.
Zin.map.ensureActive()
typed/builtin//modules/zinput/map/M/isFallback
M.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.
Returns boolean — True while the active map is the arming fallback.
if not Zin.map.isFallback() then print(Zin.map.effective()) end
typed/builtin//modules/zinput/map/M/materialize
M.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
recordany(optional) — The map (or profile) record.
Returns any — The effective map.
local eff = Zin.map.materialize(require("@builtin::inputMaps.default"))
typed/builtin//modules/zinput/map/M/removeTouchButton
M.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
actionNamestring— The action the binding was added to.bindingany(optional) — The binding table returned byaddTouchButton.
Returns boolean — Whether a binding was actually removed.
Zin.map.removeTouchButton("emit:KeyF", binding)
typed/builtin//modules/zinput/observe/M/_publish
M._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()
typed/builtin//modules/zinput/observe/M/arm
M.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
onboolean(optional) — Arm (the default) or disarm.
Zin.observe.arm(true)
typed/builtin//modules/zinput/observe/M/armedFrames
M.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.
Returns number — Frames left in the arming window.
print(Zin.observe.armedFrames())
typed/builtin//modules/zinput/observe/M/control
M.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
namestring— The control name.
Returns any — { name, carriedBy, live, entries, lastTick, why }. lastTick carries its own window: the tick's own record once the tick is keeping one, and the same answers resolved live before then.
local c = Zin.observe.control("move")
typed/builtin//modules/zinput/observe/M/frame
M.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.
Returns any — { frameId, window, maps, controls, cost }.
local f = Zin.observe.frame()
typed/builtin//modules/zinput/observe/M/means
M.means(reason: string) -> string?
What one reason name means, or nil for a name outside the set.
Parameters
reasonstring— The reason name.
Returns string? — The sentence describing it, or nil.
print(Zin.observe.means("gateRefused"))
typed/builtin//modules/zinput/observe/M/reasons
M.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.
Returns { any } — Array of { name, means }, in the order the resolver considers them.
for _, r in ipairs(Zin.observe.reasons()) do print(r.name, r.means) end
typed/builtin//modules/zinput/observe/M/wanted
M.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.
Returns boolean — Whether a report is wanted.
if Zin.observe.wanted() then ... end
typed/builtin//modules/zinput/observe/M/whySilent
M.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
namestring— The control name.
Returns any — { name, reason, means, layer, carriedBy, ... } — the extra fields depend on the reason: suppressedBy / suppressedByHolders, needsContext / currentContext, gateError, declaredClasses / presentClasses, deadzone, subscribers, value, rawReading. On groupSuppressed, suppressedBy names the map standing this control down and suppressedByHolders names the activations holding that map up — each as Zin.scheme.live() reports it, with the chunk:line it was opened at. One of those holders is where the change that gives this control back belongs.
local why = Zin.observe.whySilent("look")
typed/builtin//modules/zinput/pointer/M/lock
M.lock()
Request pointer lock (cursor grab + hide). Records intent; the cursor is captured once the surface is active — native: window focused + clicked; WASM: on the next user gesture (browser policy).
Zin.pointer.lock()
typed/builtin//modules/zinput/pointer/M/locked
M.locked() -> boolean
Convenience: query the current pointer-lock state. Identical to
Zin.state.pointerLocked().
Returns boolean — true when the pointer is currently locked.
if Zin.pointer.locked() then ... end
typed/builtin//modules/zinput/pointer/M/unlock
M.unlock()
Release pointer lock (cursor ungrab + show).
Zin.pointer.unlock()
typed/builtin//modules/zinput/profile/M/_reset
M._reset()
Test-only: clear all in-memory state. Suite isolation; not part of the public contract.
Zin.profile._reset()
typed/builtin//modules/zinput/profile/M/_resetApplied
M._resetApplied()
Test-only: clear the applied/active markers so suites can simulate a cold session without touching the registered profiles. Not part of the public contract.
Zin.profile._resetApplied()
typed/builtin//modules/zinput/profile/M/_setMapEnsureFn
M._setMapEnsureFn(fn: (string) -> boolean)
Internal: wire the map-delegation hook. Called once at module-
load time from zinput/init.luau.
Parameters
fn(string) -> boolean— The hook invoked with the resolved scheme name; returnstruewhen it handled activation (routing through the map).
Profile._setMapEnsureFn(mapEnsureDefault)
typed/builtin//modules/zinput/profile/M/activate
M.activate(name: string, opts: ActivateOpts?) -> (boolean, string?)
Activate a registered profile: clear current actions/axes/chords,
then apply the profile's definitions. Sets the active name and
notifies subscribers. Activating the already-active profile is a
no-op unless opts.reactivate = true.
Parameters
namestring— Profile name to activate.optsActivateOpts(optional) — Optional{ reactivate }— forces re-apply when already active.
Returns (boolean, string?) — (true) on success or (false, err) on failure.
Zin.profile.activate("wasd-arrows")
Zin.profile.activate("wasd-arrows", { reactivate = true })
typed/builtin//modules/zinput/profile/M/appliedName
M.appliedName() -> string?
The name of the profile whose sections are currently applied to the actions/axes/chords registries, or nil when nothing has been applied this session.
Returns string? — The applied profile name, or nil.
local n = Zin.profile.appliedName()
typed/builtin//modules/zinput/profile/M/current
M.current() -> string?
Active profile name for this session, or nil if none has been
activated yet.
Returns string? — The active profile name, or nil.
local n = Zin.profile.current()
typed/builtin//modules/zinput/profile/M/delete
M.delete(name: string) -> (boolean, string?)
Remove a user profile's JSON file. Refuses to delete built-ins.
The in-memory registry entry is also dropped. If the deleted profile
was active, current() keeps the name until a different profile is
activated — but the registry no longer has the descriptor.
Parameters
namestring— Non-empty profile name.
Returns (boolean, string?) — (true) on success or (false, err).
Zin.profile.delete("my-bindings")
typed/builtin//modules/zinput/profile/M/ensureActive
M.ensureActive(fallback: string?) -> (boolean, string?)
Bootstrap helper for controllers. If no profile is active,
activates the session-active profile or fallback if there is none.
Idempotent — if a profile is already active this is a no-op. A
camera/locomotion controller calls this on awake() so a scene with
no explicit Zin.profile.activate still has working bindings.
When the resolved scheme is the builtin "default", activation
routes through Zin.map so every device class (kbm, gamepad,
touch) comes along, not just kbm.
Parameters
fallbackstring(optional) — Optional fallback name (defaults to"default").
Returns (boolean, string?) — (true) on success or (false, err).
Zin.profile.ensureActive("wasd-arrows")
typed/builtin//modules/zinput/profile/M/export
M.export(name: string, vfsPath: string) -> (boolean, string?)
Write a named profile's serializable JSON to an arbitrary VFS path.
Same shape as save() — round-trips through import() losslessly.
Built-in profiles can be exported; their function fields (gate,
curve closure form) are stripped from the exported JSON.
Parameters
namestring— Profile name to export.vfsPathstring— Destination VFS path.
Returns (boolean, string?) — (true) on success or (false, err).
Zin.profile.export("wasd-arrows", "/source/profiles/backup.json")
typed/builtin//modules/zinput/profile/M/get
M.get(name: string?) -> any?
Active profile descriptor (no arg) or the named profile (with arg).
Returns nil if missing.
Parameters
namestring(optional) — Optional profile name; omit to query the active profile.
Returns any? — The profile descriptor, or nil.
local p = Zin.profile.get()
local p = Zin.profile.get("wasd-arrows")
typed/builtin//modules/zinput/profile/M/import
M.import(vfsPath: string, opts: ImportOpts?) -> (boolean, string)
Read a profile JSON from a VFS path and register() it. Does
NOT activate it — pass opts.activate = true (or call
activate(name) separately) to make it current.
Parameters
vfsPathstring— Source VFS path.optsImportOpts(optional) — Optional{ overrideName, activate }.
Returns (boolean, string) — (true, registeredName) on success or (false, err).
Zin.profile.import("/source/profiles/backup.json")
Zin.profile.import(path, { overrideName = "v2", activate = true })
typed/builtin//modules/zinput/profile/M/list
M.list() -> { string }
Built-in and user profile names, sorted and deduplicated.
Returns { string } — Sorted array of profile names.
local names = Zin.profile.list()
typed/builtin//modules/zinput/profile/M/load
M.load(name: string) -> (boolean, any?)
Load and register a profile. Tries @builtin::profiles.<name>
first (Luau module), then falls back to /zero/profiles/<name>.json.
Parameters
namestring— Profile name to load.
Returns (boolean, any?) — (true, profile) on success, (false, err) on failure.
local ok, p = Zin.profile.load("wasd-arrows")
typed/builtin//modules/zinput/profile/M/off
M.off(handle: number)
Drop a subscription registered via onChange. No-op if the
handle is unknown.
Parameters
handlenumber— Subscription handle returned byonChange.
Zin.profile.off(h)
typed/builtin//modules/zinput/profile/M/onChange
M.onChange(cb: ChangeCallback) -> number
Subscribe to active-profile changes. Callback fires with
(profileName, profileDescriptor) on activate and on register
of the currently active profile.
Parameters
cbChangeCallback— Callback(name: string, profile: any) -> ().
Returns number — A handle for off.
local h = Zin.profile.onChange(function(name, p) ... end)
typed/builtin//modules/zinput/profile/M/register
M.register(name: string, profile: any?) -> (boolean, string?)
Register a profile table. Validates the shape; replaces any prior registration under the same name. If the registered name is currently active, subscribers are notified (so editor UIs refresh).
Parameters
namestring— Non-empty profile name.profileany(optional) — Profile descriptor.
Returns (boolean, string?) — (ok, err) — (true) on success, (false, err) on validation failure.
Zin.profile.register("wasd-arrows", profile)
typed/builtin//modules/zinput/profile/M/save
M.save(name: string, opts: SaveOpts?) -> (boolean, string?)
Capture the current Zin.actions / Zin.axes / Zin.chords state to
a JSON file under /zero/profiles/<name>.json. Refuses to overwrite
a built-in profile name or an existing user profile (unless
opts.overwrite = true). Functions (axis gate, function-valued
curve) are dropped — only data-typed fields persist.
Parameters
namestring— Non-empty profile name.optsSaveOpts(optional) — Optional{ description, overwrite }.
Returns (boolean, string?) — (true) on success or (false, err).
Zin.profile.save("my-bindings", { description = "..." })
typed/builtin//modules/zinput/rebind/M/_advanceTime
M._advanceTime(dt: number)
Internal: per-frame timeout sweep. Called by Zin.tick after the event pump so timed-out sessions retire on the same frame their budget runs out, before the next observed event.
Parameters
dtnumber— Frame delta in seconds.
Zin.rebind._advanceTime(1 / 60)
typed/builtin//modules/zinput/rebind/M/_settled
M._settled() -> boolean
Internal: whether no rebind capture session is live. The tick's quiescence gate reads it.
Returns boolean — true when nothing is listening for a binding.
if Zin.rebind._settled() then ... end
typed/builtin//modules/zinput/rebind/M/begin
M.begin(opts: BeginOpts?) -> Session
Start a new capture session. Returns the session table. The
session auto-subscribes to Zin.events.on so once Zin.tick runs
the first matching event commits the capture without the caller
having to pump consume manually.
Parameters
optsBeginOpts(optional) — Capture options —target("action"(default) /"axis"),name(required),slot,mode("replace"(default) /"append"),timeoutSec,filter,applyOnCommit,onCommit.
Returns Session — The session table — read status and result() after a tick or call cancel() / apply() / conflicts().
local s = Zin.rebind.begin({ target = "action", name = "jump" })
typed/builtin//modules/zinput/rebind/M/cancelAll
M.cancelAll()
Cancel every live session. Test-only / shutdown.
Zin.rebind.cancelAll()
typed/builtin//modules/zinput/rebind/M/liveCount
M.liveCount() -> number
Live sessions count; primarily for test introspection.
Returns number — Number of capturing sessions still active.
local n = Zin.rebind.liveCount()
typed/builtin//modules/zinput/scheme/M/_advance
M._advance(dt: number)
One tick of binding dispatch: evaluate every live binding and fire
what changed. Wired into Zin.tick.
input fires every frame a binding is active, and once more on the
frame it goes inactive carrying the neutral value and active = false
— so one handler both starts and stops the motion it drives.
A button already held when its map comes up reads held from the first
tick, so input reports it active. Its press came before the control
did, so neither the press nor the release of that hold fires
pressed or released; the next press is the control's own.
Evaluation is per tick, so smoothing keeps converging when a host runs
several ticks inside one engine frame, and changed reports each step
of it. input, pressed and released are per engine frame: one
active call and one of each edge, whatever the tick cadence above it.
Parameters
dtnumber— Seconds since the previous tick.
Zin.scheme._advance(1 / 60)
typed/builtin//modules/zinput/scheme/M/_childBindings
M._childBindings(mapRef: any?) -> { any }
Internal: the <name>.inputBinding/ children of a map asset, as
resolved refs in name order.
Parameters
mapRefany(optional) — The inputMap asset ref.
Returns { any } — Array of inputBinding refs.
local kids = Zin.scheme._childBindings(mapRef)
typed/builtin//modules/zinput/scheme/M/_gateAllows
M._gateAllows(record: any?) -> (boolean, string?, string?)
Internal: run a control's own gate. Returns whether it allows the
control to read, and when it does not, which of gateRefused /
gateErrored happened and the error text if one was raised.
Parameters
recordany(optional) — The binding record.
Returns (boolean, string?, string?) — (allowed, reason?, error?).
local ok, why = Zin.scheme._gateAllows(record)
typed/builtin//modules/zinput/scheme/M/_readClasses
M._readClasses(record: any?, dt: number, surface: string?) -> any
Internal: read every device class a control's record carries, in the control's own unit, WITHOUT its gate and without its deadzone / curve / invert shaping. What the devices produced before the control decided what to do with it.
Parameters
recordany(optional) — The binding record.dtnumber— Seconds since the previous tick.surfacestring(optional) — Whose keys to read:"scene"(the default) or"editor".
Returns any — The unshaped reading, in the record's kind.
local raw = Zin.scheme._readClasses(record, 1 / 60)
typed/builtin//modules/zinput/scheme/M/_reset
M._reset()
Test-only: drop every live map without firing anything.
Zin.scheme._reset()
typed/builtin//modules/zinput/scheme/M/_settled
M._settled() -> boolean
Internal: whether every live control sits at rest -- nothing active, nothing waiting for its holder to release, every smoothed reading at neutral. The tick's quiescence gate reads it: a scan with an early exit, so an idle layer answers in the cost of a comparison per control rather than an evaluation.
Returns boolean — true when no control could change without new input.
if Zin.scheme._settled() then ... end
typed/builtin//modules/zinput/scheme/M/activate
M.activate(mapRef: any?, label: string?) -> { [string]: Handle }
Activate an inputMap asset: load every <name>.inputBinding/ child
it contains, validate them, add them to the live binding set, and
return the handles a controller subscribes through — one per binding,
keyed by the binding's name.
Every fault across every child is reported in one error, so a map with three broken bindings names all three rather than one per attempt.
Activating a map already live adds a holder rather than registering
the map twice, so two components sharing one map read one set of
controls. Each holder gets its OWN handles: every subscription made
through them belongs to that activation, and passing them back to
deactivate releases exactly those.
Every activation reads the map's children again, so a binding added, edited or removed since the map came up reaches the running set here — and reaches the holders already standing on it, whose handles and subscriptions carry on across the edit.
label names this activation in Zin.scheme.live(), beside where it
was opened. A probe that takes a map to look at it says so, and the
agent whose controls the map suppresses can read who is holding it.
Parameters
mapRefany(optional) — The inputMap asset ref.labelstring(optional) — What to call this activation. Optional.
Returns { [string]: Handle } — A table of handles keyed by binding name — this holder's claim.
local map = public.inputMap:activate(); map.jump:onPressed(fn)
typed/builtin//modules/zinput/scheme/M/advanceCost
M.advanceCost() -> any
What the most recent tick of binding dispatch cost, and how much it
covered: { frameId, maps, controls, ms }. ms is ONE tick's own
milliseconds — the same number profiler.stats("*zin.scheme.advance*")
reports as script.zin.scheme.advance, where it also carries the average
and the peak across every tick since the engine started.
Returns any — { frameId, maps, controls, ms }.
local cost = Zin.scheme.advanceCost()
typed/builtin//modules/zinput/scheme/M/advances
M.advances() -> number
How many evaluations of the live controls have run since the module loaded. A caller that drove a control waits for it to advance to know an evaluation has read the driven value.
Returns number — The evaluation count.
local before = Zin.scheme.advances()
typed/builtin//modules/zinput/scheme/M/bindings
M.bindings() -> { any }
Every live binding, in activation order, as
{ map, group, name, label, kind, context, suppressedBy, subscribers }.
The touch overlay builds its buttons from this, so what is on screen is
exactly what some awake component asked for and nothing is standing
down.
Returns { any } — Array of binding descriptors.
for _, b in ipairs(Zin.scheme.bindings()) do print(b.label) end
typed/builtin//modules/zinput/scheme/M/classBindings
M.classBindings(record: any?, class: string) -> { any }
Every binding a control declares for one device class, as the tick reads them.
Parameters
recordany(optional) — The binding record.classstring— One ofkbm/gamepad/touch.
Returns { any } — Array of binding descriptors.
local kbm = Zin.scheme.classBindings(record, "kbm")
typed/builtin//modules/zinput/scheme/M/deactivate
M.deactivate(mapRef: any?, claim: any?) -> boolean
Release one holder's claim on a map, disconnecting every subscription that claim made. The other holders keep theirs, and the map stays live while any of them remains; the last one out also takes the map's controls out of the live set.
A claim is the handle table activate returned. Hand it back and that
activation is what goes. Name none and the caller's own most recent
claim goes — a component releases what IT activated, whatever else has
taken the map since. A component holding none of them releases nothing;
an execute holding none of them releases the map's most recent claim.
An asset ref is shared by every holder of the same map, so the handles
are what say which of them is releasing.
Parameters
mapRefany(optional) — The inputMap asset ref.claimany(optional) — The handles this holder'sactivatereturned. Optional.
Returns boolean — True when a claim was released.
public.inputMap:deactivate(controls)
typed/builtin//modules/zinput/scheme/M/declaredClasses
M.declaredClasses(record: any?) -> { string }
Which device classes a control declares bindings for, in the order the tick consults them. A control with no binding for the class a player is driving reads nothing from that class no matter what they do.
Parameters
recordany(optional) — The binding record.
Returns { string } — Array of class names from kbm / gamepad / touch.
local classes = Zin.scheme.declaredClasses(record)
typed/builtin//modules/zinput/scheme/M/drive
M.drive(name: string, value: any?)
Drive a live control directly with the value the game should read —
the gameplay intent a game is authored in, rather than the key a map
happens to bind. true holds a button (false lets it go), a number
drives a 1-D axis, { x, y } a 2-D one. The value joins the control's
device classes like another device: shaping and smoothing apply, press
and release edges fire, and a context or suppression that stands the
control down stands it down too. It reaches every live map declaring a
control of that name, and stays until Zin.scheme.undrive lets it go.
It touches no device, so a person at the window — a focused panel, the
pointer — neither reaches it nor is reached by it. Raises for a name no
live map declares, listing the live controls, and for a value the
control's kind cannot take.
Parameters
namestring— The control's name, asZin.scheme.bindings()lists it.valueany(optional) —true/falsefor a button, a number for a 1-D axis,{ x, y }for a 2-D axis.
Zin.scheme.drive("move", { 0, 1 }) -- walk forward
Zin.scheme.drive("jump", true); Zin.scheme.undrive("jump")
typed/builtin//modules/zinput/scheme/M/drivenControls
M.drivenControls() -> { [string]: any }
The controls being driven now, each with its driven value.
Returns { [string]: any } — { [name] = value }.
for name, v in Zin.scheme.drivenControls() do print(name, v) end
typed/builtin//modules/zinput/scheme/M/drivingClass
M.drivingClass(record: any?, surface: string?) -> string?
Which device class is currently satisfying a binding, or nil when
nothing is. Used to tag what fired reports.
Parameters
recordany(optional) — The binding record.surfacestring(optional) — Whose keys to read:"scene"(the default) or"editor".
Returns string? — The class name, or nil.
local class = Zin.scheme.drivingClass(record)
typed/builtin//modules/zinput/scheme/M/entriesFor
M.entriesFor(name: string) -> { any }
Every live map that declares a named control, with the map's own record and the control's dispatch state. The lookup behind per-control questions: a name can be declared by more than one live map, and this answers with all of them in activation order.
Parameters
namestring— The control name.
Returns { any } — Array of { map, group, guid, record, state, suppressedBy }.
local entries = Zin.scheme.entriesFor("look")
typed/builtin//modules/zinput/scheme/M/fired
M.fired(peek: boolean?) -> { any }
Every control that fired since this last cleared, with how many
engine frames it was active for, which device classes drove it, and
what received it. Reading CLEARS the record unless peek is true, so
two calls around an action answer "did that input reach the game"
without a previous test's results bleeding in.
One row per LIVE MAP that fired the control, so a name two maps both declare reports each of them under its own map with its own count.
Parameters
peekboolean(optional) — Read without clearing.
Returns { any } — Array of { name, map, guid, count, classes, subscribers, delivered }, sorted by name and then by map. map is the map's name and guid is that map's own guid, which is what tells two live maps sharing a name apart. count is the number of engine frames the control was active, so a button held across two frames reads 2 for one press. The pressed / released subscriptions are what count edges. subscribers is how many the control had on its most recent firing frame and delivered says one of those frames reached a subscriber, so a press that landed reads apart from one that reached nothing.
local fired = Zin.scheme.fired()
typed/builtin//modules/zinput/scheme/M/generation
M.generation() -> number
A counter that changes whenever the live set of maps changes. Cache a view of the live bindings against it rather than rebuilding one every frame.
Returns number — The current generation.
if gen ~= Zin.scheme.generation() then print("the input scheme changed") end
typed/builtin//modules/zinput/scheme/M/has
M.has(name: string) -> boolean
Whether a named control is live in any activated map.
Parameters
namestring— The binding name.
Returns boolean — True when some live map declares it.
if Zin.scheme.has("jump") then ... end
typed/builtin//modules/zinput/scheme/M/lastFrame
M.lastFrame() -> { any }
What every live control did on the most recent tick: its value, whether it was active, whether that reached a subscriber, how many subscribers it has, and the tick's own answers about context, suppression and its gate.
Rebuilt whole each tick and read without consuming, so any number of observers in the same frame all see the same answers. The window is ONE tick — the most recent one — rather than a sum since anything last read.
Returns { any } — Array of { name, map, group, kind, label, context, value, active, delivered, subscribers, suppressedBy, inContext, heldOnArrival, gateReason, gateError }, in evaluation order.
for _, c in ipairs(Zin.scheme.lastFrame()) do print(c.name, c.active) end
typed/builtin//modules/zinput/scheme/M/live
M.live() -> { any }
Every live map, in activation order:
{ guid, name, group, suppresses, suppressedBy, pointerLock, surface, holders, heldBy, bindings } where bindings names the controls it
contributes, holders counts the activations still standing on it,
suppressedBy names the map standing this one down, if any,
pointerLock says whether it takes the cursor while the world runs, and
surface is whose keys its controls read: "scene", the keys pressed
while the scene was the active input context, or "editor".
What a debug surface lists, and what tells an author why two sticks
are on screen — or why the controls they activated answer to nothing.
heldBy is one record per activation still standing:
{ kind, entity, instance, source, label, holder, subscriptions }.
kind is "component" for an activation a component script made and
"execute" for one made from a bare call, source is the
chunk:line it was opened at, and holder says the same thing in a
sentence. A map that suppresses a group is somebody's activation, and
this is whose — the reading an agent whose controls went silent takes
to find the activation standing them down.
Returns { any } — Array of live-map records.
for _, m in ipairs(Zin.scheme.live()) do print(m.name) end
typed/builtin//modules/zinput/scheme/M/liveCount
M.liveCount() -> number
How many maps are live right now.
Returns number — The count of activated maps.
if Zin.scheme.liveCount() == 0 then ... end
typed/builtin//modules/zinput/scheme/M/markBaseline
M.markBaseline() -> number
Record the claims standing on every live map right now, as the mark
Zin.scheme.resetToBaseline takes back to. A caller running work in an
engine it shares, such as a sweep of test suites, marks before the work and
resets after it.
Returns number — The mark.
local mark = Zin.scheme.markBaseline()
typed/builtin//modules/zinput/scheme/M/neutralFor
M.neutralFor(kind: string) -> any
The neutral value for a control kind: false for a button, 0 for
an axis1, { x = 0, y = 0 } for an axis2. What a control reports
when it is not delivering.
Parameters
kindstring— The control kind.
Returns any — The kind's neutral value.
local rest = Zin.scheme.neutralFor("axis2")
typed/builtin//modules/zinput/scheme/M/recording
M.recording() -> boolean
Whether the tick is keeping a per-control record of what it did.
Zin.tick turns it on while something is observing the input layer and
off again when nothing is.
Returns boolean — Whether the record is being built.
if Zin.scheme.recording() then ... end
typed/builtin//modules/zinput/scheme/M/resetToBaseline
M.resetToBaseline(mark: number) -> number
Release every execute claim opened since mark. A component's
claim answers to the component: it goes when the component releases it
or when that component instance ends (removed, hot-reloaded, or its
entity despawned), whichever comes first.
Parameters
marknumber— WhatZin.scheme.markBaselinereturned.
Returns number — How many claims were released.
Zin.scheme.resetToBaseline(mark)
typed/builtin//modules/zinput/scheme/M/setRecording
M.setRecording(on: boolean?)
Keep — or stop keeping — a per-control record of what each tick did.
A tick that is not recording still dispatches and still reports its
cost; it only stops writing down each control's outcome, which
Zin.observe can resolve again from live state.
Parameters
onboolean(optional) — Record (the default) or stop recording.
Zin.scheme.setRecording(true)
typed/builtin//modules/zinput/scheme/M/subscriberCount
M.subscriberCount(name: string) -> number
How many subscribers a named control currently holds, summed over every live map that declares it.
A control can be live, valid and firing with nobody listening. Drawing a button for one offers a player something that cannot do anything — which is the shape of every on-screen control that has ever been reported as doing nothing.
Parameters
namestring— The binding name.
Returns number — The number of live subscribers.
if Zin.scheme.subscriberCount("jump") == 0 then ... end
typed/builtin//modules/zinput/scheme/M/subscriberEpoch
M.subscriberEpoch() -> number
Total subscribers across every live control.
Which controls are HEARD changes without the live set changing at all —
a component activates a map and subscribes a moment later, or drops its
last listener while staying awake. A consumer that caches a view of the
live set by generation alone never sees either, so this is the second
half of that cache key.
Returns number — The summed subscriber count.
if gen ~= Zin.scheme.generation() or subs ~= Zin.scheme.subscriberEpoch() then print("the input scheme changed") end
typed/builtin//modules/zinput/scheme/M/suppressed
M.suppressed() -> { [string]: string }
Which groups are currently standing down, and the map that put each
one down: { [group] = mapName }.
The answer to "the button is gone and the control is live" — a control in a suppressed group reads nothing and draws nothing until whatever suppressed it releases.
Returns { [string]: string } — A table of group name → the name of the map suppressing it.
local down = Zin.scheme.suppressed()
typed/builtin//modules/zinput/scheme/M/undrive
M.undrive(name: string?)
Let go of a driven control — or of every driven control when no name is given. The control reads its devices alone again; a driven button fires its release edge on the next evaluation.
Parameters
namestring(optional) — The control to let go; nil for all of them.
Zin.scheme.undrive("move")
Zin.scheme.undrive()
typed/builtin//modules/zinput/scheme/M/valueIsActive
M.valueIsActive(kind: string, value: any?) -> boolean
Whether a value of the given kind is anything other than that kind's neutral — a held button, an axis off centre.
Parameters
kindstring— The control kind.valueany(optional) — The value to test.
Returns boolean — Whether the value counts as active.
if Zin.scheme.valueIsActive("axis1", v) then ... end
typed/builtin//modules/zinput/state/M/_idle
M._idle() -> boolean
Internal: whether the devices are at rest this frame — nothing held, nothing moving, the pointer not locked. The tick's quiescence gate reads it: a held key or a locked pointer drives controls without producing an event, so a frame with live raw input is evaluated whatever the event count says.
Returns boolean — true when no device carries live input this frame.
if Zin.state._idle() then ... end
typed/builtin//modules/zinput/state/M/_observeEvent
M._observeEvent(ev: any?)
Internal: per-event observer called by the Zin.tick coordinator.
Updates held-time state for keys and mouse buttons. Not part of the
public contract — exposed on M so the tick coordinator can wire it.
Parameters
evany(optional) — The raw input event record (kind, code, button, repeat, …).
Zin.state._observeEvent(ev)
typed/builtin//modules/zinput/state/M/_reset
M._reset()
Test-only: clear all held-time / repeat state. Public so test suites can isolate cases.
Zin.state._reset()
typed/builtin//modules/zinput/state/M/_setEnsureLiveFn
M._setEnsureLiveFn(fn: () -> ())
Internal: wire the liveness hook. Called once at module-load
time from zinput/init.luau.
Parameters
fn() -> ()— The hook invoked on every read below.
M._setEnsureLiveFn(ensureInputLive)
typed/builtin//modules/zinput/state/M/gamepadCapable
M.gamepadCapable() -> boolean
Has this session ever seen a gamepad? Latched by the first connection, so unplugging a pad does not flip a scheme's prompts back to keyboard glyphs on a cable knock.
Returns boolean — True once a pad has connected.
if Zin.state.gamepadCapable() then ... end
typed/builtin//modules/zinput/state/M/gamepads
M.gamepads() -> { any }
Every connected gamepad this frame, in slot order. Each entry is
{ slot, name, buttons, buttons_pressed, buttons_released, axes, simulated } — the button fields are arrays of canonical names, axes
a map of canonical axis name to number. Empty when nothing is
connected.
Returns { any } — Array of pad records.
for _, pad in ipairs(Zin.state.gamepads()) do print(pad.name) end
typed/builtin//modules/zinput/state/M/get
M.get() -> Snapshot
Get the full snapshot table for this frame. Empty table if
__zero_input.snapshot is missing or returns non-table.
Returns Snapshot — The snapshot table.
local snap = Zin.state.get()
typed/builtin//modules/zinput/state/M/getRepeatDefaults
M.getRepeatDefaults() -> RepeatDefaults
Read the current synthetic-repeat defaults. Returned table is a copy.
Returns RepeatDefaults — A fresh { delay, period } table.
local d = Zin.state.getRepeatDefaults()
typed/builtin//modules/zinput/state/M/keyDown
M.keyDown(key: string, surface: string?) -> boolean
Is key currently held? True on EVERY frame the key is down —
the level, not the edge. Use it for continuous input: thrust while
W is held, hold-to-charge, camera pan. For an action that should
happen once per press (fire, jump, pause, undo) bind it and read
Zin.actions.pressed(name), which fires on the press edge only.
A key belongs to the surface it was pressed on: the scene holds the
keys pressed while it was the active input context, so a key pressed
after a click on an editor panel is not the scene's. Editor code
passes "editor" to read every key no focused widget took.
Parameters
keystring— Web-style key code (e.g."KeyW","Space").surfacestring(optional) — Whose keys to read:"scene"(the default) or"editor".
Returns boolean — true when the key is held this frame.
if Zin.state.keyDown("KeyW") then ... end -- thrust while held
typed/builtin//modules/zinput/state/M/keyDownReal
M.keyDownReal(key: string) -> boolean
Is key held by a real press — held, and NOT (also) held by the
input-emulation floor? The floor merges emulated holds into keys
so keyDown can't tell them apart; this checks keys minus
keys_emulated.
Parameters
keystring— Web-style key code (e.g."KeyW","Space").
Returns boolean — true when the key is held this frame by a real press only.
if Zin.state.keyDownReal("KeyW") then ... end
typed/builtin//modules/zinput/state/M/keyHeldTime
M.keyHeldTime(code: string) -> number?
Seconds the given key has been held since its most recent press,
or nil if the key isn't currently held. Wall-clock based,
updated as events arrive via the tick.
Parameters
codestring— Web-style key code.
Returns number? — Held duration in seconds, or nil.
local t = Zin.state.keyHeldTime("KeyW")
typed/builtin//modules/zinput/state/M/keyPressed
M.keyPressed(key: string, surface: string?) -> boolean
Was key just pressed this frame?
Parameters
keystring— Web-style key code.surfacestring(optional) — Whose keys to read:"scene"(the default) or"editor".
Returns boolean — true on the press frame only.
if Zin.state.keyPressed("Space") then ... end
typed/builtin//modules/zinput/state/M/keyReleased
M.keyReleased(key: string, surface: string?) -> boolean
Was key just released this frame?
Parameters
keystring— Web-style key code.surfacestring(optional) — Whose keys to read:"scene"(the default) or"editor".
Returns boolean — true on the release frame only.
if Zin.state.keyReleased("KeyW") then ... end
typed/builtin//modules/zinput/state/M/keyRepeatFired
M.keyRepeatFired(code: string, opts: RepeatOpts?) -> boolean
Should a synthetic key-repeat fire this frame for code?
Returns true once per period, beginning delay seconds after the
initial press. Distinct from the repeat field on OS-level key.down
events (which surfaces hardware autorepeat — see Zin.events.on).
Each call advances per-key state, so call once per frame per logical
consumer to avoid "consuming" the repeat early.
Parameters
codestring— Web-style key code.optsRepeatOpts(optional) — Optional per-call{ delay, period }overrides; falls back togetRepeatDefaults().
Returns boolean — true on each synthetic-repeat tick.
if Zin.state.keyRepeatFired("KeyW") then ... end
typed/builtin//modules/zinput/state/M/mouseButtonDown
M.mouseButtonDown(button: (number | string)?) -> boolean
Is mouse button held? Accepts the 0-based index or a name —
0/"left", 1/"right", 2/"middle".
Parameters
button(number | string)(optional) — Button index or name (default 0 = left).
Returns boolean — true when that button is currently held.
if Zin.state.mouseButtonDown(0) then ... end
if Zin.state.mouseButtonDown("left") then ... end
typed/builtin//modules/zinput/state/M/mouseButtonHeldTime
M.mouseButtonHeldTime(button: (number | string)?) -> number?
Seconds the given mouse button has been held, or nil. Accepts the
0-based index or a name — 0/"left", 1/"right", 2/"middle".
Parameters
button(number | string)(optional) — Button index or name (default 0 = left).
Returns number? — Held duration in seconds, or nil.
local t = Zin.state.mouseButtonHeldTime("left")
typed/builtin//modules/zinput/state/M/mouseButtonPressed
M.mouseButtonPressed(button: (number | string)?) -> boolean
Was mouse button just pressed this frame? Accepts the 0-based index
or a name — 0/"left", 1/"right", 2/"middle".
Parameters
button(number | string)(optional) — Button index or name (default 0 = left).
Returns boolean — true on the press frame only.
if Zin.state.mouseButtonPressed("left") then ... end
typed/builtin//modules/zinput/state/M/mouseButtonReleased
M.mouseButtonReleased(button: (number | string)?) -> boolean
Was mouse button just released this frame? Accepts the 0-based index
or a name — 0/"left", 1/"right", 2/"middle".
Parameters
button(number | string)(optional) — Button index or name (default 0 = left).
Returns boolean — true on the release frame only.
if Zin.state.mouseButtonReleased("right") then ... end
typed/builtin//modules/zinput/state/M/mouseDelta
M.mouseDelta() -> (number, number)
Mouse delta since last frame. Returns (dx, dy) as two numbers.
Returns (number, number) — Two numbers — dx, dy. Both default to 0.0 when unavailable.
local dx, dy = Zin.state.mouseDelta()
typed/builtin//modules/zinput/state/M/mouseInViewport
M.mouseInViewport() -> boolean
True while the mouse sits inside the active scene viewport's rect
(always true when no viewport widget is on screen). Reads beside
mousePosition(), which is viewport-local when a viewport is active.
Returns boolean — Whether the pointer is inside the scene viewport.
if Zin.state.mouseInViewport() and Zin.state.mouseButtonPressed(0) then ... end
typed/builtin//modules/zinput/state/M/mousePosition
M.mousePosition() -> (number, number)
Current mouse position. Returns (x, y) as two numbers.
Returns (number, number) — Two numbers — x, y. Both default to 0.0 when unavailable.
local x, y = Zin.state.mousePosition()
typed/builtin//modules/zinput/state/M/padAxis
M.padAxis(axis: string, slot: number?) -> number
A canonical pad axis's value. Stick axes run -1..1 with y
screen-down positive (matching the touch stick); trigger axes run
0..1. With no slot the reading furthest from rest across every pad
wins, so a second pad resting at zero never cancels the one being
used.
Parameters
axisstring— Canonical name: left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger, right_trigger.slotnumber(optional) — Pad slot, or nil for any pad.
Returns number — The axis value, 0 when no pad reports it.
local x = Zin.state.padAxis("left_stick_x")
typed/builtin//modules/zinput/state/M/padDown
M.padDown(button: string, slot: number?) -> boolean
Is a canonical pad button held this frame? With no slot, ANY
connected pad holding it counts — which is what a single-player scheme
wants, since whichever controller the person picked up drives the
action with no pairing step. Name a slot for local multiplayer.
Parameters
buttonstring— Canonical name: south, east, west, north, left_shoulder, right_shoulder, left_trigger, right_trigger, left_stick, right_stick, select, start, guide, dpad_up, dpad_down, dpad_left, dpad_right.slotnumber(optional) — Pad slot, or nil for any pad.
Returns boolean — True when held.
if Zin.state.padDown("south") then ... end
typed/builtin//modules/zinput/state/M/padPressed
M.padPressed(button: string, slot: number?) -> boolean
Did a canonical pad button go down this frame? Same slot rule as
padDown.
Parameters
buttonstring— Canonical button name.slotnumber(optional) — Pad slot, or nil for any pad.
Returns boolean — True on the press frame.
if Zin.state.padPressed("start") then ... end
typed/builtin//modules/zinput/state/M/padReleased
M.padReleased(button: string, slot: number?) -> boolean
Did a canonical pad button come up this frame? Same slot rule as
padDown.
Parameters
buttonstring— Canonical button name.slotnumber(optional) — Pad slot, or nil for any pad.
Returns boolean — True on the release frame.
if Zin.state.padReleased("south") then ... end
typed/builtin//modules/zinput/state/M/pointerLocked
M.pointerLocked() -> boolean
Is the pointer currently locked?
Returns boolean — true when the pointer is locked.
if Zin.state.pointerLocked() then ... end
typed/builtin//modules/zinput/state/M/sceneContextActive
M.sceneContextActive() -> boolean
True while the SCENE is the active input context — the last pointer gesture began on a scene viewport (or pointer lock holds), so continuous scene input (camera fly, buttons, wheel) belongs to the scene. A gesture that begins on a widget, or a text field taking the caret, hands the context to the UI until the scene is clicked again. With no viewport widget on screen the scene is the whole surface and this is always true.
Returns boolean — Whether the scene holds the active input context this frame.
if Zin.state.sceneContextActive() then cam:fly(dt) end
typed/builtin//modules/zinput/state/M/scrollDelta
M.scrollDelta() -> number
Mouse wheel delta this frame.
Returns number — The signed scroll delta, or 0.0 when unavailable.
local dy = Zin.state.scrollDelta()
typed/builtin//modules/zinput/state/M/setRepeatDefaults
M.setRepeatDefaults(opts: RepeatOpts?)
Set the global default delay / period for synthetic key repeat. Either field is optional; omitted fields keep their current value. Default: delay = 0.4 s, period = 0.1 s.
Parameters
optsRepeatOpts(optional) — Partial overrides fordelay/period(seconds).
Zin.state.setRepeatDefaults({ delay = 0.5, period = 0.05 })
typed/builtin//modules/zinput/state/M/uiTookPress
M.uiTookPress() -> boolean?
Whether a UI widget holds the pointer press in progress: true
when the press landed on a widget that answers for the pointer (a
button, a slider, anything sensing a click or a drag). false with no
press held, and for a press on empty space, which a HUD's own boxes do
not take, so it tells a press the game should see from one a widget is
handling. nil from the moment a press arrives until a UI pass has run
to decide it.
Returns boolean? — true while a widget holds the press, nil while undecided.
if Zin.state.uiTookPress() == true then return end
typed/builtin//modules/zinput/state/M/uiWantsKeyboard
M.uiWantsKeyboard(space: string?) -> boolean
True while the UI layer holds keyboard focus — a text field has the
caret, so keys reaching the window belong to it. Travels separately
from pointer focus: a caret in a field takes keys while the cursor sits
over the viewport, and a hovered panel takes clicks while no field has
the caret. Two UIs can hold it: the game's own, drawn in the game view
("camera" space — a chat box, a name entry), and the editor's, drawn in
the window around it ("window" space — a panel's field, an agent's
terminal). Name a space to ask about one of them; a game asking whether
its own UI is taking the keys passes "camera".
Parameters
spacestring(optional) —"camera"for the game's UI,"window"for the editor's; nil for either.
Returns boolean — Whether that UI holds keyboard focus.
if Zin.state.uiWantsKeyboard("camera") then return end -- the chat box has the keys
typed/builtin//modules/zinput/state/M/uiWantsPointer
M.uiWantsPointer() -> boolean
True while the UI layer holds pointer focus — a widget is under the cursor, so what the pointer produces belongs to it. Travels separately from keyboard focus: a hovered panel takes clicks while no field has the caret, and a caret in a field takes keys while the cursor sits over the viewport. Useful for gating game-side input when a UI is hovered.
Returns boolean — true when the UI has pointer focus this frame.
if not Zin.state.uiWantsPointer() then ... end
typed/builtin//modules/zinput/surface/M/_beginFrame
M._beginFrame()
Internal: clear this frame's per-class flags. On the first tick of the session, also captures the capability default as the reported class the change signal compares against. Called by the Zin.tick coordinator on the first tick of each engine frame.
Zin.surface._beginFrame()
typed/builtin//modules/zinput/surface/M/_observeEvent
M._observeEvent(ev: any?)
Internal: per-event observer called by the Zin.tick coordinator. Collects which device classes produced events this frame.
Parameters
evany(optional) — The raw input event record.
Zin.surface._observeEvent(ev)
typed/builtin//modules/zinput/surface/M/_reset
M._reset()
Test-only: clear resolution state and subscriptions. Suite isolation; not part of the public contract.
Zin.surface._reset()
typed/builtin//modules/zinput/surface/M/_resolveFrame
M._resolveFrame()
Internal: resolve this frame's class and fire onChange on a flip. Touch wins a frame containing any touch event (the primary contact's projected mouse events never mask their own touch).
Zin.surface._resolveFrame()
typed/builtin//modules/zinput/surface/M/_settled
M._settled() -> boolean
Internal: whether this frame carried no class-bearing device event. The tick's quiescence gate reads it.
Returns boolean — true when nothing arrived to resolve a surface class from.
if Zin.surface._settled() then ... end
typed/builtin//modules/zinput/surface/M/current
M.current() -> string
The active input-surface class: "touch", "kbm" or
"gamepad". A forced class wins; then event history; before any
input it defaults by capability (touch sessions start as "touch").
Returns string — The surface class string.
if Zin.surface.current() == "touch" then ... end
typed/builtin//modules/zinput/surface/M/force
M.force(class: string?) -> string
Pin the surface class, or hand control back to real input.
This is how a desktop session is checked as a phone: forcing
"touch" mounts the on-screen controls exactly as a device would, so
the layout can be read and its buttons tapped. Nothing else about the
session changes — the pin decides which class is reported, and every
consumer that branches on it follows.
Parameters
classstring(optional) —"kbm"|"touch"|"gamepad", or"auto"/ nil to release the pin.
Returns string — The class in effect after the call.
Zin.surface.force("touch")
Zin.surface.force("auto")
typed/builtin//modules/zinput/surface/M/forced
M.forced() -> string?
Whether the class is currently pinned by force rather than
resolved from real input.
Returns string? — The pinned class, or nil.
if Zin.surface.forced() ~= nil then ... end
typed/builtin//modules/zinput/surface/M/off
M.off(handle: number) -> boolean
Unsubscribe an onChange handle. Returns true when removed.
Parameters
handlenumber— The handle from onChange.
Returns boolean — Whether a subscription was removed.
Zin.surface.off(h)
typed/builtin//modules/zinput/surface/M/onChange
M.onChange(fn: (string, string?) -> ()) -> number
Subscribe to surface-class changes. fn(now, prev) fires from
the tick pipeline on the frame the class flips; prev is the
previously reported class (the first-tick capability default when
no event had resolved yet).
Parameters
fn(string, string?) -> ()— The callback.
Returns number — A numeric handle for Zin.surface.off.
local h = Zin.surface.onChange(function(now) print(now) end)
typed/builtin//modules/zinput/test/M/action
M.action(name: string, value: any?, duration: number?)
Perform a gameplay action through the control the game reads it
from: drive the control named name with value, hold it for
duration seconds, and let it go. A button with no value is tapped —
held until an evaluation of the input maps has read it, then released.
Returns once the game has read the release. The control must be live
(Zin.scheme.bindings() lists them); the value is true for a button,
a number for a 1-D axis, { x, y } for a 2-D axis. No device is
touched, so a person's focus neither takes the action nor receives it.
Parameters
namestring— The control's name ("jump","move","sprint").valueany(optional) — What the control reads while held;truewhen omitted.durationnumber(optional) — Seconds to hold it; one evaluation when omitted.
Zin.test.action("jump")
Zin.test.action("move", { 0, 1 }, 2) -- walk forward for 2 s
typed/builtin//modules/zinput/test/M/clickMouse
M.clickMouse(button: (number | string)?, x: number?, y: number?, duration: number?, target: string?)
Press + release a mouse button, in the order every mouse call here
takes its arguments: which button, where, how long. x and y move
the cursor before pressing; duration holds the button for that many
seconds before releasing. Does NOT tick — call Zin.tick(dt) between
clickMouse and your assertion if you need binding-action handlers
to fire.
Parameters
button(number | string)(optional) — Button index or name, the same pairpressMousetakes (default 0 = left).xnumber(optional) — Window x to move the cursor to first, in physical pixels.ynumber(optional) — Window y to move the cursor to first, in physical pixels.durationnumber(optional) — Hold seconds between press and release.targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.clickMouse()
Zin.test.clickMouse(nil, 100, 200)
Zin.test.clickMouse("right", 100, 200, 0.2)
Zin.test.clickMouse(nil, 100, 200, nil, "ui") -- a click on whatever widget is there
typed/builtin//modules/zinput/test/M/connectPad
M.connectPad(name: string?) -> number
Connect a simulated pad and return the slot it took. name is the
device name the pad reports, which decides the button legends a
prompt draws — pass a real product name to check a scheme's prompts
on that family.
Parameters
namestring(optional) — Device name; "Simulated Gamepad" when omitted.
Returns number — The slot the pad occupies.
Zin.test.connectPad("Xbox Wireless Controller")
typed/builtin//modules/zinput/test/M/disconnectPad
M.disconnectPad(slot: number?)
Disconnect the pad in slot. Anything it still held is released
first, so a held control ends rather than sticking.
Parameters
slotnumber(optional) — The pad slot; 0 when omitted.
Zin.test.disconnectPad(0)
typed/builtin//modules/zinput/test/M/dragLook
M.dragLook(dx: number, dy: number, steps: number?)
Drag across the on-screen look zone by (dx, dy) pixels, spread
over steps frames so the per-frame deltas a look binding reads are
real rather than one impossible jump.
Parameters
dxnumber— Total horizontal travel in pixels.dynumber— Total vertical travel in pixels.stepsnumber(optional) — Frames to spread it over; 8 when omitted.
Zin.test.dragLook(200, 0)
typed/builtin//modules/zinput/test/M/engage
M.engage()
Explicit opt-out for suites/harnesses that own their own tick
cadence but haven't injected synthetic input yet — call before the
first Zin.state / Zin.actions / Zin.axes read so the liveness
auto-arm doesn't start Zin.autoTick underneath the harness.
Zin.test.engage()
typed/builtin//modules/zinput/test/M/engaged
M.engaged() -> boolean
True once any synthetic test-input helper (pressKey,
touchStart, etc.) or engage() has run this VM. Read by the
liveness auto-arm so a test/harness VM keeps explicit control of
Zin.tick cadence instead of the first real input read starting
Zin.autoTick.
Returns boolean — Whether this VM has engaged synthetic test input.
if Zin.test.engaged() then ... end
typed/builtin//modules/zinput/test/M/focusScene
M.focusScene()
Hand continuous pointer input to the scene: the pointer the UI reads
and the one the scene reads both move to the middle of the scene's
viewport, and the scene becomes the active input
context, which is where a fresh VM's first simulated input already
finds it and where releaseAll puts it back. While a viewport widget
shares the screen with editor panels, the scene reads buttons, motion
and scroll only as the active context, and a gesture decides the
context by where it begins — so a test that has been clicking widgets
calls this before driving the scene again. Yields one frame.
Zin.test.focusScene()
typed/builtin//modules/zinput/test/M/held
M.held() -> Held
Everything the session is holding down right now, across every
device class: keys, mouse buttons, touch contacts, the on-screen
stick, and connected pads with the buttons they hold. atRest is
true when it holds nothing. keysEmulated is the subset of keys
the input-emulation floor holds, the same split
Zin.state.keyDownReal reads.
Read from the engine's live input state, so a hold whose caller is
gone — a driver task cancelled mid-press, a macro that errored — is
still reported and can still be let go. releaseAll lets go of what
this reports that the surface itself put down.
Returns Held — { atRest, keys, keysEmulated, mouse, contacts, stick, pads }.
local h = Zin.test.held(); if not h.atRest then Zin.test.releaseAll() end
typed/builtin//modules/zinput/test/M/holdAction
M.holdAction(name: string, value: any?)
Drive a control and leave it driven until releaseAction (or
releaseAll) lets it go. Returns once an evaluation has read it.
Parameters
namestring— The control's name.valueany(optional) — What it reads;truewhen omitted.
Zin.test.holdAction("sprint")
Zin.test.holdAction("move", { 1, 0 })
typed/builtin//modules/zinput/test/M/moveMouse
M.moveMouse(x: number, y: number, target: string?)
Move the cursor to a position on the window, in physical pixels —
the space a layout coordinate reaches through ui.pixelRatio(), and
the space camera.viewData().viewport states the scene's rect in.
Sets the absolute position; the engine computes mouse_delta as
new - old, so successive calls produce meaningful deltas for orbit /
look bindings.
Parameters
xnumber— Absolute window x coordinate, in physical pixels.ynumber— Absolute window y coordinate, in physical pixels.targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.moveMouse(100, 200)
typed/builtin//modules/zinput/test/M/moveMouseBy
M.moveMouseBy(dx: number, dy: number, steps: number?, target: string?)
Move the cursor by a relative motion, the way a mouse device
reports one: the motion lands in this frame's mouse_delta and the
position advances by the same amount. The same (dx, dy) repeated
keeps producing look movement, so an axis can be held or steered by
a controller issuing a correction each tick.
steps spreads the motion over that many frames, each carrying its
share of it, for a look sweep whose per-frame deltas are the ones a
binding would really read.
Parameters
dxnumber— Horizontal motion in screen pixels.dynumber— Vertical motion in screen pixels.stepsnumber(optional) — Frames to spread it over; 1 when omitted, which reports the whole motion in a single frame.targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.moveMouseBy(0, 140)
Zin.test.moveMouseBy(0, 140, 8)
typed/builtin//modules/zinput/test/M/padDown
M.padDown(button: string, slot: number?)
Push a canonical pad button down and leave it held.
Parameters
buttonstring— Canonical button name.slotnumber(optional) — The pad slot; 0 when omitted.
Zin.test.padDown("south")
typed/builtin//modules/zinput/test/M/padPress
M.padPress(button: string, duration: number?, slot: number?)
Press a canonical pad button — down, optionally held, then up.
Parameters
buttonstring— Canonical button name.durationnumber(optional) — Hold seconds; an instant press when omitted.slotnumber(optional) — The pad slot; 0 when omitted.
Zin.test.padPress("south")
typed/builtin//modules/zinput/test/M/padStick
M.padStick(stick: string, x: number, y: number, duration: number?, slot: number?)
Push a thumbstick to (x, y), each -1..1 with y screen-down
positive. Holds until pushed back to zero, or for duration seconds.
Parameters
stickstring— "left" or "right".xnumber— Deflection -1..1.ynumber— Deflection -1..1, screen-down positive.durationnumber(optional) — Hold seconds; left deflected when omitted.slotnumber(optional) — The pad slot; 0 when omitted.
Zin.test.padStick("left", 0, -1, 2)
typed/builtin//modules/zinput/test/M/padTrigger
M.padTrigger(trigger: string, value: number, duration: number?, slot: number?)
Squeeze a trigger to value, 0..1. Past half throw the trigger's
digital button latches too, so a scheme binding either the analog
travel or the button sees this.
Parameters
triggerstring— "left" or "right".valuenumber— Travel 0..1.durationnumber(optional) — Hold seconds; left squeezed when omitted.slotnumber(optional) — The pad slot; 0 when omitted.
Zin.test.padTrigger("right", 1)
typed/builtin//modules/zinput/test/M/padUp
M.padUp(button: string, slot: number?)
Release a canonical pad button.
Parameters
buttonstring— Canonical button name.slotnumber(optional) — The pad slot; 0 when omitted.
Zin.test.padUp("south")
typed/builtin//modules/zinput/test/M/pinch
M.pinch(amount: number, steps: number?)
Pinch two fingers together (negative amount) or spread them
apart (positive) by that many pixels, over steps frames.
Parameters
amountnumber— Pixels; negative pinches, positive spreads.stepsnumber(optional) — Frames to spread it over; 8 when omitted.
Zin.test.pinch(150)
typed/builtin//modules/zinput/test/M/pressKey
M.pressKey(code: string, target: string?)
Hold a key DOWN and leave it held until releaseKey. Returns
inside the frame where keys_pressed contains code, so
Zin.actions.pressed polls and onPressed handlers both fire as
the call returns. A second pressKey on a key that is still held
produces no new press edge — release first, or use tapKey for a
full press+release.
Parameters
codestring— Web-style key code (e.g."KeyM","Space").targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.pressKey("KeyM"); Zin.test.releaseKey("KeyM")
Zin.test.pressKey("KeyA", "ui") -- typed into the focused field
typed/builtin//modules/zinput/test/M/pressMouse
M.pressMouse(button: (number | string)?, target: string?)
Simulate a mouse button press. Accepts a 0-based index OR a
case-insensitive name ("left"/"right"/"middle", matching
Zin.bindings.mouse). Default: left.
Parameters
button(number | string)(optional) — Optional button index or name (default 0 = left).targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.pressMouse() -- left
Zin.test.pressMouse(1) -- right
Zin.test.pressMouse("right") -- right
typed/builtin//modules/zinput/test/M/pushStick
M.pushStick(x: number, y: number, duration: number?) -> number?
Push the on-screen movement stick to (x, y), each -1..1 with y
screen-down positive. Lands a contact in the stick's zone and lets
the overlay route it, so the deflection is one a player could
actually produce.
Parameters
xnumber— Deflection -1..1.ynumber— Deflection -1..1, screen-down positive. One finger drives the stick for the whole session: a second call moves the finger already down rather than adding another.durationnumber(optional) — Hold seconds, then release. Omitted, the stick STAYS deflected — release it withpushStick(0, 0)orreleaseStick().
Returns number? — The contact id while held, or nil once released.
Zin.test.pushStick(0, -1, 2)
Zin.test.pushStick(0, -1) ; ... ; Zin.test.releaseStick()
typed/builtin//modules/zinput/test/M/releaseAction
M.releaseAction(name: string?)
Let go of a control holdAction drove — or of every driven control
when no name is given. Returns once an evaluation has read the release.
Parameters
namestring(optional) — The control; nil for all of them.
Zin.test.releaseAction("sprint")
typed/builtin//modules/zinput/test/M/releaseAll
M.releaseAll() -> Released
Put the whole session down: drop the on-screen stick, lift every
touch contact, release every held key and mouse button, and
disconnect every pad connectPad connected — a controller plugged
into the machine stays connected, and held() goes on reporting it.
After it, nothing this surface put down is held, the pointer sits
over the middle of the scene's viewport and the scene holds the
input context (see focusScene).
Returns Released — { actions, keys, mouse, contacts, pads, stick } — how many of each was let go, and whether the on-screen stick was one of them.
Zin.test.releaseAll()
typed/builtin//modules/zinput/test/M/releaseKey
M.releaseKey(code: string, target: string?)
Simulate a key release. Returns inside the frame where
keys_released contains code.
Parameters
codestring— Web-style key code.targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.releaseKey("KeyM")
typed/builtin//modules/zinput/test/M/releaseMouse
M.releaseMouse(button: (number | string)?, target: string?)
Simulate a mouse button release. Accepts a 0-based index OR a
case-insensitive name ("left"/"right"/"middle"). Default: left.
Parameters
button(number | string)(optional) — Optional button index or name (default 0 = left).targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.releaseMouse()
Zin.test.releaseMouse("middle")
typed/builtin//modules/zinput/test/M/releaseStick
M.releaseStick() -> boolean
Lift the finger driving the on-screen stick, if one is down.
pushStick(0, 0) does this too; this is the explicit form, and what
a test calls to be sure it starts from rest.
Returns boolean — True when a contact was lifted.
Zin.test.releaseStick()
typed/builtin//modules/zinput/test/M/releaseUiFocus
M.releaseUiFocus()
Give the UI's focus pair back to the UI pass, which writes its own opinion on its next run.
Zin.test.releaseUiFocus()
typed/builtin//modules/zinput/test/M/runFrame
M.runFrame()
Advance one engine frame: yield until the engine has published a
later frame's snapshot, so simulated events queued before the call have
drained into the state a read after it sees. Use between manual
__zero_input.simulate* calls when you need to checkpoint state
without queueing a new event. Does NOT call Zin.tick() — run it
yourself if you want action / event handlers to dispatch.
Zin.test.runFrame()
typed/builtin//modules/zinput/test/M/scroll
M.scroll(dy: number, target: string?)
Simulate a mouse wheel scroll. Positive dy = up.
Parameters
dynumber— Signed scroll delta.targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Zin.test.scroll(1)
typed/builtin//modules/zinput/test/M/tapKey
M.tapKey(code: string, duration: number?, target: string?)
Press + release in one call. If duration is set, waits that
many seconds (held over multiple frames) between press and release.
Does NOT tick — call Zin.tick(dt) between tapKey and your
assertion if you need binding-action handlers to fire.
Parameters
codestring— Web-style key code.durationnumber(optional) — Optional held duration in seconds.targetstring(optional) —"game"(the default) drives the game's input alone — the scene reads it whatever a person at the window has focused, and no UI widget receives it."ui"reaches the UI the way a person's device does.
Zin.test.tapKey("Space", 0.1)
typed/builtin//modules/zinput/test/M/tapTouchButton
M.tapTouchButton(label: string, duration: number?)
Tap the on-screen button labelled label, at its real centre.
Raises when no live button carries that label, listing the ones that
do — a tap that silently lands on nothing is worse than one that
stops and says so.
Parameters
labelstring— The button's label, as a player reads it.durationnumber(optional) — Hold seconds; an instant tap when omitted.
Zin.test.tapTouchButton("Jump")
typed/builtin//modules/zinput/test/M/touchCancel
M.touchCancel(id: number, target: string?)
Simulate the platform cancelling a touch contact.
Parameters
idnumber— Stable finger id.targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Zin.test.touchCancel(1)
typed/builtin//modules/zinput/test/M/touchDown
M.touchDown(x: number, y: number, pressure: number?, target: string?) -> number
Put a finger down at (x, y) and return its contact id — the id
touchMove and touchUp take. Ids are handed out for you, so a
two-finger gesture is two of these rather than a bookkeeping problem.
Parameters
xnumber— Screen x.ynumber— Screen y.pressurenumber(optional) — Contact pressure 0..1; 1 when omitted.targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Returns number — The contact id.
local id = Zin.test.touchDown(200, 600)
typed/builtin//modules/zinput/test/M/touchEnd
M.touchEnd(id: number, target: string?)
Simulate a touch contact lifting.
Parameters
idnumber— Stable finger id.targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Zin.test.touchEnd(1)
typed/builtin//modules/zinput/test/M/touchMove
M.touchMove(id: number, x: number, y: number, pressure: number?, target: string?)
Simulate a touch contact moving.
Parameters
idnumber— Stable finger id.xnumber— Screen X.ynumber— Screen Y.pressurenumber(optional) — Contact pressure 0..1 (defaults to 1).targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Zin.test.touchMove(1, 440, 300)
typed/builtin//modules/zinput/test/M/touchStart
M.touchStart(id: number, x: number, y: number, pressure: number?, target: string?)
Simulate a touch contact beginning. id is any stable number
identifying the finger until its matching touchEnd/touchCancel.
The primary contact (slot 0) also drives the mouse path.
Parameters
idnumber— Stable finger id.xnumber— Screen X.ynumber— Screen Y.pressurenumber(optional) — Contact pressure 0..1 (defaults to 1).targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Zin.test.touchStart(1, 400, 300)
typed/builtin//modules/zinput/test/M/touchUp
M.touchUp(id: number, target: string?)
Lift a contact opened by touchDown.
Parameters
idnumber— The contact id.targetstring(optional) —"game"(the default) drives the game's input alone;"ui"reaches the UI the way a person's device does.
Zin.test.touchUp(id)
typed/builtin//modules/zinput/test/M/uiFocus
M.uiFocus(pointer: boolean, keyboard: boolean)
Hold the UI layer's two focus opinions, so a run with no UI on
screen can put an event on either side of consumed_by_ui. A pointer
event is judged on pointer and a key on keyboard, and both hold
until Zin.test.releaseUiFocus(). Inputs simulated after this call
are routed against the pair.
Parameters
pointerboolean— Whether the UI holds pointer focus.keyboardboolean— Whether the UI holds keyboard focus.
Zin.test.uiFocus(true, false)
typed/builtin//modules/zinput/touch/M/byId
M.byId(id: number) -> TouchPoint?
Look up an active contact by its finger id.
Parameters
idnumber— The platform finger id.
Returns TouchPoint? — The contact record, or nil.
local t = Zin.touch.byId(3)
typed/builtin//modules/zinput/touch/M/capable
M.capable() -> boolean
Whether the session has (or has declared) a touch input source. Latched by the first contact, or set at boot by platforms that know up front.
Returns boolean — True when a touch source exists.
if Zin.touch.capable() then ... end
typed/builtin//modules/zinput/touch/M/count
M.count() -> number
Number of active touch contacts this frame.
Returns number — The contact count.
if Zin.touch.count() >= 2 then ... end
typed/builtin//modules/zinput/touch/M/off
M.off(handle: any?)
Unsubscribe a handle returned by any Zin.touch.on* function.
Parameters
handleany(optional) — The subscription handle.
Zin.touch.off(h)
typed/builtin//modules/zinput/touch/M/onBegan
M.onBegan(fn: (any, boolean) -> any, opts: any?)
Subscribe to touch-contact-begin events. fn(ev, gpe) where ev =
{ kind = "touch.down", id, x, y, pressure } and gpe is the UI-focus
flag at fire time. Return "sink" to consume. Returns an
Zin.events handle for off.
Parameters
fn(any, boolean) -> any— The callback.optsany(optional) — Optional { priority, context, once } (Zin.events.on opts).
Returns The subscription handle.
local h = Zin.touch.onBegan(function(ev) print(ev.id) end)
typed/builtin//modules/zinput/touch/M/onCancelled
M.onCancelled(fn: (any, boolean) -> any, opts: any?)
Subscribe to platform touch-cancel events ({ kind = "touch.cancel", id }).
Parameters
fn(any, boolean) -> any— The callback.optsany(optional) — Optional Zin.events.on opts.
Returns The subscription handle.
local h = Zin.touch.onCancelled(function(ev) ... end)
typed/builtin//modules/zinput/touch/M/onEnded
M.onEnded(fn: (any, boolean) -> any, opts: any?)
Subscribe to touch-lift events ({ kind = "touch.up", id, x, y }).
Parameters
fn(any, boolean) -> any— The callback.optsany(optional) — Optional Zin.events.on opts.
Returns The subscription handle.
local h = Zin.touch.onEnded(function(ev) ... end)
typed/builtin//modules/zinput/touch/M/onMoved
M.onMoved(fn: (any, boolean) -> any, opts: any?)
Subscribe to touch-move events ({ kind = "touch.move", id, x, y, dx, dy, pressure }).
Parameters
fn(any, boolean) -> any— The callback.optsany(optional) — Optional Zin.events.on opts.
Returns The subscription handle.
local h = Zin.touch.onMoved(function(ev) ... end)
typed/builtin//modules/zinput/touch/M/primary
M.primary() -> TouchPoint?
The primary contact (slot 0), if a finger is down.
Returns TouchPoint? — The primary contact record, or nil.
local p = Zin.touch.primary()
typed/builtin//modules/zinput/touch/M/slots
M.slots() -> { TouchPoint }
All active touch contacts this frame, in begin order.
Returns { TouchPoint } — Array of contact records (may be empty).
for _, t in ipairs(Zin.touch.slots()) do print(t.slot, t.x, t.y) end
typed/builtin//modules/zinput/touchControls/M/_advance
M._advance()
Internal: advance the touch-controls overlay one tick — route
this tick's raw touches into Zin.virtual and recompute layout
first, then auto-mount by Zin.surface.current() and repaint the
overlay if its draw state changed. Wired into Zin.tick, before
Zin.emulation._advance() (the floor consumes the Zin.virtual
state this routing produces).
Zin.touchControls._advance()
typed/builtin//modules/zinput/touchControls/M/_forceMount
M._forceMount(on: boolean)
Test-only: force the overlay to mount regardless of
Zin.surface.current() — for headless tests that want to exercise
rendering without a real touch surface. Touch routing itself is
always active regardless of this flag.
Parameters
onboolean— Whether to force-mount.
Zin.touchControls._forceMount(true)
typed/builtin//modules/zinput/touchControls/M/_reset
M._reset()
Test-only: unmount, clear all routing/layout state, drop
bespoke buttons, release every runtime control, close the fan, clear
the viewport override, and reset the force-mount flag. Registration
(ui.registerScreen) itself is not undone.
Zin.touchControls._reset()
typed/builtin//modules/zinput/touchControls/M/_setViewportOverride
M._setViewportOverride(w: number?, h: number?)
Test-only: override the logical screen size (ui.screenSize()
space) the layout budget/fan math uses, without resizing the real
window — for headless tests that need to prove budget math at a
phone-sized viewport. Pass nil for both to clear the override and
fall back to the real ui.screenSize().
Parameters
wnumber(optional) — Override width, or nil to clear.hnumber(optional) — Override height, or nil to clear.
Zin.touchControls._setViewportOverride(400, 800)
typed/builtin//modules/zinput/touchControls/M/_settled
M._settled() -> boolean
Internal: whether the overlay could change nothing on its next advance -- no live touch assignment, no open fan, mount state matching the surface, and (while mounted) no pending redraw and the layout's inputs unchanged. The tick's quiescence gate reads it. A redraw pending on an unmounted overlay is paid when it mounts, so it holds nothing awake until then.
Returns boolean — true when the overlay is at rest.
if Zin.touchControls._settled() then ... end
typed/builtin//modules/zinput/touchControls/M/addControl
M.addControl(opts: ControlOpts) -> number
Add a control while the world is running, and put its button on screen.
The control becomes an asset — a one-control .inputMap under
/source/runtimeControls/ — which is then activated and subscribed
to, which is what makes the overlay draw for it. It carries all three
device classes like any other control: give each of kbm / gamepad
a binding, or false plus reasons.<class> saying why it refuses
that device. touch defaults to an on-screen button for a button
control.
At least one handler is required. A control nothing listens to draws
no button, so an addControl with no handler would put nothing on
screen at all.
The control follows the context that added it: one a component adds is removed when that component instance goes (removed, hot-reloaded, or its entity despawned), one a module body adds at the module's next run.
Parameters
optsControlOpts—{ name, label?, kind?, kbm, gamepad, touch?, reasons?, button?, onInput?, onPressed?, onReleased?, onChanged? }.
Returns number — A handle for M.removeControl.
local h = Zin.touchControls.addControl({ name = "cast", label = "Cast", kbm = Zin.bindings.key("KeyF"), gamepad = Zin.bindings.padButton("north"), onPressed = function() print("cast") end })
typed/builtin//modules/zinput/touchControls/M/appearance
M.appearance() -> { [string]: any }
How the overlay is currently drawn - every appearance key with
the value in force, as a copy safe to hold and mutate. The keys are
the ones M.setAppearance accepts, and each button's own resolved
appearance is reported beside it on M.layout().
Returns { [string]: any } — { inset, gap, fill, stroke, strokeWidth, textColor, fontFamily, fontScale, iconFont, iconScale, sheetFill, sheetRounding, fanFill, fanStroke, stickStroke, stickFill, stickWidth }.
local inset = Zin.touchControls.appearance().inset
typed/builtin//modules/zinput/touchControls/M/button
M.button(opts: ButtonOpts) -> number
Add an overlay button. Two flavors:
{ action = "jump" }attaches an extra touchButton binding to an existing action on the active map — a second on-screen instance of a control the map already defines.{ emit = "KeyF", label = "Cast" }registers an overlay-only button: a syntheticemit:KeyFaction carrying both the kbm key binding and the touchButton, so the emulation floor drivesKeyFstraight from this button through the same binding/emulation path every other control uses.label/zone/iconset the button's presentation, and the appearance keysZin.bindings.touchButtontakes (fill,stroke,strokeWidth,textColor,fontFamily,fontScale,iconFont,iconScale) state this one button's look over the overlay's own; when neither label nor zone is given the button falls back to the action name (or the emit key).
Parameters
optsButtonOpts—{ action?, emit?, label?, icon?, zone?, fill?, stroke?, strokeWidth?, textColor?, fontFamily?, fontScale?, iconFont?, iconScale? }- exactly one ofaction/emitis required.
Returns number — A handle for M.removeButton.
local h = Zin.touchControls.button({ action = "jump" })
local h = Zin.touchControls.button({ emit = "KeyF", label = "Cast" })
typed/builtin//modules/zinput/touchControls/M/claimed
M.claimed() -> { number }
The contact ids currently claimed by the stick, a button, or the drag zone.
Returns { number } — A sorted array of claimed touch contact ids.
local ids = Zin.touchControls.claimed()
typed/builtin//modules/zinput/touchControls/M/controls
M.controls() -> { any }
Every control M.addControl currently holds, as
{ handle, name, path } in the order they were added.
Returns { any } — Array of runtime-control records.
for _, c in ipairs(Zin.touchControls.controls()) do print(c.name) end
typed/builtin//modules/zinput/touchControls/M/layout
M.layout() -> any
The overlay's current control layout — a copy, safe to hold and
mutate. buttons is the budgeted, VISIBLE stack in draw order
(each entry { id, label, icon, actionName, priority, size, appliedSize, group, style, cx, cy, radius } in ui.screenSize()
space - size is the step the binding declared and appliedSize
the one radius came from, which differ only where a rich scheme's
step-down moved a button one size down; icon is the glyph the
circle draws in place of its label, and style the appearance it
resolved to out of its own keys and M.appearance()); fan is nil unless the
button set exceeded the two-column budget, in which case it's
{ open, cx, cy, radius, buttons } — open is whether the
overflow sheet is currently expanded, buttons the overflowed
entries at their sheet positions (tappable only while open).
stick / drag name the axis + virtual zone each region feeds,
and pinch the axis the two-finger recognizer feeds, each nil when
the active map defines none in the current context. safe is the
rect every control keeps inside: the part of the overlay no system
bar, display cutout or on-screen keyboard covers (ui.safeArea()),
in the same space.
The reading answers for the same instant Zin.scheme.bindings()
does: activating or standing down a map, subscribing a control,
changing context or resizing the screen is reflected by the next
read, in the call that made the change. Empty until a screen size
has resolved. Backs custom control-placement UI and headless tests
that need a button's (or the fan's) center to aim a simulated
touch at.
Returns any — { buttons, fan?, stick?, drag?, pinch?, width, height, safe = { x, y, width, height } }.
local jumpBtn = Zin.touchControls.layout().buttons[1]
typed/builtin//modules/zinput/touchControls/M/primaryClaimed
M.primaryClaimed() -> boolean
Whether the PRIMARY touch contact is currently claimed by an on-screen control.
The engine projects the primary contact onto the mouse so pointer UI works from a finger. A contact that begins on a drawn button, the fan or an open sheet entry is that control's alone and is not projected; one that begins on the movement stick or the look area is, and so it also reads as a held left mouse button, which fires every control bound to one.
A mouse binding means the player pressed a mouse button, and a finger
an on-screen control has claimed is not that. Zin.bindings consults
this so a thumb on the stick does not fire an unrelated control.
Returns boolean — True while an on-screen control owns the primary contact.
if not Zin.touchControls.primaryClaimed() then ... end
typed/builtin//modules/zinput/touchControls/M/removeButton
M.removeButton(handle: number) -> boolean
Remove a button previously added via M.button.
Parameters
handlenumber— The handle returned byM.button.
Returns boolean — Whether a button was actually removed.
Zin.touchControls.removeButton(h)
typed/builtin//modules/zinput/touchControls/M/removeControl
M.removeControl(handle: number) -> boolean
Release a control added by M.addControl — drop its
subscriptions and release its map, which takes it off the screen and
out of the live set. The asset it was written to stays, so the same
name can be added again and comes back with the same guid.
Parameters
handlenumber— The handle returned byM.addControl.
Returns boolean — Whether a control was actually released.
Zin.touchControls.removeControl(h)
typed/builtin//modules/zinput/touchControls/M/setAppearance
M.setAppearance(opts: { [string]: any }?)
State how the overlay is drawn. Every value a world passes replaces the engine's for as long as the world runs, so a game that typesets its own interface puts the controls the engine draws in the same faces, colours and margins as the rest of it.
The keys, and what each decides:
inset/gap- the margin the button column keeps from the right and bottom edges of the safe area (ui.safeArea(): the screen less its system bars and display cutouts), and the space between two buttons. A game laying its interface out inside a margin of its own states that margin here and the controls keep it too.fill/stroke/strokeWidth- every button's circle.textColor/fontFamily/fontScale- the label on it.fontFamilytakes the liststyle.fontFamilytakes andui.listFonts()names;fontScaleis the share of the button's radius the label is set at.iconFont/iconScale- the same two for a button that declared aniconglyph.sheetFill/sheetRounding- the overflow sheet behind the fan.fanFill/fanStroke- the fan's own circle.stickStroke/stickFill/stickWidth- the movement stick's ring and thumb.
A call states only the keys it carries and leaves the rest as they stand, so appearance composes across several calls. Passing nothing puts every value back to the engine's own.
A single button states any of the button keys for itself through
Zin.bindings.touchButton, which wins over what is set here.
Parameters
opts{ [string]: any }(optional) — A table of appearance keys, or nil to restore the defaults.
Zin.touchControls.setAppearance({ fill = "#10243a", stroke = "#c9a227", textColor = "#f2e6c8", fontFamily = "Inter", inset = 38 })
Zin.touchControls.setAppearance()
typed/builtin//modules/zinput/utils/M/applyCurve
M.applyCurve(curve: (string | (number) -> number)?, x: number) -> number
Apply a response curve to a reading. "linear" (and no curve at
all) is the identity, "quadratic" squares while keeping the sign,
"cubic" cubes, and a function is called with the reading. A function
that raises, or answers with anything other than a number, leaves the
reading as it was.
Parameters
curve(string | (number) -> number)(optional) —"linear"|"quadratic"|"cubic"| a function of the reading.xnumber— The reading to shape.
Returns number — The shaped reading.
Utils.applyCurve("quadratic", -0.5) -- → -0.25
typed/builtin//modules/zinput/utils/M/applyDeadzoneScalar
M.applyDeadzoneScalar(x: number, deadzone: number?) -> number
Apply a scalar deadzone: a magnitude below the threshold reads 0, anything at or above it passes through untouched.
Parameters
xnumber— The reading.deadzonenumber(optional) — The threshold;nilapplies none.
Returns number — The reading, or 0 inside the deadzone.
Utils.applyDeadzoneScalar(0.04, 0.1) -- → 0
typed/builtin//modules/zinput/utils/M/applyDeadzoneVector
M.applyDeadzoneVector(v: Vector2, deadzone: number?) -> Vector2
Apply a radial deadzone to a pair: the MAGNITUDE of the pair is
what the threshold is measured against, so a diagonal held past it
keeps both components and a stick resting inside it reads {0, 0}.
Parameters
vVector2— The reading, as{ x, y }.deadzonenumber(optional) — The threshold;nilapplies none.
Returns Vector2 — A fresh pair — the reading, or {0, 0} inside the deadzone.
Utils.applyDeadzoneVector({ x = 0.05, y = 0.05 }, 0.2) -- → { x = 0, y = 0 }
typed/builtin//modules/zinput/utils/M/buttonIndex
M.buttonIndex(name: string) -> number?
Convert a mouse button name to its 0-based index.
Parameters
namestring— Mouse button name ("left"/"right"/"middle").
Returns number? — The button index, or nil for unknown names.
Utils.buttonIndex("right") -- → 1
typed/builtin//modules/zinput/utils/M/buttonName
M.buttonName(idx: number) -> string?
Convert a 0-based mouse button index to its name.
Parameters
idxnumber— Mouse button index (0=left, 1=right, 2=middle).
Returns string? — The button name ("left"/"right"/"middle") or nil for out-of-range indices.
Utils.buttonName(0) -- → "left"
typed/builtin//modules/zinput/utils/M/keyFields
M.keyFields(surface: string?) -> KeyFields
The snapshot fields that carry a surface's keys: held, pressed
and released. surface is "scene" (the default), the keys pressed
while the scene was the active input context, or "editor", every key
no focused widget took.
Parameters
surfacestring(optional) —"scene"or"editor"; nil reads as"scene".
Returns KeyFields — { held, pressed, released } field names.
local f = Utils.keyFields("editor"); local held = snap[f.held]
typed/builtin//modules/zinput/utils/M/matchModifiers
M.matchModifiers(snapshot: any?, mods: Modifiers, surface: string?) -> boolean
Check whether the current frame's snapshot has the given modifiers
held. Pass any subset of { ctrl, shift, alt }; unspecified keys are
not checked. Returns false if snapshot is not a table — callers
can forward Zin.state.get() directly without nil-checking first.
Parameters
snapshotany(optional) — The frame snapshot table (fromZin.state.get()), or any non-table value (treated as "no modifiers held").modsModifiers— Subset of{ ctrl, shift, alt }booleans to require.surfacestring(optional) — Whose keys to read:"scene"(the default) or"editor".
Returns boolean — true when every specified modifier matches the held state.
Utils.matchModifiers(snap, { ctrl = true })
typed/builtin//modules/zinput/utils/M/normalizeKey
M.normalizeKey(code: string) -> string
Normalize a key code to the engine's canonical web-style form — the
KeyboardEvent.code vocabulary the input map and Zin.state.get().keys
use ("KeyW", "Space", "ArrowUp", "ShiftLeft", …). A single ASCII
letter is promoted to its Key<L> code and a single digit to its
Digit<N> code, so the common shorthand "W" resolves to the "KeyW"
the default map binds instead of a phantom key nothing consumes. Any
other single character is rejected — no bound key code is one character
long. Multi-character codes pass through unchanged.
Parameters
codestring— The raw key code ("KeyW") or a single-letter/digit shorthand ("w").
Returns string — The canonical key code.
Utils.normalizeKey("w") -- → "KeyW"
Utils.normalizeKey("KeyW") -- → "KeyW"
typed/builtin//modules/zinput/utils/M/resolveButtonIndex
M.resolveButtonIndex(button: (number | string)?) -> number
Resolve a mouse button given as a 0-based index OR a
case-insensitive name ("left"/"right"/"middle", the same names
Zin.bindings.mouse takes) to its 0-based index. nil resolves to
left (0). Raises on an unknown name so a typo is loud, not a silent
left-click — the single coercion every input surface that accepts a
button uses so index and name mean the same thing everywhere.
Parameters
button(number | string)(optional) — Button index, name, or nil.
Returns number — The 0-based button index.
Utils.resolveButtonIndex("Right") -- → 1
Utils.resolveButtonIndex(2) -- → 2
typed/builtin//modules/zinput/utils/M/shapeScalar
M.shapeScalar(x: number, deadzone: number?, curve: (string | (number) -> number)?, invert: boolean?) -> number
The whole shaping of a scalar reading: deadzone, then curve, then inversion.
Parameters
xnumber— The reading.deadzonenumber(optional) — Magnitude below which the reading is 0;nilapplies none.curve(string | (number) -> number)(optional) —"linear"|"quadratic"|"cubic"| a function;nilis linear.invertboolean(optional) — Negate the shaped reading.
Returns number — The shaped reading.
Utils.shapeScalar(0.5, 0.1, "quadratic", true) -- → -0.25
typed/builtin//modules/zinput/utils/M/shapeVector
M.shapeVector(v: Vector2, deadzone: number?, curve: (string | (number) -> number)?, invert: boolean?) -> Vector2
The whole shaping of a pair: radial deadzone, then the curve on each component, then inversion of both.
Parameters
vVector2— The reading, as{ x, y }.deadzonenumber(optional) — Magnitude of the pair below which it reads{0, 0};nilapplies none.curve(string | (number) -> number)(optional) —"linear"|"quadratic"|"cubic"| a function;nilis linear.invertboolean(optional) — Negate both components.
Returns Vector2 — A fresh, shaped pair.
Utils.shapeVector({ x = 1, y = 0 }, 0.2, "linear", true) -- → { x = -1, y = 0 }
typed/builtin//modules/zinput/utils/M/smoothToward
M.smoothToward(current: number, target: number, dt: number, tau: number) -> number
One step of an exponential approach toward a target: tau is the
time constant in seconds, and the step covers dt of it. A tau of 0
or less arrives immediately; a dt of 0 or less stays put.
Parameters
currentnumber— Where the value is now.targetnumber— Where it is heading.dtnumber— Seconds this step covers.taunumber— The time constant, in seconds.
Returns number — The value after the step.
Utils.smoothToward(0, 1, 0.05, 0.2) -- → ~0.221
typed/builtin//modules/zinput/virtual/M/_beginFrame
M._beginFrame()
Internal: frame boundary — shift button edges, clear drag deltas. Called by the Zin.tick coordinator on the first tick of each engine frame.
Zin.virtual._beginFrame()
typed/builtin//modules/zinput/virtual/M/_reset
M._reset()
Test-only: clear all virtual state.
Zin.virtual._reset()
typed/builtin//modules/zinput/virtual/M/_settled
M._settled() -> boolean
Internal: whether every virtual control sits at rest -- sticks centred, buttons up (this frame and last), drag deltas empty. The tick's quiescence gate reads it.
Returns boolean — true when the virtual layer is producing nothing.
if Zin.virtual._settled() then ... end
typed/builtin//modules/zinput/virtual/M/addDrag
M.addDrag(zone: string, dx: number, dy: number)
Accumulate a drag delta for a zone this tick (cleared at the next frame boundary, like the mouse delta).
Parameters
zonestring— The drag zone id.dxnumber— Delta X in px.dynumber— Delta Y in px.
Zin.virtual.addDrag("right", 4, -2)
typed/builtin//modules/zinput/virtual/M/button
M.button(id: string) -> boolean
A virtual button's held state.
Parameters
idstring— The button id.
Returns boolean — True while held.
if Zin.virtual.button("Jump") then ... end
typed/builtin//modules/zinput/virtual/M/buttonPressed
M.buttonPressed(id: string) -> boolean
Whether a virtual button was pressed this frame (held now, not held at the previous frame boundary).
Parameters
idstring— The button id.
Returns boolean — True on the press frame.
if Zin.virtual.buttonPressed("Jump") then ... end
typed/builtin//modules/zinput/virtual/M/buttonReleased
M.buttonReleased(id: string) -> boolean
Whether a virtual button was released this frame.
Parameters
idstring— The button id.
Returns boolean — True on the release frame.
if Zin.virtual.buttonReleased("Jump") then ... end
typed/builtin//modules/zinput/virtual/M/drag
M.drag(zone: string) -> (number, number)
A zone's accumulated drag delta this frame.
Parameters
zonestring— The drag zone id.
Returns (number, number) — dx, dy.
local dx, dy = Zin.virtual.drag("right")
typed/builtin//modules/zinput/virtual/M/setButton
M.setButton(id: string, held: boolean)
Set a virtual button's held state. Edges (pressed/released) are derived at the frame boundary.
Parameters
idstring— The button id (a touchButton'sid, else its label, else its zone).heldboolean— Whether the button is down.
Zin.virtual.setButton("Jump", true)
typed/builtin//modules/zinput/virtual/M/setStick
M.setStick(zone: string, x: number, y: number)
Set a virtual stick's normalized vector (each component -1..1; values are clamped). Persists until set again or reset.
Parameters
zonestring— The stick's zone id.xnumber— Stick X (right positive).ynumber— Stick Y (down positive, matching screen deltas).
Zin.virtual.setStick("left", 0.4, -0.9)
typed/builtin//modules/zinput/virtual/M/stick
M.stick(zone: string) -> (number, number)
A virtual stick's current vector ({x=0,y=0} when unset).
Parameters
zonestring— The stick's zone id.
Returns (number, number) — x, y components.
local x, y = Zin.virtual.stick("left")