docs: update ecast API reference with connection roles and shard details

Add Connection Roles table documenting host, player, shard, audience,
observer, and moderator roles with their capabilities and slot impact.
Add shard client/welcome capture and passive room monitoring section.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-20 11:47:47 -04:00
parent 65036a4e1b
commit a7bd0650eb

View File

@@ -15,7 +15,8 @@ Reverse-engineered documentation of the Jackbox Games ecast platform API. This c
4. [Entity Model](#entity-model)
5. [Game Lifecycle](#game-lifecycle)
6. [Player & Room Management](#player--room-management)
7. [Appendix: Raw Captures](#appendix-raw-captures)
7. [Passive Room Monitoring](#passive-room-monitoring)
8. [Appendix: Raw Captures](#appendix-raw-captures)
---
@@ -52,6 +53,25 @@ Jackbox Games uses a platform called **ecast** to manage game rooms and real-tim
The `{host}` value comes from the REST API room info response (`host` or `audienceHost` field).
### Connection Roles
Ecast supports several connection roles, each with different capabilities and visibility:
| Role | Endpoint | `here` visibility | Entity reads | Entity writes | Slot impact |
|------|----------|-------------------|-------------|--------------|-------------|
| `host` | `/rooms/{code}/play?role=host` | Full | Full | Full | Yes (always 1) |
| `player` | `/rooms/{code}/play?role=player` | Full | Own + room | Own entities | Yes (occupies a `maxPlayers` slot) |
| `shard` | `/rooms/{code}/play?role=shard` | Full (sees & is seen) | Full (via `object/get`) | Denied | No (does NOT count toward `full` / `maxPlayers`) |
| `audience` | `/audience/{code}/play` | None (`here: null`) | Receives broadcasts only | None | No (does NOT count toward `full` / `maxPlayers`) |
| `observer` | `/rooms/{code}/play?role=observer` | Unknown | Unknown | Unknown | Unknown |
| `moderator` | `/rooms/{code}/play?role=moderator` | Unknown | Unknown | Unknown | Unknown |
**Notes:**
- **`observer`**: Exists but is password-protected ("observer is for internal use, sorry: password required" — error 2017). Presumably designed for exactly the monitoring use case, but the password is not publicly known.
- **`moderator`**: Exists but requires `moderationEnabled: true` on the room (error 2023: "moderation is not enabled").
- **`spectator`**, empty string, and other arbitrary roles return error 2014: "no such role".
- Both `shard` and `audience` connections increment the raw `/connections` counter and hold their slot after disconnect (same persistence model as players). However, they do **not** count toward the `full` flag or `maxPlayers` limit — only `player` role connections do.
---
## REST API Reference
@@ -158,13 +178,13 @@ Room information with audience data and join role. Uses a different response for
### GET /api/v2/rooms/{code}/connections
Occupied slot count. Includes host (1) + all player slots + audience + shards.
Total occupied slot count across **all** connection types: host (1) + players + audience + shards.
**Response:** `{"ok": true, "body": {"connections": 3}}`
Player count = `connections - 1` (subtract the host). Since Jackbox holds player slots for reconnection (a disconnected player's slot is reserved and unavailable to new players), this directly reflects how many of the room's `maxPlayers` slots are taken. Available slots = `maxPlayers - (connections - 1)`.
For rooms with only the host and players (the typical case), player count = `connections - 1`. Since Jackbox holds player slots for reconnection (a disconnected player's slot is reserved and unavailable to new players), this directly reflects how many of the room's `maxPlayers` slots are taken. Available slots = `maxPlayers - (connections - 1)`.
> **Note:** If audience members are connected, they are included in the count. Use `room/get-audience` or the `/info` endpoint to get the audience count separately if needed.
> **Important:** This counter includes ALL connection types (audience, shard, etc.), not just players. If external monitoring connections are present, subtract them accordingly. Alternatively, use the `full` field from `GET /rooms/{code}` — this flag only considers player-role connections and is unaffected by audience/shard connections.
### POST /api/v2/rooms
@@ -273,6 +293,14 @@ wss://{host}/api/v2/audience/{code}/play
No query parameters required for audience.
**Shard connection URL:**
```
wss://{host}/api/v2/rooms/{code}/play?role=shard&name={name}&userId={userId}&format=json
```
Shard connections use the same `/rooms/{code}/play` endpoint as players but with `role=shard`. They get full `here` visibility and can query entities but cannot write.
**Required WebSocket sub-protocol:** `ecast-v0`
**Required headers:**
@@ -432,11 +460,12 @@ const ws = new WebSocket(url, ['ecast-v0'], {
| Code | Message | Description |
|------|---------|-------------|
| 2000 | `missing Sec-WebSocket-Protocol header` | WebSocket connection missing required protocol |
| 2003 | `invalid opcode` | Unrecognized opcode sent |
| 2003 | `invalid opcode` | Unrecognized opcode sent (or opcode not available for this role) |
| 2007 | `entity value is not of type {expected}` | Type mismatch (e.g., using `text/get` on an `object` entity) |
| 2023 | `permission denied` | Operation not allowed for this client's role |
| 2014 | `no such role: {role}` | Requested role does not exist |
| 2017 | `observer is for internal use, sorry: password required` | Observer role requires a password |
| 2023 | `permission denied` | Operation not allowed for this client's role (also: `moderation is not enabled`, `only the host can close the room`) |
| 2027 | `the room has already been closed` | Room ended but REST API may still return room info (REST/WS state divergence) |
| — | `only the host can close the room` | Specific error for `room/exit` from non-host |
---
@@ -755,6 +784,68 @@ Specific stat fields are game-dependent (Drawful 2, Quiplash, etc. have differen
---
## Passive Room Monitoring
It is possible to monitor a game room in real-time via WebSocket without joining as a player. There is no truly invisible WebSocket option (all connection types increment `/connections`), but there are approaches that don't consume a player slot or appear in the game UI.
### Option 1: Audience Connection (recommended)
Connect via the audience endpoint: `wss://{host}/api/v2/audience/{code}/play`
**Pros:**
- Invisible to players — does not appear in the `here` map
- Receives real-time entity broadcasts (`room`, `textDescriptions`, `audiencePlayer`)
- Does not count toward `maxPlayers` or trigger `full: true`
- No game UI impact (players don't see an audience member join unless the game has audience UI)
**Cons:**
- Increments the raw `/connections` counter by 1 (slot held after disconnect)
- `here` is `null` — cannot directly see the player list or their roles
- Cannot query entities (opcodes like `object/get` and `room/get-audience` return "invalid opcode")
- Limited to passively receiving whatever entities the host broadcasts
**What you receive:**
- `client/welcome` with full entity snapshot (`room`, `audience`, `textDescriptions`, `audiencePlayer`)
- Real-time `object` updates when entities change (player joins via `textDescriptions`, lobby state changes via `room`, game start/end)
### Option 2: Shard Connection
Connect via: `wss://{host}/api/v2/rooms/{code}/play?role=shard&name={name}&userId={userId}&format=json`
**Pros:**
- Gets the full `here` map with all connections and their roles (can filter for `player` roles to count players)
- Can actively query entities with `object/get` and `room/get-audience`
- Receives real-time entity broadcasts and periodic `shard/sync` CRDT data
- Does not count toward `maxPlayers` or trigger `full: true`
**Cons:**
- Visible to other clients — appears in the `here` map as `{roles: {shard: {}}}`
- Increments the raw `/connections` counter (slot held after disconnect)
- Other players' `here` field includes shard connections, which could confuse client code that counts `here` entries
### Option 3: REST-Only Polling (truly invisible)
Poll REST endpoints (`/rooms/{code}`, `/rooms/{code}/connections`) periodically.
**Pros:**
- Zero footprint — no WebSocket connection, no slot consumed, no `/connections` impact
**Cons:**
- No real-time events — must poll on an interval
- Cannot detect fine-grained state transitions between polls
### Recommendation
For the game picker use case, the **audience connection** is the best balance:
1. It's invisible to players (not in `here`, no game UI effect)
2. It provides real-time entity updates (lobby state, player joins, game start/end)
3. The only side effect is +1 on the raw `/connections` counter, which can be accounted for
4. Use `full` from the REST API for slot availability — this flag only counts player-role connections and is unaffected by audience/shard connections
For player counting with an audience monitor connected: either use `full` / `maxPlayers` from REST (unaffected), or use `connections - 2` (subtract host + your monitor) from the `/connections` endpoint.
---
## Appendix: Raw Captures
### Player `client/welcome` (initial connection)
@@ -813,6 +904,40 @@ Key differences from player welcome:
- `profile` is `null` (audience has no player profile)
- Includes `audiencePlayer` entity (audience-specific state)
### Shard `client/welcome`
```json
{
"opcode": "client/welcome",
"result": {
"id": 7,
"secret": "82608030-59fb-46e3-b4ab-b23c7f733526",
"reconnect": false,
"entities": {
"audience": ["crdt/pn-counter", [2,1,1,3,1,1,4,1,1], {"locked": false}],
"audiencePlayer": ["object", {"key": "audiencePlayer", "val": {"..."}, "version": 1, "from": 1}, {"locked": false}],
"room": ["object", {"key": "room", "val": {"..."}, "version": 0, "from": 1}, {"locked": false}],
"scores": ["crdt/rankset", {"ranks": []}, {"locked": false}],
"textDescriptions": ["object", {"key": "textDescriptions", "val": {"..."}, "version": 0, "from": 1}, {"locked": false}]
},
"here": {
"1": {"id": 1, "roles": {"host": {}}},
"2": {"id": 2, "roles": {"shard": {}}},
"8": {"id": 8, "roles": {"player": {"name": "TestPlayer1"}}}
},
"profile": null
}
}
```
Key differences from player welcome:
- `id` is a low single-digit number (separate from player/audience ID spaces)
- Gets the raw CRDT representation of audience counter instead of the friendly `{count: N}` form
- Gets additional entities like `scores` (CRDT rankset)
- `here` includes ALL connections with their roles (host, players, other shards)
- `profile` is `null` (shard has no player profile)
- Receives periodic `shard/sync` messages with CRDT state updates
### Reconnection `client/welcome`
```json
@@ -914,6 +1039,8 @@ The `reconnect: true` flag distinguishes reconnections from new joins. Same `id`
Known roles:
- `host` — The game console/PC running the game
- `player` — A player joined via jackbox.tv (includes `name`)
- `shard` — Internal connection (appears when audience is connected, possibly audience aggregator)
- `shard` — Internal/infrastructure connection. Externally connectable with `role=shard`. Gets full entity access (read-only), appears in `here` map, receives `shard/sync` CRDT data. IDs are in the low single-digit range.
- `observer` — Internal role (password-protected, not publicly accessible)
- `moderator` — Moderation role (requires `moderationEnabled: true` on the room)
> **`here` includes disconnected players.** Tested by connecting 3 players, closing all their WebSockets, then having a new player connect. The new player's `here` field listed all 3 previous players despite none of them having active WebSocket connections. This is consistent with Jackbox's slot-reservation model where player slots persist for reconnection.