Joining a room
A multiplayer game raises the same questions each time it changes: does a second player get in, what does a player see when the room turns them away, and does the game reach the room it is deployed beside. spawnite play join answers them with one command. It opens players’ pages headless against a room, one after another, and prints what each saw of its join. The room can be one the command did not start, such as a room a teammate runs or a deployed one, or the game’s own room, started fresh.
This page says how to join pages to a room, how to read each page’s outcome, and how to play the pages on once they are in. Multiplayer says how a room runs, and Measuring runs how to play a room with bots and no page.
Join pages to a room
Section titled “Join pages to a room”Give the room’s WebSocket address, ws:// or wss://, from the game’s folder:
pnpm spawnite play join ws://localhost:4371 --pages 2Joined 2 built pages to ws://localhost:4371: Page 1: welcomed as character 18 in 9.2 s; build 95005bcc4f62. Now joined, 2 players; round trip 54.7 ms, 1,207 B/s, the room's step 0.32 ms. Page 2: welcomed as character 19 in 5.8 s; build 95005bcc4f62. Now joined, 2 players; round trip 271.5 ms, 2,247 B/s, the room's step 0.72 ms.Without an address, the command starts the game’s own room on a free port, with the environment its spawnite.room sets, and ends it when it ends. That room logs a heartbeat each second and does not restart when a file changes, which would drop every page. --room-env NAME=VALUE sets its environment over those, such as --room-env MAX_PLAYERS=2. For its own room, the output also names the room’s build, its last heartbeat, its peak memory and its timeline as it stood last.
The command takes the following flags:
--pages <n>opens that many players’ pages, 1 to 12, one by default. Each opens in a browser context of its own, so it keeps its own storage and joins as a player of its own. A page opens once the page before it was welcomed or refused, so the pages join in their numbered order.--join-seconds <n>is how long each page has to be welcomed or refused: 90 by default, long enough for a dev server’s first start.--devserves the game’s dev server in place of its build, and--url <url>opens a page that is already served, such as the deployed game. Choose the pages says which to use.--screenshot [file]shoots every page as the command ends: to the file with each page’s number after its name, such asjoined-1.png, or under the game’s.spawnite/shotswithout one.
--json carries every page’s outcome and its whole room, as the engine’s state reads it.
An agent without a shell calls the spawnite MCP’s join_room tool, which runs the same join and returns the same lines. It takes the flags by their names in camel case, such as pages, joinSeconds and roomEnv, the room’s address as room, each press as { key, at, until } in presses, and screenshot: true for --screenshot with no file.
Read what each page saw
Section titled “Read what each page saw”Each page reads its own room through the engine, as a player’s page records it, and the command prints one of three outcomes:
- Welcomed: the room’s welcome arrived and the page holds a character, named by the id the room’s dump keys it under. A room of another build admits the page, and the line names the room’s build beside the page’s. The line adds what reaches the page as the command ends: the players it sees, the round trip, the bytes it receives a second and the room’s step. The command waits up to three seconds after the pages play for each page’s first round trip and rates, since a page publishes its rates once a second.
- Refused: the room closed the page’s socket with its word on why, and the line names the close code and reads its reason out. A full room closes with 4004 and says how full it is. A room from before the build warning closes a page of another build with 4006 and names both builds. Any other code prints its reason as the room sent it.
- No answer: neither within
--join-seconds. The line says where the page stood, such as still connecting, and the errors it threw or logged, such as a socket the room’s address refused.
Three Holdfast pages against the game’s own room, capped at two players, where the two welcomed press ready:
pnpm spawnite play join --pages 3 --room-env MAX_PLAYERS=2 --press KeyR@0 --seconds 15 --screenshot .work/ready.pngJoined 3 built pages to ws://localhost:50486, the game's own room on build 95005bcc4f62: Page 1: welcomed as character 18 in 7.0 s; build 95005bcc4f62. Now joined, 2 players; round trip 10.1 ms, 759 B/s, the room's step 0.31 ms. Shot to …/.work/ready-1.png. Page 2: welcomed as character 19 in 3.6 s; build 95005bcc4f62. Now joined, 2 players; round trip 3.2 ms, 904 B/s, the room's step 0.35 ms. Shot to …/.work/ready-2.png. Page 3: refused with close 4004 in 3.5 s: the room is full, 2 of 2 players. Shot to …/.work/ready-3.png.Pressed KeyR at 0 s on each welcomed page, playing 15.0 s after the joins.The room's last heartbeat: 2 players, 389 MB, a step 0.87 ms on average and 1.66 ms at the longest; 391 MB at its peak.Its timeline: wave 1, phase fight, kills 0, downs 0, coins 0, monsters 2, earned 0, spent 0, rerolls 0, guns 0, upgrades 0, cards 0, fed 0, fire 0, chainShocks 0, blasts 0, steamClouds 0, ownReactions 0, linePoints 0, capstones 0.The two wardens pressed ready, and the timeline shows the first wave’s fight; the third page’s shot shows the loading screen’s “The room is full: 2 of 2 players.”
A player who drops keeps her character, and her seat, for 60 seconds, as When a player drops says. So a second command against a room that stays up counts the first command’s players until their minute is over.
Choose the pages
Section titled “Choose the pages”A built page names its build as it joins, and a room of another build admits it with a note naming both, as A page and a room of another build says. The command serves the game’s build by default, with its own Vite’s vite preview, so it shows what a player’s page does. It serves the build as it stands: build the game first with its own build script, pnpm build. A build from before an edit reads as another build to a room started after it:
Joined 2 built pages to ws://localhost:4371: Page 1: welcomed as character 20 in 5.9 s; build 7c635a71e8e2, and the room runs another build, 95005bcc4f62. Now joined, 4 players; round trip 113.2 ms, 1,511 B/s, the room's step 0.41 ms. Page 2: welcomed as character 21 in 5.2 s; build 7c635a71e8e2, and the room runs another build, 95005bcc4f62. Now joined, 4 players; round trip 282.4 ms, 3,443 B/s, the room's step 0.82 ms.Against the game’s own room, the output adds that the build served is not the tree the room runs.
--dev serves the dev server’s page, which names no build, so a room of any build admits it. Use it to check a change without a build, and not to check the build. --url opens a page that is already served, such as the deployed game at its own address, with the room’s address in its room parameter; the page must be one whose engine exposes its room, as every engine from this one on does.
Play the joined pages
Section titled “Play the joined pages”Once every page has its outcome, the command can play the welcomed pages on and read them again:
--press <control@seconds>presses a control on each welcomed page, in seconds from the last page’s outcome:control@secondstaps it andcontrol@from-toholds it, such asKeyR@0orMouseLeft@1-2. The controls are the ones Name a control lists.--seconds <s>plays the pages that long before the command reads them.--until <expression>plays until the expression holds on each welcomed page, within--until-seconds, 120 by default, and the command exits 1 when it did not. The expression reads the page’s globals,documentamong them, and the engine’scamera,loop,cursorandroom, such asroom.players === 2.
The pages play live: nothing holds the room, as spawnite play start holds its own. Read what each page saw shows the two Holdfast wardens readying up with --press KeyR@0 and the first wave starting.
Join a room the backend runs
Section titled “Join a room the backend runs”A room the backend runs, such as a production room on a machine, seats only a player the platform sends: the app takes a seat through the backend’s rooms.join and hands the page its ticket, and the room checks the ticket with the backend. A page opened on its own sends no ticket, and the room refuses it with 4007, “This room seats players the platform sends.” --seat [game] takes the seat the way the app does, so the command checks such a room end to end. The game is its id, a link to its page, or, with no value, the one the game folder’s package.json names by its id, which spawnite games create writes:
SPAWNITE_CONVEX_URL=https://api.spawnite.com pnpm spawnite play join --seat 22699540 --url "https://22699540.$PLAY_DOMAIN/"PLAY_DOMAIN is the play domain that the trust boundary names.
The first line names the seat, and the game version and engine release its room runs. An Arena room on this machine that checks its tickets with a backend, joined from the game’s folder with --seat --dev, reads:
Took a seat in a room of game 48201973 through the backend's join, as player 0f3a9c2b: the room runs version 0.Joined 1 dev server page to ws://localhost:47401; a dev server's page names no build, so a room admits it whatever build it runs: Page 1: welcomed as character 19 in 3.3 s; no build, as a dev server's page. Now joined, 1 player; round trip 2.1 ms, 1,098 B/s, the room's step 0.21 ms.The same page without --seat reads refused with close 4007, "This room seats players the platform sends. Open the game from its page."
The command signs in as spawnite login signed this machine in, calls rooms.join with the game’s id, and waits for a room that is starting to open. It joins the room at the address join answered, and adds the ticket to the page’s join message as it leaves the browser, as the app’s init hands it to the engine. The ticket goes to that address alone, so the command takes no room address beside --seat: a ticket sent to another host could be replayed at the seat’s room. The room seats the page under the account’s name and its id in the game, and --json carries the seat as seat.
SPAWNITE_CONVEX_URLnames the backend, production’s API address here; without it, a run from this repository’s checkout uses the dev deployment, and a cli installed from npm uses production. A login belongs to one backend’s sign-in, so sign in against production first, with the same variable onspawnite login, and keep that login apart withSPAWNITE_CONFIG_DIR.- The page is the one
--urlopens, such as the game’s own address, which serves the published version, the one Play seats new players on; or, from a game’s folder, its build or dev server. A room of another build admits a built page with a note naming both. - One page: an account holds one seat in a game, so a second page would move the first page’s seat, and its room would close the first page with 4008.
- Join is a real player’s join. It counts toward the account’s seats and room starts, and it starts a room when none has a seat.
What the other engines do
Section titled “What the other engines do”Roblox Studio’s local server test starts a server and a number of player clients from one button. Unity’s Multiplayer Play Mode runs up to four virtual players beside the editor. Unreal’s Play In Editor takes a number of players and a net mode, and a client can connect to a server it did not start. Godot runs several instances of the game at once. Each opens windows for a person to watch. spawnite play join takes the same shape, a number of clients against a server started here or elsewhere, and reports each client’s join as text and JSON, so an agent reads the answer rather than a window.