diff --git a/docs/jackbox-ecast-api.md b/docs/jackbox-ecast-api.md index 7f25c7b..a2626b7 100644 --- a/docs/jackbox-ecast-api.md +++ b/docs/jackbox-ecast-api.md @@ -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.