# LobbyLab wire protocol (multiplayer-wire/v0)

LobbyLab is a hosted multiplayer backend. Your game keeps its own UI, engine and domain; players join LobbyLab rooms over WebSockets through `@lobbylab/client`, and LobbyLab keeps the rooms, invite codes, matchmaking and shared game state. Game rules that run on LobbyLab's servers come later. Request an alpha key at https://lobbylab.gg/developers.

Status: `multiplayer-wire/v0` runs every room on lobbylab.gg today. The Multiplayer API alpha reuses it on its own socket path. The REST routes that hand out a seat and the `lobbylab.*` intent names below arrive with the alpha and may change before then. The SDKs hide this protocol; you need it only to write a client in another language.

Messages are JSON text frames. Every message has a `type`.

## Handshake

1. Get a seat. Planned for the alpha: `POST https://api.lobbylab.gg/v1/rooms/join` with `{"code":"KXPTQM"}` returns `seat.wsUrl`, `seat.memberId` and `seat.credential`.
2. Open a WebSocket to `seat.wsUrl`. Within 5 seconds, send:

```json
{"type":"hello","wireVersion":"multiplayer-wire/v0","sessionId":"<room id>","credential":"<seat credential>","logicalStreamId":"<one id per tab, 8 to 200 characters>"}
```

3. The server answers `ready`, then a `snapshot` (turn-based rooms) or `frame` messages (live rooms), and a `members` list. Without a `hello` in 5 seconds, the server closes with `4401`.

## Client messages

| type | Fields | Use |
|---|---|---|
| `hello` | `wireVersion`, `sessionId` (1 to 200 characters), `credential` (16 to 2,000), `logicalStreamId` (8 to 200) | The first message on every socket |
| `intent` | `intentId` (8 to 200 characters, unique), `clientSequence` (goes up on each socket), `name` (1 to 100), `payload`, optional `basedOnRevision` | A change or action. Retrying with the same `intentId` is safe |
| `input` | `seq`, `payload` (at most 1 KiB) | Live rooms only: your whole player slot. `seq` must keep increasing across reconnects, so start it at the current time in milliseconds |
| `ping` | `nonce` | The server answers `pong` with the same nonce |
| `resync` | none | Asks for a fresh snapshot |

## Server messages

| type | Meaning |
|---|---|
| `ready` | You are seated: `memberId`, `phase`, `revision`, `roles`; in live rooms also the tick, frame and input rates |
| `members` | The member list (`memberId`, `displayName`, `connected`, `roles`), sent on every change |
| `snapshot` | Turn-based rooms: `revision`, `viewHash`, your `view` after a change, and its `events` |
| `frame` | Live rooms: `tick`, `serverTimeMs`, your `view`, `events` and `inputAck` (your highest input `seq` used) |
| `ack` | Your intent was accepted: `intentId`, `triggerSequence`, `revision` |
| `rejection` | Your intent was refused: `code`, `retryable`, and `intentId` when known |
| `phase` | The room phase changed; `ended` means the room closed |
| `pong`, `fatal_error` | Liveness answer; unrecoverable error with a `code` |

## Intents planned for the alpha

```ts
{ name: "lobbylab.state", payload: { ops: StateOp[]; guards?: { path: string; equals: Json }[]; ifVersion?: number } }
{ name: "lobbylab.send",  payload: { topic: string; data: Json; to?: "all" | "others" | "host" | string[]; echo?: boolean } }
{ name: "lobbylab.host",  payload: { action: "kick" | "lock" | "meta" | "transfer" | "close"; ... } }
```

## Close codes

| Code | Meaning | What a client does |
|---|---|---|
| `4401` | Bad or expired seat credential | Renew the seat once, then reconnect; stop if that fails |
| `4403` | Removed (left, kicked, banned, room closed) | Stop and report the reason |
| `4400` | A message the server could not accept | Stop |
| `4429` | Rate or abuse limit | Stop and show the error |
| `1011`, `1006`, `1001`, any other code, or no answer | Transient | Reconnect after 0.8 s, 1.6 s, 3.2 s, then every 5 s |

## Staying connected

- The server sends WebSocket protocol pings every 4 seconds and drops a socket after 2 missed pings. Browsers answer these on their own.
- A new `hello` with the same seat returns the same member and a fresh snapshot or frame, so state is current after every reconnect.
