Log inGet started

typed

Updated 6 September 2026

The typed namespace — 2087 functions.

typed/builtin//assetTypes/agentSkill/behavior/M/onChange

M.onChange(self: any?, change: any?)

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

Parameters

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

typed/builtin//assetTypes/agentSkill/behavior/M/onRegister

M.onRegister(self: any?)

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

Parameters

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

typed/builtin//assetTypes/avatar/behavior/M/onCreate

M.onCreate(name: string, opts: CreateOpts?) -> { [string]: string }

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

Parameters

  • name string
  • opts CreateOpts (optional)

Returns { [string]: string }{ ["avatar.json"] = <json> }.

asset.create("avatar", "my_hero", { body = heroBundle })  -- standard controller + locomotion
asset.create("avatar", "fp_player", { humanoid = true })  -- body-less first-person player

typed/builtin//assetTypes/bundle/behavior/M/onCreate

M.onCreate(name: string, opts: CreateOpts?) -> { [string]: string }

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

Parameters

  • name string
  • opts CreateOpts (optional)

Returns { [string]: string }{ entity_template = <JSON> } when opts.entity is given, else {}.

asset.create("bundle", "tree_prefab", { entity = rootRef })

typed/builtin//assetTypes/computeShader/behavior/TextureHandleMethods/destroy

TextureHandleMethods.destroy(self: any?) -> boolean

Destroy this texture or sampler and free its GPU memory.

Parameters

  • self any (optional)

Returns boolean — True on success.

typed/builtin//assetTypes/computeShader/behavior/TextureHandleMethods/read

TextureHandleMethods.read(self: any?) -> Readback

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

typed/builtin//assetTypes/computeShader/behavior/TextureHandleMethods/write

TextureHandleMethods.write(self: any?, data: buffer | string | { number }) -> boolean

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

Parameters

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

Returns boolean — True on success.

typed/builtin//assetTypes/computeShader/behavior/TextureHandleMethods/writeFloats

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

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

Parameters

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

Returns boolean — True on success.

typed/builtin//assetTypes/dynamicAsset/behavior/M/onChange

M.onChange(ref: any?, change: any?)

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

Parameters

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

typed/builtin//assetTypes/effect/behavior/M/onChange

M.onChange(ref: any?, change: any?)

Drop the effect declaration cached on this ref after a write inside the instance, so the next read re-parses effect.yaml from the file on disk.

Parameters

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

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/3D text and egui UI systems. Falls back to a raw font file for instances authored before the baked layout (font.register reparses, with a slow-path warn). Fired once per instance by the asset system (live on asset.create, and in the world-load sweep). Guarded so a single bad font never errors the sweep.

Parameters

  • self any (optional)

typed/builtin//assetTypes/inputMap/behavior/M/onChange

M.onChange(self: any?, change: any?)

Re-activate this map after a write inside the instance, so an edit to its bindings takes hold in the running session. Only the map that is currently active is re-activated; a removal is ignored.

Parameters

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

typed/builtin//assetTypes/mesh/behavior/M/onCreate

M.onCreate(name: string, opts: CreateOpts) -> { [string]: string }

Generic-creation hook for asset.create("mesh", name, opts). Pure: returns the content-file map; asset.create writes it to the authored destination, registering the .mesh asset under its minted guid. Disk-only — nothing is uploaded to the GPU here (the GPU mesh is a separate, explicit renderer.mesh.create step keyed by this asset's guid).

opts is raw geometry { positions, indices, normals?, uvs?, colors? } (flat float / u32 arrays, encoded to data.zmsh via renderer.mesh.encode), or a pre-encoded { bytes } payload (stored verbatim).

Parameters

  • name string — Mesh identity (the instance name).
  • opts CreateOpts

Returns { [string]: string }{ ["data.zmsh"] = <ZMSH> } — the container's primary content file.

asset.create("mesh", "tree", { positions = {...}, indices = {...} })

typed/builtin//assetTypes/population/behavior/Live/bounds

Live.bounds(self: any?) -> any

The world-space box the drawn instances occupy — each variant's mesh AABB carried through every one of its matrices. The matrices are world-space, so this is where the population stands, whatever entity owns it.

Parameters

  • self any (optional)

Returns any{ min, max } as Vec3 tables, or nil once nothing is registered.

local box = live:bounds()

typed/builtin//assetTypes/population/behavior/Live/count

Live.count(self: any?) -> number

Instances the engine reports drawing across every registration this holds. Read back from the renderer rather than from the recipe, so a registration that went away, or that the renderer turned away, counts as gone.

Parameters

  • self any (optional)

Returns number — Instance count.

print(live:count())

typed/builtin//assetTypes/population/behavior/Live/destroy

Live.destroy(self: any?)

Release every registration and its transform buffer. The draw is dropped BEFORE its buffer is destroyed: a registration reserves slots against the buffer it was given, so a buffer that goes away takes its registration with it. The meshes belong to their .mesh assets and stay. A second call finds an empty list and returns.

Parameters

  • self any (optional)
live:destroy()

typed/builtin//assetTypes/population/behavior/Live/drawCalls

Live.drawCalls(self: any?) -> number

Draw calls this population costs — one per variant the renderer is drawing, at any instance count. A variant the renderer turned away costs nothing and is counted nowhere; :errors() says why.

Parameters

  • self any (optional)

Returns number — Registration count.

print(live:drawCalls())

typed/builtin//assetTypes/population/behavior/Live/errors

Live.errors(self: any?) -> { any }

Why this population is drawing less than its recipe asks for: one entry per registration the renderer turned away, carrying the variant it belongs to, the mesh it names and the renderer's own reason. A population drawing everything it holds answers with an empty list, so this and :drawCalls() agree with the frame.

Parameters

  • self any (optional)

Returns { any } — Array of { variant, meshGuid, error }.

for _, e in ipairs(live:errors()) do warn(e.variant, e.error) end

typed/builtin//assetTypes/population/behavior/Live/settled

Live.settled(self: any?) -> boolean

Whether the renderer has answered for every registration this holds. A registration is made a stage before the renderer sees it, so the frame it is made in is one where nothing yet says whether the copies are drawn; :errors() is complete from the frame this turns true.

Parameters

  • self any (optional)

Returns booleantrue once every registration has an answer.

if live:settled() then check(live:errors()) end

typed/builtin//assetTypes/population/behavior/M/onCreate

M.onCreate(name: string, opts: CreateOpts?) -> { [string]: string }

Generic-creation hook for asset.create("population", name, opts). Pure: returns the content-file map; asset.create writes it to the mode-aware destination. Each variant contributes its mesh + material to population.json and its matrices to transforms.bin, in variant order.

Parameters

  • name string — Population identity (the instance name).
  • opts CreateOpts (optional)

Returns { [string]: string }{ ["population.json"] = <JSON>, ["transforms.bin"] = <blob> }.

asset.create("population", "forest", { variants = { { mesh = meshGuid, material = matGuid, transforms = flat } } })

typed/builtin//assetTypes/rig/behavior/M/onChange

M.onChange(self: any?, _change: any?)

Drop the cached decode when the rig's content changes — a hot-reload or a re-import — so the next :doc() re-parses the new rig.json.

Parameters

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

typed/builtin//assetTypes/rig/behavior/M/onCreate

M.onCreate(name: string, opts: CreateOpts) -> { [string]: string }

Generic-creation hook for asset.create("rig", name, opts). Pure: returns the content-file map; asset.create writes it to the authored destination, registering the .rig asset under its minted guid. The JSON document carries the skeleton, its retarget profile (the role -> bone driver, present for a humanoid), and its humanoid classification — a non-humanoid rig (a prop, a plant, a quadruped) simply has no profile in the same document.

Parameters

  • name string — Rig identity (the instance name).
  • opts CreateOpts

Returns { [string]: string }{ ["rig.json"] = <json> }.

asset.create("rig", "PolygonSyntyCharacter", { json = rigJson })

typed/builtin//assetTypes/testSuite/shared/Test/afterEach

Test.afterEach(fn: () -> ())

Register an after-each hook on the current suite. Runs after every test body in the suite. Hook errors print a warning rather than aborting Test.run.

Parameters

  • fn () -> () — The hook function.
Test.afterEach(function() layers.active:reload() end)

typed/builtin//assetTypes/testSuite/shared/Test/beforeEach

Test.beforeEach(fn: () -> ())

Register a before-each hook on the current suite. Runs before every test body in the suite. Hook errors mark the test failed rather than aborting Test.run.

Parameters

  • fn () -> () — The hook function.
Test.beforeEach(function() engine.mode = "edit" end)

typed/builtin//assetTypes/testSuite/shared/Test/beginWorldBaseline

Test.beginWorldBaseline() -> WorldBaseline

Capture a clean-slate world baseline: snapshot every non-persistent layer (by guid + additive flag) then unload them, so suites can't depend on whatever scene the engine booted with. Persistent layers (e.g. the editor overlay) are left untouched. Holds until the unload cascade has landed, so the first suite starts on a world that has stopped moving. Pair with Test.restoreWorldBaseline to put the captured layers back afterwards.

Returns WorldBaseline — A WorldBaseline token to hand to Test.restoreWorldBaseline.

local base = Test.beginWorldBaseline(); ...; Test.restoreWorldBaseline(base)

typed/builtin//assetTypes/testSuite/shared/Test/captureFailure

Test.captureFailure(fn: () -> ()) -> string?

Run a body and return the failure it records instead of recording it. Matchers report by flagging the running test, so a test that asserts a matcher rejects something would flag itself; this hands back the rendered message and leaves the surrounding test's own state untouched. A body that records more than one failure returns the headline for them — use Test.captureOutcome for the whole ordered list.

Parameters

  • fn () -> () — The body to run. Errors it raises propagate to the caller.

Returns string? — The rendered failure message, or nil when the body recorded none.

local msg = Test.captureFailure(function() Test.expect(1).toBe(2) end)
Test.expect(msg).toContain("to be 2")

typed/builtin//assetTypes/testSuite/shared/Test/captureOutcome

Test.captureOutcome(fn: () -> ()) -> Outcome

Run a body the way the runner runs a test body and hand back everything that would be reported for it: the failures it recorded in the order they happened, the headline composed from them, and whether a raise ended it. A raise is recorded as the final failure, which is how the runner treats one. The surrounding test's own record stays untouched.

Parameters

  • fn () -> () — The body to run.

Returns Outcome{ failed, error, failures, raised }.

local o = Test.captureOutcome(function() Test.expect(1).toBe(2); error("boom") end)
Test.expect(o.failures[1]).toBe("Expected 1 to be 2")

typed/builtin//assetTypes/testSuite/shared/Test/captureTestOutcome

Test.captureTestOutcome(spec: TestSpec) -> TestOutcome

Run a test's whole lifecycle the way the runner runs one — beforeEach, the body, afterEach, then the cleanups registered while it ran — and hand back the verdict composed from everything all of them recorded. The runner reads its verdict from the same place, so what this reports for a test is what a sweep reports for it. A failure a hook records names the hook it came from. The surrounding test's own record stays untouched.

Parameters

  • spec TestSpec{ body, beforeEach, afterEach, name }. Only body is required.

Returns TestOutcome{ skipped, reason, failed, error, failures, raised }.

local o = Test.captureTestOutcome({ body = function() end, afterEach = function() Test.expect(1).toBe(2) end })
Test.expect(o.failures[1]).toBe("afterEach: Expected 1 to be 2")

typed/builtin//assetTypes/testSuite/shared/Test/cleanupTmp

Test.cleanupTmp()

Remove the sandbox tmp root and everything in it. Logs a warning if the remove fails but doesn't error.

Test.cleanupTmp()

typed/builtin//assetTypes/testSuite/shared/Test/clear

Test.clear()

Clear all suites and reset stats / leaked-id tracking. Use this to re-run the suite from a clean state (in particular: re-runs in the same execute() batch).

Test.clear()

typed/builtin//assetTypes/testSuite/shared/Test/clearLoadedLayers

Test.clearLoadedLayers()

Put the world back to the clean slate a sweep begins from: the run's stand-in root released, and every non-persistent layer unloaded.

A sweep takes its baseline once and restores it once, so a suite that loads a scene and does not put it back hands that scene to every suite after it. What the next suite then meets is not the slate it was written against — and a scene expecting player spawns without a PlayerSpawn refuses the engine.mode = "play" its beforeEach asks for, so the suite fails whole for something the suite before it did. Which suites those are depends on the order the sweep sharded them into, so the failure moves between runs.

Returns Nothing.

Test.clearLoadedLayers()

typed/builtin//assetTypes/testSuite/shared/Test/closeSuiteBoundary

Test.closeSuiteBoundary(suite: string, pre: SuiteBoundary) -> SuiteHandover

Close a boundary opened by Test.openSuiteBoundary: unload the layers the suite loaded and left, name the render-feature identities it left registered, and write a line to the engine log for each of the two that has something to say. This is the per-suite handover, so a caller running one suite after another has one call to make between them. The boundary it names leaves the open set, so Test.suiteBoundary goes back to the one around it.

It answers for what is standing, and it spends no frames finding out: a caller that needs the queued teardown to have landed before it reads a count calls Test.settleWorldTeardown around its own reading, and pays the frames where it wants them.

What it answers for is what the suite ADDED to the world it was handed: a layer that appeared between the two reads is unloaded, and a render-feature identity that appeared is named. A layer or a feature the suite took away is the suite's own doing and stands as the suite left it.

Parameters

  • suite string — The name to attribute what crossed to, as it reads in the log.
  • pre SuiteBoundary — The token Test.openSuiteBoundary returned.

Returns SuiteHandover{ suite, layers, features } — the name it was given, the layer guids it asked the engine to unload, and the feature identities left standing. The same record is published as Test.lastSuiteHandover.

local left = Test.closeSuiteBoundary("vfs", pre)

typed/builtin//assetTypes/testSuite/shared/Test/componentRegistered

Test.componentRegistered(name: string) -> boolean

Whether a component type is registered under name, so that component.add(name) takes it. An authored component's asset resolves the moment its source is written; the type it declares registers when that registration drains, on a later frame, and this reads the registration through the handle's own isRegistered(), the ECS registry a queued component.add is routed by.

Parameters

  • name string — The component type name.

Returns boolean — true once the type is registered, false until then.

Test.waitUntil(function() return Test.componentRegistered("Probe") end, 120)

typed/builtin//assetTypes/testSuite/shared/Test/describe

Test.describe(name: string, fn: () -> ()) -> Suite

Define a test suite. Collects tests via Test.it / Test.skip calls inside the supplied function and registers them under name. Per-suite and per-test documentation (overall description + per-test pass condition) is authored as --!desc / --!pass doc-comments above the Test.describe / Test.it calls and extracted statically by the engine's doc parser (zero_scripting::module_docs); it is not passed at runtime.

Parameters

  • name string — Suite name (used in output).
  • fn () -> () — The suite-definition function. Runs once during describe to collect tests into the suite. Nested describes work — the current suite is restored when this call returns. A body that ends on a raise leaves a failing case behind naming the raise, so the declarations it never reached are answered for and the run ends on them; a body that stands down leaves a skipped case carrying the reason.

Returns Suite — The newly-registered suite table.

Test.describe("math", function() Test.it("adds", function() Test.expect(1+1).toBe(2) end) end)

typed/builtin//assetTypes/testSuite/shared/Test/describeValue

Test.describeValue(value: any?) -> string

Render any value as human-readable text for test output. Strings pass through unchanged; tables render structurally ({code = "x", n = 2}) with bounded breadth, depth, and string length; a value with its own __tostring uses it. Every failure message and raised error the harness reports goes through this, so a table-valued failure names its contents instead of an address.

Parameters

  • value any (optional) — Any Luau value.

Returns string — Bounded, deterministic text for value.

Test.describeValue({ code = "E1", line = 4 }) --> '{code = "E1", line = 4}'

typed/builtin//assetTypes/testSuite/shared/Test/detectLeakedGpuLabels

Test.detectLeakedGpuLabels(preSnapshot: { [string]: number }) -> { string }

Name the GPU labels the allocator holds now that preSnapshot did not, and the ones it now holds more allocations under. This is the reading for a suite whose fixtures carry engine-given labels it cannot pick out by name; a suite that names its own reads them with Test.gpuLabelsMatching, which answers the same on a second run in the same engine.

The name is the only handle a named GPU resource has, so the case that named one is the one that can release it, and this answer is the list of what the suite left standing.

Parameters

  • preSnapshot { [string]: number } — The map returned by Test.snapshotGpuLabels.

Returns { string } — The labels added since the snapshot, sorted.

local left = Test.detectLeakedGpuLabels(pre)

typed/builtin//assetTypes/testSuite/shared/Test/detectLeakedRenderFeatures

Test.detectLeakedRenderFeatures(preSnapshot: { [string]: string }) -> { string }

Name the render features registered since preSnapshot, counted by identity. A suite that re-registers an identity replaces the live instance under a new guid, so what a suite hands over is which identities are running and how many of each, not which guid is carrying them.

The answer is a report rather than a teardown. A render feature is brought up by the system that needs it, and that system keeps its own record of whether it is live: a feature destroyed from outside leaves the system believing it is still running, so it never brings it back and everything that feature drew is missing from then on. Naming the suite that left one is what a reader can act on.

The answer covers the identities the suite ADDED to the set it was handed. The set as the suite leaves it is what the next boundary opens against, so an identity the suite took down is the baseline every suite behind it is measured from.

Parameters

  • preSnapshot { [string]: string } — The map returned by Test.snapshotRenderFeatures.

Returns { string } — The identities registered since the snapshot, one entry per instance, sorted.

local left = Test.detectLeakedRenderFeatures(pre)

typed/builtin//assetTypes/testSuite/shared/Test/detectSourcePollution

Test.detectSourcePollution(pre: { [string]: boolean }) -> { string }

Compare the current top-level /zero/source entries against a pre-suite snapshot and return every NEW entry that isn't engine- or runner-managed. A non-empty result means the suite left content at the world root — pollution it must instead sandbox under Test.TMP_ROOT or remove via Test.registerCleanup.

Parameters

  • pre { [string]: boolean } — The set returned by Test.snapshotSourceContent.

Returns { string } — Array of leaked top-level entry names (empty when the suite is clean).

local leaked = Test.detectSourcePollution(pre)

typed/builtin//assetTypes/testSuite/shared/Test/detectSpawnedEntities

Test.detectSpawnedEntities(preSnapshot: { [any]: boolean }) -> { { id: any, name: string } }

Compare the entities alive now against a snapshot and report every one that appeared since. Use it to assert an operation spawns nothing: the answer is the entity set itself, so it holds whatever else the world already carries — unlike a name lookup, which reports ambient content whenever the name is one the world uses too. The entity analogue of Test.detectSourcePollution.

Parameters

  • preSnapshot { [any]: boolean } — The set returned by Test.snapshotEntities.

Returns { { id: any, name: string } } — Array of { id, name } for each entity spawned after the snapshot.

local pre = Test.snapshotEntities(); op(); Test.expect(#Test.detectSpawnedEntities(pre)).toBe(0)

typed/builtin//assetTypes/testSuite/shared/Test/ensureRootLayer

Test.ensureRootLayer() -> any

Take a root scene layer for the run, so layers.active and every scene-scoped surface reached through it (layers.active.camera, :save(), the sceneAuthoring review tools, @scene paths) answer inside a test the way they answer from execute. A sweep starts each run from a clean slate with the booted scene unloaded, so layers.active reads nil until a test asks for a scene: this returns the active root when one is loaded, and otherwise duplicates Test.DEFAULT_SCENE_FIXTURE to Test.BASELINE_SCENE and loads it. The stand-in layer goes out with the test that took it, so a suite that never asks for a root still meets a clean slate; its source is minted once and reloaded by the next test to ask, and the runner drops that on the way out. Reach for Test.useScene / Test.useSceneEach instead when the test spawns into the scene, saves it, or asserts on its entity set — those mint a fresh scene per test and tear it down after. An edit↔play flip that is rebuilding the scene is waited out first, so the root this returns is the one the body keeps rather than a proxy the rebuild is about to replace.

Returns any — The active root SceneProxy, carrying the path of the scene it holds.

local root = Test.ensureRootLayer(); Test.expect(root.guid).toBeTruthy()

typed/builtin//assetTypes/testSuite/shared/Test/expect

Test.expect(value: any?) -> any

Create an expectation builder over a value. Returns a chainable table with matchers like toBe, toBeTruthy, toContain, toMatch, toHaveLength, toBeCloseTo, and a never flip.

Parameters

  • value any (optional) — The value under inspection.

Returns any — The expectation table. Each matcher records failure (without throwing) so multiple expectations per test still execute.

Test.expect(1 + 1).toBe(2)
Test.expect("hello").toContain("ell")

typed/builtin//assetTypes/testSuite/shared/Test/generateJsonReport

Test.generateJsonReport(stats: Stats, results: { Result }, suites: { SuiteTiming }?) -> string

Render the engine test results as a machine-readable JSON document. This is the format the tests toolbox saves for diffing runs (tests.compare). Consumers should index results by (suite, test).

Parameters

  • stats Stats — Aggregate stats from Test.getStats().
  • results { Result } — Per-test records from Test.getResults().
  • suites { SuiteTiming } (optional) — Per-suite timings, carried through to the report's suites key. Omitted, the key is an empty array.

Returns string — A JSON string { date, stats, results, suites }.

local j = Test.generateJsonReport(Test.getStats(), Test.getResults())

typed/builtin//assetTypes/testSuite/shared/Test/generateMarkdownReport

Test.generateMarkdownReport(stats: Stats, results: { Result }) -> string

Render the engine test results as a Markdown report (the format written to /source/tmp/test_results.md).

Parameters

  • stats Stats — Aggregate stats from Test.getStats().
  • results { Result } — Per-test records from Test.getResults().

Returns string — The Markdown document as a string.

local md = Test.generateMarkdownReport(Test.getStats(), Test.getResults())

typed/builtin//assetTypes/testSuite/shared/Test/getResults

Test.getResults() -> { Result }

Get the detailed result records from the last Test.run.

Returns { Result } — Array of Result entries — one per test executed.

local rs = Test.getResults()

typed/builtin//assetTypes/testSuite/shared/Test/getStats

Test.getStats() -> Stats

Get the aggregate stats from the last Test.run.

Returns Stats — The Stats table: total, passed, failed, skipped.

local s = Test.getStats(); print(s.failed)

typed/builtin//assetTypes/testSuite/shared/Test/gpuLabelsMatching

Test.gpuLabelsMatching(needle: string) -> { string }

Name every GPU label the allocator holds right now that contains needle. A suite that gives its fixtures a prefix of its own reads its whole footprint this way, whatever the sweep around it has allocated, and whatever earlier run of the same suite left standing — the reading is an absolute one, so a leak already in the ledger is still reported.

Parameters

  • needle string — Substring a label must contain to be named.

Returns { string } — The matching labels, sorted.

local mine = Test.gpuLabelsMatching("_test_myfixture")

typed/builtin//assetTypes/testSuite/shared/Test/gpuLedgerReady

Test.gpuLedgerReady(frames: number?) -> boolean

Wait for the device allocator's ledger to be sampled, and say whether this engine has one at all. renderer.gpuMemory().allocator is the device allocator's own record, so it is there on a backend that allocates its own memory and absent on one whose host allocates for it — a browser's WebGPU device among them — and it arrives on a sampled frame rather than on the call that asks. Every reading built on Test.snapshotGpuLabels asks this first: on a device with no ledger each of them answers with silence, which reads exactly like a suite that left nothing behind, so a case stands itself down here instead of passing on a reading it never took.

Parameters

  • frames number (optional) — Frames to wait for the first sample. Default 300.

Returns boolean — True when the ledger is here to be read.

if not Test.gpuLedgerReady() then Test.skip("this device keeps no allocator ledger") end

typed/builtin//assetTypes/testSuite/shared/Test/it

Test.it(name: string, fn: (TestContext) -> ()) -> any?

Define a single test within the enclosing describe. The test body receives an optional context t exposing waitFrames, expect, fail, and identifying metadata.

typed/builtin//assetTypes/testSuite/shared/Test/measurement

Test.measurement(what: string) -> nil

Declare that the running test's verdict is decided by how fast the host ran — a rate, a ratio between two rates, or a wall-clock bound. Such a test asks a real question about the engine and answers it differently on a loaded machine than on a quiet one, so its verdict belongs to a run whose purpose is to measure. A run that admits measurements (Test.measurements, which tests.run { measurements = true } sets) runs the body; anywhere else the call stands the test down carrying what it measures, the way any unmet precondition does.

Parameters

  • what string — What the test measures, named so the stood-down row says which measurement the run declined.

Returns nil — Nothing. The call either returns and the body continues, or stands the test down and does not return.

Test.it("a take costs the run nothing", function() Test.measurement("the rate the engine publishes frames at") end)

typed/builtin//assetTypes/testSuite/shared/Test/openSuiteBoundary

Test.openSuiteBoundary() -> SuiteBoundary

Read the live state a suite is about to be handed, so the state it hands on can be compared against it. Pair with Test.closeSuiteBoundary, which drains the suite's teardown and reports what crossed. The suite envelope opens a boundary before it loads a suite and closes it after, and the one currently open is published as Test.suiteBoundary — so a body reads the world its own suite was handed, and a boundary the body opens nests inside it.

Returns SuiteBoundary — A SuiteBoundary token to hand to Test.closeSuiteBoundary.

local pre = Test.openSuiteBoundary()

typed/builtin//assetTypes/testSuite/shared/Test/prepareTmp

Test.prepareTmp()

Recreate the sandbox tmp root. Removes any leftover files and creates a fresh empty directory at Test.TMP_ROOT. Safe to call from Test.beforeEach.

Test.prepareTmp()

typed/builtin//assetTypes/testSuite/shared/Test/registerCleanup

Test.registerCleanup(fn: () -> ()) -> nil

Push a cleanup function onto the current test's auto-teardown list. Drained LIFO after Test.afterEach, on both success and failure paths, so the cleanup fires even when the test body errors out. Use this when a test allocates additional resources (extra layers.load after the initial useScene, temp VFS paths outside the sandbox, registered MCP tools, …) that the standard useScene auto-teardown doesn't cover.

Parameters

  • fn () -> () — No-arg function to run as cleanup. Errors are caught + logged.

Returns nil

Test.registerCleanup(function() pcall(layers.unload, myRef) end)

typed/builtin//assetTypes/testSuite/shared/Test/releaseRootLayer

Test.releaseRootLayer()

Unload the stand-in root scene layer Test.ensureRootLayer loaded and drop its source folder. The next Test.ensureRootLayer mints a fresh one. A no-op when the run never took a stand-in.

Test.releaseRootLayer()

typed/builtin//assetTypes/testSuite/shared/Test/restoreWorldBaseline

Test.restoreWorldBaseline(baseline: WorldBaseline?)

Restore the layers captured by Test.beginWorldBaseline, leaving the engine in the shape it was found. The run's stand-in root goes first, then each captured layer is reloaded by guid via asset.resolve; the additive flag decides root vs overlay. Per-layer pcall'd so one failed restore doesn't block the rest.

Parameters

  • baseline WorldBaseline (optional) — The token returned by Test.beginWorldBaseline.
Test.restoreWorldBaseline(base)

typed/builtin//assetTypes/testSuite/shared/Test/run

Test.run() -> boolean

Run every registered suite. Resets stats, executes each test body inside a pcall so a runtime error marks the test failed rather than aborting the run, snapshots/cleans up leaked entities between tests, and defensively returns the scene to edit mode if a test leaks play mode. On the way out — pass, fail, OR an uncaught framework error — it always tears down the entire sandbox tmp root, so no test artifacts ever outlive the run.

Returns booleantrue if every test passed (zero failures), false otherwise.

local ok = Test.run()

typed/builtin//assetTypes/testSuite/shared/Test/sandbox

Test.sandbox(tag: string?) -> string

Create a fresh, empty sandbox container folder under the current test's tmp dir and return its absolute path. Pass it as an into container path to asset.create / Material.Create (into = { path = Test.sandbox("mats") }) so the new asset is authored under the sync-excluded /source/tmp/tests/... sandbox instead of the world root. The runner sweeps the folder at end-of-test, and the egress filter keeps everything under it out of the bound world's content store — so even hundreds of fixtures never pollute the user's /source tree or block play / push.

Parameters

  • tag string (optional) — Folder name relative to the current test's sandbox.

Returns string — The absolute container path, guaranteed to exist.

local r = asset.create("material", "gold", { shader = "pbr", into = { path = Test.sandbox("mats") } })

typed/builtin//assetTypes/testSuite/shared/Test/sceneRenderState

Test.sceneRenderState() -> string

The scene's shading state as one string two readings can be compared by: the ambient term, the directional sun, and every punctual light the renderer resolved, with the fields ordered so the same state always encodes the same way.

A test that compares rendered frames with each other rests on all of them having been drawn under one shading state. This is the reading that says so: take it before the frames and again after, and two equal readings mean a difference between the frames belongs to what the test varied.

Returns string — The encoded shading state.

local before = Test.sceneRenderState()

typed/builtin//assetTypes/testSuite/shared/Test/settleReading

Test.settleReading(take: () -> any, agrees: (any, any) -> any, opts: { attempts: number?, framesBetween: number? }?) -> (boolean, number, any)

Take readings of a subject until two consecutive ones agree, and report how many that took.

A rig that has just been assembled reaches the picture over several frames: a material compiles, a probe fills, an atmosphere resolves. A test that measures such a rig has to hold until it stops moving, and the reading that says so is the rig's own — one taken twice.

The call takes ONE reading per attempt and compares it with the attempt before, so a subject that settles at once costs two readings and one that never settles costs attempts. agrees decides how close two readings have to be: give it the tolerance the test's own assertions leave room for. Whether two readings off a still rig come back equal is a property of the host that drew them, so a poll that asks for equality is a poll whose length the host decides — and every attempt it spends costs whatever a reading costs.

Parameters

  • take () -> any — Zero-arg function returning one reading. Called once per attempt.
  • agrees (any, any) -> any — Called with the previous reading and the current one; truthy means the subject has settled.
  • opts { attempts: number?, framesBetween: number? } (optional)attempts — how many readings to take before giving up (default 12, clamped to a minimum of 2). framesBetween — engine frames to wait between readings (default 4, clamped to a minimum of 0).

Returns (boolean, number, any) — Whether two consecutive readings agreed, how many readings that cost, and the last reading taken.

local ok, tries = Test.settleReading(shoot, function(a, b) return apart(a, b) < 1 end)

typed/builtin//assetTypes/testSuite/shared/Test/settleWorldTeardown

Test.settleWorldTeardown(maxFrames: number?) -> WorldQuiescence

Hold until the work a teardown queued has landed: yields frames until every reading — live entities, loaded layers, registered render features, resident offscreen render targets, layer loads in flight — is unchanged for five consecutive frames. A despawn applies a frame after it is asked for, the render target that entity owned is reclaimed the frame after that, and a layer unload drains over several more, so a caller that yields a single frame hands the rest of that chain to whatever runs next. A test that tears down world state and then reads a count off the engine wants this between the two.

Parameters

  • maxFrames number (optional) — How long to wait before reporting instead. Defaults to sixty. A budget below the stability window cannot reach it, so it is answered settled = false; nought or less is answered that way without waiting a frame.

Returns WorldQuiescence{ settled, frames, moving }moving names the facets seen to change on the last frame any of them did, and is empty whenever the wait saw nothing move, so settled is what says whether the world had stopped or the budget ran out first. A caller that cannot reach a frame boundary is answered settled = false after nought frames, with moving naming yield.

local q = Test.settleWorldTeardown(); print(q.settled, q.frames)

typed/builtin//assetTypes/testSuite/shared/Test/skip

Test.skip(name: string, fn: ((TestContext) -> ())?) -> any?

Skip a test, in either of the two moments a test can be skipped.

With a body, it DECLARES a skipped test: the Test.it shape, marked skip = true, so the runner counts it under skipped and leaves the body unrun. That is the form a describe takes, and a reason arriving there on its own raises naming it.

Inside a running test — its body, or one of the hooks around it — a call carrying a reason alone STANDS THAT TEST DOWN: the test ends at the call and is reported skipped, and the reason travels with it to the console, the per-test record and the report. That is the shape for a test whose precondition this engine does not meet, which has nothing to verify and has earned no pass. Anything the test's hooks record around the stand-down is a failure of the test they ran for and outranks it.

Parameters

  • name string — The test's name when declaring one; the reason it stood down when standing the running test down.
  • fn ((TestContext) -> ()) (optional) — Test body. Supplying one declares a test wherever the call is made. Left out, the call stands down the test that is running, and raises where no test is running.

Returns any? — The registered test entry when declaring one, or nil outside a describe. A stand-down ends the test rather than returning.

Test.skip("not yet", function() error("...") end)
Test.it("draws", function() if not engine.gpuCompute then Test.skip("no GPU compute") end end)

typed/builtin//assetTypes/testSuite/shared/Test/snapshotEntities

Test.snapshotEntities() -> { [any]: boolean }

Snapshot the set of entity ids currently alive. Pair with Test.sweepLeakedEntities to despawn everything spawned after the snapshot — the safety net for entities leaked outside a test body (suite-registration side effects, crashed tests) that the per-test cleanup can't see.

Returns { [any]: boolean } — A { [entityId] = true } set.

local pre = Test.snapshotEntities(); ...; Test.sweepLeakedEntities(pre)

typed/builtin//assetTypes/testSuite/shared/Test/snapshotGpuLabels

Test.snapshotGpuLabels() -> { [string]: number }

Snapshot the GPU resources the allocator holds right now, mapping each label to how many allocations carry it. Pair with Test.detectLeakedGpuLabels, which names the labels a suite added and left standing. A GPU resource created under a name — a compute storage target, a 3D texture, a history pair — answers to that name for as long as the engine runs, so whoever names one owns releasing it: a root scene load and renderer.collect both leave it where it is.

Returns { [string]: number } — A { [label] = allocations } map.

local pre = Test.snapshotGpuLabels()

typed/builtin//assetTypes/testSuite/shared/Test/snapshotLayers

Test.snapshotLayers() -> { [string]: boolean }

Snapshot the layers loaded right now, by guid. Pair with Test.sweepLeakedLayers, which unloads every layer loaded after the snapshot — the layer analogue of Test.snapshotEntities, for a suite that loads a scene and leaves it standing for every suite behind it.

Returns { [string]: boolean } — A { [layerGuid] = true } set.

local pre = Test.snapshotLayers(); ...; Test.sweepLeakedLayers(pre)

typed/builtin//assetTypes/testSuite/shared/Test/snapshotRenderFeatures

Test.snapshotRenderFeatures() -> { [string]: string }

Snapshot the render features registered right now, mapping each guid to the asset identity it was created from. Pair with Test.detectLeakedRenderFeatures, which names the ones registered after the snapshot. A feature draws on every frame of every suite behind the one that registered it, so one left standing changes what a later suite's pixel and draw-call readings measure.

Returns { [string]: string } — A { [featureGuid] = identity } map.

local pre = Test.snapshotRenderFeatures()

typed/builtin//assetTypes/testSuite/shared/Test/snapshotScreens

Test.snapshotScreens() -> { [string]: boolean }

Snapshot the UI screens currently registered, mapping each name to its current visibility. Pair with Test.sweepLeakedScreens, which unregisters every screen registered after the snapshot AND restores the visibility of the snapshotted screens — the safety net for UI screens a suite mounts and never tears down (which pile up as ghost screens) and for a suite that toggles a pre-existing screen's visibility (e.g. Z.screens.hideAll) and leaves the user's own UI hidden after the run.

Returns { [string]: boolean } — A { [screenName] = visible } map.

local pre = Test.snapshotScreens(); ...; Test.sweepLeakedScreens(pre)

typed/builtin//assetTypes/testSuite/shared/Test/snapshotSourceContent

Test.snapshotSourceContent() -> { [string]: boolean }

Snapshot the set of top-level /zero/source entry names that exist before a suite runs. Paired with Test.detectSourcePollution to flag content a suite leaks OUTSIDE its sandbox — world state that would bleed into later suites (and the publish gate). The world-content analogue of Test.snapshotEntities.

Returns { [string]: boolean } — A name->true set of the current top-level entries.

local pre = Test.snapshotSourceContent()

typed/builtin//assetTypes/testSuite/shared/Test/snapshotSourceSync

Test.snapshotSourceSync() -> SourceSyncMark

Snapshot what holds /zero/source and how far the engine has got materialising the bound world's own content into it. Two marks bracket a suite and feed Test.sourceDiffAttributable, which reads them to say whether the entries that appeared at the world root across that window are the suite's doing.

Returns SourceSyncMark{ worldBound, subscribed, contentSynced, applied } for this instant.

local mark = Test.snapshotSourceSync()

typed/builtin//assetTypes/testSuite/shared/Test/sourceDiffAttributable

Test.sourceDiffAttributable(pre: SourceSyncMark, post: SourceSyncMark) -> (boolean, string?)

Whether a /zero/source name diff taken across two marks names only what the suite between them wrote. A world materialises its own content into the world root on its own schedule — a session that is still applying content, that applied some while the suite ran, or that never reported at all while a world held the root, puts entries there that the diff would otherwise read as the suite's.

Parameters

  • pre SourceSyncMark — The mark taken before the suite ran.
  • post SourceSyncMark — The mark taken after it finished.

Returns (boolean, string?)true when the diff is the suite's alone; otherwise false plus a phrase naming what held the source root instead.

local mine, why = Test.sourceDiffAttributable(pre, Test.snapshotSourceSync())

typed/builtin//assetTypes/testSuite/shared/Test/sweepLeakedEntities

Test.sweepLeakedEntities(preSnapshot: { [any]: boolean }) -> number

Despawn every entity not present in preSnapshot (from Test.snapshotEntities). Unlocks destroy-locked entities first so even PlayerOwned leaks can be cleaned. Yields one frame so the deferred despawns drain before the next suite snapshots its own baseline.

Parameters

  • preSnapshot { [any]: boolean } — The set returned by Test.snapshotEntities.

Returns number — Number of entities despawned.

Test.sweepLeakedEntities(pre)

typed/builtin//assetTypes/testSuite/shared/Test/sweepLeakedLayers

Test.sweepLeakedLayers(preSnapshot: { [string]: boolean }) -> { string }

Unload every non-persistent layer loaded since preSnapshot. Persistent layers (the editor overlay) are left alone. The answer names the layers it asked the engine to unload, which covers what appeared between the snapshot and now; a layer that went stays gone. Each unload lands over the frames after the call, so a caller reading the loaded set straight back waits for what it asked for — Test.waitUntil on Test.snapshotLayers, or Test.settleWorldTeardown when it wants the whole cascade behind it.

Parameters

  • preSnapshot { [string]: boolean } — The set returned by Test.snapshotLayers.

Returns { string } — The guids unloaded.

Test.sweepLeakedLayers(pre)

typed/builtin//assetTypes/testSuite/shared/Test/sweepLeakedScreens

Test.sweepLeakedScreens(preSnapshot: { [string]: boolean }) -> number

Restore the screen set captured by Test.snapshotScreens: unregister every screen registered after the snapshot, and put each snapshotted screen's visibility back to how the run found it. Screens reloaded with a world layer come back in the restore pass, so only screens a suite mounted outside a layer get swept; a suite that hid a pre-existing screen (e.g. via Z.screens.hideAll) gets that screen re-shown so the user's UI doesn't stay blank after a test run. Yields one frame so the changes drain before the caller reads the screen set again.

Parameters

  • preSnapshot { [string]: boolean } — The map returned by Test.snapshotScreens.

Returns number — Number of screens unregistered (leaked).

Test.sweepLeakedScreens(pre)

typed/builtin//assetTypes/testSuite/shared/Test/tmpPath

Test.tmpPath(relativePath: string?) -> string

Build a sandboxed path for a test fixture under the CURRENT test's own folder (TMP_ROOT/<suite>/<test>/...). Every write a test makes should go through this so it lands inside the per-test sandbox the runner cleans up automatically — never at a hand-rolled /source/... path that survives the run and syncs to spacetime. Empty / non-string relativePath returns the test's folder itself. Called outside a running test it falls back to the suite folder, then to TMP_ROOT (see currentTestDir).

Parameters

  • relativePath string (optional) — Path fragment relative to the current test's folder.

Returns string — The fully-qualified path under the per-test sandbox.

local p = Test.tmpPath("Foo.component/init.luau")

typed/builtin//assetTypes/testSuite/shared/Test/uniqueName

Test.uniqueName(stem: string) -> string

A name no earlier run and no earlier case in this engine has used, so a registry, a cache, a log scope or a VFS path keyed by name answers for the thing this case just made under it. The counter separates the cases within one engine and the random draw separates one engine's names from another's, which is what a suite sharded across parallel engines over one world needs.

Parameters

  • stem string — What the name is for. It stays at the front, so an artifact that outlives its case still says which case minted it.

Returns string<stem>_<n>_<r> — the stem, this VM's next counter value, and a random draw.

local name = Test.uniqueName("probe_material") --> "probe_material_7_412903"

typed/builtin//assetTypes/testSuite/shared/Test/useScene

Test.useScene(opts: { fixture: string?, additive: boolean? }?) -> any

Mint an isolated scene for the current test. Duplicates the body of opts.fixture (a .scene folder) into a unique path under Test.TMP_ROOT/scenes/ and loads it through layers.load. The freshly-loaded layer becomes the active root (or an additive overlay when opts.additive == true).

The duplicate is registered for auto-teardown on the current test — when the test body finishes (success OR failure), the runner unloads the layer and removes the sandbox copy. Whatever was authored on top of the duplicate (entities, components, dirty edits) is therefore discarded automatically. Nothing the test writes lands in user content.

Tests that need a richer baseline (a Player template, a Camera rig, custom lighting) pass an explicit opts.fixture pointing at a fixture folder of their own — useScene treats it like any other .scene source.

Parameters

  • opts { fixture: string?, additive: boolean? } (optional) — Optional { fixture, additive }. fixture is the VFS path of a .scene folder (defaults to Test.DEFAULT_SCENE_FIXTURE). additive controls whether the duplicate loads as an additive overlay alongside whatever else is loaded, or replaces the current root (the default).

Returns any — The loaded SceneProxy (same instance layers.active / layers.find would hand back) so callers can read .guid / .name / spawn entities into it / register additive overlays.

local scene = Test.useScene()                            -- empty root
local hud = Test.useScene({ additive = true })           -- empty overlay
local rig = Test.useScene({ fixture = "/zero/source/libs/@builtin/_canonical/static_player.scene" })

typed/builtin//assetTypes/testSuite/shared/Test/useSceneEach

Test.useSceneEach(opts: { fixture: string?, additive: boolean? }?) -> nil

Suite-level sugar for "mint a fresh scene before every test in this suite". Equivalent to writing Test.beforeEach(function() Test.useScene(opts) end) but composable — preserves any beforeEach the suite already registered and adds the useScene call after it.

Parameters

  • opts { fixture: string?, additive: boolean? } (optional) — Same shape as Test.useScene's opts. Forwarded as-is.

Returns nil

Test.describe("VFS Entity", function()
Test.useSceneEach()              -- empty root scene per test
Test.it("...", function() ... end)
end)

typed/builtin//assetTypes/testSuite/shared/Test/waitForSteadyScene

Test.waitForSteadyScene(stableFrames: number?, maxFrames: number?) -> boolean

Hold until the scene's shading state has read the same for stableFrames frames in a row and no scene load is in flight.

A scene the engine is still settling into changes what it draws over the frames after the call that changed it: a layer's entities drain, their lights leave the resolved set, and the picture moves with them. Waiting on the reading rather than on a frame count holds for as long as the machine the test runs on needs.

Parameters

  • stableFrames number (optional) — How many consecutive frames must read the same. Defaults to 4; clamped to a minimum of 1.
  • maxFrames number (optional) — Frame budget before giving up. Defaults to 240.

Returns boolean — Whether the scene reached that many identical readings in budget.

Test.waitForSteadyScene()

typed/builtin//assetTypes/testSuite/shared/Test/waitFrames

Test.waitFrames(n: number?)

Wait at least n engine frames before continuing. Yields the running coroutine and returns control after at least n frame boundaries have elapsed. When invoked through a non-yieldable C-call chain the underlying task.wait raises; the runner catches that sentinel and treats the test as SKIPPED for that invocation. Run suites via the tests toolbox (tests.run) — or call Test.run from a yieldable context — to actually exercise the wait paths.

Parameters

  • n number (optional) — Frame count. Defaults to 1; clamped to a minimum of 1.
function(t) t.waitFrames(2); Test.expect(entity.exists(id)).toBe(false) end

typed/builtin//assetTypes/testSuite/shared/Test/waitUntil

Test.waitUntil(predicate: () -> any, maxFrames: number?) -> boolean

Parameters

  • predicate () -> any
  • maxFrames number (optional)

Returns boolean

typed/builtin//assetTypes/testSuite/shared/Test/withSteadyScene

Test.withSteadyScene(body: () -> any, attempts: number?) -> (any?, string?)

Run body inside a window in which the scene's shading state holds still, so the frames it takes can be compared with each other.

The call waits for the scene to steady, reads Test.sceneRenderState(), and runs the body while a watcher reads the state again on every frame the body spans. A body whose frames were taken across a change in that state measured something the test did not vary, so it is run again on a scene that has settled — up to attempts times, after which the call reports what moved instead of handing back frames. Reading every frame catches a change that lands and is undone inside one window.

The body measures and returns; put the assertions on what it returns. A body that runs twice records anything it asserts twice.

Parameters

  • body () -> any — Zero-arg function that takes the frames and returns them.
  • attempts number (optional) — How many windows to try. Defaults to 4; clamped to 1.

Returns (any?, string?) — What the body returned, or (nil, reason) when the scene kept moving.

local frames, reason = Test.withSteadyScene(function() ... end)

typed/builtin//components/Asset/public/bakeIntoScene

public.bakeIntoScene()

Promote the bundle's spawned children from temporary to permanent scene state and remove the Asset component, "baking" the bundle's contents directly into the owning scene. Subsequent saves persist the baked entities verbatim and stop replaying the bundle template.

Returns true once the bake completes.

asset:bakeIntoScene()

typed/builtin//components/BoxCollider/public/setHalf

public.setHalf(size: vec3)

Set the box's half-extents. Recreates the collision shape.

Parameters

  • size vec3 — Half-extents as { x, y, z } or { x = , y = , z = }.
collider:setHalf({ 1, 2, 0.5 })

typed/builtin//components/BoxCollider/public/setTrigger

public.setTrigger(trigger: boolean)

Toggle trigger mode. A trigger reports overlaps without blocking.

Parameters

  • trigger boolean — true to detect overlaps only, false to collide solidly.
collider:setTrigger(true)

typed/builtin//components/Camera/public/capture

public.capture()

Capture a frame from this camera to its current render target. Alias of render() — kept so the verb matches the agent-facing capture toolbox.

cam:capture()

typed/builtin//components/Camera/public/lookAt

public.lookAt(target: string | table) -> (boolean, string?)

Aim this camera at a world position or another entity. Returns whether the camera was rotated, so a target naming an entity the scene does not carry is reported rather than leaving the camera on its previous aim.

Parameters

  • target string | table — An entity id string, an entity name, an entity proxy, or a position table ({x, y, z} array form or {x=, y=, z=} map form).

Returns (boolean, string?) — True when the camera was rotated, and nil for the second value. False plus the reason otherwise — "unresolved" when the target names no entity, "no-transform" when one of the two carries no transform, "degenerate" when the camera already sits on the point it was asked to face.

cam:lookAt(playerId)
cam:lookAt("player")
cam:lookAt({0, 1, 0})
cam:lookAt({x = 0, y = 1, z = 0})

typed/builtin//components/Camera/public/render

public.render(rtGuid: string?)

Schedule a single-frame render for this camera. Works whether the component is enabled or disabled. Uses the camera's current transform, fov, near/far, and render layers. Pass a render-target guid to render this frame into THAT texture instead of the camera's configured output — the one-shot-target form the capture path uses to read a camera's exact view back without disturbing where it normally renders.

typed/builtin//components/Camera/public/setTargetTexture

public.setTargetTexture(tex: renderer.TextureHandle?)

Point the camera at a GPU texture to render into, or back to the main viewport. Create the texture first with renderer.texture.create({ width, height }) (it owns its own size); the camera only references it — free it with renderer.destroy(handle) when done.

Parameters

  • tex renderer.TextureHandle (optional) — A TextureHandle to render into, or nil for the main viewport.
local tex = renderer.texture.create({ width = 512, height = 512 })
cam:setTargetTexture(tex)   -- render into the texture
cam:setTargetTexture(nil)   -- back to the main viewport

typed/builtin//components/Humanoid/public/attach

public.attach(point: string, childId: string)

Parent an existing entity under the bone at point (see socket), so it rides that bone's animation. Convenience over socket(point) + setParent.

Parameters

  • point string — string — a canonical role, or an exact bone name.
  • childId string — string — the entity to attach.

Returns entity (the socket bone) | nil when the point doesn't resolve.

typed/builtin//components/Humanoid/public/bone

public.bone(name: string)

Lookup a single bone entity by canonical name ("Head", "Hand_R", etc). Returns nil when the bone isn't present (e.g. a skeleton missing the finger sub-tree).

Parameters

  • name string — string — canonical bone name from the humanoid table

Returns entity | nil

typed/builtin//components/Humanoid/public/socket

public.socket(point: string)

Resolve an attachment point to the live bone entity for THIS body, so content attaches a prop to a known point WITHOUT knowing the per-mesh bone name. point is a canonical ROLE ("righthand", "lefthand", "head", "hips", …) resolved mesh-independently via the body's RetargetProfile; if it matches no role it falls back to an exact bone NAME (per-rig custom attachment). Returns the bone entity, or nil when absent.

Parameters

  • point string — string — a canonical role, or an exact bone name.

Returns entity | nil

typed/builtin//components/Light/public/lightMobility

public.lightMobility() -> string

How GI baking should treat this light, resolved to "static", "mixed" or "dynamic". Every component that puts a light in the scene answers this, which is how a bake finds the lights it has to account for without knowing what component authored them.

Returns string — The resolved mobility.

if light:lightMobility() == "dynamic" then ... end

typed/builtin//components/Light/public/resolveMobility

public.resolveMobility() -> string

Resolve this light's mobility to "static", "mixed" or "dynamic" — how GI baking treats it. An explicit mobility field wins; "auto" resolves to "dynamic" for a light something carries (non-world participation, an animated or physics-driven entity) and "mixed" for the rest.

"static" bakes the light whole — direct light and bounce — and withholds its live contribution, so it costs nothing per frame and lights nothing that was not there at bake time.

"mixed" bakes only its bounce and keeps its direct light and shadows live, so it still lights and shadows anything that moves. This is what "auto" picks, because a light that stands still still shines on characters walking under it.

"dynamic" keeps the light out of the bake entirely.

Returns string

local mob = light:resolveMobility()

typed/builtin//components/Light/public/setCastsShadows

public.setCastsShadows(b: boolean)

Enable or disable shadow casting. Point lights cast omnidirectional (cube) shadows; spot lights cast a single projected shadow. Capacity is capped per kind — past the cap the light stays lit but unshadowed.

Parameters

  • b booleantrue to cast shadows, false to disable.
light:setCastsShadows(true)

typed/builtin//components/Light/public/setColor

public.setColor(color: table)

Set the light color. Values >1 are auto-scaled from 0..255.

Parameters

  • color table{r, g, b} array or {r=, g=, b=} map.
light:setColor({1, 0.8, 0.5})
light:setColor({r = 255, g = 200, b = 128})

typed/builtin//components/Light/public/setDirection

public.setDirection(dirOrX: table | number, y: number?, z: number?)

Set the direction vector (directional lights only). Accepts three numbers or a single {x, y, z} / {x=, y=, z=} vector.

Parameters

  • dirOrX table | number — Either the x component, or a {x, y, z} array / {x=, y=, z=} map.
  • y number (optional) — The y component when the first argument is a number.
  • z number (optional) — The z component when the first argument is a number.
light:setDirection(-0.5, -1, -0.3)
light:setDirection({-0.5, -1, -0.3})

typed/builtin//components/Light/public/setIntensity

public.setIntensity(i: number)

Set the light intensity (0..N).

Parameters

  • i number — Intensity scalar.
light:setIntensity(2.5)

typed/builtin//components/Light/public/setKind

public.setKind(lightKind: string)

Switch the light kind ("point" / "directional" / "ambient" / "distant"). "directional" aims the scene's sun, which is a single field: setting it replaces whatever the sun was. "distant" is parallel light held as a row of the scene's light buffer, so several coexist — DirectionalLight is the component that authors one.

Parameters

  • lightKind string — One of "point", "directional", "ambient", "distant".
light:setKind("directional")

typed/builtin//components/Light/public/setRadius

public.setRadius(r: number)

Set the radius (point lights only).

Parameters

  • r number — Radius in world units.
light:setRadius(15)

typed/builtin//components/Model/public/applySessionMaterial

public.applySessionMaterial(handle: any?)

Show a session-created runtime material on this Model in place of its authored material. The authored material field is never touched — persisted state always carries the authored ref, so nothing broken can be saved. The handle is kept in the session store under renderer.material.sessionKeyFor(entityId), so awake re-adopts it across VM reloads; on a fresh boot (runtime materials gone) the Model renders its authored material again automatically. Called with no handle, it copies this Model's current material into a fresh session material keyed to this entity, so later setMaterialProperty edits land on the copy instead of the shared authored material.

A copy per entity is what a DIFFERENT LOOK per entity costs — its own pipeline binding and its own row in the material table. When entities want the same look and differ only in a VALUE, renderer.instanceData.set writes one of the four vec4 lanes every drawn object already carries, which the shader reads as input.shader_data[lane], and one material serves them all.

Parameters

  • handle any (optional) — The runtime material handle OBJECT, as renderer.material.create and renderer.material.animatedTexture return it. Its guid field is the material's registry key, which is what renderer.material.setProperty / describe / destroy take; this call takes the handle itself. Omit to copy this Model's current material into a new session material.
model:applySessionMaterial(handle)
model:applySessionMaterial()  -- copy the current material for per-entity edits

typed/builtin//components/Model/public/clearOutline

public.clearOutline()

Remove the outline from this mesh.

model:clearOutline()

typed/builtin//components/Model/public/clearTint

public.clearTint()

Reset the tint so the mesh renders with its original material colour.

model:clearTint()

typed/builtin//components/Model/public/getMaterialProperty

public.getMaterialProperty(property: string)

Read a property from this Model's material — the session material when one is active, else the authored material.

Parameters

  • property string — Property name (string).

Returns The current value of the property, or nil when no material is assigned.

local color = model:getMaterialProperty("baseColor")

typed/builtin//components/Model/public/getMaterialPropertyNames

public.getMaterialPropertyNames()

List all property names available on this Model's material — the session material when one is active, else the authored material.

Returns Array of property name strings (empty when no material is assigned).

for _, name in ipairs(model:getMaterialPropertyNames()) do print(name) end

typed/builtin//components/Model/public/resolveMobility

public.resolveMobility() -> string

Resolve this entity's effective mobility: "static" (stands still — receives lightmaps, occludes baked light) or "movable" (moves — samples probe volumes for indirect light). An explicit mobility field value wins; "auto" derives it from the entity AND everything carrying it: anything that animates, simulates, or is driven (a non-static Physics body, a SkinnedModel, a ClipPlayer, a Mover/FreeMover — on this entity or any ancestor — or a non-world runtime participation) is movable, everything else is static.

Returns string"static" or "movable".

if model:resolveMobility() == "static" then Lightmap.bake(id) end

typed/builtin//components/Model/public/restoreSessionMaterial

public.restoreSessionMaterial() -> boolean

End the session-material swap: the Model renders its authored material again.

Returns booleantrue when a session material was active, else false.

model:restoreSessionMaterial()

typed/builtin//components/Model/public/setMaterialProperties

public.setMaterialProperties(props: { [string]: any }) -> number

Set many properties on this Model's material in one call, the plural of setMaterialProperty and with the same target: this entity's session copy when one is active, else the authored material every entity sharing it takes. Each key resolves against the material's declared vocabulary the way the singular resolves it; a property the material's shader does not expose is skipped, so one patch table serves models on different shaders.

Parameters

  • props { [string]: any } — Table of { [propertyName] = value } pairs.

Returns number — Number of properties applied.

model:setMaterialProperties({ roughness = 0.2, metallic = 0.9 })

typed/builtin//components/Model/public/setMaterialProperty

public.setMaterialProperty(property: string, value: any?)

Set a property on this Model's material. With a session material active (see applySessionMaterial), the write lands on this entity's session copy alone; otherwise it changes the shared material at runtime, which every entity sharing it takes since material properties are registry-wide. Either way the change reaches the GPU and not the material's mat.yaml — call material:saveDefinition() to write the current values into the asset. The key resolves against the material's declared vocabulary on both targets, so the same name reaches the same uniform.

Parameters

  • property string — Property name (string).
  • value any (optional) — New value for the property. Type depends on the property.
model:setMaterialProperty("baseColor", {1, 0, 0, 1})
model:setMaterialProperty("roughness", 0.5)

typed/builtin//components/Model/public/setOutline

public.setOutline(color: table, intensity: number?)

Set the outline colour and intensity for this mesh.

Parameters

  • color table — Outline color as {r, g, b} array or {r=, g=, b=} map. Channel values are 0-1.
  • intensity number (optional) — Outline intensity (0 = none, 1 = full). Defaults to 1.
model:setOutline({0, 1, 0})
model:setOutline({r = 1, g = 1, b = 0}, 0.8)

typed/builtin//components/Model/public/setTint

public.setTint(color: table, blend: number?)

Set the mesh tint colour and blend amount.

Parameters

  • color table — Tint color as {r, g, b} array or {r=, g=, b=} map. Channel values are 0-1.
  • blend number (optional) — Blend amount (0 = no tint, 1 = full tint). Defaults to 1.
model:setTint({1, 0, 0})
model:setTint({r = 1, g = 0, b = 0}, 0.5)

typed/builtin//components/Physics/public/addVelocity

public.addVelocity(dx: number, dy: number, dz: number)

Add to the body's current linear velocity.

Parameters

  • dx number — Velocity delta along world X.
  • dy number — Velocity delta along world Y.
  • dz number — Velocity delta along world Z.
body:addVelocity(0, 2, 0)

typed/builtin//components/Physics/public/applyForce

public.applyForce(x: number, y: number, z: number)

Apply a continuous force to this body (Newtons).

Parameters

  • x number — Force component along world X.
  • y number — Force component along world Y.
  • z number — Force component along world Z.
body:applyForce(0, 100, 0)

typed/builtin//components/Physics/public/applyImpulse

public.applyImpulse(x: number, y: number, z: number)

Apply an instantaneous impulse to this body (kg·m/s).

Parameters

  • x number — Impulse component along world X.
  • y number — Impulse component along world Y.
  • z number — Impulse component along world Z.
body:applyImpulse(5, 0, 0)

typed/builtin//components/Physics/public/applyTorque

public.applyTorque(x: number, y: number, z: number)

Apply a torque to this body (rotational force).

Parameters

  • x number — Torque around world X.
  • y number — Torque around world Y.
  • z number — Torque around world Z.
body:applyTorque(0, 5, 0)

typed/builtin//components/Physics/public/setVelocity

public.setVelocity(x: number, y: number, z: number)

Set the body's linear velocity directly.

Parameters

  • x number — Velocity component along world X.
  • y number — Velocity component along world Y.
  • z number — Velocity component along world Z.
body:setVelocity(0, 10, 0)

typed/builtin//components/PlayerNameLabel/public/refresh

public.refresh()

Re-read the owner and rasterise the label. Called by PlayerAvatar when the owning identity or its display name changes.

typed/builtin//components/ProceduralSky/public/applyPreset

public.applyPreset(preset: string)

Apply a named look (clear_day, sunset, sunrise, overcast, night).

Parameters

  • preset string — One of the named presets.
sky:applyPreset("sunset")

typed/builtin//components/ProceduralSky/public/setGroundColor

public.setGroundColor(color: { [any]: number })

Set the ground (below-horizon) colour. Values >1 auto-scale from 0..255.

Parameters

  • color { [any]: number }{r, g, b} array or {r=, g=, b=} map.
sky:setGroundColor({0.3, 0.25, 0.2})

typed/builtin//components/ProceduralSky/public/setHorizonColor

public.setHorizonColor(color: { [any]: number })

Set the horizon sky colour. Values >1 auto-scale from 0..255.

Parameters

  • color { [any]: number }{r, g, b} array or {r=, g=, b=} map.
sky:setHorizonColor({0.9, 0.4, 0.2})

typed/builtin//components/ProceduralSky/public/setTimeOfDay

public.setTimeOfDay(hours: number)

Set the time of day in hours (0..24). Orients the sun and the day/night gradient.

Parameters

  • hours number — 0..24.
sky:setTimeOfDay(18.5)

typed/builtin//components/ProceduralSky/public/setZenithColor

public.setZenithColor(color: { [any]: number })

Set the zenith (straight-up) sky colour. Values >1 auto-scale from 0..255.

Parameters

  • color { [any]: number }{r, g, b} array or {r=, g=, b=} map.
sky:setZenithColor({0.05, 0.1, 0.3})

typed/builtin//components/SkinnedModel/public/applySessionMaterial

public.applySessionMaterial(handle: any?)

Show a session-created runtime material on this SkinnedModel in place of its authored material. The authored material field is never touched — persisted state always carries the authored ref. The handle is kept in the session store under renderer.material.sessionKeyFor(entityId), so awake re-adopts it across VM reloads; on a fresh boot (runtime materials gone) the SkinnedModel renders its authored material again automatically.

Parameters

  • handle any (optional) — The runtime material handle (from renderer.material.create).
skinned:applySessionMaterial(handle)

typed/builtin//components/SkinnedModel/public/clearOutline

public.clearOutline()

Remove the outline from this mesh.

sm:clearOutline()

typed/builtin//components/SkinnedModel/public/clearTint

public.clearTint()

Reset the tint so the mesh renders with its original material colour.

sm:clearTint()

typed/builtin//components/SkinnedModel/public/getMaterialProperty

public.getMaterialProperty(property: string)

Read a property from this model's material.

Parameters

  • property string — Property name (string).

Returns The current value of the property, or nil when no material is assigned.

local color = sm:getMaterialProperty("baseColor")

typed/builtin//components/SkinnedModel/public/getMaterialPropertyNames

public.getMaterialPropertyNames()

List all property names available on this model's material.

Returns Array of property name strings (empty when no material is assigned).

for _, name in ipairs(sm:getMaterialPropertyNames()) do print(name) end

typed/builtin//components/SkinnedModel/public/restoreSessionMaterial

public.restoreSessionMaterial() -> boolean

End the session-material swap: the SkinnedModel renders its authored material again.

Returns booleantrue when a session material was active, else false.

skinned:restoreSessionMaterial()

typed/builtin//components/SkinnedModel/public/setMaterialProperties

public.setMaterialProperties(props: { [string]: any }) -> number

Set many properties on this model's material in one call, the plural of setMaterialProperty. Affects every entity sharing the material since material properties are registry-wide. A property the material's shader does not expose is skipped, so one patch table serves models on different shaders.

Parameters

  • props { [string]: any } — Table of { [propertyName] = value } pairs.

Returns number — Number of properties applied.

sm:setMaterialProperties({ roughness = 0.5, metallic = 0.2 })

typed/builtin//components/SkinnedModel/public/setMaterialProperty

public.setMaterialProperty(property: string, value: any?)

Set a property on this model's material at runtime. Affects every entity sharing the material since material properties are registry-wide, and reaches the GPU rather than the material's mat.yaml — call material:saveDefinition() to write the current values into the asset.

Parameters

  • property string — Property name (string).
  • value any (optional) — New value for the property. Type depends on the property.
sm:setMaterialProperty("roughness", 0.5)

typed/builtin//components/SkinnedModel/public/setOutline

public.setOutline(color: table, intensity: number?)

Set the outline colour and intensity for this mesh.

Parameters

  • color table — Outline color as {r, g, b} array or {r=, g=, b=} map. Channel values are 0-1.
  • intensity number (optional) — Outline intensity (0 = none, 1 = full). Defaults to 1.
sm:setOutline({0, 1, 0})
sm:setOutline({r = 1, g = 1, b = 0}, 0.8)

typed/builtin//components/SkinnedModel/public/setTint

public.setTint(color: table, blend: number?)

Set the mesh tint colour and blend amount.

Parameters

  • color table — Tint color as {r, g, b} array or {r=, g=, b=} map. Channel values are 0-1.
  • blend number (optional) — Blend amount (0 = no tint, 1 = full tint). Defaults to 1.
sm:setTint({1, 0, 0})
sm:setTint({r = 1, g = 0, b = 0}, 0.5)

typed/builtin//components/Text3D/public/getSize

public.getSize()

Measure the rasterised text in pixels.

Returns Table with width and height in pixels (zero when not yet rasterised).

typed/builtin//components/Text3D/public/getText

public.getText()

Read the current text content.

Returns Current text string.

typed/builtin//components/Text3D/public/getWorldSize

public.getWorldSize()

Read the world-space size of the text quad after rasterisation.

Returns Table with width and height in world units.

typed/builtin//components/Text3D/public/refresh

public.refresh()

Force the text to re-rasterise now.

t:refresh()

typed/builtin//components/Text3D/public/setStyle

public.setStyle(options: table)

Update one or more style fields in a single call. Unknown keys are ignored. Each changed field fires the reactive rebuild (position-only for offset/pivot keys, a re-rasterise for styling keys).

Parameters

  • options table — Table of style overrides keyed by public-field name.
t:setStyle({fontSize = 64, color = "yellow"})

typed/builtin//components/Text3D/public/setText

public.setText(content: string)

Replace the displayed text content.

Parameters

  • content string — New text string.
t:setText("Hello World")

typed/builtin//controller/camera_rig/R/applyPose

R.applyPose(selfId: string, camX: number, camY: number, camZ: number, lookX: number, lookY: number, lookZ: number)

Write a camera pose onto an entity, aiming at a point. Both the pose and the aim point are expressed in the camera entity's own transform frame — parent-relative when the camera is parented, world space when it is not.

Parameters

  • selfId string — The camera entity id.
  • camX number — Camera X.
  • camY number — Camera Y.
  • camZ number — Camera Z.
  • lookX number — Aim point X.
  • lookY number — Aim point Y.
  • lookZ number — Aim point Z.
CameraRig.applyPose(id, cx, cy, cz, tx, ty, tz)

typed/builtin//controller/camera_rig/R/damp

R.damp(current: number, goal: number, tau: number, dt: number) -> number

Frame-rate independent damping toward a goal.

Parameters

  • current number — Current value.
  • goal number — Target value.
  • tau number — Time constant in seconds; 0 snaps.
  • dt number — Frame delta in seconds.

Returns number — The damped value, which never overshoots the goal.

pos.x = CameraRig.damp(pos.x, goal.x, 0.2, dt)

typed/builtin//controller/camera_rig/R/pullIn

R.pullIn(pivotX: number, pivotY: number, pivotZ: number, camX: number, camY: number, camZ: number, radius: number, excludeId: string?) -> (number, number, number)

Pull a camera position in toward the pivot when geometry intervenes.

Parameters

  • pivotX number — Pivot X.
  • pivotY number — Pivot Y.
  • pivotZ number — Pivot Z.
  • camX number — Desired camera X.
  • camY number — Desired camera Y.
  • camZ number — Desired camera Z.
  • radius number — Sphere radius used for the cast; 0 disables the pull-in.
  • excludeId string (optional) — Entity whose subtree never blocks the camera (the subject).

Returns (number, number, number) — The corrected camera position.

local x, y, z = CameraRig.pullIn(px, py, pz, cx, cy, cz, 0.3, bodyId)

typed/builtin//controller/camera_rig/R/resolveFollow

R.resolveFollow(selfId: string, followField: any?) -> string?

Resolve the entity this rig follows: the explicit field, else the standard Camera.follow slot.

Parameters

  • selfId string — This rig's entity id.
  • followField any (optional) — The rig's own follow field value.

Returns string? — Entity id, or nil when nothing resolvable is set.

local target = CameraRig.resolveFollow(public.entity.id, public.follow)

typed/builtin//controller/camera_rig/R/ringAt

R.ringAt(rings: Rings, t: number) -> (number, number)

Height and radius of the orbit at a point along the three-ring spline.

Parameters

  • rings Rings — The bottom / center / top rings.
  • t number — Position along the spline, 0 at the bottom ring and 1 at the top.

Returns (number, number) — Height and radius at t.

local h, rad = CameraRig.ringAt(rings, 0.5)

typed/builtin//controller/camera_rig/R/visibleForward

R.visibleForward(targetId: string) -> (number, number, number)

The direction a subject visibly faces, in world space. A body declares its own visible forward (Locomotion.forward), because a mesh faces whichever way its source authored it: a Synty/FBX body carries {0,0,1}, a glTF body {0,0,-1}. Reading that declaration is what lets a rig sit behind ANY subject rather than behind one particular art pipeline. Falls back to the entity's own transform forward when nothing declares one.

Parameters

  • targetId string — The subject to read.

Returns (number, number, number) — Unit world-space forward as three numbers.

local fx, fy, fz = CameraRig.visibleForward(bodyId)

typed/builtin//controller/orbital_follow/public/onFollowChanged

public.onFollowChanged(newFollow: any?, oldFollow: any?)

Re-pose after the Camera's follow slot changed. follow lives on the Camera, so this rig receives no property notification of its own for it and the Camera calls this instead. A target assigned after the rig attached is picked up here, in edit mode as well as play.

Parameters

  • newFollow any (optional) — The entity the camera now follows.
  • oldFollow any (optional) — The entity it followed before.
-- called by Camera.onPropertyChanged, not usually by hand

typed/builtin//editor/edui/editorPanel/behavior/M/onChange

M.onChange(self: any?, change: any?)

Parameters

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

typed/builtin//editor/edui/editorPanel/behavior/M/onRegister

M.onRegister(self: any?)

Parameters

  • self any (optional)

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

  • i number

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, and how many automatic relaunches it has spent.

Parameters

  • i number

Returns any{termId, running, label, relaunches}.

typed/builtin//modules/api/editor/agents/spec

spec(kickoffPrompt: string?) -> any

The process spec the launcher runs for the active agent — the command, its flags, its working directory, and the environment it inherits.

Parameters

  • kickoffPrompt string (optional) — Text submitted as the agent's first message.

Returns any — A terminal.spawn spec: {cmd, args, cwd, env, envRemove}.

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

  • entityId string | 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

  • entityId string | 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

  • entityId string | 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

  • entityId string — The body the animator drives.
  • facts { [string]: any }{ driver, bound, playing, outputKind, failure, clips }, where each clip is { name, nodeKind, time, duration, playing, finished, looping, weight, matched, total, unmatched }.
animation.declare(body.id, { driver = "MyAnimator", playing = true, clips = {} })

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

  • entityId string — 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, retargetMatchedNoRoles, stopped, finished, paused, poseNotApplied, poseUnchanged — the nearest cause, so the answer names the thing to change. A second return carries the animator's own words when it could not build a graph.

An entity the engine holds no animation state for is answered from the entity itself, in the same order the engine resolves a body it does hold: one carrying no rigged Skeleton is noRiggedSkeleton, and a rigged one nothing drives is noGraph. An id no entity carries is neither — the reason is nil and the detail says so.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.

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

  • ref RefArg — Any name the asset has.
  • tag string — 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

  • ref RefArg — The asset gaining the name.
  • alias string — The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).

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

  • ref RefArg — 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

  • typeName string — 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's ancestors and return an AssetRef handle for the OUTERMOST category-folder containing it (e.g. main.scene for "/source/scenes/main.scene/scene.json"). Returns nil for paths outside any registered asset type.

Parameters

  • path string — 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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

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

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/<name>.<type>/. This is the single generic asset-creation API. Refuses to clobber an existing edit-mode asset unless opts.overwrite = true, which re-authors it in place and keeps the existing guid (only the checksum changes). Pairs with asset.exists for content generators that re-run over the same names.

A create made from a script component's callback or a scene entrypoint is output the world reproduces on every load, so it is filed in the ephemeral /runtime/assets/ store instead, where the saved manifest never carries a second copy of it. name and folder spell the same IDENTITY in either store, so a reference written against that identity resolves the asset wherever the call filed it, and one generator run from an execute and from a component names one asset.

Parameters

  • typeName string — Registered asset type to instance (e.g. "material", "texture").
  • name string — Destination asset name (becomes /source/<name>.<typeName>). A bare identity — pass opts.folder to 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 its behavior.luau exports a namePattern, as guide does to take getting-started and 01-overview. This call names the category FIRST and the asset second. Every other asset.* call taking both names them the other way round — asset.exists(name, category), asset.tryResolve(ref, category) — so a create-then-check pair reads if 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's onCreate hook, minus four framework keys consumed here and never seen by the hook: folder (a relative subfolder under /source to author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix), into (author INSIDE a resolved container ref), dest (an absolute destination path), and overwrite (re-author in place, keeping the guid).

Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.

local m = asset.create("material", "brick", { shader = "pbr" })
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

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.

Parameters

  • call string — The callee as it is written at a call site.
  • position number — Which argument holds the name, counting from 1.
  • assetType string
asset.declareReferenceArg("spawnModel", 3, "mesh")

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.

Parameters

  • call string — The callee as it is written at a call site.
  • field string — The options-table field holding the name, read at the table's own level.
  • assetType string
asset.declareReferenceField("fx.beam", "material", "material")

typed/builtin//modules/api/engine/asset/asset/declareReferenceKey

asset.declareReferenceKey(assetType: string, key: string, refType: string)

Declare that, in a data file belonging to an assetType asset, the top-level key names an asset of 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.

Parameters

  • assetType string — The category owning the file, e.g. "material".
  • key string — The top-level key holding the name(s).
  • refType string
asset.declareReferenceKey("material", "shader", "shader")

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

  • ref RefArg — Any name the asset has.
  • type string (optional) — Category hint (optional).

Returns DepsResult{ deps = { { asset_guid, origin, literal, via, ... } } }.

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.

Parameters

  • typeName string — 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, so an asset that loaded a preview image instead of its payload shows the wrong filename rather than a successful load. The payload's bytes are read by the engine's own decoder wherever it has one for that container, so usable is the verdict a load would reach and the call costs that decode.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

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

  • name string
  • typeName string

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

  • ref RefArg — Any name the asset has.
  • key string — Field name.

Returns any — Field value or nil.

local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → 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

  • ref RefArg — 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

  • ref RefArg — Any name the asset has.
  • type string (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

  • ref RefArg — Any name the asset has.
  • key string — 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

  • ref RefArg — Any name the asset has.
  • tag string — 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

  • ref RefArg — Any name the asset has.
  • type string (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) -> 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. This is the deterministic, on-demand counterpart to the engine's automatic import-on-write: it runs in the calling task and returns only when the import is complete. Pair it with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the raw bytes without firing the automatic importer, then asset.import(path) imports them under your control, so you can act on the result instead of polling for the import to appear.

Parameters

  • path string — The raw source VFS path to import (e.g. a just-written .glb).

Returns string? — The produced asset path, or nil when nothing claimed it.

local bundle = asset.import("/zero/source/generated/chest.glb")

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

  • ref RefArg — An AssetRef, an identity string, or a path.
  • type string (optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.

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/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). 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. An unknown key raises, as does a table setting both type and its older spelling category. A static (literal) type or path makes the enumeration part of the calling file's content dependencies when it is saved — the set travels with published content, so consumers get at-least the authoring world's assets.

Parameters

  • type_or_opts (AssetCategory | ListOpts) (optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.
  • scope string (optional) — Scope filter (when first arg is a type).
  • opts ListOpts (optional) — The query table — see ListOpts.

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()

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

  • key string — 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

  • ref RefArg — Any name the asset has.
  • type string (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

  • ref RefArg — Any name the asset has.
  • type string (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), and totals — the aggregates those rows sum to, so the listing reconciles against renderer.textureMemory() and renderer.gpuMemory().

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/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

  • ref RefArg — Any name the asset has.
  • opts { [string]: any } (optional) — Optional { size = { width, height }, angle = { yaw, pitch } }.
  • type string (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. resolved is false when no declaration matched and path is then absent; declared is the type's own primary list in match order.

Parameters

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.

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.

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string (optional) — Category to restrict the match to. Omit to search every category.

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

  • ref RefArg — Any name the asset has.
  • key string — 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

  • ref RefArg — Any name the asset has.
  • tag string — 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

  • ref RefArg — The asset to find — an identity, a guid, a VFS path, or a handle.
  • type (C & string) (optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.png after the texture importer turned it into wall.texture) resolves to the promoted asset, and says so in the log once per reference.

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/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, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace. Clear a whole field with asset.remove_field; replace the entire sidecar with asset.set_metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
  • value any (optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings 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

  • ref RefArg — Any name the asset has.
  • data AssetMeta — Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })

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

  • ref RefArg — Any name the asset has.
  • type string (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

  • ref RefArg — 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 still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.

It reaches the asset, so the name carries the same reference contract asset.resolve's does: a literal is recorded as this source's dependency, a computed name is a dynamic resolve — free in a tool, refused on the gameplay path. asset.exists(name, type) is the probe a computed name can ask.

Parameters

  • ref RefArg — The asset to look up — an identity, a guid, a VFS path, or a handle.
  • type string (optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same as asset.resolve.
  • base string (optional) — Referring VFS path a ~ / ~.tail ref expands against, the same as asset.resolve's — so the two answer the same question and differ only in what a miss is.

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.

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

  • ref RefArg — Any name the asset has.
  • type string (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

  • ref RefArg — Any name the root asset has — handle, identity, guid, or path.
  • opts WarmupOpts (optional) — Optional { vias, max } — restrict ref-edge kinds / cap closure size.

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

  • zaud buffer | 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

  • sourceBytes buffer | string — Encoded source audio bytes — a buffer or a binary string.
  • opts { [string]: any } (optional){ codec: "opus"|"pcm"?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loopStart: number?, loopEnd: number? }

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

  • pcm any (optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shape microphone.samples and audio.decode hand back, or a flat number array. A byte payload's samples are its 4-byte lanes, and a length that stops partway through one comes back as (nil, err) naming the whole samples it holds and the bytes past them.
  • sampleRate number — Source sample rate in Hz.
  • channels number — 1 or 2, and a divisor of the sample count.
  • opts { [string]: any } (optional) — Same shape as audio.encode.

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

  • zaud buffer | 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

  • zaud buffer | 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

  • window number — A windows count taken from audio.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/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

  • channel string — The channel name, matching Audio.channel.
  • volume number — Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end

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

  • volume number — 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

  • muted boolean — 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.

Parameters

  • entityId string — 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

  • entityId string — 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?

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. 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. Returns a stream URL, or nil when unsupported or a session is already active.

Parameters

  • opts LiveOpts (optional) — Encoder options.

Returns string? — Stream URL or nil.

local url = av.live({ width = 1280, height = 720, fps = 60 })

typed/builtin//modules/api/engine/av/av/record

av.record(path: string, opts: RecordOpts?) -> (string?, string?)

Start recording the engine output to a VFS path. Default dir is /zero/runtime/recordings/ when path is not absolute. 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. 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. 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. The engine opens the session, so the call waits for it: run it where it can yield, wrapping it in task.spawn from a callback that cannot. How a take finished reads back as av.status().recordingEnd; a refused request puts its reason there and on recordingError and leaves no take report behind, while a request refused because a take is already running leaves that take's report as it is.

Parameters

  • path string — VFS destination path.
  • opts RecordOpts (optional) — Encoder options (optional).

Returns (string?, string?) — Destination VFS path of the open recording, or nil. Why the recording was refused, when it was.

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

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 encode runs on the CPU — and hardware is true for the first two, 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. live is true while the av.live stream is running. 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.

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 on its next tick: wait for av.is_recording() to go false, then read what it produced off av.status().

Parameters

  • handle string (optional) — Promise handle of a specific recording (optional).

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

  • text string — Base64 text to decode.

Returns (string?, string?) decoded bytes on success, or (nil, errmsg).

local bytes = base64.decode(text)

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

  • bytes buffer | 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

  • handle number — 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.
  • totalStride number (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

  • outBuffer Substrate.TypedBuffer — The buffer written into.
  • layout number — Layout handle.
  • aBuffer Substrate.TypedBuffer — The A side of the fade.
  • bBuffer Substrate.TypedBuffer — The B side of the fade.
  • t number — Crossfade weight on B (0..1).

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

  • outBuffer Substrate.TypedBuffer — The buffer written into.
  • layout number — 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.

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

Parameters

  • entityId string (optional) — Entity id of the camera to route the viewport to, or nil to clear.
camera.setEditorOverride(camera.editor())
camera.setEditorOverride(nil)

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/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

  • opts ChannelOpts{ 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

  • handle number — 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

  • ch number — Channel handle.
  • time number — Sample time in seconds.
  • buf Substrate.TypedBuffer — The buffer written into.
  • offset number — Starting f32 index in the buffer.

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

  • ch number — Channel handle.
  • time number — Sample time in seconds.
  • buf Substrate.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

  • ch number — Channel handle.
  • time number — 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

  • ch number — Channel handle.
  • time number — 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

  • value any (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

  • c Color — 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

  • c Color — Input color.
  • amount number — 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

  • c Color — Input color.
  • amount number — 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

  • hexString string — 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

  • h number — Hue (degrees, 0-360).
  • s number — Saturation (0-1).
  • l number — 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

  • h number — Hue (0-360).
  • s number — Saturation (0-1).
  • l number — Lightness (0-1).
  • a number — 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

  • h number — Hue (0-360).
  • s number — Saturation (0-1).
  • v number — 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

  • c Color — Input color.
  • amount number — 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

  • r number — Linear red (0-1).
  • g number — Linear green (0-1).
  • b number — Linear blue (0-1).
  • a number (optional) — Alpha (0-1, default 1).

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

  • c1 Color — First color.
  • c2 Color — Second color.
  • t number — 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

  • c1 Color — First color.
  • c2 Color — Second color.
  • t number — 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

  • l number — Lightness (0-1).
  • c number — Chroma / saturation (0-0.4).
  • h number — 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

  • r number — Red channel (0-255).
  • g number — Green channel (0-255).
  • b number — 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

  • r number — Red channel (0-255).
  • g number — Green channel (0-255).
  • b number — Blue channel (0-255).
  • a number — 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

  • c Color — Input color.
  • degrees number — 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

  • c Color — Input color.
  • amount number — 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

  • c Color — 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

  • c Color — 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

  • c Color — 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

  • c Color — 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

  • c Color — Input color.
  • a number — 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)
shader: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

  • id number — Build id from compute.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

Compile a compute shader from inline WGSL + a declarative binding schema — the same codegen a .computeShader asset uses. The engine generates the @group/@binding declarations from bindings/params, so the source writes only @compute fn main. Symmetric with registerShader, but with zero-scaffolding bindings (incl. textures, samplers, storage textures and a params uniform). For asset-backed shaders prefer authoring a .computeShader (compiled automatically); use this for dynamic/generated compute shaders.

Parameters

  • nameOrHandle string | { [string]: any } | AssetRef — Shader identity to register under (string or asset handle).
  • opts { [string]: any } (optional){ source, entryPoint?, bindings, params? }bindings is an ordered list of { name, kind, access?, element?, format?, array? }.

Returns boolean — True on success.

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

  • ref string | { [string]: any } | AssetRef — A .computeShader identity/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?) -> 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. Rows must be a multiple of 256 bytes (at rgba16f, any width from 32 up in powers of two).

Parameters

  • bufferName string — Source compute buffer.
  • textureKey string — Cache key to register the texture under.
  • width number — Texture width in texels.
  • height number — Texture height in texels.
  • format string (optional) — Texel format: "rgba16f" (default), "rgba32f", "rgba8".

Returns boolean — True when the copy was queued.

compute.copyBufferToTexture("gi_resolved", "lm_wall", 128, 128)

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

  • name string — The name a dispatch binds it by.
  • opts { [string]: any }{ size, readback? }size in 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

  • name string
  • opts { [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

  • name string
  • opts { [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

  • name string — 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

  • name string
  • opts { [string]: any }

Returns boolean

typed/builtin//modules/api/engine/compute/compute/destroyBuffer

compute.destroyBuffer(name: string) -> boolean

Release the buffer allocated under name.

Parameters

  • name string — 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

  • name string — 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

  • name string — 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

  • name string

Returns boolean

typed/builtin//modules/api/engine/compute/compute/destroyStorageTexture2D

compute.destroyStorageTexture2D(name: string) -> boolean

Destroy a named 2D storage texture.

Parameters

  • name string

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

  • textureKey string — 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

  • name string

Returns boolean

typed/builtin//modules/api/engine/compute/compute/destroyTextureHistory

compute.destroyTextureHistory(name: string) -> boolean

Destroy a named texture-history buffer.

Parameters

  • name string

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

  • key string — 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

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts DispatchOpts{ buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

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

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts { [string]: any }{ resources, workgroups } — each resource is { kind, name }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

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

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts DispatchOnVerticesOpts{ model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

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

  • id number — Build id from compute.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

  • resultKey string — Key returned by readBuffer().

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

  • resultKey string — Key returned by readBuffer().

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

  • resultKey string — Key returned by readBuffer().

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

  • resultKey string — Key returned by readBuffer().

Returns boolean — True if the result is ready.

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

Register a compiled program under key from WGSL plus a declared binding schema. The engine generates the @group/@binding declarations from the schema, expands #includes, naga-validates, and registers the result.

Parameters

  • key string — The key to register under.
  • spec { [string]: any }{ source, entryPoint?, bindings, params } — the parsed schema.

Returns boolean — True on success.

typed/builtin//modules/api/engine/compute/compute/program/destroy

compute.program.destroy(key: string) -> boolean

Release the program registered under key.

Parameters

  • key string — 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

  • key string — The program's key.
  • opts { [string]: any }{ buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

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

  • key string — The program's key.
  • opts { [string]: any }{ resources, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

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

  • key string — The program's key.
  • opts { [string]: any }{ model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).

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/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

  • key string — The program's key.
  • prop string — Parameter name as declared.
  • value number — 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

  • key string — 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. Wait for "ready" before a dispatch whose result is read back, rather than for a count of frames.

Parameters

  • ref string | { [string]: any } | AssetRef — A .computeShader identity/guid string, a resolved asset handle, or the name a raw registration chose.

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

  • name string — 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

  • name string

Returns string

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), or "unknown" (never issued by readBuffer(), or already drained — a result is delivered once).

Parameters

  • resultKey string — Key returned by readBuffer().

Returns string"pending", "ready", 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

  • nameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts ShaderOpts (optional) — Shader options. bindings comes from the source's own @group(0) @binding(n) declarations when omitted; supplying a count that disagrees with them raises. Every readOnlyBindings entry names one of those declared bindings, as a whole number from 0 to bindings - 1; an entry outside that run raises.

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

  • nameOrHandle string | { [string]: any } | AssetRef
  • opts { [string]: any } (optional)

Returns boolean

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

  • owner any (optional) — A .computeShader ref, its guid, or its asset identity. Omit for every resource compute holds. A value carrying no shader raises, so a narrowing that cannot be done reads as an error rather than as the whole inventory. A guid stands for itself, so resources outlive the asset that made them and stay reachable by their owner.

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

  • nameOrHandle string | { [string]: any } | AssetRef — Shader identity (the .computeShader asset name), or the handle asset.load / asset.resolve returns — the same forms dispatch takes.
  • prop string — Parameter name as declared in bindings.yaml.
  • value number — New scalar value (numbers only).

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

  • id number — Build id from compute.beginBvh.
  • budgetMs number (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/textureFormatBytes

compute.textureFormatBytes(format: string) -> number

Bytes-per-voxel for a texture format string (rgba16f, r8, ...).

Parameters

  • format string

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

  • name string — The name it was created under.
  • values { number } | buffer | string — The floats to write, or a buffer / binary string already holding them.
  • offset number (optional) — 32-bit word offset to write at.

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

  • name string — The name it was created under.
  • bytes buffer | string — The payload.
  • offsetBytes number (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

  • name string — The name it was created under.
  • values { number } | buffer | string — The words to write.
  • offsetBytes number (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

  • name string
  • floats { 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

  • name string — Volume name.
  • data buffer | string | { number } — Voxel bytes as a buffer, 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

  • expr string — Luau expression.

Returns number — Watch id.

typed/builtin//modules/api/engine/debugger/debugger/continue_

debugger.continue_() -> boolean

Resume the paused thread.

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

  • handle number — 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

  • expr string — Luau expression.
  • frame number (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

  • frame number (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

  • frame number (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

  • id number — 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

  • id number — 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

  • id number — 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

  • path string — VFS path, require identity, or chunk name.
  • line number — 1-based source line.
  • opts BreakpointOpts (optional){ condition?, logMessage?, hitCount?, enabled? }.

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

  • enabled boolean — 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

  • path string — VFS path, require identity, or chunk name.
  • line number — 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

  • handle string — Handle returned by ecs.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.

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.

typed/builtin//modules/api/engine/effects/effects/backends

effects.backends() -> { string }

The backend kinds an effect can be built out of, in name order. The runtime ships emitter, geometry, material, decal and feature.

Returns { string } — Array of kind names.

print(table.concat(effects.backends(), ", "))

typed/builtin//modules/api/engine/effects/effects/describe

effects.describe(identity: string) -> { [string]: any }

What an effect declares about itself: its family, a one-line summary, every parameter with its type, default and documented range, and the cost one unpooled play of it was measured to draw. The one call to make against an unfamiliar effect before playing it.

Parameters

  • identity string — The effect's canonical identity, or a short name.

Returns { [string]: any }{ identity, family, summary, cost, params }.

local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)

typed/builtin//modules/api/engine/effects/effects/drain

effects.drain() -> { [string]: number }

Free every backend the pool is holding idle. The pool keeps what it has leased for as long as the engine runs — that is what makes repeated firing cost nothing after the first — and this is the one call that gives it back. A backend a live play still holds is left to that play's own end.

Returns { [string]: number }{ freed, kept }.

print(effects.drain().freed)

typed/builtin//modules/api/engine/effects/effects/families

effects.families() -> { string }

Every family the effects in this world declare, sorted — the values list { family = … } filters on. An effect declaring no family is not one of them.

Returns { string } — Array of family names.

for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end

typed/builtin//modules/api/engine/effects/effects/list

effects.list(opts: table?) -> { string }

The canonical identity of every effect this world can play, sorted. These are the exact strings play takes. Pass { family = "combat" } to get only the effects of one family — the catalogue filtered the way an effect declares itself.

Parameters

  • opts table (optional){ family? = string }. A family is matched without regard to case.

Returns { string } — Array of identities.

for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end

typed/builtin//modules/api/engine/effects/effects/observe

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

What the runtime is holding and driving right now — every live play with the reason it is silent when it is, plus what the pool has leased out and what it is keeping idle, in instances and in GPU bytes. This is how a caller and a test tell a working effect from a silent one, and how they tell a pool warming to a wider burst from something leaking: the pool is sized by the most effects it has had to cover at once, which peakLive and peakLeased report beside the current totals.

Returns { [string]: any } — The observation.

local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers

typed/builtin//modules/api/engine/effects/effects/play

effects.play(identity: string, opts: table?) -> any

Play an effect once at a world position. The effect allocates what it needs from the shared pool, draws itself, and gives everything back when it ends — with no update loop on the caller's side.

typed/builtin//modules/api/engine/effects/effects/playOn

effects.playOn(identity: string, target: any?, opts: table?) -> any

Play an effect on an entity: it starts where the entity stands and ends if the entity leaves the world. Move it with the entity by calling handle:retarget(theEntity) as it goes.

Parameters

  • identity string — The effect's canonical identity, or a short name.
  • target any (optional) — An entity proxy or entity id.
  • opts table (optional) — The same options play takes; position is read from the entity.

Returns any — The play handle.

local h = effects.playOn("explosion", drum, { params = { scale = 3 } })

typed/builtin//modules/api/engine/effects/effects/registerBackend

effects.registerBackend(kind: string, backend: table)

Register a new way of drawing under a kind name, so an effect family that needs one the runtime does not ship adds it rather than widening the runtime. Every effect reaches it through ctx.lease(kind, spec).

Parameters

  • kind string — The kind name a spec asks for.
  • backend table — The backend — key, acquire, seat, start, stop, quiet, place, bytes, active, silence and free.
effects.registerBackend("ribbonTrail", myBackend)

typed/builtin//modules/api/engine/effects/effects/silenceReasons

effects.silenceReasons() -> { { reason: string, means: string } }

The closed set of reasons a play can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every reason an observation reports is one of these.

Returns { { reason: string, means: string } } — Array of { reason, means }.

for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end

typed/builtin//modules/api/engine/egress/egress/credentialNames

egress.credentialNames() -> { string }

List the names of configured credentials. 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?) -> string?

Perform an HTTP request with a named credential injected server-side (in Rust). Returns a promise handle for task.await(), or nil when the credential is unknown or url is outside the credential's allowed base_url. The secret is never exposed to Luau. This is the seam that production points at the ZeroMind egress endpoint.

Parameters

  • name string — Credential name registered by the trusted VM.
  • method string — HTTP method, e.g. "GET" or "POST".
  • url string — Request URL (must start with the credential's base_url).
  • headers Headers (optional) — Extra header key-value pairs.
  • body JsonBody (optional) — JSON body (encoded automatically).
  • response EgressResponseType (optional)"json" (default) or "bytes".

Returns string? — Promise handle for task.await(), or nil if refused.

local h = egress.fetch("meshy", "POST", url, nil, { prompt = p })

typed/builtin//modules/api/engine/egress/egress/hasCredential

egress.hasCredential(name: string) -> boolean

Whether a named credential is configured. Returns only a boolean — never the value. Service handlers use this to fail with a clear "not configured" message.

Parameters

  • name string — 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/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, and make that the point engine.resetScriptingState() restores to. Replaces any previous mark. 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

  • id number — Watcher id returned by engine.onDeviceRebuilt.

Returns boolean

local id = engine.onDeviceRebuilt(function() end)
engine.offDeviceRebuilt(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

  • id number — Watcher id returned by engine.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

  • id number — Watcher id returned by engine.onPauseChange.

Returns boolean

typed/builtin//modules/api/engine/engine/M/offWorldLoaded

M.offWorldLoaded(id: number) -> boolean

Remove an onWorldLoaded subscriber by its watcher id.

Parameters

  • id number

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

  • id number — Watcher id returned by engine.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

  • id number — Watcher id returned by engine.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
regenerateNoiseTexture()
end)

typed/builtin//modules/api/engine/engine/M/onModeChange

M.onModeChange(callback: (string, string) -> ()) -> number

Register a callback that fires synchronously whenever engine.mode changes. Callback receives (newMode, oldMode) as strings. Returns a watcher id for future removal. Consumers (player_spawner, camera_spawner, editor-UI bootstrap, world entrypoint top-level onModeChange, etc.) all subscribe through this single API — there is no other fire path. Mode is engine state, so the watcher hangs off the engine module.

Parameters

  • callback (string, string) -> () — Function invoked as (newMode: string, oldMode: string).

Returns number

local id = engine.onModeChange(function(new, old)
print("flipped " .. old .. " -> " .. new)
end)

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/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/resetScriptingState

M.resetScriptingState() -> { [string]: number }

Drop every world-event subscription, lifecycle watcher and cached module registered since the last engine.markScriptingBaseline(), 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. Raises when no mark has been taken. Returns per-registry counts of what was removed: worldEvents, modeWatchers, worldReadyWatchers, worldUnloadingWatchers, pauseWatchers, modules, and total.

Returns { [string]: number }

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, 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? }?) -> { mode: string, bypassed: { any } }

Change the engine mode with per-call control over the play gate, and read back what the change went past. engine.mode = value is the same flip with the defaults.

options.strict = false lets THIS call enter play while your own content carries error-severity diagnostics. It settles with the call: the world's lsp.strict_mode is untouched, so no other session and no later session of the world sees a different gate. The returned bypassed array holds the diagnostics the call went past — each { path, line, col, code, message, severity } — and the engine log carries the same list. An error in content another session wrote never gates the flip, so it never appears here; a push still refuses to publish while any of them stands.

Parameters

  • mode string"edit" or "play".
  • options { strict: boolean? } (optional){ strict: boolean? }. strict = false waives the play gate for this call; true or omitted honours the world's lsp.strict_mode.

Returns { mode: string, bypassed: { any } }{ mode, bypassed } — the mode now in force and the diagnostics this call entered play past (empty when it went past none).

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/engine/discardPlayChanges

engine.discardPlayChanges() -> ()

Arm the leave-play safeguard's deliberate discard for the play session this is called from, so that session's play to edit flip proceeds and discards its unaccepted changes.

Returns ()

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/paused

engine.paused -> boolean

Whether gameplay is paused: update(dt) component callbacks are gated off while editorUpdate(dt) keeps firing in edit mode.

Returns 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 and to update(dt): 1.0 is real time, 0.0 frozen, 2.0 double speed.

Returns number

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/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 of entity.batchSpawn or entity.findAll).
  • type_name string — Component type to add to every entity.
  • data table (optional) — Init data table, applied identically to every entity — the same shape the second arg to entity(id).component.add(type, data) takes.

Returns number — How many entities had the component added.

local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 })

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 of entity.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: buffer?) -> { 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. target accepts an entity-id array or a ecs.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 from ecs.bindEntities(ids).
  • component string (optional) — Component type name (e.g. "Transform").
  • field string (optional) — Field name (e.g. "position").
  • sink buffer (optional) — Typed CPU buffer from substrate.createBuffer({...}) to memcpy field values into. Required when target is 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")

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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to read.
  • buffer number — 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?) -> { 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

  • count number — How many entities to spawn (capped at 1,000,000).
  • name_prefix string (optional) — Display-name prefix appended with the 1-based index. Defaults to "entity".

Returns { string } — Array of newly-spawned entity ids.

local ids = entity.batchSpawn(100, "grass_")

typed/builtin//modules/api/engine/entity/E/batchWrite

E.batchWrite(target: { string | entityRef } | binding, component: string, field: string, source: { any? } | buffer) -> 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; values is an array the same length as ids (nil slots are skipped). Use for one-shot writes.
  • entity.batchWrite(binding, comp, field, values) — binding handle from ecs.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 from ecs.bindEntities(ids).
  • component string — Component type name.
  • field string — Field name to write.
  • source { any? } | buffer — Per-entity values array (nil entries are skipped), or a typed CPU buffer from substrate.createBuffer({...}). A buffer source requires a binding target.

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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — 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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to write.
  • buffer number — Buffer id from substrate.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 })

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 }) — Entity ids minted while the builder ran, in creation order; the error it raised (or the refusals it hit), or nil; and the ids a component the builder attached minted in its own lifecycle.

local ids, err, reproduced = entity.capture(function() entity.spawn("chair") 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

  • target string | 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

  • sourceId string | entityRef — Entity id or entity proxy of the source entity to clone.
  • name string (optional) — Display name for the copy (defaults to source name + " (copy)").
  • opts table (optional){ includeTemporary?: boolean, name?: string }name is the same field the name argument 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 })

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

  • idOrProxy string | 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.

Parameters

  • nameOrGlob string — 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(...)).

Parameters

  • nameOrGlob string (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.

Parameters

  • id string | 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.

Parameters

  • id string | 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.

Parameters

  • id string | 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: number, 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

  • handle number — Template handle from entity.template.
  • count number — 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/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.
  • opts SpawnOpts (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, 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 } }

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.
  • opts SpawnOpts (optional) — Same options as entity.spawn (synced is 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) -> number

Construct a reusable spawn template. Captures a shared entity config ONCE and returns a stable handle for entity.instantiate(handle, count, fn?) — one call per batch instead of one per entity. 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

  • def EntityTemplateDef — Template definition: components / ecs / temporary / active / internal / attributes. Per-instance name / position / rotation / scale / parent and any override go through the instantiate callback.

Returns number — Stable template 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

  • entityId string
  • assetPath string
  • opts { [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.

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

  • key string

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.

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

  • type AssetRef | string
  • data table (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

  • type AssetRef | string
  • data table (optional)

Returns table?

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

  • type AssetRef | string
  • data table (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.

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

  • type AssetRef | 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

  • type AssetRef | 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

  • type AssetRef | string

Returns boolean

typed/builtin//modules/api/engine/entity/members/entityComponents/list

entityComponents.list() -> table

The component types this entity carries.

Returns table

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 } | string
  • flags { [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) -> boolean

Whether an add of this type is queued and has not landed yet.

Parameters

  • type AssetRef | string

Returns boolean

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

  • type AssetRef | string
  • instanceName string (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

  • type AssetRef | string
  • enabled boolean

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

  • self
  • lifetime number (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 }?

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

  • self
  • name string (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.

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

  • self
  • names { 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

  • self
  • target any
  • up any (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

  • self
  • newName string

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

  • self
  • name string

Returns any

typed/builtin//modules/api/engine/entity/members/entityRef/setActive

entityRef.setActive(self, active: boolean) -> ()

Set whether this entity is active.

Parameters

  • self
  • active boolean

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setInternal

entityRef.setInternal(self, internal: boolean) -> ()

Set whether default listings skip this entity.

Parameters

  • self
  • internal boolean

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setNetworkScope

entityRef.setNetworkScope(self, scope: string) -> ()

Set how far this entity replicates.

Parameters

  • self
  • scope string

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setParent

entityRef.setParent(self, parentId: string, opts: { [string]: any }?) -> ()

Reparent this entity.

Parameters

  • self
  • parentId string
  • opts { [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

  • self
  • mode string

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setRenderLayer

entityRef.setRenderLayer(self, names: any) -> ()

Set this entity's render layer.

Parameters

  • self
  • names any

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

  • self
  • names any

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setRenderLayers

entityRef.setRenderLayers(self, names: any) -> ()

Set this entity's render layers.

Parameters

  • self
  • names any

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setSessionScoped

entityRef.setSessionScoped(self, scoped: boolean) -> ()

Scope this entity to the session.

Parameters

  • self
  • scoped boolean

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setSynced

entityRef.setSynced(self, synced: boolean) -> ()

Set whether this entity replicates.

Parameters

  • self
  • synced boolean

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setSyncedTree

entityRef.setSyncedTree(self, synced: boolean) -> ()

Set replication for this entity and its descendants.

Parameters

  • self
  • synced boolean

Returns ()

typed/builtin//modules/api/engine/entity/members/entityRef/setTemporary

entityRef.setTemporary(self, temporary: boolean) -> ()

Set whether saves skip this entity.

Parameters

  • self
  • temporary boolean

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

  • self
  • opts { [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

  • id string — The entity id string.

Returns EntityRef — The (cached) entity proxy — an EntityRef.

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

  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — 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

  • x number (optional) — World X of the capture position. Defaults to 0.
  • y number (optional) — World Y of the capture position — the altitude a height-dependent atmosphere is sampled at. Defaults to 0.
  • z number (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

  • slot number — Reflection-probe slot (0-based).
  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — 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). NATIVE only — the wasm async-readback path is a tracked follow-up.

Parameters

  • name string — Destination asset identity (writes /source/<name>.texture/).
  • slot number — Reflection-probe slot (0-based).
  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — World Z of the capture position.
  • timeoutFrames number (optional) — Optional max frames to wait for the readback (default 180).

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

  • name string — Destination asset identity (writes /source/<name>.texture/).
  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — 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

  • name string — 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

  • name string — Source asset identity (reads /source/<name>.texture/).
  • slot number — 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. priority defaults 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

  • active boolean — Whether reflections fall back to the captured sky.

Returns boolean — True — the change was queued.

environment.setSkyFallback(false)

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

  • name string — Registered family name.
  • codepoint number — 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/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

  • bytes buffer | 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(vfs.read("/zero/source/Inter.ttf"))

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 face that answered, and reason why when it is not the one asked for. 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. Passing raw font bytes still works but logs a slow-path warning — bake with font.parse at import. Re-registering the same name replaces it. 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

  • name string — Family name to register under.
  • zfnt string — Baked ZFNT payload from font.parse (binary-safe string).
  • opts table (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", font.parse(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

  • name string — Registered family name.
  • text string — String to lay out.
  • opts table (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

  • texture string — Guid of the render target the image was drawn into (a Camera's textureHandle).
  • stream string — Stream handle from stream.open.
  • opts AttachOpts (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

  • handle string — 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. lastOutcome names what became of the most recent frame offered. nil when handle names no live session.

Parameters

  • handle string — 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/http/http/get_bytes

http.get_bytes(url: string, headers: Headers?) -> PromiseId

Async HTTP GET returning raw bytes (binary-safe string). Suitable for piping into vfs.write to download a file.

Parameters

  • url string — Request URL.
  • headers Headers (optional) — Header key-value pairs (optional).

Returns PromiseId — Promise handle for task.await().

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?) -> PromiseId

Async HTTP GET returning JSON. Returns a promise handle — wrap with task.await() to block until the response arrives.

Parameters

  • url string — Request URL.
  • headers Headers (optional) — Header key-value pairs (optional).

Returns PromiseId — Promise handle for task.await().

local data = task.await(http.get_json("https://api.example.com/info"))

typed/builtin//modules/api/engine/http/http/post_bytes

http.post_bytes(url: string, headers: Headers?, body: JsonBody?) -> PromiseId

Async HTTP POST returning raw bytes — use for APIs that accept JSON input but return binary output (audio, images).

Parameters

  • url string — Request URL.
  • headers Headers (optional) — Header key-value pairs (optional).
  • body JsonBody (optional) — JSON body (optional).

Returns PromiseId — Promise handle for task.await().

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?) -> PromiseId

Async HTTP POST returning JSON. Body is a Luau table; the FFI layer JSON-encodes it before the request goes out.

Parameters

  • url string — Request URL.
  • headers Headers (optional) — Header key-value pairs (optional).
  • body JsonBody (optional) — JSON body (optional).

Returns PromiseId — Promise handle for task.await().

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?) -> 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

  • method string — HTTP verb (case-insensitive).
  • url string — Request URL.
  • headers Headers (optional) — Header key-value pairs (optional).
  • body JsonBody (optional) — JSON body (optional).

Returns PromiseId — Promise handle for task.await().

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?) -> 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

  • method string — HTTP verb (case-insensitive).
  • url string — Request URL.
  • headers Headers (optional) — Header key-value pairs (optional).
  • body buffer | string | nil (optional) — Raw binary request body (optional).

Returns PromiseId — Promise handle for task.await().

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

  • path string — Path under the /app mount, 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

  • target string — 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. Raising inside it 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

  • method string — HTTP verb, e.g. "GET" or "POST".
  • path string — Path under the /app mount, e.g. "/status" or "/files/*".
  • handler HttpHandler — Called with the request table; returns a response table or a body string.
  • options HttpRouteOptions (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

  • handle number — The handle httpServer.route returned.

Returns boolean — True when a route with this handle was registered.

httpServer.unroute(h)

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

  • ref AssetRef<scene> | string — A scene AssetRef, or an identity string resolved through asset.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

  • proxy any (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

  • proxy any (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

  • proxy any (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

  • ref AssetRef<scene> | string — A scene AssetRef, 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.

typed/builtin//modules/api/engine/layers/M/load

M.load(ref: AssetRef<scene> | string, opts: LoadOpts?) -> any

Load a scene into the root non-additive slot ("main") OR as an additive overlay alongside it. Identity is ref-based: pass an AssetRef<scene> envelope (preferred — caught at the callsite by the LSP) or an identity string (resolved via asset.ref at entry, hard-error if no stable guid comes back). For non-additive, idempotency is by guid: re-loading the same scene logs and returns the existing proxy without tearing anything down. Different guid → unloads the current root + cascades every additive overlay it spawned + transitions the multiplayer room + loads the new scene. Logs every step at info level so a silent no-op is impossible.

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

  • h number — The handle layers.onBeforeLoad returned.

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

  • h number — The handle returned by layers.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

  • h number — The handle layers.onLoad returned.

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

  • h number — The handle layers.onUnload returned.

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 rebuild(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) spawnPlayerFor(scene) 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) releaseHandlesFor(scene) 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 scene AssetRef, 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, or an edit↔play flip's transition is materialising the layer set. 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, so 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, a validator judging the live tree — polls it down to false first.

Returns boolean — true while a load or a mode-flip transition is converging.

if not layers.rebuildInFlight() then judge(layers.active) 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.

Parameters

  • ref (AssetRef<scene> | string) (optional) — A scene AssetRef, or an identity string. Omit to reload the active root scene.

Returns any? — What the scene's build.luau did — { built = true, content, editorOnly } with the entity counts each half placed, 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.

Parameters

  • refOrProxy (AssetRef<scene> | string | any) (optional) — A scene AssetRef, 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 scene AssetRef, 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.

Parameters

  • path string — 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>. Once the engine has fetched the pinned commit, the imported tree answers to require("@<namespace>::path"), is listed by library.list(), and is readable under /zero/source/libs/@<namespace>/.

Parameters

  • namespace string — Library namespace, with or without the leading @ (e.g. "@mylib" or "mylib").
  • worldRef string — The upstream world's guid, or its name as it appears in world.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.

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

  • opts LogQueryOpts (optional) — Filter options, as logs.query takes.

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

  • limit number (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

  • text string — Substring to search for.
  • limit number (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

  • opts LogQueryOpts (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

  • limit number (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

  • message string — 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

  • limit number (optional) — Maximum entries to return.

Returns { LogEntry } — Array of WARN+ log-entry tables.

print(#logs.warnings(), "warnings")

typed/builtin//modules/api/engine/lsp/lsp/check

lsp.check(path: string, opts: CheckOpts?) -> 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, so errors == 0 means a code body was read and is clean.

Parameters

  • path string — VFS path.
  • opts CheckOpts (optional){ severity?, limit?, context? }.

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.

Parameters

  • opts CheckAllOpts (optional){ scope?, severity?, limit? }.

Returns CheckAllResult{ filesChecked, errors, warnings, info, hints, budgetExceeded, diagnostics }.

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.

Parameters

  • source string — Luau source.
  • opts CheckOpts (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/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

  • path string — Doc path (e.g. "asset/resolve", "renderer.texture.create").
  • opts DescribeOpts (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

  • path string — 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

  • path string — 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

  • kind string — Registration kind.

Returns { MethodSummary } — Array of doc summary tables.

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 — much smaller, and nothing to unwrap. 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

  • namespace string — Namespace name (e.g. "entity", "modules/Transform").
  • opts MethodsOpts (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.

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

  • opts NamespacesOpts (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

  • source string — Luau source text.

Returns DirectiveBlock{ mode, codes? }.

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

  • query string — Substring to search for.
  • opts SearchOpts (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

  • enabled boolean — 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

  • mode StrictMode"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/tools

lsp.tools() -> { ToolEntry }

List every code-mode tool registered in the VFS under /zero/docs/tools/<category>/<tool>.

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.

Parameters

  • expr_source string — Luau expression source (no surrounding chunk).
  • context_path string (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/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

  • name string — 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/dump

luau_profile.dump(path: string) -> DumpResult

Write the folded-stack dump to path on the HOST filesystem, one line per stack as <ticks> <stack_csv> — the format upstream Luau emits and tools/perfgraph.py consumes unchanged. A path naming the engine filesystem (/zero/..., or a bare root such as /source/...) is refused, and says so: luau_profile.folded() with vfs.write puts the dump there.

Parameters

  • path string — Absolute host filesystem path to write.

Returns DumpResult{ ok, path, samples, stacks, bytes }.

local r = luau_profile.dump("/tmp/profile.folded")

typed/builtin//modules/api/engine/luau_profile/luau_profile/dump_regions

luau_profile.dump_regions(path: string) -> DumpRegionsResult

Write per-region stats to path as JSON, on the HOST filesystem. A path naming the engine filesystem is refused, the same way dump refuses one.

Parameters

  • path string — Absolute host filesystem path to write.

Returns DumpRegionsResult{ ok, path, regions, bytes }.

luau_profile.dump_regions("/tmp/regions.json")

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

  • id number — Region id returned by begin().
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_n number (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

  • hz number (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

  • dstBuffer Substrate.TypedBuffer — The buffer written into.
  • srcBuffer Substrate.TypedBuffer — The buffer read from.
  • count number — Number of vec3 elements.
  • scale number — 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

  • buffer Substrate.TypedBuffer — The buffer to operate on.
  • offset number — Starting f32 index.
  • count number — Number of scalars.
  • target number — Target value all scalars approach.
  • smoothTime number — Time constant (≤ 0 snaps to target).
  • dt number — 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

  • buffer Substrate.TypedBuffer — The buffer to operate on.
  • offset number — Starting f32 index.
  • count number — Number of vec3 elements.
  • tx number — Target X.
  • ty number — Target Y.
  • tz number — Target Z.
  • t number — 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

  • buffer Substrate.TypedBuffer — The buffer to operate on.
  • offset number — Starting f32 index.
  • count number — 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

  • buffer Substrate.TypedBuffer — The buffer to operate on.
  • offset number — Starting f32 index.
  • count number — Number of quaternions.
  • tx number — Target quat X.
  • ty number — Target quat Y.
  • tz number — Target quat Z.
  • tw number — Target quat W.
  • t number — 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

  • buffer Substrate.TypedBuffer — The buffer to operate on.
  • offset number — Starting f32 index.
  • count number — 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

  • limit number (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

  • timeout number (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

  • max number (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().

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

  • opts MicOpts (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/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 booleantrue 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 booleantrue 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/setInFlight

M.setInFlight(v: boolean) -> nil

Set whether a mode-flip transition is materialising the scene.

Parameters

  • v booleantrue for the span of the transition, false once 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

  • v booleantrue to claim ownership, false to release.

Returns nil

typed/builtin//modules/api/engine/modelImport/modelImport/decompose

modelImport.decompose(bytes: buffer | string, format: string) -> string

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

  • bytes buffer | string — Raw model file bytes (from vfs.readAsync).
  • format string — The source file extension (lowercase, no dot).

Returns string — 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) -> string

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.
  • mainName string — Basename of the model file to import (one of files' names).

Returns string — 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) -> string

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

  • sourcePath string — VFS path to the source model file.
  • clipName string — Clip name as returned by result(handle).animations[i].name.

Returns string — Promise handle for task.await.

typed/builtin//modules/api/engine/modelImport/modelImport/extractAnimationResult

modelImport.extractAnimationResult(handle: 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

  • handle string — Promise handle from extractAnimation.

Returns string? — The clip's .zanim payload bytes, or nil.

typed/builtin//modules/api/engine/modelImport/modelImport/result

modelImport.result(handle: 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

  • handle string — Promise handle from decompose.

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: (() -> ())?) -> string?

Call makeHandle — which returns a promise-handle string, or a falsy value on a transient failure (e.g. a source read that raced a pending write during a parallel import) — up to retries + 1 times, yielding via yield between attempts so a pending write can land before the next try. Returns the handle string once one is produced, or nil when every attempt failed. Callers task.await the result only when it is non-nil, so a transient miss never reaches task.await as a non-string.

Parameters

  • makeHandle () -> any — Returns a promise-handle string, or a falsy value on failure.
  • retries number (optional) — Extra attempts after the first (default 3).
  • yield (() -> ()) (optional) — Called between attempts (default task.wait).

Returns string? — The handle string, or nil when every attempt failed.

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 auto-derived humanoid profile, and the humanoid classification. The source rig a skinned mesh's clips retarget through. Returns nil when the bytes are not a mesh or carry no skin.

Parameters

  • meshBytes buffer | 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 humanoid profile + classification are derived as for rigFromMeshSkin. The source rig a standalone clip retargets through. Returns nil on malformed input.

Parameters

  • skeleton AnimationSkeleton{ names, parents, locals } (a decompose result's skeleton).

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

  • description string — 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. Returns true if the claim was tentatively granted (relay confirmation pending).

Parameters

  • entityId (string | entityRef) (optional) — Entity id or proxy to claim.

Returns boolean — True if the claim was tentatively accepted.

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

  • relayUrl string — 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.
  • componentType string — Component type name, e.g. "Health".
  • property string — Property name as written in the component's sync {} block.

Returns DeliveryVerdictarriving 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 — 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. 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 warnThePlayer(d.rttMs) end
print(d.messagesAwaitingEntity .. " message(s) waiting for their entity")

typed/builtin//modules/api/engine/multiplayer/multiplayer/getPeerId

multiplayer.getPeerId() -> number?

Get this client's peer ID in the current session.

Returns number? — Local peer ID, or nil if not connected.

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

  • roomKey string — 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.

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 active and connected to a relay.

Returns boolean — True if connected.

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/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 to self.entityId in 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

  • roomKey string — 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. No-op when not connected or already joined.

Parameters

  • roomKey string — 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

  • roomKey string — 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.

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.
  • componentType string — 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.

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

  • entityId string — 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.

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.

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. Arguments may be any synced value (nil, boolean, number, string, Vec3, entity/component proxy, or table) and are delivered to listeners in order. No-op when not connected.

Parameters

  • channel string — Channel name listeners subscribe to via multiplayer.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. opts.severity defaults to "info"; opts.includeLocation attaches the emitting call site.

Parameters

  • template string — Fixed sentence identifying the notice.
  • params { [string]: any } (optional) — Optional table of named values rendered alongside the template.
  • opts NoticeOpts (optional) — Optional table: severity ("info" | "warn" | "error"), includeLocation (boolean).
notices.post("wave complete", { wave = 3 })
notices.post("save slot corrupted, using defaults", { slot = id }, { severity = "warn" })

typed/builtin//modules/api/engine/packages/packages/list

packages.list() -> { PackageEntry }

List every registered package across scopes.

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_scope string — Package name, or scope if a second arg is given.
  • name string (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/particles/particles/create

particles.create(spec: table?) -> any

Create a GPU particle system from a spec, allocating its buffers and registering it with the auto-update driver. The handle it returns carries :emit, :update, the setters, :observe, and :getCreator.

Parameters

  • spec table (optional){ maxCount, rate, lifetime, speed, shape, ... } — every field optional, each falling back to the emitter's default. owner and name say whose the emitter is: owner is the key list(owner) matches, so a creator reaches exactly its own emitters after it has lost their handles, and name says which of them this one is. An emitter that states neither is still attributed to the module and line it was created from. optional, each falling back to the emitter's default. A field that names one of a closed set takes a name from it and raises with the whole set otherwise; a key the spec does not define is reported on its own, naming the key that writes what it was written for. man particles.create lists every key the spec defines.

Returns any — The particle system handle.

local fire = particles.create({ maxCount = 2000, rate = 100 })
local star = particles.create({ owner = "starfield", name = "shell" })

typed/builtin//modules/api/engine/particles/particles/list

particles.list(filter: (string | ParticleCreatorFilter)?) -> { any }

Every particle system this VM has created and not destroyed, in creation order — or, given a filter, the ones whose creator matches it. Answered from the emitter registry, so finding an emitter costs nothing per entity in the scene.

Called with nothing it answers with every emitter in the VM, which is what makes it the way to reach one whose creator has lost its handle, and :getCreator() on an entry says whose that one is. A filter narrows it to one creator's own, so a module clears what a previous load of it left behind and leaves every other emitter in the world standing.

typed/builtin//modules/api/engine/particles/particles/observe

particles.observe(system: any?) -> { [string]: any }

What the engine is simulating and drawing for particles right now. With no argument, every live emitter plus the totals they sum to; with an emitter, that one's reading. An engine holding no emitters answers count = 0 with an empty list, which reads differently from an engine whose emitters are all silent (count > 0, silent = count).

Parameters

  • system any (optional) — Optional particle system handle to read on its own.

Returns { [string]: any } — table The observation.

local o = particles.observe(); print(o.count, o.alive, o.silent)
local r = particles.observe(fire); print(r.alive, r.bytes.total)

typed/builtin//modules/api/engine/particles/particles/silenceReasons

particles.silenceReasons() -> { { reason: string, means: string } }

The closed set of reasons an emitter can be producing nothing, in the order a reading resolves them — nearest cause first — each with what it means. Every observe().reason is one of these.

Returns { { reason: string, means: string } } — table Array of { reason, means }.

for _, r in ipairs(particles.silenceReasons()) do print(r.reason, r.means) end

typed/builtin//modules/api/engine/particles/particles/whySilent

particles.whySilent(system: any?) -> (string?, string?)

Why one emitter is producing nothing, from the closed set silenceReasons() enumerates — or nil when it is producing. The second return is the detail line naming what the reason is about.

Parameters

  • system any (optional) — The particle system handle to ask about.

Returns (string?, string?) — string? The reason, or nil. string? The detail line for that reason.

local why, detail = particles.whySilent(fire)

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

  • entityId string | entityRef — Target entity id.
  • component string — One of Physics.COLLIDER_COMPONENTS.
  • config table (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

  • entityId string | entityRef — Target entity id.
  • opts table (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

  • entityIdA string | entityRef — Entity that hosts the Joint component.
  • entityIdB string | entityRef — Connected entity.
  • opts table (optional) — Optional joint description (kind, anchors, axis, stiffness, damping, restLength, maxDistance, breakForce, breakTorque).
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 })

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

  • a string | 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.
  • c number (optional) — dz or dy depending on call form.
  • d number (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.

Parameters

  • entityId string | entityRef — Target entity id.
  • config table (optional) — Optional wheel configuration (radius?, suspensionDistance?, springRate?, damperRate?, motorTorque?, brakeTorque?, steerAngle?, forwardFriction?, sidewaysFriction?, is2D?).
Physics.addWheelCollider(id, { radius = 0.35, motorTorque = 500 })

typed/builtin//modules/api/engine/physics/P/applyForce

P.applyForce(entityIdOrForce: string | entityRef | vec3, force: vec3?)

Apply a force to an entity's rigid 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.

Parameters

  • entityIdOrForce string | entityRef | vec3 — Entity id (when paired with force) OR a force vector for the script-context entity.
  • force vec3 (optional) — Optional force vector when targeting an explicit entity.
Physics.applyForce({x=0, y=10, z=0})
Physics.applyForce(entityId, {x=0, y=10, z=0})

typed/builtin//modules/api/engine/physics/P/applyForceAtPoint

P.applyForceAtPoint(entityId: string | entityRef, force: vec3, point: vec3)

Apply a force at a specific world-space point — generates the matching torque from the lever arm.

Parameters

  • entityId string | entityRef — Target entity id.
  • force vec3 — Force vector.
  • point vec3 — World-space application point.
Physics.applyForceAtPoint(id, {x=0,y=10,z=0}, {x=1,y=0,z=0})

typed/builtin//modules/api/engine/physics/P/applyImpulse

P.applyImpulse(entityIdOrImpulse: string | entityRef | vec3, impulse: vec3?)

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.

Parameters

  • entityIdOrImpulse string | entityRef | vec3 — Entity id (with impulse) OR an impulse vector for the script-context entity.
  • impulse vec3 (optional) — Optional impulse vector when targeting an explicit entity.
Physics.applyImpulse({x=0, y=5, z=0})
Physics.applyImpulse(entityId, {x=0, y=5, z=0})

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

  • entityIdOrTorque string | entityRef | vec3 — Entity id (with torque) OR a torque vector for the script-context entity.
  • torque vec3 (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/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

  • entityId string | entityRef — Entity id or proxy.

Returns PhysicsBodyState? — A PhysicsBodyState — with exists = false for an entity that carries no rigid body — or nil when nothing in the scene answers to that id.

local b = Physics.bodyState(id); print(b.bodyType, b.mass, b.stillness)
if not Physics.bodyState(id).exists then print("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.

Parameters

  • origin vec3 — Box center at the start of the cast.
  • halfExtents vec3 — Half the size of the box on each axis.
  • direction vec3 — Cast direction.
  • maxDistance number (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 }, 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.

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.

Parameters

  • origin vec3 — Capsule centre at the start of the cast.
  • radius number — Capsule radius.
  • halfHeight number — Distance from the centre to either cap centre. The capsule stands halfHeight + radius tall in each direction.
  • direction vec3 — Cast direction.
  • maxDistance number (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 }, 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.

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

  • options table (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

  • entityId string | 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

  • entityId string | 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) -> { 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.

Parameters

  • entityId string | entityRef — Entity id or proxy.

Returns { PhysicsContact } — An array of PhysicsContact — empty when the body touches nothing, or when the entity carries no rigid body.

for _, c in Physics.contacts(id) do print(c.other, c.deepestPenetration) end

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.

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

  • entityId string | entityRef — Target entity id (must carry a WheelCollider component).

Returns table?{ isGrounded, compression, angularVelocity }, or nil if the component is absent.

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

  • fromId string — Viewer entity id.
  • toId string — Target entity id.

Returns booleantrue 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?)

Toggle ignored-collision state between two specific entities.

Parameters

  • entityIdA string | entityRef — First entity id.
  • entityIdB string | entityRef — Second entity id.
  • ignore boolean (optional) — When true (default) collisions between the pair are skipped.
Physics.ignoreCollision(a, b, true)

typed/builtin//modules/api/engine/physics/P/isSleeping

P.isSleeping(entityId: (string | entityRef)?) -> boolean?

Whether an entity's rigid 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 no rigid 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.

Parameters

  • entityId string | 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.
  • opts table (optional){ bodies: boolean?, contactPoints: boolean? }bodies = false builds the world accounting alone, and contactPoints = false keeps 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/overlapSphere

P.overlapSphere(center: vec3, radius: number) -> table

Find every entity id whose colliders overlap a sphere.

Parameters

  • center vec3 — Sphere center in world space.
  • radius number — 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/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/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.

Parameters

  • origin vec3 — Ray origin in world space.
  • direction vec3 — Ray direction (does not need to be unit-length; the engine normalises).
  • maxDistance number (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 }, 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. 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 })
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.

Parameters

  • origin vec3 — Ray origin in world space.
  • direction vec3 — Ray direction.
  • maxDistance number (optional) — Optional distance limit along the ray.
  • maxHits number (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 } — 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.

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

  • fromId string — Origin entity id.
  • toId string — Target entity id.
  • maxDistance number (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

  • sx number — Screen X in viewport-local pixels (the space of input.mouse_position and screenToRay).
  • sy number — Screen Y in viewport-local pixels.
  • maxDistance number (optional) — Maximum distance along the ray (defaults to 1000, matching raycast).
  • 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 }, 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.

local m = input.mouse_position; local hit = Physics.raycastScreen(m[1], m[2])

typed/builtin//modules/api/engine/physics/P/removeCollider

P.removeCollider(entityId: string | entityRef) -> string?

Remove whichever collider component an entity carries.

Parameters

  • entityId string | 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

  • entityId string | entityRef — Target entity id.
  • index number (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

  • entityId string | 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

  • entityId string | entityRef — Target entity id.
Physics.removeWheelCollider(id)

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

  • entityIdOrDamping string | entityRef | number — Entity id (with damping) OR damping value (script-context entity).
  • damping number (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

  • a string | 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.
  • c number (optional) — z-component or y-component depending on call form.
  • d number (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

  • entityId string | entityRef — Target entity id.
  • bodyType string — 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

  • entityIdOrEnabled string | entityRef | boolean — Entity id (with enabled) OR boolean (script-context entity).
  • enabled boolean (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.

Parameters

  • entityId string | entityRef — Target entity id.
  • membership number — Bitmask: which groups this collider belongs to.
  • filter number — 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

  • gravity vec3 — 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

  • entityIdOrScale string | entityRef | number — Entity id (with scale) OR scale value (script-context entity).
  • scale number (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

  • entityId string | entityRef — Target entity id (must carry a Joint component).
  • targetVelocity number — Desired joint velocity.
  • maxForce number — 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

  • entityIdOrDamping string | entityRef | number — Entity id (with damping) OR damping value (script-context entity).
  • damping number (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

  • entityIdOrMass string | entityRef | number — Entity id (with mass) OR mass value (script-context entity).
  • mass number (optional) — Optional explicit mass when targeting another entity.
Physics.setMass(10)
Physics.setMass(entityId, 10)

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

  • entityId string | entityRef — Target entity id.
  • x boolean — Lock rotation about the world X axis.
  • y boolean — Lock rotation about the world Y axis.
  • z boolean — 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

  • entityId string | entityRef — Target entity id.
  • x boolean — Lock translation along the world X axis.
  • y boolean — Lock translation along the world Y axis.
  • z boolean — 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

  • a string | 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.
  • c number (optional) — z-component or y-component depending on call form.
  • d number (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/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.

Parameters

  • origin vec3 — Sphere center at the start of the cast.
  • radius number — Sphere radius.
  • direction vec3 — Cast direction.
  • maxDistance number (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 }, 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.

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.

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

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/touching

P.touching(entityId: string | entityRef, otherId: string | entityRef) -> (boolean, number, { PhysicsContactPoint })

Whether two entities are touching, and how deeply.

Parameters

  • entityId string | entityRef — Entity id or proxy.
  • otherId string | 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/wakeUp

P.wakeUp(entityId: (string | entityRef)?)

Wake an entity's sleeping rigid 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

  • entityId string | 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/playerSetupValidation/M/checkActiveScene

M.checkActiveScene() -> { { entity: string, message: string } }

Gather every player-setup validation message for the live active-layer scene: the per-entity rules across all PlayerSpawn / PlayerPrototype entities, the competing-camera rule, and the scene-intent rule. Returns a flat list an agent can read to see what to fix. The verdict belongs to the settled scene, so the call holds while a scene load or a mode-flip transition is rebuilding the live tree, and judges what the rebuild lands on.

Returns { { entity: string, message: string } } — An array of { entity = name/id, message = string }.

typed/builtin//modules/api/engine/playerSetupValidation/M/checkEntity

M.checkEntity(entityId: string) -> { string }

Validate a single PlayerSpawn or PlayerPrototype entity, returning agent-facing messages naming what is wrong and what to do. An entity carrying neither component (or one that does not exist) yields no messages.

Parameters

  • entityId string — The entity to inspect.

Returns { string } — An array of message strings; empty when the entity is well-formed.

typed/builtin//modules/api/engine/playerSetupValidation/M/checkScene

M.checkScene(opts: { playerIntent: string, spawnCount: number, cameraCount: number }) -> { string }

Validate a scene's player intent against its PlayerSpawn / Camera counts, returning agent-facing messages. A "spawns" scene with no PlayerSpawn, or a "none" scene with no Camera, yields a message; every other combination is clean.

Parameters

  • opts { playerIntent: string, spawnCount: number, cameraCount: number }{ playerIntent: string, spawnCount: number, cameraCount: number }.

Returns { string } — An array of message strings; empty when the scene is well-formed.

typed/builtin//modules/api/engine/playerSetupValidation/M/checkSceneJson

M.checkSceneJson(sceneJson: { [string]: any }) -> { { code: string, severity: string, message: string } }

Validate a decoded scene.json document statically: the full player-setup rule set (per-entity, competing-camera, scene-intent) run over the scene's authored entity tree without loading it. This is what the scene assetType's validate hook calls, so asset.validate / worldValidation / the world.push gate all report a broken player setup at authoring time.

Parameters

  • sceneJson { [string]: any } — The decoded scene.json table ({ player, version, entities }).

Returns { { code: string, severity: string, message: string } } — An array of { code, severity, message } problem records.

typed/builtin//modules/api/engine/playerSetupValidation/M/playReadinessProblems

M.playReadinessProblems() -> { { entity: string, message: string } }

The player-setup problems that must block a flip into play: the per-entity spawn/prototype rules (body + camera refs set, resolving to descendants, a single referenced camera) and the scene-intent rule, over the LIVE active scene. A "spawns" scene with none of these problems is ready to play. The competing-camera rule is deliberately excluded — the editor's own free-fly camera is a live viewport camera outside every prototype, so running it here would false-positive on every edit session; that rule stays a static / publish-time concern. Empty for a non-"spawns" scene (no player requirement), and empty while the active scene is still being materialised — the verdict belongs to the settled scene, so it waits for the layer to finish loading and for any mode-flip transition to converge.

Returns { { entity: string, message: string } } — An array of { entity = name/id, message = string }; empty = ready.

typed/builtin//modules/api/engine/player_prototype_spawn/M/applyNetworkScope

M.applyNetworkScope(cloneRootId: string, isOwner: boolean, isAuthority: boolean)

Prune a clone subtree by each node's networkScope against the caller's role. Walks the subtree from cloneRootId; a node scoped OwnerOnly is despawned when the caller is not the owner, AuthorityOnly when the caller is not the authority, and Replicated (or any other value) is kept.

Parameters

  • cloneRootId string — The clone's root entity id.
  • isOwner boolean — Whether the caller owns this clone.
  • isAuthority boolean — Whether the caller is the simulation authority for this clone.

typed/builtin//modules/api/engine/player_prototype_spawn/M/authorDefault

M.authorDefault() -> { [string]: string }

Author the canonical default player setup into the active scene — the same shape the default world and the static_player canonical scene ship: a PrototypeOnly prototype whose body adopts the humanoid avatar and whose OwnerOnly camera rig runs the orbital follow behavior, plus a spawn at the origin. Returns the authored entity ids. This is the single builder scene.player("spawns") and the "player" scene template both resolve to, so a joining user's avatar always replaces the same authored body.

Returns { [string]: string }{ setups, prototype, body, camera, spawns, spawn } — the authored ids.

typed/builtin//modules/api/engine/player_prototype_spawn/M/captureTemplatesFromEntities

M.captureTemplatesFromEntities(entities: { any })

Build the prototype-template registry from a scene's authored entity records (the parsed scene data, not live entities). This is the primary capture path: it is independent of scene-load order and runtime composition, so it captures the clean authored subtree (no composed avatar) and works in the runtime profile, which boots straight to play. Called by the scene loader for v7 scenes.

Parameters

  • entities { any } — The scene's authored entity records (each { id, name, parent, networkScope, renderLayer, transform, components }).

typed/builtin//modules/api/engine/player_prototype_spawn/M/capturedTemplate

M.capturedTemplate(prototypeId: string) -> any

The captured authored subtree for a PlayerPrototype — the clone source spawnFor instantiates for each joining player, keyed by the prototype's authored entity id (the id a PlayerSpawn's prototype field carries). Each node is { id, name, participation, networkScope, renderLayer, position, rotation, scale, components = { [type] = data }, children }. Where the materialisation keeps authored prototype subtrees out of the live scene — play — this template is the authored prototype, and it is the subtree spawnFor clones for each joining player.

Parameters

  • prototypeId string — The PlayerPrototype root's authored entity id.

Returns any — The template node, or nil when no template is captured for that id.

local proto = player_prototype_spawn.capturedTemplate(spawn.prototype.id)

typed/builtin//modules/api/engine/player_prototype_spawn/M/chooseSpawn

M.chooseSpawn(ctx: any?) -> (string?, { [string]: any }?, string?)

Pick the PlayerSpawn-carrying entity to spawn from. Enumerates entities carrying the PlayerSpawn component in the joining user's ROOT scene, skipping any that live in an additive overlay layer (editor UI, HUD scenes). When the root scene resolves (ctx.rootSceneGuid, else layers.active.guid), only spawns in that scene's layer are considered; otherwise every non-overlay spawn is eligible. Spawns with no layer attribution yet belong to the world root and stay eligible either way. Honors an optional ctx.spawnId override (used for deterministic selection), otherwise returns the first matching spawn.

Parameters

  • ctx any (optional) — A table; ctx.spawnId optionally names the spawn entity to select, ctx.rootSceneGuid optionally names the scene layer to scope the search to.

Returns (string?, { [string]: any }?, string?)(spawnEntityId, playerSpawnComponentProxy, spawnSceneLayer), or (nil, nil, nil) when none match. spawnSceneLayer is the chosen spawn's own scene-layer guid — the scene the clone must belong to.

typed/builtin//modules/api/engine/player_prototype_spawn/M/clearJoinHook

M.clearJoinHook(guid: string)

Clear a scene's join-hook flag. Called when a "spawns" scene unloads so the once-registered connect / play-entry handlers stand down (they no-op while no wired scene remains). Idempotent for an unknown guid.

Parameters

  • guid string — The scene guid passed to installJoinHook.

typed/builtin//modules/api/engine/player_prototype_spawn/M/installJoinHook

M.installJoinHook(sceneProxy: any?) -> boolean

Wire spawnFor to the world's connected-user join event. When a user connects, the hook picks a PlayerSpawn and instantiates that user's prototype instance (internal identity + avatar + camera-follow) via spawnFor. The trigger is world.connectedUsers.onConnect — the WORLD-level "a user joined the session" event — not the room players registry, so the internal identity the clone becomes (which folds into that registry) does not re-trigger a spawn. Entering play spawns every already-connected user (their onConnect fired in edit, ignored then). A scene wired while ALREADY in play — the runtime profile boots straight into play, or a scene swapped in mid-play — gets that same sweep immediately, since no play flip follows to trigger it. Every spawn path is per-user idempotent: a user who already owns a live clone is skipped, so overlapping paths and re-flips never produce a second player. Idempotent per scene proxy: a second call for the same scene installs nothing further.

Parameters

  • sceneProxy any (optional) — A non-additive Scene proxy.

Returns boolean — true when the hook was installed (or was already installed), false when the connected-users surface is unavailable.

typed/builtin//modules/api/engine/player_prototype_spawn/M/runtimeSpawnedInfo

M.runtimeSpawnedInfo(id: string) -> { [string]: any }?

Read back the runtime provenance stamped on a clone root by spawnFor.

Parameters

  • id string — The clone root entity id.

Returns { [string]: any }?{ sourcePrototype, ownerUserId, ownerPlayer }, or nil when the entity carries no provenance.

typed/builtin//modules/api/engine/player_prototype_spawn/M/spawnFor

M.spawnFor(ctx: any?) -> string?

Spawn a player instance for a joining user from the chosen PlayerSpawn's prototype. Chooses a spawn (honoring ctx.spawnId), resolves and validates its prototype, clones the prototype subtree, activates and reveals the clone, prunes it by networkScope against the caller's owner/authority role (both default true), marks the clone RuntimeOnly, stamps provenance attributes, places the clone at the spawn's world transform, and registers it with the prototype lifecycle so it is despawned on the return to edit.

Parameters

  • ctx any (optional){ userId, playerEntityId?, spawnId?, isOwner?, isAuthority? }.

Returns string? — The clone root entity id, or nil when no eligible spawn / prototype exists.

typed/builtin//modules/api/engine/player_prototype_spawn/M/storePrototypeTemplate

M.storePrototypeTemplate(prototypeId: string)

Capture a PlayerPrototype's authored subtree into the template registry. Called by PlayerPrototype.awake (before its Asset composes and before it deactivates) so spawnFor can instantiate the authored structure per player.

Parameters

  • prototypeId string — The PlayerPrototype root entity id.

typed/builtin//modules/api/engine/player_prototype_spawn/M/userHasSpawnedPlayer

M.userHasSpawnedPlayer(userId: string?) -> boolean

Whether a live clone spawned by spawnFor already carries this user's owner provenance. Scans the live entities for a root whose ownerUserId attribute matches. The idempotency guard the auto-spawn paths use so a user who already has a spawned player never gets a second one.

Parameters

  • userId string (optional) — The joining user's account id.

Returns boolean — true when a live clone owned by userId exists.

typed/builtin//modules/api/engine/players/playerHandle/avatar

playerHandle.avatar -> entityRef?

This player's body in the 3D world, or nil until a live one is bound. Assign a live entity ref to bind a body, or nil to clear it.

Returns entityRef?

typed/builtin//modules/api/engine/players/playerHandle/displayName

playerHandle.displayName -> string

The name this player shows as, empty until the owner stamps it or it replicates.

Returns string

typed/builtin//modules/api/engine/players/playerHandle/identity

playerHandle.identity -> string

The account id this player belongs to, under the second name it answers to.

Returns string

typed/builtin//modules/api/engine/players/playerHandle/isLocal

playerHandle.isLocal -> boolean

Whether this session owns this player.

Returns boolean

typed/builtin//modules/api/engine/players/playerHandle/ready

playerHandle.ready -> boolean

Whether this player has a live body bound.

Returns boolean

typed/builtin//modules/api/engine/players/playerHandle/userId

playerHandle.userId -> string

The account id this player belongs to, empty until the owner stamps it or it replicates.

Returns string

typed/builtin//modules/api/engine/players/players/count

players.count(self) -> number

How many players are connected to this room.

Parameters

  • self

Returns number

typed/builtin//modules/api/engine/players/players/exists

players.exists(self, id: string) -> boolean

Whether a player with this account id or identity entity id is connected.

Parameters

  • self
  • id string

Returns boolean

typed/builtin//modules/api/engine/players/players/get

players.get(self, key: string) -> playerHandle?

The connected player with this account id or identity entity id, or nil.

typed/builtin//modules/api/engine/players/players/list

players.list(self) -> { playerHandle }

Every player connected to this room, as a snapshot.

typed/builtin//modules/api/engine/players/players/localPlayer

players.localPlayer -> playerHandle?

The player this session owns, or nil before its identity has landed.

Returns playerHandle?

typed/builtin//modules/api/engine/players/players/localPlayerEntityId

players.localPlayerEntityId -> string?

The id of the local identity entity, answerable before its UserIdentity component has attached.

Returns string?

typed/builtin//modules/api/engine/players/players/localReady

players.localReady -> boolean

Whether the local player is loaded: its identity exists with a live avatar, or the scene opted the avatar out.

Returns boolean

typed/builtin//modules/api/engine/players/players/offJoin

players.offJoin(self, handle: number) -> boolean

Remove an onJoin subscription by its handle.

Parameters

  • self
  • handle number

Returns boolean

typed/builtin//modules/api/engine/players/players/offLeave

players.offLeave(self, handle: number) -> boolean

Remove an onLeave subscription by its handle.

Parameters

  • self
  • handle number

Returns boolean

typed/builtin//modules/api/engine/players/players/offLocalReady

players.offLocalReady(self, handle: number) -> boolean

Remove an onLocalReady subscription by its handle.

Parameters

  • self
  • handle number

Returns boolean

typed/builtin//modules/api/engine/players/players/offPlayerJoined

players.offPlayerJoined(self, handle: number) -> boolean

Remove an onPlayerJoined subscription by its handle.

Parameters

  • self
  • handle number

Returns boolean

typed/builtin//modules/api/engine/players/players/offPlayerLeft

players.offPlayerLeft(self, handle: number) -> boolean

Remove an onPlayerLeft subscription by its handle.

Parameters

  • self
  • handle number

Returns boolean

typed/builtin//modules/api/engine/players/players/onJoin

players.onJoin(self, cb: (playerHandle) -> ()) -> number

Subscribe to players joining the room, under the short name onPlayerJoined also answers to.

Parameters

  • self
  • cb (playerHandle) -> ()

Returns number

typed/builtin//modules/api/engine/players/players/onLeave

players.onLeave(self, cb: (playerHandle) -> ()) -> number

Subscribe to players leaving the room, under the short name onPlayerLeft also answers to.

Parameters

  • self
  • cb (playerHandle) -> ()

Returns number

typed/builtin//modules/api/engine/players/players/onLocalReady

players.onLocalReady(self, cb: (playerHandle) -> ()) -> number

Subscribe to the local player becoming ready, firing immediately for a subscriber that arrives after it already has. Answers a handle for offLocalReady.

Parameters

  • self
  • cb (playerHandle) -> ()

Returns number

typed/builtin//modules/api/engine/players/players/onPlayerJoined

players.onPlayerJoined(self, cb: (playerHandle) -> ()) -> number

Subscribe to players joining the room, firing once for each player already in it. Answers a handle for offPlayerJoined.

Parameters

  • self
  • cb (playerHandle) -> ()

Returns number

typed/builtin//modules/api/engine/players/players/onPlayerLeft

players.onPlayerLeft(self, cb: (playerHandle) -> ()) -> number

Subscribe to players leaving the room. Answers a handle for offPlayerLeft.

Parameters

  • self
  • cb (playerHandle) -> ()

Returns number

typed/builtin//modules/api/engine/players/players/ownerOf

players.ownerOf(self, avatar: any) -> playerHandle?

The connected player whose body is this avatar, taken as an entity ref or an entity id.

Parameters

  • self
  • avatar any

Returns playerHandle?

typed/builtin//modules/api/engine/postprocess/postprocess/add

postprocess.add(name: string, shader: string | AssetRef, opts: PostprocessOpts?) -> boolean

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 also accepted, and then opts.properties is the whole schema. 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

  • name string — Unique effect name.
  • shader string | AssetRef — A resolved shader asset reference, or author WGSL (fn fragment(in: PostInput) only).
  • opts PostprocessOpts (optional){ priority = 100, enabled = true, layer = "all"|"scene", properties = {{name, type, default?, min?, max?, textureDefault?}} } — with a shader asset, properties layers over the asset's own schema. layer picks which composited image the effect grades: "scene" runs it before the UI is drawn, so it grades the rendered picture and leaves every widget on screen as authored, and "all" (the default) runs it after the UI has landed, so the interface is graded along with the picture — an effect that belongs to the world's look wants "scene", since a screen another author drew is otherwise graded by it too. textureDefault is what a type = "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 until setTexture binds a texture that exists.

Returns boolean — True when the chain registered the effect; false 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 — the compiler's message is in the engine log. 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

  • name string — 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).

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

  • name string — Effect name to remove.

Returns boolean — True — the mutation was queued.

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

  • name string — Effect name.
  • enabled boolean — True to enable, false to disable.

Returns boolean — True — the mutation was queued.

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

  • name string — Effect name.
  • prop string — 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

  • name string — 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

  • name string — Effect name.
  • prop string — Declared texture-property name.
  • path string — 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

  • name string — Block name (e.g. "MyComponent.update").
profiler.begin("MyComponent.update"); ...; profiler.finish()

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

  • seconds number (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

  • name string (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

  • label string (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/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 retro tool renders the agent-facing report. Latency-immune: the data is historical.

Parameters

  • seconds number (optional) — How many seconds back to include (default: whole ring).
  • label string (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. 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.

Parameters

  • label string (optional) — Capture label (default "capture").

Returns boolean — True if capture started, false if a capture is already active.

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

  • pattern string (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. Compute duration as ended_at - started_at.

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

  • ceilingMs number — Effective frame-time ceiling in ms.
  • mode string (optional) — "record" (default) or "pause".
  • excludeAgent boolean (optional) — Subtract agent cost before comparing (default true).
  • maxHits number (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/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

  • id string — 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, unless opts.bake == false, bakes it). The probe is an editor gizmo — invisible in play mode. Returns the probe entity id.

Parameters

  • x number — World X.
  • y number — World Y.
  • z number — World Z.
  • opts { [string]: any } (optional) — Optional { radius = 12, probeId = "...", name = "..." }. probeId is the STABLE asset identity (so a re-created probe reloads the same baked cube); defaults to the entity id. The probe does NOT bake on add — call bakeAll() 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 become inert placeholders. Called automatically by add / bake / remove; call it directly after moving a probe entity.

Returns number — The number of active probes applied.

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

  • id string — 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/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/list

reflectionProbe.list() -> { any }

List every registered probe: { { id, slot, radius, priority, asset, position }, ... }.

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

  • id string — 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

  • id string — Probe entity id.
  • radius number — Influence radius (world units) — surfaces within blend it.
  • key string (optional) — Optional STABLE asset identity (the probe's probeId). Defaults to id. The baked cube persists at probe_<key>.texture so 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

  • id string — Probe entity id.
  • priority number — 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

  • id string — Probe entity id.
  • kind string — "box" (sized by all three half-extents), "sphere" (sized by x), or "none" to sample along the raw reflection vector.
  • x number — Half-extent along X, in world units — the sphere radius for "sphere".
  • y number — Half-extent along Y.
  • z number — 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

  • id string — Probe entity id.
  • radius number — 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

  • id string — 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?) -> () -> ()

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.

Parameters

  • share number (optional) — How much of the authored air reaches the image, in [0, 1]. Defaults to 0 — no air at all.

Returns () -> () — A function that releases this hold. Calling it twice releases once.

local release = renderer.atmospherics.hold() ; local png = tools.use("capture", "fromPosition", { position = { 0, 2, 8 } }) ; release()

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) pushParams() end)

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

  • id string — 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.

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

  • name string — The view name.

Returns number? — The channel number or nil.

ctx.enqueue { ..., debugChannel = renderer.captureView.channelId("lightmap") }

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.

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. False for an unregistered name.

Parameters

  • name string — 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

  • name string — The view name, selected via capture pass=<name>.
  • config any (optional){ description?, ensure?, warmup?, renderLayers? }. ensure is called before a capture of this view so the feature that draws it is live (e.g. create it on demand). warmup is how many present frames a capture lets the view accumulate before it reads — set it when the feature retains prior-frame state (a temporal diff) so the first capture reads a warm result. renderLayers is the layer spec a capture of this view uses when the caller named none — a view that draws its own geometry and wants the scene's kept out of the frame (and out of the depth buffer it tests against) names only its own layer.

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, description, warmup } record, or nil when no view is registered under name.

Parameters

  • name string — 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

  • name string — 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 the directional shadow is the cascades' alone again and the layer the hero view rendered into is given back.

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 views again. Called with no argument, drops every registration.

Parameters

  • mesh string (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/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.

Returns RuntimeCollection{ released = { texture, material, mesh, feature }, kept, entries } — the counts released per kind, how many stayed, and every resource's status with action = "released" | "kept".

local c = renderer.collect() print(c.released.texture, c.kept)

typed/builtin//modules/api/engine/renderer/renderer/compiledShaders

renderer.compiledShaders() -> { string }

Every name renderer.compiledSource answers for — one per name a shader compile has run under this session, whether it succeeded or failed. What makes the composed-source surface enumerable rather than something to guess a key for.

Returns { string } — An array of shader names, 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 under any name a compile ran under (identity, guid, alias, or a program from renderer.shaderVariants()), 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 under the same name.

Parameters

  • shader string — Any name a shader compiled under — identity, guid, alias, or a shaderVariants() program name.

Returns string? — The composed WGSL, or nil for a name no compile has run under.

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 pre-pass, 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 a shadow view's own volume did with the frame's casters is on that view's row in renderer.shadowViews().

Returns { total: number, culled: number, visible: number, occlusionTested: number, occlusionCulled: number }

local s = renderer.cullStats(); print(s.visible - s.occlusionCulled, "drawn")

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

  • name string — 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.

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

  • channel number — The channel number.

Returns string? — The pass name, or nil.

local name = renderer.debugPass.name(7)   -- "normal"

typed/builtin//modules/api/engine/renderer/renderer/depthPrepass

renderer.depthPrepass() -> boolean

Whether the opaque depth pre-pass is currently enabled.

Returns boolean

typed/builtin//modules/api/engine/renderer/renderer/depthPrepassOrder

renderer.depthPrepassOrder() -> { runs: number, reordered: number }

What the last frame's depth pre-passes planned, and how far their sequences were from near-to-far before they ordered. runs counts the instanced draws planned; reordered counts the adjacent pairs the sort moved past each other, taken before it ran. Both are summed over every pre-pass the frame ran — the window plus each render-target camera, each ordering against its own camera. Both read 0 while the pre-pass or the ordering is off, and reordered reads 0 for a frame that already stood in order. The ordering leaves no other trace — the draws, the depth and the image are the same either way.

Returns { runs: number, reordered: number }

local o = renderer.depthPrepassOrder()  -- o.reordered > 0 → it sorted

typed/builtin//modules/api/engine/renderer/renderer/depthPrepassOrdering

renderer.depthPrepassOrdering() -> boolean

Whether the depth pre-pass is submitted nearest-first.

Returns boolean

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

  • handleOrKind any (optional) — A MeshHandle, TextureHandle, MaterialHandle or feature handle; the id itself; or the kind ("texture", "material", "mesh", "feature") with the id as the second argument.
  • id string (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 it has stopped trying 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 the engine gave up — the adapter refused every attempt, so this session draws no more frames.

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's parameter group, and materialBindsElided how many times a pass reached that decision and found the group already bound. 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. The ratio inside the pair is what material binding costs the frame: the batched opaque geometry is gathered into runs sharing a material, so a frame of many such draws over few materials binds about once per material rather than once per unit. materialExtraBinds and materialExtraBindsElided are the same pair for the second group, the storage bindings a shader declares for itself, which only the shaders that have 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, the main camera's draws and every shadow view's summed together. renderer.shadowViews() splits compacted and compactedDrawn across the views that made them, and carries the camera's own share beside them.

Returns { draws: number, instances: number, compacted: number, compactedDrawn: number, materialBinds: number, materialBindsElided: number, materialExtraBinds: number, materialExtraBindsElided: number, pipelineBinds: number, pipelineBindsElided: 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 binds over", d.materialBinds + d.materialBindsElided, "units")
local d = renderer.drawStats(); print(d.pipelineBinds, "pipeline sets over", d.pipelineBinds + d.pipelineBindsElided, "units")

typed/builtin//modules/api/engine/renderer/renderer/feature/create

renderer.feature.create(ref: any?, guid: string?) -> 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

  • ref any (optional) — An AssetRef<renderFeature>, or a string identity/guid resolved via asset.resolve(ref, "renderFeature").
  • guid string (optional) — Optional explicit handle guid (minted when omitted).

Returns any — A RenderFeatureHandle.

local h = renderer.feature.create(asset.resolve("acrylic", "renderFeature"))
local h = renderer.feature.create("acrylic")

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

  • handleOrGuid any (optional) — A RenderFeatureHandle or its guid string.

Returns boolean

renderer.feature.destroy("eb623975-d8bd-47a1-a0a4-5c0e56238e2f")

typed/builtin//modules/api/engine/renderer/renderer/feature/list

renderer.feature.list() -> { { guid: string, identity: string } }

List every render feature currently live (running its render(ctx) each frame). Each entry is { guid, identity } — 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).

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

  • width number — Layer width in pixels.
  • height number — Layer height in pixels.
  • layers number — 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

  • layer number — Which layer of the array to write into.
  • textureKey string — The source texture's name — the one it was created under. A compute.createStorageTexture2D target, a compute.createTextureHistory pair (its current side), and a texture a compute.copyBufferToTexture wrote all answer to the name they were given.
  • x number — Left edge of the destination rectangle, in pixels.
  • y number — 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.

Returns { width, height, layers, filled }

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
refillMyCookie()
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 that completion signal has stopped arriving and the pacer stood down rather than hold the image indefinitely; it clears on the first frame that finds the count back under the bound.

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.

Returns FramePacing?{ framesInFlight, maxFramesInFlight, pacedFrames, submittedFrames, waitMs, mechanism, stalled, producing, idleSkippedFrames, presentMode, presentModes }, 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 to categorised — are the renderer's own accounting of what it asked for on purpose. Always present, on every backend.
  • allocator is the device allocator's ledger, with a row per creation label largest first, which is what names an allocation no category claims. It exceeds categorised by the per-frame render targets and the scratch nothing categorises. The allocator hands memory out from blocks it reserves whole from the device and returns a block only once nothing is left in it, so reservedBytes runs above allocatedBytes by what those blocks hold unused; blocks lists them emptiest first with the labels that keep each one alive, and emptyBytes plus slackBytes is that distance exactly — the pool held in empty blocks, and the room pinned inside blocks something still sits in.
  • driver.deviceLocalBytes is what the graphics driver charges this process, out of the kernel's own accounting. It is the biggest of the three and the one that fills a card, because it also holds the swapchain, the images the driver keeps on the renderer's behalf, and the rounding to whole pages and heap blocks that neither figure above sees. Read it when the question is how much of the machine's GPU this engine is using; read the two above when the question is what the engine spent it on. A platform with no per-process accounting reports available = false and the reason.
  • driver.outsideAllocatorBytes is that charge less everything the allocator reserved — what the driver holds on its own account, and the one figure here nothing releases: a dropped pipeline, another scene and renderer.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. 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.

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

  • handleOrKind any (optional) — The resource's handle, or its kind ("texture", "material", "mesh", "feature") with the guid or key as the second argument.
  • id string (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

  • target string | entityRef — The entity — a proxy from entity(...) / 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.

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/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

  • texture string | AssetRef — The texture — a guid, an identity, a name, a path, or a texture AssetRef.
  • 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

  • content MaterialContent
  • key string

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 }) 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 render state its draws are looked up under, whether a pipeline exists for that key, and how many draws the observed frame gave it. 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.

Parameters

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.

Returns anyMaterialContent? with resident: boolean and renderer: MaterialObservation? 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

  • key string | { [string]: any } | AssetRef — The material's registry key (the one passed to create), the MaterialHandle create returned, or an AssetRef from asset.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, and the shader it binds. renderer.references("material", key) says what is still holding a row, and renderer.collect() releases the rows nothing holds.

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. 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 observed — those five read 0 until something arms per-draw recording, which renderer.materialCost() and renderer.drawDiagnostics() do. nil means the renderer holds no material under this key at all — the writes landed on a record nothing is drawing with.

Parameters

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.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

  • entityId string — 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/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

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.
  • name string — Property name.
  • value any (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) -> ()

Push one changed texture slot to a registered material's GPU record. Keyed by the material's registry key.

Parameters

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.
  • slot string — Texture slot name ("base_color_texture", …).
  • ref string | { [string]: any } | AssetRef — Texture reference — a .texture guid / identity / name / path, the image path it was imported from, a color: / default: form, a live GPU handle, or a texture AssetRef carrying one. An asset reference is materialised (Disk→CPU→GPU) and bound by the key the upload lands under.

Returns ()

renderer.material.setTexture("sky", "sky_texture", "panorama.texture")

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; binds is how many material-owned bind groups the frame's passes SET for it and bindsElided how many of its draws wanted a group the pass already held, which is what draw-key sorting buys; a draw that fell back to the placeholder bound the placeholder's group, so it counts in placeholderDraws and in neither bind count. uniformBytes is the GPU uniform buffer's own size, which is the reflected property block raised to the 16-byte floor and rounded up to the copy alignment. renderer.drawDiagnostics() names WHICH renderable is not drawing what its material says, and why.

Returns { MaterialObservation }

for _, m in renderer.materialCost() do print(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 afterwards, so two renderables that differ only in material read different indices, and one renderable reads the same index frame after frame. It follows that the table keeps a row for every material name drawn this session, whether or not anything still draws with it. 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 instance_data[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 one the renderer has not drawn with yet. Pass it to a shader (or compare it against what a shader read out of instance_data[slot].material_index) to tell which material a drawing instance carries.

Parameters

  • name stringstring Material name, as renderer.material.create filed it.

Returns number?

local red = renderer.materialIndex("brick_red")

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/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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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

  • mesh string | { [string]: any } | AssetRef — A mesh loaded into the CPU store (renderer.mesh.loadCpu) — the MeshCpuHandle, a MeshHandle, a guid, or a mesh AssetRef.

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

  • ms number (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, 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, plus a copy for each queued mesh the engine holds no definition for. inFlight is one row per queued bake — { guid, cpuMs, frames, slices, bytes, state }: 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, and "baking" for the one being advanced against "queued" for the ones waiting their turn.

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

  • clusterBytes buffer | 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/clusters

renderer.mesh.clusters(mesh: string | { [string]: any } | AssetRef) -> { [string]: any }?

The shape of the cluster-LOD hierarchy the renderer holds for a mesh: clusterCount across every level, levelCount with the finest counted as one, and triangleCount across every cluster. The renderer keys one entry per mesh that carries a hierarchy, so this answers whether the mesh has clusters as well as what they are — nil for a mesh that carries none.

Parameters

  • mesh string | { [string]: any } | AssetRef — The mesh to read — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

Returns { [string]: any }?{ clusterCount: number, levelCount: number, triangleCount: number }?

local c = renderer.mesh.clusters(gpu) ; if c then print(c.levelCount) 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.

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

  • zmsh buffer | 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

  • mesh string | { [string]: any } | AssetRef — The mesh to release — a MeshHandle, a guid, a MeshCpuHandle or a mesh AssetRef.

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 once per frame, which is what buys that full-pass parity. Rewrite the buffer between frames and the instances move — no re-registration, no re-upload.

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 every frame like the transforms, 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

  • mesh string | { [string]: any } | AssetRef — The mesh the population draws — a MeshHandle, the guid renderer.mesh.list hands out, a MeshCpuHandle or a mesh AssetRef. 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.
  • opts any (optional){ transformBuffer, instanceCount, material, instanceDataBuffer?, reserveCount?, renderLayer?, castsShadows?, mobility? }.

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

Detach a mesh's cluster-LOD hierarchy and cancel a bake still in flight for it, so the renderer holds none for it. The inverse of renderer.mesh.uploadClusters.

Parameters

  • mesh string | { [string]: any } | AssetRef — The mesh to detach — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

Returns boolean — True if a hierarchy was attached 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

  • draw InstancedDraw — The InstancedDraw to 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

  • geom MeshGeometryMeshGeometry — flat per-vertex float / u32 arrays plus optional skinning and skins.

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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/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

  • draw InstancedDraw — The InstancedDraw to 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

  • draw InstancedDraw — The InstancedDraw whose 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/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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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

Parameters

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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.

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

  • ref string | AssetRef — A mesh AssetRef (carries .guid and reads its primary via getBytes), or any string asset.ref resolves to one — the guid encodeCpu takes, 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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
  • 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/readback

renderer.mesh.readback(mesh: string | { [string]: any } | AssetRef) -> 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.

Parameters

  • mesh string | { [string]: any } | AssetRef — The mesh — the MeshHandle renderer.mesh.create returned, a guid, or a mesh AssetRef.

Returns MeshCpuHandle

local cpu = renderer.mesh.readback(handle); local zmsh = cpu:encode(); cpu: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.

Parameters

  • requests { { entity: string, mesh: any } }{ { entity = <id>, mesh = <mesh> } } — the entity to read, and the mesh it is posed from (a guid, MeshHandle or mesh AssetRef).

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) -> boolean

Queue a cluster-LOD bake for the static CPU mesh held under guid, and attach the DAG to the GPU mesh of that same guid on the frame it finishes. The CPU mesh may be unloaded on the very next line; the DAG is then built one bounded slice per frame, so a dense mesh virtualizes without the frame loop stopping for the whole bake.

One bake is advanced per frame — the one at the head of the queue — and the geometry is read on the frame a bake gets there, from the definition the engine holds for the mesh. A queue of meshes the engine holds definitions for therefore holds one mesh's geometry rather than one per mesh, whatever its depth. A mesh the engine holds no definition for is copied into the queue as it is scheduled, since the CPU store is then the only thing holding it. 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

  • mesh string | { [string]: any } | AssetRef — A mesh loaded into the CPU store (renderer.mesh.loadCpu) — the MeshCpuHandle, a MeshHandle, a guid, or a mesh AssetRef.

Returns boolean — True if a bake was queued.

renderer.mesh.scheduleClusters(cpu) ; 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

  • draw InstancedDraw — The InstancedDraw to reconfigure.
  • count number — 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/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

  • draw InstancedDraw — The InstancedDraw to reconfigure.
  • renderLayer number — The membership bitmask, the same value drawInstanced takes as renderLayer. 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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
  • 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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

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

  • mesh string | { [string]: any } | AssetRef — The mesh to update — a MeshHandle, a guid, a MeshCpuHandle or a mesh AssetRef.
  • src any (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

Attach a cluster-LOD DAG (bytes from renderer.mesh.buildClusters) to the GPU mesh keyed by guid, enabling the continuous-cut cluster draw path for that mesh.

Parameters

  • mesh string | { [string]: any } | AssetRef — The mesh the clusters belong to — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
  • clusters string — Serialized cluster bytes (binary-safe).

Returns boolean — True if the upload was queued.

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

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/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. 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, 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/pointShadowBudget

renderer.pointShadowBudget() -> PointShadowBudget

The point-light shadow pool now in force. A point light with castsShadows renders an omnidirectional cube map, six faces of depth, and slots is how many of them fit — a further caster is lit but throws no shadow, and the engine log names how many were turned away. The slot count is bought rather than authored: megabytes of VRAM at resolution texels per face is what decides it.

Returns PointShadowBudget — The pool — see PointShadowBudget.

local p = renderer.pointShadowBudget()
print(("%d shadowed point lights, %.1f MiB"):format(p.slots, p.bytes / 1024 / 1024))

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/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

  • origin vec3vec3 ray start in world space
  • direction vec3vec3 ray direction; any length, the engine normalises
  • maxDistance number (optional)number? how far the ray reaches, in world units. Default 1000
  • exclude (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 camera.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

  • origin vec3vec3 ray start in world space
  • direction vec3vec3 ray direction; any length, the engine normalises
  • maxDistance number (optional)number? how far the ray reaches, in world units. Default 1000
  • maxHits number (optional)number? how many surfaces to return. Default 32
  • exclude (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

  • handleOrKind any (optional) — The resource's handle, or its kind with the id second.
  • id string (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. 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 plus 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, 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().

Parameters

  • handleOrKind any (optional) — The resource's handle, or its kind with the id second.
  • id string (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/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 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 channel and G-buffer over them under a camera's scene render. A draw builds what it needs, and the set goes once no live camera names the target and sixty frames have passed without a draw, so a target nothing draws into carries no such row; the colour image drawn into belongs to the texture cache and outlives every one of those releases. totalBytes is what the resident targets hold together. Measured at the end of the last rendered frame.

Returns { targets, totalBytes, residentCount }

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/resolutionScale

renderer.resolutionScale() -> number

The fraction of the display resolution the scene is currently rendered at. 1 until something sets it.

Returns number

local s = renderer.resolutionScale()

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

  • level number — 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

  • enabled booleanboolean

Returns ()

renderer.setBlendedBatching(false)  -- a draw per blended renderable

typed/builtin//modules/api/engine/renderer/renderer/setDepthPrepass

renderer.setDepthPrepass(enabled: boolean) -> ()

Enable or disable the opaque depth pre-pass. While enabled the renderer resolves opaque depth in its own pass before shading, so each shaded pixel runs its material once instead of once per surface stacked behind it, and the resolved depth is what occlusion culling reads. Enabled by default.

Parameters

  • enabled booleanboolean

Returns ()

renderer.setDepthPrepass(false) -- shade every layer, for comparison

typed/builtin//modules/api/engine/renderer/renderer/setDepthPrepassOrdering

renderer.setDepthPrepassOrdering(enabled: boolean) -> ()

Submit the depth pre-pass nearest-first. Renderables reach the pre-pass in the order they were registered, which stands in no relation to where the camera is: a scene built back-to-front makes every layer write depth and be overwritten by the layer in front of it. Ordered, the nearest surface writes first and the surfaces behind it are rejected by the depth test before they write. The same draws go out either way and the depth that comes out is the same, so scene.depth_prepass in profiler.gpuFrame() is what moves. Enabled by default.

Parameters

  • enabled booleanboolean

Returns ()

renderer.setDepthPrepassOrdering(false) -- submit in registration order

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

  • frames number (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/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

  • frames number — 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/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

  • pixels numbernumber — 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 pre-pass depth 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. Requires the depth pre-pass.

Parameters

  • enabled booleanboolean

Returns ()

renderer.setOcclusionCulling(true); print(renderer.cullStats().occlusionCulled)

typed/builtin//modules/api/engine/renderer/renderer/setPointShadowBudget

renderer.setPointShadowBudget(cfg: {
    megabytes: number?,
    resolution: number?,
}) -> number

Set how much VRAM the point-light shadow atlas may hold, and at what per-face resolution. An omitted field keeps its current value. The atlas is reallocated on the next frame, so renderer.pointShadowBudget().slots reports the new pool one frame later; the returned number is what this budget buys. Raising resolution sharpens every point shadow and spends the same memory on fewer of them — doubling it quarters the slot count. Values are clamped: megabytes [1, 1024], resolution [64, 4096], and the pool never exceeds renderer.pointShadowBudget().maxSlots. One slot is always granted, so a budget too small for a single cube shadows one light and the pool costs what that slot costs rather than what was asked for — { megabytes = 1, resolution = 4096 } buys 384 MiB of ceiling. Read pointShadowBudget().bytes back to see what a budget actually bought, and renderer.shadowMemory().point to see what the scene has made resident.

Parameters

  • cfg { megabytes: number?, resolution: number?, } — The fields to change — megabytes and/or resolution.

Returns number — Cube slots this budget buys.

renderer.setPointShadowBudget({ megabytes = 96 })
renderer.setPointShadowBudget({ resolution = 1024, megabytes = 96 })

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

  • mode string — 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/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

  • x number — Horizontal offset in NDC. One pixel is 2.0 / width.
  • y number — Vertical offset in NDC. One pixel is 2.0 / height.
renderer.setProjectionOffset(0.5 * 2 / w, -0.25 * 2 / h)

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

  • enabled booleanboolean

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 and present it at the display's own size. 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. UI and text are unaffected — they are drawn after the scene is brought back up to size. The scene rows in profiler.gpuFrame() are what move.

Parameters

  • scale numbernumber — 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/setShadowCaching

renderer.setShadowCaching(enabled: boolean) -> ()

Whether a shadow map that nothing changed is kept rather than drawn again. On by default: a shadow view — one directional cascade, one atlas layer of spot tiles, one face of a point light's cube — is rasterized on the frames its own inputs change and holds the depth it drew on the ones they do not. Off, every view is drawn on every pass, which is what a shadow suspected of holding a stale image is compared against.

Parameters

  • enabled booleanboolean

Returns ()

renderer.setShadowCaching(false)  -- draw every shadow view, every frame

typed/builtin//modules/api/engine/renderer/renderer/setShadowCasterBatching

renderer.setShadowCasterBatching(enabled: boolean) -> ()

Whether a shadow view draws every caster of one mesh together. On by default: a view — one directional cascade, one atlas layer of spot tiles, one face of a point light's cube — submits one draw per geometry over every caster of it the view admits, wherever those casters sit in render order and whatever transform slots they hold. Off, a view draws the runs of render-order neighbours that share a mesh AND hold consecutive slots, so a scene that has spawned and despawned anything fragments into many more draws. The image is the same either way, which is what makes this the comparison a shadow suspected of being placed by the batching is made against.

Parameters

  • enabled booleanboolean

Returns ()

renderer.setShadowCasterBatching(false)  -- draw the runs the scene presents

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 cascade, spot and cube face, 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 — minRadiusPx and/or maxDistance.

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: {
    resolution: number?,
    cascades: number?,
    distance: number?,
    splitLambda: number?,
    fadeFraction: number?,
    softness: number?,
}) -> ShadowConfig

Set the directional shadow quality. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. Values are clamped: resolution [64, 8192], cascades [1, 4], distance >= 0, splitLambda [0, 1], fadeFraction [0, 1], softness [0, 1]. Changing resolution or cascades reallocates the depth array; the rest are per-frame values. A distance of 0 hands the range to the frame — the splits are cut over the depth its own shadow-taking renderables reach — and a positive one caps it, which is what a scene bounding its shadow cost states.

Parameters

  • cfg { resolution: number?, cascades: number?, distance: number?, splitLambda: number?, fadeFraction: number?, softness: number?, } — The fields to change — see ShadowConfig.

Returns ShadowConfig — The full config now in force.

renderer.setShadowConfig({ softness = 0.65, fadeFraction = 0.28 })
renderer.setShadowConfig({ cascades = 4, resolution = 2048, distance = 240 })

typed/builtin//modules/api/engine/renderer/renderer/setShadowHero

renderer.setShadowHero(entity: string, padding: number?) -> ()

Give one caster a directional shadow view of its own, fit to its world bounds.

A cascade covers the slab of world the camera sees, so its texels are spread over tens of metres and one character standing in the middle of it is resolved by a handful of them. The hero view is the same light and the same depth range zoomed onto that entity's bounds, so the whole map goes into the shadow it and the ground under it carry — renderer.shadowHero().zoom is the factor its texel density gains.

It renders beside the cascades, into a layer of the same texture allocated while a hero is registered, and every surface inside it reads it in place of the cascade, crossing back at its edge. Nothing else about the shadow changes: the same casters reach it, at the same depth range, through the same filter.

Parameters

  • entity string — The entity whose renderables the view is fit around.
  • padding number (optional) — How much room the fit leaves around those bounds — for a pose that leaves the bind-pose box and for the filter that samples outside a silhouette. 1.0 fits them exactly.

Returns ()

renderer.setShadowHero(player.id)
print(("hero shadow: %.1f texels/unit vs %.1f"):format(
renderer.shadowHero().texelsPerUnit, renderer.shadowHero().cascadeTexelsPerUnit))

typed/builtin//modules/api/engine/renderer/renderer/setShadowProxy

renderer.setShadowProxy(mesh: string, proxy: string) -> ()

Rasterize proxy in place of mesh in every shadow view. 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

  • mesh string — The mesh a caster draws, as a guid or any mesh reference.
  • proxy string — The mesh it rasterizes into shadow views 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, and so does each shadow view and the velocity pass while renderer.shadowCasterBatching() is on — that switch is what makes a depth view form its draws by geometry at all. The camera depth pre-pass submits its casters nearest-first, which is a run per span of neighbours rather than a draw per geometry, so a crowd costs a draw per member there. 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

  • enabled booleanboolean

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

  • enabled booleanboolean

Returns ()

renderer.setSkinningPoseHold(false)  -- dispatch every pose, every frame

typed/builtin//modules/api/engine/renderer/renderer/setSpotShadowBudget

renderer.setSpotShadowBudget(cfg: {
    megabytes: number?,
    resolution: number?,
}) -> number

Set how much VRAM the spot/area shadow atlas may hold, and the per-side resolution of one layer. An omitted field keeps its current value. The atlas is reallocated on the next frame, so renderer.spotShadowBudget() reports it one frame later; the returned number is what this budget buys. Raising resolution sharpens the lights that cover the most screen and spends the same memory on fewer layers — doubling it quarters the layer count. Raising megabytes buys layers, which is what lets several lights hold a large tile at once. Values are clamped: megabytes [1, 1024], resolution [64, 4096], and the atlas never exceeds spotShadowBudget().maxLayers. One layer is always granted, so a budget too small for one still shadows lights and the atlas costs what that layer costs rather than what was asked for.

Parameters

  • cfg { megabytes: number?, resolution: number?, } — The fields to change — megabytes and/or resolution.

Returns number — Atlas layers this budget buys.

renderer.setSpotShadowBudget({ megabytes = 64 })
renderer.setSpotShadowBudget({ resolution = 2048, megabytes = 64 })

typed/builtin//modules/api/engine/renderer/renderer/setTextureBudget

renderer.setTextureBudget(opts: TextureBudgetOpts) -> TextureBudget

Bound the VRAM a world's textures occupy, by keeping only the mip levels the frame is actually sampling. Pass { megabytes = 256 }; 0 — the default — leaves texture residency alone and every texture stays fully resident the way it uploaded.

With a budget armed, 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 world whose look wants a different trade than one texel per pixel.

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, and whenever the budget was armed — 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 nowhere else, so one enrolled while it is resident holds them in system memory (renderer.textureMemory().streamSourceBytes) from the upload until a surface has worn it and gone, and releases them then, which is what keeps it out for the rest of the session; one whose pixels were already released when the budget was armed is out from the start. 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. Disarming returns every texture to the level it uploaded at, and arming again governs the textures the frame's surfaces are wearing then.

Parameters

  • opts TextureBudgetOpts{ megabytes: number?, bias: number? }

Returns TextureBudget{ megabytes, bias } — the budget now in force

renderer.setTextureBudget({ megabytes = 128 })
renderer.setTextureBudget({ megabytes = 128, bias = -1 })  -- one level finer everywhere
renderer.setTextureBudget({ megabytes = 0 })               -- leave residency alone

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 opacity (base_color alpha under a transparent blend) or transmission also draws into a light-space transmittance map, and the colour it lets through multiplies into the directional light reaching whatever stands behind it. Stacked casters compose. Opaque casters are unaffected, and a scene with no translucent caster allocates nothing and records no pass.

Parameters

  • enabled booleanboolean

Returns ()

renderer.setTransmissionShadows(true)  -- stained glass tints the floor

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.

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

Returns { ShaderCost }

local top = renderer.shaderCost()[1]; print(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

  • subject string | { [string]: any } — The entity — a proxy from entity(...) or an entity-id string — or the material, as its registry key or the MaterialHandle renderer.material.create returned.

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 last frame did with the shadow maps it already had. A shadow view — one directional cascade, one atlas layer of spot tiles, one face of a point light's cube — is drawn again only when something it draws from changed: its light moved, a caster it can see moved or appeared or vanished, a caster's geometry or material changed, a caster changed pose or moved the nodes its parts are placed by, or the map it writes into was reallocated. Anything else keeps the depth already in the texture, so a scene that stops moving reads rendered 0 while cached keeps climbing. A mesh whose vertices a compute pass writes — a population, or a mesh built from a compute buffer — re-renders the views it stands in every frame. A shadowed point light contributes six views, one per cube face, so a caster moving on one side of it re-renders the face that can see it and leaves the other five holding what they have. Counted per light kind, plus the totals across all three.

These are totals over every view of a kind. renderer.shadowViews() is the same frame one view at a time, each row naming the light that owns it and what it drew.

Returns { directionalRendered, directionalCached, spotRendered, spotCached, pointRendered, pointCached, rendered, cached }

local s = renderer.shadowCacheStats()
print(("shadow views: %d drawn, %d cached"):format(s.rendered, s.cached))

typed/builtin//modules/api/engine/renderer/renderer/shadowCaching

renderer.shadowCaching() -> boolean

Whether a shadow view may keep the depth it already holds.

Returns boolean

typed/builtin//modules/api/engine/renderer/renderer/shadowCasterBatching

renderer.shadowCasterBatching() -> boolean

Whether a shadow view draws every caster of one mesh together.

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 view. A shadow view 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 pass per shadowed light 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 directional shadow quality now in force. resolution and cascades size the cascade depth array; distance and splitLambda place the splits along the view; fadeFraction and softness shape how the result is sampled.

Returns ShadowConfig — The full config — see ShadowConfig.

print(renderer.shadowConfig().cascades)

typed/builtin//modules/api/engine/renderer/renderer/shadowHero

renderer.shadowHero() -> ShadowHeroReport

The registered hero caster and what the last frame's fit produced. A frame that fit nothing says why in decline.

Returns ShadowHeroReport — See ShadowHeroReport.

local h = renderer.shadowHero()
print(h.active, h.zoom, h.decline)

typed/builtin//modules/api/engine/renderer/renderer/shadowMemory

renderer.shadowMemory() -> {

How much GPU memory the shadow maps hold right now, in bytes, by the light kind that owns them. The spot atlas and the point pool are sized to the casters in the scene rather than to the budget, so spot and point move as lights that cast shadows appear and leave, and a scene with one shadowed light holds far less than one that fills every slot. A budget is the ceiling they grow within — renderer.spotShadowBudget().layers and renderer.pointShadowBudget().slots report that ceiling, unmoved by how many casters exist. Raising shadow resolution costs the square of the change across every cascade.

Returns { directional, spot, point, total } in bytes

local m = renderer.shadowMemory()
print(("shadows: %.1f MiB"):format(m.total / 1024 / 1024))

typed/builtin//modules/api/engine/renderer/renderer/shadowProxies

renderer.shadowProxies() -> ShadowProxyReport

The shadow proxies in force and what the last frame's shadow passes did with them. triangles and sourceTriangles are what those passes submitted and what they 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/shadowViews

renderer.shadowViews() -> ShadowViewReport?

Every shadow view the last rendered frame considered, and what each one cost.

A frame rasterizes a depth view per directional cascade, one for the hero caster, one per shadow-casting spot and six per shadow-casting point. renderer.shadowCacheStats() counts those views by light kind, renderer.drawStats() sums their draws with the camera's, and profiler.gpuFrame() carries one scene.shadow span across all of them. This is the same frame read one view at a time.

Each row names the view and the light that owns it, says whether it drew or kept the depth it already held, and carries the draws, the instances and the casters that went into it. span is the label the view's pass is timed under, so its GPU time is a lookup in profiler.gpuFrame(); every one of those labels is a variant of scene.shadow, which still carries their total. camera carries the same instance counters for the main camera, so the camera's share of a frame-wide total is a read rather than a measurement taken by turning every light's shadow off.

A cascade's near and far are where the split scheme cut its slice, not the world it covers: the fit takes the bounding sphere of that slice and rasterizes the ortho box around it, and both reach past far. What the cascade covers is center and radius, with viewProj the exact test; coversNear and coversFar read that volume back along one ray, the camera's view axis. directional states the axis reading for the set — how far it reaches (coversFar), the range the splits were run over (distance), how far the camera draws (cameraFar), and the depth past the reach the camera still draws (uncovered). A receiver further along the axis than coversFar has no directional depth map over it and is shaded as if the sun reached it, so uncovered is the room a missing shadow has and a surface standing in that room is what makes one; @builtin::systems.proxyOcclusion occludes past the cascades. The box is bounded in every direction, so a receiver standing wide of the axis leaves it at its own distance even where uncovered is 0 — viewProj is what answers for that receiver.

The list is rebuilt every frame: a view whose light stopped casting is absent from the next report rather than standing at the numbers it last had, and a frame that drew no shadow view answers a report whose views is empty. views grouped the way the shadow cache decides — a row per cascade, per spot atlas layer, per point cube — counts what renderer.shadowCacheStats() reports as rendered + cached.

The frame names its views only while something is reading them, so this call asks the frames after it to name 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()
print(("camera submitted %d of %d instances"):format(
report.camera.submittedInstances, renderer.drawStats().compactedDrawn))
for _, view in report.views do
print(("%s %d (%s): %d draws, %d instances, %s"):format(
view.role, view.index, view.light, view.draws, view.instances,
view.rendered and "drew" or "cached"))
end
local d = report.directional
if d ~= nil and d.uncovered > 0 then
print(("sun shadows stop at %.0f; the camera draws to %.0f"):format(
d.coversFar, d.cameraFar))
end

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.

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.

Returns { instances: number, poses: number, dispatches: number, held: number, reusedSlices: number, liveBytes: number, unsharedBytes: number, poolBytes: 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

  • bytes any (optional) — Capture bytes — .spz or .ply, as a buffer or a binary string.
  • convention string (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.readBytes("captures/ceramic.spz"))

typed/builtin//modules/api/engine/renderer/renderer/spotShadowBudget

renderer.spotShadowBudget() -> SpotShadowBudget

The spot and area-light shadow atlas now in force. Each shadow-casting spot is given a tile of it every frame, sized to what the camera can resolve: a light filling the view gets a whole layer at resolution, one far away gets a minResolution tile, and the atlas holds tiles of the smallest kind. That is what lets one budget serve a close hero light and a street of distant ones without either the memory or the sharpness being set for the worst case.

Returns SpotShadowBudget — The atlas — see SpotShadowBudget.

local s = renderer.spotShadowBudget()
print(("%d layers of %d, %.1f MiB"):format(s.layers, s.resolution, s.bytes / 1024 / 1024))

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; a deterministic capture takes a hold of its own that states no key, so it runs once the clock is handed back.

Parameters

  • at number (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.
  • options TemporalHoldOptions (optional)owner is the key this hold is taken under, and an exclusive hold states one. A hold that states no key is labelled with the agent the call is attributed to, which is the account the caller presented a token for and is shared by every session driving this engine under it. exclusive takes 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 png = 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) pushClock(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

  • owner string — The key the holds to release were taken under — what owner stated when they were taken, which renderer.temporal.owner reports.

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

  • texture string | { [string]: any } | AssetRef — The texture to read back — a TextureHandle, a guid, a TextureCpuHandle or a texture AssetRef.

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

  • width number — number Canvas width in pixels.
  • height number — number Canvas height in pixels.
  • fill any (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

  • bytes buffer | string — The ZTEX or image bytes.
  • encodeOpts any (optional){ format?, srgb?, generateMipmaps?, maxDimension? } applied when the bytes are an encoded image and need the engine-native encode.

Returns TextureCpuHandle

local cpu = renderer.texture.cpuFromBytes(vfs.read(path)); local png = cpu:encodePng(); cpu:unload()

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 to take the surface's own. 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 reading that has to be the viewport's own comes from screenSpace = "composite", or from a target created without screen. NEVER takes an AssetRef — load the CPU first.

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

  • ref string | AssetRef — A texture AssetRef, or a string naming one (guid, identity, name or source path).
  • encodeOpts any (optional){ format?, srgb?, generateMipmaps?, maxDimension?, filter? } applied when the primary is an encoded source image and needs the engine-native encode (a .ztex primary is decoded as-is).
  • keepCpu boolean (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) -> (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 ZTEX payload — handed back at the texel format the payload was written in, so a height field read back here keeps every bit it was authored with. A ZTEX holding 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).

Parameters

  • bytes buffer | string — A ZTEX payload or source image bytes.

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

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

  • texture string | { [string]: any } | AssetRef — The texture to release — a TextureHandle, a guid, a TextureCpuHandle or a texture AssetRef.

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. opts.format selects the on-disk precision: "rgba8" / "srgb" (default, 8 bits/channel, rgba is widthheight4 bytes) or the high-precision data formats "rgba16" (16-bit unsigned normalized, widthheight8 bytes) / "rgba32f" (32-bit float, widthheight16 bytes) — for height/displacement fields, baked lightmaps, and other data rasters an 8-bit format quantizes visibly. The two high-precision formats store rgba verbatim and reject opts.generateMipmaps / opts.maxDimension.

Parameters

  • rgba any (optional) — Pixel payload at opts.format's native byte width — a buffer, a binary string, or a number array.
  • width number — number
  • height number — number
  • opts any (optional){ format?, srgb?, generateMipmaps?, maxDimension? }

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.

Parameters

  • bytes buffer | string — source image bytes.
  • opts any (optional){ format?, srgb?, generateMipmaps?, maxDimension? }

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

  • texture string | AssetRef — The texture — a guid, an identity, a name, a path, or a texture AssetRef.

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, dimensions, mip count, filter ("nearest" or "linear" — the sampler baked into the blob from the asset's settings.filter), 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

  • ztex buffer | string — ZTEX bytes.

Returns (any, any)(table?, string?) { format, width, height, mipCount, filter, 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/isResident

renderer.texture.isResident(texture: string | { [string]: any } | AssetRef) -> boolean

True if a GPU texture is resident under this texture's guid.

Parameters

  • texture string | { [string]: any } | AssetRef — The texture — a TextureHandle, a TextureCpuHandle, a guid, or a texture AssetRef.

Returns boolean

print(renderer.texture.isResident(handle))

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 and its texel format, 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, "retained" when the cache holds the pixels for it. 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.

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

  • ref string | AssetRef — A texture AssetRef (carries .guid, reads its primary via getBytes), or any string asset.ref resolves to one — a guid, an identity, a name or a source path.
  • encodeOpts any (optional){ format?, srgb?, generateMipmaps?, maxDimension? } applied when the primary is an encoded source image and needs the engine-native encode (a .ztex primary 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

  • texture string | { [string]: any } | AssetRef — The texture — the TextureHandle renderer.texture.create returned, a guid, or a texture AssetRef.

Returns TextureCpuHandle

local cpu = renderer.texture.readback(handle); local ztex = cpu:encode(); cpu:unload()

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

  • histogram any (optional) — A histogram from cpu: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/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 raw 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. Returns a handle carrying the new dimensions: the handle it was given, refreshed, and a handle over the guid otherwise.

Parameters

  • texture string | { [string]: any } | AssetRef — The texture to update — a TextureHandle, a guid, a TextureCpuHandle or a texture AssetRef.
  • src any (optional) — New raw pixels {rgba, width, height, srgb?, format?}rgba as a buffer, a binary string, or a number array.

Returns TextureHandle — A TextureHandle for the updated texture.

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 format = "bc7" appears in the compressed columns at a quarter of what the same image costs as RGBA8. blockCompressionSupported is whether this adapter can hold block-compressed textures at all; where it is false a BC7 payload is uploaded decoded and lands in the uncompressed columns instead, so the texture is present everywhere and compressed where the hardware allows it. Measured at the end of the last rendered frame. streamableTextures is how many of them a 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, retainedTextures hold the payload because a script uploaded their pixels and the GPU copy is the only other one there is. streamSourceBytes is what those held payloads occupy in system memory — bytes that are not VRAM — so it is a reading on the retained half alone. pinnedTextures counts the textures big enough to stream that stand at a level nothing can move: their pixels were released and no asset holds them, 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. It reads 0 while no budget is armed.

Returns { blockCompressionSupported, compressedTextures, compressedBytes, uncompressedTextures, uncompressedBytes, streamableTextures, assetStreamedTextures, retainedTextures, pinnedTextures, streamSourceBytes }

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 armed budget, and 0 means residency is left alone. 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's demand alone would have cost, 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.

With budgetBytes at 0 nothing holds a level back, so residentBytes, plannedBytes and demandedBytes all read the whole chain of every texture still enrolled and textures is empty — which is how a session that armed a budget and dropped it reads back that the levels came home.

Returns TextureStreaming{ budgetBytes, 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 completed frame spent re-describing its renderables to the GPU. 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. bytes is what those uploads 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/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

  • program string — A program name from renderer.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/retarget/retarget/animation

retarget.animation(clipRef: any?, targetMeshRef: any?, sourceMeshRef: any?) -> (boolean, string)

Retarget an animation clip onto a target rig, returning the VFS path of a new .anim whose channels name the target skeleton's bones with bind-pose corrected rotations. The source rig is the clip's embedded rig.zmsh (else sourceMeshRef's skin, else the skinned mesh beside the clip in its bundle); the target rig is targetMeshRef's skin. Play the result with animGraph.addClip(entity, path). Pure asset transform — no entity/ECS state.

Parameters

  • clipRef any (optional) — Animation asset to retarget.
  • targetMeshRef any (optional) — Target rig mesh whose skin defines the destination skeleton.
  • sourceMeshRef any (optional) — Source rig mesh the clip was authored for; omit to use the clip's embedded rig.

Returns (boolean, string) — Success flag and the retargeted clip's VFS path (empty on failure).

local ok, path = retarget.animation(clipRef, targetMeshRef)

typed/builtin//modules/api/engine/retarget/retarget/extractRig

retarget.extractRig(meshBytes: buffer | string) -> string?

Strip a .mesh (ZMSH) payload to a lean skin-only rig: the skeleton with geometry removed, re-encoded as a ZMSH whose only content is the skin. Returns the rig bytes, or nil when the mesh carries no skin. A .animation composite embeds this as rig.zmsh so a clip travels with its own source rig.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns string? — Skin-only ZMSH rig bytes, or nil when the mesh has no skin.

local rig = retarget.extractRig(meshBytes)

typed/builtin//modules/api/engine/retarget/retarget/humanoidProfile

retarget.humanoidProfile(meshBytes: buffer | string) -> HumanoidHolder?

Derive the humanoid retarget holder for a rig from a .mesh (ZMSH) payload, when that skeleton has the essential humanoid structure (a hips root, a head or neck, at least one full arm chain and one full leg chain). Returns nil for a rig that is not a humanoid — a prop, a plant whose leaves animate, a quadruped — so a clip from it stays a plain clip rather than joining the shared humanoid-animation pool. A rig whose bone hierarchy loops answers nil and a message naming the bone edge that closes the loop, so a caller reading the second return value can tell malformed input from a plain non-humanoid.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns HumanoidHolder?{ base, boneCount, roles = { [role] = boneName } }, or nil when the rig is not a humanoid; nil and a message naming the closing bone edge when its hierarchy loops.

local holder = retarget.humanoidProfile(meshBytes)

typed/builtin//modules/api/engine/retarget/retarget/isHumanoid

retarget.isHumanoid(meshBytes: buffer | string) -> boolean

Whether a rig is a humanoid avatar — true when humanoidProfile resolves a holder for it. Use this to tell a humanoid character apart from a generic animated mesh (a prop, a plant, a quadruped) before treating its clips as shareable humanoid animations.

Parameters

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

Returns boolean — True when the rig has the essential humanoid structure.

if retarget.isHumanoid(meshBytes) then ... end

typed/builtin//modules/api/engine/retarget/retarget/serializeProfile

retarget.serializeProfile(holder: HumanoidHolder) -> string

Serialize a humanoid holder to the humanoid.profile file body: an editable YAML role -> bone-name map. Roles list hips-first head-to-toe through the limbs, then any extras name-sorted, so the file reads top-down and diffs stably. Edit a value to correct an auto-derived mapping.

Parameters

  • holder HumanoidHolder — A holder from humanoidProfile.

Returns string — The YAML body to store as humanoid.profile.

files["humanoid.profile"] = retarget.serializeProfile(holder)

typed/builtin//modules/api/engine/runtime_participation/M/isSaved

M.isSaved(mode: string) -> boolean

Whether an entity with this mode is written to the persisted world. True for WorldEntity, PrototypeOnly, and EditorOnly; false for RuntimeOnly.

Parameters

  • mode string — A RuntimeParticipation mode string.

Returns boolean — true when the mode is persisted on scene save.

typed/builtin//modules/api/engine/runtime_participation/M/liveInEdit

M.liveInEdit(mode: string) -> boolean

Whether an entity with this mode is live while authoring in edit mode. True for WorldEntity, PrototypeOnly, and EditorOnly; false for RuntimeOnly.

Parameters

  • mode string — A RuntimeParticipation mode string.

Returns boolean — true when the mode is present in edit mode.

typed/builtin//modules/api/engine/runtime_participation/M/liveInPlay

M.liveInPlay(mode: string) -> boolean

Whether an entity with this mode is live during play. True for WorldEntity and RuntimeOnly; false for PrototypeOnly and EditorOnly.

Parameters

  • mode string — A RuntimeParticipation mode string.

Returns boolean — true when the mode is present in play mode.

typed/builtin//modules/api/engine/runtime_participation/M/modeOf

M.modeOf(entityId: string) -> string

The entity's RuntimeParticipation mode. Defaults to "WorldEntity".

Parameters

  • entityId string — Entity id to read.

Returns string — One of "WorldEntity" / "PrototypeOnly" / "EditorOnly" / "RuntimeOnly".

typed/builtin//modules/api/engine/runtime_participation/M/set

M.set(entityId: string, mode: string)

Sets the RuntimeParticipation mode on an entity. A mode that is not saved marks the entity temporary so the scene-save exclusion drops it.

typed/builtin//modules/api/engine/runtime_participation/M/standsDown

M.standsDown(mode: string, engineMode: string) -> boolean

Whether an entity with this mode stands down — stops rendering and ticking — when an EDITOR session is in engineMode. This is the question a mode flip actually asks, and it is not liveInPlay: that answers which entities a SHIPPED RUNTIME contains, where there is no authoring surface at all. A session able to flip modes is an editor session by construction (the runtime profile forbids mode swaps), so the editor's own cameras, panels and gizmos are present in both of its modes and stand down in neither. What stands down in play is a template, whose clones are what runs; what stands down in edit is a runtime entity.

Parameters

  • mode string — A RuntimeParticipation mode string.
  • engineMode string — The engine mode the session is in, "play" or "edit".

Returns boolean — true when the mode should not be participating in that mode.

if rp.standsDown(entity(id).participation, tostring(engine.mode)) then ... end

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() -> string?

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 string? — 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?) -> string?

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

  • offering string — Fully-qualified offering identity provider/name (e.g. "origozero/mesh_gen").
  • endpoint string — Logical endpoint name (e.g. "create_preview").
  • opts InvokeOpts (optional){ params?, headers?, body?, idempotency_key? }.

Returns string? — 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/jobStatus

service.jobStatus(jobId: string) -> string?

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

  • jobId string — Job id returned by submitJob.

Returns string? — 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?) -> string?

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

  • offering string — Fully-qualified offering identity provider/name (e.g. "origozero/mesh_gen").
  • endpoint string — Logical endpoint name (e.g. "create_preview").
  • opts InvokeOpts (optional){ params?, headers?, body?, idempotency_key? }.

Returns string? — 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 every key.

Parameters

  • keys string | { string } — One registration key, or the array of keys (guid, identity, aliases) the one compiled program answers to.
  • opts { [string]: any }{ source, domain?, properties? } — the author's WGSL, its @domain, and the declared property schema.

Returns boolean — True when the compile was queued.

shader.compile({ ref.guid, ref.identity }, { source = wgsl, properties = props })

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

  • keys string | { string } — One key, or the array of keys this module answers to.
  • source string — 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

  • name string — Shader identity or guid — the key it compiled under.

Returns (string, string?) — Status, and the compiler error when it failed.

local status, err = shader.status(ref.guid)

typed/builtin//modules/api/engine/shell/shell/run

shell.run(command: 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, so from a coroutine it is queued to run off the frame loop and this yields until it finishes; everything else runs inline. That queueing runs the whole line, so a line that also ran a command of its own comes back with the explanation in stderr and shell.runAsync as the way to run it whole.

Parameters

  • command string — Shell command to execute.

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) -> string

Asynchronous version of shell.run. Returns a promise ID that resolves to a JSON-encoded result string. Use with task.await().

Parameters

  • command string — Shell command to execute.

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

  • sinkHandle number — Sink handle from bindPose.
  • poseBuffer Substrate.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

  • zanimBytes buffer | string — The clip's data.zanim payload 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.
  • opts SkeletonLayout{ 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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).

Returns { string }? — Bone names referenced by the clip, or nil.

local names = skeleton.clipBones(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 binary parse is the engine's; json.decode the result to inspect or transform a clip's channels (e.g. the retarget bake) in Luau. Returns nil on bytes that aren't a valid zanim payload. Inverse of clipEncode.

Parameters

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).

Returns string? — The clip as a JSON string, or nil.

local clip = json.decode(skeleton.clipDecode(bytes))

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. Returns nil on invalid JSON.

Parameters

  • jsonString string — A clip JSON document.

Returns string? — The clip's zanim payload bytes, or nil.

local bytes = skeleton.clipEncode(json.encode(clip))

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

  • entityId string | 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

  • handle number — Sampler handle from bindClip.
  • time number — Sample time in seconds.
  • poseBuffer Substrate.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

  • handle number — 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

  • sinkHandle number — 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].

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/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

  • name string — 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.

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

  • dir SkyColor — 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

  • time number — 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

  • listener string — 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

  • handle string — 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?) -> string

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

  • url string — Listen URL — scheme://host:port.
  • opts StreamListenOpts (optional) — Per-connection capacities and the accept backlog (optional).

Returns string — 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

  • listener string — 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?) -> string

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>&notify=<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

  • url string — Stream URL — scheme://target[?k=v&k=v].
  • opts StreamOpenOpts (optional) — Buffer capacities (optional).

Returns string — 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&notify=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.

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

  • handle string

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

  • handle string — Stream handle from stream.open.
  • bytes string — 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

  • scene any (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

  • scene any (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

  • scene any (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

  • subject any (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

  • s string — The text to read.
  • pos number (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

  • id string — 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).

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.

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

Parameters

  • opts BufferOpts{ type, len, kind?, usage?, name? }type is "f32", "vec3", "vec4", "quat" or "mat4"; kind is "cpu" (the default) or "gpu". name is the name a dispatch binds a "gpu" buffer by, and the name substrate.getBuffer and substrate.destroyBuffer reach it under.

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 values = pose:read(0, 16):result()

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

  • name string — 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/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

  • name string — 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

  • key string (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, and refs — how many holders it has. This is what states which names are taken and at what shape.

Returns { NamedBuffer } — Array of { id, name, type, len, refs }.

for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end

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

  • handle any (optional) — Text handle from text.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.

typed/builtin//modules/api/engine/text/text/destroy

text.destroy(handle: any?) -> boolean

Destroy a text handle and release its raster + glyph layout.

Parameters

  • handle any (optional) — Text handle from text.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 asked for. requested is what was asked, resolved is the face that answered, matched says whether they agree and reason says why when they do 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.

Parameters

  • handle any (optional) — Text handle from text.create.

Returns any{ requested, resolved, postScriptName, matched, reason, faces, glyphCount }, 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/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

  • ref any (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

  • handle any (optional) — Text handle from text.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

  • handle any (optional) — Text handle from text.create.
  • texture any (optional) — Destination GPU texture handle (renderer.texture.create) or its guid string — WHERE the raster lands.
  • scale number (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

  • handle any (optional) — Text handle from text.create.
  • style table — 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

  • handle any (optional) — Text handle from text.create.
  • content string — 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

  • handle any (optional) — Text handle from text.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.

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

  • id string — 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

  • screen string — Screen id containing the target Window.
  • window string — Widget id of the Window.
  • opts CaptureOpts (optional){ width, height } (optional).

Returns CaptureResult?{ rtHandle, texturePath } or nil.

typed/builtin//modules/api/engine/ui/ui/click

ui.click(callbackId: string, value: any?)

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.

Parameters

  • callbackId string — Callback id assigned to the widget.
  • value any (optional) — Optional value to pass with the callback.

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

  • name string — Style name.
  • style StyleProps — 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

  • name string — 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

  • widgetId string — The id the widget records layout under.

Returns WidgetPaint? — The widget's row, or nil before the UI has published a frame.

"hud-healthbar"

typed/builtin//modules/api/engine/ui/ui/dragState

ui.dragState() -> { payload: string, x: number, y: number }?

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. Poll during a drag to drive live feedback (a placement ghost following the cursor); the drop itself still lands through the target's onDrop. Snapshotted each frame.

Returns { payload: string, x: number, y: number }?{ payload, x, y } 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

  • screenName string — Screen id passed to ui.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

  • widgetId string — Widget id to focus.

typed/builtin//modules/api/engine/ui/ui/focusedWidget

ui.focusedWidget() -> string?

Return the widget id of whichever widget currently holds keyboard focus, or nil. Snapshotted post-render each frame.

Returns string? — Focused widget id or nil.

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

  • id string — 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

  • id string — 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

  • id string — 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

  • widgetId string (optional) — Optional widget id to query.

Returns LayoutInfo? — Layout info table 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

  • screenName string — 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

  • name string — 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

  • typeName string — 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

  • name string — 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 in the space ui.screenSize() reports — the same space getLayoutInfo rects and gui.clickAt use.

Parameters

  • x number — Logical X.
  • y number — 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.

640, 360

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

  • screenName string (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. 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, 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, layer, and whether the screen has a populated root widget tree, 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/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. generation advances once per re-rendered frame, so two calls reporting the same number describe the same frame.

Parameters

  • screenName string (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.

"hud"

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

  • a string — First widget id.
  • b string — Second widget id.

Returns number? — -1, 0, 1, or nil when the reading holds no row for one of them.

"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().

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.

Parameters

  • shaderHandle any (optional) — The screen .shader's asset handle, from asset.resolve.
  • width number (optional) — Render target width (default 1280).
  • height number (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

  • key string — Stable identifier for this registration (re-register replaces).
  • env { [string]: any } — Environment table whose onCallback receives 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).

Parameters

  • name string — Unique screen identifier.
  • widgetTree WidgetTree — Root widget table.
  • layer number (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

  • name string — Theme name to register.
  • theme ThemeDefinition — Flat-resolved theme table.

typed/builtin//modules/api/engine/ui/ui/removeScreen

ui.removeScreen(name: string) -> boolean

Alias for ui.unregisterScreen.

Parameters

  • name string — 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

  • id string — 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

  • widgetId string — The widget id (NOT the onClick / onChange callback id).

Returns WidgetResponse? — WidgetResponse or nil.

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

  • name string — 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.

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

  • deltaX number — Horizontal scroll delta.
  • deltaY number — 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

  • id string — Area widget id.
  • x number — Target pivot x (screen coords).
  • y number — 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

  • id string — Area widget id.
  • w number — Target width (screen coords).
  • h number — 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

  • dockId string — DockArea widget id.
  • panelId string — Id of the panel held by the window to place.
  • x number — Window left edge (screen coords).
  • y number — Window top edge (screen coords).
  • width number — Window width (screen coords).
  • height number — 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 defaults to the ui bit; the editor places its chrome on EditorUI so agent captures can drop it. Masks come from __renderLayers.bit(name).

Parameters

  • name string — Screen identifier.
  • mask number — Render-layer membership bitmask.

typed/builtin//modules/api/engine/ui/ui/setScrollPosition

ui.setScrollPosition(widgetId: string, offsetY: number)

Set the scroll offset of a scrollArea widget.

Parameters

  • widgetId string — Scroll area widget id.
  • offsetY number — 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

  • name string — 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

  • name string — 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

  • name string — Screen identifier to show.

Returns boolean — True when a screen by this name is registered.

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

  • key string — The key passed to ui.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

  • name string — 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

  • name string — 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

  • name string — Screen identifier to update.
  • widgetTree WidgetTree — 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

  • themeName string — 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

  • widgetId string — Widget id whose state to read.
  • key string — State key.
  • default any (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

  • widgetId string — Widget id whose state to clear.
  • key string — 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

  • widgetId string — Widget id to scope the state under.
  • key string — State key.
  • value any (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

  • opts PickOpts (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", 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

  • opts PickOpts (optional) — Picker options (optional); folder is forced true.

Returns PickResult — The decoded result table.

local r = userfile.pickFolder({ writeTo = "/source/imported/" })

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/copy

vfs.copy(src: string, dst: string) -> (boolean, 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

  • src string — Source absolute VFS path (file or directory).
  • dst string — Destination absolute VFS path.

Returns (boolean, string?) — True on success; (false, errmsg) on failure.

vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")

typed/builtin//modules/api/engine/vfs/vfs/currentAuthor

vfs.currentAuthor() -> { id: string, name: string? }?

The agent this call is attributed to — the author a /source write made right now would be recorded under in the play shadow. Nil when the call carries no actor identity, which is the case for engine-authored work and for a caller that presented no token. Compare its id against vfs.playShadowAuthors() to separate your own pending edits from a co-author's.

Returns { id: string, name: string? }?{ id, name } for the acting agent, or nil when unattributed.

local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")

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

  • path string — VFS path whose bytes should be evicted.
  • opts VfsOpts (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

  • path string — VFS path to check.
  • opts VfsOpts (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/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

  • path string — VFS path to classify.
  • opts VfsOpts (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

  • path string — VFS path to classify.
  • opts VfsOpts (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.

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

  • path string — Directory path.
  • opts VfsOpts (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, string?)

Move a file from src to dst. By default fires the destination's write side effects; pass opts.quiet = true to suppress them. 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

  • src string — Source absolute VFS path.
  • dst string — Destination absolute VFS path.
  • opts { quiet: boolean? } (optional) — Optional { quiet: boolean? }.

Returns (boolean, string?) — True on success; (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")

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/pendingWrites

vfs.pendingWrites() -> { string }

List the /source paths with an in-flight local write the synced manifest has not reflected yet — the read-your-writes frontier. A just-written 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. Empty when fully synced.

Returns { string } — Array of VFS paths with pending (unconfirmed) local writes.

for _, p in ipairs(vfs.pendingWrites()) do print(p) end

typed/builtin//modules/api/engine/vfs/vfs/playShadowAuthors

vfs.playShadowAuthors() -> { [string]: { id: string, name: string? } }

The agent behind each currently-shadowed /source path: the ZeroMind user id the write was attributed to, and the username to show for it. 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. A path written with no actor identity carries no entry — it belongs to no agent in particular, and stays settleable by any of them.

Returns { [string]: { id: string, name: string? } } — Map of shadowed path to { id, name }.

local mine = vfs.currentAuthor()
for path, who in pairs(vfs.playShadowAuthors()) do
if mine == nil or who.id ~= mine.id then print(path, "belongs to", who.name) end
end

typed/builtin//modules/api/engine/vfs/vfs/playShadowPaths

vfs.playShadowPaths() -> { string }

List the /source paths edited during running play that are currently held as copy-on-write SHADOWS (MemFs-only, on-disk original untouched) — 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

Promote a single play-shadow edit into a canonical write. Re-asserts the live overlay bytes through the full write pipeline with the play write lock released, then unmarks the path. The bytes stay in the engine end to end, so binary content promotes exactly. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A promotion that cannot happen raises with the reason: the path is not shadowed, the path is a folder covering shadowed edits, play is running, 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.

Parameters

  • path string — Shadowed VFS path to promote (one of vfs.playShadowPaths()).

Returns string — The path the call settled — it is no longer shadowed.

engine.paused = true
local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg")
engine.paused = false

typed/builtin//modules/api/engine/vfs/vfs/read

vfs.read(path: string, opts: VfsOpts?) -> string?

Read a file from the virtual filesystem. Binary-safe. Returns file contents as a string, or nil if the file is not known. Relative paths resolve under opts.root (default /source/). When called from a coroutine and the bytes aren't locally cached, transparently yields the coroutine while the lazy fetch runs.

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

  • path string — VFS path.
  • opts VfsOpts (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/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.

Parameters

  • modulePath string (optional) — Module identity to reload (omit to reload all).

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.

Parameters

  • path string — VFS path to remove.
  • opts VfsOpts (optional){ root = "/source/", recursive = 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

Revert a single play-shadow edit: restore 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. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A revert that cannot happen raises with the reason, on the same terms as vfs.promotePlayShadow.

Parameters

  • path string — Shadowed VFS path to revert (one of vfs.playShadowPaths()).

Returns string — The path that is now reverted and no longer shadowed.

engine.paused = true
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
engine.paused = false

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

  • path string — 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

  • watcherId number — Watcher id returned by vfs.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) -> ()) -> 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

  • path string — Exact path, or folder path ending in /.
  • callback (string, string) -> ()(mutated_path, kind) -> (), kind "write" or "remove".

Returns number — Watcher id.

local id = vfs.watch("/zero/source/", function(path, kind) print(kind, path) end)

typed/builtin//modules/api/engine/vfs/vfs/write

vfs.write(path: string, content: string, opts: VfsOpts?) -> (boolean, 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 with disk source untouched, and is discarded on a guarded play-exit unless accepted. Scratch under /source/tmp/ writes through untouched. 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.

Parameters

  • path string — VFS path to write to.
  • content string — File content (binary-safe).
  • opts VfsOpts (optional){ root = "/source/", overwrite = true, quiet = false, durable = false }.

Returns (boolean, string?) — True on success; on failure returns false + error message. A durable write raises instead of returning false.

vfs.write("/zero/source/notes.md", body)
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.

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

  • handle string — Video handle from video.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.

Parameters

  • handle string — Video handle.

Returns VideoInfo?{ width, height, duration, currentTime, state, rate, loop } 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

  • handle string — Video handle.

Returns boolean — True if the video was playing and is now paused.

video.pause(rt)

typed/builtin//modules/api/engine/video/video/play

video.play(handle: string) -> boolean

Start or resume video playback.

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

  • handle string — Video handle.
  • time number — Target time in seconds.

Returns boolean — True if the seek was performed.

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

  • handle string — Video handle.
  • loop boolean — 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

  • handle string — Video handle.
  • rate number — 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

  • handle string — Video handle.

Returns boolean — True if the command was accepted.

video.stop(rt)

typed/builtin//modules/asset_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

  • envelope any (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("modules.asset_ref").build({ type = "material", path = "/zero/source/Gold.material", ... })

typed/builtin//modules/asset_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("modules.asset_ref").flushPendingPersists()

typed/builtin//modules/asset_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 guid, and a type may be holding it directly.

Parameters

  • guid string — The asset's stable guid.

Returns boolean — True when there was runtime state to forget.

require("modules.asset_ref").forgetRuntime(ref.guid)

typed/builtin//modules/asset_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_type string — 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("modules.asset_ref").loadTypeBehavior("dialogue")

typed/builtin//modules/asset_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 (modules/asset_change_dispatch) can reach a type's onChange hook without duplicating the resolution logic.

Parameters

  • asset_type string — The type name (e.g. "dynamicAsset", "material").

Returns { [string]: any }? — The type module table, or nil.

local m = require("modules.asset_ref").loadTypeModule("dynamicAsset")

typed/builtin//modules/asset_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

  • self any (optional) — Any AssetRef.
require("modules.asset_ref").persistInEditMode(matRef)

typed/builtin//modules/asset_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//modules/asset_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//modules/asset_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//modules/asset_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//modules/asset_ref/assetRef/exists

assetRef.exists(self) -> boolean

Whether this asset's path reads back as content.

Parameters

  • self

Returns boolean

typed/builtin//modules/asset_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

  • self
  • filename string (optional)

Returns string?

typed/builtin//modules/asset_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

  • self
  • filename string (optional)

Returns string?

typed/builtin//modules/asset_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

  • self
  • filename string (optional)

Returns string?

typed/builtin//modules/asset_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//modules/asset_ref/assetRef/has_backing_asset

assetRef.has_backing_asset -> boolean

Whether a /zero/source/ asset backs this ref.

Returns boolean

typed/builtin//modules/asset_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//modules/asset_ref/assetRef/meta

assetRef.meta -> { [string]: any }?

This asset's .meta sidecar, parsed.

Returns { [string]: any }?

typed/builtin//modules/asset_ref/assetRef/modules

assetRef.modules() -> { [string]: any }?

The shared modules this asset's type ships, reached as ref.modules.<name>.

typed/builtin//modules/asset_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//modules/asset_ref/assetRef/typeRef

assetRef.typeRef -> string?

The guid of the asset type this asset was authored against, read from its .refs sidecar.

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

  • bundle BundleNamespace — The target namespace table. No-op when given a non-table value.
require("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

  • entityId string — The entity whose hierarchy is captured into the bundle.
  • bundleRef BundleRef (optional) — Optional. When omitted, the ref is inferred from the entity's Asset.source field. When given, the explicit ref wins.

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

  • data any (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. @builtin::systems.particles.curves reads the same three keypoint shapes.

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

  • entityId string — The entity id.
  • component string — 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

  • ref ComponentRef — 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

  • component string — The component name.

Returns booleantrue 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

  • api string — The calling API, named in any diagnostic.
  • entityId string — The entity id.
  • component string — The component name.
  • field string — 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

  • api string — The calling API, named in the line.
  • component string — The component name.
  • missing number — How many of the named entities do not carry it.
  • total number — How many entities the call named.
  • outcome string — 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

  • api string — The calling API, named in the line.
  • entityId string — The entity that does not carry it.
  • component string — The component name.
  • outcome string — 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

  • api string — The calling API, named in the line.
  • component string — The component name.
  • field string — The field name that resolved to nothing.
  • outcome string — 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

  • api string — The calling API, named in the error so the message points at the call.
  • component string — 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

  • api string — The calling API, named in any diagnostic.
  • entityId string — The entity id.
  • component string — The component name.
  • field string — The field name.
  • value any (optional) — The value to write.

Returns booleantrue 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("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

  • v any (optional) — The value to test.

Returns booleantrue for a computed sentinel, false otherwise.

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/entity

connectedUser.entity -> entityRef?

This user's world-presence body: the avatar bound to them in the active scene, resolved on access.

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/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

  • identity string

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.

typed/builtin//modules/connected_users/connectedUsers/list

connectedUsers.list() -> { connectedUser }

Every connected user, ordered by identity.

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

  • handle number

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

  • handle number

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

  • handle number

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

  • path string — VFS path whose content changed.
require("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 never written this VM). 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.

typed/builtin//modules/data_schema/M/applyDefaults

M.applyDefaults(merged: { [string]: FieldSpec }, values: { [string]: any }) -> { [string]: any }

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

Parameters

  • merged { [string]: FieldSpec } — Merged field map from mergeChain.
  • values { [string]: any } — The instance's raw value table.

Returns { [string]: any } — New table: explicit values + defaults.

local filled = DS.applyDefaults(merged, rawValues)

typed/builtin//modules/data_schema/M/mergeChain

M.mergeChain(chain: { Schema? }) -> ({ [string]: FieldSpec }?, { string })

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

Parameters

  • chain { Schema? } — Array of Schema, root parent first. A nil hole (e.g. a failed parseSchema result passed straight in) is a problem entry.

Returns ({ [string]: FieldSpec }?, { string }) — Merged { [string]: FieldSpec }, or nil if any problem was found. Array of problem strings (empty on success).

local merged, problems = DS.mergeChain({ itemSchema, weaponSchema })

typed/builtin//modules/data_schema/M/parseSchema

M.parseSchema(raw: any?) -> (Schema?, { string })

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

Parameters

  • raw any (optional) — The decoded document ({ extends?, fields }).

Returns (Schema?, { string }) — The parsed Schema, or nil if any problem was found. Array of problem strings (empty on success).

local schema, problems = DS.parseSchema(Yaml.decode(bytes))

typed/builtin//modules/data_schema/M/validateValues

M.validateValues(merged: { [string]: FieldSpec }, values: { [string]: any }, resolvers: Resolvers) -> { Violation }

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

Parameters

  • merged { [string]: FieldSpec } — Merged field map from mergeChain.
  • values { [string]: any } — The instance's raw value table.
  • resolvers Resolvers — assetExists / contractSatisfied callbacks.

Returns { Violation } — Array of { path, message } violations (empty = valid).

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

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

  • id any (optional) — Entity id, name, or proxy table.
  • lifetime number (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

  • handleOrId any (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.

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

  • id any (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/theme/M/activate

M.activate(name: string) -> boolean

Activate a registered theme. Thin wrapper over ui.setTheme(name) for symmetry with register / load.

Parameters

  • name string — The registered theme name to activate.

Returns booleantrue on success, false when name is invalid or the FFI binding is missing.

Theme.activate("dark")

typed/builtin//modules/deprecated/zui/theme/M/defaults

M.defaults() -> TokenMap

Return the raw fallback token map shipped with this module. Used by tests / introspection; not part of the cascade.

Returns TokenMap — The static DEFAULTS table — same reference each call.

local d = Theme.defaults()

typed/builtin//modules/deprecated/zui/theme/M/load

M.load(name: string) -> (boolean, string?)

Convenience: require("@builtin::themes." .. name) then register. Built-in themes (dark, light, debug) live at src/lua/lib/themes/<name>.module/init.luau.

typed/builtin//modules/deprecated/zui/theme/M/register

M.register(name: string, theme: any?) -> (boolean, string?)

Register a theme with the engine under name. Walks the theme's tokens + styles, resolves every $variable reference (with cycle detection), and pushes flat values to the active ThemeRegistry.

Parameters

  • name string — The name to register the theme under.
  • theme any (optional) — The theme table { tokens, styles }name is overridden by the caller-supplied name.

Returns (boolean, string?)(true, nil) on success, or (false, errMsg) on cascade error or missing FFI binding.

local ok, err = Theme.register("dark", themeTable)

typed/builtin//modules/deprecated/zui/theme/M/resolve

M.resolve(theme: any?) -> (ResolvedTheme?, string?)

Resolve $variable references in a theme table and return the flat { name, tokens, styles } shape the engine consumes. Pure function — used by register and exposed for tests.

Parameters

  • theme any (optional) — A theme table with name, tokens, styles.

Returns (ResolvedTheme?, string?)(resolved, nil) on success or (nil, errMsg) on cascade failure.

local res, err = Theme.resolve({ name = "dark", tokens = {...} })

typed/builtin//modules/deprecated/zui/theme/M/tokenNames

M.tokenNames() -> { string }

Return the sorted list of token names shipped with this module. Useful for theme editors / token pickers.

Returns { string } — Sorted array of token name strings.

for _, n in ipairs(Theme.tokenNames()) do print(n) end

typed/builtin//modules/deprecated/zui/theme/M/with

M.with(overrides: TokenMap?) -> ThemeView

Build a read-only theme view that overlays the given overrides on top of the engine-or-defaults token map. Module functions are exposed alongside tokens so the view doubles as the namespace.

Parameters

  • overrides TokenMap (optional) — Optional table of { [tokenName] = value } overrides.

Returns ThemeView — A read-only view — index returns override / function / token.

local view = Theme.with({ accent = "#ff0" })
local view = Theme.with(nil)  -- defaults only

typed/builtin//modules/deprecated/zui/theme/cascade/M/resolveStyles

M.resolveStyles(theme: any?, resolvedTokens: any?) -> (StyleMap?, string?)

Walk theme.styles and resolve every $reference inside style values against resolvedTokens. Returns a { [selector]: { [prop]: value } } map with no remaining $variable strings. Selector and property keys pass through unchanged (cascade ordering is the caller's concern).

Parameters

  • theme any (optional) — A theme-shaped table with a styles field.
  • resolvedTokens any (optional) — Pre-flattened tokens (output of resolveTokens).

Returns (StyleMap?, string?)(flatStyles, nil) on success or (nil, errMsg) on first cycle / unknown reference.

local styles, err = Cascade.resolveStyles({ styles = { ["btn"] = { color = "$accent" } } }, tokens)

typed/builtin//modules/deprecated/zui/theme/cascade/M/resolveTokens

M.resolveTokens(theme: any?) -> (TokenMap?, string?)

Walk theme.tokens and resolve every $reference to a literal. Tokens that reference other tokens are flattened — after this pass, every value is a literal string / number / etc.

Parameters

  • theme any (optional) — A theme-shaped table with a tokens field.

Returns (TokenMap?, string?)(flatTable, nil) on success or (nil, errMsg) on first cycle / unknown reference. Errors include the offending key for debugging.

local tokens, err = Cascade.resolveTokens({ tokens = { a = "$b", b = "#fff" } })

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

  • id string (optional) — Widget id used by the engine for event routing and DOM mirror.
  • opts CanvasOpts (optional) — Options table — commands array plus optional width/height, interaction handlers, role/tag overrides, ARIA passthroughs (any aria* 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

  • widgetType string — The widget kind string (e.g. "label", "panel", "canvas").
  • opts Opts (optional) — Optional. id, classes (or class), props, style.
  • children any (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/editor/component_inspectors/M/get

M.get(typeName: string) -> InspectorView?

Look up the registered view for a component type. Returns nil when the type has no custom inspector (the caller renders the generic fields alone).

typed/builtin//modules/editor/component_inspectors/M/register

M.register(typeName: string, view: InspectorView)

Register a custom inspector view for a component type. Idempotent — re-registering replaces the previous view (hot-reload re-runs component load code, so last-write-wins is the correct semantic).

Parameters

  • typeName string — The component's short authored name (e.g. "Generator").
  • view InspectorView{ sections = fn(entityId, proxy) -> { fields, actions }? }.
Inspectors.register("Generator", require("~.generator_inspector"))

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

  • entityId string — The entity to replace (must exist). Must be a non-empty string.
  • assetPath string — The asset path to spawn. Must be a non-empty string.
  • opts SwapOpts (optional) — Optional SwapOpts. 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/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

  • rid string — Runtime entity id carrying the instance.
  • componentType string — Component type name as the entity reports it.
  • instanceName string (optional) — Name of the instance, or nil for an anonymous one.
  • data any (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. 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 the fields holding such a reference in entity_fields, taken from the component's declared field kinds, so a rebuild resolves exactly those.

Parameters

  • rid string — Runtime entity id to capture.
  • parentOriginalId string (optional) — Parent's original_id, or nil for a root record.

Returns any — One record table.

local rec = EntityRecords.captureRecord(id, nil)

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

  • rid string — Runtime entity id carrying the instance.
  • componentType string — 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

  • rootId string — 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 as compose.

Returns { any } — Flat array of records covering every root's hierarchy.

local records = EntityRecords.composeMany({ idA, idB })

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

  • entityIdA string — First entity id.
  • entityIdB string — 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

  • entityId string — 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

  • entityId string — The entity id.
  • component string — The component name (e.g. "Transform", "Model").
  • field string — 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

  • entityId string — 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

  • entityId string — 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

  • entityId string — 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

  • entityId string — 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

  • componentName string — 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

  • entityId string — The entity id.

Returns booleantrue 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

  • entityId string — The entity id.
  • component string — The component name.
  • fields { [string]: any }{ fieldName = value, ... } map of fields to write.

Returns booleantrue 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

  • entityId string — The entity id.
  • component string — The component name.
  • field string — The field name.
  • value any (optional) — The new value.

Returns booleantrue 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

  • entityId string — The entity id.
  • pos Vec3 — 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

  • entityId string — The entity id.
  • scale Vec3 — 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

  • entityId string — 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/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".

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, so c.color = c.color round-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

  • target string | { string } — A single target field name, or an array of target field names.
  • description string (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, 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 — Asset category as a string literal ("material", "mesh", etc.). Inferred into C.
  • default AssetRef<C> | I | nil (optional) — AssetRef envelope, identity string, or nil.
  • mode SyncModeSync or NoSync — 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)

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

  • bits number — How many bits wide the mask is, 1..53.
  • default number (optional) — Numeric default for public.<field>, or nil to leave it unset.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor carrying a bitmask 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

  • default boolean (optional) — Boolean default for public.<field>, or nil to leave it unset.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

enabled = Field.bool(true, Sync)

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

  • default color (optional) — color default for public.<field>.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

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

  • componentType T & string — Component type name as a string literal. Inferred into T.
  • default ComponentRef<T> | nil (optional) — ComponentRef envelope or nil.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc<ComponentRef> descriptor.

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

  • contract C & string — The required dataType contract identity. Inferred into C.
  • default AssetRef<"data"> | string | nil (optional) — AssetRef envelope, identity string, or nil.
  • mode SyncModeSync or NoSync — 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.

Parameters

  • default EntityRef | string | nil (optional) — Live entity proxy, entity-id string, or nil.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

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.
  • default string (optional) — The default member, or nil to leave the field unset.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor carrying an enum 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

  • default AssetRef<any> | string | nil (optional) — AssetRef envelope, identity string, or nil.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc<AssetRef> descriptor.

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

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

  • default number (optional) — Numeric default for public.<field>, or nil to leave it unset.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor consumed by component registration.

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

  • default quat (optional) — quat default for public.<field>.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

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

  • min number (optional) — Lowest accepted value, or nil to leave the low side open.
  • max number (optional) — Highest accepted value, or nil to leave the high side open.
  • default number (optional) — Numeric default for public.<field>, or nil to leave it unset.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor carrying a range 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

  • category C & string — Resource category string literal ("mesh", "texture", "material", ...). Inferred into C.
  • default AssetRef<C> | Handle<C> | string | nil (optional)AssetRef<C> / Handle<C> / identity string / nil.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc<AssetRef | Handle> descriptor.

model = Field.resource("mesh", nil, Sync)

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

  • default string (optional) — String default for public.<field>, or nil to leave it unset.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

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

  • schema FieldSchema — Map of subfield name to a Field constructor descriptor.
  • mode SyncMode — 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

  • default T — Table value to use as the default for public.<field>.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

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

  • tag string — The tag an assigned asset must carry.
  • default AssetRef<any> | string | nil (optional) — AssetRef envelope, identity string, or nil.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc<AssetRef> descriptor.

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

  • default vec2 (optional)
  • mode SyncMode
  • marker (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

  • default vec3 (optional) — vec3 default for public.<field>.
  • mode SyncModeSync or NoSync — required.
  • marker (SerializedMode | string | FieldOptions) (optional)Serialized, a description string, or a { serialized, description } options table; omit for neither.

Returns FieldDesc descriptor.

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

  • dx number — Orbit direction X (subject toward camera, unit length).
  • dy number — Orbit direction Y.
  • dz number — 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

  • box Aabb — The AABB to frame.
  • opts FitOpts (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

  • hx number — Half-extent on X.
  • hy number — Half-extent on Y.
  • hz number — Half-extent on Z.
  • dx number — Orbit direction X (subject toward camera, unit length).
  • dy number — Orbit direction Y.
  • dz number — Orbit direction Z.
  • tanH number — Tangent of the half horizontal FOV.
  • tanV number — 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

  • hx number — Half-extent on the first extent axis.
  • hy number — Half-extent on the second.
  • hz number — Half-extent on the third.
  • axes CameraAxes{ forward, right, up }, in the SAME frame the half-extents are measured in.
  • tanH number — Tangent of the half horizontal FOV.
  • tanV number — 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

  • target any (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

  • language string — Language name ("lua", "json", "yaml", "wgsl", "markdown", or aliases "yml"/"md").
  • text string — 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/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

  • text string — The JSON source to highlight. Coerced via tostring and 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. Walks lines, dispatching the pre-comment portion through the code tokenizer (string/number/keyword/identifier) and the ---introduced comment tail through the comment color.

Parameters

  • text string — The Luau source to highlight. Coerced via tostring and defaults to "" when nil.

Returns { Segment } — An array of { text, color, monospace } segments suitable for the zui code renderer.

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

  • text string — The Markdown source to highlight. Coerced via tostring and 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

  • text string — The WGSL source to highlight. Coerced via tostring and 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

  • text string — The YAML source to highlight. Coerced via tostring and 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/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/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/jobs/JobHandle/id

JobHandle.id -> number

This job's substrate id. Zero once the handle has been cancelled.

typed/builtin//modules/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/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/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/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/jobs/JobHandle/valid

JobHandle.valid(self) -> boolean

Whether the substrate still tracks this job.

Parameters

  • self

Returns boolean

typed/builtin//modules/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

  • name string — Job name supplied to jobs.register.

Returns JobHandle? — JobHandle or nil.

local job = jobs.find("animation_blend_main")

typed/builtin//modules/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

  • target JobHandle | string — JobHandle, or job name string.

Returns JobInfo? — Snapshot row or nil.

local info = jobs.inspect("animation_blend_main")

typed/builtin//modules/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.

typed/builtin//modules/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\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

  • descriptor table — Job declaration with the JobDescriptor shape — name?, phase, reads?, writes?, executor, ordering?, pure?, origin?, metadata?. Param is typed as table rather than JobDescriptor because the LSP doesn't yet narrow string literals to their literal types in record fields, so a JobDescriptor annotation rejects the tagged-union executor discriminator on every call site (literal kind = "kernel" infers as kind: string, doesn't subtype KernelExecutor.kind: "kernel"). Runtime validation in __jobs.register enforces the actual structure; see the JobDescriptor type 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 } } })

typed/builtin//modules/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

  • self JobHandle — JobHandle returned by jobs.register.

Returns boolean — True if the substrate still tracked the job at call time.

job:cancel()

typed/builtin//modules/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

  • self JobHandle — JobHandle returned by jobs.register.

Returns JobInfo? — Snapshot row or nil if the job is no longer tracked.

local info = job:info(); print(info.origin)

typed/builtin//modules/jobs/jobhandle_pause

jobhandle_pause(self: JobHandle) -> boolean

Pause the job — skipped on subsequent ticks but stays registered.

Parameters

  • self JobHandle — JobHandle returned by jobs.register.

Returns boolean — True if the substrate still tracked the job.

local job = jobs.register({...}); job:pause()

typed/builtin//modules/jobs/jobhandle_resume

jobhandle_resume(self: JobHandle) -> boolean

Resume a paused or errored job (clears ErroredPending).

Parameters

  • self JobHandle — JobHandle returned by jobs.register.

Returns boolean — True if the substrate still tracked the job.

job:resume()

typed/builtin//modules/jobs/jobhandle_status

jobhandle_status(self: JobHandle) -> JobStatus?

Read the job's current scheduler status.

Parameters

  • self JobHandle — JobHandle returned by jobs.register.

Returns JobStatus? — "pending" / "running" / "errored", or nil if the id is no longer tracked.

local s = job:status() -- "pending"

typed/builtin//modules/jobs/jobhandle_valid

jobhandle_valid(self: JobHandle) -> boolean

Whether the substrate still tracks this job (false after :cancel/:destroy).

Parameters

  • self JobHandle — JobHandle returned by jobs.register.

Returns boolean — True if the underlying job is still registered.

if not job:valid() then return end

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

  • str string — 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).

Parameters

  • value any (optional) — Any Lua value (nil, boolean, number, string, table).
  • indent string (optional) — Optional indent string. Reserved — the compact encoder ignores it; use encodePretty for indented output.
  • currentIndent string (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.

Parameters

  • value any (optional) — Any Lua value.
  • indentStr string (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

  • jsonStr string (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

  • jsonStr string (optional) — The JSON input. nil, empty, or "null" short-circuit to nil.

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

  • n number — The number to format.
  • decimals number (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

  • jsonStr string (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 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

  • src string — 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

  • src string — 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

  • src string — Source text — both the memo key and the argument passed to computeFn.
  • computeFn (string) -> any — Called as computeFn(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

  • src string — 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

  • src string — 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

  • src string — 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

  • src string — 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/publicFields

M.publicFields(src: string) -> { FieldEntry }

Parses public = { name = Field.<kind>(...), ... } table-literal entries and module-scope public.<name> = Field.<kind>(...) assignments. For a ref constructor (assetRef / dataRef / resource / componentRef) the first positional argument is captured as category and the second as default; for every other kind the first argument is the default. Both are raw trimmed source-text slices, comments stripped. A trailing Sync/NoSync identifier becomes sync. Entries whose key is a computed/indirect expression ([expr] = Field...) are omitted. Comments and string literals never register as entries. Folds in desc from docstrings(src).

Parameters

  • src string — Luau/asset source text.

Returns { FieldEntry } — Ordered array of field entries, source order preserved.

local f = I.publicFields(src); print(f[1].name, f[1].category)

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

  • src string — 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

  • src string — Luau/asset source text.
  • assignmentName string — 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

  • src string — Luau/asset source text.
  • assignmentName string — 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

  • entityId string — Target entity id.
  • materialRef MaterialRefOrName — 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).

typed/builtin//modules/material_utils/M/Exists

M.Exists(name: MaterialRefOrName) -> boolean

PascalCase back-compat alias for exists.

Parameters

  • name MaterialRefOrName — 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

  • materialName MaterialRefOrName — AssetRef envelope or material-name string.
  • propertyName string — 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

  • materialName MaterialRefOrName — 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

  • materialName MaterialRefOrName — AssetRef envelope or material-name string.
  • property string — Property name.
  • value any (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

  • materialName MaterialRefOrName — AssetRef envelope or material-name string.
  • slot string — Texture slot name ("albedo", "normal", etc.).
  • textureRef string — 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

  • target any (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

  • entity any (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.
  • materialRef MaterialRefOrName — Either an AssetRef envelope or a material-name string.

Returns boolean — True on success.

Material.apply(entityId, "gold")
local mat = Material.create("gold", { ... }); 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.

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

  • name MaterialRefOrName — 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

  • materialName MaterialRefOrName — AssetRef envelope or material-name string.
  • propertyName string — 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

  • materialName MaterialRefOrName — 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

  • target any (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

  • target any (optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.
  • property string — Property name.
  • value any (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

  • materialName MaterialRefOrName — AssetRef envelope or material-name string.
  • slot string — Texture slot name ("albedo", "normal", etc.).
  • textureRef any (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

  • target any (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

  • data any (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

  • min number — Lower bound.
  • max number (optional) — Upper bound; defaults to min.

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

  • opts Fft1dOpts{ input, output?, size, inverse? }. input / output are GPU buffer handles; output defaults to input. inverse=true runs the inverse transform (scaled by 1/N).

Returns booleantrue 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

  • opts Fft2dOpts{ input, output?, width, height, inverse? }. input / output are GPU buffer handles; output defaults to input. inverse=true runs the inverse (scaled by 1/(width*height)).

Returns booleantrue 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

  • opts Fft1dOpts — Same as M.fft1d. Mutates opts.inverse.

Returns booleantrue 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

  • opts Fft2dOpts — Same as M.fft2d. Mutates opts.inverse.

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • x NxScalarOrBuffer — Either a scalar or a same-shaped buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer (mutated).
  • scalar number — Per-element addend.
  • stride number — Element stride.
  • offset number (optional) — Optional element offset (default 0).

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • src NxBuffer — Source buffer.
  • scale number (optional) — Optional src scale factor (default 1.0).
  • dst_stride number — Destination element stride.
  • dst_off number (optional) — Optional destination offset (default 0).
  • src_stride number — Source element stride.
  • src_off number (optional) — Optional source offset (default 0).

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer.

Returns booleantrue 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

  • signal NxBuffer — Signal buffer (mutated).
  • window NxBuffer — Window buffer.

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • a number — Scale applied to dst.
  • src NxBuffer — Source buffer.
  • b number — Scale applied to src.

Returns booleantrue 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

  • b NxBuffer — Target buffer (mutated).
  • min_v number — Lower bound.
  • max_v number — Upper bound.

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • src NxBuffer — Source buffer.

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • src NxBuffer — Source buffer.
  • scale number (optional) — Optional src scale factor (default 1.0).
  • dst_stride number — Destination element stride.
  • dst_off number (optional) — Optional destination offset (default 0).
  • src_stride number — Source element stride.
  • src_off number (optional) — Optional source offset (default 0).

Returns booleantrue 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.

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

  • dst NxBuffer — Target buffer (mutated).
  • x NxScalarOrBuffer — Either a scalar or a same-shaped buffer.

Returns booleantrue 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

  • a NxBuffer — First buffer.
  • b NxBuffer — 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

  • re NxBuffer — Real-component buffer (mutated).
  • im NxBuffer — Imaginary-component buffer (mutated).
  • inverse boolean (optional) — When true runs the inverse transform.

Returns booleantrue on success, false on length mismatch / invalid handle.

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

  • re NxBuffer — Real-component buffer (mutated).
  • im NxBuffer — Imaginary-component buffer (mutated).
  • width number — 2D width in samples.
  • height number — 2D height in samples.
  • inverse boolean (optional) — When true runs the inverse transform.

Returns booleantrue on success, false on length mismatch / invalid handle.

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

  • b NxBuffer — Target buffer (mutated).
  • value number (optional) — Fill value (default 0.0).

Returns booleantrue 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

  • b NxBuffer — Target buffer (mutated).
  • mean number (optional) — Optional mean (default 0.0).
  • stddev number (optional) — Optional standard deviation (default 1.0).
  • seed NxSeed — Optional seed — number, "frame", or nil (0).

Returns booleantrue 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

  • b NxBuffer — Target buffer (mutated).
  • min_v number (optional) — Optional lower bound (default 0.0).
  • max_v number (optional) — Optional upper bound (default 1.0).
  • seed NxSeed — Optional seed — number, "frame" (per-frame value), or nil (0).

Returns booleantrue 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

  • b NxBuffer — Target buffer (mutated).
  • value number — Per-element value.
  • stride number — Element stride.
  • offset number (optional) — Optional element offset (default 0).

Returns booleantrue 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

  • n number — Record count.
  • type_ NxType (optional) — Optional element layout (default "f32").
  • value number (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

  • re NxBuffer — Real-component buffer (mutated).
  • im NxBuffer — Imaginary-component buffer (mutated).

Returns booleantrue on success, false on length mismatch / invalid handle.

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

  • re NxBuffer — Real-component buffer (mutated).
  • im NxBuffer — Imaginary-component buffer (mutated).
  • width number — 2D width.
  • height number — 2D height.

Returns booleantrue on success, false on length mismatch / invalid handle.

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

  • pos NxBuffer — Position buffer (mutated).
  • vel NxBuffer — Velocity buffer.
  • dt number — Time step.

Returns booleantrue 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

  • re NxBuffer — Real-component buffer.
  • im NxBuffer — Imaginary-component buffer.

Returns NxBuffer? — Real-valued output buffer on success, nil on failure.

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

  • re NxBuffer — Real-component buffer.
  • im NxBuffer — Imaginary-component buffer.
  • width number — 2D width.
  • height number — 2D height.

Returns NxBuffer? — Real-valued output buffer on success, nil on failure.

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

  • dst NxBuffer — Target buffer (mutated).
  • src NxBuffer — Source buffer.
  • t number — Interpolation factor.

Returns booleantrue 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

  • b NxBuffer — 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

  • dst NxBuffer — Target buffer (mutated).
  • x NxScalarOrBuffer — Scalar or same-shaped buffer.

Returns booleantrue 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

  • b NxBuffer — 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

  • b NxBuffer — 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

  • dst NxBuffer — Target buffer (mutated).
  • x NxScalarOrBuffer — Scalar or same-shaped buffer.

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • x NxScalarOrBuffer — Either a scalar or a same-shaped buffer.

Returns booleantrue 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

  • b NxBuffer — Target buffer (mutated).
  • scalar number — Per-element multiplier.
  • stride number — Element stride.
  • offset number (optional) — Optional element offset (default 0).

Returns booleantrue 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

  • b NxBuffer — 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

  • b NxBuffer — 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

  • b NxBuffer — Vec3 buffer (mutated).

Returns booleantrue 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

  • n number — 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

  • b NxBuffer — Target buffer (mutated).
  • p number — Scalar exponent.

Returns booleantrue 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

  • dst NxBuffer — Quaternion buffer (mutated).
  • yaw NxBuffer — Source yaw scalars buffer.

Returns booleantrue 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

  • signal NxBuffer (optional) — Real-valued input buffer.

Returns (NxBuffer?, NxBuffer?)(re_buf, im_buf) on success, nil otherwise.

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

  • signal NxBuffer — Real-valued input buffer (row-major).
  • width number — 2D width.
  • height number — 2D height.

Returns (NxBuffer?, NxBuffer?)(re_buf, im_buf) on success, nil on failure.

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

  • b NxBuffer — Target buffer (mutated).
  • s number — Scalar multiplier.

Returns booleantrue 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

  • src NxBuffer — Source angles buffer.
  • sin_dst NxBuffer — Destination buffer for the sine values.
  • cos_dst NxBuffer — Destination buffer for the cosine values.

Returns booleantrue 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

  • dst NxBuffer — Target buffer (mutated).
  • x NxScalarOrBuffer — Either a scalar or a same-shaped buffer.

Returns booleantrue 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

  • b NxBuffer — 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

  • args NxWanderArgs — Table with { pos, yaw, rot?, step?, yawDelta?, seed }.

Returns booleantrue 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

  • n number — 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

  • n number — 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

  • n number — 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

  • n number — 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/create

M.create(name: string, entityId: string, componentType: string, opts: PresetCreateOpts?) -> PresetCreateResult

Snapshot an existing component's public property table into a preset asset on disk. Bare names land under /source/presets/<name>.preset/preset.yaml; absolute paths must point to a .preset directory or its inner preset.yaml.

typed/builtin//modules/preset/M/load

M.load(source: AssetRef<preset>, overrides: table?) -> { [string]: any }

Load a preset asset and return the plain component property table.

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

  • e any (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

  • e any (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.
  • angle number — 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

  • v number — The value.
  • lo number — Lower bound.
  • hi number — 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 }; parent is 0-based, -1 for a root.
  • gPos { { number } } — Global positions parallel to bones, as returned by computeGlobals.

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 } }; parent is 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

  • n number — 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.
  • t number — 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.
  • s number — 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/scene_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 array entity.capture hands 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//modules/scene_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

  • folder string — The build's own folder, which the assets it produces are authored inside.
  • sourceDigest string — The digest of the build script running now, as M.digest reports it — what decides whether an asset it produced is still the asset the code states.

Returns BuildSurface — The table bound as the build global for that build.

local surface = SceneBuild.buildSurface(dir, SceneBuild.digest(src))

typed/builtin//modules/scene_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

  • source string — The script body to digest.

Returns string — The digest, as a hex string.

local key = SceneBuild.digest(vfs.read(path))

typed/builtin//modules/scene_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

  • owner string (optional) — Optional build name, as M.ownerOf reports it, to read just that build. Omitted, every build-owned entity in the scene is read.

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//modules/scene_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

  • entityId string — Runtime entity id of the owned entity.
  • values { [string]: any } — The values the entity carries now, by name.
  • componentType string (optional) — The component that states them, so a rebuild knows to state that type again instead of leaving it to the scene.

Returns nil

SceneBuild.notePreview(id, { count = 9 }, "SceneModule")

typed/builtin//modules/scene_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

  • entityId string — 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//modules/scene_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

  • entityId string — Runtime entity id of the owned entity.

Returns string? — The component type name, or nil.

if SceneBuild.previewedComponentType(id) == "SceneModule" then end

typed/builtin//modules/scene_build/M/reconcile

M.reconcile(records: { any }, target: EntityRef | string | { guid: string }, owner: string, opts: { participation: string?, source: string? }?) -> ({ [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. 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 — what M.run returns.
  • target EntityRef | string | { guid: string } — Entity ref, entity id, or scene layer every root record lands in.
  • owner string — Name of the build, unique among the builds sharing this target.
  • opts { participation: string?, source: string? } (optional)participation sets the lifecycle mode every placed entity takes, ahead of the mode any record carries. source names the file the build is written in, which M.sourceOf reports for every entity the build places.

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//modules/scene_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//modules/scene_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

  • owner string — Build name, as M.ownerOf reports it.

Returns string? — Path of the file the build is written in, or nil.

local file = SceneBuild.sourceOf(SceneBuild.ownerOf(id))

typed/builtin//modules/scene_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

  • entityId string — 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//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 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.

typed/builtin//modules/scopes/M/release

M.release(scope: string) -> { Released }

End every resource registered under scope, across every subsystem. Reaches contexts no seam does — the chunk of an execute call that registered something and ended without releasing it.

Parameters

  • scope string — A scope tag, as the scope field of a list() 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/session/Session/get

Session.get(key: string) -> any

The value stored under key this session, or nil.

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.

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.

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

  • key string — Dotted-path key.
  • default boolean (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

  • key string — Dotted-path key.
  • default number (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

  • key string — Dotted-path key.
  • default string (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.

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

  • entityId string — 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. Called by the component hot-reload / teardown path so a reloaded instance's stale connections don't accumulate.

Parameters

  • instanceId string — Component instance whose sourced connections to drop.

Returns number

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/spawnModel/impl/spawnModel

impl.spawnModel(name: string, source: AssetRef<bundle|mesh>) -> string

Spawn an entity with a Model (or registered Asset bundle) plus a Collider at a position, in one call. Static physics by default; opts.physics (e.g. "dynamic") makes it a dynamic body of that kind. opts also accepts scale (number or vector) and rotation / rot (a quaternion when it carries w/[4], else euler degrees). A string identity / guid / path is resolved to an AssetRef, so a primitive name ("cube") or a mesh path passes straight through.

Parameters

  • name string — Entity name for the spawned model.
  • source AssetRef<bundle|mesh> — Model source: a primitive name, a library identity/guid, a mesh path, or an AssetRef (bundle or mesh).

Returns string — The spawned entity id.

"crate", "cube", 0, 1, 0
"rock", meshRef, { 2, 0, 2 }, { physics = "dynamic" }

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

  • entity EntityNamespace — The target entity namespace. Must already carry batchWrite, batchWriteBound, batchWriteFromBuffer, batchRead, and batchReadToBuffer as functions.
require("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

  • newText string (optional) — The full added content.
  • opts DiffOpts (optional) — Same shape as M.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

  • oldText string (optional) — The previous text content. nil is treated as "".
  • newText string (optional) — The new text content. nil is treated as "".
  • opts DiffOpts (optional) — Optional. context overrides the default 3-line context window; path and action are 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

  • oldText string (optional) — The full removed content.
  • opts DiffOpts (optional) — Same shape as M.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

  • fileDiff FileDiff — The structured FileDiff table.

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

  • fileDiff FileDiff — The structured FileDiff table. 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]). Sequence tables are emitted as inline arrays, and string-keyed tables in value position (e.g. array elements) 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). Throws with the line number on syntax errors.

Parameters

  • src string — 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/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 use_tool MCP tool's named args, so a name resolves the same way whichever surface the caller reached for. Reads the schema from tools.get, matches each name to a parameter (exactly first, then case-insensitively), and reports the first unresolvable name, a name that a positional argument already filled, a call longer than the signature, and a required parameter skipped over while a later one is filled. Resolves the call only — running it is tools.use.

Parameters

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

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

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

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.

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's shared.module/init.luau. The toolbox template ALWAYS ships a shared.module/ with default ok/fail result-envelope constructors so tools in this toolbox can require(".shared") from day one. Supply sharedCode only when you want to override that default with custom cross-tool helpers (parsers, registries, …); the module body is left untouched if you omit it. path — explicit VFS destination (/zero/source/... form). Defaults to /zero/source/tools/<name> if omitted; pass an explicit path to author a library-scoped or package-internal toolbox.

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

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

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

  • identity string — 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.

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.

M.search(query: string?, opts: { toolbox: string?, limit: number? }?) -> { stdout: string, value: any }

Search registered code-mode tools by relevance, 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. Enumerates every tool across every toolbox (reusing the same filesystem discovery tools.list uses), then scores each against the query: a token hit in the tool NAME weighs most, then its DESCRIPTION, then its TAGS. Tools scoring above zero are returned best-first. With an empty/omitted query and no toolbox filter, returns the whole catalogue (name + signature

  • toolbox) so the agent can browse. Each returned name is the tool's <toolbox>.<tool> identity; invoke it with the use_tool MCP tool (toolbox + tool + args, an array in signature order or an object naming the parameters), the form each entry's examples are rendered in.

Parameters

  • query string (optional) — Free-text search string. Tokenized lowercase on non-alphanumeric boundaries; each token is matched against tool name, description, and tags. Empty or nil with no toolbox filter lists every tool.
  • opts { toolbox: string?, limit: number? } (optional) — Optional filters table. toolbox — keep only tools whose owning toolbox exactly matches. limit — maximum entries to return (default 10; pass a larger value to return more, up to every match — there is no upper cap).

Returns { stdout: string, value: any } — Table shaped { stdout: string, value: { SearchEntry } }. value is the ranked array; each entry is { name = "<toolbox>.<tool>", signature?, description, toolbox?, tags?, examples? }. stdout is a one-line human summary of the match count.

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

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/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 fields are the ones the use_tool MCP surface reports, so a script comparing several calls in one pass reads the same names it would over MCP, and reads them without wrapping each call in pcall.

Parameters

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

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

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 Luau-code counterpart to the use_tool MCP tool. The normal path is the use_tool MCP tool; this is the escape hatch for editor panels and shipped modules that script a tool from engine Luau. Resolves the toolbox from the runtime store (builtin, library, and user-authored runtime toolboxes all work), calls the named tool with the remaining args, and returns the tool's value directly — unwrapping the ZmToolResult envelope and RAISING a Luau error when the tool fails. Because every call names both toolbox and tool, it can never read as a tools.<box> namespace. tools.tryUse takes the same arguments and reports the outcome as a value instead of raising.

Parameters

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

Returns ...any — Everything the tool returned, in the order it returned it — a tool declaring (path, reason, frame) hands back all three, so a second and third value the tool states are read the way the tool's own signature says. Raises on an unknown toolbox / tool or a tool-reported failure. These are the tool's OWN values, not the { ok, value } envelope the use_tool MCP surface reports — reach for tools.tryUse when the call's outcome is what you want.

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

  • fromX number — From x.
  • fromY number — From y.
  • fromZ number — From z.
  • toX number — To x.
  • toY number — To y.
  • toZ number — 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

  • entityA string | EntityRef — Source entity (id string or proxy).
  • entityB string | 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

  • x1 number — First point x.
  • y1 number — First point y.
  • z1 number — First point z.
  • x2 number — Second point x.
  • y2 number — Second point y.
  • z2 number — Second point z.

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

  • entityA string | EntityRef — First entity (id string or proxy).
  • entityB string | 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

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — 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

  • yaw number — Y-axis rotation in radians.
  • pitch number (optional) — X-axis rotation in radians. Defaults to 0.
  • roll number (optional) — Z-axis rotation in radians. Defaults to 0.

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

  • ax number — Start x.
  • ay number — Start y.
  • az number — Start z.
  • bx number — End x.
  • by number — End y.
  • bz number — End z.
  • t number — Interpolation factor [0, 1].

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

  • a number — Start value.
  • b number — End value.
  • t number — 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

  • a number — Start angle in radians.
  • b number — End angle in radians.
  • t number — 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

  • px number — Parent position x.
  • py number — Parent position y.
  • pz number — Parent position z.
  • pqx number — Parent rotation x.
  • pqy number — Parent rotation y.
  • pqz number — Parent rotation z.
  • pqw number — Parent rotation w.
  • lx number — Local x.
  • ly number — Local y.
  • lz number — Local z.

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

  • entityOrId string | EntityRef — Entity id, name, or proxy for the entity to rotate.
  • txOrTarget any (optional) — A number (world x), a point table, or an entity id / name / proxy whose world position is resolved as the look-at target.
  • ty any (optional) — World y of the target. Omitted when txOrTarget is a point or an entity.
  • tz number (optional) — World z of the target. Omitted when txOrTarget is a point or an entity.
  • up any (optional) — Optional world up hint deciding the roll — { x, y, z }, { x =, y =, z = } or a vector. World +Y when omitted. It never bends the aim; it only says which way is up around it. When the target slot is an entity or a point this is the third argument, and when it is coordinates the fifth.

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

  • fx number — Origin x.
  • fy number — Origin y.
  • fz number — Origin z.
  • tx number — Target x.
  • ty number — Target y.
  • tz number — 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

  • fx number — Eye x — where the entity stands.
  • fy number — Eye y.
  • fz number — Eye z.
  • tx number — Target x — the world point it faces.
  • ty number — Target y.
  • tz number — Target z.
  • ux number (optional) — Up hint x. World +Y when the hint is omitted.
  • uy number (optional) — Up hint y.
  • uz number (optional) — Up hint z.

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

  • a number — 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

  • centerX number — Center x.
  • centerY number — Center y.
  • centerZ number — Center z.
  • radius number — Horizontal distance from the center.
  • height number — Vertical offset from centerY.
  • angle number — Orbital angle in radians.

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

  • ax number — Axis x.
  • ay number — Axis y.
  • az number — Axis z.
  • angle number — 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

  • rx number — Right axis x.
  • ry number — Right axis y.
  • rz number — Right axis z.
  • ux number — Up axis x.
  • uy number — Up axis y.
  • uz number — Up axis z.
  • fx number — Forward axis x.
  • fy number — Forward axis y.
  • fz number — Forward axis z.

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

  • yaw number — 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

  • yaw number — Y-axis rotation in radians.
  • pitch number — 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

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — 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

  • ax number — Left quat x.
  • ay number — Left quat y.
  • az number — Left quat z.
  • aw number — Left quat w.
  • bx number — Right quat x.
  • by number — Right quat y.
  • bz number — Right quat z.
  • bw number — Right quat w.

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

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
  • vx number — Vector x.
  • vy number — Vector y.
  • vz number — Vector z.

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

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — 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

  • value Vec3Input — The vector to normalize.
  • label string (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

  • ax number — Start quaternion x.
  • ay number — Start quaternion y.
  • az number — Start quaternion z.
  • aw number — Start quaternion w.
  • bx number — End quaternion x.
  • by number — End quaternion y.
  • bz number — End quaternion z.
  • bw number — End quaternion w.
  • t number — Interpolation factor [0, 1].

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 }.
  • step number | 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

  • rotation any (optional) — The rotation to normalize, in any form of the RotationInput union.
  • label string (optional) — Name reported in the error when the value is not a rotation. Defaults to "Transform".

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

  • rotation any (optional) — The value to read as a rotation.
  • label string (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

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.

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

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.

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

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.

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

  • x number — Vector x.
  • y number — Vector y.
  • z number — 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

  • x number — Vector x.
  • y number — Vector y.
  • z number — 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

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
  • s number — 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

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.

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

  • px number — Parent position x.
  • py number — Parent position y.
  • pz number — Parent position z.
  • pqx number — Parent rotation x.
  • pqy number — Parent rotation y.
  • pqz number — Parent rotation z.
  • pqw number — Parent rotation w.
  • wx number — World x.
  • wy number — World y.
  • wz number — World z.

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.

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

  • easing string | EasingFn — Either an easing name (case-insensitive, optional ease prefix), or an EasingFn (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/flamegraph/M/build

M.build(opts: BuildOpts) -> any

Build a flamegraph canvas widget from a folded-stack snapshot. The snapshot shape is whatever luau_profile.snapshot() returns ({ stacks = { { stack, self_us }, ... } }). Returns a single canvas widget node ready to drop into any parent layout.

Parameters

  • opts BuildOpts — Build options — see BuildOpts. width/height default to 720x280; rowHeight defaults to 18px; search dims non-matching frames; onClick/onDoubleClick are forwarded to the canvas event protocol; background is the canvas fill.

Returns any — The widget node.

local view = Flame.build({ snapshot = luau_profile.snapshot() })
local view = Flame.build({ snapshot = snap, search = "physics", onClick = "flame-click" })

typed/builtin//modules/ui/flamegraph/M/formatFrame

M.formatFrame(label: string) -> string

Expose the internal frame label extractor — useful for the region/top-stacks tables in the same tab so they format frames identically to the flamegraph.

Parameters

  • label string — Raw frame label as emitted by the sampler.

Returns string — Display-friendly string.

print(Flame.formatFrame("path.luau,foo,42"))  -- "foo:42"

typed/builtin//modules/ui/flamegraph/M/splitFrames

M.splitFrames(stack: string) -> { string }

Walk a folded stack into its component frames. Convenience for tabs that want to render the top frame separately from the full chain.

Parameters

  • stack string — Folded-stack string from the sampler.

Returns { string } — Array of frame strings, root first.

local frames = Flame.splitFrames("a;b;c")

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("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 prelude calls this once at boot with the engine's vfs global; users shouldn't call it directly. No-op if the target doesn't expose both read and readAsync as functions.

Parameters

  • vfs VfsNamespace — The target namespace table. Must carry read and readAsync function fields (the engine's vfs global does); otherwise the install is a no-op.
require("modules.vfs_async_read").installInto(vfs)

typed/builtin//modules/viz/viz/clear

viz.clear(vizId: string, channel: string?)

Clear a specific visualization by ID (and optional channel).

Parameters

  • vizId string — The visualization identifier.
  • channel string (optional) — Optional channel. Empty string means "any".
viz.clear("flash:box")

typed/builtin//modules/viz/viz/clearAll

viz.clearAll()

Clear every active visualization.

viz.clearAll()

typed/builtin//modules/viz/viz/create

viz.create(name: string, duration: number, updateSource: string?) -> VizContext

Create a managed visualization context. Returns a context table with :spawn(name), :track(id), and :done(). Entities created via :spawn() are placed in the __viz scene layer and auto-despawned when the viz expires; existing entities can be marked for auto-despawn via :track.

typed/builtin//modules/viz/viz/flashOutline

viz.flashOutline(entityId: string, duration: number?, color: Color3?)

Flash an outline on an entity (no scale change). Good for modification feedback.

Parameters

  • entityId string — The target entity.
  • duration number (optional) — Flash length in seconds. Defaults to 0.5.
  • color Color3 (optional) — Outline RGB. Defaults to {1, 0.8, 0} (gold).
viz.flashOutline("box")
viz.flashOutline("box", 0.3, { 1, 0.2, 0.2 })

typed/builtin//modules/viz/viz/flashTint

viz.flashTint(entityId: string, duration: number?, color: Color3?)

Brief colour tint that fades. Good for property-change feedback.

Parameters

  • entityId string — The target entity.
  • duration number (optional) — Tint length in seconds. Defaults to 0.3.
  • color Color3 (optional) — Tint RGB. Defaults to {0.2, 1, 0.4} (green).
viz.flashTint("box")
viz.flashTint("box", 0.2, { 1, 0.3, 0.3 })

typed/builtin//modules/viz/viz/label

viz.label(entityId: string, text: string, duration: number?)

Spawn a 3D text label above an entity that fades after duration. Currently a no-op pending the Luau WASM -fwasm-exceptions rebuild (see #1230) — viz.label is purely cosmetic and was breaking the WASM logic tick when its spawn-chain ran before vtables were published. Keeps the surface stable so tool scripts can still call it; behaviour returns when the engine binding is unblocked.

Parameters

  • entityId string — The entity to label.
  • text string — The label text.
  • duration number (optional) — How long to show in seconds. Defaults to 1.5.
viz.label("box", "selected")

typed/builtin//modules/viz/viz/lightRadius

viz.lightRadius(entityId: string, radius: number, duration: number?, color: Color3?)

Light radius indicator — brief sphere outline showing a light's range. Flashes the light entity itself.

Parameters

  • entityId string — The light entity.
  • radius number — Light radius (currently unused — accepted for future expansion).
  • duration number (optional) — How long to show in seconds. Defaults to 0.8.
  • color Color3 (optional) — Indicator RGB. Defaults to {1, 0.9, 0.4} (warm yellow).
viz.lightRadius("lamp", 8)

typed/builtin//modules/viz/viz/popIn

viz.popIn(entityId: string, duration: number?, color: Color3?)

Pop-in animation: entity scales from 0 to 1 with a coloured outline pulse. Call this right after spawning an entity to give it a smooth entrance. Uses VizTransform overlay — never touches the entity's real Transform.

Parameters

  • entityId string — The entity to animate.
  • duration number (optional) — Animation length in seconds. Defaults to 0.4.
  • color Color3 (optional) — Outline RGB (3-element array). Defaults to {0.3, 0.6, 1.0} (blue).
viz.popIn("my_entity")
viz.popIn("my_entity", 0.6, { 1, 0.8, 0 })

typed/builtin//modules/viz/viz/shrinkOut

viz.shrinkOut(entityId: string, duration: number?)

Shrink-out animation: entity scales from 1 to 0 with a red tint. Uses VizTransform overlay — never touches the entity's real Transform. Call this BEFORE despawning to give a smooth exit; the actual entity.despawn should be issued by the caller after this duration.

Parameters

  • entityId string — The target entity.
  • duration number (optional) — Animation length in seconds. Defaults to 0.3.
viz.shrinkOut("box"); task.wait(0.3); entity.despawn("box")

typed/builtin//modules/viz/viz/smooth

viz.smooth(entityId: string, duration: number?)

Smooth visual transition for any transform change (position, rotation, scale). Pure visual — never touches the entity's real Transform. Call this after making any transform change; it automatically reads PreviousTransform (auto-maintained by the engine) to compute the delta and animate the visual offset back to identity.

Parameters

  • entityId string — The entity to smooth.
  • duration number (optional) — Transition length in seconds. Defaults to 0.25.
entity.find("box").localPosition = { 0, 5, 0 }; viz.smooth("box")

typed/builtin//modules/viz/viz/spin

viz.spin(entityId: string, opts: SpinOpts?)

Spin animation: entity rotates around an axis over duration then stops. Uses VizTransform overlay — never touches the entity's real Transform.

Parameters

  • entityId string — The entity to spin.
  • opts SpinOpts (optional) — Optional spin parameters: axis ("x", "y", "z" — default "y"), turns (default 1), duration (default 0.6), color (outline RGB, default {0.8, 0.6, 1.0}).
viz.spin("box")
viz.spin("box", { axis = "x", turns = 2, duration = 1.0 })

typed/builtin//modules/viz/viz/trigger

viz.trigger(nameOrOpts: string | TriggerOpts, source: string?, duration: number?, updateSource: string?)

Trigger a raw visualization. Accepts either a name + positional args, or an opts table.

Parameters

  • nameOrOpts string | TriggerOpts — Either a visualization name (string) or an options table ({ name, source, duration, update }).
  • source string (optional) — Optional Luau source evaluated once at start (ignored when nameOrOpts is a table).
  • duration number (optional) — Optional duration in seconds.
  • updateSource string (optional) — Optional per-frame update callback source.
viz.trigger("flash:box", "", 0.4)
viz.trigger({ name = "ring", duration = 0.5, update = src })

typed/builtin//modules/world_defaults/M/offLoaded

M.offLoaded(handle: number) -> boolean

Stop a callback registered with world.onLoaded from running.

Parameters

  • handle number — The handle world.onLoaded returned.

Returns booleantrue 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

  • handle number — The handle world.onSaved returned.

Returns booleantrue 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

  • handle number — The handle world.onUnloaded returned.

Returns booleantrue 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_sync/M/installInto

M.installInto(world: WorldNamespace)

Graft syncStatus / resolveConflict onto the supplied world namespace. The prelude calls this once at boot.

Parameters

  • world WorldNamespace — The target namespace — typically the engine's world global.
require("modules.world_sync").installInto(world)

typed/builtin//modules/world_sync/world/resolveConflict

world.resolveConflict(path: string, mode: string, content: string?) -> ResolveResult

Resolve one conflicted /source record, one path at a time. 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. Errors when the record is absent, a merge has no common ancestor or hits binary content, or a write fails.

Parameters

  • path string — Canonical /source path of the conflicted record.
  • mode string — One of merge | apply | take-local | take-backend | discard.
  • content string (optional) — Finalized bytes for apply mode.

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, progress, pending_writes, unsaved_writes, uploads_abandoned, conflicts, binding }. conflicts maps each conflicted /source path to { base_sha, local_sha, backend_sha, isBinary }. unsaved_writes lists the /source paths this session wrote that the server does not hold, 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. binding names which world holds /sourcebound, unbound, binding, session_only, or unclassified before the boot has decided — and, when an authorization attempt is on record, which attempt is running and what the last one answered. 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.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/__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 stages held'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/__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

  • text string — The file content to scan.

Returns booleantrue when a marker line is present.

worldVcs.__hasConflictMarkers("<<<<<<< ours\nx\n") --> true

typed/builtin//modules/world_vcs/M/__installVerdict

M.__installVerdict(ok: boolean, payload: string) -> 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

  • ok boolean — Whether the closure procedure returned successfully.
  • payload string — The response body when ok, the error message otherwise.

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/__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

  • base string — The origin checksum recorded the last time this row was pulled or advanced.
  • ours string — This world's current local checksum for the row.
  • theirs string — The latest upstream checksum.

Returns string — One of "fast_forward" (untouched locally — take theirs), "noop" (upstream unchanged — nothing to do), "converged" (both sides already match — advance provenance only), or "merge" (all three differ — three-way merge required).

worldVcs.__reconcileDecision("A", "A", "B") --> "fast_forward"

typed/builtin//modules/world_vcs/M/__releaseClaimedByOthers

M.__releaseClaimedByOthers(claims: { any }, staged: { string }, unstage: (string) -> (), held: { [string]: boolean }?) -> { string }

Release the paths a bulk stage swept out of another caller's hands. The working tree is one per (world, branch) and staging areas are not, so the dirty set a bulk stage reads spans every caller authoring in the world. A path another area holds is that caller's claim on it: selected for a commit of its own and not yet committed. This unstages each such path from the area the bulk stage filled and answers with the ones it let go, so the caller learns what its stage does not carry rather than discovering it in someone else's file. A path the calling area held before the sweep is that caller's own, whoever else holds it: naming a path is how a claim is handed over, so the sweep leaves a path this caller already took where the caller put it.

Parameters

  • claims { any } — The rows other staging areas hold, { path, stage_name, ... }.
  • staged { string } — The paths the bulk stage put into the calling area.
  • unstage (string) -> () — Removes one path from the calling area.
  • held { [string]: boolean } (optional) — The paths the calling area held before the sweep, as a set.

Returns { string } — The released paths, in the order staged listed them.

worldVcs.__releaseClaimedByOthers(claims, staged, unstage, held)

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

  • dir string — 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/__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

  • msg string — 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 shared default area. Two callers that name different areas stage into different rows, so each commits the paths it staged and leaves the other's staged.

Parameters

  • opts any (optional) — The options table the caller passed, or nil.
  • verb string — 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/__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

  • msg string — 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/installInto

M.installInto(world: WorldNamespace)

Parameters

  • world WorldNamespace

typed/builtin//modules/world_vcs/world/add

world.add(path: string, opts: AddOpts?)

Stage one path's manifest row, expanding to the full asset family if the path lives inside a composite asset. Idempotent at (stage, manifest_row). 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 one of the caller's own staging areas instead of the shared default one, so a commit naming that area freezes these paths and leaves every other caller's staged.

Parameters

  • path string — The path to stage. Must be a non-empty string.
  • opts AddOpts (optional) — Optional { force: boolean?, stage: string? }. Defaults to { force = false } on the default staging area.
world.add("/source/foo.luau")
world.add("/source/scene_dirty/entities/42.json", { force = true })
world.add("/source/fauna.module", { stage = "fauna" })

typed/builtin//modules/world_vcs/world/add_all

world.add_all(opts: StageOpts?) -> { string }

Stage the dirty manifest rows this caller can claim, skipping any path that matches .zmignore / .gitignore. Paths that match an ignore pattern are silently 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. A path another staging area holds is held back as well: the working tree is one per branch and staging areas are not, so a path some other caller has already selected for a commit of its own belongs to that caller until it commits or hands it over. world.add(path) names a path deliberately and takes it either way, which is how a claim is handed over — and a path this area already holds stays staged here, whoever else holds it too.

Parameters

  • opts StageOpts (optional) — Optional { stage: string? } naming the staging area to stage into. Omitted, the call stages into the shared default area.

Returns { string } — The paths that were held back — those another staging area holds, then the conflicted ones. Empty when none were.

world.add_all()
world.add_all({ stage = "fauna" })

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

  • token string — 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

  • opts AssetInstallableOpts{ guid } names the asset; ref pins a commit-id instead of the latest.

Returns AssetInstallableReport — An AssetInstallableReportinstallable, 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/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 }

Discover upstream changes for every asset this world has pulled. Read-only — makes no local mutation and no VCS write. Each locally-pulled row identifies its own origin asset via origin_asset_guid (recorded as that entry's own asset guid at pull time — see world.installAsset). For every distinct origin asset among the pulled rows, this re-resolves that asset's latest transitive closure and compares each returned entry's checksum against the matching local row's recorded origin_checksum.

Returns { UpdateReport } — An array of UpdateReport, one per re-resolved origin asset whose closure produced at least one changed entry. Empty when every pulled row is already current. A root whose closure can't be re-resolved (e.g. the origin world is unreachable) is silently skipped rather than aborting the whole scan.

local reports = 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. Uncommitted work is not at risk: it already has its row on the branch it was written against and is in the tree again when you check that branch out. 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

  • branch string — The branch to switch to.

Returns string — The branch now checked out.

world.checkout("feature")

typed/builtin//modules/world_vcs/world/commit

world.commit(message: string, opts: CommitOpts?) -> string

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.

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

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.

Parameters

  • message string — The commit message.
  • opts CommitOpts (optional) — Optional { stage: string? } naming the staging area to materialise. Omitted, the commit materialises the shared default area.

Returns string — The newly-allocated commit id (ULID string).

local id = world.commit("feat: ship widget")
local id = world.commit("fauna: the swallow colony", { stage = "fauna" })

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 }?) -> { { asset: string, typeName: string, detail: string } }

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). The same walk world.push gates on: push refuses while this list is non-empty, and world.publishBlockers reports it as the content class beside the other two. Empty list = every checked asset meets its type's contract.

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 { { asset: string, typeName: string, detail: string } } — Array of { asset, typeName, detail } requirement rows.

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

  • opts ContributeOpts (optional) — Optional ContributeOptstargets (origin world guids; default all 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/createBranch

world.createBranch(name: string, fromCommit: string?)

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

Parameters

  • name string — The new branch name.
  • fromCommit string (optional) — Commit id to start at. Defaults to world.head().
world.createBranch("feature")

typed/builtin//modules/world_vcs/world/deleteBranch

world.deleteBranch(branch: string)

Delete a branch — git branch -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. Uncommitted work on that branch goes with it and is NOT recoverable from trash, so the call refuses the first time and returns the affirmation needed to go through with it — affirm with world.affirm(<token>). Refuses the branch this session is on (check out another first) and the world's last branch.

Parameters

  • branch string — The branch to delete.
world.deleteBranch("feature")

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 one of the caller's own staging areas in place of the shared default one.

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/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

  • opts StageOpts (optional) — Optional { stage: string? } naming the staging area to drop. Omitted, the call drops the shared default area.
world.discard()
world.discard({ stage = "fauna" })

typed/builtin//modules/world_vcs/world/discardFile

world.discardFile(path: string)

Discard one file's unstaged working edits, taking its content back to what it was staged or committed as — the git restore <path> shape. 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: a path that reverts to a committed version snapshots the discarded bytes to trash first, so that case is recoverable via world.restore(<handle>). Errors when the path is not dirty (nothing to discard).

Parameters

  • path string — The VFS path whose unstaged edits to discard.
world.discardFile("/source/foo.luau")

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

  • branch string (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? }source is the source world GUID; branches default to "main"; maxBatches caps 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. Reports what it wrote so a caller can tell a fresh install from a no-op.

Parameters

  • opts InstallAssetOpts{ guid } names the root asset to install; ref pins a specific commit-id instead of the latest.

Returns InstallAssetResult{ assets_written, blobs_downloaded, root_guid, root_path, root_version, deps }.

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 — it's fetched from the source world on demand by the engine's library resolver.

Parameters

  • opts InstallLibraryOpts — See InstallLibraryOpts. opts.world is the upstream world's guid (required). opts.commit is the upstream commit_id to pin (optional; resolves opts.ref or main if omitted). opts.as is the local library name (defaults to the upstream world's slug). opts.ref is 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/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.

typed/builtin//modules/world_vcs/world/log

world.log(opts: LogOpts?) -> { CommitRow }

Return the commit log for the current branch, newest first. Pass opts.path to get the per-path history (git log -- <path>): only the commits that touched that file, newest-first.

Parameters

  • opts LogOpts (optional) — Optional. opts.limit caps the number of commits (default 50, 0 = all). opts.path scopes the log to one file.

Returns { CommitRow } — Array of CommitRow tables.

local commits = world.log({ limit = 20 })
local touched = world.log({ path = "/source/foo.luau" })

typed/builtin//modules/world_vcs/world/merge

world.merge(sourceBranch: string) -> 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. Requires a clean working tree (commit or stash first — that is also what makes abort exact). Clean → a two-parent merge commit lands on the session branch and the merged content appears in the working tree. 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 / rewrite / remove the file), then world.add + world.commit — that commit records the merge (second parent = the source head) and clears the unmerged set.

Parameters

  • sourceBranch string — The branch to merge in.

Returns MergeResult — A MergeResultstatus is clean (with commit), conflicts (with conflicts), or up_to_date.

local r = world.merge("feature")

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

  • worldGuid string (optional) — The world the pull request lives in. Defaults to the bound world.
  • number number — 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

  • worldGuid string (optional)
  • number number (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

  • worldGuid string — The world the pull request lives in.
  • number number — The pull request number.
  • strategy string (optional)merge (default), squash, or fast_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

  • opts PrOpenOptstitle (required), plus description, sourceWorld, sourceBranch, targetWorld, targetBranch to 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

  • worldGuid string (optional) — The world the pull request lives in (its target world). Defaults to the session world.
  • number number — 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

  • opts InstallAssetOpts{ guid, at?, ref? } — same shape as installAsset.

Returns PreviewResult — nodes + totals + a truncated flag.

world.previewInstall({ guid = "..." })

typed/builtin//modules/world_vcs/world/publishBlockers

world.publishBlockers() -> { PublishBlockerClass }

List every reason world.push would refuse to publish this world, as one entry per blocker class: script errors in user content, assets that don't meet their type's content requirements, and asset references that can't be statically pinned. 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. Empty list = the world publishes. zm status prints this list.

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?) -> PullResult

Fetch and reconcile with origin — git pull. Strictly behind → fast-forward (the branch head moves to the origin mirror, no merge commit). Diverged → three-way merge of the origin mirror, with the same conflict/marker/resolve flow as world.merge (resolve the unmerged paths, then world.add + world.commit; abortable with world.mergeAbort). Requires a clean working tree.

Parameters

  • branch string (optional) — Remote branch to pull. Defaults to the session branch.

Returns PullResult — A PullResult table.

local r = world.pull()

typed/builtin//modules/world_vcs/world/pullAsset

world.pullAsset(opts: PullAssetOpts?) -> PullAssetResult

Pull upstream changes into a previously-installed asset, three-way reconciling each entry against local edits. Re-resolves the root's transitive closure at opts.ref (default latest), then for every entry decides fast_forward / noop / converged / merge from (row.origin_checksum, localChecksum, entry.checksum) (M.__reconcileDecision):

  • noop — upstream hasn't moved; skipped.
  • fast_forward / converged — the entry's latest text is written to the local path and the row's origin pointer advances. Binary and composite entries can't be content-synced through this call's only cross-world read primitive (text only), so a non-text entry with a real upstream change is surfaced as a conflict instead of silently going stale.
  • merge (text entries) — a three-way vcs.merge3 runs over (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 for world.resolvePullConflict).
  • merge (binary / composite entries) — no text merge is possible; flagged conflicted with the structured base/theirs checksums (no marker write).

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 clean (pruned), or flagged conflicted when it carries local edits.

Parameters

  • opts PullAssetOpts (optional) — See PullAssetOpts. opts.guid or opts.path is required; opts.ref pins the re-resolve to a specific upstream commit (defaults to latest finalized).

Returns PullAssetResult summarising what merged, conflicted, was pruned, and was newly added.

world.pullAsset({ guid = "..." })

typed/builtin//modules/world_vcs/world/push

world.push(commitId: string?) -> (string?, 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

  • commitId string (optional) — Optional. A single commit to push verbatim. Omit for the squash push of the whole unpushed stack (the normal path).

Returns (string?, 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()
world.push("01HABC...")

typed/builtin//modules/world_vcs/world/reset

world.reset(targetCommitId: string) -> string?

Rewind HEAD to targetCommitId in one shot. Non-destructive — orphaned commits stay in storage and each becomes a trash entry the user can world.restore (in chain order) to re-attach the branch. Errors when targetCommitId is not an ancestor of HEAD. Returns a summary of what was rewound.

Parameters

  • targetCommitId string — The commit id to rewind to.

Returns string? — A summary message: the target plus the list of commits rewound past.

world.reset("01HABC...")

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

  • path string — The conflicted row's local VFS path.
  • choice string"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

  • handle any (optional) — The trash row id. May be a number or a numeric string.
world.restore(42)

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/stash

world.stash(label: string?)

Save the caller's current pending dirty + staged state on the active (world, branch) into a stash row. label is optional free-form text. Non-destructive — dirty + staged state is preserved on disk.

Parameters

  • label string (optional) — Optional. Free-form text label for the stash.
world.stash("wip widget refactor")

typed/builtin//modules/world_vcs/world/stashDrop

world.stashDrop(handle: any?)

Request an affirmation token to drop a stash. Always errors — successful mint surfaces the token as affirmation required: zm affirm <token>. The agent runs zm affirm <token> to actually drop; restoration via world.restore() reappears the stash under a new row_id.

Parameters

  • handle any (optional) — The stash row id. May be a number or numeric string.
world.stashDrop(7)

typed/builtin//modules/world_vcs/world/stashPop

world.stashPop(handle: any?) -> StashSnapshot

Author-only. Pop the stash row (deletes it server-side) and return the decoded snapshot. The caller is responsible for re-applying the snapshot to disk via the normal write paths (so ACL gates fire on every restored path).

Parameters

  • handle any (optional) — The stash row id. May be a number or numeric string.

Returns StashSnapshot containing dirty + staged entries.

local snap = world.stashPop(7)

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/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

  • name string — 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/unstage

world.unstage(path: string, opts: StageOpts?)

Remove a path from the staging area. Live manifest dirty state is untouched.

Parameters

  • path string — The path to unstage.
  • opts StageOpts (optional) — Optional { stage: string? } naming the staging area the path was staged into. Omitted, the call acts on the shared default area.
world.unstage("/source/foo.luau")
world.unstage("/source/foo.luau", { stage = "fauna" })

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. 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 of the most recent writer; dirty_since_micros is the microsecond timestamp of the first write of the current dirty run. local_identity is this session's own writer identity in that same namespace — compare the two to tell your own writes from another account's. claimed_by_other_stages names the paths some other staging area holds and which area holds each — the grain that tells two callers apart when they share one writer identity, and the set world.add_all holds back.

Parameters

  • opts StageOpts (optional) — Optional { stage: string? } naming the staging area whose staged set to report. 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

  • text string — 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/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/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

  • handle number — 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:

  1. clear chord per-tick fire flags, Gestures per-tick deltas, Surface per-frame device-class flags, Virtual button edges + drag deltas
  2. 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.
  3. advance Axes smoothing, Chords window aging, Rebind timeouts, Gestures time-based recognition (long-press) + pinch/pan deltas
  4. drain __zero_input.events() once (first tick of the frame only)
  5. feed each event into State (held-time), Chords (state machines), Input (lastInputType tracking), Gestures (contact tracks + discrete recognition), Surface (device-class flags)
  6. resolve Surface's device class for this frame, then dispatch event subscribers (Zin.events.on / Zin.input.on* callbacks — priority + sink/pass + gpe)
  7. dispatch action handlers (Zin.actions.bind / onPressed / onReleased / onChanged / onHeld)
  8. dispatch chord onFire callbacks and Gestures discrete-gesture callbacks for anything that fired this tick

Parameters

  • dt number (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

  • firstTickThisFrame boolean (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

  • handle number — 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

  • name string — 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.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe) -> "sink" | any.
  • opts BindOpts (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() 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

  • spec ActionSpec — Map of name -> 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

  • handle ActionHandle — The handle from bind (or onPressed/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.

typed/builtin//modules/zinput/actions/M/handlerCount

M.handlerCount(name: string) -> number

Number of registered handlers for an action (0 if none / unknown).

Parameters

  • name string — 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

  • name string — 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

  • name string — The action name.

Returns boolean — Whether the action is held this frame.

if Zin.actions.held("attack") then 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

  • name string — 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

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts (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

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts (optional) — Optional { priority, once }.

Returns ActionHandle? — The numeric handle, or nil when the action isn't defined.

Zin.actions.onHeld("interact", function(_, _, io) 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

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts (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

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts (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

  • name string — 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

  • name string — 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

  • name string — 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

  • name string — 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

  • name string — 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

  • name string — 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

  • opts Steering (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

  • mode string (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 (typically Zin.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 booleantrue 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

  • dt number

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

  • spec AxisSpec — Map of name -> { 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).

typed/builtin//modules/zinput/axes/M/has

M.has(name: string) -> boolean

True if an axis with this name is defined.

Parameters

  • name string — 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

  • name string — 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

  • name string — 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

  • name string — 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

  • name string — 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/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

  • plus Binding — The binding whose travel contributes positively.
  • minus Binding — The binding whose travel contributes negatively.
  • opts StickOpts (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?) -> 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

  • b any (optional) — The binding to evaluate.

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?) -> boolean

Is this binding currently delivering a held signal? key/mouse/modKey/pointerLocked → boolean held. axis/vector/mouseDelta/scroll → magnitude non-zero.

Parameters

  • b any (optional) — The binding to evaluate.

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?) -> 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

  • b any (optional) — The binding to evaluate.

Returns boolean — Whether the press edge fired this frame.

if Zin.bindings.evalPressed(b) then jump() end

typed/builtin//modules/zinput/bindings/M/evalReleased

M.evalReleased(b: any?) -> boolean

Did this binding's release edge fire this frame?

Parameters

  • b any (optional) — The binding to evaluate.

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?) -> 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

  • b any (optional) — The binding to evaluate.

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

  • b any (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

  • bindings any (optional) — The bindings to format.
  • sep string (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 binding descriptors that resolve to a scalar (axis, a mouseDelta / scroll with a non-nil axis field, or touchPinch).

Parameters

  • binding any (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

  • binding any (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 binding descriptors that resolve to a 2-D vector (vector, a mouseDelta / scroll with a nil axis field, touchStick, or touchDrag).

Parameters

  • binding any (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

  • code string — 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

  • spec string — 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

  • button string — 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 returning dx
  • axis = "y" → scalar binding returning dy Values are not normalised to [-1, 1] — mouse delta is a physical pixel count. A control carrying this beside a stick declares as = "delta" with unitsPerSecond on the stick class, so both classes reach the consumer measured in these pixels.

Parameters

  • axis string (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

  • axis string — Canonical name: left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger, right_trigger.
  • opts StickOpts (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

  • button string — 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.
  • slot number (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

  • stick string — "left" or "right".
  • opts StickOpts (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

  • trigger string — "left" or "right".
  • slot number (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 delta
  • axis = "x" → scalar binding returning 0 (no horizontal-scroll surface today; reserved for future hardware support)

Parameters

  • axis string (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

  • binding any (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: { zone: string?, label: string?, icon: string?, priority: number?, size: string?, group: string? }) -> Binding

Virtual-button binding: a boolean fed by the on-screen button identified by opts.label (else opts.zone). label and icon also carry the control's presentation. 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.

Parameters

  • opts { zone: string?, label: string?, icon: string?, priority: number?, size: string?, group: string? } — { zone: string?, label: string?, icon: string?, priority: number?, size: string?, group: string? } — at least one of zone/label.

Returns Binding — The binding descriptor.

B.touchButton({ zone = "right-lower", label = "Jump", priority = 10 })

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

  • opts StickOpts (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

  • right Binding — The binding that drives +x.
  • left Binding — The binding that drives -x.
  • up Binding — The binding that drives +y.
  • down Binding — 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

  • _dt number — 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

  • ev any (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

  • name string — 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

  • spec ChordSpec — Map of name -> { 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

  • name string — The chord name.

Returns boolean — Whether the chord fired this frame.

if Zin.chords.fired("konami") then unlockBonus() end

typed/builtin//modules/zinput/chords/M/get

M.get(name: string) -> ChordDef?

Internal introspection: definition record (or nil).

typed/builtin//modules/zinput/chords/M/has

M.has(name: string) -> boolean

True if a chord with this name is defined.

Parameters

  • name string — 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

  • handle OnFireHandle — The handle returned by onFire.
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

  • name string — The chord name.
  • callback () -> () — Invoked when the chord fires.
  • opts OnFireOpts (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

  • name string — 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

  • name string — 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, or nil to restore the default source.
require("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("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.
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

  • b any (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

  • binding any (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

  • actionA string — The first action name.
  • actionB string — 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

  • binding any (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

  • actionA string — The first action name.
  • actionB string — 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

  • actionA string — The first action name.
  • actionB string — 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

  • name string — The context name to look for.

Returns boolean — Whether name appears anywhere in the stack.

if Zin.context.contains("ui") then pauseGame() 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

  • name any (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

  • name string — The context to push during the call.
  • fn () -> T... — The thunk to run with name on 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

  • opts FpsOpts — 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

  • opts FreeOpts — 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

  • opts OrbitOpts — OrbitOpts table.

Returns OrbitController — An OrbitController object with :start/:stop/:update.

local c = Zin.controllers.orbit({ entity = cam, target = pivot })

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

  • n number — 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

  • events any (optional) — The frame's events as returned by M.frame().
M._dispatch(Zin.events.frame())

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

  • handle SubHandle | 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().

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 showPadPrompts() 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 startCoop() 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

  • slot number (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.

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

  • button string — Canonical button name.
  • slot number (optional) — Pad slot, or nil for the first connected pad.

Returns string — The label to draw.

ui.text("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.

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

  • _dt number
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

  • ev any (optional) — The raw input event record.
Zin.gestures._observeEvent(ev)

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/_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

  • handle number — 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) openContextMenu(ev) 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.

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.

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

  • ev any (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

  • handle number — Candidate handle id.

Returns booleantrue 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 () -> number allocator 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

  • handle Handle — The composite handle returned by an on* call.

Returns booleantrue 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"?.
  • opts SubOpts (optional) — Optional { priority, context, once } forwarded to Zin.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"?.
  • opts SubOpts (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"?.
  • opts SubOpts (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"? where text is the committed string.
  • opts SubOpts (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

  • record any (optional) — The map record, freshly read from its source.

Returns any — The effective map that was applied.

Zin.map._reactivate(loadRecord(self))

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

  • record any (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: { zone: string?, label: string?, icon: string? }, emitKey: string?) -> any

Add a touchButton binding to an action's touch class on the active effective map, then re-flatten and re-activate so it takes effect immediately. Creates the action entry if actionName doesn't exist yet (the overlay's synthetic emit:<code> buttons).

Parameters

  • actionName string — The action to attach the button to.
  • buttonOpts { zone: string?, label: string?, icon: string? }{ zone: string?, label: string?, icon: string? } — the touchButton binding's presentation (see Zin.bindings.touchButton).
  • emitKey string (optional) — Optional key code — when set and the action has no kbm class yet, seeds it with B.key(emitKey) so a synthetic action is self-contained from the moment it's created.

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

  • name string — 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

  • eff any (optional) — An effective map (from materialize/effective).
  • name string — The action or axis name.
  • class string — "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 draw(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

  • record any (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

  • actionName string — The action the binding was added to.
  • binding any (optional) — The binding table returned by addTouchButton.

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

  • on boolean (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

  • name string — 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

  • reason string — 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

  • name string — The control name.

Returns any{ name, reason, means, layer, carriedBy, ... } — the extra fields depend on the reason: suppressedBy, needsContext / currentContext, gateError, declaredClasses / presentClasses, deadzone, subscribers, value, rawReading.

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 booleantrue 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; returns true when 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

  • name string — Profile name to activate.
  • opts ActivateOpts (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

  • name string — 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

  • fallback string (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

  • name string — Profile name to export.
  • vfsPath string — 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.

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

  • vfsPath string — Source VFS path.
  • opts ImportOpts (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.

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.

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

  • handle number — Subscription handle returned by onChange.
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

  • cb ChangeCallback — 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

  • name string — Non-empty profile name.
  • profile any (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

  • name string — Non-empty profile name.
  • opts SaveOpts (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

  • dt number — 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

  • opts BeginOpts (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.

Parameters

  • dt number — Seconds since the previous tick.
Zin.scheme._advance(1 / 60)

typed/builtin//modules/zinput/scheme/M/_childBindings

M._childBindings(mapRef: any?) -> { any }

Parameters

  • mapRef any (optional)

Returns { any }

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

  • record any (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) -> 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

  • record any (optional) — The binding record.
  • dt number — Seconds since the previous tick.

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: the <name>.inputBinding/ children of a map asset, as resolved refs in name order. 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 — Array of inputBinding refs. true when no control could change without new input.

local kids = Zin.scheme._childBindings(mapRef)
if Zin.scheme._settled() then ... end

typed/builtin//modules/zinput/scheme/M/activate

M.activate(mapRef: any?) -> { [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 returns its existing handles rather than registering it twice, so two components sharing one map get one set of controls.

Parameters

  • mapRef any (optional) — The inputMap asset ref.

Returns { [string]: Handle } — A table of handles keyed by binding name.

local map = self.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/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

  • record any (optional) — The binding record.
  • class string — One of kbm / 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?) -> boolean

Release a map. The last holder to release it takes its controls out of the live set and disconnects everything subscribed through it; an earlier one just drops its own claim, so a map two components share survives one of them going away.

Parameters

  • mapRef any (optional) — The inputMap asset ref.

Returns boolean — True when the map was live.

self.inputMap:deactivate()

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

  • record any (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/drivingClass

M.drivingClass(record: any?) -> string?

Which device class is currently satisfying a binding, or nil when nothing is. Used to tag what fired reports.

Parameters

  • record any (optional) — The binding record.

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

  • name string — 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 times and which device classes drove 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.

Parameters

  • peek boolean (optional) — Read without clearing.

Returns { any } — Array of { name, map, count, classes }, sorted by name.

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 rebuild() end

typed/builtin//modules/zinput/scheme/M/has

M.has(name: string) -> boolean

Whether a named control is live in any activated map.

Parameters

  • name string — 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, bindings } where bindings names the controls it contributes, suppressedBy names the map standing this one down, if any, and pointerLock says whether it takes the cursor while the world runs. 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.

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/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

  • kind string — 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/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

  • on boolean (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

  • name string — 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 rebuild() 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/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

  • kind string — The control kind.
  • value any (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/_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

  • ev any (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.

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

Parameters

  • key string — Web-style key code (e.g. "KeyW", "Space").

Returns booleantrue 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

  • key string — Web-style key code (e.g. "KeyW", "Space").

Returns booleantrue 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

  • code string — 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) -> boolean

Was key just pressed this frame?

Parameters

  • key string — Web-style key code.

Returns booleantrue on the press frame only.

if Zin.state.keyPressed("Space") then ... end

typed/builtin//modules/zinput/state/M/keyReleased

M.keyReleased(key: string) -> boolean

Was key just released this frame?

Parameters

  • key string — Web-style key code.

Returns booleantrue 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

  • code string — Web-style key code.
  • opts RepeatOpts (optional) — Optional per-call { delay, period } overrides; falls back to getRepeatDefaults().

Returns booleantrue 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 booleantrue 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 booleantrue 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 booleantrue 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

  • axis string — Canonical name: left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger, right_trigger.
  • slot number (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

  • button string — 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.
  • slot number (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

  • button string — Canonical button name.
  • slot number (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

  • button string — Canonical button name.
  • slot number (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 booleantrue 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

  • opts RepeatOpts (optional) — Partial overrides for delay / period (seconds).
Zin.state.setRepeatDefaults({ delay = 0.5, period = 0.05 })

typed/builtin//modules/zinput/state/M/uiWantsKeyboard

M.uiWantsKeyboard() -> 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.

Returns boolean — Whether the UI layer holds keyboard focus.

if not Zin.state.uiWantsKeyboard() then ... end

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 booleantrue 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

  • ev any (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

  • class string (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

  • handle number — 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/clickMouse

M.clickMouse(opts: ClickOpts?)

Press + release a mouse button. opts.button (default 0); opts.x / opts.y optionally move the cursor before pressing; opts.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

  • opts ClickOpts (optional) — Click options: { button, x, y, duration }.
Zin.test.clickMouse({ x = 100, y = 200 })

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

  • name string (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

  • slot number (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

  • dx number — Total horizontal travel in pixels.
  • dy number — Total vertical travel in pixels.
  • steps number (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 moves 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 exactly what this reports.

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/moveMouse

M.moveMouse(x: number, y: number)

Move the cursor to a screen position. Sets the absolute position; the engine computes mouse_delta as new - old, so successive calls produce meaningful deltas for orbit / look bindings.

Parameters

  • x number — Absolute screen x coordinate.
  • y number — Absolute screen y coordinate.
Zin.test.moveMouse(100, 200)

typed/builtin//modules/zinput/test/M/moveMouseBy

M.moveMouseBy(dx: number, dy: number)

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.

Parameters

  • dx number — Horizontal motion in screen pixels.
  • dy number — Vertical motion in screen pixels.
Zin.test.moveMouseBy(0, 140)

typed/builtin//modules/zinput/test/M/padDown

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

Push a canonical pad button down and leave it held.

Parameters

  • button string — Canonical button name.
  • slot number (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

  • button string — Canonical button name.
  • duration number (optional) — Hold seconds; an instant press when omitted.
  • slot number (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

  • stick string — "left" or "right".
  • x number — Deflection -1..1.
  • y number — Deflection -1..1, screen-down positive.
  • duration number (optional) — Hold seconds; left deflected when omitted.
  • slot number (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

  • trigger string — "left" or "right".
  • value number — Travel 0..1.
  • duration number (optional) — Hold seconds; left squeezed when omitted.
  • slot number (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

  • button string — Canonical button name.
  • slot number (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

  • amount number — Pixels; negative pinches, positive spreads.
  • steps number (optional) — Frames to spread it over; 8 when omitted.
Zin.test.pinch(150)

typed/builtin//modules/zinput/test/M/pressKey

M.pressKey(code: 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

  • code string — Web-style key code (e.g. "KeyM", "Space").
Zin.test.pressKey("KeyM"); Zin.test.releaseKey("KeyM")

typed/builtin//modules/zinput/test/M/pressMouse

M.pressMouse(button: (number | 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).
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

  • x number — Deflection -1..1.
  • y number — 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.
  • duration number (optional) — Hold seconds, then release. Omitted, the stick STAYS deflected — release it with pushStick(0, 0) or releaseStick().

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/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 connected pad. After it, held() reads atRest, the pointer sits over the middle of the scene's viewport and the scene holds the input context (see focusScene).

Returns Released{ 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)

Simulate a key release. Returns inside the frame where keys_released contains code.

Parameters

  • code string — Web-style key code.
Zin.test.releaseKey("KeyM")

typed/builtin//modules/zinput/test/M/releaseMouse

M.releaseMouse(button: (number | 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).
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 so simulated events drain and the next snapshot is published. 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)

Simulate a mouse wheel scroll. Positive dy = up.

Parameters

  • dy number — Signed scroll delta.
Zin.test.scroll(1)

typed/builtin//modules/zinput/test/M/tapKey

M.tapKey(code: string, duration: number?)

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

  • code string — Web-style key code.
  • duration number (optional) — Optional held duration in seconds.
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

  • label string — The button's label, as a player reads it.
  • duration number (optional) — Hold seconds; an instant tap when omitted.
Zin.test.tapTouchButton("Jump")

typed/builtin//modules/zinput/test/M/touchCancel

M.touchCancel(id: number)

Simulate the platform cancelling a touch contact.

Parameters

  • id number — Stable finger id.
Zin.test.touchCancel(1)

typed/builtin//modules/zinput/test/M/touchDown

M.touchDown(x: number, y: number, pressure: number?) -> 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

  • x number — Screen x.
  • y number — Screen y.
  • pressure number (optional) — Contact pressure 0..1; 1 when omitted.

Returns number — The contact id.

local id = Zin.test.touchDown(200, 600)

typed/builtin//modules/zinput/test/M/touchEnd

M.touchEnd(id: number)

Simulate a touch contact lifting.

Parameters

  • id number — Stable finger id.
Zin.test.touchEnd(1)

typed/builtin//modules/zinput/test/M/touchMove

M.touchMove(id: number, x: number, y: number, pressure: number?)

Simulate a touch contact moving.

Parameters

  • id number — Stable finger id.
  • x number — Screen X.
  • y number — Screen Y.
  • pressure number (optional) — Contact pressure 0..1 (defaults to 1).
Zin.test.touchMove(1, 440, 300)

typed/builtin//modules/zinput/test/M/touchStart

M.touchStart(id: number, x: number, y: number, pressure: number?)

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

  • id number — Stable finger id.
  • x number — Screen X.
  • y number — Screen Y.
  • pressure number (optional) — Contact pressure 0..1 (defaults to 1).
Zin.test.touchStart(1, 400, 300)

typed/builtin//modules/zinput/test/M/touchUp

M.touchUp(id: number)

Lift a contact opened by touchDown.

Parameters

  • id number — The contact id.
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

  • pointer boolean — Whether the UI holds pointer focus.
  • keyboard boolean — 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

  • id number — 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

  • handle any (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.
  • opts any (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.
  • opts any (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.
  • opts any (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.
  • opts any (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

  • on boolean — 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

  • w number (optional) — Override width, or nil to clear.
  • h number (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, no pending redraw, mount state matching the surface, and (while mounted) the layout's inputs unchanged. The tick's quiescence gate reads it.

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.

Parameters

  • opts ControlOpts{ 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() castSpell() end })

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 synthetic emit:KeyF action carrying both the kbm key binding and the touchButton, so the emulation floor drives KeyF straight from this button through the same binding/emulation path every other control uses. label/zone/icon set the button's presentation (see Zin.bindings.touchButton); when neither is given the button falls back to the action name (or the emit key).

Parameters

  • opts ButtonOpts{ action?, emit?, label?, icon?, zone? } — exactly one of action/emit is 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, actionName, priority, size, appliedSize, group, 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); 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, or nil when the active map defines none in the current context. 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?, 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, and that projection happens where the contact arrives — before this overlay decides what the contact is for. A finger resting on the Jump button therefore 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 tap on one button 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

  • handle number — The handle returned by M.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

  • handle number — The handle returned by M.addControl.

Returns boolean — Whether a control was actually released.

Zin.touchControls.removeControl(h)

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.
  • x number — 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

  • x number — The reading.
  • deadzone number (optional) — The threshold; nil applies 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

  • v Vector2 — The reading, as { x, y }.
  • deadzone number (optional) — The threshold; nil applies 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

  • name string — 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

  • idx number — 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/matchModifiers

M.matchModifiers(snapshot: any?, mods: Modifiers) -> 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

  • snapshot any (optional) — The frame snapshot table (from Zin.state.get()), or any non-table value (treated as "no modifiers held").
  • mods Modifiers — Subset of { ctrl, shift, alt } booleans to require.

Returns booleantrue 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

  • code string — 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

  • x number — The reading.
  • deadzone number (optional) — Magnitude below which the reading is 0; nil applies none.
  • curve (string | (number) -> number) (optional)"linear" | "quadratic" | "cubic" | a function; nil is linear.
  • invert boolean (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

  • v Vector2 — The reading, as { x, y }.
  • deadzone number (optional) — Magnitude of the pair below which it reads {0, 0}; nil applies none.
  • curve (string | (number) -> number) (optional)"linear" | "quadratic" | "cubic" | a function; nil is linear.
  • invert boolean (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

  • current number — Where the value is now.
  • target number — Where it is heading.
  • dt number — Seconds this step covers.
  • tau number — 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

  • zone string — The drag zone id.
  • dx number — Delta X in px.
  • dy number — 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

  • id string — 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

  • id string — 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

  • id string — 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

  • zone string — 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

  • id string — The button id (a touchButton's id, else its label, else its zone).
  • held boolean — 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

  • zone string — The stick's zone id.
  • x number — Stick X (right positive).
  • y number — 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

  • zone string — The stick's zone id.

Returns (number, number) — x, y components.

local x, y = Zin.virtual.stick("left")

typed/builtin//systems/anim/AnimGraph/AnimGraph/bindSink

AnimGraph.bindSink(body: EntityRef)

Bind a pose sink targeting body's armature, stored on the graph so :tick(dt) applies the evaluated pose to it. The bone order is the graph's layout. Re-binding replaces any prior sink. Raises when the sink cannot be bound (the body has no Skeleton + Model when the sink is created).

Parameters

  • body EntityRef — The EntityRef whose bones the graph drives (carries the Skeleton).
graph:bindSink(skinnedBody)

typed/builtin//systems/anim/AnimGraph/AnimGraph/crossfadeTo

AnimGraph.crossfadeTo(toNode: any?, duration: number, removeFromOnDone: boolean?)

Crossfade from the current output to toNode over duration seconds. Inserts a 2-input Mixer over the previous output and the new node and ramps weights from (1, 0) to (0, 1); on completion the output collapses to toNode. Falls back to an instant swap when there is no active output or duration <= 0 (the old output is freed unless removeFromOnDone is false).

Parameters

  • toNode any (optional) — Target node (already constructed).
  • duration number — Fade time in seconds (≥ 0).
  • removeFromOnDone boolean (optional) — When true (default), free the old output when the fade completes.
graph:crossfadeTo(runClip, 0.25)

typed/builtin//systems/anim/AnimGraph/AnimGraph/destroy

AnimGraph.destroy()

Free the whole graph: cascade-destroy() the output subtree, unbind the pose sink, and clear any active crossfade.

graph:destroy()

typed/builtin//systems/anim/AnimGraph/AnimGraph/evaluate

AnimGraph.evaluate() -> any?

Evaluate the output subtree and return its pose buffer. Returns nil when there is no output.

Returns any? — Pose buffer handle (caller MUST NOT destroy), or nil.

local pose = graph:evaluate()

typed/builtin//systems/anim/AnimGraph/AnimGraph/layoutForEntity

AnimGraph.layoutForEntity(body: EntityRef, opts: { symmetrize: boolean? }?) -> Layout

Build the layout for a graph that drives a skinned body. Resolves the body's rig from its ecs.Skeleton and packs everything Clip nodes need to retarget clips onto it and apply poses relative to its canonical bind: boneOrder, the parsed targetRig, its stride-10 canonical restPose, and the bake-cache key. Raises when body has no rigged Skeleton — call it on a body you intend to animate, after its skeleton is hydrated.

Parameters

  • body EntityRef — The EntityRef of the body to drive (carries a Skeleton with a rig).
  • opts { symmetrize: boolean? } (optional){ symmetrize } — absolute (true) vs relative (default) bind correction.

Returns Layout — A Layout for AnimGraph.new + Clip.new.

local layout = AnimGraph.layoutForEntity(skinnedBody)

typed/builtin//systems/anim/AnimGraph/AnimGraph/new

AnimGraph.new(layout: Layout, driver: string?) -> AnimGraph

Construct an empty AnimGraph with no output. :setOutput names the root node the graph drives; :bindSink binds the body the pose is applied to.

Parameters

  • layout Layout{ boneOrder, stride?, slotLayout? } — the skeleton layout shared by every node in the graph.
  • driver string (optional) — A name for whatever owns this graph — the component, tool or system an author would recognise. :tick publishes it every frame, so it is what animation.body(...).driver names for the body this graph poses.

Returns AnimGraph — The new AnimGraph instance.

local g = AnimGraph.new(AnimGraph.layoutForEntity(body), "Locomotion")

typed/builtin//systems/anim/AnimGraph/AnimGraph/play

AnimGraph.play()

Start the graph's per-frame update loop. Equivalent to :setPlaying(true).

typed/builtin//systems/anim/AnimGraph/AnimGraph/publish

AnimGraph.publish(driver: string?)

Publish what this graph is running on the body it drives, so the engine's animation observation names the clips, their playheads and their retarget coverage beside the pose it measures. :tick calls this every frame; call it directly when advancing a graph by hand.

Parameters

  • driver string (optional) — A name for whatever owns this graph, shown as the body's driver.
graph:publish("Locomotion")

typed/builtin//systems/anim/AnimGraph/AnimGraph/setOutput

AnimGraph.setOutput(node: any?)

Name the node the graph drives. The node and the subtree it owns become the graph's output; :update / :evaluate / :destroy cascade from here. Replacing the output does NOT free the old one — detach or destroy it first if it is no longer used.

Parameters

  • node any (optional) — The root node (any Clip / Mixer / BlendSpace2D).
graph:setOutput(blendSpace)

typed/builtin//systems/anim/AnimGraph/AnimGraph/setPlaying

AnimGraph.setPlaying(p: boolean)

Set the playing flag explicitly. true resumes per-frame updates; false freezes them.

Parameters

  • p boolean — Whether the graph should run per-frame updates.
graph:setPlaying(false)

typed/builtin//systems/anim/AnimGraph/AnimGraph/state

AnimGraph.state() -> { [string]: any }

Snapshot of graph state — handy for tools and debugging. Walks the output subtree; no internal references are leaked.

Returns { [string]: any }{ playing, crossfade?, output = { kind, time, duration, playing, finished, children? } }.

local snap = graph:state()

typed/builtin//systems/anim/AnimGraph/AnimGraph/stop

AnimGraph.stop()

Stop the graph's per-frame update loop. Equivalent to :setPlaying(false).

graph:stop()

typed/builtin//systems/anim/AnimGraph/AnimGraph/tick

AnimGraph.tick(dt: number, sink: any?) -> any?

One-call per-frame driver. Advances the graph + crossfade by dt, evaluates the output, and (when a sink is bound or passed) hands the pose buffer to skeleton.applyPose. Returns the pose buffer so callers can read it directly (e.g. screenshot tests).

Parameters

  • dt number — Seconds to advance.
  • sink any (optional) — A SinkHandle from skeleton.bindPose. Omitted, the sink bound via :bindSink is used; pass false to advance without applying.

Returns any? — The pose buffer handle, or nil when there is no output.

graph:bindSink(skinnedId); graph:tick(dt)

typed/builtin//systems/anim/AnimGraph/AnimGraph/update

AnimGraph.update(dt: number)

Advance the output subtree and the active crossfade by dt. No-op when the graph is not playing. When a crossfade reaches the end the output collapses to the target node and the crossfade mixer (plus, by default, the faded-out source) is freed.

Parameters

  • dt number — Seconds to advance.
graph:update(1 / 60)

typed/builtin//systems/anim/AnimGraph/Node/BlendSpace2D/BlendSpace2D/destroy

BlendSpace2D.destroy()

Destroy the internal Mixer — which cascade-destroy()s the sample sources it holds — then clear sample/triangle state. Destroying a BlendSpace2D frees the subtree below it, same as any composite node.

bs:destroy()

typed/builtin//systems/anim/AnimGraph/Node/BlendSpace2D/BlendSpace2D/evaluate

BlendSpace2D.evaluate() -> any

Find the triangle containing the current parameter, compute barycentric weights, push them into the internal Mixer, evaluate. Falls back to the nearest sample (weight 1) when the parameter lies outside the triangulated hull.

Returns any — Owned pose buffer handle (provided by the internal Mixer). Caller must NOT destroy.

local pose = bs:evaluate()

typed/builtin//systems/anim/AnimGraph/Node/BlendSpace2D/BlendSpace2D/new

BlendSpace2D.new(layout: Layout, samples: { Sample }) -> any

Construct a BlendSpace2D Node. Triangulates the sample anchors once at construction time and reuses an internal Mixer to do the per-frame barycentric weighted blend.

Parameters

  • layout Layout — Layout descriptor — boneOrder is required, stride defaults to 10, slotLayout is passed through to the internal Mixer.
  • samples { Sample } — Array of { x, y, source } entries — source is any Node.

Returns any — The constructed BlendSpace2D node.

local bs = BlendSpace2D.new(layout, { { x=0, y=0, source=idle }, { x=1, y=0, source=walk } })

typed/builtin//systems/anim/AnimGraph/Node/BlendSpace2D/BlendSpace2D/setParams

BlendSpace2D.setParams(x: number, y: number)

Set the (x, y) parameter that drives the blend.

Parameters

  • x number — Parameter X.
  • y number — Parameter Y.
bs:setParams(0.5, 0.3)

typed/builtin//systems/anim/AnimGraph/Node/BlendSpace2D/BlendSpace2D/update

BlendSpace2D.update(dt: number)

Cascade update(dt) to every sample source that defines it.

Parameters

  • dt number — Frame delta time in seconds.
bs:update(dt)

typed/builtin//systems/anim/AnimGraph/Node/Clip/Clip/destroy

Clip.destroy()

Free the bound clip sampler and the owned pose buffer.

clip:destroy()

typed/builtin//systems/anim/AnimGraph/Node/Clip/Clip/evaluate

Clip.evaluate() -> any

Sample the clip at the current playhead. With a body layout, returns a COMPLETE local pose: the body's bind, with every driven bone's rotation and translation overlaid — translation applied relative to the bind (rest plus the clip's displacement from its own start). Without a body layout, returns the raw sample.

Returns any — Owned pose buffer handle. Caller must NOT destroy — the Clip owns it.

local pose = clip:evaluate()

typed/builtin//systems/anim/AnimGraph/Node/Clip/Clip/new

Clip.new(clipRef: any?, layout: Layout, looping: boolean, speed: number?, sourceRig: any?) -> (any, string?)

Construct a Clip Node from a .animation asset reference. Reads the clip bytes, retargets them onto the layout's target rig (when the layout carries a body), binds the result to the bone order via skeleton.bindClip, and records which bones the clip drives so evaluate overlays only those on the bind pose. matched == 0 (the clip drives none of these bones) is logged so a silent rest pose never goes unexplained.

Parameters

  • clipRef any (optional) — Asset identity string or AssetRef for the .animation.
  • layout Layout — Layout descriptor — boneOrder is required; stride defaults to 10.
  • looping boolean — When true the playhead wraps at duration; otherwise it clamps and marks the clip finished.
  • speed number (optional) — Playback rate multiplier (defaults to 1).
  • sourceRig any (optional) — Optional explicit .rig ref overriding the clip's recorded source rig.

Returns (any, string?) — The constructed Clip node, or nil + error message when the clip cannot be read or bound.

local clip = Clip.new(clipRef, layout, true, 1.0)

typed/builtin//systems/anim/AnimGraph/Node/Clip/Clip/rewind

Clip.rewind()

Rewind the playhead to 0, clear finished, and resume playback.

clip:rewind()

typed/builtin//systems/anim/AnimGraph/Node/Clip/Clip/setPlaying

Clip.setPlaying(p: boolean)

Pause or resume the playhead. Any value that is not the boolean true becomes playing = false.

Parameters

  • p boolean — New playing state.
clip:setPlaying(false)

typed/builtin//systems/anim/AnimGraph/Node/Clip/Clip/update

Clip.update(dt: number)

Advance the playhead by dt * speed. Wraps at duration when looping is true; otherwise clamps and marks the clip finished / playing = false. No-op when not playing or already finished.

Parameters

  • dt number — Frame delta time in seconds.
clip:update(dt)

typed/builtin//systems/anim/AnimGraph/Node/Layer/Layer/destroy

Layer.destroy()

Cascade-destroy() base + overlay, then free this Layer's output buffer. A Layer owns both inputs (the graph is a tree), so destroying it frees the subtree. An input detached (set to nil) is skipped.

layer:destroy()

typed/builtin//systems/anim/AnimGraph/Node/Layer/Layer/evaluate

Layer.evaluate() -> any

Evaluate base + overlay, then per-bone blend overlay onto base by mask[bone] * weight (translation lerp, rotation nlerp, scale lerp) into the output buffer. With weight 0 (or an all-zero mask) the base passes through.

Returns any — Owned pose buffer handle. Caller must NOT destroy.

local pose = layer:evaluate()

typed/builtin//systems/anim/AnimGraph/Node/Layer/Layer/new

Layer.new(layout: Layout, base: any?, overlay: any?, mask: { number }) -> any

Construct a Layer Node that blends overlay over base per the per-bone mask scaled by the layer weight. Allocates an output pose buffer.

Parameters

  • layout Layout — Layout descriptor — boneOrder required, stride defaults to 10.
  • base any (optional) — The base Node (e.g. the locomotion output) — passes through where mask*weight is 0.
  • overlay any (optional) — The overlay Node (e.g. an attack clip) — taken where mask*weight is 1.
  • mask { number } — Per-bone weight array (one entry per bone, in boneOrder); see AnimGraph.mask. Missing entries are 0.

Returns any — The constructed Layer node.

local l = Layer.new(layout, loco, attack, AnimGraph.mask(layout, "upperBody", rig))

typed/builtin//systems/anim/AnimGraph/Node/Layer/Layer/setWeight

Layer.setWeight(w: number)

Set the layer's global weight (0 = base only, 1 = full overlay where the mask is 1). The owner ramps this to fade the action in and out.

Parameters

  • w number — New weight, typically in [0, 1].
layer:setWeight(0.5)

typed/builtin//systems/anim/AnimGraph/Node/Layer/Layer/update

Layer.update(dt: number)

Cascade update(dt) to the base and overlay sources. The Layer is the graph output, so it owns advancing both subtrees.

Parameters

  • dt number — Frame delta time in seconds.
layer:update(dt)

typed/builtin//systems/anim/AnimGraph/Node/Mixer/Mixer/destroy

Mixer.destroy()

Cascade-destroy() every input source, then free this Mixer's own blend layout and output buffer. A Mixer owns its inputs (the graph is a tree), so destroying it frees the subtree below it. An input whose source was detached (set to nil — e.g. a crossfade survivor handed back to the graph) is skipped.

mix:destroy()

typed/builtin//systems/anim/AnimGraph/Node/Mixer/Mixer/evaluate

Mixer.evaluate() -> any

Evaluate inputs in turn, then weighted-blend their pose buffers into the output buffer. Inputs with weight <= 0 are skipped.

Returns any — Owned pose buffer handle. Caller must NOT destroy.

local pose = mix:evaluate()

typed/builtin//systems/anim/AnimGraph/Node/Mixer/Mixer/new

Mixer.new(layout: Layout, inputs: { MixerInput }) -> any

Construct a Mixer Node. Allocates an output pose buffer sized for the layout and the engine-side blend layout.

Parameters

  • layout Layout — Layout descriptor — boneOrder is required, stride defaults to 10, slotLayout defaults to translation lerp + rotation slerp + scale lerp.
  • inputs { MixerInput } — Array of { source = Node, weight = number } entries.

Returns any — The constructed Mixer node.

local mix = Mixer.new(layout, { { source = clipA, weight = 1 }, { source = clipB, weight = 0 } })

typed/builtin//systems/anim/AnimGraph/Node/Mixer/Mixer/setWeight

Mixer.setWeight(i: number, w: number)

Set or update an input's weight at index i. Out-of-range index is a no-op.

Parameters

  • i number — 1-based input index.
  • w number — New weight (typically in [0, 1]).
mix:setWeight(1, 0.5)

typed/builtin//systems/anim/AnimGraph/Node/Mixer/Mixer/update

Mixer.update(dt: number)

Cascade update(dt) to every input source that defines it.

Parameters

  • dt number — Frame delta time in seconds.
mix:update(dt)

typed/builtin//systems/anim/AnimGraph/Node/Node/destroy

Node.destroy()

Default :destroy — no resources to free. Subclasses override to release owned Channel/Buffer/Layout handles.

node:destroy()

typed/builtin//systems/anim/AnimGraph/Node/Node/evaluate

Node.evaluate() -> any?

Default :evaluate — returns nil. Subclasses override and return a Buffer handle (caller MUST NOT destroy).

Returns any? — Pose Buffer handle, or nil from the default implementation.

local pose = node:evaluate()

typed/builtin//systems/anim/AnimGraph/Node/Node/new

Node.new() -> Node

Allocate a bare Node table (no Channels, no Buffer). Subclasses typically wrap setmetatable(Node.new(), <Subclass>) and then set the subclass-specific fields.

Returns Node — The new Node instance.

local n = Node.new()

typed/builtin//systems/anim/AnimGraph/Node/Node/update

Node.update(dt: number)

Default :update — no internal state to advance. Subclasses override to step playhead, ramp weights, etc.

Parameters

  • dt number — Seconds elapsed since the last update.
node:update(1 / 60)

typed/builtin//systems/anim/AnimGraph/rigResolve/R/parseBodyRig

R.parseBodyRig(body: EntityRef) -> any?

The parsed rig of a skinned body, read straight from the ECS — its ecs.Skeleton bones (rest pose + hierarchy) plus its ecs.RetargetProfile roles when present. Everything the graph needs is already in the ECS by this point; no asset is resolved and no document is parsed. A body whose Skeleton carries no bones returns nil. A body with no RetargetProfile is a non-humanoid rig — the returned rig simply has an empty role map, so the graph binds its own clips directly instead of retargeting.

Parameters

  • body EntityRef — The EntityRef of the body to drive.

Returns any?

typed/builtin//systems/anim/AnimGraph/rigResolve/R/parseFromClip

R.parseFromClip(ref: any?, rigOverride: any?) -> any?

The parsed source rig a clip was authored on. ref is a .animation ref; pass rigOverride (a .rig ref) to force a specific source rig.

Parameters

  • ref any (optional)
  • rigOverride any (optional)

Returns any?

typed/builtin//systems/anim/AnimGraph/rigResolve/R/refId

R.refId(v: any?) -> string?

The asset identity / guid behind a ref value (an AssetRef or a string).

Parameters

  • v any (optional)

Returns string?

typed/builtin//systems/anim/AnimGraph/rigResolve/R/restPose

R.restPose(parsedRig: any?) -> { number }

The stride-10 bind pose (translation.xyz + rotation.xyzw + scale.xyz per bone, in rig order) the graph poses relative to. Undriven channels hold this; a clip overlays only the channels it drives.

Parameters

  • parsedRig any (optional) — A parsed rig (from retarget.parseRig).

Returns { number }

typed/builtin//systems/anim/AnimGraph/rigResolve/R/rigKey

R.rigKey(parsedRig: any?) -> string

The identity of a rig AS A RETARGET TARGET — equal for two rigs a clip bakes onto identically, different whenever the bake would differ. This is the cacheKey half retarget.bakeBytes documents as "target rig identity": key a bake on the rig it targets and every body built from that rig shares one bake, instead of each re-baking all of its clips.

A rig has no asset identity to borrow — parseBodyRig builds it from the body's live Skeleton and RetargetProfile — so the key is taken over the content the bake actually reads: bone names and parents, each bone's rest transform, and the profile's base + role map. Rests are included because the bake scales translation by the source/target height ratio, so two skeletons sharing bone names but not proportions must NOT share a bake. Rest components are quantized before hashing so a value that differs only in float noise still lands on one key.

Parameters

  • parsedRig any (optional) — A parsed rig (from retarget.parseRig).

Returns string — A short stable string, usable directly as a cache key.

local key = rigResolve.rigKey(rig)

typed/builtin//systems/characterController/characterController/physics/ceiling/M/cast

M.cast(x: number, y: number, z: number, height: number, skinWidth: number, selfId: string) -> any

Cast a short ceiling-detection ray upward from the character's head. The ray starts at (x, y + height - skinWidth, z) and travels up for skinWidth * 2.

Parameters

  • x number — Character feet position X.
  • y number — Character feet position Y.
  • z number — Character feet position Z.
  • height number — Character capsule height.
  • skinWidth number — Collision skin margin.
  • selfId string — Entity ID to exclude from the raycast.

Returns any — Raycast hit table, or nil when nothing overhead.

local hit = Ceiling.cast(px, py, pz, 1.8, 0.01, selfId)

typed/builtin//systems/characterController/characterController/physics/ground/M/cast

M.cast(x: number, y: number, z: number, skinWidth: number, maxDist: number, selfId: string, riseDist: number?) -> any

Cast a ground-detection ray downward from a position. Origin is raised by skinWidth plus riseDist so the ray starts above the feet, and the returned hit.distance is adjusted back to be relative to the feet (not the ray origin) — negative for ground that stands above them.

Parameters

  • x number — Character feet position X.
  • y number — Character feet position Y.
  • z number — Character feet position Z.
  • skinWidth number — Small offset above the feet to start the ray from.
  • maxDist number — How far below the feet to check.
  • selfId string — Entity ID to exclude from the raycast.
  • riseDist number (optional) — How far above the feet to check as well. Ground found up there comes back with a negative distance, which is how much the character has to rise to stand on it. Defaults to 0.

Returns any — Raycast hit table with distance adjusted to be feet-relative, or nil.

local hit = Ground.cast(px, py, pz, 0.01, 0.2, selfId, 0.3)

typed/builtin//systems/characterController/characterController/physics/ground/M/projectOnSlope

M.projectOnSlope(moveX: number, moveY: number, moveZ: number, normalX: number, normalY: number, normalZ: number) -> (number, number, number)

Project a movement vector onto the slope plane defined by a surface normal. Computes v - (v . n) * n — the component of v that lies in the plane orthogonal to n.

Parameters

  • moveX number — Movement X.
  • moveY number — Movement Y.
  • moveZ number — Movement Z.
  • normalX number — Surface normal X.
  • normalY number — Surface normal Y.
  • normalZ number — Surface normal Z.

Returns (number, number, number)projX, projY, projZ — the projected movement vector.

local px, py, pz = Ground.projectOnSlope(dx, 0, dz, nx, ny, nz)

typed/builtin//systems/characterController/characterController/physics/ground/M/slopeAngle

M.slopeAngle(nx: number, ny: number, nz: number) -> number

Compute the slope angle (degrees) between a ground normal and world up. Returns 0 when the input is the zero vector.

Parameters

  • nx number — Ground normal X.
  • ny number — Ground normal Y.
  • nz number — Ground normal Z.

Returns number — Slope angle in degrees, in [0, 180].

local angle = Ground.slopeAngle(hit.normal.x, hit.normal.y, hit.normal.z)

typed/builtin//systems/characterController/characterController/physics/walls/M/castBody

M.castBody(x: number, y: number, z: number, dirX: number, dirZ: number, height: number, radius: number, skinWidth: number, maxSlopeAngle: number, selfId: string) -> any

Sweep the character's capsule from (x, y, z) (its feet) in direction (dirX, 0, dirZ) and report the wall it runs into within skinWidth of its own surface. Because the whole body is swept, a passage narrower than 2 * radius blocks the character even when nothing stands on its centre line.

The sweep starts above the ground band — the depth a cap of radius reaches below ground of maxSlopeAngle, which is how far into the slope the body's own lower cap sits while it stands there. Below that line the surface belongs to the ground and step passes; above it the sweep runs clear of the ground and reports the outward normal of the surface standing across the path.

A contact whose surface faces up or down — ramps the character walks up, to maxSlopeAngle, and anything directly overhead — belongs to the ground and ceiling passes. The sweep carries on past those to the surface that stands across the path.

Parameters

  • x number — Character feet X.
  • y number — Character feet Y.
  • z number — Character feet Z.
  • dirX number — Horizontal movement direction X (normalized).
  • dirZ number — Horizontal movement direction Z (normalized).
  • height number — Character capsule height.
  • radius number — Character capsule radius.
  • skinWidth number — How far past the body's own surface the sweep reaches.
  • maxSlopeAngle number — Steepest surface, in degrees, the character walks on.
  • selfId string — Entity ID to exclude from the sweep.

Returns any — Hit table { entityId, point, normal, distance }, or nil when the body's path is clear.

local hit = Walls.castBody(px, py, pz, dx, dz, 1.8, 0.3, 0.01, 45, selfId)

typed/builtin//systems/characterController/characterController/physics/walls/M/castDirection

M.castDirection(x: number, y: number, z: number, dirX: number, dirZ: number, radius: number, skinWidth: number, selfId: string) -> any

Cast a horizontal wall-detection ray from (x, y, z) in direction (dirX, 0, dirZ). Ray length is radius + skinWidth.

Parameters

  • x number — Character centre X.
  • y number — Character centre Y (sample height).
  • z number — Character centre Z.
  • dirX number — Horizontal direction X (normalized).
  • dirZ number — Horizontal direction Z (normalized).
  • radius number — Character capsule radius (ray starts at the centre).
  • skinWidth number — Extra margin for depenetration.
  • selfId string — Entity ID to exclude from the raycast.

Returns any — Raycast hit table, or nil when nothing in front.

local hit = Walls.castDirection(px, py, pz, dx, dz, 0.3, 0.01, selfId)

typed/builtin//systems/characterController/characterController/physics/walls/M/slideAlongWall

M.slideAlongWall(moveX: number, moveZ: number, normalX: number, normalZ: number) -> (number, number)

Compute a wall-slide direction given the desired horizontal movement and a wall normal. Strips the component of move that goes into the wall; returns the input unchanged when the movement isn't pressing into the wall (dot >= 0) or the normal is effectively zero in the XZ plane.

Parameters

  • moveX number — Desired horizontal movement X.
  • moveZ number — Desired horizontal movement Z.
  • normalX number — Wall surface normal X.
  • normalZ number — Wall surface normal Z.

Returns (number, number) — Adjusted moveX, moveZ that slides along the wall.

local mx, mz = Walls.slideAlongWall(dx, dz, nx, nz)
  • api
  • reference