Skip to content

Rooms and Lobbies

Rooms are the core coordination unit in Signal Fish Server. A room groups players together for a single game session, manages a lobby where everyone signals readiness, and distributes legacy peer metadata when the game starts.

Creating a Room

A room is created when a player sends a JoinRoom message without a room_code. The server generates a unique room code and returns it in the RoomJoined response.

Client sends:

JSON
{
  "type": "JoinRoom",
  "data": {
    "game_name": "my-game",
    "player_name": "Alice",
    "max_players": 4
  }
}

Server responds:

JSON
{
  "type": "RoomJoined",
  "data": {
    "room_id": "550e8400-e29b-41d4-a716-446655440000",
    "room_code": "HK7T3W",
    "player_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "game_name": "my-game",
    "max_players": 4,
    "supports_authority": true,
    "current_players": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "name": "Alice",
        "is_authority": true,
        "is_ready": false,
        "connected_at": "2025-01-15T10:30:00Z"
      }
    ],
    "is_authority": true,
    "lobby_state": "waiting",
    "ready_players": [],
    "relay_type": "matchbox",
    "current_spectators": []
  }
}

Alice now shares the room code HK7T3W with other players (via your game's UI, a chat message, or any other channel).

JoinRoom Resolution Rules

JoinRoom is used for both creation and joining. The server resolves each request using the (game_name, room_code) pair:

JoinRoom payload Server behavior Result
room_code omitted Implicit room creation New room + generated code in RoomJoined
room_code provided and room exists for game_name Join existing room RoomJoined + PlayerJoined broadcast to others
room_code provided and room does not exist for game_name Explicit room creation New room using requested room_code

Creation-specific fields such as max_players and supports_authority apply only when a room is created (implicit or explicit). When joining an existing room, current room settings remain unchanged.

Joining an Existing Room

Other players join by including the room_code in their JoinRoom message:

JSON
{
  "type": "JoinRoom",
  "data": {
    "game_name": "my-game",
    "room_code": "HK7T3W",
    "player_name": "Bob"
  }
}

Bob receives a RoomJoined response with the current room state. All existing players in the room receive a PlayerJoined broadcast:

JSON
{
  "type": "PlayerJoined",
  "data": {
    "player": {
      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "name": "Bob",
      "is_authority": false,
      "is_ready": false,
      "connected_at": "2025-01-15T10:31:00Z"
    }
  }
}

Room Codes

Room codes are auto-generated 6-character alphanumeric strings designed to be easy to read aloud and type. The "clean" code generator excludes characters that are visually confusing:

  • 0 (zero) and O (letter O)
  • 1 (one) and I (letter I)

This leaves 32 unambiguous characters: 2-9 and A-H, J-N, P-Z.

The code length is configurable via the room_code_length protocol setting (default: 6).

Room Lifecycle

Every room moves through a simple state machine. The following diagram shows the three states and the transitions between them.

Text Only
+------------------------------------------+
|            ROOM LIFECYCLE                 |
+------------------------------------------+

  Room created (JoinRoom, no code)
             |
             v
      +-----------+
      |  Waiting  | <--- Empty room, waiting for the first player
      +-----------+
             |
             | First player joins
             v
      +-----------+
      |   Lobby   | <--- Players toggle ready/unready via PlayerReady
      +-----------+      (max_players is a ceiling; room need not be full)
             |
             | Every current player ready, a member sends StartGame
             v
      +-----------+
      | Finalized | ---> GameStarting sent to all players
      +-----------+

A player leaving during the lobby no longer regresses a partial room to Waiting; the remaining members stay in the lobby.

Waiting

The initial state. The room exists but is empty, awaiting its first player.

  • No players are present yet.
  • No ready-state tracking happens in this state.
  • The room stays here until a player joins or it expires from inactivity.

Lobby

Players are present and can coordinate readiness. max_players is a ceiling, not a required count — the room enters the lobby with one or more players and need not be full to start.

  • Players send PlayerReady to toggle their ready/unready status.
  • Each readiness change triggers a LobbyStateChanged broadcast so everyone sees who is ready; all_ready is set once every current player is ready.
  • Readiness does not start the game on its own. Once every current player is ready, a member finalizes the lobby with an explicit StartGame.
  • More players may keep joining until the room reaches max_players.

LobbyStateChanged example (one player ready):

JSON
{
  "type": "LobbyStateChanged",
  "data": {
    "lobby_state": "lobby",
    "ready_players": [
      "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    ],
    "all_ready": false
  }
}

Finalized

A member has sent StartGame while every current player was ready. The server finalizes the game:

StartGame is accepted only when every current player is ready (GAME_START_NOT_READY otherwise) and the sender is permitted to start — the room's authority player if it has one, else any member (GAME_START_FORBIDDEN otherwise). On finalization the server:

  • Records a finalization timestamp.
  • Sends a GameStarting message to every player with legacy peer metadata.
  • For every negotiated-v3 member, follows with a per-recipient SessionPlan that describes topology, transport, peers, relay fallback, and ICE only when the selected transport is WebRTC. The relay floor is an explicit no-peer relay/relay plan; v2 members receive no plan.
JSON
{
  "type": "GameStarting",
  "data": {
    "peer_connections": [
      {
        "player_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "player_name": "Alice",
        "is_authority": true,
        "relay_type": "matchbox"
      },
      {
        "player_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
        "player_name": "Bob",
        "is_authority": false,
        "relay_type": "matchbox"
      }
    ]
  }
}

After finalization, the room is typically cleaned up by the server.

Message Flow Example (2 Players)

Here is the full message sequence for a two-player game from room creation to game start:

Text Only
Alice (Client)             Server              Bob (Client)
     |                        |                       |
     |-- JoinRoom (create) -->|                       |
     |<-- RoomJoined ---------|                       |
     |   (code: HK7T3W)      |                       |
     |                        |                       |
     |                        |<-- JoinRoom (join) ---|
     |<-- PlayerJoined -------|--- RoomJoined ------->|
     |<-- LobbyStateChanged --|-- LobbyStateChanged ->|
     |   (state: lobby)       |   (state: lobby)      |
     |                        |                       |
     |-- PlayerReady -------->|                       |
     |<-- LobbyStateChanged --|-- LobbyStateChanged ->|
     |                        |                       |
     |                        |<-- PlayerReady -------|
     |<-- LobbyStateChanged --|-- LobbyStateChanged ->|
     |   (all_ready: true)    |   (all_ready: true)   |
     |                        |                       |
     |-- StartGame ---------->|                       |
     |<-- GameStarting -------|--- GameStarting ----->|
     |   (legacy metadata)    |   (legacy metadata)   |

The move into finalized is signaled by GameStarting itself — the server never broadcasts a LobbyStateChanged carrying finalized. The finalized value appears only in the lobby_state field of a later RoomJoined / Reconnected snapshot.

Once both players are ready, either member may send StartGame (this room has no claimed authority, so any member may start). The game does not start until that explicit message arrives.

Single-Player and Solo Starts

max_players is a ceiling, so a room can start with fewer players than its limit — including a single player. The lone member sends PlayerReady and then StartGame to finalize a solo session and receive legacy GameStarting peer metadata. A single ready member is always permitted to start (it is the only member, so the "any member" authorization rule applies when no authority is set).

Configuration

Key room-related configuration options:

  • max_players -- Maximum players per room. Default: 8, maximum: 100. Set per room at creation time via the JoinRoom message.
  • room_code_length -- Length of generated room codes. Default: 6.
  • room_cleanup_interval -- Seconds between cleanup sweeps. Default: 60.
  • empty_room_timeout -- Seconds before an empty room is removed. Default: 300.
  • inactive_room_timeout -- Seconds before an inactive room is removed. Default: 3600.

For the full list of server configuration options, see the Configuration.

Next Steps