Asset
Promote the bundle's spawned children from temporary to permanent scene state and remove the Asset component, "baking" the bundle's contents directly into the owning scene. Subsequent saves persist the baked entities verbatim and stop replaying the bundle template.
Asset
Promote the bundle's spawned children from temporary to permanent scene state and remove the Asset component, "baking" the bundle's contents directly into the owning scene. Subsequent saves persist the baked entities verbatim and stop replaying the bundle template.
Location: src/lua/lib/components/Asset.component
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
conforms to
zero/source-extract/v2Asset Component Instantiates a scene-instantiable asset onto this entity through the uniform AssetRef contract `source:instantiate(target?, opts?)`. `source` accepts ANY asset whose type can be instantiated into a scene — gated by CAPABILITY (`Field.instantiableRef`), not a hardcoded type list — so a new scene-instantiable asset type works here the moment its assetType defines `instantiate`, with no change to this component. Today: a bundle explodes its entity_template hierarchy onto the owner; an avatar composes its body, controller, and animation onto the owner (the owner becomes the movable character); a mesh adds a Model; a dynamicAsset spawns its generated model as a child. Reacts to `source` changes and asset-content reloads, and tears the instance down on destroy. Stable child ids: on the FIRST spawn we call `bundle.instantiate` with the current `public.idMap` (empty → Rust generates deterministic ids via seed fallback) and capture the returned map back into `public.idMap`. The scene-save snapshotter persists this map into scene.json. On every subsequent spawn (restart, hot-reload, layers.load) `bundle.instantiate` receives the saved map and reuses every child id verbatim — cross-entity references to bundle children (e.g. `SkinnedModel.skeletonRoot = "ent_xxx"`) stay valid. Sparse overrides: `public.diff` captures per-child deltas vs the bundle's template. Anything a user/script changed on a spawned child since instantiation — position/rotation/scale, rename, render-layer membership, attributes, component add/remove, component public-state edits — lands in `diff` keyed by `original_id`, and `bundle.instantiate` replays it so the runtime state round-trips. The capture runs at save time (via `onBeforeSave`) and again whenever this component itself rebuilds the composition, so a content reload of the source hands the children back carrying what the world put on them.
despawnBundleChildren( ) → void
markBundleChildrenTemporary( ) → void
setTemporaryRecursive(rootId: ?, value: ?) → void
| arg | type | description |
|---|---|---|
| rootId | ? | |
| value | ? |
hostRenderLayer( ) → string
The render layer the entity this component composes onto is on, or nil when it is on `default` — which is what every composed part is born on, so a host on `default` changes nothing and every existing scene composes exactly as it did.
inheritHostLayer(rootId: string, layer: string) → void
Put every composed part the asset left on `default` onto the host's layer. A composed asset is what the host LOOKS like, so it is on the layer the host is on. Without this, putting an avatar on a layer moved its root and left every mesh it is drawn with on `default` — the layer said one thing and the picture another, and anything that excludes a layer, an environment capture above all, excluded an empty root and kept the body. A part the asset put on a layer of its own keeps it.
| arg | type | description |
|---|---|---|
| rootId | string | |
| layer | string |
assetRefKey(assetRef: any) → string
| arg | type | description |
|---|---|---|
| assetRef | any |
assetRefPath(assetRef: ?) → void
| arg | type | description |
|---|---|---|
| assetRef | ? |
assetRefGuid(t: ?) → void
Canonical identity for an AssetRef-shaped table. Returns the stable guid when the table looks like an asset reference, nil otherwise. Bundles persist a rich envelope in their entity_template (`{ __ref, guid, identity, path, name, type }`) but the runtime component snapshot only carries the slim form (`{ __ref, name, type }`). Deep field-by-field equality treats these as different, which flags every unchanged AssetRef field as "modified". Comparing by guid makes the two forms equivalent so the diff captures only real user edits.
| arg | type | description |
|---|---|---|
| t | ? |
deepEqual(a: ?, b: ?) → void
Deep-equals for plain tables / primitives. The component snapshot and template component data are both JSON-shaped — this covers the field types we compare (numbers, strings, bools, arrays, objects, AssetRef envelopes). AssetRef envelopes compare by guid so the rich template form and the slim runtime form are recognised as the same reference.
| arg | type | description |
|---|---|---|
| a | ? | |
| b | ? |
parseDefaultLiteral(v: any) → void
Parse the source-text default expression `asset.inspect` surfaces (e.g. `"true"`, `"\"auto\""`, `"nil"`, `"0.5"`) into a typed value. Returns (value, known); `known` is false for an expression that doesn't reduce to a scalar (a table, a call), so the caller keeps the field rather than guess its default. A value that is already typed (not a string) passes through as known.
| arg | type | description |
|---|---|---|
| v | any |
makeDefaultsCache( ) → void
Declared field defaults per component leaf type, memoized for one `buildDiff` pass. A runtime field the bundle template omitted is compared against its component's DECLARED default (not against template-absence): a field still holding its default is not a user edit and stays out of the diff, while a field moved off its default — e.g. a bake setting `Model.lightmapData` — is captured. Reads the same field defaults `asset.inspect` surfaces (as source-text literals).
parseBundleTemplate(content: ?) → void
Parse bundle-template bytes into { [original_id] = templateEntity }.
| arg | type | description |
|---|---|---|
| content | ? |
readBundleTemplateSource( ) → string
The bytes of the source's `entity_template`, as the VFS holds them now.
loadBundleTemplate( ) → void
Read the on-disk bundle template so we can compute `public.diff` as a sparse delta. Returns { [original_id] = templateEntity } or nil.
teardownInstance( ) → void
Tear down the current instantiation: despawn the spawned children and drop the provenance attribute (children's attributes go with them via despawn).
compositionIsLive( ) → boolean
Whether the composition `public.idMap` names is already standing. A direct `ref:instantiate()` composes inline and only then attaches this component to own the reference, handing over the map it produced — those entities are alive before awake() ever runs, so composing again would build a second body beside the first. A scene load arrives holding the same map with none of its entities alive, because the composition is spawned temporary and never saved, and composes.
applyAsset( ) → void
normalizeForDiff(value: ?, runtimeToOriginal: ?) → void
Normalize a runtime value for diffing against a template value. Cross- entity references inside a bundle live as `original_id` strings in the template (e.g. `SkinnedModel.skeletonRoot = "PolygonSyntyCharacter/Root"`) and as runtime entity ids in live state (e.g. `"ent_a9bbcab9f49d56e7"`). `bundle.instantiate`'s job IS to perform that remap, so the diff must undo it before comparing or every referenced-child field will be falsely flagged as modified on every save.
| arg | type | description |
|---|---|---|
| value | ? | |
| runtimeToOriginal | ? |
runtimeOwnsAttribute(key: any) → boolean
Whether an attribute name belongs to the RUNTIME rather than to the world. `bundleProvenance` is the live link `instantiate` writes across every id a compose produced, and a `_`-prefixed name is the contract the scene saver already spends on component data: the value lives on the live entity and the saved scene never holds it. The record this component keeps reads the rule on BOTH sides: such a name is neither captured as something the world wrote nor named as something the world cleared, so a record measured against a template that carries one describes the same world state as one measured against a template that does not.
| arg | type | description |
|---|---|---|
| key | any |
buildChildDiff(originalId: ?, runtimeId: ?, templateEntry: ?, runtimeToOriginal: ?, componentDefaults: ?) → void
Build a sparse delta for one child (runtime entity) vs its template entry. Returns the diff table, or nil when nothing differs. Pass `runtimeToOriginal` so cross-entity id references normalise back to their template form before comparing.
| arg | type | description |
|---|---|---|
| originalId | ? | |
| runtimeId | ? | |
| templateEntry | ? | |
| runtimeToOriginal | ? | |
| componentDefaults | ? |
typeLeaf(typeName: ?) → void
Build lookups keyed by the component type's LEAF name: templates store short names ("SkinnedModel") while the runtime reports full identities ("@builtin::components.SkinnedModel") — matching on the raw strings would flag every template component as removed+re-added.
| arg | type | description |
|---|---|---|
| typeName | ? |
looksUnresolved(runtimeValue: ?, templateValue: ?) → boolean
Compute a sparse per-field delta between two component-data tables. Returns (changed, removed) where `changed` is a map of keys whose value differs (carrying the RAW runtime value — still keyed on runtime ids so the saved diff restores user edits verbatim when the bundle is re-instantiated) and `removed` is an array of keys present in the template but absent at runtime. Nil when they are deep-equal after normalisation. Template-present keys diff against the template value. A key the template omitted diffs against the component's DECLARED default — the Rust-side template emitter is not uniform about which fields it serialises, so template-absence is not "expects nil". Comparing a template-absent field against its declared default keeps runtime defaults (e.g. `Model.enabled = true`) out of the diff while capturing a field moved off its default (e.g. a bake setting `Model.lightmapData`). A field the template FILLED reads back empty while the composition is still resolving: an asset reference that has not resolved reads nil, and an id reference into the composition (`SkinnedModel.skeletonRoot`) reads "" until the id remap lands. That is not the user clearing the field, and the diff is a record of what the USER changed. Recording it is unrecoverable rather than merely wrong: the diff replays over the template on every later load, so one save taken mid-compose pins the unresolved state forever — `skeletonRoot = ""` makes `SkinnedModel` drop the Skeleton outright, and a captured `removedFields = { "model" }` deletes the mesh reference on every load. Between "the user cleared a field the template filled" and "this has not resolved yet", the second is the safe reading: a genuine clear is re-recorded by the next save, while a wrong capture never heals.
| arg | type | description |
|---|---|---|
| runtimeValue | ? | |
| templateValue | ? |
namesSameAsset(named: any, live: any) → boolean
| arg | type | description |
|---|---|---|
| named | any | |
| live | any |
componentFieldDelta(leaf: ?, runtimeData: ?, templateData: ?) → void
| arg | type | description |
|---|---|---|
| leaf | ? | |
| runtimeData | ? | |
| templateData | ? |
snapshotEntity(runtimeId: ?, runtimeToOriginal: ?) → void
Collect the full transform + component snapshot for an entity, used when recording an `addedEntity` (runtime-only child that the bundle template does not know about). Returns nil when the entity proxy isn't live.
| arg | type | description |
|---|---|---|
| runtimeId | ? | |
| runtimeToOriginal | ? |
buildDiff(baseline: any?) → void
Measure the live composition against a bundle template and return the sparse per-child delta. `baseline` names the template to measure against; without one the source's current `entity_template` is read, which is the baseline a save wants.
| arg | type | description |
|---|---|---|
| baseline | any? |
walkDescendants(parentRuntimeId: ?, underTemporary: ?) → void
| arg | type | description |
|---|---|---|
| parentRuntimeId | ? | |
| underTemporary | ? |
captureLiveOverrides( ) → void
Record what the world has written onto the live composition, so the rebuild that follows replays it instead of handing back bare template children. The baseline is the template the standing children were built from — measuring against it separates a field the world changed from a field the template has changed since, and only the first belongs in the record the rebuild replays.
awake( ) → void
onPropertyChanged(key: ?, value: ?, oldValue: ?) → void
| arg | type | description |
|---|---|---|
| key | ? | |
| value | ? | |
| oldValue | ? |
onAssetReload(field: ?) → void
Generic asset-content reload hook fired by the engine when the contents of an asset referenced by one of this component's declared asset fields change on disk (any VFS write under the asset's `.meta`-covered path). For the `source` field this means the bundle's `entity_template` (or any file inside the bundle directory) was rewritten — tear the current instance down and re-instantiate so the spawned children match the new template. The bundle's guid hasn't changed, only its contents, so the standard `applyAsset` early-return on identical guids would otherwise skip the reload. Teardown runs FIRST — the previous children must despawn before the fresh instantiation, or each reload event stacks another full copy of the hierarchy under the owner.
| arg | type | description |
|---|---|---|
| field | ? |
onBeforeSave( ) → void
shortName(typeName: any) → string
The last part of a component's authored name: `Model` of `@builtin::components.Model`.
| arg | type | description |
|---|---|---|
| typeName | any |
describeChildDiff(d: any) → void
What one child row of a diff says was changed, in words: the fields of the entity itself, then each component added, removed or edited.
| arg | type | description |
|---|---|---|
| d | any |
overrides( ) →
How this instance stands against its asset, measured now: every part the asset composed (`linked` — as the asset has it, `modified` — edited in the scene, `removed` — deleted from the scene), and every entity added under the instance that the asset does not have (`added`). A modified part lists what was changed. The same measurement a scene save records as this component's `diff`, taken against the asset the parts were built from.
examples
local o = entity("lamp_east").component.get("Asset"):overrides()revert(part: string?) → boolean
Put a part back the way the asset has it — or, with no argument, the whole instance. A modified part loses its edits, a removed part comes back, and an entity added under the instance is taken away. The rebuilt parts keep their entity ids, so references to them stay valid.
| arg | type | description |
|---|---|---|
| part | string? | The part's entity id or its id in the asset (a part's `id` or `templateId` from `overrides()`); omit for every part. |
examples
entity("lamp_east").component.get("Asset"):revert("ent_3fa2c1")entity("lamp_east").component.get("Asset"):revert()bakeIntoScene( ) →
Promote the bundle's spawned children from temporary to permanent scene state and remove the Asset component, "baking" the bundle's contents directly into the owning scene. Subsequent saves persist the baked entities verbatim and stop replaying the bundle template.
examples
asset:bakeIntoScene()
onDestroy( ) → void
Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
agent_score is exposed.+ quality × 0.35
+ performance × 0.25
± compat factor
Usability ratings
Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".
Scoped to this part · feeds back into the world's score.