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

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.

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

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

Asset 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

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

argtypedescription
rootIdstring
layerstring

assetRefKey(assetRef: any) → string

argtypedescription
assetRefany

assetRefPath(assetRef: ?) → void

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

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

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

argtypedescription
vany

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

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

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

argtypedescription
keyany

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.

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

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

argtypedescription
runtimeValue?
templateValue?

namesSameAsset(named: any, live: any) → boolean

argtypedescription
namedany
liveany

componentFieldDelta(leaf: ?, runtimeData: ?, templateData: ?) → void

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

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

argtypedescription
baselineany?

walkDescendants(parentRuntimeId: ?, underTemporary: ?) → void

argtypedescription
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

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

argtypedescription
field?

onBeforeSave( ) → void

shortName(typeName: any) → string

The last part of a component's authored name: `Model` of `@builtin::components.Model`.

argtypedescription
typeNameany

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.

argtypedescription
dany

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.

argtypedescription
partstring?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.

17items
·
other · born here
▤file
▲ 0↑ born
▣
module · born here
❒asset
# component_snapshot Serialized snapshots of script-component public data. A component's `public` table is a live proxy — its fields live behind `__iter` / `__index` metamethods, so copying or JSON-encoding the proxy directly yields an empty table. This module materializes the plain, serializable form. It is the script-component parallel to `ecs.snapshot` (native components). ```lua local componentSnapshot = require("modules.component_snapshot") -- one component: live proxy -> plain table local data = componentSnapshot.snapshot(entity(id).component.get("Camera")) -- whole entity: { [type] = data }, named/multi instances nested as -- { [type] = { [instanceName or "__default"] = data } } local all = componentSnapshot.snapshotEntity(entity(id)) ``` `componentSnapshot.plainCopy(value)` materializes ONE live value the same way — a table behind a proxy is walked into a plain table, every other value passes through — for callers reading a single field rather than a whole component. Serialized form: entity refs become id strings, asset refs become `{ __ref, name, type }` envelopes, vectors and colors their plain-table forms; framework functions are omitted. `snapshot` returns nil for a proxy with no serializable fields. Persistence (scene serializer, bundle capture, prototype spawn) and inspection tooling (`debug.inspect`) read component data through this module. Live gameplay reads and writes stay on the proxy itself via `entity(id).component.get` / `getAll`.
▲ 0↑ born
▣
module · born here
❒asset
# json JSON encode/decode library for Luau. Encodes Lua values to JSON strings and decodes JSON strings back to Lua values. Used for communication with the Rust side of the engine, the VFS read/write bridge, and any wire-format that needs JSON. Pure Luau, no engine dependencies. Compact and pretty-printed encoders, plus a hand-rolled decoder that streams the input by position so it works under WASM as well as native. ## Exports - `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`. - `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly). - `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array. - `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure. ## Usage ```luau local Json = require("@builtin::modules.json") local widget = { type = "button", text = "Click Me" } local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}' local pretty = Json.encodePretty(widget, " ") local decoded = Json.decode(compact) local v, err = Json.decode("oops") -- v = nil, err = error message ``` ## Notes - Object keys are sorted alphabetically in both encoders for consistent output across runs. - Numeric keys on objects are stringified at encode time (JSON has no numeric keys). Pure-integer key sets get detected as arrays via `isArray` and encoded with brackets. - NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round trips through `decode` recover `null` (Lua `nil`), so they don't preserve. - Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe. Only the BMP is covered; supplementary planes via surrogate pairs are not. - Functions encode as `null`. - Decode is character-streamed — no regex, no `string.match` patterns on the whole input — so the line-and-column information needs to be reconstructed from the position offset.
▲ 0↑ born
▣
module · born here
❒asset
# asset_instance_inspector The `Asset` component's custom entity-inspector view: an instance of an asset placed in the scene, read against the asset it came from. The generic field grid shows the component's `source`, `idMap` and `diff` — a reference and two opaque tables. This view shows what they mean instead: - a summary of how the instance stands — in sync with its asset, or how many parts are edited, added or removed in the scene; - the `source` field, with the reference chip's reveal / open / replace; - **Overrides** — every part that differs from the asset, with what changed (`position`, `name`, `Model tintR`, …) and a Revert (or Remove, for an entity added under the instance); - **Linked parts** — every part still as the asset has it; - Revert all to asset, and Unpack into scene. It reads the instance through the component's own `overrides()` and writes through `revert(part?)` and `bakeIntoScene()`, so the view holds no knowledge of how the diff is measured. `M.sections(entityId, proxy)` answers the view as plain data; `M.build(ctx)` renders it into the entity inspector's card with the Context's widgets, the `source` field going through `ctx.setSettings` as one undoable edit. ```luau local Inspector = require("@builtin::modules.editor.asset_instance_inspector") local sections = Inspector.sections(entityId, entity(entityId).component.get("Asset")) ``` The `Asset` component's `inspector.luau` hands `build` to the entity inspector as the card's `settings` section, for every entity carrying the component.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born
▣
module · born here
❒asset
# editorSelection Named selection scopes for the editor. A scope is an independently tracked, ordered set of typed refs plus a primary (the last ref added or clicked). Distinct scopes never clobber each other, so viewport, outliner, inspector and asset browser share one answer to "what is selected" per kind. A ref is `{ kind: string, id: string }` — a stable id, never a display string, so rename/move never invalidates a selection. ## Exports - `M.scope(name) -> Scope` — get/create a named scope handle. - `M.set(scope, refs)` — replace refs in order; primary becomes the last. - `M.get(scope) -> { Ref }` — refs in click order (fresh array). - `M.primary(scope) -> Ref?` — last-clicked ref, or nil. - `M.clear(scope)` — empty a scope. - `M.toggle(scope, ref)` — add if absent, remove if present. - `M.add(scope, refs)` — add each ref not already present; primary becomes the last added. - `M.remove(scope, refs)` — remove each of `refs`; primary becomes the last remaining or nil. - `M.contains(scope, ref) -> boolean` — membership by `(kind, id)`. - `M.count(scope) -> number` — the scope's selection size. - `M.subscribe(scope, fn) -> handle` / `M.unsubscribe(scope, handle) -> boolean`. - `M.context() -> { scope, refs, primary }` — last-focused scope, for command ctx. ## Usage ```luau local Selection = require("@builtin::modules.api.editor.selection") local scope = Selection.scope("entity") Selection.set(scope, { { kind = "entity", id = "player_1" } }) Selection.subscribe(scope, function() refreshInspector() end) ``` ## Notes - State lives in a fixed `_G` slot, seeded pre-seal by the boot chain (`prelude.luau`); it survives hot-reload and edit↔play flips. Mutations after boot write into nested tables only. - `set` / `toggle` / `clear` mark their scope as last-focused, which drives `context()` and therefore which scope commands act on. - The service holds the editor's selection state in Luau. Refs handed back by `get` / `primary` / `context` are copies, so a caller can hold or mutate them without touching internal state.
▲ 0↑ born

Problems

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

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

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

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

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

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