Skip to article Developer portalAnnounce, upload and sell your game

GAMENIGHT / DOCUMENTATION

Build a lobby

A lobby is a persistent party interface, implemented as a separate executable. The runtime owns players, controller bindings and game sessions. The lobby chooses how to present them. The Godot Living Room is a runnable example with names, custom faces, guest joining, game selection, queueing and resume.

Run the Godot example

Install Godot 4.5 or newer and the development dependencies. From the repository root:

python scripts/run-local.py --lobby godot --godot /path/to/godot --shelf /path/to/games.json

On Windows, pass the Godot executable path. With no shelf, the script supplies the terminal SDK demo. --skip-build reuses existing Rust binaries. The default lobby remains the bundled platformer; this option selects godot-lobby.

Open sdk/godot/project.godot in the editor to change the example. Start it through the runtime to get an authenticated connection. Opening the project alone displays a connecting screen, not a synthetic party.

Controls: connected controllers join automatically. LT/RT cycle games; the selected game is larger in the centre of its two neighbours. The stick/D-pad moves between controls. On a game card, A appends a new occurrence to the queue; X inserts it first among upcoming games. Neither action starts gameplay. The merged queue panel shows its first entry as Up next, with the remaining games alongside it. Select Start game to launch the head of the queue. Back returns to the lobby or resumes an overlay-paused game. Y leaves that controller's seat.

The controller hint bar shows X Play next · A Queue · LT Previous · RT Next with Xbox-style face-button and trigger icons. These hints are not selectable; focus stays on the game cards and the other lobby controls.

Talk to the assistant

When GAMENIGHT_ASSISTANT_URL supplies an authenticated session entry, select Talk to assistant and hold A, or hold the control with the mouse. Keyboard users can hold C. A pulsing microphone shows when recording is active. Release to transcribe and send; B or Escape cancels before submission. The Menu/Start button has no assistant shortcut. The lobby remains open throughout the request and shows the assistant's result. Closing a submitted request's panel does not undo a game change already sent to the host.

This source example currently transcribes with the installed Windows speech recognizer. Enable microphone access for desktop apps and install a Windows speech language. Recording lasts at most 20 seconds and stops on release, focus loss, controller disconnection or loss of the runtime connection. Audio is transcribed locally; only the text reaches the existing assistant API. The temporary WAV is deleted after transcription. A process crash may leave that file in Godot's user-data directory. The recording effect only runs while the talk control is held, up to the 20-second limit.

The current native sign-in path supports the disposable preview's loopback /start/… entry. It keeps its session cookie in memory and uses the existing CSRF-protected assistant endpoints. Redirects are not followed. An ordinary hosted /agent page cannot share a browser's login with a native process; production native sign-in and non-Windows speech adapters remain unimplemented. Local AI requests send on release. Paid requests first show the maximum credit estimate and require explicit confirmation in the lobby.

lobby/assistant.gd and lobby/transcribe-windows.ps1 belong to the example, not the ordinary game addon. The current runner uses the source project; packaged exports still need helper extraction and packaging support.

Run python scripts/test-lobby-assistant.py --godot /path/to/godot for the loopback authentication, transcript, result and credit-confirmation checks. Add --check-windows-speech to also transcribe generated speech and check that the temporary recording is removed, without opening a microphone. The lobby flow tests hold/release ownership and disconnect cancellation with simulated controller frames. These tests do not validate a physical microphone or speech accuracy in a noisy room.

Queue behaviour

The lobby sends queue_game with first: false for A and first: true for X. This is one atomic host command, so simultaneous additions cannot overwrite each other's order. It preserves the current game and its position. next starts the prepared queue head. The older play_next command can start an idle game and is not used for either catalog button.

Register a replacement lobby

Set GAMENIGHT_LOBBY_GAME on the daemon to your shelf entry's ID. Include "GAMENIGHT_LOBBY_API": "1" in that entry's launch.env. This declares runtime input and party preservation before the executable connects, including when it fails during startup. The launch command may be an exported executable or Godot with --path pointing at a project.

The runtime supplies GAMENIGHT_ADDR, GAMENIGHT_GAME_ID and GAMENIGHT_TOKEN. Use one WebSocket connection (plain JSON lines also work):

{"type":"hello","role":"lobby","game":"my-lobby","token":"<GAMENIGHT_TOKEN>"}

Only the configured ID with its launch token can claim this role. Unlike the game development handshake, a tokenless lobby is rejected. The welcome carries the complete party snapshot. Send {"type":"lobby_ready"} after building the first usable frame. The runtime then sends lobby_focus with screen ownership. The lobby does not receive disposable game sessions.

This is an additive extension of protocol v1. Older daemons do not support the lobby role; deploy the updated daemon and lobby together. A launch token binds the connection to the selected process; it does not sandbox native code. Existing overlay connections retain their trusted local scope.

State and commands

The lobby receives party_state, lobby_focus, controller_frame and error. It may issue the existing party commands, plus lobby_ready and quit_party. It cannot report another game's session lifecycle or publish physical controller frames.

Request Meaning
join_party, leave_party, profile and seat commands Change runtime-owned players
queue_next {game} Prepare a chosen game without starting it
play_next {game} Choose the next game; starts when ready only if no game is active
queue_game {game,first} Append a new occurrence, or insert first among upcoming games; never starts playback
next Start the prepared next game, or wait for its preparation
move_playlist_entry {expected,from,to} / remove_playlist_entry {expected,index} Edit an occurrence using the latest playlist snapshot; stale edits are rejected
open_overlay Pause the active game and return to the lobby
close_overlay Resume only the pause caused by opening the lobby
set_setting {game,key,value} Apply a setting validated by the runtime
quit_party Explicitly shut down the runtime and its children

Use party_state as the authoritative result. Commands currently have no request IDs or individual success acknowledgements. Do not blindly replay commands after a lost connection. Reconnect with the launch token and rebuild from the new snapshot. The SDK never queues disconnected writes.

The Godot client lives in addons/gamenight/lobby.gd. Instantiate it directly; do not enable the game plugin's GameNight or GameNightScreen autoloads in a lobby. Its signals supply snapshots, controller frames, connection status, focus and errors. The example displays embedded or HTTPS catalog artwork.

Minimal Godot client

Attach this script to your lobby scene. Connect signals before adding the client, so the view receives the initial welcome. The client sends lobby_ready after one scene-tree frame; finish any asynchronous screen setup before adding it.

extends Control

var lobby = preload("res://addons/gamenight/lobby.gd").new()
var party: Dictionary = {}

func _ready() -> void:
    lobby.party_changed.connect(func(snapshot):
        party = snapshot
        queue_redraw())
    lobby.rejected.connect(func(message): push_warning(message))
    add_child(lobby)

func add_to_queue(game_id: String) -> void:
    lobby.queue_game(game_id)        # A: append

func put_first(game_id: String) -> void:
    lobby.queue_game(game_id, true)  # X: put first, without starting

func start_queue() -> void:
    lobby.start_next()              # Separate Start action

This is the connection and queue portion only. A complete lobby must also handle focus_changed and controllers_changed as described below. The runnable scene is sdk/godot/lobby/main.gd.

Queueing adds an occurrence, so the same game can appear more than once. Use playlist indices to move or remove one occurrence; game IDs alone are ambiguous. The expected field must contain the full latest party.playlist value. On a stale-edit error, redraw from the latest snapshot instead of silently retrying. The SDK's move_in_queue and remove_from_queue helpers include this guard.

For loading feedback, read warm_session.phase, progress and progress_label, plus installs for downloads. An entry being in the queue does not mean it is ready. Keep the selected next game's artwork visible while it prepares. The runtime's playlist currently wraps when it reaches the end; it is not a finite queue that automatically stops the night.

Input and screen ownership

Replacement lobbies receive runtime-sampled controller frames. Device tokens are opaque, and must be matched against seats[].controller. The runtime joins connected controllers, handles controller activity and Back/Select. It consumes an A press held during connection and Back before broadcasting frames so clients cannot accidentally start a game while joining or immediately reopen a resumed game.

Controller frames use a sampler-owned delivery path, separate from party-state serialization. Large catalog snapshots must not block stick updates. Back is consumed by the host; an A button held when a controller connects is suppressed until release. The stream retains opaque controller tokens and full analog axes.

Render the active lobby at a normal frame rate. On lobby_focus: false, mute, release the screen and reduce background work while keeping the connection alive. Only explicit player requests start or resume games; window focus is not such a request. The Godot example uses window minimisation and restoration.

Player and controller state survives a replacement lobby disconnect. A socket failure can reconnect to the same process. A dead process gets up to three restart attempts before opening the local recovery page. The game is paused and players remain registered. Recovery offers Retry lobby, Resume game and End game night. Retry resets the restart budget; it does not erase the party. The installed launcher keeps browser requests available throughout the session. Development hosts need GAMENIGHT_WEB=1 to serve the recovery page. quit_party is the deliberate exit path. The legacy platformer retains its old input and disconnect behaviour until it adopts this role.

Faces and names

Read names, skin colours, clothing colours and avatar payloads from the current snapshot. The reusable addons/gamenight/face.gd implements the canonical face coordinates, transparent pixels, historic 16/32 pixel anchors and nearest-neighbour artwork. It caches decoded textures, bounds its cache and falls back to a default face for missing or malformed artwork.

var faces = preload("res://addons/gamenight/face.gd").new()

func _draw():
    faces.draw_face(self, player, Vector2(100, 100), 32)

Call it from a CanvasItem's drawing callback. Set that CanvasItem's texture_filter to TEXTURE_FILTER_NEAREST. Supply the head centre and radius; leave space around the head for hats and hair. Repaint when a party snapshot changes a profile. The example keeps names and portraits beside each other.

Verification and current limits

Build the daemon, then run:

python scripts/test-godot-lobby.py --godot /path/to/godot
python scripts/test-godot-lobby.py --godot /path/to/godot --capture-dir .local/lobby-previews

The flow test runs the actual Godot scene against a real daemon, using synthetic profiles and protocol stub games. It checks queue versus play, pause/resume, live names and artwork, reconnect, leaving and joining. The capture option requires a display and saves desktop and narrow-window PNGs. Stub games do not validate real gameplay, controller drivers, audio or OS focus handoff. Test those with real games and devices on every supported platform before release.

The example has separate player-colored selections, queue controls and media controls. LT/RT cycle each player's selected game on a trigger press; holding the trigger does not keep cycling. The selected game is larger in the center, with smaller neighbors on either side. The stick and D-pad move between controls, with animated focus and card movement. The layout expands to the display aspect ratio, keeps players in a compact strip and gives Up Next a large game image. Guests get their random face and hat from the host. Music depends on an active OS media session; it does not provide a streaming service itself.

Phone joining and profile pickup

The public local-web server supplies GET /api/player-links. Its room contains the current code, join_url and qr_svg; the URL uses the actual LAN host and web port for a local room, or the registered cloud room's origin. Do not build a QR from the lobby's WebSocket address. Unregistered rooms do not get a QR. scripts/run-local.py --lobby godot starts local-web automatically.

The Godot view defaults to http://127.0.0.1:7913. Set GAMENIGHT_LINKS_URL only when running a different local-web bridge. Phone profiles waiting for pickup are returned in the same response. A local controller selects its profile and the native lobby posts to /api/room-pickup/{pending_id}/{player_id} with X-GameNight-Local-Pickup: 1. Pickup requires a loopback connection and this header; a phone cannot assign itself to another controller through this API.

Game settings and artwork

The Settings button opens the selected game's declared settings. Toggles, bounded integers and choices map to set_setting; the view reads the accepted values from party.settings. A game must connect and declare its settings before they appear. Queue it to load them. The host validates values, and the game receives setting_changed.

addons/gamenight/artwork.gd loads PNG, JPEG and WebP asynchronously. It accepts embedded images and HTTPS URLs, plus loopback HTTP for development. Downloads are limited to two at a time, eight seconds and 8 MiB each; decoded images must fit within 4096 × 4096. The texture cache holds 32 entries. Missing, invalid or failed artwork leaves the game title visible instead of blocking navigation.

Choose a lobby in the installed app

Register an installed executable in local-games.json in the GameNight data directory, alongside the launcher's shelf.json. Use an absolute executable path and an absolute existing cwd when supplied:

[{"id":"my-lobby","title":"My lobby","launch":{
  "command":"/absolute/path/to/my-lobby",
  "env":{"GAMENIGHT_LOBBY_API":"1"}
}}]

Both bundled lobbies have Select other lobby in their Start menu. In Living Room, press Start on a controller, M on a keyboard, or click Start · Menu. The platformer uses the player's existing Start menu. The chooser opens on the host computer and lists each installed lobby by name. Saving does not interrupt the current game: the selected lobby is used the next time GameNight starts.

Living Room shows Current game and Up next side by side, or stacked in a narrow window. Current game shows the game's screenshot and Resume, even when other games are queued. If there is no current game, only Up next is shown. Resume keeps the current session and queue intact; Start game under Up next advances the queue.

Launch gamenight-launcher --choose-lobby, or open http://127.0.0.1:7913/host/lobby on the host while the installed app runs. Choose the bundled platformer or a registered replacement. The choice is saved in selected-lobby.json and applies on the next launch. Missing executables fall back to the bundled lobby. Registration does not download or install a third-party lobby. Only install code you trust.

The preference and recovery controls are host-only. They reject LAN requests; mutations also require X-GameNight-Host: 1. The recovery page uses retry_lobby, close_overlay and quit_party through a trusted local overlay. Retry is available only after the automatic restart budget is exhausted.

Automated coverage

python scripts/test-godot-sdk.py --godot /path/to/godot checks the shared game adapter's seat mapping, profile updates, lifecycle guards, stale input and focus behaviour. python scripts/test-lobby-recovery.py checks the actual daemon's crash budget, retry and party retention. Set GAMENIGHT_TEST_DAEMON to a built daemon binary to use a non-default build directory.

Run python scripts/test-host-lobby.py for host preference and room-link HTTP checks (requires local port 7913). The lobby flow also checks settings and artwork fallback. These checks do not certify physical controller drivers, TV focus handoff, media sessions or phone camera scanning. Those still need device tests on each release platform.

Closed games and failed launches

Read party.game_issues before showing loading or ready status. Each item has a game ID and a kind: closed, disconnected, exited_unexpectedly, failed_to_start, or startup_timeout. warming still identifies the intended next game; an issue means its process is stopped, not currently loading.

The host clears the lost session and broadcasts the issue to connected lobbies and overlays. It also checks every 200 ms for processes that exit before sending hello. Startup has a ten-minute connection deadline to allow development builds. A missing executable is reported immediately. Games remain stopped until an explicit queue/start command retries them. Show a Retry action using next for the head of the queue, or queue_next for a specific game without starting it.

A clean process exit can be labelled “Game closed”. A broken connection alone cannot prove a crash or a deliberate quit; show “Game disconnected”. An observed nonzero exit is “Game exited unexpectedly”. These states describe evidence, not user intent. Reconnecting games clear their issue and must prepare again before being shown as ready.