Skip to content
Docs
Rooms

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.

A player types a message. The room receives a chat-request and decides. If it calls room.send, every player sees the message; if it doesn't, nobody does.

This room shows every message as written, with its author:

room.js

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.

room.js

Names without the game: prefix must be built-in events; an unknown name is a TypeError, which catches typos.

Events

EventPayloadWhen
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 payloadthe 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 room starts Ready. start makes it Running; pause and resume move between Running and Paused; stop returns to Ready. A game script error makes it Faulted, from which stop returns to Ready. close ends the room from any state.
room.js
  • A game fault is not a crash: the room goes to faulted, emits game-fault and keeps serving players and chat. It can then stop(), load() another game or close().

  • 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 emits close. It is idempotent and never rejects. The close reason 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.