Log inGet started
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofmodule shared.module·originates fromworld 07158574-5…

changeDispatch

Installs `_G.__zero_dispatch_asset_change`, the Luau half of the asset-type **change-callback** system. The engine calls it once per VFS source write (via `zero_scripting::ffi_callbacks::fire_asset_change_dispatch`, queued as `VfsMutation::AssetSourceWritten`).

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

asset_change_dispatch (module)

Installs _G.__zero_dispatch_asset_change, the Luau half of the asset-type change-callback system. The engine calls it once per VFS source write (via zero_scripting::ffi_callbacks::fire_asset_change_dispatch, queued as VfsMutation::AssetSourceWritten).

What it does

  1. Receives the written VFS path.
  2. Walks up the path to the enclosing <name>.<type>/ typed-asset folder (deepest registered-type suffix wins, so the direct owner of the write is chosen for nested typed assets).
  3. Loads that type's <type>.assetType/behavior.luau via @builtin::assetTypes.assetType.shared.ref.loadTypeModule.
  4. If the module exports an onChange function, calls onChange(ref, change) where ref is the typed AssetRef for the changed asset and change = { path, asset, type }.

This is the type-level analogue of the component-centric onAssetReload(field) fan-out: components react to assets they reference; an asset type reacts to writes inside its own instances.

Re-entrancy

A best-effort synchronous guard suppresses dispatch for an asset while its own onChange is running, so a handler that writes back inline doesn't recurse immediately. It does not span asynchronous work — writes queued during a synchronous onChange are processed on a later drain. onChange handlers must therefore be convergent: diff the meaningful state before acting and short-circuit when there's nothing to do (the dynamicAsset example only regenerates when the prompt actually changed and bails while a generation is in flight). Idempotency is the contract, exactly as it is for services' start/stop.

Related

  • @builtin::assetTypes.assetType.shared.ref — owns loadTypeModule + the per-type ref method dispatch.
  • assetTypes/assetType.assetType/template/behavior.luau — the documented template that shows how to author ref, global, and onChange.
  • assetTypes/dynamicAsset.assetType — the reference type that uses onChange for prompt-driven regeneration with version control.

Interface

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

conforms to

zero/source-extract/v2

module AssetChangeDispatch Routes a VFS source write to the enclosing typed-asset's `onChange(ref, change)` hook. Installs `_G.__zero_dispatch_asset_change`, which the engine calls (via `ffi_callbacks::fire_asset_change_dispatch`) once per source write. require modules/asset_change_dispatch about This is the type-level analogue of the component-centric `onAssetReload(field)` fan-out. Components react to assets they *reference*; an asset *type* reacts to writes INSIDE its own instances. When any file under a `<name>.<type>/` folder changes on the VFS, the engine hands the written path here; this module finds the enclosing typed-asset folder, loads its `<type>.assetType/behavior.luau`, and — if that module exports an `onChange` function — invokes `onChange(ref, change)`. The engine half is deliberately thin (a generic "this path was written" signal); all the resolution + behaviour lives here in Luau, mirroring the `__build_asset_ref_proxy` split.

category_set( ) → void

Build a set of registered asset-type names so we can recognise a `<name>.<type>/` boundary. `asset.categories` answers from a cache keyed on the asset index's revision, and a miss walks every registered claim in the world, so one dispatch reads it once and hands the set to each resolver below rather than asking again per question.

enclosing_types(path: string, cats: { [string]: boolean }) → void

Every registered type whose asset ENCLOSES `path`, innermost last. `resolve_enclosing_asset` keeps only the innermost, which is the asset whose `onChange` owns the write. Containment is what makes that too narrow for anything keyed on an OUTER asset's content: a write to `Shop.dialogue/greeting.dialogueLine/init.luau` is a change to the LINE and equally a change to the DIALOGUE that contains it, and only the outer one knows it now has a different set of children.

argtypedescription
pathstring
cats{ [string]: boolean }

is_directory(path: string) → boolean

Whether `path` is a DIRECTORY on the VFS. Discriminates a typed-asset FOLDER from a loose FILE whose extension happens to collide with a registered category name (a loose `foo.glb` when `glb` is a registered category, a loose `notes.json` against the `json` category) — the suffix match alone cannot tell them apart. One dispatch runs per applied source write, on every session bound to the world, so this asks about the ONE path written and costs what that write is rather than what the folder it landed in holds.

argtypedescription
pathstring

dispatch_loose(path: string, origin: string?) → void

Loose-file seam. When a written path has NO enclosing typed-asset folder (a bare `/source/foo.glb`), there's no per-type `onChange` to fire — but a write still happened, and some system may want to claim the orphan. This is the generic "a path owned by no asset type was written" hook. The importer SYSTEM is its first consumer: the importer assetType's shared module exposes `onLooseWrite(path, origin)` and decides whether a registered `.importer/` claims the file. The engine stays generic — it never names importers; this dispatcher only forwards the orphan write to the importer type's shared layer (resolved by name, exactly like `onChange` resolves a type's behavior).

argtypedescription
pathstring
originstring?

dispatch(path: string, kind: string?, origin: string?) → void

Dispatch the asset-type change hook for a VFS path. Called by the engine for two orthogonal axes: - `kind`: a per-file EDIT (`"edited"`, `path` is the written file) vs an asset COMPLETION (`"seeded"`, `path` is the typed-asset folder, now fully present from a world seed). The type inspects `kind` to decide whether to filter by which file changed or act on the whole asset. - `origin`: `"local"` for a write made on this client, `"remote"` for a peer-synced write — forwarded so hooks can gate on it (importers run on the originator only). Resolves the typed asset, loads its `behavior.luau`, and invokes `onChange(ref, change)` (`change = { path, asset, type, kind, origin }`). When the path has no enclosing typed asset, forwards to the loose-file seam (`dispatch_loose`) so the importer system can claim orphan writes.

argtypedescription
pathstringThe VFS path: the written file (edited) or the asset folder (seeded).
kindstring?"edited" (default) or "seeded".
originstring?"local" (default) or "remote".

examples

__zero_dispatch_asset_change("/zero/source/Goblin.dynamicAsset/prompt.json", "edited", "local")

dispatchDelete(path: string) → void

Dispatch the asset-type DELETE hook for a removed typed-asset FOLDER — the teardown counterpart of `dispatch`. `path` is the folder that was removed; this resolves its `<name>.<type>` identity, loads the type's `behavior.luau`, and invokes `onDelete(ref)` if defined (a `.component` unregisters its type). No-op when the path isn't a registered typed asset or the type defines no `onDelete`. The engine calls this via `ffi_callbacks::fire_asset_delete_dispatch` once per typed-asset folder removal.

argtypedescription
pathstringThe removed asset folder's VFS path.

examples

__zero_dispatch_asset_delete("/zero/source/Spinner.component")

Sub-parts

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

3items
·
metadata · born here
file
▲ 0↑ born
backing path · assetTypes/assetType.assetType/shared.module/changeDispatch.module

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.