Log inGet started
·
assettype · drop-in viewer
asset⌬ assettypeassetTypeprimary: behavior.luau·originates fromworld 07158574-5…

assetType.assetType

The `assetType` is the **type of types** — the meta-type that every `<typename>.assetType/` folder is an instance of, including itself. A `<typename>.assetType/` folder declares a new asset type called `<typename>`: its on-disk shape, its identity rule, and the structural contrac…

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

assetType (asset type)

The assetType is the type of types — the meta-type that every <typename>.assetType/ folder is an instance of, including itself. A <typename>.assetType/ folder declares a new asset type called <typename>: its on-disk shape, its identity rule, and the structural contract instances of the type must satisfy.

This type is self-hosting. assetType.assetType/ conforms to its own schema — a type.yaml plus a README.md — so the type system is defined in terms of itself rather than a hidden engine special-case (the same way a C compiler is written in C). Every asset in the engine, including each .assetType definition, now resolves to a registered assetType: a .material resolves to material.assetType, and material.assetType resolves to assetType.assetType, which resolves to itself.

Why it exists

Assets used to be mapped to their type implicitly, by matching the folder suffix against a name-keyed registry. That match carried no stored link: nothing on a Foo.material recorded which material.assetType it was written against, so two worlds shipping a same-named type could silently disagree. Making assetType a real, resolvable asset closes that gap — the link from an asset to its type is now an explicit reference pinned in the asset's .refs sidecar (via: "asset_type"), exactly like every other dependency.

Where it lives

  • Source: /zero/source/.../<typename>.assetType/
  • Identity: <typename> (the .assetType suffix strips from the identity; the folder retains the suffix on disk).
  • Folder shape:
    • type.yaml — the structural spec the validator reads. Required.
    • README.md — type-level documentation. Required.
    • behavior.luau — optional behavior/scaffolding module.
    • template/ — optional canonical placeholder body the templater copies into a new asset of the type.
    • <name>.module/ — optional shared code the type ships to every instance of it, conventionally shared.module/ (see below).

Shared code its instances reach (asset.containing + .modules)

A type ships code every instance of it uses by declaring the modules in its own behavior.luau, as a map of tracked requires:

-- inside <typename>.assetType/behavior.luau
M.modules = { shared = require(".shared") }   -- the sibling shared.module/

An instance reaches it with one location-independent line:

-- inside any foo.<typename>/init.luau
local api = asset.containing(__FILE__).modules.shared

__FILE__ is the chunk's own VFS path; asset.containing resolves the calling asset, and .modules.<name> follows the instance's pinned typeRef (the type's guid, recorded in the instance's .refs as via = "asset_type") to the module its type declares. Because resolution follows that link rather than a category name, the same line in an instance of a different type resolves to that type's module, and identically-named modules in two types never collide. asset.typeRef(target) reads that guid for any target; asset.resolve(asset.typeRef(target)) gives the full AssetRef<assetType>. See assetTypes/README.md § "Shared code: type-owned modules" for the full mechanism.

How to create one

Authoring a new asset type is just creating a <typename>.assetType/ folder under /zero/source/. The engine registers <typename> as a category the moment the folder is written — no engine restart, no Rust change, and it works for built-in (@builtin/assetTypes/) and user-defined types identically. Once <typename> is registered, <name>.<typename>/ folders are recognised as assets of that type and resolve their asset_type link back to this definition.

Creating a <name>.<typename>/ folder before its <typename>.assetType/ exists fails fast: the suffix has no registered type, so the folder can't be a valid asset. Author the type first.

The behavior.luau contract

behavior.luau is the type's code. It returns ONE table, and the keys below are the ones the framework reads off it for EVERY type — the surface you get by declaring them. Anything else on the table is your own: reachable by name through require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("<typename>") for a system that knows your type (computeShader publishes dispatchKey that way), and never called by the framework.

local M = {}
M.ref     = { ... }   -- methods on every AssetRef of this type
M.modules = { ... }   -- shared code the instances reach
M.global  = {}        -- reserved key; ship the empty table
function M.onCreate(name, opts) ... end
return M

The tables

KeyShapeWhat the engine does with it
M.ref{ [name] = function(self, ...) }Every function becomes a method on every AssetRef of this type: ref:name(...), with self the ref. This is how a type gives its instances an API. Two names in here are contracted — see below.
M.modules{ [name] = require(".sibling") }Shared code the type's instances reach through asset.containing(__FILE__).modules.<name>, resolved by the instance's pinned typeRef guid. See the section above.
M.refShapes{ [method] = "TypeName" }Declares the result type of a M.ref method so the LSP can check what a call site does with it. See the M.refShapes section below.
M.eventsan events schemaThe events an ASSET of this type fires, declared the way a component declares its own. Readers subscribe through ref.events.<name>:connect(...); firing authority stays with the type. Keyed by the asset's guid, so every resolver of the same asset shares the signals and a re-resolved ref re-attaches to subscriptions already there.
M.namePatterna Lua pattern stringThe name shape asset.create enforces for instances of this type, replacing the default ^[A-Za-z][A-Za-z0-9_]*$.
M.global{}Reserved. Ship the empty table.

The lifecycle hooks

Each is optional; a type that omits one costs nothing. The engine calls them:

HookSignatureWhen it fires
M.onCreate(name, opts) -> { [filename] = contents }On asset.create("<type>", name, opts). Returns the files that scaffold the new instance, as a map of relative path to contents; the framework writes them. The opts type annotation is the schema asset.create validates the caller's arguments against, so annotate it.
M.onRegister(self)Exactly once per instance, the first time it registers — on engine.onWorldLoaded for instances already in the world, and immediately on a live asset.create. Guarded by the ref's shared runtime table, so a double trigger never double-registers. This is what makes "write an asset into the world and it takes effect live" work for content that registers into a runtime registry. The @builtin library is excluded from the sweep.
M.onChange(ref, change)On every VFS write to a file INSIDE one of this type's instances. change is { path, asset, type, kind, origin }kind is "edited" or "seeded", origin is "local" or "remote" (an importer runs on the originator only). Filter on change.path: the hook fires for any file under the folder, so act on the one you care about. It runs SYNCHRONOUSLY and a re-entrancy guard suppresses writes back into the same asset inline — but the guard does not span asynchronous work, so the hook must be convergent on its own: diff the meaningful state and short-circuit while your own regeneration is in flight.
M.onDelete(ref)When an instance of this type is deleted.
M.validate(assetRef) -> { { code, message, severity? } }On asset.validate, after the structural type.yaml check, for the type's own SEMANTIC validation. severity defaults to "error"; error-severity problems flip ok to false, warnings do not. A hook that raises or returns a non-table is itself reported as a validate.hook_failed error. world.push runs this per user asset, so declaring it enforces your type's rules at publish time with no further wiring.

The two contracted M.ref names

Most M.ref methods are your type's own surface: name them what you like, return what you like. Two are read by the engine and mean the same thing for every type, so their shape is fixed:

  • instantiate(self, target?, opts?) -> (root, idMap) — makes an instance part of the scene. Defining it is the whole opt-in: ref:canInstantiate() is true exactly when it exists. Its return is a contract — see the next section.
  • inspect(self) -> detail — the type-specific half of asset.inspect. See its section below.

Names a type cannot shadow

Some keys resolve on every AssetRef before per-type dispatch, so an M.ref entry of the same name is never reached: canInstantiate, getSource / getBytes / getText, exists, deps, meta, runtime, events, modules, typeRef, and the residency flags has_backing_asset, has_runtime_changes, cpu_resident, gpu_resident. These are the behaviours every asset must expose identically, which is why they win.

Reloading

A behavior.luau edit ripples to existing refs with no restart: dispatch reads mod.ref fresh each time and require's cache is re-run in place. A type whose folder appears at runtime is picked up on the next dispatch — negative results are not cached either.

What search does with your type (indexing:)

A type declares what search embeds for its instances. This is not optional and there is no useful default: ZeroMind runs your declaration and adds nothing of its own, so anything you do not point at is absent from the index — silently, and for every instance of the type forever. Twenty types once shipped with no indexing: block and every instance of them was unfindable.

Two words carry the whole thing, and they are the same two the search tool exposes:

  • identity — what an instance IS. For anything you can look at, that is the picture (content: + modality: image, matched directly by a text query). For everything else it is the authored text that says what it is.
  • capability — what it DOES and how it is made. Code, settings, the model-written summary of them.

A type with no behaviour has no capability entry. A type with nothing to look at has no image. Leaving a slot empty is a statement; filling it with whatever happens to be lying around is not.

The one you have to decide when authoring a type is: for an instance of this, what is the thing a person would recognise it by, and what is the thing it does? A texture answers "the image" and "its compression settings". A module answers "its README" and "its code". Write those two answers into indexing: and the rest follows.

Three specifics worth knowing before you write one:

  • derive is instructed by this type's own README.md. A model reads an instance's source and writes a description; what it embeds is the description, not the source. It follows your README to know what it is looking at, so a type whose README says what its instances are gets good derivations for free. There is no prompt to name.
  • derive loses detail. It keeps the main technique and drops secondary ones. If the source is code you want searchable by its own vocabulary, declare the same files a second time as capability with extractor: verbatim. Two entries, same role, different failure modes.
  • Never source: { field: name }. A filename is one or two words with no usable embedding, and a corpus indexed that way answers "language runtime" with say_runtime_2.soundClip.

facets: is the second stage: a file: embedded whole (.metadata, so keys nobody declared still get indexed) plus computed keys that state a value for every instance including false — the raw file can imply "not rigged" and can never say it.

The full schema, every extractor and the facet sources are in assetTypes/README.md; the scaffolded block with inline guidance is in template/type.yaml.

Typing a result from the instance (M.refShapes)

Every method on M.ref is checked against the type its --!return names, and every asset of the type gets the same one. That is right for most methods and wrong for the ones whose result is shaped by the ASSET: a method answering one entry per child folder, per row of a config, per binding an input map declares. The widest true annotation for those is { [string]: Thing }, and an indexer accepts every key — so a caller's typo reads as valid and nothing reports.

M.refShapes lets the type state the result per instance. Each entry is function(self) -> (typeExpression, source?): a Luau type expression for THIS asset, and the module whose type vocabulary the expression uses. Read the asset; never run it — stating a type must not take effect.

-- inputMap.assetType/behavior.luau — the shipped example.
-- `activate()` hands back one handle per control the map declares.
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) resolves and map.noexisting is reported with the map's real controls. Without it, both pass silently.

What the call site has to say

A per-instance type is matched by the asset's IDENTITY, so it applies only where the reference the method is called on says WHICH asset. Two spellings do:

-- a component field, whose declared default names the asset
public = { map = Field.assetRef("inputMap", "@builtin::inputMaps.default", Sync) }
local controls = public.map:activate()   -- typed for THAT map

-- an annotation, naming category and identity
local m: AssetRef<"inputMap", "@builtin::inputMaps.default">

asset.resolve("<identity>", "<category>") carries the CATEGORY, so the methods the category defines are checked on the result — but not which asset it found, so a per-instance result keeps the category's declared return. A reference that names no asset (asset.resolve(someVariable), a value passed in as a parameter) carries neither, and calls on it are unchecked. That is the same rule everywhere: the checker states what the code states, and a name computed at runtime states nothing.

The result is a value, not a binding

The type belongs to the expression, so it travels the way any inferred type travels. Indexing the call's result is checked; stashing it in a module local and indexing it from another function is not, because the local's declared type is what carries across — and any (or no annotation) carries nothing:

-- checked: `d.greting` is reported
function awake()
    local d = public.dialogue:lines()
    d.greeting:say()
end

-- NOT checked: `lines` is a module local typed `any`
local lines: any = nil
function awake() lines = public.dialogue:lines() end
function start() lines.greting:say() end

Bind what you need at the call site and the names stay checked in both places — the asset-derived type at the boundary, ordinary scoping after it:

local lineGreeting = nil
function awake()
    local d = public.dialogue:lines()
    lineGreeting = d.greeting        -- `d.greting` is reported here
end
function start() lineGreeting:say() end

When nothing is reported and you expected something

From a call site, a type that is correct and a type that was never published look the same: no diagnostic either way. require("@builtin::assetTypes.assetType.shared.refShapes").published() answers which it is. It returns two maps — the category surfaces, and the per-asset results keyed by identity and then by method:

local shapes = require("@builtin::assetTypes.assetType.shared.refShapes").published()

shapes.categories.dialogue
--> "{ lines: () -> any, lineNames: () -> { string } }"

shapes.returns.Shopkeeper.lines
--> "{ browsing: Line, farewell: Line, greeting: Line, wares: Line }"

An identity absent from returns was never published for, which is a different thing from published-and-correct — and the reason to look here rather than at the call site.

The answers are recomputed when an instance is written, so a control added to a map reaches the checker with no restart. Only types that declare refShapes are read per instance, and at most 64 assets of one type are — past that the type keeps its declared return and the engine log names what was dropped.

The instantiate hook (M.ref.instantiate) — a contracted return

A type opts into becoming part of the scene by defining instantiate on its ref table. That is the whole opt-in: ref:canInstantiate() is true exactly when the hook exists, so consumers offer a scene path — an Asset.source field, a viewport drop, a tool argument — by CAPABILITY rather than by a list of type names.

Unlike inspect, this hook's return is a contract. A caller writes one piece of code against every instantiable type, so what comes back cannot vary by type:

-- behavior.luau
local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")
local M = {}

M.ref = {
  instantiate = function(self, target, opts)
    local root = Instantiable.root(self, target, opts)   -- IN:  the base opts
    -- ... compose whatever this type is, under `root`, NOW ...
    return Instantiable.result(self, root, idMap)        -- OUT: the contract
  end,
}

return M

IN. target is an owning entity ref: the instance lands under (or, for a hierarchy type, onto) that owner. With no target the type spawns a fresh root. position, rotation, scale, name and temporary mean the same for every type — Instantiable.root applies them to a root you mint, Instantiable.place to one you adopt, so you never re-read the spec. Honour more opts of your own if your type needs them, and document them in the hook's --!arg docs.

OUT. Return (root, idMap) through Instantiable.result:

  • root — the composed root, as an EntityRef. Composition is synchronous: everything your type builds is live when you return, so the caller can parent to it and read its components in the same statement. Do not defer the work to a component's awake and return an empty shell — a root that is not live is refused.
  • idMap — the originalId -> runtimeId map naming what you spawned, or nil for a type with no addressable children (result normalises it to {}). A caller never receives nil. A component that re-composes your asset on every load keeps this map and passes it back in, which is how a cross-entity reference into the composition survives a reload.

AssetRef runs every instantiate through result on the way out whether or not your type called it, so returning something else fails at your own call rather than handing a caller a nil root. Calling it yourself is still how you say what you return, and running it twice changes nothing.

@builtin::assetTypes.assetType.shared.instantiable's README is the full reference for both halves.

The inspect hook (M.ref.inspect)

asset.inspect(ref) resolves ref, builds a common envelope shared by every asset (identity, name, guid, source, typeName, typeDefinitionPath, scope, origin, description, tags), then calls the resolved type's own inspect hook — M.ref.inspect(self) on behavior.luau — for the type-specific detail. The hook returns only detail; the framework fills in everything else:

-- behavior.luau
local M = {}

M.ref = {
  inspect = function(self)
    -- self.path is the asset's own folder. Parse the asset's OWN
    -- files — source text, a native payload header, its `.metadata`
    -- — never runtime state (a live component instance, a GPU
    -- upload, a compiled schema): inspect must work on an asset that
    -- hasn't been compiled, uploaded, or registered yet.
    return {
      -- type-specific fields, whatever this type wants to surface
    }
  end,
}

return M

A type that omits M.ref.inspect yields the generic record: the envelope alone, detail = nil. That's a valid, working state — not an error — but it means agents calling asset.inspect / tools.use("assets","describe", ...) on an instance of the type see only identity and scope, none of what makes an instance of it meaningful. Declaring the hook is what makes a type's instances inspectable for what they actually are.

A hook that raises leaves detail = nil and sets record.warning to the error text — asset.inspect itself never raises for a resolved ref.

Discovery

  • asset.list("assetType") — every registered asset type.
  • asset.inspect("<typename>") — the type's identity, source path, and this README.
  • asset.resolve("<typename>", "assetType") — the <typename>.assetType asset itself (used by the validator to find the type's type.yaml).

Related

  • assetTypes/README.md — the full type.yaml schema and validator rules.
  • @builtin::assetTypes.assetType.shared.instantiable — the instantiation contract's shared implementation, both halves.
  • Every other *.assetType/ here — the concrete types built on this meta-type.

Interface

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

conforms to

zero/asset-type/v1
⌬ Spec
suffix.assetTypecontainernoprimary aliasestype.yaml, behavior.luaurequired filestype.yaml, README.mdoptional filesbehavior.luau, behavior.lua, template, shared.module, .metadata
Exposed API
⌬ Instance methods

getReadme(self: ?) → string

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

argtypedescription
self?

examples

print(assetTypeRef:getReadme())
⌬ Hooks

onCreate(name: string)

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

argtypedescription
namestringThe new type's name. Becomes its `.<name>` instance suffix.

examples

asset.create("assetType", "Waypoint")

onChange(ref: ?, change: ?) → void

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

argtypedescription
ref?The `AssetRef<assetType>` for the edited type.
change?`{ path, asset, type, kind, origin }` for the write.

examples

-- the engine calls this; a behavior.luau edit is picked up live

Sub-parts

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

32items
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` 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`. | | `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. | ## 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
module · born here
asset
# asset_change_dispatch (module) Installs `_G.__zero_dispatch_asset_change`, the Luau half of the asset-type **change-callback** system. The engine calls it once per VFS source write (via `zero_scripting::ffi_callbacks::fire_asset_change_dispatch`, queued as `VfsMutation::AssetSourceWritten`). ## What it does 1. Receives the written VFS path. 2. Walks up the path to the enclosing `<name>.<type>/` typed-asset folder (deepest registered-type suffix wins, so the direct owner of the write is chosen for nested typed assets). 3. Loads that type's `<type>.assetType/behavior.luau` via `@builtin::assetTypes.assetType.shared.ref.loadTypeModule`. 4. If the module exports an `onChange` function, calls `onChange(ref, change)` where `ref` is the typed `AssetRef` for the changed asset and `change = { path, asset, type }`. This is the type-level analogue of the component-centric `onAssetReload(field)` fan-out: components react to assets they *reference*; an asset *type* reacts to writes inside its own *instances*. ## Re-entrancy A best-effort synchronous guard suppresses dispatch for an asset while its own `onChange` is running, so a handler that writes back inline doesn't recurse immediately. It does **not** span asynchronous work — writes queued during a synchronous `onChange` are processed on a later drain. `onChange` handlers must therefore be **convergent**: diff the meaningful state before acting and short-circuit when there's nothing to do (the `dynamicAsset` example only regenerates when the prompt actually changed and bails while a generation is in flight). Idempotency is the contract, exactly as it is for services' `start`/`stop`. ## Related - `@builtin::assetTypes.assetType.shared.ref` — owns `loadTypeModule` + the per-type `ref` method dispatch. - `assetTypes/assetType.assetType/template/behavior.luau` — the documented template that shows how to author `ref`, `global`, and `onChange`. - `assetTypes/dynamicAsset.assetType` — the reference type that uses `onChange` for prompt-driven regeneration with version control.
▲ 0↑ born
module · born here
asset
# asset_create `asset.create` — the single, generic "instance a new asset of an existing type" API. The same entry point scripts and agents use; there is deliberately no tool wrapper, because agents author in Luau and call `asset.create(...)` directly. ```lua local inst = asset.create("material", "my_metal", { base_color = { 0.8, 0.7, 0.2 } }) -- → AssetRef: inst.path == "/zero/source/my_metal.material", inst.guid, plus the -- material type's ref methods. ``` `asset.create` makes a new *instance of an already-registered type* by running that type's `onCreate(name, opts)` behaviour hook (declared in its `behavior.luau`) and writing the produced files to `/zero/source/<name>.<typeName>/` — the one authored location, in every mode. While play runs, the play-mode write lock takes that write onto the **play shadow**: live in the session, disk source untouched, listed by `vfs.playShadowPaths()`, and promoted or discarded on a guarded play-exit. The asset created in play therefore carries the same identity, path and `require` spelling it has in edit, and the session decides whether it stays. A create whose output the world **reproduces** on every load — one made from a component or a scene entrypoint — writes to the copy-on-write runtime store at `/zero/runtime/assets/<identity>.<typeName>/` instead, so the code that rebuilds it each load is its only source and the saved manifest never carries a second copy. ## Placement Four `opts` keys are consumed by the framework before the type's `onCreate` hook runs, and steer where the instance lands: | Key | Effect | |-----|--------| | `folder` | A relative subfolder under the source root: `/zero/source/<folder>/<name>.<typeName>/`. Groups a generator's output instead of accumulating it at the source root. | | `into` | A resolved container ref (`.toolbox` / `.package`) to author INSIDE — lands at `<container>/<name>.<typeName>/` and registers as a member. Edit-mode only. | | `dest` | An absolute destination path, owned by the caller (an importer building a `<name>.bundle/`). | | `overwrite` | Re-author an existing destination in place, keeping its `.meta` guid so every reference stays valid. | The `name` is always the asset's bare identity — the path goes in `folder`: ```lua local rock = asset.create("mesh", "rock", { positions = p, indices = i, folder = "terrain/props" }) -- → rock.path == "/zero/source/terrain/props/rock.mesh" ``` `dest` and `into` both take precedence over `folder`. `folder` decides the asset's identity, so it applies in every context: the runtime store is flat and spells that identity in one folder name. ```lua -- the same call from a component's awake() -- → rock.path == "/zero/runtime/assets/terrain.props.rock.mesh" -- → rock.identity == "terrain.props.rock", as it is from an execute ``` Types without an `onCreate` hook fall back to cloning their verbatim `template/` skeleton to the same destination, so `create` works for every type. Returns the created asset's `AssetRef` (`.path` / `.guid` plus the type's ref methods — the same interned instance `asset.resolve` returns) on success and raises (via `error`) on bad arguments or a failing `onCreate`. Installed onto the FFI `asset` namespace by the prelude (`M.installInto(asset)`). See `docs/specs/runtime-asset-copying.md`.
▲ 0↑ born
module · born here
asset
# asset_events Per-asset runtime for declared asset events. An assetType's `behavior.luau` declares an `events` schema the same way a component declares one; this module turns that schema into the live objects an asset's ref fires and listens on. ## Three faces, one Signal — keyed by the asset The construction is `component_events`': one `Signal` per declared event, reachable through a private fire-capable table, an owner-side emitter the type's own behavior fires with, and a subscribe-only facade every other holder of the ref sees. The facade exposes `connect` / `once` / `wait` and has no `fire` at any key, so firing authority stays with the type. What differs is the owner. A component event belongs to one instance on one entity; an asset event belongs to the **asset**, so the runtime is keyed by the asset's stable guid. Every resolver of that guid subscribes to the same Signals, and a ref that is reclaimed and re-resolved re-attaches to the subscriptions already there — the same reason `asset_ref`'s `runtime` table is guid-keyed rather than stored on the envelope. Both tables reject an undeclared event name: the private table's metatable raises on a bad key and the facade raises on a bad key, so a typo surfaces at the subscribe call instead of returning nil. ## Declaring events on a type ```lua -- <name>.assetType/behavior.luau local M = {} M.events = { changed = { payload = { value = Field.number(0, NoSync) } }, } M.ref = { poke = function(self) local emitter = require("@builtin::assetTypes.assetType.shared.events").emitter(self.guid) if emitter ~= nil then emitter.changed:fire({ value = 1 }) end end, } return M ``` ```lua -- any holder of the ref local ref = asset.resolve("@builtin::…") ref.events.changed:connect(function(p) print(p.value) end) ``` ## Exports - `M.forAsset(guid, eventSchema)` — the runtime for one guid, built on first use and reused after. A later call with a different schema reconciles: surviving events keep their Signal and their subscribers, new events get a fresh one, removed events are disconnected and dropped. - `M.facade(guid)` — the subscribe-only view, or nil before a runtime exists. - `M.emitter(guid)` — the owner-side `:fire` view, or nil before a runtime exists. - `M.has(guid)` — whether a guid holds a live runtime. - `M.teardown(guid)` — drop one asset's runtime, disconnecting subscribers. - `M.teardownAll()` — drop every runtime. Called on an engine mode flip so a play-mode subscription does not survive into edit. - `M.liveGuids()` — every guid holding a runtime, sorted. ## Notes - Subscriptions made through the facade are ordinary `signal.module` connections and disconnect the same way any other does. - The runtimes table is held strongly, and its lifetime is bounded by the mode flip that clears it.
▲ 0↑ born
module · born here
asset
# scene_instantiable (module) The instantiation contract's shared implementation. An asset type opts into scene instantiation by defining `instantiate(self, target?, opts?)` on its behaviour `ref` table; this module carries the parts that mean the same thing for every type, so a caller writes ONE piece of code against all of them. ```lua local root, idMap = ref:instantiate(target?, opts?) ``` ## The contract **IN — the base opts.** `target` is an owning entity ref: the instance lands under (or, for a hierarchy type like a bundle, onto) that owner. With no target the type spawns a fresh root. `position`, `rotation`, `scale`, `name` and `temporary` place that root and mean the same for every type. `rotation` takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion. A type may honour more opts of its own — `params` for a type built from declared inputs, `idMap` / `diff` / `sourceTag` for the bundle override contract — and says so in its own `instantiate` docs. **OUT — the two returned values.** Every type returns the same pair: - **`root`** — the composed root, as an `EntityRef`. Composition is **synchronous**: the root and everything the type built under it are live the moment the call returns, so a caller can parent to it, read its components and hand it on in the same statement. There is no frame to wait for and no callback. - **`idMap`** — the `originalId -> runtimeId` map naming what the composition spawned, `{}` for a type with no addressable children. **Never nil.** A component that re-composes the asset on every load (`Asset`) keeps this map and passes it back in, which is how a cross-entity reference into the composition — `SkinnedModel.skeletonRoot` pointing at a bone — survives a reload. The return is enforced, not merely described: `AssetRef` dispatches every `ref:instantiate(...)` through `result` on the way out, whether or not the type called it. A type that returns something else fails at its own call rather than handing its caller a nil root or a map that is sometimes absent. ## Exports - `M.root(self, target?, opts?) -> root` — stand the root entity for a type that spawns one: parented to `target`, born temporary when `opts.temporary`, named `opts.name` else the asset's own name, placed. - `M.place(root, opts?) -> root` — apply the placement opts to a root the type already has (the adopt path: a bundle exploding onto its target, a sceneModule reconciling under one). - `M.result(self, root, idMap?) -> (root, idMap)` — return through the contract. Checks `root` is a live entity ref, normalises a missing map to `{}`, and errors naming the asset type when either is something else. - `M.isOwned(opts?) -> boolean` — whether a component drives this call. The `Asset` / `SceneModule` components tag their own calls with `sourceTag`; they hold the reference and re-compose on every load. An untagged call came straight from `ref:instantiate(...)` and has no such owner. - `M.own(root, self, idMap?) -> root` — hand an already-composed root to an `Asset` component pointing at `self`, so the reference and its map persist and the composition is rebuilt on the next load. The component adopts the live composition rather than building a second one. - `M.check(value, constraint) -> ok, reason?` — the `sceneInstantiable` field-constraint validator, also registered under that kind on load. ## The field constraint `Field.instantiableRef` emits `constraint = { kind = "sceneInstantiable" }`, and this module registers the validator for it on load. Given a written value: 1. `nil` passes — the field is optional (no asset assigned). 2. Reads the value's identity — a bare string, or a table's `__ref` / `guid` / `identity` / `name`. 3. Resolves it (`asset.resolve(identity)` — any category) and asks the resolved ref `ref:canInstantiate()`: true iff the asset's type defines an `instantiate` method. This is what lets `Field.instantiableRef` accept by CAPABILITY rather than a hardcoded type list — a new scene-instantiable asset type is accepted the moment it defines the hook, with no edit to the field or its consumers.
▲ 0↑ born
module · born here
asset
# asset_on_register Drives the optional `onRegister(self)` assetType lifecycle hook — the surface that lets an asset instance run code exactly once when it first registers, operating on the per-instance ref (`self`). This is what makes "write an asset into the world → it takes effect live, no restart" work for content that registers into a runtime registry (editor panels, and anything else that opts in). A type opts in by exporting `onRegister` from its `behavior.luau`. The hook fires exactly once per instance: - On `engine.onWorldLoaded` — a batched sweep over every world instance of every type that implements `onRegister`, yielding every 32 instances so a large instance count never stalls a frame. The @builtin library (`/zero/source/libs/`) is excluded. - On live `asset.create` — the create path calls `M.fireFor(ref)` on the freshly-created instance. A per-VM once-guard keyed by the instance's VFS path ensures the two triggers can't double-register the same instance. ## Exports - `M.fireFor(ref, onRegisterFn?) -> boolean` — fire one instance's `onRegister(self)` hook exactly once. Resolves the type's hook from `ref.type` when `onRegisterFn` isn't supplied. Idempotent: a second call on the same instance is a no-op. Returns true if the hook fired this call. - `M.sweep()` — batched sweep: fire `onRegister` once for every world instance of every registered type that implements it. Yields, so it must run inside a task. Safe to call repeatedly. - `M.install()` — install the world-loaded sweep. Idempotent. Runs the sweep on a system task so it ticks through pause and never blocks the caller.
▲ 0↑ born
module · born here
asset
# assetType.shared.ref Builds the metatable every `AssetRef` (the `{ __ref, type, name, guid, identity, path }` envelope returned by `asset.resolve` / `asset.ref`) carries. Replaces the per-call C metatable that used to live in `crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable`. ## Public API ```lua local assetRef = require("@builtin::assetTypes.assetType.shared.ref") -- Called by the Rust factory (push_asset_ref_handle) via -- _G.__build_asset_ref_proxy after the envelope fields are set. assetRef.build(envelope) -- attaches AssetRefMT, returns the table ``` The module exposes no invalidation surface — discovery is registry-driven and hot-reload is transparent (see "Discovery and hot-reload" below). ## Method dispatch order `ref.foo` is resolved in this order: 1. **Default methods** — `getSource` / `getBytes` / `getText` / `exists` / `inspect` and the lazy `meta` property. Per-type definitions cannot override these (engine contract from gh#1889). 2. **Per-type `ref` table** — discovered through `asset.resolve(<typename>, "assetType")` + `require(<identity>.behavior)`. The lookup resolves to whichever `<typename>.assetType/` folder is currently registered for the typename (built-in, user, or library-vendored) and pulls its `ref` methods. 3. **Nil** — unknown key, same shape as the original C metatable. ## Adding methods for a new asset type Drop a `behavior.luau` into the type folder: ``` src/lua/lib/assetTypes/<typename>.assetType/ ├── type.yaml -- structural spec (existing) ├── behavior.luau -- ref + global behaviour (new) └── template/ -- placeholder body (existing) ``` `behavior.luau` returns: ```lua return { ref = { --!desc Per-instance method on every `<typename>` AssetRef. myMethod = function(self, ...) ... end, }, global = { -- Reserved for the asset.<typename>.* surface — not consumed -- yet; included so the contract from gh#1889 lands cleanly. }, } ``` Within `ref` methods, `self` is the AssetRef envelope: `self.path`, `self.identity`, `self.guid`, `self.type`, `self.name` are available. Engine-required fields (`guid`, `path`, `identity`, `type`) must not be redefined. ## Discovery and hot-reload Type discovery uses the asset registry, so it doesn't matter where a `<typename>.assetType/` folder lives — engine built-ins under `@builtin::assetTypes.<X>`, user types dropped under `/zero/source/<X>.assetType/`, or types vendored inside an imported library all resolve identically. The dispatcher walks `asset.resolve(<typename>, "assetType")` → canonical identity → `require <identity>.behavior` on every miss; the asset registry is the cache for the resolve half and Luau's `_LOADED` is the cache for the require half. Hot-reload is transparent. When a `<typename>.assetType/behavior.luau` is rewritten through VFS, the engine's `invalidate_require_cache_for_state` re-runs the module and mutates the cached `_LOADED[<identity>.behavior]` table in place. Dispatch reads `mod.ref` on every access (rather than caching a sub-pointer), so existing AssetRefs see the new methods on the very next access — no Rust-side hook into this module is needed, and no invalidation API is exposed for callers to call. The same registry-driven path also handles type **registration** correctness: dropping a new `<typename>.assetType/behavior.luau` updates the asset registry, the next `asset.resolve(<typename>, "assetType")` returns the new ref, and AssetRefs of that type immediately gain the new methods. ## Why Luau, not Rust The asset surface needed per-category behaviour (`material:getProperties()`, `bundle:instantiate(entityId)`, `tool:run(args)`) and the old shape couldn't grow without an FFI change per method. Owning the proxy in Luau means: - Type-specific methods ship as content (the `<typename>.assetType/` folder), not engine code. - Adding a new asset type drops in a new folder; no Rust rebuild. - Methods can call any other Luau global (`vfs`, `asset`, `Material`, `entity`, etc.) directly, instead of going through narrow FFI primitives. ## Consumers - `crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable` — the Rust factory that pushes envelopes invokes `_G.__build_asset_ref_proxy` once per push. - Every Luau call site of `asset.resolve` / `asset.ref` / `entity(id).component.X.material` / any binding declared `AssetRef<...>` — they all receive envelopes built through this module.
▲ 0↑ born
module · born here
asset
# asset_ref_shapes Publishes what every `AssetRef<category>` answers to, so a member read on an asset-typed value is checked against the category's own surface. An asset category's per-instance surface is authored: a `<name>.assetType/behavior.luau` declares `M.ref = { ... }`, and each category declares its own. The members an `AssetRef<inputMap>` carries are therefore knowable only to `inputMap` itself — no fixed set of types covers the ones a world defines, and the checker has no way to guess them. This module reads each registered category's `M.ref` table from its source (never executing it), renders it as a Luau table type, and hands the set to the engine. From there a `ref:method(...)` on an asset-typed value resolves against the category's real surface: a name it does not carry is reported along with the ones it does. ## Results shaped by the asset Some results are shaped by the asset rather than by its category — `inputMapRef:activate()` answers one handle per binding THAT map declares, a set that is authored and differs per map. A category states those by declaring `refShapes`, and this module asks it once per instance and publishes the answers keyed by asset identity. `types/assetType` documents the authoring side. ## When it publishes Publishing is driven by use: every entry point that produces diagnostics calls `ensure`, which is a no-op once the set is current. A world load, a write inside a type definition, and a write inside any asset whose container computes shapes each mark it stale, so the cost lands on the next check and only if one comes. The sweep is complete and replaces what was published before, so a category whose type is removed stops being published. ## Reading what the checker believes From a call site, a type that is correct and one that was never published are the same absence of a diagnostic. `published()` tells them apart: ```lua local shapes = require("@builtin::assetTypes.assetType.shared.refShapes").published() shapes.categories.inputMap --> "{ activate: () -> any, controls: () -> { any }, ... }" shapes.returns["@builtin::inputMaps.default"].activate --> "{ crouch: Handle, interact: Handle, jump: Handle, ... }" ```
▲ 0↑ born
·
metadata · born here
file
▲ 0↑ born
module · born here
asset
# validate Pure validator for `asset.create` opts — checks a creation `opts` table against the schema produced by `asset.createSchema(typeName)`. Schema in, validated opts (or a teaching error string) out. Consumed by `asset.create`, which calls `M.validate` before forwarding opts to the type's `onCreate`. No FFI, no VFS. Errors are teaching errors: they state what was wrong at which path (`opts.cfg.size`), what was expected, and then render the full creation-parameter contract so the caller can fix the call without reading the type's `behavior.luau`. Unknown parameter names get an edit-distance "did you mean" suggestion. ## Types - `ValidatorShape` — `{ kind, literals?, item?, members?, fields?, indexer? }`, the recursive shape encoding. - `ValidatorParam` — `{ name, shape?, optional?, default?, desc? }`. - `ValidatorSchema` — `{ kind, desc?, example?, open?, error?, params?, indexer? }`. ## Exports - `M.validate(schema, opts?) -> (validatedOpts?, err?)` — validate and default-fill an `opts` table against an `asset.createSchema` result. Returns `(validatedOpts, nil)` on success (a new table for `schema`-kind schemas) or `(nil, err)` where `err` is a bare teaching error string. The caller owns presentation. Handles the `legacy`, `error`, `none`, and `schema` schema kinds. - `M.shapeName(shape) -> string` — render a single parameter shape (literal unions as `"png" | "jpg"`, arrays as `{ T }`, tables as `table`, primitives as their kind, nil/malformed as `any`). Used by `asset.describe`. - `M.renderContract(schema) -> string` — render a schema's creation-parameter contract as one aligned line per parameter, with an `[extra keys]` line for open schemas. ## Usage ```luau local V = require("@builtin::modules.api.engine.asset.create.validate") local out, err = V.validate(schema, { bytes = png }) if err then error("asset.create: " .. err) end ```
▲ 0↑ born
·
metadata · born here
file
▲ 0↑ born
backing path · assetTypes/assetType.assetType

Problems

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

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
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.