asset
The asset namespace — 141 functions.
asset/create
asset.create(typeName: string, name: string, opts: table?) -> table
[Library] Instance a new asset of an existing registered type: runs that type's onCreate(name, opts) behavior hook (or clones its template/ skeleton when it has none) and writes the produced files to /zero/source/<name>.<type>/ — the one authored location, in every mode. While play runs, the play write lock takes that write onto the play shadow (live in the session, disk source untouched, listed by vfs.playShadowPaths(), promoted or discarded on a guarded play-exit), so the asset keeps the identity, path and require spelling it has in edit. A create made from a component or a scene entrypoint — output the world reproduces on every load — writes to the copy-on-write /zero/runtime/assets/<identity>.<type>/ store instead, so the saved manifest never carries a second copy; name and folder spell the same identity in either store, so a reference written against that identity resolves the asset wherever the call filed it. Four framework opts keys steer placement and are consumed before the hook runs: folder (a relative subfolder under /source, and the dotted prefix of the asset's identity), into (a resolved container ref to author inside), dest (an absolute destination path) and overwrite (re-author in place, keeping the guid). Returns the created asset's AssetRef; raises on bad arguments or a failing onCreate.
Parameters
typeNamestring— Registered asset type to instance (e.g. "material", "texture").namestring— Destination asset name (becomes/source/<name>.<typeName>). A bare identity — passopts.folderto place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare:^[A-Za-z][A-Za-z0-9_]*$unless itsbehavior.luauexports anamePattern, asguidedoes to takegetting-startedand01-overview. This call names the category FIRST and the asset second. Every otherasset.*call taking both names them the other way round —asset.exists(name, category),asset.tryResolve(ref, category)— so a create-then-check pair readsif not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.opts{ [string]: any }(optional) — Optional table forwarded to the type'sonCreatehook, minus four framework keys consumed here and never seen by the hook:folder(a relative subfolder under/sourceto author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix),into(author INSIDE a resolved container ref),dest(an absolute destination path), andoverwrite(re-author in place, keeping the guid).
Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.
local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild
globals/asset/add_tag
asset.add_tag(ref: RefArg, tag: string)
Add a tag to the asset's .metadata.tags. Idempotent.
Creates the sidecar and the tags array if missing.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to add.
asset.add_tag("brick", "wip")
globals/asset/alias
asset.alias(ref: RefArg, alias: string) -> boolean
Add a name the asset answers to. asset.resolve, leaf
shorthand, and the typed-argument coercion that content refs
travel through all reach the asset by the alias from here on,
exactly as they do by its identity — so a material naming a
shader by its alias resolves, and content written against an
older name keeps working after a rename. The name survives
writes to the asset's own files.
Raises when the name already resolves to a DIFFERENT asset — an
alias extends the identity namespace and never takes a name out
of another asset's hands — and when it is shaped like a guid or
a VFS path, forms that resolve before identity lookup, so an
alias in that shape could never answer.
Parameters
refRefArg— The asset gaining the name.aliasstring— The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
Returns boolean — True when newly added, false when the asset already answered to it.
asset.alias("@builtin::shaders.pbr", "standard")
globals/asset/aliases
asset.aliases(ref: RefArg) -> { string }
The additional names this asset answers to, beyond its own
identity — what asset.alias registered, plus the
package-relative ~pkg.tail form when the asset lives inside a
package.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of alias names in canonical identity form.
for _, n in asset.aliases("pbr") do print(n) end
globals/asset/canCreate
asset.canCreate(typeName: string) -> boolean
Whether asset.create can instance typeName: the type declares
creation logic (a behavior.luau onCreate hook) or ships a
template/ skeleton the hookless fallback clones. A type with neither
— one whose instances only arrive by import — answers false. The query
a creation UI derives its offering from, so what it offers is what
asset.create accepts.
Parameters
typeNamestring— Registered asset type (e.g. "material", "scene").
Returns boolean — true when asset.create(typeName, …) can produce one.
if asset.canCreate(kind) then asset.create(kind, name) end
globals/asset/categories
asset.categories() -> { string }
List every asset category the engine currently recognises.
Use to discover valid type argument values for the rest of
asset.*.
Returns { string } — Array of category names.
for _, c in asset.categories() do print(c) end
globals/asset/containing
asset.containing(path: string) -> AssetRef?
Walk path's ancestors and return an AssetRef handle for
the OUTERMOST category-folder containing it (e.g. main.scene
for "/source/scenes/main.scene/scene.json"). Returns nil for
paths outside any registered asset type.
Parameters
pathstring— VFS path to inspect.
Returns AssetRef? — AssetRef handle, or nil.
local a = asset.containing("/source/scenes/main.scene/scene.json")
globals/asset/cpuResident
asset.cpuResident(ref: RefArg, typeName: string?) -> boolean
True when the asset is CPU-resident — a live script-component context
holds it (a component's assetRef field, or an imperative
asset.resolve/ref made while a component is the caller), which is what
warms its bytes into memory. The CPU pool is a different pool from the
device's: asset.observe().cpu lists it, asset.observe().textures /
.meshes list what the device holds, and an asset can be in one and not
the other.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true when a live context holds it.
if asset.cpuResident(ref) then print("bytes are warm") end
globals/asset/create
asset.create(typeName: string, name: string, opts: { [string]: any }?) -> AssetRef
Instance a new asset of an existing type. Runs the type's
behavior.luau onCreate(name, opts) hook to produce the asset's
files, then writes them under /source/<name>.<type>/. This is the
single generic asset-creation API. Refuses to clobber an existing
edit-mode asset unless opts.overwrite = true, which re-authors it in
place and keeps the existing guid (only the checksum changes). Pairs with
asset.exists for content generators that re-run over the same names.
A create made from a script component's callback or a scene entrypoint is
output the world reproduces on every load, so it is filed in the ephemeral
/runtime/assets/ store instead, where the saved manifest never carries a
second copy of it. name and folder spell the same IDENTITY in either
store, so a reference written against that identity resolves the asset
wherever the call filed it, and one generator run from an execute and
from a component names one asset.
Parameters
typeNamestring— Registered asset type to instance (e.g. "material", "texture").namestring— Destination asset name (becomes/source/<name>.<typeName>). A bare identity — passopts.folderto place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare:^[A-Za-z][A-Za-z0-9_]*$unless itsbehavior.luauexports anamePattern, asguidedoes to takegetting-startedand01-overview. This call names the category FIRST and the asset second. Every otherasset.*call taking both names them the other way round —asset.exists(name, category),asset.tryResolve(ref, category)— so a create-then-check pair readsif not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.opts{ [string]: any }(optional) — Optional table forwarded to the type'sonCreatehook, minus four framework keys consumed here and never seen by the hook:folder(a relative subfolder under/sourceto author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix),into(author INSIDE a resolved container ref),dest(an absolute destination path), andoverwrite(re-author in place, keeping the guid).
Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.
local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild
globals/asset/declareReferenceArg
asset.declareReferenceArg(call: string, position: number, assetType: string)
Declare that call's argument at 1-based position names an
asset of type, so a string literal written there is recorded as a
reference. The positional counterpart of
asset.declareReferenceField, for a call that takes its asset as a
plain argument — including a world's own spawn helper, which is
where a name most often stops being visible to the reference graph.
A lookup whose asset is its FIRST argument (asset.resolve and its
siblings) is already read and needs no declaration. Only a literal —
or a name the file holds in a top-level string constant — is
recorded; anything computed is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.positionnumber— Which argument holds the name, counting from 1.assetTypestring
asset.declareReferenceArg("spawnModel", 3, "mesh")
globals/asset/declareReferenceField
asset.declareReferenceField(call: string, field: string, assetType: string)
Declare that call's options table names an asset of type in
its field, so a string literal written there is recorded as a
reference by whatever writes the file. This is what puts an API
that takes an asset BY NAME into the reference graph: the named
asset becomes a dependency, travels with the content that names it
into a pack or a pull, and a name nothing answers to becomes an
unresolved dependency worldValidation reports and the push gate
refuses. A field holding a TABLE of names — a material's textures
— records every name in it. Declare once, beside the API; a call
taking its asset as the FIRST positional argument is already read
and needs no declaration. Only a literal is recorded; a computed
name resolves at runtime and is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.fieldstring— The options-table field holding the name, read at the table's own level.assetTypestring
asset.declareReferenceField("fx.beam", "material", "material")
globals/asset/declareReferenceKey
asset.declareReferenceKey(assetType: string, key: string, refType: string)
Declare that, in a data file belonging to an assetType asset,
the top-level key names an asset of type — a .material's
mat.yaml naming the shader it draws with and the textures it
binds. The names a format holds are references as surely as ones
written in code: recording them carries a material's shader along
with the material into a pack or a pull, and turns a name nothing
answers to into an unresolved dependency instead of a surface that
renders as the magenta error material. A key holding a table of
names records one per entry.
Parameters
assetTypestring— The category owning the file, e.g. "material".keystring— The top-level key holding the name(s).refTypestring
asset.declareReferenceKey("material", "shader", "shader")
globals/asset/deps
asset.deps(ref: RefArg, type: string?) -> DepsResult
Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns DepsResult — { deps = { { asset_guid, origin, literal, via, ... } } }.
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end
globals/asset/describe
asset.describe(typeName: string) -> DescribeResult
The creation contract for an asset type: the parameters its
onCreate(name, opts) hook accepts, as data. kind is "schema"
(typed contract), "legacy" (untyped opts — anything passes), "none"
(template scaffold — takes no opts), or "error" (the type's schema
failed to parse; error says why). contract is the human-readable
rendering validation errors print.
Parameters
typeNamestring— Registered asset type to describe (e.g. "texture").
Returns DescribeResult — the creation contract.
local contract = asset.describe("texture").contract
globals/asset/diagnose
asset.diagnose(ref: RefArg) -> any
Why one asset can or cannot be used, read from the engine rather than
from what the caller asked for. Always carries usable; when false,
reason is one of asset.unusableReasons() and detail is the engine's
own message. primary names the file the type's declared primary list
resolved to, so an asset that loaded a preview image instead of its
payload shows the wrong filename rather than a successful load. The
payload's bytes are read by the engine's own decoder wherever it has one
for that container, so usable is the verdict a load would reach and the
call costs that decode.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — DiagnoseRecord
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end
globals/asset/exists
asset.exists(name: string, typeName: string) -> boolean
Parameters
namestringtypeNamestring
Returns boolean
globals/asset/get_field
asset.get_field(ref: RefArg, key: string) -> any
Read one top-level field from the asset's .metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns any — Field value or nil.
local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table
globals/asset/gpuResident
asset.gpuResident(ref: RefArg) -> boolean
True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns boolean — true when the device holds it.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))
globals/asset/guid
asset.guid(ref: RefArg, type: string?) -> string
Return the guid for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Guid.
local g = asset.guid("@builtin::components.Camera")
globals/asset/has_field
asset.has_field(ref: RefArg, key: string) -> boolean
True when the asset's .metadata carries the named field.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns boolean — True when present.
if asset.has_field("brick", "author") then end
globals/asset/has_tag
asset.has_tag(ref: RefArg, tag: string) -> boolean
True when the asset's .metadata.tags contains tag.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to check for.
Returns boolean — True when present.
if asset.has_tag("brick", "wip") then end
globals/asset/identity
asset.identity(ref: RefArg, type: string?) -> string
Return the canonical identity for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Canonical identity.
local id = asset.identity("brick")
globals/asset/import
asset.import(path: string) -> string?
Import a raw source file NOW and return the produced asset path
(a .bundle for a model, .texture for an image, .audio for a
sound, …), or nil if no importer claims it. This is the deterministic,
on-demand counterpart to the engine's automatic import-on-write: it runs
in the calling task and returns only when the import is complete. Pair it
with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the
raw bytes without firing the automatic importer, then asset.import(path)
imports them under your control, so you can act on the result instead of
polling for the import to appear.
Parameters
pathstring— The raw source VFS path to import (e.g. a just-written.glb).
Returns string? — The produced asset path, or nil when nothing claimed it.
local bundle = asset.import("/zero/source/generated/chest.glb")
globals/asset/inspect
asset.inspect(ref: RefArg, type: string?) -> InspectRecord
Everything known about one asset in a single record: identity, guid,
source, type, scope and origin, its description and tags, the ref methods
its type exposes, and the type's own inspect detail when it declares one.
The read-everything counterpart to asset.resolve, which hands back a ref.
Parameters
refRefArg— AnAssetRef, an identity string, or a path.typestring(optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
Returns InspectRecord — The inspect record.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)
globals/asset/list
asset.list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?) -> ListResult
Query registered assets, returning each match as a resolved
AssetRef handle. Every filter narrows the same enumeration and
they compose: path selects a VFS subtree (the folder and
everything under it), type keeps only those asset types within
it, scope keeps only that scope, and fields keeps only assets
whose .metadata matches. type and path each take one value or
a list matching any of its entries, and all / any / none
group whole filters — none excludes what it matches. order,
limit, and offset shape the result: matches come back ordered
by identity unless order names another field
(name / path / type / guid).
Each entry is the same envelope asset.resolve returns (__ref /
type / name / guid / identity / path), so it can be passed
anywhere an AssetRef is accepted, and the result carries
:first() / :random() / :filter() / :sort() and friends.
type takes the same values asset.categories() lists. The first
positional argument is a path when it is absolute, a type
otherwise. An unknown key raises, as does a table setting both
type and its older spelling category.
A static (literal) type or path makes the enumeration part of the
calling file's content dependencies when it is saved — the set
travels with published content, so consumers get at-least the
authoring world's assets.
Parameters
type_or_opts(AssetCategory | ListOpts)(optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.scopestring(optional) — Scope filter (when first arg is a type).optsListOpts(optional) — The query table — seeListOpts.
Returns ListResult — The matched AssetRef handles, as a result carrying query methods.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()
globals/asset/list_field_values
asset.list_field_values(key: string) -> { any }
Distinct values seen for the named field across every
asset's .metadata.
Parameters
keystring— Field name.
Returns { any } — Array of distinct values.
local authors = asset.list_field_values("author")
globals/asset/list_fields
asset.list_fields() -> { string }
Distinct top-level field keys observed across every asset's
.metadata. Useful for tooling discovering custom keys in use.
Returns { string } — Array of field names.
for _, k in asset.list_fields() do print(k) end
globals/asset/meta
asset.meta(ref: RefArg, type: string?) -> AssetMeta
Read the asset's engine-owned identity record (guid /
checksum). Distinct from .metadata (agent-editable);
for that use asset.metadata.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — Metadata table.
local m = asset.meta("brick") -- { guid = ..., checksum = ... }
globals/asset/metadata
asset.metadata(ref: RefArg, type: string?) -> AssetMeta
Read the asset's agent-editable .metadata sidecar as a
Lua table. Missing sidecar returns {}. Distinct from
asset.meta (engine-owned).
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — JSON-shaped table; empty when no sidecar exists.
local md = asset.metadata("brick")
globals/asset/observe
asset.observe() -> any
What the engine is holding for content right now, in one reading:
textures and meshes (one row per resource the device holds, each with
the bytes it costs, its dimensions or buffer split, and where it came
from), cpu (one row per asset a live script-component context holds),
and totals — the aggregates those rows sum to, so the listing reconciles
against renderer.textureMemory() and renderer.gpuMemory().
Each pool is named because they are different pools: an asset can be on
the device and not CPU-resident, or the reverse. devicePublished is
false when no renderer has published an inventory and cpuPublished when
the scripting VM has not published its pool — an engine that cannot answer
reads differently from one answering with nothing resident.
Returns any — ResidencyReading
local r = asset.observe() print(#r.textures, r.totals.textureBytes)
globals/asset/preview
asset.preview(ref: RefArg, opts: { [string]: any }?, type: string?) -> { [string]: any }
Render a preview of an asset. Resolves the ref and dispatches to its
type's preview ref-method when present; otherwise returns the
{ available = false } sentinel ("no preview available for this type").
Parameters
refRefArg— Any name the asset has.opts{ [string]: any }(optional) — Optional{ size = { width, height }, angle = { yaw, pitch } }.typestring(optional) — Category hint (optional).
Returns { [string]: any } — { available, imageBase64?, width?, height?, bounds?, stats?, reason? }.
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })
globals/asset/primaryFile
asset.primaryFile(ref: RefArg) -> any
The file the asset type's declared primary list resolves to inside
this asset, as the loader itself resolves it. resolved is false when no
declaration matched and path is then absent; declared is the type's
own primary list in match order.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — { path: string?, resolved: boolean, isFolder: boolean, declared: { string } }
print(asset.primaryFile("myTex").path)
globals/asset/ref
asset.ref(ref: RefArg, type: string?) -> AssetRef
Build a reference handle for an asset — the canonical ref
envelope constructor. Identical shape to asset.resolve;
preferred name for the author-side use case (embedding refs in
YAML / JSON / Luau output).
Naming the asset here reads exactly as naming it in asset.resolve,
down to raising on a miss: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to reference — an identity, a guid, a VFS path, or a handle.typestring(optional) — Category hint (optional).
Returns AssetRef — Ref handle. Raises on a miss or ambiguity, as asset.resolve does.
local r = asset.ref("animations.idle", "animation")
globals/asset/reloadPending
asset.reloadPending(ref: RefArg, typeName: string?) -> boolean
True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true while a content-change reload is still owed.
repeat task.wait() until not asset.reloadPending(ref)
globals/asset/reloadSeq
asset.reloadSeq(ref: RefArg, typeName: string?) -> number
How many content-change reloads this asset has been through — the
count of onAssetReload dispatches the engine has RUN for it. A write to
a file inside an asset does not reload it on the spot: the writes of one
authoring step are collected for a settle window and the reload runs on a
later frame. Read this, write, then poll for a larger number to learn the
write's reload has actually reached subscribers. Monotonic per asset and
session-scoped; 0 for an asset whose content has not changed since boot.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns number — content-change reloads dispatched for this asset.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at
globals/asset/remove_field
asset.remove_field(ref: RefArg, key: string)
Remove one top-level field from the asset's .metadata.
No-op when the field isn't present.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
asset.remove_field("brick", "author")
globals/asset/remove_tag
asset.remove_tag(ref: RefArg, tag: string)
Remove a tag from the asset's .metadata.tags. No-op when
the tag isn't present.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to remove.
asset.remove_tag("brick", "wip")
globals/asset/resolve
asset.resolve(ref: RefArg, type: (C & string)?) -> AssetRef<C>
Find an asset. The returned handle carries every name form
the asset has (guid, identity, path, type) so
downstream code can read any one of them without calling
resolve again. Raises when ref resolves to no asset — or, with a
type, to no asset of that type — and when ref reaches more than one
asset, where it names the candidates for you to pick from instead of
picking one of them. A <scope>::-qualified identity reaches exactly one:
@root::name for the asset this world holds at its source root, the
library identity (@builtin::…) for a library's. For the same lookup
answering a miss with nil, use asset.tryResolve(ref, type).
A name written as a string LITERAL is recorded as this source's
dependency on that asset, so the asset travels with the content and
still resolves once someone installs it in another world. A COMPUTED
name cannot be written down, so nothing pins what it reaches: that is a
dynamic resolve — free in a tool, refused on the gameplay path (a
component or scene entrypoint). asset.tryResolve, asset.ref and
asset.source read the name they are given exactly this way too, so
which of the four you reach for changes neither answer. To ask whether a
computed name has files without reaching a handle, use
asset.exists(name, type).
Parameters
refRefArg— The asset to find — an identity, a guid, a VFS path, or a handle.type(C & string)(optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.pngafter the texture importer turned it intowall.texture) resolves to the promoted asset, and says so in the log once per reference.
Returns AssetRef<C> — Asset handle, carrying the category when one was named — so the methods that category defines are checked on the result. Raises (rather than returning nil) on a miss, and on a name that reaches more than one asset.
local a = asset.resolve("@builtin::components.Camera")
globals/asset/set_field
asset.set_field(ref: RefArg, key: string, value: any?)
Set one field in the asset's .metadata, creating the sidecar if missing.
Sibling fields are preserved. When the new value AND the existing value are
both maps (objects), the new value DEEP-MERGES into the existing one, so
writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace.
Clear a whole field with asset.remove_field; replace the entire sidecar with
asset.set_metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.valueany(optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept
globals/asset/set_metadata
asset.set_metadata(ref: RefArg, data: AssetMeta)
Replace the asset's .metadata sidecar with the given
table. Pass an empty table to clear all fields.
Parameters
refRefArg— Any name the asset has.dataAssetMeta— Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })
globals/asset/source
asset.source(ref: RefArg, type: string?) -> string
Return the VFS source path for an asset.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — VFS source path.
local p = asset.source("brick") -- "/source/brick.material"
globals/asset/tags
asset.tags(ref: RefArg) -> { string }
Convenience read of the .metadata.tags array.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of tag strings.
for _, t in asset.tags("brick") do print(t) end
globals/asset/tryResolve
asset.tryResolve(ref: RefArg, type: string?, base: string?) -> AssetRef?
The same lookup asset.resolve performs, answering a miss with nil
instead of raising. Every name form, the same type narrowing, and the
same handle on success — so "use it if it is there" needs no pcall
around a call whose failure would otherwise be indistinguishable from a
real error.
This consults the asset REGISTRY, so it sees registered assets wherever
their files live, @builtin:: ones included. asset.exists(name, type)
answers the narrower question of whether an asset's files are present in
the current mode's store.
A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to look up — an identity, a guid, a VFS path, or a handle.typestring(optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same asasset.resolve.basestring(optional) — Referring VFS path a~/~.tailref expands against, the same asasset.resolve's — so the two answer the same question and differ only in what a miss is.
Returns AssetRef? — Asset handle, or nil when the reference resolves to no asset.
local mat = asset.tryResolve(name, "material")
globals/asset/typeRef
asset.typeRef(target: RefArg) -> string?
Return the pinned asset_type reference (the type's guid)
that the asset is an instance of. Resolve the full type with
asset.resolve(asset.typeRef(target)). Returns nil for loose
files / assets with no pinned type.
Parameters
targetRefArg— Asset handle / identity / guid / VFS path.
Returns string? — Pinned type's guid, or nil.
local t = asset.resolve(asset.typeRef("brick"))
globals/asset/unusableReasons
asset.unusableReasons() -> { string }
Every reason asset.diagnose can report an asset unusable for, sorted.
Returns { string } — Array of reason names.
for _, r in ipairs(asset.unusableReasons()) do print(r) end
globals/asset/validate
asset.validate(ref: RefArg, type: string?) -> ValidateResult
Validate an asset folder against its type's type.yaml, plus
the type's own semantic validation. Structural problems come from
type.yaml — missing required files, unsatisfied one_of_group
alternatives, and (when allow_unlisted: false) unexpected
children. validated = false when no type.yaml is registered
— nothing structural to check. On top of that, when the asset's
type ships a behavior.luau exporting a top-level
validate(assetRef) -> { { code, message, severity? } }, its
reported problems (severity defaults to "error") are appended to
problems; error-severity problems flip ok to false, warnings
leave it untouched. A hook that raises or returns a non-table is
itself reported as a validate.hook_failed error problem — a
broken hook blocks. A type with no validate export behaves
exactly as the structural check alone.
world.push calls this per user asset, so a type's semantic
validation is enforced at publish time with no further wiring.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns ValidateResult — { ok, typeName, validated, problems }.
local v = asset.validate("@builtin::components.Camera")
globals/asset/warmup
asset.warmup(ref: RefArg, opts: WarmupOpts?) -> WarmupResult
Warm an asset's bytes into CPU memory and follow its declared
content dependencies to each referenced asset, deduped by
guid. Type-agnostic (reads the generic ref graph) and CPU-only —
never touches the GPU. Side-effect-free name resolution (uses
asset.guid/asset.deps, not asset.resolve).
Parameters
refRefArg— Any name the root asset has — handle, identity, guid, or path.optsWarmupOpts(optional) — Optional{ vias, max }— restrict ref-edge kinds / cap closure size.
Returns WarmupResult — { closure, count } — the deduped guid closure warmed and its size.
local w = asset.warmup("@builtin::scenes.test_arena")
modules/asset/README
require("@builtin/modules/api/engine/asset") -- asset (also available as global 'asset')
Asset resolver, ref envelope builder, sidecar metadata. Public Luau surface over the __asset Internal FFI namespace.
Usage: local asset = require("@builtin/modules/api/engine/asset") Also available as global: asset
modules/asset/add_tag
add_tag(ref: RefArg, tag: string)
Add a tag to the asset's .metadata.tags. Idempotent.
Creates the sidecar and the tags array if missing.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to add.
asset.add_tag("brick", "wip")
modules/asset/alias
alias(ref: RefArg, alias: string): boolean
Add a name the asset answers to. asset.resolve, leaf
shorthand, and the typed-argument coercion that content refs
travel through all reach the asset by the alias from here on,
exactly as they do by its identity — so a material naming a
shader by its alias resolves, and content written against an
older name keeps working after a rename. The name survives
writes to the asset's own files.
Raises when the name already resolves to a DIFFERENT asset — an
alias extends the identity namespace and never takes a name out
of another asset's hands — and when it is shaped like a guid or
a VFS path, forms that resolve before identity lookup, so an
alias in that shape could never answer.
Parameters
refRefArg— The asset gaining the name.aliasstring— The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
asset.alias("@builtin::shaders.pbr", "standard")
modules/asset/aliases
aliases(ref: RefArg): { string }
The additional names this asset answers to, beyond its own
identity — what asset.alias registered, plus the
package-relative ~pkg.tail form when the asset lives inside a
package.
Parameters
refRefArg— Any name the asset has.
for _, n in asset.aliases("pbr") do print(n) end
modules/asset/canCreate
canCreate(typeName: string): boolean
Whether asset.create can instance typeName: the type declares
creation logic (a behavior.luau onCreate hook) or ships a
template/ skeleton the hookless fallback clones. A type with neither
— one whose instances only arrive by import — answers false. The query
a creation UI derives its offering from, so what it offers is what
asset.create accepts.
Parameters
typeNamestring— Registered asset type (e.g. "material", "scene").
if asset.canCreate(kind) then asset.create(kind, name) end
modules/asset/categories
categories(): { string }
List every asset category the engine currently recognises.
Use to discover valid type argument values for the rest of
asset.*.
for _, c in asset.categories() do print(c) end
modules/asset/containing
containing(path: string): AssetRef?
Walk path's ancestors and return an AssetRef handle for
the OUTERMOST category-folder containing it (e.g. main.scene
for "/source/scenes/main.scene/scene.json"). Returns nil for
paths outside any registered asset type.
Parameters
pathstring— VFS path to inspect.
local a = asset.containing("/source/scenes/main.scene/scene.json")
modules/asset/cpuResident
cpuResident(ref: RefArg, typeName: string?): boolean
True when the asset is CPU-resident — a live script-component context
holds it (a component's assetRef field, or an imperative
asset.resolve/ref made while a component is the caller), which is what
warms its bytes into memory. The CPU pool is a different pool from the
device's: asset.observe().cpu lists it, asset.observe().textures /
.meshes list what the device holds, and an asset can be in one and not
the other.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring?(optional) — Category to restrict the match to. Omit to search every category.
if asset.cpuResident(ref) then print("bytes are warm") end
modules/asset/create
create(typeName: string, name: string, opts: { [string]: any }?): AssetRef
Instance a new asset of an existing type. Runs the type's
behavior.luau onCreate(name, opts) hook to produce the asset's
files, then writes them under /source/<name>.<type>/. This is the
single generic asset-creation API. Refuses to clobber an existing
edit-mode asset unless opts.overwrite = true, which re-authors it in
place and keeps the existing guid (only the checksum changes). Pairs with
asset.exists for content generators that re-run over the same names.
A create made from a script component's callback or a scene entrypoint is
output the world reproduces on every load, so it is filed in the ephemeral
/runtime/assets/ store instead, where the saved manifest never carries a
second copy of it. name and folder spell the same IDENTITY in either
store, so a reference written against that identity resolves the asset
wherever the call filed it, and one generator run from an execute and
from a component names one asset.
Parameters
typeNamestring— The asset category to instance — one ofasset.categories().namestring— The new asset's name.opts{ [string]: any }?(optional) — Optional table forwarded to the type'sonCreatehook, minus four framework keys consumed here and never seen by the hook:folder(a relative subfolder under/sourceto author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix),into(author INSIDE a resolved container ref),dest(an absolute destination path), andoverwrite(re-author in place, keeping the guid).
local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild
modules/asset/declareReferenceArg
declareReferenceArg(call: string, position: number, assetType: string)
Declare that call's argument at 1-based position names an
asset of type, so a string literal written there is recorded as a
reference. The positional counterpart of
asset.declareReferenceField, for a call that takes its asset as a
plain argument — including a world's own spawn helper, which is
where a name most often stops being visible to the reference graph.
A lookup whose asset is its FIRST argument (asset.resolve and its
siblings) is already read and needs no declaration. Only a literal —
or a name the file holds in a top-level string constant — is
recorded; anything computed is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.positionnumber— Which argument holds the name, counting from 1.assetTypestring
asset.declareReferenceArg("spawnModel", 3, "mesh")
modules/asset/declareReferenceField
declareReferenceField(call: string, field: string, assetType: string)
Declare that call's options table names an asset of type in
its field, so a string literal written there is recorded as a
reference by whatever writes the file. This is what puts an API
that takes an asset BY NAME into the reference graph: the named
asset becomes a dependency, travels with the content that names it
into a pack or a pull, and a name nothing answers to becomes an
unresolved dependency worldValidation reports and the push gate
refuses. A field holding a TABLE of names — a material's textures
— records every name in it. Declare once, beside the API; a call
taking its asset as the FIRST positional argument is already read
and needs no declaration. Only a literal is recorded; a computed
name resolves at runtime and is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.fieldstring— The options-table field holding the name, read at the table's own level.assetTypestring
asset.declareReferenceField("fx.beam", "material", "material")
modules/asset/declareReferenceKey
declareReferenceKey(assetType: string, key: string, refType: string)
Declare that, in a data file belonging to an assetType asset,
the top-level key names an asset of type — a .material's
mat.yaml naming the shader it draws with and the textures it
binds. The names a format holds are references as surely as ones
written in code: recording them carries a material's shader along
with the material into a pack or a pull, and turns a name nothing
answers to into an unresolved dependency instead of a surface that
renders as the magenta error material. A key holding a table of
names records one per entry.
Parameters
assetTypestring— The category owning the file, e.g. "material".keystring— The top-level key holding the name(s).refTypestring
asset.declareReferenceKey("material", "shader", "shader")
modules/asset/deps
deps(ref: RefArg, type: string?): DepsResult
Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end
modules/asset/describe
describe(typeName: string): DescribeResult
The creation contract for an asset type: the parameters its
onCreate(name, opts) hook accepts, as data. kind is "schema"
(typed contract), "legacy" (untyped opts — anything passes), "none"
(template scaffold — takes no opts), or "error" (the type's schema
failed to parse; error says why). contract is the human-readable
rendering validation errors print.
Parameters
typeNamestring— Registered asset type to describe (e.g. "texture").
local contract = asset.describe("texture").contract
modules/asset/diagnose
diagnose(ref: RefArg): any
Why one asset can or cannot be used, read from the engine rather than
from what the caller asked for. Always carries usable; when false,
reason is one of asset.unusableReasons() and detail is the engine's
own message. primary names the file the type's declared primary list
resolved to, so an asset that loaded a preview image instead of its
payload shows the wrong filename rather than a successful load. The
payload's bytes are read by the engine's own decoder wherever it has one
for that container, so usable is the verdict a load would reach and the
call costs that decode.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end
modules/asset/exists
exists(name: string, typeName: string): boolean
Parameters
namestringtypeNamestring
modules/asset/get_field
get_field(ref: RefArg, key: string): any
Read one top-level field from the asset's .metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table
modules/asset/gpuResident
gpuResident(ref: RefArg): boolean
True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))
modules/asset/guid
guid(ref: RefArg, type: string?): string
Return the guid for an asset.
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
local g = asset.guid("@builtin::components.Camera")
modules/asset/has_field
has_field(ref: RefArg, key: string): boolean
True when the asset's .metadata carries the named field.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
if asset.has_field("brick", "author") then end
modules/asset/has_tag
has_tag(ref: RefArg, tag: string): boolean
True when the asset's .metadata.tags contains tag.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to check for.
if asset.has_tag("brick", "wip") then end
modules/asset/identity
identity(ref: RefArg, type: string?): string
Return the canonical identity for an asset.
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
local id = asset.identity("brick")
modules/asset/import
import(path: string): string?
Import a raw source file NOW and return the produced asset path
(a .bundle for a model, .texture for an image, .audio for a
sound, …), or nil if no importer claims it. This is the deterministic,
on-demand counterpart to the engine's automatic import-on-write: it runs
in the calling task and returns only when the import is complete. Pair it
with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the
raw bytes without firing the automatic importer, then asset.import(path)
imports them under your control, so you can act on the result instead of
polling for the import to appear.
Parameters
pathstring— The raw source VFS path to import (e.g. a just-written.glb).
local bundle = asset.import("/zero/source/generated/chest.glb")
modules/asset/inspect
inspect(ref: RefArg, type: string?): InspectRecord
Everything known about one asset in a single record: identity, guid,
source, type, scope and origin, its description and tags, the ref methods
its type exposes, and the type's own inspect detail when it declares one.
The read-everything counterpart to asset.resolve, which hands back a ref.
Parameters
refRefArg— AnAssetRef, an identity string, or a path.typestring?(optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)
modules/asset/list
list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?): ListResult
Query registered assets, returning each match as a resolved
AssetRef handle. Every filter narrows the same enumeration and
they compose: path selects a VFS subtree (the folder and
everything under it), type keeps only those asset types within
it, scope keeps only that scope, and fields keeps only assets
whose .metadata matches. type and path each take one value or
a list matching any of its entries, and all / any / none
group whole filters — none excludes what it matches. order,
limit, and offset shape the result: matches come back ordered
by identity unless order names another field
(name / path / type / guid).
Each entry is the same envelope asset.resolve returns (__ref /
type / name / guid / identity / path), so it can be passed
anywhere an AssetRef is accepted, and the result carries
:first() / :random() / :filter() / :sort() and friends.
type takes the same values asset.categories() lists. The first
positional argument is a path when it is absolute, a type
otherwise. An unknown key raises, as does a table setting both
type and its older spelling category.
A static (literal) type or path makes the enumeration part of the
calling file's content dependencies when it is saved — the set
travels with published content, so consumers get at-least the
authoring world's assets.
Parameters
type_or_opts(AssetCategory | ListOpts)?(optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.scopestring?(optional) — Scope filter (when first arg is a type).optsListOpts?(optional) — The query table — seeListOpts.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()
modules/asset/list_field_values
list_field_values(key: string): { any }
Distinct values seen for the named field across every
asset's .metadata.
Parameters
keystring— Field name.
local authors = asset.list_field_values("author")
modules/asset/list_fields
list_fields(): { string }
Distinct top-level field keys observed across every asset's
.metadata. Useful for tooling discovering custom keys in use.
for _, k in asset.list_fields() do print(k) end
modules/asset/meta
meta(ref: RefArg, type: string?): AssetMeta
Read the asset's engine-owned identity record (guid /
checksum). Distinct from .metadata (agent-editable);
for that use asset.metadata.
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
local m = asset.meta("brick") -- { guid = ..., checksum = ... }
modules/asset/metadata
metadata(ref: RefArg, type: string?): AssetMeta
Read the asset's agent-editable .metadata sidecar as a
Lua table. Missing sidecar returns {}. Distinct from
asset.meta (engine-owned).
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
local md = asset.metadata("brick")
modules/asset/observe
observe(): any
What the engine is holding for content right now, in one reading:
textures and meshes (one row per resource the device holds, each with
the bytes it costs, its dimensions or buffer split, and where it came
from), cpu (one row per asset a live script-component context holds),
and totals — the aggregates those rows sum to, so the listing reconciles
against renderer.textureMemory() and renderer.gpuMemory().
Each pool is named because they are different pools: an asset can be on
the device and not CPU-resident, or the reverse. devicePublished is
false when no renderer has published an inventory and cpuPublished when
the scripting VM has not published its pool — an engine that cannot answer
reads differently from one answering with nothing resident.
local r = asset.observe() print(#r.textures, r.totals.textureBytes)
modules/asset/preview
preview(ref: RefArg, opts: { [string]: any }?, type: string?): { [string]: any }
Render a preview of an asset. Resolves the ref and dispatches to its
type's preview ref-method when present; otherwise returns the
{ available = false } sentinel ("no preview available for this type").
Parameters
refRefArg— Any name the asset has.opts{ [string]: any }?(optional) — Optional{ size = { width, height }, angle = { yaw, pitch } }.typestring?(optional) — Category hint (optional).
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })
modules/asset/primaryFile
primaryFile(ref: RefArg): any
The file the asset type's declared primary list resolves to inside
this asset, as the loader itself resolves it. resolved is false when no
declaration matched and path is then absent; declared is the type's
own primary list in match order.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
print(asset.primaryFile("myTex").path)
modules/asset/ref
ref(ref: RefArg, type: string?): AssetRef
Build a reference handle for an asset — the canonical ref
envelope constructor. Identical shape to asset.resolve;
preferred name for the author-side use case (embedding refs in
YAML / JSON / Luau output).
Naming the asset here reads exactly as naming it in asset.resolve,
down to raising on a miss: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to reference — an identity, a guid, a VFS path, or a handle.typestring?(optional) — Category hint (optional).
local r = asset.ref("animations.idle", "animation")
modules/asset/reloadPending
reloadPending(ref: RefArg, typeName: string?): boolean
True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring?(optional) — Category to restrict the match to. Omit to search every category.
repeat task.wait() until not asset.reloadPending(ref)
modules/asset/reloadSeq
reloadSeq(ref: RefArg, typeName: string?): number
How many content-change reloads this asset has been through — the
count of onAssetReload dispatches the engine has RUN for it. A write to
a file inside an asset does not reload it on the spot: the writes of one
authoring step are collected for a settle window and the reload runs on a
later frame. Read this, write, then poll for a larger number to learn the
write's reload has actually reached subscribers. Monotonic per asset and
session-scoped; 0 for an asset whose content has not changed since boot.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring?(optional) — Category to restrict the match to. Omit to search every category.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at
modules/asset/remove_field
remove_field(ref: RefArg, key: string)
Remove one top-level field from the asset's .metadata.
No-op when the field isn't present.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
asset.remove_field("brick", "author")
modules/asset/remove_tag
remove_tag(ref: RefArg, tag: string)
Remove a tag from the asset's .metadata.tags. No-op when
the tag isn't present.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to remove.
asset.remove_tag("brick", "wip")
modules/asset/resolve
resolve<C>(ref: RefArg, type: (C & string)?): AssetRef<C>
Find an asset. The returned handle carries every name form
the asset has (guid, identity, path, type) so
downstream code can read any one of them without calling
resolve again. Raises when ref resolves to no asset — or, with a
type, to no asset of that type — and when ref reaches more than one
asset, where it names the candidates for you to pick from instead of
picking one of them. A <scope>::-qualified identity reaches exactly one:
@root::name for the asset this world holds at its source root, the
library identity (@builtin::…) for a library's. For the same lookup
answering a miss with nil, use asset.tryResolve(ref, type).
A name written as a string LITERAL is recorded as this source's
dependency on that asset, so the asset travels with the content and
still resolves once someone installs it in another world. A COMPUTED
name cannot be written down, so nothing pins what it reaches: that is a
dynamic resolve — free in a tool, refused on the gameplay path (a
component or scene entrypoint). asset.tryResolve, asset.ref and
asset.source read the name they are given exactly this way too, so
which of the four you reach for changes neither answer. To ask whether a
computed name has files without reaching a handle, use
asset.exists(name, type).
Parameters
refRefArg— The asset to find — an identity, a guid, a VFS path, or a handle.type(C & string)?(optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.pngafter the texture importer turned it intowall.texture) resolves to the promoted asset, and says so in the log once per reference.
local a = asset.resolve("@builtin::components.Camera")
modules/asset/set_field
set_field(ref: RefArg, key: string, value: any)
Set one field in the asset's .metadata, creating the sidecar if missing.
Sibling fields are preserved. When the new value AND the existing value are
both maps (objects), the new value DEEP-MERGES into the existing one, so
writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace.
Clear a whole field with asset.remove_field; replace the entire sidecar with
asset.set_metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.valueany(optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept
modules/asset/set_metadata
set_metadata(ref: RefArg, data: AssetMeta)
Replace the asset's .metadata sidecar with the given
table. Pass an empty table to clear all fields.
Parameters
refRefArg— Any name the asset has.dataAssetMeta— Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })
modules/asset/source
source(ref: RefArg, type: string?): string
Return the VFS source path for an asset.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
local p = asset.source("brick") -- "/source/brick.material"
modules/asset/storeHoldsIdentity
storeHoldsIdentity(identity: string, typeName: string, reproduced: boolean): boolean
Whether an asset of type typeName that name reaches has files in
the store asset.create writes from this context — the authored /source
tree, plus the ephemeral /runtime/assets/ store when the caller is a
script component or a scene entrypoint, which is where a create there
files it. name is any name the registry answers to for that asset: its
canonical identity ("a.b.thing"), a registered alias, or the bare leaf
("thing") — the spelling asset.list publishes as an entry's name and
the spelling asset.create was given alongside opts.folder. So the
if not asset.exists(n, t) then asset.create(t, n, opts) end pairing is
satisfied by its own create, spelled the way the create was, in either
store and at any folder depth. A plain existence probe — it does NOT
resolve a handle or pin a content dependency, so it is safe to call with a
COMPUTED name (unlike asset.resolve, whose handle would become a
static-pinned dependency).
The name is read the way asset.resolve reads one; the ANSWER comes from
the store, so a registered asset whose files live outside the store this
context writes — every @builtin:: asset among them — is false.
asset.tryResolve(name, typeName) asks the registry the wider question
and hands back the handle — and because it reaches the asset, the name it
is given follows the reference rule every lookup follows: a literal is
pinned, a computed one is a dynamic resolve. So a name this source
computed is what asset.exists is for.
This call names the asset FIRST and its category second, as every
asset.* call taking both does except asset.create(category, name, opts). A call carrying a category where the asset goes and a non-category
where the category goes is refused, naming which way round the call reads,
rather than reporting the asset absent.
Parameters
identitystringtypeNamestring— The asset type (e.g. "mesh", "texture", "material") — one ofasset.categories().reproducedboolean
if not asset.exists(meshName, "mesh") then asset.create("mesh", meshName, geo) end
if not asset.exists(n, "mesh") then asset.create("mesh", n, { folder = "props", positions = p, indices = i }) end
modules/asset/tags
tags(ref: RefArg): { string }
Convenience read of the .metadata.tags array.
Parameters
refRefArg— Any name the asset has.
for _, t in asset.tags("brick") do print(t) end
modules/asset/tryResolve
tryResolve(ref: RefArg, type: string?, base: string?): AssetRef?
The same lookup asset.resolve performs, answering a miss with nil
instead of raising. Every name form, the same type narrowing, and the
same handle on success — so "use it if it is there" needs no pcall
around a call whose failure would otherwise be indistinguishable from a
real error.
This consults the asset REGISTRY, so it sees registered assets wherever
their files live, @builtin:: ones included. asset.exists(name, type)
answers the narrower question of whether an asset's files are present in
the current mode's store.
A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to look up — an identity, a guid, a VFS path, or a handle.typestring?(optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same asasset.resolve.basestring?(optional) — Referring VFS path a~/~.tailref expands against, the same asasset.resolve's — so the two answer the same question and differ only in what a miss is.
local mat = asset.tryResolve(name, "material")
modules/asset/typeRef
typeRef(target: RefArg): string?
Return the pinned asset_type reference (the type's guid)
that the asset is an instance of. Resolve the full type with
asset.resolve(asset.typeRef(target)). Returns nil for loose
files / assets with no pinned type.
Parameters
targetRefArg— Asset handle / identity / guid / VFS path.
local t = asset.resolve(asset.typeRef("brick"))
modules/asset/unusableReasons
unusableReasons(): { string }
Every reason asset.diagnose can report an asset unusable for, sorted.
for _, r in ipairs(asset.unusableReasons()) do print(r) end
modules/asset/validate
validate(ref: RefArg, type: string?): ValidateResult
Validate an asset folder against its type's type.yaml, plus
the type's own semantic validation. Structural problems come from
type.yaml — missing required files, unsatisfied one_of_group
alternatives, and (when allow_unlisted: false) unexpected
children. validated = false when no type.yaml is registered
— nothing structural to check. On top of that, when the asset's
type ships a behavior.luau exporting a top-level
validate(assetRef) -> { { code, message, severity? } }, its
reported problems (severity defaults to "error") are appended to
problems; error-severity problems flip ok to false, warnings
leave it untouched. A hook that raises or returns a non-table is
itself reported as a validate.hook_failed error problem — a
broken hook blocks. A type with no validate export behaves
exactly as the structural check alone.
world.push calls this per user asset, so a type's semantic
validation is enforced at publish time with no further wiring.
Parameters
refRefArg— Any name the asset has.typestring?(optional) — Category hint (optional).
local v = asset.validate("@builtin::components.Camera")
modules/asset/warmup
warmup(ref: RefArg, opts: WarmupOpts?): WarmupResult
Warm an asset's bytes into CPU memory and follow its declared
content dependencies to each referenced asset, deduped by
guid. Type-agnostic (reads the generic ref graph) and CPU-only —
never touches the GPU. Side-effect-free name resolution (uses
asset.guid/asset.deps, not asset.resolve).
Parameters
refRefArg— Any name the root asset has — handle, identity, guid, or path.optsWarmupOpts?(optional) — Optional{ vias, max }— restrict ref-edge kinds / cap closure size.
local w = asset.warmup("@builtin::scenes.test_arena")
typed/builtin//modules/api/engine/asset/asset/add_tag
asset.add_tag(ref: RefArg, tag: string)
Add a tag to the asset's .metadata.tags. Idempotent.
Creates the sidecar and the tags array if missing.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to add.
asset.add_tag("brick", "wip")
typed/builtin//modules/api/engine/asset/asset/alias
asset.alias(ref: RefArg, alias: string) -> boolean
Add a name the asset answers to. asset.resolve, leaf
shorthand, and the typed-argument coercion that content refs
travel through all reach the asset by the alias from here on,
exactly as they do by its identity — so a material naming a
shader by its alias resolves, and content written against an
older name keeps working after a rename. The name survives
writes to the asset's own files.
Raises when the name already resolves to a DIFFERENT asset — an
alias extends the identity namespace and never takes a name out
of another asset's hands — and when it is shaped like a guid or
a VFS path, forms that resolve before identity lookup, so an
alias in that shape could never answer.
Parameters
refRefArg— The asset gaining the name.aliasstring— The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
Returns boolean — True when newly added, false when the asset already answered to it.
asset.alias("@builtin::shaders.pbr", "standard")
typed/builtin//modules/api/engine/asset/asset/aliases
asset.aliases(ref: RefArg) -> { string }
The additional names this asset answers to, beyond its own
identity — what asset.alias registered, plus the
package-relative ~pkg.tail form when the asset lives inside a
package.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of alias names in canonical identity form.
for _, n in asset.aliases("pbr") do print(n) end
typed/builtin//modules/api/engine/asset/asset/canCreate
asset.canCreate(typeName: string) -> boolean
Whether asset.create can instance typeName: the type declares
creation logic (a behavior.luau onCreate hook) or ships a
template/ skeleton the hookless fallback clones. A type with neither
— one whose instances only arrive by import — answers false. The query
a creation UI derives its offering from, so what it offers is what
asset.create accepts.
Parameters
typeNamestring— Registered asset type (e.g. "material", "scene").
Returns boolean — true when asset.create(typeName, …) can produce one.
if asset.canCreate(kind) then asset.create(kind, name) end
typed/builtin//modules/api/engine/asset/asset/categories
asset.categories() -> { string }
List every asset category the engine currently recognises.
Use to discover valid type argument values for the rest of
asset.*.
Returns { string } — Array of category names.
for _, c in asset.categories() do print(c) end
typed/builtin//modules/api/engine/asset/asset/containing
asset.containing(path: string) -> AssetRef?
Walk path's ancestors and return an AssetRef handle for
the OUTERMOST category-folder containing it (e.g. main.scene
for "/source/scenes/main.scene/scene.json"). Returns nil for
paths outside any registered asset type.
Parameters
pathstring— VFS path to inspect.
Returns AssetRef? — AssetRef handle, or nil.
local a = asset.containing("/source/scenes/main.scene/scene.json")
typed/builtin//modules/api/engine/asset/asset/cpuResident
asset.cpuResident(ref: RefArg, typeName: string?) -> boolean
True when the asset is CPU-resident — a live script-component context
holds it (a component's assetRef field, or an imperative
asset.resolve/ref made while a component is the caller), which is what
warms its bytes into memory. The CPU pool is a different pool from the
device's: asset.observe().cpu lists it, asset.observe().textures /
.meshes list what the device holds, and an asset can be in one and not
the other.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true when a live context holds it.
if asset.cpuResident(ref) then print("bytes are warm") end
typed/builtin//modules/api/engine/asset/asset/create
asset.create(typeName: string, name: string, opts: { [string]: any }?) -> AssetRef
Instance a new asset of an existing type. Runs the type's
behavior.luau onCreate(name, opts) hook to produce the asset's
files, then writes them under /source/<name>.<type>/. This is the
single generic asset-creation API. Refuses to clobber an existing
edit-mode asset unless opts.overwrite = true, which re-authors it in
place and keeps the existing guid (only the checksum changes). Pairs with
asset.exists for content generators that re-run over the same names.
A create made from a script component's callback or a scene entrypoint is
output the world reproduces on every load, so it is filed in the ephemeral
/runtime/assets/ store instead, where the saved manifest never carries a
second copy of it. name and folder spell the same IDENTITY in either
store, so a reference written against that identity resolves the asset
wherever the call filed it, and one generator run from an execute and
from a component names one asset.
Parameters
typeNamestring— Registered asset type to instance (e.g. "material", "texture").namestring— Destination asset name (becomes/source/<name>.<typeName>). A bare identity — passopts.folderto place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare:^[A-Za-z][A-Za-z0-9_]*$unless itsbehavior.luauexports anamePattern, asguidedoes to takegetting-startedand01-overview. This call names the category FIRST and the asset second. Every otherasset.*call taking both names them the other way round —asset.exists(name, category),asset.tryResolve(ref, category)— so a create-then-check pair readsif not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.opts{ [string]: any }(optional) — Optional table forwarded to the type'sonCreatehook, minus four framework keys consumed here and never seen by the hook:folder(a relative subfolder under/sourceto author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix),into(author INSIDE a resolved container ref),dest(an absolute destination path), andoverwrite(re-author in place, keeping the guid).
Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.
local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild
typed/builtin//modules/api/engine/asset/asset/declareReferenceArg
asset.declareReferenceArg(call: string, position: number, assetType: string)
Declare that call's argument at 1-based position names an
asset of type, so a string literal written there is recorded as a
reference. The positional counterpart of
asset.declareReferenceField, for a call that takes its asset as a
plain argument — including a world's own spawn helper, which is
where a name most often stops being visible to the reference graph.
A lookup whose asset is its FIRST argument (asset.resolve and its
siblings) is already read and needs no declaration. Only a literal —
or a name the file holds in a top-level string constant — is
recorded; anything computed is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.positionnumber— Which argument holds the name, counting from 1.assetTypestring
asset.declareReferenceArg("spawnModel", 3, "mesh")
typed/builtin//modules/api/engine/asset/asset/declareReferenceField
asset.declareReferenceField(call: string, field: string, assetType: string)
Declare that call's options table names an asset of type in
its field, so a string literal written there is recorded as a
reference by whatever writes the file. This is what puts an API
that takes an asset BY NAME into the reference graph: the named
asset becomes a dependency, travels with the content that names it
into a pack or a pull, and a name nothing answers to becomes an
unresolved dependency worldValidation reports and the push gate
refuses. A field holding a TABLE of names — a material's textures
— records every name in it. Declare once, beside the API; a call
taking its asset as the FIRST positional argument is already read
and needs no declaration. Only a literal is recorded; a computed
name resolves at runtime and is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.fieldstring— The options-table field holding the name, read at the table's own level.assetTypestring
asset.declareReferenceField("fx.beam", "material", "material")
typed/builtin//modules/api/engine/asset/asset/declareReferenceKey
asset.declareReferenceKey(assetType: string, key: string, refType: string)
Declare that, in a data file belonging to an assetType asset,
the top-level key names an asset of type — a .material's
mat.yaml naming the shader it draws with and the textures it
binds. The names a format holds are references as surely as ones
written in code: recording them carries a material's shader along
with the material into a pack or a pull, and turns a name nothing
answers to into an unresolved dependency instead of a surface that
renders as the magenta error material. A key holding a table of
names records one per entry.
Parameters
assetTypestring— The category owning the file, e.g. "material".keystring— The top-level key holding the name(s).refTypestring
asset.declareReferenceKey("material", "shader", "shader")
typed/builtin//modules/api/engine/asset/asset/deps
asset.deps(ref: RefArg, type: string?) -> DepsResult
Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns DepsResult — { deps = { { asset_guid, origin, literal, via, ... } } }.
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end
typed/builtin//modules/api/engine/asset/asset/describe
asset.describe(typeName: string) -> DescribeResult
The creation contract for an asset type: the parameters its
onCreate(name, opts) hook accepts, as data. kind is "schema"
(typed contract), "legacy" (untyped opts — anything passes), "none"
(template scaffold — takes no opts), or "error" (the type's schema
failed to parse; error says why). contract is the human-readable
rendering validation errors print.
Parameters
typeNamestring— Registered asset type to describe (e.g. "texture").
Returns DescribeResult — the creation contract.
local contract = asset.describe("texture").contract
typed/builtin//modules/api/engine/asset/asset/diagnose
asset.diagnose(ref: RefArg) -> any
Why one asset can or cannot be used, read from the engine rather than
from what the caller asked for. Always carries usable; when false,
reason is one of asset.unusableReasons() and detail is the engine's
own message. primary names the file the type's declared primary list
resolved to, so an asset that loaded a preview image instead of its
payload shows the wrong filename rather than a successful load. The
payload's bytes are read by the engine's own decoder wherever it has one
for that container, so usable is the verdict a load would reach and the
call costs that decode.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — DiagnoseRecord
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end
typed/builtin//modules/api/engine/asset/asset/exists
asset.exists(name: string, typeName: string) -> boolean
Parameters
namestringtypeNamestring
Returns boolean
typed/builtin//modules/api/engine/asset/asset/get_field
asset.get_field(ref: RefArg, key: string) -> any
Read one top-level field from the asset's .metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns any — Field value or nil.
local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table
typed/builtin//modules/api/engine/asset/asset/gpuResident
asset.gpuResident(ref: RefArg) -> boolean
True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns boolean — true when the device holds it.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))
typed/builtin//modules/api/engine/asset/asset/guid
asset.guid(ref: RefArg, type: string?) -> string
Return the guid for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Guid.
local g = asset.guid("@builtin::components.Camera")
typed/builtin//modules/api/engine/asset/asset/has_field
asset.has_field(ref: RefArg, key: string) -> boolean
True when the asset's .metadata carries the named field.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns boolean — True when present.
if asset.has_field("brick", "author") then end
typed/builtin//modules/api/engine/asset/asset/has_tag
asset.has_tag(ref: RefArg, tag: string) -> boolean
True when the asset's .metadata.tags contains tag.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to check for.
Returns boolean — True when present.
if asset.has_tag("brick", "wip") then end
typed/builtin//modules/api/engine/asset/asset/identity
asset.identity(ref: RefArg, type: string?) -> string
Return the canonical identity for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Canonical identity.
local id = asset.identity("brick")
typed/builtin//modules/api/engine/asset/asset/import
asset.import(path: string) -> string?
Import a raw source file NOW and return the produced asset path
(a .bundle for a model, .texture for an image, .audio for a
sound, …), or nil if no importer claims it. This is the deterministic,
on-demand counterpart to the engine's automatic import-on-write: it runs
in the calling task and returns only when the import is complete. Pair it
with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the
raw bytes without firing the automatic importer, then asset.import(path)
imports them under your control, so you can act on the result instead of
polling for the import to appear.
Parameters
pathstring— The raw source VFS path to import (e.g. a just-written.glb).
Returns string? — The produced asset path, or nil when nothing claimed it.
local bundle = asset.import("/zero/source/generated/chest.glb")
typed/builtin//modules/api/engine/asset/asset/inspect
asset.inspect(ref: RefArg, type: string?) -> InspectRecord
Everything known about one asset in a single record: identity, guid,
source, type, scope and origin, its description and tags, the ref methods
its type exposes, and the type's own inspect detail when it declares one.
The read-everything counterpart to asset.resolve, which hands back a ref.
Parameters
refRefArg— AnAssetRef, an identity string, or a path.typestring(optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
Returns InspectRecord — The inspect record.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)
typed/builtin//modules/api/engine/asset/asset/list
asset.list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?) -> ListResult
Query registered assets, returning each match as a resolved
AssetRef handle. Every filter narrows the same enumeration and
they compose: path selects a VFS subtree (the folder and
everything under it), type keeps only those asset types within
it, scope keeps only that scope, and fields keeps only assets
whose .metadata matches. type and path each take one value or
a list matching any of its entries, and all / any / none
group whole filters — none excludes what it matches. order,
limit, and offset shape the result: matches come back ordered
by identity unless order names another field
(name / path / type / guid).
Each entry is the same envelope asset.resolve returns (__ref /
type / name / guid / identity / path), so it can be passed
anywhere an AssetRef is accepted, and the result carries
:first() / :random() / :filter() / :sort() and friends.
type takes the same values asset.categories() lists. The first
positional argument is a path when it is absolute, a type
otherwise. An unknown key raises, as does a table setting both
type and its older spelling category.
A static (literal) type or path makes the enumeration part of the
calling file's content dependencies when it is saved — the set
travels with published content, so consumers get at-least the
authoring world's assets.
Parameters
type_or_opts(AssetCategory | ListOpts)(optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.scopestring(optional) — Scope filter (when first arg is a type).optsListOpts(optional) — The query table — seeListOpts.
Returns ListResult — The matched AssetRef handles, as a result carrying query methods.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()
typed/builtin//modules/api/engine/asset/asset/list_field_values
asset.list_field_values(key: string) -> { any }
Distinct values seen for the named field across every
asset's .metadata.
Parameters
keystring— Field name.
Returns { any } — Array of distinct values.
local authors = asset.list_field_values("author")
typed/builtin//modules/api/engine/asset/asset/list_fields
asset.list_fields() -> { string }
Distinct top-level field keys observed across every asset's
.metadata. Useful for tooling discovering custom keys in use.
Returns { string } — Array of field names.
for _, k in asset.list_fields() do print(k) end
typed/builtin//modules/api/engine/asset/asset/meta
asset.meta(ref: RefArg, type: string?) -> AssetMeta
Read the asset's engine-owned identity record (guid /
checksum). Distinct from .metadata (agent-editable);
for that use asset.metadata.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — Metadata table.
local m = asset.meta("brick") -- { guid = ..., checksum = ... }
typed/builtin//modules/api/engine/asset/asset/metadata
asset.metadata(ref: RefArg, type: string?) -> AssetMeta
Read the asset's agent-editable .metadata sidecar as a
Lua table. Missing sidecar returns {}. Distinct from
asset.meta (engine-owned).
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — JSON-shaped table; empty when no sidecar exists.
local md = asset.metadata("brick")
typed/builtin//modules/api/engine/asset/asset/observe
asset.observe() -> any
What the engine is holding for content right now, in one reading:
textures and meshes (one row per resource the device holds, each with
the bytes it costs, its dimensions or buffer split, and where it came
from), cpu (one row per asset a live script-component context holds),
and totals — the aggregates those rows sum to, so the listing reconciles
against renderer.textureMemory() and renderer.gpuMemory().
Each pool is named because they are different pools: an asset can be on
the device and not CPU-resident, or the reverse. devicePublished is
false when no renderer has published an inventory and cpuPublished when
the scripting VM has not published its pool — an engine that cannot answer
reads differently from one answering with nothing resident.
Returns any — ResidencyReading
local r = asset.observe() print(#r.textures, r.totals.textureBytes)
typed/builtin//modules/api/engine/asset/asset/preview
asset.preview(ref: RefArg, opts: { [string]: any }?, type: string?) -> { [string]: any }
Render a preview of an asset. Resolves the ref and dispatches to its
type's preview ref-method when present; otherwise returns the
{ available = false } sentinel ("no preview available for this type").
Parameters
refRefArg— Any name the asset has.opts{ [string]: any }(optional) — Optional{ size = { width, height }, angle = { yaw, pitch } }.typestring(optional) — Category hint (optional).
Returns { [string]: any } — { available, imageBase64?, width?, height?, bounds?, stats?, reason? }.
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })
typed/builtin//modules/api/engine/asset/asset/primaryFile
asset.primaryFile(ref: RefArg) -> any
The file the asset type's declared primary list resolves to inside
this asset, as the loader itself resolves it. resolved is false when no
declaration matched and path is then absent; declared is the type's
own primary list in match order.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — { path: string?, resolved: boolean, isFolder: boolean, declared: { string } }
print(asset.primaryFile("myTex").path)
typed/builtin//modules/api/engine/asset/asset/ref
asset.ref(ref: RefArg, type: string?) -> AssetRef
Build a reference handle for an asset — the canonical ref
envelope constructor. Identical shape to asset.resolve;
preferred name for the author-side use case (embedding refs in
YAML / JSON / Luau output).
Naming the asset here reads exactly as naming it in asset.resolve,
down to raising on a miss: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
typed/builtin//modules/api/engine/asset/asset/reloadPending
asset.reloadPending(ref: RefArg, typeName: string?) -> boolean
True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true while a content-change reload is still owed.
repeat task.wait() until not asset.reloadPending(ref)
typed/builtin//modules/api/engine/asset/asset/reloadSeq
asset.reloadSeq(ref: RefArg, typeName: string?) -> number
How many content-change reloads this asset has been through — the
count of onAssetReload dispatches the engine has RUN for it. A write to
a file inside an asset does not reload it on the spot: the writes of one
authoring step are collected for a settle window and the reload runs on a
later frame. Read this, write, then poll for a larger number to learn the
write's reload has actually reached subscribers. Monotonic per asset and
session-scoped; 0 for an asset whose content has not changed since boot.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns number — content-change reloads dispatched for this asset.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at
typed/builtin//modules/api/engine/asset/asset/remove_field
asset.remove_field(ref: RefArg, key: string)
Remove one top-level field from the asset's .metadata.
No-op when the field isn't present.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
asset.remove_field("brick", "author")
typed/builtin//modules/api/engine/asset/asset/remove_tag
asset.remove_tag(ref: RefArg, tag: string)
Remove a tag from the asset's .metadata.tags. No-op when
the tag isn't present.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to remove.
asset.remove_tag("brick", "wip")
typed/builtin//modules/api/engine/asset/asset/resolve
asset.resolve(ref: RefArg, type: (C & string)?) -> AssetRef<C>
Find an asset. The returned handle carries every name form
the asset has (guid, identity, path, type) so
downstream code can read any one of them without calling
resolve again. Raises when ref resolves to no asset — or, with a
type, to no asset of that type — and when ref reaches more than one
asset, where it names the candidates for you to pick from instead of
picking one of them. A <scope>::-qualified identity reaches exactly one:
@root::name for the asset this world holds at its source root, the
library identity (@builtin::…) for a library's. For the same lookup
answering a miss with nil, use asset.tryResolve(ref, type).
A name written as a string LITERAL is recorded as this source's
dependency on that asset, so the asset travels with the content and
still resolves once someone installs it in another world. A COMPUTED
name cannot be written down, so nothing pins what it reaches: that is a
dynamic resolve — free in a tool, refused on the gameplay path (a
component or scene entrypoint). asset.tryResolve, asset.ref and
asset.source read the name they are given exactly this way too, so
which of the four you reach for changes neither answer. To ask whether a
computed name has files without reaching a handle, use
asset.exists(name, type).
Parameters
refRefArg— The asset to find — an identity, a guid, a VFS path, or a handle.type(C & string)(optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.pngafter the texture importer turned it intowall.texture) resolves to the promoted asset, and says so in the log once per reference.
Returns AssetRef<C> — Asset handle, carrying the category when one was named — so the methods that category defines are checked on the result. Raises (rather than returning nil) on a miss, and on a name that reaches more than one asset.
local a = asset.resolve("@builtin::components.Camera")
typed/builtin//modules/api/engine/asset/asset/set_field
asset.set_field(ref: RefArg, key: string, value: any?)
Set one field in the asset's .metadata, creating the sidecar if missing.
Sibling fields are preserved. When the new value AND the existing value are
both maps (objects), the new value DEEP-MERGES into the existing one, so
writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace.
Clear a whole field with asset.remove_field; replace the entire sidecar with
asset.set_metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.valueany(optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept
typed/builtin//modules/api/engine/asset/asset/set_metadata
asset.set_metadata(ref: RefArg, data: AssetMeta)
Replace the asset's .metadata sidecar with the given
table. Pass an empty table to clear all fields.
Parameters
refRefArg— Any name the asset has.dataAssetMeta— Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })
typed/builtin//modules/api/engine/asset/asset/source
asset.source(ref: RefArg, type: string?) -> string
Return the VFS source path for an asset.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — VFS source path.
local p = asset.source("brick") -- "/source/brick.material"
typed/builtin//modules/api/engine/asset/asset/tags
asset.tags(ref: RefArg) -> { string }
Convenience read of the .metadata.tags array.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of tag strings.
for _, t in asset.tags("brick") do print(t) end
typed/builtin//modules/api/engine/asset/asset/tryResolve
asset.tryResolve(ref: RefArg, type: string?, base: string?) -> AssetRef?
The same lookup asset.resolve performs, answering a miss with nil
instead of raising. Every name form, the same type narrowing, and the
same handle on success — so "use it if it is there" needs no pcall
around a call whose failure would otherwise be indistinguishable from a
real error.
This consults the asset REGISTRY, so it sees registered assets wherever
their files live, @builtin:: ones included. asset.exists(name, type)
answers the narrower question of whether an asset's files are present in
the current mode's store.
A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to look up — an identity, a guid, a VFS path, or a handle.typestring(optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same asasset.resolve.basestring(optional) — Referring VFS path a~/~.tailref expands against, the same asasset.resolve's — so the two answer the same question and differ only in what a miss is.
Returns AssetRef? — Asset handle, or nil when the reference resolves to no asset.
local mat = asset.tryResolve(name, "material")
typed/builtin//modules/api/engine/asset/asset/typeRef
asset.typeRef(target: RefArg) -> string?
Return the pinned asset_type reference (the type's guid)
that the asset is an instance of. Resolve the full type with
asset.resolve(asset.typeRef(target)). Returns nil for loose
files / assets with no pinned type.
typed/builtin//modules/api/engine/asset/asset/unusableReasons
asset.unusableReasons() -> { string }
Every reason asset.diagnose can report an asset unusable for, sorted.
Returns { string } — Array of reason names.
for _, r in ipairs(asset.unusableReasons()) do print(r) end
typed/builtin//modules/api/engine/asset/asset/validate
asset.validate(ref: RefArg, type: string?) -> ValidateResult
Validate an asset folder against its type's type.yaml, plus
the type's own semantic validation. Structural problems come from
type.yaml — missing required files, unsatisfied one_of_group
alternatives, and (when allow_unlisted: false) unexpected
children. validated = false when no type.yaml is registered
— nothing structural to check. On top of that, when the asset's
type ships a behavior.luau exporting a top-level
validate(assetRef) -> { { code, message, severity? } }, its
reported problems (severity defaults to "error") are appended to
problems; error-severity problems flip ok to false, warnings
leave it untouched. A hook that raises or returns a non-table is
itself reported as a validate.hook_failed error problem — a
broken hook blocks. A type with no validate export behaves
exactly as the structural check alone.
world.push calls this per user asset, so a type's semantic
validation is enforced at publish time with no further wiring.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns ValidateResult — { ok, typeName, validated, problems }.
local v = asset.validate("@builtin::components.Camera")
typed/builtin//modules/api/engine/asset/asset/warmup
asset.warmup(ref: RefArg, opts: WarmupOpts?) -> WarmupResult
Warm an asset's bytes into CPU memory and follow its declared
content dependencies to each referenced asset, deduped by
guid. Type-agnostic (reads the generic ref graph) and CPU-only —
never touches the GPU. Side-effect-free name resolution (uses
asset.guid/asset.deps, not asset.resolve).
Parameters
refRefArg— Any name the root asset has — handle, identity, guid, or path.optsWarmupOpts(optional) — Optional{ vias, max }— restrict ref-edge kinds / cap closure size.
Returns WarmupResult — { closure, count } — the deduped guid closure warmed and its size.
local w = asset.warmup("@builtin::scenes.test_arena")