Builtin Library
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:
- Default methods —
getSource/getBytes/getText/exists/inspectand the lazymetaproperty. Per-type definitions cannot override these (engine contract from gh#1889). - Per-type
reftable — discovered throughasset.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 itsrefmethods. - 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_proxyonce per push.- Every Luau call site of
asset.resolve/asset.ref/entity(id).component.X.material/ any binding declaredAssetRef<...>— they all receive envelopes built through this module.