five-on-it / apps/api/src/lobbies
apps/api/src/lobbies
The lobbies (GAME_DESIGN §13, TECHNICAL_SPECS §9.6): where the players of a multiplayer mission gather, see everything about it, pick their sides, get ready, play its run, see its results and vote on the next mission, on this level or another, so the group plays on from run to run. The LobbyHub keeps them in the API's process, in memory, as the SocialHub keeps the parties: the runs they open and their results are the store's (runs.ts). At most LOBBY_LIMITS.lobbies are open at once.
A lobby is for one mission of one game (a published one, or a curated game of a level that ships), public (listed, found by Quick play) or private (by its link), led by its host. Its members are up to the mission's players.max (more than a party: parties stay at MAX_PARTY), each on a team (versus) or on the one team (co-op); in a free-for-all every member plays on their own and the seats are made one a player when the run opens (seat i on team p<i+1>). A party's leader brings the party along, and a party stays together on one team: it joins a team with room for all of it, and switches team all together. A member of the party who is seated in a run, or in another lobby counting down or voting, is not taken out of it: they get the lobby as an invitation instead. A party with a member the host kicked is refused as a whole.
- Gathering, then the ready check (
ready: every team has its minimum): a member marks themselves ready; the lobby starts by itself once every member is ready, at once when its last place fills, or when its host starts it early (every team at its minimum). - Countdown (LOBBY_TIMING
.countdownMs): the run's room is opened at its start (RunRoutes.openRoster, its roster the members on their teams) so its world warms while the players wait. Before every run its game is read again (a game taken down or gone, or a mission no longer in it, closes the lobby, saying why). A member still seated in another open run is left out of this one (told why); when that leaves a team under its minimum the countdown stops. When no game server takes the run (503, the city busy, or no answer in LOBBY_TIMING.openTimeoutMs), the countdown says so and tries again everybusyRetryMsforbusyForMs, then gives up and goes back to gathering: the lobby is never lost. One opening at a time per lobby: a later countdown's waits for an earlier one's to finish, and a run opened for a countdown that is over (stopped, or its lobby closed) is abandoned at once (RunRoutes.abandon: counting for nothing, its room closed), as is a run whose opening answered after its time was out. Nobody joins during the countdown; a member who leaves once the room opened leaves their seat empty (the run plays on without them: a versus team left empty forfeits). - Running: when the countdown ends every seated member is sent their join URL in their feed (
run), and their page goes to the run's room. Members may join meanwhile, for the next run. A member who leaves the lobby while its run is live stays in the run: that is the room's business (their minute to come back, then their seat gone). - Results: when the run's result reaches the API (or the sweep finds it lost), the lobby shows each seat's part and the winner, and offers a vote (LOBBY_TIMING
.voteMs) on what comes next: the same mission, another mission of the game, a mission on another level whose players take the group (the playlist's), or Replay (the same mission and teams, counting down at once). A majority picks at once; once everyone voted without one, or when the time is out, the most votes win, the host's vote breaking a tie (no vote at all: the same mission). Members gone from the page (not seen forawayMs, by the lobby nor the feed's presence) have no say and are dropped (toldremoved); the game picked is read again (gone: the lobby stays on its own, or closes when that is gone too), then the lobby moves to the mission picked (its teams re-made when the teams differ) and gathers again.
Playlists (playlists.ts): a lobby opened from one (Quick play, or the Play together screen) remembers it. Its vote's offers come from it: the game's next mission of the playlist, and the playlist's next mission on another level (its rotation's); and a vote nobody took part in moves it on along the playlist (to that other level's mission, else the game's next) rather than playing the same mission again.
Never stranded: a playlist's lobby that moved on to another mission and whose run there cannot be opened (the city busy until it gave up, a refusal, the mission gone meanwhile) goes back to the mission it came from and its last results, saying why, with a vote again (its offers never the mission that failed): its members vote, replay, or leave, as after any run.
Seats filled under way (joinInProgress): a lobby's run whose room says it has seats open takes the players Quick play places in it (LobbyHub.fillRun): the group seated on the team with seats for all of it that has the fewest players, then the least skill of the members on it; each seat added to the run (fillSeat) and its player made a member of the lobby, sent their join URL (run), the members told of them; the members whose seats are gone for good from the run leave the lobby then, told so (removed).
When the host leaves, the lead passes to the member who came first after them; a member who leaves frees their place; an empty lobby closes. The host may kick a member, who may not come back to that lobby. A lobby never opens a run outside its mission's players and teams (runs.ts checks the roster again). Changes are pushed to the members' feeds (lobby), gathered for LOBBY_TIMING.coalesceMs into one push carrying the lobby as each member sees it, so their pages follow without polling and a flurry of ready marks reaches nobody more than a few times a second. A member's invitations to friends are one per friend and lobby, at most LOBBY_LIMITS.invites a minute, and hold LOBBY_LIMITS.inviteMs. A reader who is not a member sees names and counts, never accounts nor the run.
What Respect plugs in (respect.ts): a lobby keeps a pool (Quick play groups players of one pool together: the conduct pool), the admit hook a lobby asks before a member joins or a run starts (the Respect gates, LobbyDeps.admit; the accounts looked at first, warm, so it is answered at once), each member's tier on the lobby's card, and the teams balanced by the members' hidden skill (skillOf): a group joining goes on the team with room that has the fewest members, then (of those) whose members' ratings add up to the least, then the first in the mission's order, and teams made again place the largest parties first, the strongest first among parties of a size, each on such a team. Without skillOf the teams are made by their numbers alone, as before. A countdown never starts on teams whose sizes differ more than remaking them would leave them (the members picked their sides that way): they are made again first.