# Events An observer subscribes to one event type at one integer address. Address `0` belongs to the world; an entity ID addresses that entity. The entry plugin below watches every despawn at the world address: ```teal local Transform2D = tecs.Transform2D return tecs.newApplication({ plugin = function(world: tecs.World) world:observe( 0, tecs.ecs.OnDespawn, function(event: tecs.ecs.OnDespawn) -- OnDespawn runs before commit removes the row. local transform = world:get( event.entity, Transform2D ) if transform then spawnDebrisAt(world, transform.x, transform.y) end end ) end, }) ``` The platform event stream uses the same bus. The host emits each platform kind at address `0`; [`tecs.platform.events`](/modules/platform/events) defines those event types. ## World and entity addresses Use address `0` for messages that belong to the world: ```teal world:observe(0, GamePaused, onGamePaused) world:emit(0, GamePaused) ``` Use an entity ID for a subscription tied to that entity: ```teal world:observe( player, DamageReceived, function(event: DamageReceived) applyDamage(player, event.amount) end ) world:emit(player, DamageReceived, 15) ``` When an entity despawns, the world clears every observer at that address before the slot can belong to another entity. World-address observers remain. ## Observer timing `world:emit` invokes matching observers before it returns. The observer runs in the emitter's phase and deferred scope. Platform events arrive before `world:update`, so their observers run outside the phase tree. They do not receive fixed-step timing, phase order, or state gating. Fold an event into state when a reaction needs those properties. [`Input`](/modules/input) follows that pattern for keyboard, pointer, and gamepad events. Observers suit immediate notification. Systems suit ordered frame work. ## Subscription lifetime `world:observe` accepts an optional string ID. Remove a subscription with its callback or ID: ```teal world:observe(0, GamePaused, onGamePaused, "pause-ui") world:stopObserving(0, GamePaused, onGamePaused) world:stopObserving(0, GamePaused, "pause-ui") ``` Passing a function removes every matching registration of that function. Passing an ID removes the first matching registration. When an observer unsubscribes during dispatch, the bus waits until the current dispatch unwinds before changing its list. `world:clearObservers(address)` clears an address that game code manages. Entity despawn handles entity addresses automatically. `world:hasObservers` matters when building the payload itself costs work: ```teal if world:hasObservers(enemy, PathChanged) then world:emit(enemy, PathChanged, buildPathSnapshot(enemy)) end ``` For ordinary constructor arguments, call `world:emit(address, EventType, ...)` directly. The world checks for observers before it constructs an event. ## Table events Define a record, give it an in-place initializer, then register it: ```teal local record PlayerDamaged is tecs.events.Event amount: number source: string metamethod __call: function( self, amount: number, source: string ): PlayerDamaged end PlayerDamaged.init = function( event: PlayerDamaged, amount: number, source: string ) event.amount = amount event.source = source end tecs.events.newEvent(PlayerDamaged) world:emit(player, PlayerDamaged, 10, "fire") ``` Registration assigns the event type its ID. Register each type once. `PlayerDamaged(10, "fire")` allocates an independent instance. Use that form when code must retain the value or send it through a standalone `MessageBus`. `world:emit(player, PlayerDamaged, 10, "fire")` leases pooled backing storage and returns it after dispatch. Do not retain the instance passed to an observer. The emitter owns the payload during dispatch, so observers should treat its fields as read-only and copy any values that must outlive the callback. ## FFI events `newFFIEvent` stores fixed-size fields in a C struct: ```teal local record DamageEvent is tecs.events.Event amount: number entity: integer metamethod __call: function( self, amount: number, entity: integer ): DamageEvent end tecs.events.newFFIEvent( DamageEvent, { {"amount", "float"}, {"entity", "double"}, }, "Game_DamageEvent" ) world:emit(0, DamageEvent, 15.5, enemy) ``` Without a custom `init`, the generated initializer follows field order. Field names must form unique C identifiers. `eventId` and `typeId` belong to Tecs and cannot appear in `fields`. Use `double` for an entity ID. The packed slot and generation do not fit in a 32-bit integer. FFI events cannot carry Lua strings, tables, functions, or userdata; table events can. `OnSpawn` and `OnDespawn` use FFI storage with a `double entity` field. ## Standalone message buses Each world owns a `MessageBus`. `tecs.events.newMessageBus()` creates the same address router without a world. A standalone bus dispatches an event instance that the caller constructs. It also exposes `observeOnce`, per-address clearing, entity-address clearing, and a full reset. World methods add lazy construction, pooled emission, and automatic cleanup when entities die.