five-on-it / apps/api/src/runs
apps/api/src/runs
Runs (TECHNICAL_SPECS §9.5, §9.6): one play of one mission in a room of its own, for a roster of seats. A run is opened from a game's page (a published game, or a curated game of a level that ships, whose game is bundled with the game servers: the room is opened on the level, bundled), for the caller alone (a solo mission) or for the caller's party (a co-op or versus one, its leader asking; refused, saying why, outside the mission's players), or by a creator's playtest of one mission of a draft (POST /drafts/:id/playtest with a mission: its minimums of players relaxed, the teams' too, marked a playtest, counting for nothing), or by a lobby (lobbies.ts) for its members on the teams they picked (RunRoutes.openRoster: an explicit roster, each seat's team its own, checked against the mission's players and teams; a free-for-all's seat i on team p<i+1>). The route's party fills a versus mission's teams in turn (assignTeams: the leader on the first, each next player on the team with the fewest so far that has room, every team's minimum met or the run refused, saying which), a free-for-all's one a player (p1, p2…), and opens its run through the same roster. Every seat gets a join URL whose token names the run, the seat and the team; the party's other members get theirs in their feed (room), as a party follows its leader into a room (a lobby tells its members theirs itself, when its countdown ends). When every game server has all the rooms it takes, the run is refused (503) with the city busy, to be tried again. Whoever opened a run hears when it ends (RunContext.onEnded: its result taken, or swept lost): a lobby shows its results.
A run whose mission takes players under way (joinInProgress: untilCheckpoint until its first checkpoint, or always) has open seats while it is live, which its room's status says (RoomStatus.open); a player Quick play places in one is seated by RunRoutes.fill, one seat of a run at a time: the run's gate and its lobby's kicks asked (RunContext.admitLate, whoever calls it), the seat reserved first on the run's roster in the store (Store.addRunPlayer, marked late, the store's next index), then asked of the run's server (Fleet.addSeat, five seconds at most) with the player's Respect tier; a refusal of the server's (a 4xx) takes the reservation back (Store.removeRunPlayer), while no answer keeps it (the server may have the seat: one whose player never comes is a seat with no span, which earns and costs nothing), so the server never holds a seat the platform lacks. The result is matched to the roster by reconciliation (reconcileResult): the seats the run was opened with exactly, the late ones by their account (one the server reports but the roster lacks ignored and logged, never paid; one the roster has but the result lacks a seat that never came in), each with its span. A late seat's account is seated in the run only while its room says it holds the seat (RoomStatus.late).
An account has a seat in one open run at most (RunRoutes.seatOf): a run is refused (409, saying which run) while the caller or a member of their party sits in one still open. Its room's seat is theirs until the run ends, or until the room's status says the seat is gone for good (RoomStatus.gone: a minute after they left it, or not come before the run went live), or the room is no longer on its server: from then on the account is free for another run while that one plays on. An owner has one open run at most (they sit in every run they open). A run nobody will play (a lobby's that moved on before its countdown ended) is abandoned (RunRoutes.abandon: ended abandoned with no result, counting for nothing, its room closed on its server). A run spends one of the caller's rooms of the day, given back when no game server opens its room. The game servers cap their own rooms (their players and their rooms, capacity.ts): the fleet passes a server at its cap by.
The game server reports how the run ended (POST /runs/:id/result) with a token the API made for that run alone and keeps only as its SHA-256; the same result sent again is taken once (200), a different one refused (409), and its seats must be the run's roster. The sweep marks an open run lost (it counts for nothing) only when its room is out of sight: never while its server lists it, whatever the time; at once once its server answered without it on two sweeps in a row; and, its server not answering, once past its lostAfter: its mission's attempts (attemptSeconds each, or UNTIMED_ATTEMPT_S when a step has no clock, times its retries and one) and RUN_MARGIN_S.
| Route | Who | What |
|---|---|---|
POST /games/:id/missions/:mission/runs | player | Opens a run of a mission (the caller's, or their party's): the run, its room, the caller's join URL. |
GET /runs/:id | its players, the moderators | The run: its state, its roster, its result once in. |
POST /runs/:id/rejoin | its players | A fresh join URL to the caller's seat while the run is open. |
POST /runs/:id/result | its game server, Bearer <the run's token> | How the run ended (@foi/protocol's RunResult). |