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

events

Per-asset runtime for declared asset events. An assetType's `behavior.luau` declares an `events` schema the same way a component declares one; this module turns that schema into the live objects an asset's ref fires and listens on.

bylumi·posted 1mo ago
What it does

asset_events

Per-asset runtime for declared asset events. An assetType's behavior.luau declares an events schema the same way a component declares one; this module turns that schema into the live objects an asset's ref fires and listens on.

Three faces, one Signal — keyed by the asset

The construction is component_events': one Signal per declared event, reachable through a private fire-capable table, an owner-side emitter the type's own behavior fires with, and a subscribe-only facade every other holder of the ref sees. The facade exposes connect / once / wait and has no fire at any key, so firing authority stays with the type.

What differs is the owner. A component event belongs to one instance on one entity; an asset event belongs to the asset, so the runtime is keyed by the asset's stable guid. Every resolver of that guid subscribes to the same Signals, and a ref that is reclaimed and re-resolved re-attaches to the subscriptions already there — the same reason asset_ref's runtime table is guid-keyed rather than stored on the envelope.

Both tables reject an undeclared event name: the private table's metatable raises on a bad key and the facade raises on a bad key, so a typo surfaces at the subscribe call instead of returning nil.

Declaring events on a type

-- <name>.assetType/behavior.luau
local M = {}

M.events = {
    changed = { payload = { value = Field.number(0, NoSync) } },
}

M.ref = {
    poke = function(self)
        local emitter = require("@builtin::assetTypes.assetType.shared.events").emitter(self.guid)
        if emitter ~= nil then emitter.changed:fire({ value = 1 }) end
    end,
}

return M
-- any holder of the ref
local ref = asset.resolve("@builtin::…")
ref.events.changed:connect(function(p) print(p.value) end)

Exports

  • M.forAsset(guid, eventSchema) — the runtime for one guid, built on first use and reused after. A later call with a different schema reconciles: surviving events keep their Signal and their subscribers, new events get a fresh one, removed events are disconnected and dropped.
  • M.facade(guid) — the subscribe-only view, or nil before a runtime exists.
  • M.emitter(guid) — the owner-side :fire view, or nil before a runtime exists.
  • M.has(guid) — whether a guid holds a live runtime.
  • M.teardown(guid) — drop one asset's runtime, disconnecting subscribers.
  • M.teardownAll() — drop every runtime. Called on an engine mode flip so a play-mode subscription does not survive into edit.
  • M.liveGuids() — every guid holding a runtime, sorted.

Notes

  • Subscriptions made through the facade are ordinary signal.module connections and disconnect the same way any other does.
  • The runtimes table is held strongly, and its lifetime is bounded by the mode flip that clears it.

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_events Per-asset runtime for declared asset events. An assetType's `behavior.luau` declares an `events` schema the same way a component declares one; this module turns that schema into the live objects an asset's ref fires and listens on. The three-faces construction is `component_events`': one Signal per declared event, reachable through a private fire-capable table, an owner-side emitter the type's own behavior fires with, and a subscribe-only facade every other holder of the ref sees. Firing authority lives with the type, because the facade has no `fire` at any key to find. What differs is the OWNER. A component event belongs to one instance on one entity; an asset event belongs to the ASSET, so the runtime is keyed by the asset's stable guid rather than by an instance id. Every resolver of the same guid subscribes to the same signals, and a ref reclaimed and re-resolved re-attaches to the subscriptions already there.

forAsset(guid: string, eventSchema: { [string]: any }) → any

The event runtime for one asset guid, built on first use from `eventSchema` and reused afterwards. Returns `{ signals, emitter, facade }`, all three over the same Signals. A later call with a DIFFERENT schema for the same guid reconciles: surviving events keep their Signal and their subscribers, new events get a fresh one, removed events are disconnected and dropped.

argtypedescription
guidstringThe asset's stable guid — the runtime's identity.
eventSchema{ [string]: any }The type's declared event schema.

facade(guid: string) → any

The subscribe-only facade for one asset guid, or nil when the asset has no runtime yet. What `ref.events` reads.

argtypedescription
guidstring

emitter(guid: string) → any

The owner-side emitter for one asset guid, or nil when the asset has no runtime yet. An assetType's own behavior fires through this; nothing outside the type reaches it.

argtypedescription
guidstring

has(guid: string) → boolean

Whether a guid has a live event runtime.

argtypedescription
guidstring

subscriberCount(guid: string, event: string?) → number

How many subscribers an asset's events currently hold, across every declared event or one named event. What it answers is whether anything is LISTENING. An asset can be live, valid and firing with nobody on the other end, and a surface that presents it to a player — a button drawn for a control no code subscribed to — is offering something that cannot do anything.

argtypedescription
guidstringThe asset's stable guid.
eventstring?Optional event name; omit to count across all of them.

countOne(sig: any) → number

argtypedescription
sigany

teardown(guid: string) → boolean

Drop one asset's event runtime, disconnecting every subscriber first. Returns true when a runtime was there to drop.

argtypedescription
guidstring

teardownAll( ) → void

Drop every asset event runtime, disconnecting all subscribers. A runtime's lifetime follows its HOLDERS, not the engine mode. Per- asset runtime state is wiped on a mode flip because a play-mode value override must not leak into edit; a subscription is not a value, it belongs to whoever connected it, and whoever connected it is torn down on the flip anyway. Wiping these on the flip instead orphans them: a holder that survives the flip still believes it is subscribed, its signals are gone, and the dispatch finds no emitter — so the asset is live and permanently silent, with every diagnostic reporting health.

liveGuids( ) → void

Every guid holding a live event runtime, sorted — what a debug surface lists.

Sub-parts

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

2items
This part has no composite children. See the Files segment for its leaf payloads.
backing path · assetTypes/assetType.assetType/shared.module/events.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.