Skip to content

The netcode probe ​

What other cars and people do on a client's screen against what the server said they did, measured. No gate: an instrument, run by hand (npm run test:netcode -- …), with its own deterministic invariants under vitest (harness.test.ts on synthetic traces with the unit tests, stale-samples.integration.test.ts on a real test-traffic-junction server with the integration tests: every car within 1.3 times its authoritative speed in every tier, no sample that is no measurement, no figure sliding or walking on the spot, no teleport within 80 m, no freeze at a reappearance, the far tiers' spikes no more than one a turn and no larger than what one turn of a driver's emergency braking makes of a measurement step). Before 0.68 (commit f579be849) they were it.fails documenting the bursts, slides and stale fills the fix removed.

The pieces ​

FileWhat
record.tsA bot over a real WebSocket (a server it starts, or --url), joining as the web client does (the level asked for by its hash, tests/load/server.ts' joinUrl: a join that carried the level held the server's loop, and everyone's snapshots, for 140 ms on Riverbend), sent where asked (--at @x,z,bearing, on foot with --foot, or driving a road to and fro with --drive), every binary frame written as it came with its arrival time, /health and /metrics once a second. --delay/--jitter go into the trace's meta for the replay; --loss asks for the acked snapshots and loses frames while recording.
trace.tsThe trace (JSON lines) and the pure link impairments: a jittered delay that never reorders (TCP), a stall that holds a window and delivers it at once.
harness.tsThe replay: the client stack run over a trace exactly as game.ts runs it (ClientClock advanced by each frame's time before its frames are read, SnapshotReader, RemoteIntake, RemoteBuffer, frameTicks, the sample at renderTick less the tier's delay, anim-state's clip from the sample's speed), on even frames unless a hitch is asked for. Every frame's news of a remote kept (a measurement pushed, or a change without one); every frame's drawn pose and tick, speed, distance, flags and clip kept; a remote dropped ends its track, its id back a new one.
metrics.tsPure: bursts, freezes, jumps, spikes, moving-while-idle and sliding frames per tier, per kind and for the near band (the entity-frames drawn at the local time or on their way to it, and those within 10 m of the player), the ratio's distribution, the shares extrapolated/held/blended/predicted/blending, the predicted twins' corrections (how many, their sizes), the render tick's continuity, the snapshots' inter-arrival, the server's step, each event laid at a mechanism (prediction and prediction-correction among them, the browser's alone), the worst events with the buffer's samples and an excerpt; and the server's own accelerations per kind (accelerationStats: the speed change between two consecutive measurements of a remote over their time, from the measured samples alone, no netcode in it: the distribution, the shares over 3, 5, 8 and 12 m/s², the hard flips over 3 m/s² both ways in consecutive intervals, the hardest), what the eye reads as the other cars' "weird accelerations" when the drawing is faithful. The thresholds and why are at its top.
report.tsMarkdown and JSON per run; the table the command line prints.
browser.tsThe ground truth: the web client in headless Chromium reading window.__remotes() and window.__clock() every frame (each remote's blend toward its twin, weight, blending, and its twin's correction, correcting, correction, with the rest), beside a bot's trace on the same server, both analysed the same way. With --drive <road> the page's own car drives the road to and fro (a pure pursuit on the keys in the page) and the bot the same road 30 m behind, so cars pass the page at touching range.
impact.tsThe player's own car run into something at speed in the browser (a named scenario: the wall, the parked car, the hydrant, the junction's traffic, Riverbend's building, hydrant and car park; or a spot, a speed and a server's address), window.__impact() read every frame, and a timeline per run laid at the tick the prediction felt the crash: the drawn car's stop, the crash heard and shown, the server's word, the corrections and the offset, the camera, a light piece knocked loose and drawn, the predictor's agreements and rewinds, the server's physics steps. The judging is the e2e support's (tests/e2e/support/impact.ts), which E2E-642 to E2E-644, E2E-646 and E2E-647 assert on.
synthetic.tsTraces made through the protocol's encoder with the server's tiers, for the reproductions.
drive.tsThe scripted drive: a pure pursuit of a road's line, to and fro.

Running it ​

console
npm run test:netcode -- record --level main-riverbend --at @-0.9,3.9,0 --foot --seconds 120 --out reports/netcode/gate.jsonl
npm run test:netcode -- record --level main-riverbend --drive road-broadway --seconds 120 --out reports/netcode/drive.jsonl
npm run test:netcode -- record --level test-traffic-junction --traffic 16 --pedestrians 0 --at @-120,-120,45 --foot --seconds 90 --out reports/netcode/junction.jsonl
npm run test:netcode -- record … --delay 40 --jitter 20           # the link impaired at replay (seeded)
npm run test:netcode -- record … --loss 2                         # the acked snapshots, 2 % lost each way
npm run test:netcode -- record --url wss://… --no-health …        # a server whose /health is not public (the live platform): no wait, no server numbers
npm run test:netcode -- replay reports/netcode/gate.jsonl [--out-dir reports/netcode] [--plain] [--hitch 20000:100] [--stall 30000:300]
npm run test:netcode -- browser --level main-riverbend --at @-0.9,3.9,0 --foot --seconds 30 --port 7791 --web-port 5195
npm run test:netcode -- browser --level main-riverbend --drive road-broadway --seconds 60 --port 7791 --web-port 5195
npm run test:netcode -- impact --scenario wall --runs 3 --rtt 150 --port 7770 --web-port 5197   # wall, wall-60, wall-100, parked, hydrant, junction-north, rb-building, rb-hydrant…
npm run test:netcode -- impact --url wss://play.fiveonit.online/game --runs 2   # somebody else's server, the car driven on from its spawn

A server the recorder starts gets FOI_DEBUG=1 (the Go to is a debug command) and --traffic, --pedestrians, --police as FOI_*; the rest are the server's defaults (Riverbend's game: 40 cars, 40 people, police). The summary line ends with the vehicles' own accelerations (own |a| p99, the share over 5 m/s², the hard flips a minute): on the live platform at 0.68.1 (Riverbend, 90 s at the gate and driving Broadway) they read p50 / p90 / p99 / max 1.5 / 5.8 / 12.4 / 99.6 m/s², 12.6 % over 5, 61 hard flips a minute, with the netcode clean (the drivers' doing: a throttle that answered a speed error); the drivers that command an acceleration (0.69) read, recorded the same way on a local server of the same level, 1.3 / 2.6 / 3.8 / 52 at the gate and 1.4 / 2.7 / 4.4 / 53 driving Broadway, 0.6 to 0.7 % over 5 (emergencies within the driver's reserve of 8, and the few collisions: a bus clipping a kerb or a prop at a stop) and no hard flip. The reports go to reports/netcode/ (git-ignored) unless --out-dir says: a browser run writes <name>.page.md/.json (the page's own frames analysed), <name>.bot.md/.json (the bot's trace replayed), <name>.frames.json (the page's frames as read) and <name>.bot.jsonl (the trace). A browser run's frames carry the lead (the inputs unacknowledged: the ticks the local world ran ahead), reported with the render tick: a run with a lead over ten or a server step over 25 ms was disturbed (the machine busy, a stall), and is run again.

What it mirrors, and what it leaves out ​

The replay uses the product's own code for everything that decides what is drawn: the reader, the clock, the renderer's intake of a frame (RemoteIntake, @foi/net-client: the one rule of what a frame does to the remotes, written once for game.ts and the replay, so the instrument cannot drift from the game), the buffer with its tier's delay, the frame's ticks and the clip chooser. It leaves out the prediction world (a car near the player's is drawn where the local prediction has it; here everything is interpolated), the figures' one-shots and time in the air, and the frame's own cost. The browser run shows the renderer with all of it: its near band is where the prediction path shows, and an event there is laid at prediction (the remote on its way between the interpolated and the predicted timelines, or drawn at its twin) or at prediction-correction (drawn at its twin while a snapshot's correction of the twin decays in its drawing, SmoothingOffset), before the buffer's mechanisms. A remote drawn at the local time stands for a later moment than the tick its buffer drew (moment: the local world's present for one at its twin, between the two by its weight while it blends), and is judged against the server's speed at that moment, so a car pulling away drawn at the present is not a burst against its stale measurement. The blend between the timelines is bounded to a third again the remote's pace (PredictedRemotes, packages/net-client), so a prediction burst (over 1.5×) is a defect; an excess over that bound on a blending frame that the correction decaying in the drawing could add (its size times SMOOTHING_DECAY_PER_SECOND) is laid at the correction. A correction is what the twin's coasting guess (a car's) or its walk on as measured (a person's) missed of its driver's or its own next move, reported by its size.