Protocol
Every message is JSON with v: 1.
| To server | To client | ||
|---|---|---|---|
auth {ticket} |
first message, ticket mode | welcome / sync {you, boot, ver, code, private, max, match, rev, term, host, started, players, shared, now, tick, saved, resumed, kept?, saveBusy?, saveError?} (ver: the room’s game version) |
full state |
join {spec, code?, private?, name?, hidden?, f?} |
delta {rev, at, players?, pt?, shared?, st?, sc?, fx?} |
changes and effects, batched per tick; pt {player id: sample time} and sc (shared) are the senders’ own sample times on the shared clock (c in me/shared, trusted within 500 ms back and 50 ms ahead, else the hub’s time): pages draw others at these times, so updates sent 80 ms apart are drawn 80 ms apart whatever the fan-out did; the page applies players, then shared, then fx [{from, name, data}]. f: ["fxb"] = the page reads fx in the frame; a page without it gets single fx messages. The host’s frame leaves out its own shared writes; a hidden page gets no frames (one sync when it is back) |
|
me {s, c?} / shared {term, p, c?} |
own state / host’s shared state (c: when the values were set, shared clock ms) |
joined / online {player}, left {player} |
|
act {id, name, data} / result {id, ok} |
ask the host / host answers | host {term, player} |
new host term |
newer |
the game was edited and someone opened the new version with this room’s code: it is theirs now (this room carries on under a hidden code); game.js says press restart | ||
fx {name, data}, alive {term}, vis {hidden} |
action {id, from, name, data} |
host only | |
start {resume?}, end, kick {player}, name {name}, leave, time {c} |
result {id, ok, error?}, fx (old pages only), started {resumed?}, ended, error {code, ref?}, time {c, s} |
ref of bad_value: the path that broke the shape (mobs.12.name) |
|
sync |
after a refused write: send me your copy | sync {...} (as welcome) |
room.js replaces its copy with it |
rooms {spec?} |
the other open rooms of this game (joined sockets) | rooms {rooms: [{code, host, players, away, max, started, match, ping, slowed, full, you?}], at} or rooms {off: true} |
see Other open rooms |
stay |
the person pressed Keep it after an idle warning | idle {closesAt} |
the room closes then for lack of activity (0 = not any more) |
rate {div} |
send me every div-th frame (1, 2, 4; anything else = 1) | rate {div, every} |
the rate in use and ms between frames; changes only at a frame every rate gets |
time {c, r?} |
r: the page’s best recent round trip (ms), shown in room lists |
time {c, s} |
|
profile, save {p} |
read / merge into your saved profile | profile {p, v} or {off:true}, saved {at, v} (v: the save format number) |
|
world {clear:true} |
host: forget the saved game | welcome also has saved {at, v} and resumed |
Close codes: 1012 server restart (reconnect, the first try spread over 0.3 to 3 s so a whole server does not come back at once; with onSnapshot/importRooms the room carries on: welcome has kept: true, the page says “Back after a short pause”), 1013 busy (reconnect; over 200 new sockets a second or 200 unverified), 4009 a visible page silent for 15 s (reconnect; its seat is held), 1011 the room failed and was closed (reconnect: a new room; the rest of the server is untouched), 4001 not allowed, 4002 full, 4003 replaced by another tab, 4004 too fast, 4005 bad message, 4006 removed by the host, 4007 game updated (reload; also sent by room.js itself when a reconnect lands in another version than the page joined first), 4008 too slow, 4010 closed because nobody played for idleMs (final: the lobby offers a new room).
A write that breaks the shape or size limits is refused whole (error bad_value, too_big or out_of_range), never in part, and the connection stays. room.js checks the shape before it applies or sends a write (the game gets error with the path, nothing goes out) and, when the server refuses one anyway, asks sync and takes the server’s copy, so the host never stays out of step. Sizes are counted in bytes, as the server counts them (a Vietnamese letter is 3, an emoji 4); room.js splits a big update at any depth and reports too_big for one value it cannot split. A message over 16,000 bytes never leaves relay.js: the game gets error too_big.
Edited games: a join always carries the page’s current version (the ticket’s hash). Joining a room code whose room runs an older version gives that code to a new room on the current version, so everyone who presses restart lands together; the old room keeps playing for whoever is in it, under a new hidden code, and hears newer. A page left open on the old version that reconnects into the new one stops with 4007 (press restart) instead of playing old code in it.
A page that reloads (or a second tab) takes over its seat and counts its actions from 1 again; its setup() runs, but its g.shared writes there count only on a page that saw the match start (or when the shared state holds only shell keys), so a host that reloads mid-match never resets the live game; actions the old page sent and the host had not answered are dropped. A page that comes back after its seat was given up sends its own state again. The relay’s connect() may reject with {final: true, reason} (signed out, the game deleted, rooms off): it then stops and the game shows the reason; 10 minutes of failed tries in a row end the same way.