Events, chat & lifecycle
Room events, chat moderation, messages, game events and the room lifecycle.
A room reacts to what happens in it through events: a player joins,
someone types in chat, the game reports a goal, the match ends. You subscribe
with room.on(event, handler) and unsubscribe with room.off(event, handler).
One thing is different from most event systems: the game never waits for
your handlers. The match keeps running at 60 ticks per second no matter
what your Node code is doing, so a slow handler can't freeze the game. Your
handlers run on Node's main thread, in the order you subscribed them, and may
be async.
By the time an event reaches you, the room's properties already reflect it:
in a player-join handler, room.players already includes the new player.
Chat
In a disko room, chat doesn't go straight from one player to the others.
A player's message reaches the room first as a chat-request, and nothing is
shown until the room sends it with room.send. That puts the room in charge:
it can filter words, add a prefix, or mute someone simply by not sending
their messages.
This room shows every message as written, with its author:
room.send takes a string, or { author?, content, sound? } where content
is a string or an array of spans { text, color?, weight?, style? } and
sound is "none", "normal" or "notification". Messages are limited to
32 spans and 2048 bytes of text.
Game events
The room and the game can tell each other things with events, without knowing each other's code.
When the game calls game.emit(name, payload), the room receives the
game:<name> event, with the payload frozen. The payload is whatever the
game sends; for example Simple Football emits goal with the team and the
score.
The other direction works the same way: room.emit(name, payload) sends an
event to the game, which handles it with game.on(name). The name is 1 to
128 visible characters, the payload (default null) must be
JSON-serializable, and the Promise resolves once the event is queued. A bad
name or payload rejects with invalid_event.
Names without the game: prefix must be built-in events; an unknown name is
a TypeError, which catches typos.
Events
| Event | Payload | When |
|---|---|---|
player-join | { player } | a player was admitted |
player-leave | { player } | a player left or was disconnected |
chat-request | { id, player, text } | a player sent chat |
game:<name> | the payload | the game called game.emit |
state | { state, previous, reason? } | a lifecycle change; reason is finished, stopped or fault where it applies |
game-fault | { message, stack, phase } | a game script error faulted the game; phase is start, tick or event |
game-loaded | { reference, path, digest, deprecation } | after launch and every successful load |
directory | { status, error? } | the directory lease is registered, retrying or rejected |
close | { reason } | the room closed |
Handlers subscribed right after await Room.launch(...) still receive the
launch's game-loaded.
Lifecycle
A room is always in one of five states. start(), stop(), pause() and
resume() move it between them, and each rejects with invalid_state when
the room isn't in a state it can move from.
A game fault is not a crash: the room goes to
faulted, emitsgame-faultand keeps serving players and chat. It can thenstop(),load()another game orclose().Handler errors are never caught. A throw, or a rejected Promise returned by a handler, goes to Node's normal handling, which by default crashes the process and ends its rooms. Install your own process handlers to survive them.
close({ reason })disconnects everyone with the reason (default"closed", at most 256 bytes), releases the directory lease, stops the engine and emitsclose. It is idempotent and never rejects. Theclosereason is"engine_error"after a native engine failure, and"controller_overloaded"when the event queue filled (see Multiple rooms).If the directory rejects the lease, the room keeps running but is no longer listed or joinable through the directory.