Skip to content

apps/server ​

The authoritative game server. One process runs several rooms (games being played), ticked by one fixed loop: the default room (the level FOI_LEVEL names) and the rooms the platform API opens.

FileWhat
game-server.tsGameServer: the world built from a Level, one session per client (seated at the first spawn point no connected player holds; the welcome carries the level, spliced in from the JSON made as the room opened, or a big level's hash to a client that asked: a join is handled on the loop between two steps and costs it a few milliseconds, the first player's into a warmed room as any other's, join.integration.test.ts), its inputs (input-queue.ts), snapshots every third tick (the ordered deltas, or over a transport that loses frames (Transport.lossy) or for a client that asks (ACKED_REQUEST, its own simulated loss) the acked ones, @foi/protocol's AckedEncoder, onto the frame the client acknowledged in its input frames; a frame over 16 kB on the reliable channel); the AI world beside the simulation (policeSystem, then aiSystem, every tick), the level's parked cars (its vehicle spawners, run every tick before the step), the cars' key tags to every player (keys text frames), the chasers the options ask for, the clients' debug commands (a chaser behind the player, tuned; clear; a Wanted level) and the chaser readout while debug commands are on (the police left out); each player's Wanted level (wanted text frames: the stars, the Heat, whether the police see them, the fine of an arrest), the police on by the game's rules unless the police option says; the game's rules in the simulation and the welcome (who may be carjacked); the room's items (the common ones and every mission's, in the welcome), the level's crates, lockers and items on the ground laid out with the world and on a level reset (the doors shut and locked as the level has them), each player's inventory text frame (what they hold, the container they have open) when it changes, the item events kept off the wire; a leaving player's car kept while somebody sits in it, gone once empty; how many ambient cars drive the level (the traffic option, else the game's rules.traffic, else MAIN_TRAFFIC, 32, on a main-* level and none on a test-* one), cleared on a level reset, the readout carrying them too; stealth: the guards and civilians of the level's NPC spawners put in the AI world with the simulation and again on a level reset (the cameras back on and whole, the book of stealth emptied), each sentry's alert phase in the snapshots, each player's stealth text frame (seen or hidden, the room's alert) when it changes where the room has sentries or cameras, every sentry and camera in the readout; each player's snapshot cut to its area of interest (below). No sockets. The city: spawn points by role (spawnPointsOf: the players', the hospitals, the police's, the traffic's, the guards'), the room clock (rules.clock, a clock frame to each player when it changes) and the junctions switched by hand (a signals frame), the game's restricted zones (the AI's, and the welcome's with the race gates), the level's shops and role NPCs, gates and barriers (gateSystem before each step), every player's Cash (the inventory frame; fines taken from it).
input-queue.tsInputQueue: a client's inputs waiting for their ticks, from the single inputs (kind 1) and the frames of the last few (kind 7, oldest first): put in their order by number (an input older than the newest queued but not yet run is not late), each taken once (a press sent in four frames presses once), a tick whose input has not come run on a guess (the last input held without its presses), a late input's presses carried onto the next tick once (the ticks whose inputs were taken remembered for the last 64), the oldest of a full queue given up
mission-host.tsMissionHost: a game's missions in the room (@foi/missions): every sixth tick the facts from the simulation (players, their health, their Wanted level and what they carry, mission vehicles with their health, drivers and locks and whether they were carjacked, picked, resprayed or stored since, the garages bound to garage actors and who stands in them, the members of the mission groups, the missions' items on the ground, containers and doors, the item events since the last step), the interpreter stepped, its commands carried out (mission vehicles spawned at their places, whose and locked as they say, locked and unlocked, despawned or let go; garages bound; groups of enemies spawned as chasers of the AI world after the mission's player, within 24 a room, and despawned with their mission; a player's Wanted level set; items laid out, containers spawned, the level's doors bound, locked and unlocked, items given (what does not fit at the player's feet) and taken; people of the contact role and terminals put at their places, keypad doors locked and opened to their code; guards and civilians spawned as sentries and removed with their mission, the level's cameras bound and switched, the alarm raised or called off; the facts of stealth: each NPC actor's phase and status, each camera actor's state and spots, the compound's alert, each player's kills, alarms and sightings), each player's tracker, notices and phone sent as text frames; the players' phone commands and the interaction key at a contact, a terminal or a keypad reach the interpreter as inputs. The city's commands: the clock set, a junction switched, a race started for its rivals, the drivers, targets and VIPs spawned (out of the guards' spawn points for a group), shops, rewards paid into Cash; the facts of the roles, the clock and each player's Cash. In a run's room: owner 0, rebind, the enemies after the nearest live seat, hits on them credited (sim.events).
room-social.tsRoomSocial: a room's roster (each player's name, account and party), its channel (lines, quick-chat, emotes; a public room's lines checked by the platform's moderation first, a hidden one only its sender's; eight lines in ten seconds) and the votes to kick (a majority of the room; the account kept out; who voted told to the room, voted, a run's result saying so). See Social.
rooms.tsRoomHost: a GameServer per room, warmed as it opens (GameServer.warmUp, before anybody can join: the physics' first step and the traffic's and the crowd's coming around the players' first spawn point, with a stand-in for the players there, until the streets are full and the steps quiet for a snapshot interval's worth of ticks, on a whole half second of ticks so every periodic job keeps its phase, at most ten seconds of ticks; warmed in 150 ticks, 1352 ms on Riverbend with its game, 30 to 60 ticks on a test level; the room clock then starts at the game's time, and the room's tick never starts at zero), every room with a player in it ticked on the one loop (an empty room's world waits, costing nothing), players refused when a room is full, an empty room closed after FOI_ROOM_IDLE_S; openRoomFromRequest: the API's room request, its artifact fetched and its game checked against its level. A request may name a level that ships (bundled) and a run: its room takes its roster alone (4403), a quarter of the city's life, closes 15 s after the result.
run-host.tsRunHost: a run's room (TECHNICAL_SPECS §9.5): its phase (gathering, a countdown holding every car, live, ended), each seat's connection (a minute to come back; the seats gone for good told to the missions, a versus team left empty forfeiting), what it measures (connected, idle, harm to teammates and the most of it one teammate took, a seat voted out and the yes votes from outside its party), the result reported with the run's token (a versus run's winner, forfeit and race places, each seat's sabotage as the missions count it), the frame its players are sent (the teams' scores, who is out, each seat's client and Respect tier), the room's time to close. A run of teams starts each team at its spawn; a player out of the run is taken out of the world, their input held and their area of interest on the teammate they watch (game-server.ts). The accounts of the seats gone for good (goneAccounts), which the room's status lists (RoomStatus.gone) so the platform frees them for another run. A run that takes players under way (joinInProgress) lists its open seats (openSeats, RoomStatus.open) and takes a seat the platform adds for a late joiner (addSeat, POST /rooms/:id/seats), who comes in at their team's spawn or the run's last checkpoint (game-server.ts); each seat's span (joinedS, leftS) in the result; a seat's minute to come back from FOI_RUN_GRACE_S (runTimingOf).
debug-runs.tsRuns without the platform on a level server whose debug commands are on: /?run=<mission>&team=<team> (&tag=, &roster=) takes a seat in a run of the default room's game, the first connection opening its room; its result kept for GET /runs/<id> (the core e2e of versus).
level-files.tsThe levels of the open rooms served by their hash (GET /levels/<hash>.json: immutable, gzipped, open to any page) and what a welcome says of its level: inline, or a big one's hash and length to a client that connected with ?levels=hash; a level's JSON and hash made once, as its room opens, never on a join
level-rooms.tsThe rooms of the levels that ship, opened when a connection asks (/game?level=main-*, the main menu): its bundled game, the default room's switches; a test level refused
room-options.tsThe switches the environment sets on the rooms the process opens itself (pedestrians, chasers, debug, police, traffic)
http.tsThe endpoints (/health with the default room at the top level and every room under rooms, GET /rooms, POST and DELETE /rooms and POST /rooms/<id>/seats with the room secret, compared in constant time; an id that does not decode answers 400) and the WebSocket routing (the root joins the default room, /rooms/<id>?token=… joins a room once its join token checks out; refusals close with 4401, 4404 or 4409).
loop.tsstartFixedLoop: the 60 Hz driver with an accumulator, catch-up limit and step-time metrics; behind, one step per callback, the next on setImmediate so the inputs that came meanwhile are read first; injectable clock.
metrics.tsThe Prometheus metrics at /metrics (prom-client): the loop’s steps, every room’s players, snapshots, AI and bytes, the process’s CPU and memory.
capacity.tsThe fleet's admission: no new room while draining, past FOI_CAPACITY players (128), while the loop's p95 step is past 8 ms with players on it, or past a cap of rooms it was given (FOI_MAX_ROOMS, none by default) (POST /rooms 503 saying why, a level's room to open refused); runs apart, at most FOI_MAX_RUNS (2, measured in Performance) open and on their way, a run's place held before its game is fetched (RoomHost.reserve); a draining server refuses every connection (restarting).
fleet-client.tsThe heartbeat to the platform's fleet registry every 5 s (FOI_FLEET_URL, FOI_FLEET_SECRET, FOI_SERVER_ID, FOI_PUBLIC_URL, FOI_INTERNAL_URL; {port} filled in): who the server is, its version, players, capacity, p95 step, rooms and whether it drains.
supervisor.tsFOI_PROCESSES=<n>: n processes on consecutive ports from PORT, each started again with a backoff when it stops, SIGTERM passed on to each (each drains).
nav-cache.tsThe levels’ navigation built in a worker thread, once per level object, so a room opening never stalls the others (RoomHost.openWhenReady).
ws-transport.tsAdapts a ws socket to the Transport interface.
rtc-transport.tswithRtc(connection, host): a client's WebSocket connection whose unreliable frames go over its WebRTC data channel while one is open (lossy, onLossyChange), the signalling kept from the game, and every way back to the WebSocket (the peer failing, the client's close, 30 binary frames in a row over the WebSocket, the debug command rtcDrop, the socket closing). See Netcode.
rtc-host.tsRtcHost: the RTC worker started on the first offer, the peers relayed to it and back in one batch per event-loop turn, the worker started again after a stop with a backoff (its peers fall back), the numbers for /health and the metrics.
rtc-worker.tsThe RTC worker thread: werift's peer connections (no STUN, the configured ports and addresses, the public address announced), the DTLS record count moved past the handshake's (werift 0.24.4's replay bug), and the batches to and from the game thread. Bundled as rtc-worker.mjs beside server.mjs.
rtc-peers.tsRtcPeers: the worker's peers on any peer connection (werift's, or the tests' fakes): the caps on peers and handshakes, one answer at a time, the 8 s timeout, the candidates held on either side of the answer, the backlog; announceCandidate and announceSdp.
rtc-wire.tsWhat the game thread and the worker say to each other: the control messages and the turn's frames packed into one moved buffer (Outbox, unpackFrames).
rtc-settings.tsrtcSettings: FOI_RTC, FOI_RTC_PORTS, FOI_RTC_ADDRESSES, FOI_RTC_PUBLIC_IP, FOI_RTC_MAX_PEERS; slicePorts, the slice of the range each supervised process takes.
level-source.tsresolveGame: FOI_GAME as a game file, else the bundled game of the level, checked against the level and the vehicle classes. resolveLevel: FOI_LEVEL as a bundled level name (test-<words> or main-<words>; by default main-riverbend by name, else the first main-* in name order, else the first test-*) or the path of a .json level file, validated.
index.tsThe process: HTTP /health (with the level, the road graph and the AI counts and step time, and the ticks by the physics steps they took, substepTicks), WebSocket upgrade, pino logging, graceful shutdown. PORT (7777), LOG_LEVEL, FOI_LEVEL, FOI_GAME, FOI_DEFAULT_ROOM, FOI_ROOM_SECRET, FOI_JOIN (token: every connection with a join token, the default room's and the levels' too, checked before a level's room opens), FOI_ROOM_IDLE_S, FOI_PEDESTRIANS (the crowd kept around the players on the level's pavements: 40 on a main-* level, 24 on a test-* one by default), FOI_CHASERS_FOOT, FOI_CHASERS_CAR, FOI_CHASER_PROFILE, FOI_CHASER_CLASS, FOI_DEBUG, FOI_POLICE, FOI_TRAFFIC (ambient cars in the default room: FOI_TRAFFIC=0 none, FOI_TRAFFIC=48 a busy town; by default the game's rules.traffic, else 32 on a main-* level and none on a test-* one).

Area of interest. Every player's snapshot carries every player's character and whatever a player sits in (with its trailer), every enemy NPC (the chasers and a mission's enemies: few, and the minimap shows them wherever they are) and every light piece lying loose (the client plants a piece again once its snapshots stop listing it); everything else (ambient cars and their drivers, pedestrians, empty cars, projectiles) only within AOI_RADIUS, 250 m, of the player's character or the car it sits in. It is cheap because the client already handles entities coming and going: the mirror drops the twin of an entity that leaves its snapshots and makes a new one when it comes back, and the remotes draw it from the new snapshots on. The minimap therefore shows pedestrians and traffic within 250 m only. The traffic lights' colours never travel: they are a fixed plan the client computes from the level and the tick (see packages/ai/README.md).

Step time. /health reports the loop's averageStepMs and maxStepMs and the AI's own ai.averageStepMs, with ai.traffic (cars wanted, driving, drivers on foot). On the first hillside (now test-curves), with 32 ambient cars and eight WebSocket bots driving around, averageStepMs read 4.6 ms (2.6 without the traffic) and ai.averageStepMs 0.56, against the 16.7 ms a 60 Hz tick has; see packages/ai/README.md.

Every setting, with its default and a sample: the root .env.example's game server section, and deploy/.env.example for the ones the Compose stack sets too (FOI_LEVEL, FOI_DRAIN_S, FOI_ROOM_SECRET, FOI_JOIN, the data channel's, FOI_PUBLIC_URL, FOI_FLEET_SECRET; FOI_FLEET_URL is in deploy/fleet/.env.fleet.example).

Run it alone with npm run -w apps/server dev (the data channel on: FOI_RTC=1). rtc.integration.test.ts opens a data channel between the real RTC worker and a werift client over a real ws server, carries frames both ways and falls back on the debug drop; rtc-transport.test.ts, rtc-peers.test.ts, rtc-host.test.ts, rtc-wire.test.ts and rtc-settings.test.ts cover the routing, every fall back, the caps and the timeouts with fakes. game-server.test.ts covers the core over the memory transport; server.integration.test.ts adds lagging bot clients that use the real client netcode; lossy.integration.test.ts puts them on the lossy memory link (10 % of the frames lost, some duplicated and held back, 150 ms) with the acked snapshots and their inputs four to a frame: the prediction within the same half metre, every press of E taken exactly once (after a hitch of the page too), never a keyframe asked for; input-queue.test.ts pins the queue's order, duplicates and late presses; mission-logic.integration.test.ts plays the missions of test-mission-logic on their level, the AI moving the enemies; mission-verbs.test.ts the verbs past v1 (an item handed to a person, a chase's target sent off, a light piece destroyed, a photo framed through the physics world, an arrest); items.test.ts lays a level's items out, moves them through the keys and plays a mission of items through; stealth.test.ts puts the guards of test-stealth in its room and plays its Clean Exit through, unseen and seen.