Skip to content

five-on-it / apps/server/src/game-server

apps/server/src/game-server ​

The authoritative game server core, independent of sockets and timers: it owns one SimWorld, a session per connected client and the input queues. tick() is called at 60 Hz by index.ts; every third tick a snapshot goes to every client.

A client is a character. On join it gets a car and is seated in it as the driver; it can get out, walk, and enter any car with a free seat. Its inputs land on the character, which forwards them to the car it drives. On leave the character is removed and its car with it, unless someone else is still inside.

Clients only ever send inputs, one per tick of their clock, numbered by it: input n is the client's tick n, and the server runs exactly one of a client's ticks with each of its own, from its first input on (the InputQueue: inputs in their order, each taken once, a bounded queue). A client sends its last few inputs in every frame (one over a WebSocket, four over a transport that loses frames), with the newest snapshot tick it applied. When the input for the tick due has not arrived (a frame stall, a lag spike, a tick skipped by a client that fell behind, a frame lost), the server runs the tick on a guess, the last input held (minus the one-shot presses and the trigger: no shot is ever fired that the client did not ask for; a grenade with its pin out stays held, so a gap never throws it), and that tick is spent: the input meant for it, arriving later, is too late to steer. So the server never runs more of a client's ticks than time has passed, and a late burst never drives the car on twice, once on the guess and again on the inputs it stood for. When a burst overflows the queue, the oldest inputs are dropped. A late or dropped input's one-shot presses are handed on to the next tick the server runs, once, so no press is ever lost or doubled.

A client whose transport loses frames (Transport.lossy, a WebRTC data channel), or that asks for them (its own simulated loss, ACKED_REQUEST), is sent the acked snapshots (@foi/protocol's AckedEncoder: each frame onto the newest one it acknowledged); any other the ordered deltas. A switch between the two starts with a keyframe.

Text frames from a client are phone commands (packages/protocol: what the player did with their phone, or with a screen the world opened), which the missions check, and debug commands, honoured while debug commands are on (a test-* level, or FOI_DEBUG=1) or from a player whose join token carries debug (the platform's FOI_DEBUG_ACCOUNTS); a readout of every chaser goes twice a second to every client whose debug commands are honoured. One honoured in a run's room makes the run pay nothing.

The NPCs live in an AI world beside the simulation (packages/ai): aiSystem runs every tick before the step and writes their inputs; the server never moves them itself. Before it, the police (policeSystem) turn the last step's crimes into each player's Wanted level and the stars into police cars, and make their arrests; each player is sent their Wanted level when it changes (the Heat at most twice a second), with the fine when they have just been arrested. The game's rules say whether there are police (free roam has them).

The ambient traffic is the AI's too: the server only says how many cars (the traffic option, else the game's rules.traffic, else a few dozen on a main-* level and none on a test-* one), and the readout tells them while debug commands are on. So is the crowd: the server says how many pedestrians to keep around the players (the pedestrians option), the AI spawns them on the level's pavements with their roles and takes them off far away, the simulation's pedestrian system walks them (run here before the AI), and the readout tells them too.

Snapshots carry what is near each player (an area of interest of AOI_RADIUS): every player, whatever a player sits in, every enemy NPC and every light piece lying loose always, the rest (ambient cars, pedestrians, empty cars, projectiles) only within the radius; the client drops the twins of what leaves its snapshots and makes new ones for what comes back.

A server given a game (@foi/missions) runs its missions for every player: the MissionHost steps them after the physics, spawns their vehicles and their enemies (chasers of the AI world, after the mission's player) and sends each player their mission tracker, notices and phone as text frames; every character starts with a phone (the last slot of its inventory) unless the game's rules say otherwise (free roam, with no missions, has none). The game's rules go into the simulation (whether players hurt each other decides who may be carjacked) and into the welcome, for the clients' interaction prompt; free roam lets players hurt each other.

Players are who their join token says (an account, a name, a party; anyone who joined the default room is "player N"). The room's social side (room-social.ts) keeps the roster, relays the room's channel (through the platform's moderation in a public room), its quick-chat and emotes, and runs the votes to kick; a player voted out is disconnected and their account refused from then on. A party's members are numbered in the room and the simulation knows them (Character.party: never carjacked by each other, never hurt by each other under rivals), and they play their missions together. The game's safe zones and its time as a ghost after respawning go into the simulation and into the welcome.

Items (@foi/sim's items.ts): the room's items are the common ones and every mission's own (the welcome lists them); the level's crates, lockers and items on the ground are laid out with the world and again on a level reset (which shuts the doors and locks them as the level has them); each player is sent what they hold, the container they have open and the cars they keep in the garages, whenever it changes (checked with every snapshot).

Cars (@foi/sim's locks.ts, garages.ts, spawners.ts): a player's car at their spawn is theirs and open; the level's vehicle spawners park their cars with the world and again on a level reset, and put the next one in a bay whose car is gone (run here every tick, before the step: the clients never spawn anything); the garages' prices are the game's (rules.garages) over the level's, in the welcome; the tags the cars' keys answer to go to every player whenever they change (the keys frame), so the prompt knows a car its player carries the key of.

Stealth (@foi/ai's sentries and book): the level's NPC spawners put their guards and civilians in the room with the world (and again on a level reset, which also puts every camera back on and whole and empties the book); the snapshots carry each sentry's alert phase (for the "?" and "!" over its head); each player is sent whether a guard or a camera sees them and the room's alert when it changes (where the room has sentries or cameras); the readout tells every sentry and camera while debug commands are on.

The city (GAME_DESIGN §5.3–§5.7, §14): the level's spawn points (and the game's places) carry roles: the players come in at theirs, come back at the nearest hospital under respawn: hospital when there is one, the police cars and the ambient traffic come out of theirs (or the game's spawn areas), a mission's groups out of the guards'. The game's restricted zones go to the AI (the alert or the Heat on a player's entry), its safe zones are those its rules name and those of the safe kind, and the welcome lists its restricted zones and race gates for the minimap. The room clock starts at the game's time and pace (noon, an hour a minute, in free roam); every player is sent it as they join and whenever a mission (or the debug command) changes it, and the signals a mission switched by hand likewise. The level's NPC spawners put drivers, targets and VIPs in the room too, and any of them on a schedule; its shops' counters sell at the game's prices. The gates and barriers open by themselves for a car whose driver may pass (run here every tick, before the step). Each player's Cash goes out with their inventory; an arrest's fine is taken from it.

A run's room (run option: TECHNICAL_SPECS §9.5) plays one mission for its roster (the RunHost: its phase, its seats, its result): a player sits in the seat their join token's account has on the roster (another account is refused before it gets here, rooms.ts), and is numbered in the simulation by their team (Character.party: the team's index in the mission's teams, plus one), so the game's rivals rule means no friendly fire (a run whose mission asks for none plays rivals even in a game of on; one whose mission allows it plays on). Until the run goes live every player's input is held (the car braked where it stands); the missions step only while it is live; a player who drops keeps their seat a minute (the missions let them go when it runs out), and one who comes back takes it again. Nobody resets a run's level. Each player is sent the run (run frames) when it changes, and the hits between teammates count against the hitter. In a run of teams (versus), a team's players start at their team's spawn (its zone's place, spread a car's width apart, the first free spot); a seat gone for good is told to the missions (a team left with nobody forfeits); a player out of the run (the missions took their body out of the world) has their input held and is sent what is round the teammate they watch (a spectate frame says whom: their area of interest is centred there; anybody still in it, once their side has nobody left), and a caught player not out is sent back as a team's players start, at the wheel of a fresh car at its spawn. A run's player who drops leaves their character and car in the world where they were for their minute, input held (hit, caught, put down like anyone's: a stopped car beside a chaser is caught), and one who comes back within it takes that same character, car, pose and inventory again; only a seat gone for good is taken out of the world. Co-op runs too. A run that takes players under way gets seats added by the platform (GameServer.addSeat); their players come in where the run stands: in versus at their team's spawn, in co-op at its last checkpoint (where they come back after a death too), or at the start when none was saved, the tracker the run's own.

Classes ​

Interfaces ​

Type Aliases ​

Variables ​

Functions ​