Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdd348ac6c3

README.md

assetType.shared.ref

Builds the metatable every AssetRef (the { __ref, type, name, guid, identity, path } envelope returned by asset.resolve / asset.ref) carries. Replaces the per-call C metatable that used to live in crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable.

Public API

local assetRef = require("@builtin::assetTypes.assetType.shared.ref")

-- Called by the Rust factory (push_asset_ref_handle) via
-- _G.__build_asset_ref_proxy after the envelope fields are set.
assetRef.build(envelope)        -- attaches AssetRefMT, returns the table

The module exposes no invalidation surface — discovery is registry-driven and hot-reload is transparent (see "Discovery and hot-reload" below).

Method dispatch order

ref.foo is resolved in this order:

  1. Default methods — getSource / getBytes / getText / exists / inspect and the lazy meta property. Per-type definitions cannot override these (engine contract from gh#1889).
  2. Per-type ref table — discovered through asset.resolve(<typename>, "assetType") + require(<identity>.behavior). The lookup resolves to whichever <typename>.assetType/ folder is currently registered for the typename (built-in, user, or library-vendored) and pulls its ref methods.
  3. Nil — unknown key, same shape as the original C metatable.

Adding methods for a new asset type

Drop a behavior.luau into the type folder:

src/lua/lib/assetTypes/<typename>.assetType/
├── type.yaml      -- structural spec (existing)
├── behavior.luau      -- ref + global behaviour (new)
└── template/      -- placeholder body (existing)

behavior.luau returns:

return {
    ref = {
        --!desc Per-instance method on every `<typename>` AssetRef.
        myMethod = function(self, ...) ... end,
    },
    global = {
        -- Reserved for the asset.<typename>.* surface — not consumed
        -- yet; included so the contract from gh#1889 lands cleanly.
    },
}

Within ref methods, self is the AssetRef envelope: self.path, self.identity, self.guid, self.type, self.name are available. Engine-required fields (guid, path, identity, type) must not be redefined.

Discovery and hot-reload

Type discovery uses the asset registry, so it doesn't matter where a <typename>.assetType/ folder lives — engine built-ins under @builtin::assetTypes.<X>, user types dropped under /zero/source/<X>.assetType/, or types vendored inside an imported library all resolve identically. The dispatcher walks asset.resolve(<typename>, "assetType") → canonical identity → require <identity>.behavior on every miss; the asset registry is the cache for the resolve half and Luau's _LOADED is the cache for the require half.

Hot-reload is transparent. When a <typename>.assetType/behavior.luau is rewritten through VFS, the engine's invalidate_require_cache_for_state re-runs the module and mutates the cached _LOADED[<identity>.behavior] table in place. Dispatch reads mod.ref on every access (rather than caching a sub-pointer), so existing AssetRefs see the new methods on the very next access — no Rust-side hook into this module is needed, and no invalidation API is exposed for callers to call.

The same registry-driven path also handles type registration correctness: dropping a new <typename>.assetType/behavior.luau updates the asset registry, the next asset.resolve(<typename>, "assetType") returns the new ref, and AssetRefs of that type immediately gain the new methods.

Why Luau, not Rust

The asset surface needed per-category behaviour (material:getProperties(), bundle:instantiate(entityId), tool:run(args)) and the old shape couldn't grow without an FFI change per method. Owning the proxy in Luau means:

  • Type-specific methods ship as content (the <typename>.assetType/ folder), not engine code.
  • Adding a new asset type drops in a new folder; no Rust rebuild.
  • Methods can call any other Luau global (vfs, asset, Material, entity, etc.) directly, instead of going through narrow FFI primitives.

Consumers

  • crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable — the Rust factory that pushes envelopes invokes _G.__build_asset_ref_proxy once per push.
  • Every Luau call site of asset.resolve / asset.ref / entity(id).component.X.material / any binding declared AssetRef<...> — they all receive envelopes built through this module.