Appearance
Lobbies: rooms of up to N players
You'll learn: what a lobby is, who lands in which one, what's shared and what isn't, and how to run rounds. You'll need this when: your game has a player limit, rounds or parties.
Status: Built (headless, and in the plots dev stack): lobbies, the platform's lobby choice, locking, the start countdown, siloing, suspend and resume, the cross-lobby calls, the featured lobby and the fade at the boundary. Not in the live world yet.
A lobby is a room, not a waiting room
A lobby is a siloed room of up to lobbies.size players over your plot: a layer over the same physical bounds, not a separate world. It has no game state of its own: no "waiting", no "running". It's just a room. A puzzle adventure for four is a lobby of 4 that anyone can walk into; 1,000 players at a Bedwars plot with lobbies of 16 are 63 lobbies, all in the same place, each seeing only its own.

A runtime declares only the size in plot.json:
json
{ "kind": "runtime", "lobbies": { "size": 4 } }area (optional, relative to the plot's min corner) is where a new lobby's players start: { "size": 16, "area": { "min": [0, 64, 0], "max": [16, 72, 16] } }. "lobbies": null (or none) means no lobbies: the plot is part of the world's own lobby ("main"), which can hold 1,000 or more players.
Who lands where
The platform does it, so you don't have to:
- As a player approaches (about 2 chunks out), the platform picks a lobby for them: the fullest lobby that isn't full or locked, or a new one if there's none. They see that lobby's players before they walk in.
- While they're outside, the choice can change (that lobby fills, or locks).
- When they cross the boundary, it's fixed. Players from other lobbies fade out over 0.5 s.
A lobby is full when players.length >= size. You don't manage lobby creation, filling or idling.
To choose yourself, a runtime exports chooseLobby(player, lobbies): return a lobby id, or null for a new one. A choice that's full or locked falls back to the platform's.
ts
// keep friends together: the lobby a player's party is already in, else the platform's usual choice
export function chooseLobby(player: PlayerId, lobbies: Lobby[]): LobbyId | null {
const party = storage.getPlayerProfile<LobbyId | null>(player, "partyLobby");
const open = lobbies.filter((l) => !l.locked && l.players.length < l.size);
return open.find((l) => l.id === party)?.id ?? open.sort((a, b) => b.players.length - a.players.length)[0]?.id ?? null;
}Locked lobbies
lobbies.lock() (runtime only, from a lobby hook) closes the current lobby to newcomers, even if it has room; lobbies.unlock() opens it again. Use it for:
- A round in progress: lock when the round starts, unlock when it's over, so the next players can join.
- A fixed party: lock once the group has set off (a story dungeon after the first door), so nobody walks into the middle of their story.
ts
const l = lobbies.current(); // { id, players, size, locked, countdownEndsAt?, suspended, ... }
if (l.players.length >= l.size && !l.locked) lobbies.lock();The start countdown (optional)
Some rounds should start fair: everyone arrives, nobody can be hit early, and everyone sees the same "3, 2, 1". lobbies.startCountdown(opts?) (runtime only) does that:
- The base game draws the countdown (a plot can't fake it, and the plot can't draw over it).
- It runs until the newest member's 10 s entry protection ends, or
opts.minSecondspass, whichever is later. Then youronCountdownEnd()runs and the round is yours. - By default, only players still in their own entry protection are protected; those who've been in for a while aren't protected again.
{ protectEveryone: true }protects every member until it ends: an equal start. - If someone leaves meanwhile,
onCountdownPlayerLeft(p)runs:startCountdown()again,cancelCountdown(), or let it run.
ts
export function onPlayerEnter(p: PlayerId) {
const l = lobbies.current();
if (l.players.length >= l.size && !l.locked) {
lobbies.lock(); // no newcomers mid-round
lobbies.startCountdown({ protectEveryone: true }); // an equal start
}
}
export function onCountdownEnd() { /* teleport, kits, go */ }You don't need it. A puzzle room for four that anyone can walk into has no countdown and no lock: players arrive, their entry protection ends (onPlayerProtectionEnd), and they play.
Everything is siloed
Players, mobs and entities, block edits, mesh-terrain changes, storage, timers, audio, UI, visibility sets and chat all belong to one lobby. Nothing crosses unless your plot-level code moves it. What players break and place under the base game's rules, and the items they drop, are their lobby's too.
ts
// in a lobby hook: acts on this lobby only
world.setBlock([8, 70, 8], STONE); // this lobby's edit layer
storage.set("score", 7); // this lobby's storage
ui.chat("lobby", "gg"); // this lobby's playersLobbies are containers, "like a file"
A lobby has a stable id and keeps its data until you delete it. Your plot owns its data:
ts
// plot level, runtime only
const id = lobbies.create({ size: 8, seed: 42 }); // a lobby made by you (its own world-gen seed)
lobbies.runWithin(id, () => storage.get("score")); // read or write inside it
const data = lobbies.exportData(id); // { id, lobby, storage, edits, entities }
lobbies.movePlayers(["p1", "p2"], id); // move players (refused while they're protected)
lobbies.setFeatured(id); // the lobby distant viewers, the map and the feed see
lobbies.delete(id); // refused with "lobbyNotEmpty" while players are in itMerging lobbies, a global leaderboard, a persistent world: all yours to build from these. The platform doesn't merge anything for you.
Empty lobbies are suspended. No players and no due timers: the lobby stops ticking and costs only storage (suspended: true). It's loaded again (onLobbyLoad) when a player is put in it or a timer falls due. Pass neverSuspend: true to lobbies.create to keep one running within your CPU budget.
Write code for one lobby
One sandbox runs all your lobbies. @inventery/lobby keeps one state object per lobby so you can write as if there were only one:
ts
import { defineLobbyGame } from "@inventery/lobby";
const game = defineLobbyGame({
setup: () => ({ round: 0, alive: [] as string[] }),
onPlayerEnter(s, p) { s.alive.push(p); },
tickLobby(s, dt) { /* ... */ },
});
export const { onLobbyLoad, onPlayerEnter, tickAllLobbies } = game;The state lives in the lobby's storage, so it survives restarts. See the reference.
The draft lobby
Every plot also has a private lobby, draft, for you and invited testers, where new builds hot-reload. See Drafts and publishing.