# 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.

```mermaid alt="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."
sequenceDiagram
  participant P as Player
  participant R as Your room
  participant E as Everyone
  P->>R: chat-request "hello"
  Note over R: your handler decides
  R->>E: room.send(...)
```

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

```js title="room.js"
room.on("player-join", ({ player }) => {
  void room.send(`${player.name} joined`);
});

room.on("chat-request", ({ player, text }) => {
  // Mute by not sending; everything else is shown with its author.
  if (!text.startsWith("!")) {
    void room.send({ author: player, content: text });
  }
});
```

`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`.

```js title="room.js"
// The game calls game.emit("match-over", { winner: "red" }). The payload
// arrives as frozen JSON; its shape is whatever the game sends.
room.on("game:match-over", (payload) => {
  const { winner } = /** @type {{ winner: string }} */ (payload);

  void room.send({
    content: [{ text: `${winner} wins!`, weight: "bold" }],
    sound: "notification",
  });
});
```

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.

```mermaid alt="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."
stateDiagram-v2
  [*] --> ready
  ready --> running: start()
  running --> paused: pause()
  paused --> running: resume()
  running --> ready: stop()
  paused --> ready: stop()
  running --> faulted: game error
  faulted --> ready: stop()
  ready --> closed: close()
  closed --> [*]
```

```js title="room.js"
room.on("state", ({ state, previous, reason }) => {
  console.log(`state ${previous} -> ${state}`, reason ?? "");
});

room.on("game-fault", ({ message, phase }) => {
  console.error(`game fault during ${phase}: ${message}`);
  // The room keeps serving players; reload a known-good release.
  void room.load("gabriel/arena@1.4.1");
});

room.on("directory", ({ status, error }) => {
  console.log(`directory: ${status}`, error ?? "");
});

process.on("SIGTERM", () => {
  void room.close({ reason: "maintenance" });
});
```

- 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](/rooms/multiple-rooms)).
- If the directory rejects the lease, the room keeps running but is no longer
  listed or joinable through the directory.
