Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 07158574-5…

Model

Loads and displays a 3D mesh on an entity. Disabling the component hides the mesh. Supports procedural meshes (`cube`, `sphere`, ...), model files, and remote URLs. Tint and outline are shader effects applied to the mesh.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 3mo ago
What it does

Model

Loads and displays a 3D mesh on an entity. Disabling the component hides the mesh. Supports procedural meshes (cube, sphere, ...), model files, and remote URLs. Tint and outline are shader effects applied to the mesh.

Material lives on this component. There is no standalone Material component — a material only renders where there is a mesh to render it on, so the material field is part of Model (and SkinnedModel). Set it at add time or assign the field later; properties are registry-wide.

Public fields: model, material (AssetRef<material>), tintR/G/B, tintBlend, outlineR/G/B, outlineIntensity.

Methods: :setTint(color, blend?) (color: {r, g, b} array or {r=, g=, b=} map), :clearTint(), :setOutline(color, intensity?), :clearOutline(), :setMaterialProperty(prop, value), :getMaterialProperty(prop), :getMaterialPropertyNames().

entity(id).component.add("Model", { model = "cube", material = "gold" })
entity(id).component.get("Model").material = "checkerboard"   -- swap material
entity(id).component.get("Model"):setTint({1, 0, 0}, 0.5)
entity(id).component.get("Model"):setMaterialProperty("roughness", 0.2)

Tint is synced. Every tint* field replicates, so :setTint is the colour every participant sees. A colour that changes every frame on many entities (a stress map, a heat map, a damage flash, per instance colour) belongs in an instance-data lane instead: wear @builtin::materials.instance_color (or instance_color_unlit) and write renderer.instanceData.set(id, 2, r, g, b, coverage), which replicates nothing and costs about a twentieth of a tint write. guides { path = "topics/rendering-instance-data" } has the lane layout.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

Model Component Loads and displays a 3D mesh on the entity. Disabling hides the mesh. Tint recolours the mesh; outline draws a coloured silhouette rim around it. Usage: entity.find("box1").component.add("Model", { model = "cube" }) entity.find("box1").component.add("Model", { model = "cube", material = "gold" }) entity.find("box1").component.get("Model"):setTint({1, 0, 0}, 0.5) The `model` field accepts a builtin primitive name (cube, sphere, ...), a library identity (@publisher.path), or an AssetRef handle — the framework resolves all forms to an AssetRef before awake() runs. The `material` field references a named material definition in the registry (library or scene-created). Material lives ON the Model component — not as a standalone component — because a material only renders where there is a mesh to render it on. There is no per-instance material override: material properties are registry-wide (edits via `setMaterialProperty` below affect every entity sharing that material). This component drives the renderer entirely through the public `ecs.*` API (ecs.Mesh -> MeshInstance, ecs.Material -> MaterialRef). The async ecs.insert is correct here: a mesh/material ref may need to load, and that load must not stall the main thread. The typed ecs.Mesh / ecs.Material constructors own the camelCase -> snake_case mapping and the assetRef -> guid coercion, so this file carries no hand-maintained field map.

toHandle(ref: ?) → void

Bridge a resource to its live GPU HANDLE for the ecs layer. An AssetRef materialises its GPU resource via `:handle()` (the Disk→CPU→GPU upload — a mesh uploads its geometry, a material uploads the material and its textures) and the handle is returned; a value that already is a handle is returned as-is. The component owns this asset→GPU step and hands the ecs layer a resident handle.

argtypedescription
ref?

materialHandle(ref: ?) → void

Resolve the material AssetRef to a live GPU handle, robust to a stale guid. A material is registry-keyed by its identity; deleting and recreating one mints a fresh guid and orphans the old, so a saved-scene ref that still carries the old guid resolves to nothing and the mesh renders the white fallback. When the ref's guid is no longer a live material, resolve through the stable identity — which tracks the current material — so the mesh binds the material the author named. The guid stays the preferred lookup; the identity is consulted only when the guid has gone dead.

argtypedescription
ref?

meshFields( ) → void

Collect the public fields into the ecs.Mesh constructor's input shape. The constructor maps these to MeshInstance's rust fields (tint expands to the per-channel scalars, camelCase -> snake_case for the rest). `mesh` is bridged to its GPU handle (toHandle) so the ecs layer receives a resident GPU resource — the ecs layer just reads the handle's guid.

applyOutline( ) → void

Drive the canonical outline (a jump-flood silhouette rim) from the outline fields. `outlineIntensity` scales the rim's pixel thickness; 0 clears it. The fields and the `setOutline`/`clearOutline` methods share this one path, so authoring the outline as component data renders identically to calling the method.

renderBroken(reason: string) → void

argtypedescription
reasonstring

applyMesh( ) → void

Apply `public.model` to the entity's MeshInstance. Absent model → remove the component; present → insert (async; the renderable appears once the mesh load resolves). insert is an upsert, so re-applying re-syncs every renderer-facing field at once.

applyMaterial( ) → void

Apply the entity's rendered material to its MaterialRef: the session material when one is active, else the authored `material`. The renderer pairs MaterialRef with the MeshInstance on the same entity automatically.

awake( ) → void

onPropertyChanged(key: ?, value: ?, _oldValue: ?) → void

argtypedescription
key?
value?
_oldValue?

onEnable( ) → void

onDisable( ) → void

onError(error: ?) → void

argtypedescription
error?

onAssetReload(field: ?) → void

The mesh or material this model draws with was edited in place — same ref, new content. Baked lighting reads a surface's albedo and emissive out of its material and its shape out of its mesh, so tell the bake: nothing about this entity changed, but the light it bounces did.

argtypedescription
field?

onDestroy( ) → void

setTint(color: table, blend: number?) → void

Set the mesh tint colour and blend amount. The tint fields are synced, so every participant sees it. A colour rewritten every frame on many entities (stress colour, heat map, damage flash) is cheaper as an instance-data lane: `@builtin::materials.instance_color` reads one, written with `renderer.instanceData.set(id, 2, r, g, b, coverage)`. are 0-1.

argtypedescription
colortableTint color as {r, g, b} array or {r=, g=, b=} map. Channel values
blendnumber?Blend amount (0 = no tint, 1 = full tint). Defaults to 1.

examples

model:setTint({1, 0, 0})
model:setTint({r = 1, g = 0, b = 0}, 0.5)

clearTint( ) → void

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

examples

model:clearTint()

setOutline(color: table, intensity: number?) → void

Set the outline colour and intensity for this mesh. values are 0-1.

argtypedescription
colortableOutline color as {r, g, b} array or {r=, g=, b=} map. Channel
intensitynumber?Outline intensity (0 = none, 1 = full). Defaults to 1.

examples

model:setOutline({0, 1, 0})
model:setOutline({r = 1, g = 1, b = 0}, 0.8)

clearOutline( ) → void

Remove the outline from this mesh.

examples

model:clearOutline()

sessionSchema( ) → any

The session copy's vocabulary — the properties its record carries and the texture slots its shader declares or it binds — in the shape `MaterialSchema.route` reads, held by the renderer module per record and answered as a table lookup. A write resolves its key here and asks the renderer nothing: what the renderer holds for the material is a different document, and reading it is a cost no write pays. nil when the record carries neither, which writes every key as the caller spelled it: the permissive stance the authored path takes when a shader's schema can't be resolved.

routeSessionWrite(schema: any?, name: string, value: any) → void

Where a written key lands on the session copy — the surface its shader declares the name under, and the name it declares. With no vocabulary reported the key is taken as the caller spelled it.

argtypedescription
schemaany?
namestring
valueany

setMaterialProperty(property: string, value: any) → void

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.

argtypedescription
propertystringProperty name (string).
valueanyNew value for the property. Type depends on the property.

examples

model:setMaterialProperty("baseColor", {1, 0, 0, 1})
model:setMaterialProperty("roughness", 0.5)

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.

argtypedescription
props{ [string]: any }Table of `{ [propertyName] = value }` pairs.

examples

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

getMaterialProperty(property: string) →

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

argtypedescription
propertystringProperty name (string).

examples

local color = model:getMaterialProperty("baseColor")

getMaterialPropertyNames( ) →

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

examples

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

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.

examples

local Lightmap = require("@builtin::systems.globalIllumination.lightmap")
if model:resolveMobility() == "static" then Lightmap.bake(id) end

applySessionMaterial(handle: any) → void

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

argtypedescription
handleanyThe runtime material handle OBJECT, as `renderer.material.create`

examples

model:applySessionMaterial(handle)
model:applySessionMaterial()  -- copy the current material for per-entity edits

restoreSessionMaterial( ) → boolean

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

examples

model:restoreSessionMaterial()

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

58+items
·
assettype · born here
❒asset
# Component (asset type) A component is a reusable behavior + data unit attached to an entity. The component file declares public fields, methods, and lifecycle callbacks; the engine instantiates one per attachment and drives its hooks every frame. Components are the primary unit of gameplay code — movement, AI, animations, UI panels, network sync, and per-entity state all live here. ## When to use one - You need per-entity state that survives across frames. - You need per-entity behavior driven by engine lifecycle hooks (`start`, `update`, `onDestroy`). - You want to expose configurable fields to designers / `entity.spawn` callers / scenes without code changes. If you need a stateless pure-function library, use a `.module`. If you need to generate content declaratively, use a `.service`; compute work belongs in a `.computeShader`. ## Where it lives - Source: `/zero/source/.../<Name>.component/` - Identity: `<Name>` (the `.component` suffix strips from the identity, but the folder retains the suffix on disk). - Folder shape: - `init.luau` (or `init.lua`) — declares the component. **Required.** - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** The structural contract is enforced by `asset.validate` against this type's `type.yaml`. ## How to create one Always start with the scaffold — it guarantees the folder shape matches `type.yaml` from the moment it's on disk: ```luau asset.create("component", "<Name>") -- Creates: /zero/source/<Name>.component/ -- init.luau (canonical declare + public block + hooks stubbed) -- README.md (instance README template) ``` After scaffolding, edit `init.luau` to fill in the actual fields and behaviour. The component registers as soon as the file is written — hot-reload is automatic; no engine restart required. ## How it operates 1. **Registration.** Writing `init.luau` into a `.component/` folder indexes the asset, mints a guid (in the `.meta` sidecar), and surfaces the type to the engine's component registry. Same-frame visible to `entity.component.add`. 2. **Attachment.** `entity(id).component.add(name, data)` (or the batch form `tools.use("entityOps", "addComponents", targets, { [name] = data })`) attaches an instance. The `data` table's fields override the component's declared defaults; omitted fields take the declared default. The `data` values are copied into `public.*` BEFORE `awake()` runs. 3. **Lifecycle.** The hooks run in this order: - `awake()` — same frame as attach, immediately after the data copy. Initialise state, set up native bridges, register with other systems. Other components on the same entity may not have awoken yet. - `start()` — the next frame. Every component on the entity has completed `awake()` by then, so cross-component setup is safe here. - `update(dt)` — same frame as `start()`, then every frame after, for as long as the gameplay clock is running. `fixedUpdate(dt)` runs once per physics step, straight after it, with the step's `dt`, so a frame can run it several times or not at all; `editorUpdate(dt)` runs while the clock is held. Which of them ticks is decided by `engine.mode` AND `engine.paused` together — see **Per-frame hooks and the two axes** below, and `man modes`. - `onDestroy()` — when the component is removed via `component.remove` or when the owning entity is despawned. Release resources, drop native ECS components. For the full list (`onEnable`, `onDisable`, `fixedUpdate`, `editorUpdate`, `onPropertyChanged`, `onAssetReload`, `onModuleReload`, `onBeforeSave`, …) and the timing details, **read `man components` — that is the canonical lifecycle reference. Generic component patterns (declare blocks, public fields, sync, ECS bindings, typed methods) all live there.** 4. **Public surface.** Fields declared in `public` are inspectable, editable in the inspector, and persist with the entity. Methods are declared on that same table with a colon — `typed function public:method(args)` — and every one of them takes the component ref as `self`, so a caller invokes it with a colon too: `entity(id).component.get(name):method(args)`. That is the one calling convention a component ref answers to: the reflection methods `ref:fields()` / `ref:getField(name)` / `ref:setField(name, value)` take the same spelling. A function the chunk declares at file scope (`function helper()`) is the component's own, reachable from its hooks under its bare name; `public` is what the ref carries. 5. **Settings and the Inspector card.** `type.yaml` names the component's settings schema as `settings: { schema: { instance: "init.luau", export: "public" } }`: the `public` table IS the schema, and its values live on each entity the component is on, so `asset.schema(componentRef)` answers the Field descriptors and no values file exists. On an entity the Inspector shows the component as a card whose generic `settings` section edits those fields as typed controls, each edit checked against the schema and written to the live component as one undoable operation. `type.yaml` also names `inspector: { instance: "inspector.luau" }`: a component that ships an `inspector.luau` beside its `init.luau` shapes its card — the file returns `{ sections, replace, order }` (see `assetTypes/README.md`, "`inspector:`"), each section's `build(ctx)` reading `ctx.component` (the live component) and `ctx.selection.id` (the entity). `SceneModule.component/inspector.luau` replaces `settings` with the bound module's declared inputs. 6. **Events.** An `events` block declares typed moments the component fires from inside — `events = { onHit = Event({ dmg = Field.number(0, NoSync) }) }`. The owner fires with `events.onHit:fire({ dmg = 10 })`; outside code subscribes through the proxy's subscribe-only facade, `entity(id).component.get(name).events.onHit:connect(fn)` (`:once` / `:wait` too). Pass `Sync` as the event's second argument to replicate a fire to other peers. Events let one component react to another's moments without polling. Full reference: **`man components`**, Events section. 7. **Hot reload.** Editing `init.luau` reloads the component definition; live instances are reconciled with the new declaration in-place, and each crosses one lifecycle boundary: `onDestroy()` ends the running life, then `awake()` and `start()` construct the one that replaces it, and `update()` resumes on the next frame. `public` and `private` values carry across; module-level `local`s belong to the chunk, so the new life starts with them at their initial values and its `awake`/`start` are what fill them in. Whatever the old life handed elsewhere — a subscription, a shared registration, a spawned entity — is released in `onDestroy`, the call that pairs with the construction about to run. An edit that leaves the source byte-identical crosses no boundary at all. A field carries its value across when something stated it: a key of the `data` table the instance was attached with (a scene record's data is that table), or a write made since. Every other field takes the edited source's declared default, so changing a default reaches every instance that left the field to it. An instance that states its own value for a field whose default the edit changed keeps that value, and the reload logs a `[HotReload]` warning naming the field, the entity, the value kept and both defaults. ## Discovery - `asset.list("component")` — every registered component (engine + world + libraries). - `asset.inspect("<name>")` — identity, scope, public fields, lifecycle methods, source path, and this type README. `detail` carries the component's declared `fields`, `methods`, and `events`, parsed from its source — `tools.use("assets","describe", "<name>")` renders it as markdown, so a component's events are visible without opening its source. - `ls /zero/source/<Name>.component` lists the files the folder holds. - `entity(id).component.list()` — components attached to an entity. ## Per-frame hooks and the two axes `engine.mode` (`"edit"` / `"play"`) and `engine.paused` (the gameplay clock) are independent, and the per-frame hooks read them separately: - `update(dt)` / `fixedUpdate(dt)` tick while **the gameplay clock is running** — whichever mode the engine is in. - `editorUpdate(dt)` ticks while **the clock is held** — because the mode is `edit`, or because play is paused — and also alongside `update` whenever edit is running. Which gives four states, and this table is the contract: | `engine.mode` | `engine.paused` | hooks that tick | |---|---|---| | `edit` | `true` | `editorUpdate` | | `edit` | `false` | `update`, `fixedUpdate`, `editorUpdate` | | `play` | `false` | `update`, `fixedUpdate` | | `play` | `true` | `editorUpdate` | Entering edit sets `paused = true` and entering play sets it back to `false`, so a session that never writes `engine.paused` only ever sees rows 1 and 3 — which is where the shorthand "`update` is the play tick, `editorUpdate` is the edit tick" comes from. It is the two default rows of a four-row table, not the rule. **Row 2 is the one that surprises.** With `engine.paused = false` in edit — set by hand, or by a tool that advances the gameplay clock to sample it, such as a `capture.collage` given a `duration` — `update` and `editorUpdate` both fire, once each, every frame. A component that declares both and routes them to one body therefore runs that body **twice per frame**: ```luau local function drive(dt) ... end function update(dt) drive(dt) end function editorUpdate(dt) drive(dt) end -- twice per frame in row 2 ``` Nothing reports the doubling; it reads as an animation running at twice its intended speed. To get exactly one tick per frame in every row, ask which axis you meant and answer it in the body: ```luau function update(dt) drive(dt) end function editorUpdate(dt) if engine.paused then drive(dt) end end ``` A scene `entrypoint.luau` declaring the same two hook names is dispatched by the scene loader, not by this table: there `update` needs play AND running, `editorUpdate` needs edit, and play-paused ticks neither. Read `man scenes` before carrying a component's rule to an entrypoint. ## Authoring conventions - Annotate public fields with `--!desc` so the inspector + LSP show documentation alongside the field name. - Declare lifecycle hooks (`start`, `update`, `onDestroy`) explicitly rather than relying on engine defaults — readers can see at a glance what runs when. - Keep `update(dt)` cheap — it runs every frame per instance. Move heavy work to `start`, to a `.computeShader`, or behind a job (`man jobs`). - For multi-entity batching, prefer `tools.use("entityOps", "addComponents", targets, components)` over a loop of `entity(id).component.add` — it takes many targets and many components in one call. ## Common pitfalls - **Naming.** The component identity is the folder stem with `.component` stripped (e.g. `<Spinner>.component/` → identity `Spinner`). Don't repeat the suffix in the identity passed to `component.add`. - **`init.lua` vs `init.luau`.** Either is accepted (it's a `one_of_group: "entrypoint"`), but `.luau` is canonical. Mixing both in one folder is invalid. - **`update` in edit mode.** It runs whenever the gameplay clock is running, and entering edit stops that clock — so `update` is idle in a freshly-entered edit session and ticks again the moment anything sets `engine.paused = false`. Use `tools.use("wld", "play")` to test update logic under the mode it ships in; `tools.use("wld", "edit")` to return. See **Per-frame hooks and the two axes** above. - **Renaming.** Renaming the folder changes the identity. References to the component are recorded against the component itself, so renaming or moving it does not break them. ## Related types - `.module` — pure-function library, no per-entity state. - `.service`: a declarative content generator, no per-entity state. - `.preset` — a captured component configuration, replayable into `component.add` without re-typing fields. - `.bundle` — entity hierarchies that may carry component attachments as part of the bundle. ## In the Inspector Fields: its execution order, hooks, and how many methods and events it declares, then the fields its `public` table declares as read-only typed controls holding their defaults, with **Check** and **Open** for its entry file. Instances: a link to every entity carrying it. What its card shows on an entity comes from the component's own `inspector.luau`.
▲ 0↑ born
▣
module · born here
❒asset
# lightmap Bakes full global illumination (shadowed direct light plus multi-bounce indirect gathered by path tracing through the scene BVH) into per-entity lightmaps: the bake output is read back, dilated across UV seams, and copied into the receiver's region of ONE shared atlas that the PBR module samples through the receiver's per-instance lanes — the receiver keeps the material it was authored with. Each texel traces `samples` cosine-weighted chains of up to `bounces` surface interactions, so color bleed, emissive surfaces, and sky occlusion all land in the map. Receiver selection is the `Model.mobility` axis: only static-mobility world entities bake. Movable geometry samples the published probe fields instead. Set `Model.mobility = "static"` to override a wrong derivation. Every bake persists: the dilated texels land in the scene's baked-lighting container (`lightmapData` asset) keyed by the entity, and the receiver's `Model.lightmapData` ref points at it. A fresh boot restores the lightmap from the asset (`applyFromAsset`, called by the Model component's awake) with zero re-baking. ## Exports - `Lightmap.bake(entityId: string, opts: BakeOpts?) -> BakeReport` — bake one static entity's lightmap into the shared atlas and point the receiver at it. Yields while the GPU bake + read-back complete. - `Lightmap.bakeAll(opts: BakeOpts?) -> { BakeAllEntry }` — bake every static receiver in the world, reusing a single light + BVH cache across them. - `Lightmap.applyFromAsset(entityId: string) -> (boolean, string?)` — restore a receiver's baked lightmap from its persisted container entry (the boot path; no re-bake). - `Lightmap.clear(entityId: string)` — destroy the bake buffer, the runtime lightmap texture, and the per-entity material, restore the entity's original material, and remove the container entry. - `Lightmap.bufferName(entityId: string) -> string` — stable per-entity bake-output buffer name (`"gi_lightmap_<sanitized id>"`). - `Lightmap.materialName(entityId: string) -> string` — stable per-entity material name produced by `bake`. Types: - `ColorRGB = { r: number, g: number, b: number }` - `BakeOpts = { resolution?, texelsPerMeter?, samples?, bounces?, intensity?, rayMax?, shadowSoftness?, sky?, lightsCache?, bvhCache?, rastCache?, containerCache? }` - `BakeReport = { ok, resolution, texelCount, validTexels, ms, bufferName, materialName, textureGuid }` - `BakeAllEntry = { id: string, report: BakeReport }` ## Usage ```luau local Lightmap = require("@builtin::systems.globalIllumination.lightmap") local report = Lightmap.bake(entityId, { resolution = 64, samples = 16, bounces = 3 }) print(report.ms, "ms") -- Bake every static receiver in the world: local reports = Lightmap.bakeAll({ samples = 32 }) -- Tear down: Lightmap.clear(entityId) ``` ## Notes - `bake` refuses non-static receivers with an error naming the mobility rule — the published probe fields are the movable-geometry counterpart. - `bakeAll` builds a single BVH and gathers lights once, threading the caches through every per-entity bake. Add/move lights between invocations to invalidate. - The lightmap texture is a runtime GPU resource under a stable per-entity guid (`BakeReport.textureGuid`); re-bakes update it in place, so the material keeps rendering the latest bake with no re-binding. - Texel coverage rides in the texture's alpha; the upload dilates covered texels over uncovered neighbours so bilinear filtering never bleeds black across UV island seams. - `clear` is a no-op when the entity is missing or never baked.
▲ 0↑ born
▣
module · born here
❒asset
# renderError Shared "this render is broken" marker. `visibleError(entityId)` swaps an entity to the builtin ERROR text model + error material, so a failed render reads as an unmistakable 3D "ERROR" sign on screen instead of empty pixels — empty silently reads as "fine" and a broken object gets mistaken for working. Every render component (built-in `Model` / `SkinnedModel`, or a user-authored one) calls this, so the error model is defined in one place: change it here and every render component updates.
▲ 0↑ born
▣
module · born here
❒asset
# outline The public outline API — mark entities to draw with a coloured silhouette rim. Its registry (entity id → `{ color, thickness }`) is read every frame by the `@builtin::renderFeatures.outline` render feature, which draws a screen-space jump-flood rim hugging each marked entity's silhouette (uniform width, no gaps on hard-edge or multi-part meshes). The feature is ensured live on first use, so `outline.set(entity)` is all a caller needs. An entity's outline lasts as long as the entity: when it is despawned, by `entity.despawn`, a parent's cascade, a scene rebuild or reload, its id leaves the registry, so the feature's per-frame cost follows the outlined entities in the world. A `Model` or `SkinnedModel` drawing an outline from its outline fields clears it when the component is removed. ```luau local outline = require("modules.outline") outline.set(entity, { color = { 1, 0.5, 0, 1 }, thickness = 3 }) outline.clear(entity) outline.clearAll() ```
▲ 0↑ born
▣
module · born here
❒asset
# material_schema A shader's declared property vocabulary, and the routing of an authored key onto it. A material's properties belong to its **shader**. The uniform buffer is packed by reflected field name, so a key the shader does not declare is never read by anything: it sits in the material's property table, reads back as though it applied, and contributes nothing to the render. This module is the single place that answers "is this key real, is it a texture slot, or is it neither" — so every path that accepts material properties gives the same answer. Consumed by `material.assetType` (the `.material` asset path) and by `renderer.material.create` (the runtime GPU path). ## API ```luau local schema = require("modules.material_schema") local s = schema.forShader("pbr") -- nil when the shader can't resolve schema.route(s, "base_color", {1,0,0,1}) -- "property", "base_color" schema.route(s, "color", {1,0,0,1}) -- "property", "base_color" (alias) schema.route(s, "base_color_texture", tex) -- "texture", "base_color_texture" schema.route(s, "baseColor", {1,0,0,1}) -- "unknown", "baseColor" schema.declaredNamesList(s) -- "alpha_cutoff, anisotropy, …" schema.undeclaredMessage(name, key, "pbr", s) schema.acceptsSlot(s, "base_color_texture") -- true: the shader declares the slot schema.acceptsSlot(s, "base_color") -- false: a property, not a texture slot schema.declaredSlotsList(s) -- "base_color_texture, emissive_texture, …" schema.undeclaredSlotMessage(name, slot, "pbr", s) schema.isTextureSlotType("texture") -- true schema.isTextureSlotType("texture_array") -- true schema.isTextureSlotType("color") -- false ``` A `properties.yaml` entry of type `texture` or `texture_array` declares a texture slot; every other type except `buffer` declares a property. A texture reaches the bind group only through a slot the shader declares, since the material interface is generated from those slots. `renderer.material.create`, `renderer.material.setTexture` and a material asset's `setTexture` / `writeTexture` refuse any other slot through `acceptsSlot` and report it with `undeclaredSlotMessage`. A schema that holds no names at all (a shader whose vocabulary has not been read yet) accepts every slot. ## Routing order `route` applies the alias table first, then decides by surface: 1. **Alias** — `color` → `base_color`, but only when the shader declares the canonical name and *not* the alias, so a shader that genuinely exposes `color` keeps its own meaning. 2. **Texture** — the schema declares the name as a texture slot, *or* the value is itself a texture (handle / `AssetRef<texture>`). A texture stored as a uniform property reads back via `getProperty` but never reaches the bind group, so the value's own shape decides regardless of the key. 3. **Property** — the schema declares the name as a uniform property. 4. **Unknown** — neither. Nothing reads it; the caller warns with `undeclaredMessage` and drops it. ## When the shader can't be resolved `forShader` returns `nil` for a shader that is not registered yet (a runtime shader still compiling, a name that resolves later). Callers stay permissive in that case rather than reject every key against an empty vocabulary — the check applies where the vocabulary is actually known.
▲ 0↑ born
▣
module · born here
❒asset
# session Session-scoped key-value state. The store lives in the module's environment, which loads once per engine process, so values survive VM reloads (play flips, hot reloads) and die with the process. Use it for state whose lifetime must equal the session's runtime artifacts (GPU resources, runtime materials, bake outputs), which outlive a world save and would leave stale claims otherwise. ```lua local Session = require("modules.session") Session.set("my_system_" .. entityId, handle) local handle = Session.get("my_system_" .. entityId) ```
▲ 0↑ born
▣
module · born here
❒asset
# bakeState Keeps the scene's baked lighting honest about the scene it was baked from. A bake is a photograph of a moment: move a wall, edit a static light, or delete a prop and every lightmap and probe field taken before that moment describes geometry that is no longer there. `BakedLighting.component` drives this module off the engine's authored change feed (`layers.onEntityChanged`). The bake attaches that component itself, so producing baked artifacts and maintaining them are one act. ## Invalidation is scene-wide Indirect light is global. One moved wall changes the bounce reaching every surface around it, so invalidating only the wall would leave its neighbours holding light that wall no longer casts. `invalidate` drops every lightmap, retires every probe field, and reinstates every withheld light row. That is also what makes the invalidation visible: the viewport goes from baked to realtime light the moment the edit lands, instead of continuing to show a stale photograph. ## What reaches a bake `affects(entityId)` answers it: | Edit | Reaches the bake | Reason | |---|---|---| | Static geometry moved / changed | yes | `geometry` | | Static light edited | yes | `light` | | A tracked receiver or light despawned | yes | `removed` | | Movable geometry, dynamic light | no | realtime, never baked | A despawned entity can no longer be read, so `dependents()` — refreshed after every bake and every invalidation — is the record of what the bake depended on. ## API - `BakeState.affects(entityId) -> (boolean, reason?)` - `BakeState.isBaked(entityId) -> boolean` - `BakeState.dependents() -> { string }` - `BakeState.hasBake() -> boolean` - `BakeState.refresh()` - `BakeState.hasMaintainer() -> boolean` - `BakeState.ensureMaintainer() -> boolean` - `BakeState.setLightsBaked(ids, baked) -> number` - `BakeState.invalidate(reason?) -> InvalidateReport` - `BakeState.rebake(opts?) -> RebakeReport` `rebake` recomputes the volumes already placed and the static receivers already there. Placing new volumes is an authoring decision the `baking` tools own.
▲ 0↑ born
◐
material · born here
❒asset
# Default The neutral fallback surface — a plain white, non-metallic PBR material with mid roughness (`0.5`), sampling the built-in white texture. This is what an object renders as when no material is assigned. Built on `@builtin::shaders.pbr`. ## Inputs - `colors.base_color` — the flat tint (white by default). - `floats.roughness` (`0.5`) / `floats.metallic` (`0.0`) — a neutral matte dielectric. - `textures.base_color_texture` — the built-in `default:white` texture.
▲ 0↑ born
·
assettype · born here
❒asset
# assetType (asset type) An asset type says what a kind of asset is: the files an asset of the kind is made of, the code that gives it behaviour, the interface a person uses to work with it, and how it is found, pictured and checked. A `<typename>.assetType/` folder declares a type called `<typename>`, and every `<name>.<typename>/` folder is an asset of it. `assetType` is itself a type, so a type is an asset like any other: it is created, versioned, installed and contributed the same way. ## Making a type `asset.create("assetType", "<typename>")` scaffolds a type. The type registers the moment its folder is written, anywhere under `/zero/source/`, and from then on `<name>.<typename>/` folders are its assets. An asset of a type that is not registered yet is not an asset, so make the type first. A type folder is a root: `~.x` in any file inside it names the type's own `x`, wherever the type is installed. Keep the type's whole implementation inside its folder and reach it that way. What a type folder must hold is set by this type's own `type.yaml`, the same way every type's `type.yaml` sets what its assets hold. `asset.validate` on a type reports anything missing, and the scaffold produces a folder that passes. ## What sets your type's schema (`type.yaml`) `type.yaml` is the structural contract every asset of the type is checked against. The keys below are the ones the engine reads; a key not listed here is ignored. | Key | What it controls | |---|---| | `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. | | `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. | | `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. | | `is_container` | Whether the suffix stays in the identities of what the asset holds. | | `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. | | `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. | | `is_text_default` | Whether the asset's files are text unless stated otherwise. | | `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. | | `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. | | `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. | | `files: { required, optional }` | The files an asset holds (below). | | `allow_unlisted` | `false` reports every file the type does not declare. | | `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. | | `settings:` | Typed per-asset settings (below). | | `inspector:` | The interface people use for the type and its assets (below). | | `preview:` | How an asset is pictured (below). | | `references:` | Names of other assets the type's files carry (below). | | `indexing:` | What search reads for each asset (below). | | `describe:` | How a missing description is generated (below). | ### The files an asset holds Each entry under `files.required` or `files.optional` is `{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches within one segment. Entries that share a `one_of_group` are satisfied by any one of them. `content` lists checks on the file's bytes: | Check | Passes when | |---|---| | `non_empty` | the file has content | | `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape | | `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML | | `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") | Each check takes a `message`, which is what the author is told when it fails. ### Settings `settings: { values, schema: { type | instance, export? } }` gives every asset typed settings. `values` names the file an asset's values live in; `schema.type` declares one schema for the whole type, and `schema.instance` names a file each asset declares its own in. `asset.create` writes the resolved values, and a write to the values file reaches `onChange` with what changed. ## The human side (`inspector.luau`) A type is used by people through its Inspector, and without one the type is usable only from code. The Inspector is the type's interface for a person: for a procedural graph it is the graph editor, for a kit it is the tracking view of what the kit holds and what is still missing. Writing it is part of writing the type. `inspector: { type, instance? }` in `type.yaml` names the files. `type` is the interface for the type itself and for its assets; `instance` names a file an asset may ship to add its own. Each returns: ```luau return { sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } }, replace = { contents = { ... } }, -- replace a default section by id order = { "graph", "contents" }, -- section order } ``` `build(ctx)` receives the asset and builds the section. A section's `title`, and the text its rows carry, is a string or an `i18n.text` value read from a catalog (`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType", ...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a type that names no inspector, or names one that does not load, reports it. ## Say what your type is for (`about.yaml`) Every type carries an `about.yaml` that says what the type is for, in three plain-language texts: ```yaml intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering." function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text." usage: "You choose it by name for your UI or your text, and the text appears in that typeface." ``` - **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes. - **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes. - **`usage`** is what a person or an agent does with one. Up to 1200 bytes. Write about the type as a whole, not one asset of it, and say what it does now. Agents read these texts to learn when to reach for the type and how; people searching find the type by them. The scaffold starts each text with `[[describe]]` and a note on what to write; a text that still starts that way counts as not written. `services describe` can write them. They show up in `asset.inspect` as `detail.about`, in `assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide, and at the top of the type's Inspector. ## Ask your assets to say what they are A type can require each asset to carry a description: a file, written by the author or generated, saying what the asset is and what it is for. Declare it with a `described` check: ```yaml files: required: - path: "README.md" kind: "file" content: - { check: "described", message: "Say what this widget is for and where it goes." } ``` A file that is missing, empty or still has a line starting with `[[describe]]` fails the check, and so does a YAML file whose named `keys` are missing or over their `max_bytes`, or a Markdown file missing a named heading. `asset.validate` names the asset, and a push that carries it is refused with `message`. `services describe` finds such assets and writes their descriptions. `describe:` steers what is generated: ```yaml describe: guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI." examples: - "<identity of an asset of this type whose description reads well>" ``` An example is an asset of your type whose description is a true, written one, never a placeholder and never the asset being described; with none, leave `examples` empty. A type whose assets the model cannot perceive (a sound, a motion) says so in `guidance`, and the description is drawn only from what the asset's own files state. Start the file unwritten in your `template/`: a heading and one line starting with `[[describe]]`, never example content, which every new asset would carry verbatim. A type whose assets say what they are through their own content (a mesh, a texture) declares no check. ## The type's code (`behavior.luau`) `behavior.luau` returns one table. These are the keys the engine reads from it; anything else on the table is the type's own. ### What the type's assets can do | Key | Shape | What the engine does with it | |---|---|---| | `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. | | `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. | | `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. | | `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). | | `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. | | `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. | Some `ref` names mean the same for every type, and the engine calls them: - **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below). - **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below). - **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe. - **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints. - **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode. Some names are answered for every asset before the type is asked, so a `ref` entry of the same name is never reached: `getSource`, `getBytes`, `getText`, `exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`. ### When the engine calls the type | Hook | Signature | When it runs | |---|---|---| | `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. | | `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. | | `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. | | `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. | | `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. | | `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. | | `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. | ### Settings hooks | Hook | When it runs | |---|---| | `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. | | `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. | | `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. | | `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. | | `settingsHealed(values, stored)` | On a read, to repair stored values. | | `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. | `creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options that are inputs to the type rather than settings. ### Reloading An edit to `behavior.luau` reaches existing assets with no restart. ## How an asset is pictured (`preview:`) | `kind` | The picture is | The type provides | |---|---|---| | `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` | | `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order | | `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` | | `custom` | whatever `ref.preview` returns | `ref.preview` | ```yaml preview: kind: surface mesh: sphere ``` `angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop and the framing, so an asset pictures the same in every world. A type with no `preview:` that defines `instantiate` still answers `ref:preview()`. A picture is rendered when something asks to view it (the editor showing the asset, `ref:preview()`), when previews are generated on request (`assets.generatePreviews`), and when the asset is staged. An edit alone renders nothing. The picture records what it was made from, so a stale one is told from a current one. ## The names your type's assets hold (`references:`) Where your type's own format or API carries the name of another asset, declare it here: ```yaml references: keys: # a top-level key of the type's files that holds a name, or a table of them - { key: "shader", type: "shader" } args: # an argument of a call, counting from 1 - { call: "g:node", position: 2, type: "procNode" } fields: # a field of the options table a call takes - { call: "fx.beam", field: "material", type: "material" } ``` `type` is the category the name resolves in. `asset.declareReferenceKey`, `asset.declareReferenceArg` and `asset.declareReferenceField` declare the same from a script. ## What search reads (`indexing:`) `indexing:` is how assets are found. Search reaches an asset only through what its type's `indexing:` names, and adds nothing of its own. That includes you: an asset indexed badly is one you cannot find later, when you are making something else and need it. A type whose assets are never looked for says `indexing: none`. ```yaml indexing: data: # text, read from the asset's files - role: identity extractor: verbatim source: { file: "README.md" } - role: interface extractor: symbols source: { files: ["init.luau"] } - role: implementation extractor: verbatim source: { files: ["init.luau"] } content: # the picture - role: identity modality: image source: { file: "preview.png" } facets: # what a query filters and re-ranks by file: ".metadata" rigged: { type: bool, from: { components: { contains: SkinnedModel } } } roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } } ``` **Roles.** Each `data` and `content` entry has a `role`, and a query matches in two slots: - **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is. - **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both. Ask what a person would recognise an asset of the type by, and what it does, and point each role at the file that says it. For text, that is the asset's description: the file your template starts with a `[[describe]]` block, which the author writes or `services describe` fills in (see "Ask your assets to say what they are"). **`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`. Never index a file name (`source: { field: name }`): one or two words carry nothing to match. **`content`.** `modality: image` embeds the picture itself, so a text query matches what the asset looks like. A type whose assets are looked at makes the picture its identity. **`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded whole, so keys nobody declared in it are still found. Each further key is computed for every asset and states a value for every one, including `false`: a file can imply "not rigged" but never say it, and a query separates rigged from unrigged only because the unrigged ones say `rigged: false`. A key is `{ type: bool | number | enum, filter?: range, from }`, where `from` is `{ file, field }` (a field of one of the asset's files, dotted for nesting), `{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the asset's entities carry). ## What blocks a push A push checks every asset it carries against its type. These refuse it: - a required file that is missing, a `content` check that fails, a `described` file not written yet; - an error from the type's `validate`. Unlisted files and warnings do not. A `described` file blocks only a push that changes the asset. When a push is refused, the message names each asset and what it lacks. `world.publishBlockers()` and `zm status` read the same list without pushing. ## Typing a result from the asset (`refShapes`) A `ref` method whose result depends on the asset (one entry per child folder, per row of a config, per control a map declares) can only be annotated as `{ [string]: Thing }`, which accepts any key. `refShapes` states the result for each asset instead: a function returning a Luau type expression for that asset, and the module whose types the expression uses. Read the asset; never run it. ```lua -- inputMap.assetType/behavior.luau M.refShapes = { activate = function(self): (string, string) local names = {} for _, control in ipairs(M.ref.controls(self)) do table.insert(names, control.name .. ": Handle") end if #names == 0 then return "", "" end return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme" end, } ``` With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported. A per-asset type applies where the call site names which asset: a component field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`. `asset.resolve(name, category)` carries only the category, and a name computed at runtime carries nothing, so calls on those keep the declared return. The type belongs to the call's result: index it there, or bind what you need from it there, and the names stay checked. `require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows what was published: `categories` (each type's method surface) and `returns` (per-asset results by identity, then method). An identity missing from `returns` was never published. Results are recomputed when an asset is written; at most 64 assets of one type are read. ## Putting an asset in a scene (`ref.instantiate`) A caller writes one piece of code against every instantiable type, so what `instantiate` returns is fixed: ```lua local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable") M.ref = { instantiate = function(self, target, opts) local root = Instantiable.root(self, target, opts) -- compose the asset under `root`, now return Instantiable.result(self, root, idMap) end, } ``` **In.** `target` is the entity the asset lands under, or onto for a hierarchy type; with none, the type spawns a new root. `position`, `rotation` (three numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the same for every type: `Instantiable.root` applies them to a root you make, `Instantiable.place` to one you adopt. Document any options of your own in the hook's `--!arg` docs. **Out.** `(root, idMap)`. `root` is the composed root, live when you return: build synchronously, never defer to a component's `awake`. `idMap` maps each original id to the spawned one, so a component that composes the asset again on every load can pass it back in and keep references into the composition. Every call goes through `Instantiable.result`, which refuses a root that is not live and turns a missing `idMap` into `{}`. ## Inspecting an asset (`ref.inspect`) `asset.inspect(ref)` builds a record every asset shares (`identity`, `name`, `guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then calls `ref.inspect(self)` for the type's own `detail`: ```lua M.ref = { inspect = function(self) -- read the asset's own files, never runtime state: inspect works on an asset -- that has not been compiled, uploaded or registered return { --[[ what makes this asset what it is ]] } end, } ``` Without it, the record is the shared part alone, and an agent inspecting the asset learns nothing of what it is. A hook that raises leaves `detail` empty and sets `record.warning`. ## Discovery - `asset.list("assetType")`: every registered type. - `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts. - `asset.resolve("<typename>", "assetType")`: the type itself.
▲ 0↑ born
·
assettype · born here
❒asset
# Module (asset type) A module is a reusable, stateless (or globally-stateful) Luau library that other code pulls in with `require()`. Modules are the unit of factored-out logic in a world: helpers, math libraries, format parsers, registries, shared algorithms. ## When to use one - You have logic you want to share across multiple components, services, scenes, or tools. - You have a registry, lookup table, or shared cache that callers all read from / write to. - You want a stable API surface (`M.fn`) other code can depend on. If the logic is bound to one entity, use a `.component`. If you want a named UI of fields rather than functions, use a `.preset`. ## Where it lives - Source: `/zero/source/.../<name>.module/` - Identity: the source path with `/` written as `.` and each segment's type suffix dropped — `/zero/source/demo/beats.module` is `demo.beats`, and `/zero/source/kit.package/beats.module` is `kit.beats`, the container's name kept and its `.package` suffix gone. `asset.create` returns this string, `asset.resolve(...).identity` reads it back, and the `require` forms below write it as `<identity>`. - Folder shape: - `init.luau` (or `init.lua`) — module body. **Required.** Returns a table (conventionally named `M`). - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** ## How to create one ```luau asset.create("module", "<name>") -- Creates: /zero/source/<name>.module/ -- init.luau (canonical `local M = {} … return M`) -- README.md (instance README template) -- `folder` places it in a subfolder of /source, and that subfolder -- leads the identity: `{ folder = "lib" }` produces `lib.<name>`. -- `into` authors it inside a resolved container (a package, a toolbox), -- whose own identity leads the same way: asset.create("module", "<name>", { folder = "lib" }) asset.create("module", "<name>", { into = asset.resolve("myPkg.package") }) ``` ## How it operates 1. **Registration.** Writing `init.luau` into a `.module/` folder indexes the asset and registers the module path with the resolver. 2. **Loading.** The first `require("<identity>")` runs the file's body and caches the returned table. Subsequent requires from anywhere return the **same table** — this is the shared-state lever for "module-local" registries. The body runs in an environment of the module's own: a name it declares without `local` is readable by the module's own functions and by no other script, so an export reaches its callers through the returned table and nothing else. Requiring a component's folder runs its chunk the same way, once, and its `update`, `start` and `public` stay in that environment; a component runs on an entity through `entity(id).component.add(name)`. 3. **Hot reload.** Editing the file re-runs the module body and folds what it returns into the table already loaded, so that table keeps its identity: a caller that captured it with `require` reads the new source's functions and values through the reference it already holds, and a later `require` returns that same table. A key the new source drops goes away; a key something set at runtime stays. The body's own `local`s start again from their initial values, so state that must outlive an edit lives in `modules.state()` (`guides { path: "core/scripting-and-tasks" }`). 4. **Identity resolution.** A module is reached by its `<identity>`, and where the caller lives decides which spelling of it resolves. For a module in a **world**, from a caller in that same world — another module, a component, a scene entrypoint, or an `execute` chunk: - `require("<identity>")` — the identity on its own, resolved against the caller's own root. It carries every folder segment, so a module created with `{ folder = "demo" }` is `require("demo.beats")`. - `require("@root::<identity>")` — the same root-relative resolution, written out. - `require(".sibling")` — a module in the same folder, by its bare name; `..name` steps up one folder, `...name` two. Logs, stack traces and hot-reload notices name that same module `@local.source.<identity>` — its key under the world source root. The `<identity>` tail of that key is the form above, so a trace reading `@local.source.demo.beats` is `require("demo.beats")`. For a module in a **library** — content under `/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's root from anywhere: `require("@builtin::modules.transform")`. Mounting a world as a library moves its files under such a root and flips the caller root to `@<lib>::`, which is what keeps the root-relative forms above resolving across the move. ## Discovery - `asset.list("module")` — every registered module. - `asset.inspect("<name>")` — public functions, source path, this type README. - `ls /zero/source/<name>.module` lists the files the folder holds. ## Authoring conventions - Return a single `M` table from `init.luau`. Top-level statements with side effects run on first require — useful for one-time initialization, dangerous if they touch the engine before it's ready. - Write exported functions as `typed function`, not `function`: the argument types are enforced at the call and the checker knows the signature, so a caller's mistake is reported where they made it. - Annotate exported functions with `--!desc` / `--!arg` / `--!return` / `--!example` so the LSP, `tools.list`, and `asset.inspect` surface them. A function with no `--!desc` is reachable only by someone already reading this file. - Declare a VALUE with `Field.<kind>(default, mode, description)`, and a table whose members are reached through a metatable with `--!members <TypeName>` — the latter gives the surface its accepted member set, its type, and its documentation from one table. See `guides { path: "core/authoring" }` for all four rules and `modules/api/engine/entity.module/members.module` for a worked member table. - Keep module state in module-local upvalues or in `modules.state()`. A name the body declares without `local` lives in the module's environment, which every run of the module shares: a function the previous run left in place reads what the reload assigned. Module-locals start again from their initial values with each hot reload; the table `modules.state()` returns is the same table on both sides of every reload. - Prefer focused modules over kitchen-sink modules. If two halves of a module have no shared state, split them. ## Common pitfalls - **Cyclic requires.** A `require` chain that loops will return the partial table (the half built before the cycle was detected). Design for one-way dependencies. - **Engine-time side effects.** Don't `entity.spawn` at module top-level — the engine may not be ready. Expose an `M.init` and call it from a scene entrypoint. - **Hot-reload + module-local cache.** A module that caches expensive computation in upvalues loses that cache on reload — fine for dev, but be aware in performance work. - **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical. - **Writing one while play runs.** A `/source` write made while play is running lands on the play shadow, and a guarded play-exit discards it unless it is kept. One `.module` write shadows `init.luau`, the `README.md` and the `.metadata` together. `vfs.write(path, bytes, { durable = true })` skips the shadow, and `vfs.promotePlayShadow` keeps what is already on it — the `core/vfs` guide has the model and what each route costs. ## Related types - `.component` — for entity-bound state + lifecycle hooks. - `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider. - `.tool` — for a single agent-callable function with a YAML schema. - `.package` — to group several modules + components + scenes into one shippable folder. ## In the Inspector Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
assettype · born here
❒asset
# Material (asset type) A material is a named, configured instance of a `.shader`. The shader defines the surface look (lighting model, available property fields, texture slots); the material picks values for those fields and a shader to bind to. Renderable entities reference materials, not shaders, so the same geometry can wear different materials and the same material can be reused across many entities. Entities sharing one material can still differ in a value: every drawn object carries four `vec4` lanes of its own that a surface shader reads as `input.shader_data[lane]` and `renderer.instanceData.set` writes. Use that for a dissolve at its own progress per subject, a hit flash at its own age, a fill level per instance — a material per entity is for when the LOOK differs, not when a number does. > New to how a material relates to its shader, its textures, and the live GPU > material behind it? Read `core/resource-model` first. It also covers the > difference between an authored `.material` asset (this type) and a runtime GPU > material made with `renderer.material.create`. ## When to use one - You want a named surface configuration an entity wears by referencing it from its `Model` component (`material = "@.../<Name>"`). - You want runtime property tweaks (`asset.resolve(ref):setProperty(...)`) to affect every entity using the material at once. - You want to publish a curated look (`Gold`, `RedGlow`, `BrickWall`) other authors can reuse without re-typing shader bindings. If you need a brand-new lighting model or a custom visual effect, you need a `.shader` first; the material wraps it. ## Where it lives - Source: `/zero/source/.../<Name>.material/` - Identity: `<Name>` (the `.material` suffix strips). - Folder shape: - `mat.yaml` — canonical material spec (shader + property values). **Required.** - `README.md` / `.metadata` — optional prose + tags, indexed when an author writes real ones. - `textures/` — optional folder for embedded textures. Validator allows the folder; its interior is unrestricted. - `shaders/` — optional folder for a co-located custom shader. Same rule. ## How to create one ```luau asset.create("material", "<Name>") -- Creates: /zero/source/<Name>.material/ -- mat.yaml (shader binding + property scaffold) -- `folder` places it in a subfolder of /source instead of the root: asset.create("material", "<Name>", { folder = "materials" }) ``` Pass the shader and initial values through `asset.create`, then tune with `setProperty` and keep what you tuned with `saveDefinition`: ```luau local mat = asset.create("material", "<Name>", { shader = "@builtin::shaders.pbr" }) mat:setProperty("base_color", { 0.8, 0.6, 0.1, 1 }) mat:setProperty("roughness", 0.3) mat:saveDefinition() -- writes the tuned values into mat.yaml ``` This mints the `.material` asset and returns a handle you tune with `setProperty`. A set is a runtime change: it reaches the GPU material and every entity wearing it, and `saveDefinition` is the call that writes it into `mat.yaml`, so a property driven every frame costs no file write and leaves the authored file as its author wrote it. A purely runtime `asset.create` lives only for the session. For an ephemeral, runtime-only GPU material that is not an asset at all, use `renderer.material.create` instead (see `core/resource-model`). ## How it operates 1. **Property reflection.** The bound shader's uniform struct fields become the material's configurable properties — no manual property declarations needed. 2. **Property values.** `mat.yaml` groups values by type, each block keyed by the shader's field names: `properties:` holds scalars and colors/vectors, `ints:` holds integer-typed fields, `bools:` holds boolean-typed fields, and `textures:` holds texture slots. Omitted fields take the shader's default. A scalar is a bare number (`roughness: 0.5`); a color or vector is an array (`base_color: [1, 1, 1, 1]`). `shader:` is the shader's identity string. This is the shape every material the engine writes uses. A `saveDefinition` writes the changed values into the file where they already stand and leaves the rest of it — its comments, the order of its keys and blocks, the spelling of its literals, untouched. 3. **Render state.** `render:` holds how the mesh is drawn rather than what the shader reads: `blend` (the equation the fragment composites with, also spelled `type`), `cull`, `depth_write`, `depth_compare`, `topology` and `queue` (the draw order, a whole number: `2000` opaque, `3000` transparent, `4000` overlay). The `.shader` type README's "The render block" section lists every value each key takes, and `asset.create`'s `render` argument takes the same keys and values. 4. **Application.** An entity wears a material by referencing it from its `Model` component (`material = "@.../<Name>"`). Materials affect every entity that references them; runtime `asset.resolve(ref):setProperty(...)` changes propagate to all of them immediately, and last until the next mode change unless `saveDefinition` writes them into `mat.yaml`. 5. **Texture binding.** Texture slots accept `color:r,g,b,a` for solid colors, `default:white` / `default:black` / `default:normal` for the engine's own fallback texels, `@builtin::textures.<name>` for builtins, `path/to/image.png` for file textures, or a render-target handle for camera renders. The GPU texture cache reads the `color:` and `default:` forms from the string itself, so a slot written in one names no asset and carries nothing with the material when it travels; every other form names a texture asset the slot depends on. A texture in `base_color_texture` is multiplied by `base_color`: every built-in surface shader declaring both draws the texture's colour times `base_color`, channel by channel. Under the default white `base_color` the texture shows as itself, and a coloured `base_color` tints it. The two read a colour differently: `base_color` is linear, and the slot reads its texels as sRGB-encoded, a `color:` value's included, the way it reads an image. A colour stated in both places is therefore multiplied by a darker copy of itself: `color:0.62,0.14,0.05,1` in the slot decodes to `0.34, 0.017, 0.0039`, so beside `base_color` `0.62, 0.14, 0.05` the surface draws `0.21, 0.0024, 0.0002`, far darker than either. State a solid colour once, as `base_color`, and leave the slot at its default white. 6. **Hot reload.** Editing `mat.yaml` repacks the GPU uniform buffer on the next frame; visible immediately. **Properties that follow a named value.** `values:` binds a property to a named value of the entity wearing the material (`values.set(entity, name, v)`, the `topics/values` guide): `property: valueName`, or `property: { value: "name", scale: s, offset: o }`, and the property draws `value * scale + offset` on each entity holding the value, read on the draw. A boolean reads as 1 or 0, and a number fills every channel of a colour or vector property. The wearer's own value is read, else its nearest ancestor's, so a lamp mesh under a car lights from the car's `brake`; a wearer holding none draws the material's own value. A scalar, vector, colour or bool property binds (at most 32 per material); a texture slot does not. `asset.create("material", name, { values = { ... } })` and `renderer.material.create({ ..., values = { ... } }, key)` take the same map. ```yaml values: emissive_intensity: { value: "brake", scale: 3 } ``` 7. **Inspector.** `type.yaml` names `inspector: { type: "inspector.luau" }`, and `inspector.luau` declares the material's sections beside the generic ones: `shader` (the bound shader, swappable), `properties` (each shader field as its typed control) and `textures` (each texture slot as an asset chip). They read the material through `mat:getShaderRef()` (the bound shader's ref, `~` expanded) and `mat:getTextures()` (slot name → `{ ref, name }`: the bound texture's guid and the id the material names it by). An edit there is an authoring write: `mat:writeProperty(name, value)` and `mat:writeTexture(slot, texture)` write the value into `mat.yaml` (and drop the session's own value for it, so the material reads what the file holds), and `setShader` rewrites the file; inside an undo operation each file write is recorded, so undo puts `mat.yaml` back. `setProperty` / `setTexture` stay runtime writes that last until the next mode change. ## Discovery - `asset.list("material")` — every registered material. - `asset.inspect("<name>")` — shader binding, property values, source path, this type README. - `ls /zero/source/<Name>.material` lists the files the folder holds. - `asset.resolve("<name>"):getProperties()` — the property fields exposed by the bound shader, each at the value it currently holds: what `mat.yaml` authored until something writes to the material, and the written value from then on, whichever call wrote it. That is the value the surface is drawn with. Read them back rather than guessing names; which fields exist comes from the shader. - `asset.resolve("<name>"):getDefinition()` — the authored `mat.yaml` bytes, the value the material rests at. - `asset.resolve("<name>"):getShader()` — the shader the material binds. ## Authoring conventions - Use lowercase, descriptive names (`brushed_steel`, `red_glow`, `tarmac_wet`). Avoid shader-name suffixes in the material name — the binding lives in `mat.yaml::shader`. - Drive every property the shader exposes; defaults are a fallback, not a contract for which fields exist. - Embed textures alongside the material (`textures/` subfolder) when they're used only by this material; reference shared textures from `@builtin::textures.*` or a library. ## Common pitfalls - **Property names must match the shader's uniform struct exactly** (case-sensitive). Misnamed properties silently get the shader's default. - **`base_color_texture` multiplies `base_color`.** A `color:` texture holding the same value as `base_color` multiplies the colour by its own sRGB-decoded value, so the surface renders far darker than the value written. Write a solid colour as `base_color` alone. - **Broken shaders break the material.** A material whose shader has a parse error renders as the magenta checkerboard. Inspect the shader first if the material looks wrong. - **`setProperty` targets the material, not the entity.** Changes affect every entity that references the material. - **`setProperty` is a runtime change.** It reaches the GPU, not the file: call `saveDefinition` to write the current values into `mat.yaml`. For a per-entity override that no other entity sees, give the entity its own copy with `Model:applySessionMaterial(renderer.material.create(...))`. - **Renaming the folder changes the identity.** A `Model` component or other reference to the material is recorded against the material itself, so renaming or moving it does not break them. ## Related types - `.shader` — defines what properties exist; required before a material can bind to it. - `.preset` — captures a component configuration; conceptually similar to a material, but for components instead of shaders.
▲ 0↑ born
▣
module · born here
❒asset
# luau_introspect (module) Parses Luau/asset source TEXT into structured declarations. Pure string/pattern parsing (`string.find` / `string.match` / `string.gmatch`) over the literal source — no compiling, no VM spawn, no runtime state, no engine FFI. Given source text, it extracts what is written in the file. This is the shared parser every per-assetType `M.ref.inspect` hook uses to turn a component/module/tool's own source into the structured record `asset.inspect` returns. ## Exports - `docstrings(src) -> DocMap` — scans `--!desc` / `--!arg` / `--!return` / `--!example` comment runs and binds each run to the name of the declaration on the next non-comment line (the identifier after `function`, `public:`, `local function`, or `M.`). - `publicFields(src) -> { FieldEntry }` — parses `public = { name = Field.<kind>(default, mode), ... }` table-literal entries and module-scope `public.<name> = Field.<kind>(...)` assignments. - `events(src) -> { EventEntry }` — parses `events = { name = Event(payloadSchema?, syncMode?), ... }` table-literal entries. - `methods(src) -> { MethodEntry }` — parses `function public:<name>(...)` / `typed function public:<name>(...)` declarations (params + optional return type). - `lifecycleHooks(src, catalog) -> { string }` — top-level `function <name>(` declarations whose name is in the caller-supplied `catalog` array. - `moduleExports(src) -> { ExportEntry }` — parses `M.<name> = function` assignments, `function M.<name>(...)` declarations, and a trailing `return { foo = foo, ... }` export table. - `forSource(src, computeFn) -> any` — memoises `computeFn(src)` keyed on `src` itself in a module-local table. The same source text returns the cached result without re-running `computeFn`; different source text recomputes. Keyed on the source content (not an asset checksum) because composite/container assets (component folders, scene folders, etc.) all hash to the empty-string checksum — a checksum-keyed cache collapses every such asset's detail onto whichever one was inspected first. ## Usage ```luau local Introspect = require("modules.luau_introspect") local src = self:getInitScript() local detail = Introspect.forSource(src, function(s) return { fields = Introspect.publicFields(s), events = Introspect.events(s), methods = Introspect.methods(s), hooks = Introspect.lifecycleHooks(s, lifecycleCatalog), } end) ``` ## Notes - Every function is a pure string→table transform: same source text in, same structured table out, every time. No reads of live component state, no entity/world queries. - Structural scanning runs over a comment- and string-aware **mask** of the source (a same-length copy where comment bodies and string-literal contents are blanked to spaces, delimiters and newlines preserved). Pattern matches and brace/paren balancing run on the mask — so a brace, `Field.`, `Event(`, or a whole entry that appears inside a comment or a string literal never registers — while captured values are sliced from the original source at the same (aligned) indices. This makes the parser robust to apostrophes in comments, commented-out entries, string defaults containing `}` / `Field.` / `Event(`, and `[[ ]]` long strings. - `default` / `category` are raw trimmed source-text slices (with any trailing comment stripped), not evaluated values. - For a **ref constructor** (`Field.assetRef` / `dataRef` / `resource` / `componentRef`) the FIRST positional argument is the category / contract / componentType selector, captured under `category`; the SECOND argument is captured under `default`. - For every **other** Field kind the first argument is the `default` and `category` is nil. - A trailing `Sync` / `NoSync` identifier is captured as `sync`, tolerating a trailing comma or a trailing comment after the last argument. - Field/Event entries whose key is a computed or indirect expression (`[expr] = Field.number(...)`) are omitted — only literal identifier keys are recognised. - `methods` captures a `...` variadic parameter (as `name = "..."`), with its `: <type>` annotation when present. - A `--!desc` written above a field/event declaration (`<name> = Field.<kind>(...)` / `<name> = Event(...)`) or a `public.<name> =` assignment binds to that name in `docstrings`, and is folded into the corresponding `publicFields` / `events` entry's `desc`. - `lifecycleHooks` never hardcodes the lifecycle-callback catalog; the caller passes the list of names to match against. - `forSource`'s memo is bounded (oldest-key eviction) so a long session touching many distinct asset versions can't grow it without limit.
▲ 0↑ born
▣
module · born here
❒asset
# assetType.shared.settings Resolves four questions about an asset's typed settings, all answered from its type's `type.yaml` — nothing here is found by convention. ```lua local Settings = require("@builtin::assetTypes.assetType.shared.settings") -- or, from inside the assetType runtime: local Settings = require("@builtin::assetTypes.assetType.shared").settings ``` ## Where does a type's schema live? `type.yaml` names it in a `settings:` block: ```yaml settings: values: "settings.yaml" # where an instance's own values live schema: { type: "settings.luau" } # one schema for every asset of this type ``` or, for a type whose settings differ per instance (an importer): ```yaml settings: values: "settings.yaml" schema: { instance: "settings.luau" } ``` or, for a type whose schema is a table its instance's own source declares and whose values live elsewhere (a component: its `public` fields, valued on each entity it is on): ```yaml settings: schema: { instance: "init.luau", export: "public" } ``` Whether an instance may lack its schema file is what the type's `files:` block says of that file. Listed under `files.optional`, it may be absent, and an instance without it declares no settings — the importer type lists it there, since most importers take none. Listed under `files.required`, or not listed at all, it must load, and an instance missing it is a broken declaration and is logged. `schema.type` is required at the folder relative to the TYPE's own `.assetType/` directory (one schema shared by every instance). `schema.instance` is required relative to each INSTANCE's own folder (`init` resolves the instance's own identity; anything else is a sibling module under it). Without `schema.export`, the file is required as a module and its return value IS the schema. With `schema.export`, the file is run in an environment of its own and `export` is read from a returned table carrying it, or else from the globals the file assigned — a component's `public` is such a global. That environment reads through to `_G` and takes every write itself; its own `_G` is itself, its `declare` and `computed` record into it, and its `task` records spawned, deferred and delayed work instead of starting it. Every other global is the real one, so a call the file makes through one runs as it does anywhere. The schema is the declared table's Field descriptors. A schema that resolves to nothing says why through `Settings.schemaProblemOf(ref)`: the file could not be read, compiled or run, or declared no such table. An instance schema is read again when its file, its type's `type.yaml`, or the entry file of any module the asset `require`s changes. `Settings.blockFor(typeName)` returns the raw block; `Settings.schemaForType(typeName)` and `Settings.schemaOfInstance(ref)` resolve the two schema shapes respectively; `Settings.schemaFor(ref)` tries the type-level schema first, falling back to the instance-level one. `ref` naming an assetType asset itself — e.g. `asset.resolve("texture", "assetType")`, rather than an instance of it — reports that type's own declared schema, the same one every instance of it reports. ## Where do an asset's values live? `settings.values` names a file living beside the asset's own content — `Settings.valuesPathOf(ref)` joins it onto the asset's path. `Settings.valuesOf(ref)` / `Settings.writeValues(ref, values)` read and write it; this module only names where it is. A type that declares its schema with no `settings.values` keeps no values in the asset — a component's live on each entity it is on — so `valuesOf` answers nil for it, `asset.create` refuses a `settings` table for it, and `asset.setSettings` refuses a write. `Settings.recordedWrite(path, text)` writes a file the way every values file is written, and an editor uses it for any other file it writes. It writes the whole contents, records the write into the open undo operation when one is open (so undoing it restores what the file held), and raises when the write does not land. ## In what order are a schema's fields declared? Every `Field.*` constructor stamps its descriptor with `declaredOrder`, one more than the last. Luau evaluates a table constructor's entries in the order they are written, so `Settings.orderedNames(schema)` lists a schema's fields — and a struct's subfields — in the order the source writes them. A values file, or a legacy `.metadata` settings block, may hold an older spelling — a renamed field, an older enum member. Every read passes what is stored through the type's `normalizeSettings` (`Settings.normalizeFor`), so it answers in the fields the schema declares today, and writes nothing. The next local edit of that asset rewrites the file in the declared spelling (`Settings.healLegacyIfNeeded`). `asset.setSettings(ref, patch)` writes a partial patch: `Settings.resolvePatch(ref, patch)` normalises it, merges it over the current values and applies the type's derived defaults for the fields it changed, and `Settings.writeValues(ref, values, patch)` applies the corrections, checks the schema and writes the file. ## How is a new asset's settings resolved at creation? `asset.create` calls `Settings.resolveForCreate(typeName, name, destPath, creationOpts, callerSettings, importerSettings)` before running the type's `onCreate`, checks the result against the schema, writes it to the values file, and hands `onCreate` a COPY of it as `opts.settings` (`Settings. cloneValues`) — never the same table the values file gets, so nothing `onCreate` does to its own copy can reach what was stamped. `onCreate` reads settings from `opts.settings` and never writes to it. `asset.create` also seeds the change baseline with the stamped values (`Settings.seedChangeBaseline`), so the values file's own first dispatch reports nothing changed and the payload `onCreate` encoded is the only encode a create runs. The layers, lowest priority first, each overriding the ones before it field by field: 1. The schema's own declared defaults (`Settings.defaultsOf`). 2. The type's own `settingsDefaults(name, opts, asked)` hook, when its `behavior.luau` exports one. `asked` holds what layers 3 to 5 name, so a default can follow a field one of them sets. 3. The Preset Manager's matching row (`Settings.presetLayerFor`). 4. The creating importer's settings for this type. 5. The `settings` table the caller passed to `asset.create`. 6. The type's own `settingsDerivedDefaults(values, explicit, changed)` hook, when its `behavior.luau` exports one — a field whose default follows another field, set only when no layer from 2 to 5 named it. 7. The type's own `settingsCorrections(values, name, opts)` hook, when its `behavior.luau` exports one — applied last, once every other layer has merged. Layers 2 to 5 are each read through the type's `normalizeSettings`, so a layer naming a field in an older spelling sets the field it names today. `asset.create`'s top-level settings alias accepts those older names too (`Settings.acceptedFieldNames`). Layer 7 is `Settings.applyCorrections(typeName, values, explicit, context)`, the one corrections path every settings write takes — this create chain, and `Settings.writeValues` for every later write (`asset.setSettings`, a type's first-encode seed, the legacy heal). `explicit` names the fields the writer asked for: the caller's `settings` table here, the patch for `asset.setSettings`. A correction lands SILENTLY on a field the writer did not name — nobody asked for that value, so overriding it just states a fact. A correction that contradicts a field the writer named REFUSES the write, naming the field, the value asked for, and why it cannot hold — the same as every other place this codebase refuses an explicit request it cannot satisfy (a `Field.checkSchema` violation, a BC7 alignment error) rather than silently rewriting it into something else. So `asset.setSettings(tex, { compression = "rgba16" })` on a mipped texture stores `generateMipmaps = false` beside it, while `asset.setSettings(tex, { compression = "rgba16", generateMipmaps = true })` is refused. `Field.checkSchema` runs once, over the result AFTER the corrections hook has applied — a bad value nothing corrects refuses the creation. ### Four hooks, four different jobs Every hook is an optional export on a type's `behavior.luau`, alongside `onCreate` / `onChange`. **None is ever handed a table to mutate** — each reads what it's given and RETURNS a table of what it wants changed (or nil); `resolveForCreate`, `resolvePatch`, `applyDerivedDefaults` and `applyCorrections` are the only places that write to the values, by merging what a hook returns the same way every other layer merges. - **`normalizeSettings(values) -> values`** maps every older spelling a stored file or a write may carry to the field and member the schema declares now, returning a new table; a table naming both an older field and its replacement keeps the replacement. The companion export **`normalizedSettingNames = { ... }`** lists the older field names it accepts beyond the schema, so a write naming one is not refused as an undeclared field. `texture.assetType`'s maps the fields its earlier schema stored to `colorSpace` + `compression`, and the older `filter` keywords to `linear` / `nearest`. - **`settingsDerivedDefaults(values, explicit, changed) -> partial?`** answers the values of fields whose default follows another field. At creation `changed` holds every field; on a write it holds the fields the write moved. A returned field applies only where `explicit` — the fields the writer named — does not name it. - **`settingsDefaults(name, opts, asked) -> partial?`** supplies a DEFAULT the schema itself has no syntax for — something only knowable at creation time, like a naming convention, a fact read off a payload the caller handed in, or a value that follows a field a higher layer sets (`asked` holds what the Preset Manager, the importer and the caller name, the higher one's value where two name a field). It runs early (layer 2 above), so any later layer — Preset Manager, importer, caller — freely overrides it. `texture.assetType`'s returns the `textureType` the name suggests (a `_normal`-suffixed name is a `normalMap`), `compression = "uastc"` and `maxDimension = 4096`, with `rgba8` for a `textureType` of `sprite` that a higher layer names; for a pre-encoded ZTEX payload it returns the colour space, compression and sampler the payload's own header carries. Either way, an explicit `settings = { ... }` from the caller still wins. - **`settingsCorrections(values, name, opts) -> (partial?, reasons?)`** enforces a CROSS-FIELD CONSTRAINT the schema itself has no syntax for — a fact about one field that depends on another field's resolved value, true regardless of which layer produced that value. It runs last (layer 7 above), over the fully merged `values`, and again on every later write of the values file. Nothing is left unmet in what gets validated and stamped — but a value the caller explicitly asked for is never silently rewritten to satisfy it; `applyCorrections` refuses instead (see above). The optional second return, `{ [field]: reason }`, supplies the "why" for that refusal message; a field with no reason falls back to a generic one. `texture.assetType`'s returns `{ generateMipmaps = false, maxDimension = 8192, colorSpace = "linear" }` whenever the RESOLVED `compression` is high-precision (the encoder stores one verbatim, linear, and refuses a mip chain or a downsample for it outright), paired with a reason naming the compression, and `nil` otherwise. ## What does a declarer declare? `Field.settingsOf(identity, mode)` embeds another declarer's schema as a struct field without restating it. The identity names one of two kinds of declarer, and `Settings.schemaOfDeclarer(identity)` tells them apart by what `identity` resolves to: - **An assetType** (e.g. `"@builtin::assetTypes.texture"`) — embeds the schema every instance of that type carries, resolved via `schemaForType`. This is the type-level shared schema from the section above, not a per-instance one — an assetType has no settings of its own to declare per instance. - **Anything else** (an importer, or any other asset that declares settings of its own) — embeds that asset's own schema, resolved via `schemaOfInstance`. Every schema this module serves has already run its `Field.settingsOf` fields through this resolution — a caller reading `schema.someField.fields` never sees an unresolved `declarer` with no `fields`. Resolution descends into an already-literal struct's `fields` and a list's `element` the same way, so a declarer embedded several fields deep is still reached. A declarer that embeds itself, directly or through another declarer, stops one level short of the loop instead of recursing forever. ## What are the defaults? `Settings.defaultsOf(schema)` reads every field's declared `default` into a plain table — a field declared without one is absent from the result rather than holding a `nil` value.
▲ 0↑ born
◇
component · born here
❒asset
# AreaLight A rectangular light source on an entity. The rect is centred on the entity's world position, faces along the entity's world rotation applied to `face` (default "Front", the entity-local -Z that `transform.forward` reports and that `entity:lookAt` aims), and spans `width` by `height`. A rect under a carrier moves and turns with it. Its light arrives from a surface rather than a point, so shading falls off gradually across a wall, highlights stretch to the shape of the source, and shadows carry a penumbra that widens with the rect's size and the caster's distance.
▲ 0↑ born
◇
component · born here
❒asset
# BoxCollider (component) A box collision shape. Works with or without a `Physics` component: on its own it is static collision geometry, and alongside one it moves with the body. ## Sizing `half` is the box's half-extents in the entity's local space, multiplied by the entity's world scale. Leave it unset and the box derives from the entity's model bounds, so a bare `BoxCollider` matches what is rendered at any scale. ```lua entity.find("crate").component.add("BoxCollider", {}) -- fits the mesh entity.find("wall").component.add("BoxCollider", { half = { 2, 4, 0.2 } }) -- authored ``` ## Fields | Field | Meaning | |-------|---------| | `half` | Half-extents. Unset derives from model bounds. | | `isTrigger` | Report overlaps without blocking. | | `friction` | Surface friction. | | `restitution` | Bounciness. | | `rollingResistance` | How strongly the surface resists a body rolling on it: a rolling body slows at this many times gravity on level ground, so a ball rolled at v stops after v^2 / (2 * rollingResistance * 9.81). 0 rolls on. | | `spinningResistance` | The same for a body spinning on the surface about the contact normal. 0 spins on. | | `surface` | The surface the collider is made of, by name: one `Physics.defineSurface` defined gives it its friction, restitution and resistances in place of the fields above, and contacts, contact events and ray and shape casts report the name either way. Empty names none. | | `oneWay` | The direction, in the collider's own frame, bodies pass through it along: a body moving that way goes through, and a body meeting it from the side the direction points to stands on it (a jump-through platform is `{0, 1, 0}`). `Physics.passThrough(entity)` drops a standing body through. Unset blocks from every side. | | `surfaceVelocity` | The velocity, m/s in the collider's own frame, its surface moves at along itself while the collider stays put: friction carries what rests on it (a conveyor belt, a treadmill, an escalator). Write it at runtime to change the speed or direction. Unset holds the surface still. | | `contactTag` | The tag the world's contact filter reads this collider by: `Physics.setContactFilter(tagA, tagB, mode)` makes two tags pass through each other (`"ignore"`), report contacts and push nothing (`"sensor"`) or collide (`"solid"`). Empty names none. | ## Composing A collider on a child entity attaches to the nearest ancestor carrying a `Physics` component, so a body made of several shapes puts each on its own entity and each keeps its own material and trigger flag. Past a few dozen boxes sharing one material, `CompoundCollider` is cheaper: it is a single broadphase volume where separate components are one each.
▲ 0↑ born
▣
module · born here
❒asset
# assetType runtime The machinery that reads a `<typename>.assetType/behavior.luau` and turns its declarations into live behaviour. A `behavior.luau` is a table of declarations — `ref` methods, `modules` shared code, an `events` schema, a `namePattern`, `refShapes`, and the `onCreate` / `onRegister` / `onChange` / `onDelete` / `validate` / `settingsDefaults` / `settingsCorrections` hooks. None of it runs itself. This is the code that runs it, and it lives here because reading a type's declarations is the **assetType type's own behaviour** — the same way `material.assetType` owns what a `.material` does. ## Sub-modules | Module | Reads | Responsibility | |---|---|---| | `ref.module` | `ref`, `modules` | Builds every `AssetRef` envelope, attaches the shared metatable, and dispatches per-type methods and shared modules onto it. Also owns edit-mode write-behind persistence. | | `create.module` | `onCreate`, `namePattern`, `validate` | Backs `asset.create`: resolves the type, checks the name against its pattern, runs `onCreate` (or clones `template/`), and writes the instance. `validate.module` beside it checks a creation `opts` table against the type's `createSchema`. Before running `onCreate`, resolves the type's settings chain (below) and hands the result to the hook as `opts.settings`. | | `settings.module` | `settings`, `settingsDefaults`, `settingsCorrections` | A type's declared settings schema and where its values live (`type.yaml`'s `settings:` block). `Settings.resolveForCreate` layers the schema's own defaults, the type's own `settingsDefaults(name, opts, asked)` hook (a DEFAULT the schema can't express — a name-derived convention, a fact read off a payload the caller handed it, a value following a field `asked` shows a higher layer naming; any later layer overrides it), the Preset Manager, an importer's settings, the caller's `settings` table, then the type's own `settingsCorrections(values, name, opts)` hook (a CROSS-FIELD CONSTRAINT the schema can't express, applied last over the fully merged values). A correction over a default/preset/importer value lands silently; a correction that would override what the caller's own `settings` table explicitly asked for refuses the creation instead, naming the field and why. Neither hook is ever handed a table to mutate; each returns what it wants changed (`settingsCorrections` may also return a `{ [field]: reason }` table for that refusal message). `Settings.cloneValues` deep-copies a values table — `create.module` hands `onCreate` a copy, never the table it stamps. `Settings.valuesOf` / `writeValues` read and write an asset's own values file. | | `typeYaml.module` | `type.yaml` | The folder a type lives in and its parsed `type.yaml`, decoded once per content version of the file. `settings.module` and `inspector.module` read their blocks through it. `about` / `aboutAt` read a type's `about.yaml` (`intent`, `function`, `usage`), leaving out any key that is empty or still starts with `[[describe]]`; `aboutRows` and `aboutMarkdown` lay the texts out for the Inspector and the `types/<name>` guide. | | `inspector.module` | `type.yaml`'s `inspector:` block | Which inspector a type declares for its assets (`inspector: { type: ... }` or `{ instance: ... }`), and the declaration that file returns — the sections it adds, the generic ones it replaces, and their order — or the problem that kept it from loading. | | `onRegister.module` | `onRegister` | Fires an instance's once-only registration hook — on arrival, on world load, and for `@builtin` library instances. | | `changeDispatch.module` | `onChange`, `onDelete`, `refShapes` | Routes a VFS source write or a folder delete to the enclosing typed asset's type hooks. | | `refShapes.module` | `refShapes`, `ref` | Publishes what every `AssetRef<category>` answers to, so a member read on an asset-typed value is checked against that category's real surface. | | `events.module` | `events` | The per-asset event runtime behind `ref.events`. | | `instantiable.module` | `ref.instantiate` | The instantiation contract an asset type opts into: `root` stands the root entity, `place` applies the base placement opts to one the type adopted, and `result` shapes the `(root, idMap)` every `instantiate` returns. Also registers the `sceneInstantiable` field-constraint validator. | | `declaration.module` | `type.yaml` | The type's decoded declaration, read once and held until a write inside an `.assetType` folder; what every question about a type as a whole reads. | | `previewRecipe.module` | `type.yaml` `preview:` | How an instance's `preview.png` is produced: which of the four recipes (`instantiate`, `image`, `surface`, `custom`) the type declares, and what that recipe needs. | ## Reaching it ```luau local rt = require("@builtin::assetTypes.assetType.shared") rt.ref.loadTypeBehavior("material") ``` Fields resolve lazily. The prelude requires the sub-modules directly, in dependency order (`ref`, then `changeDispatch`, `onRegister.install()`, `refShapes.install()`) — a table that required all six on load would take that ordering away from it.
▲ 0↑ born
backing path · components/Model.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.