Skip to content

Host-authoritative multiplayer and cross-platform determinism (0.4.0) - #5

Merged
lowlevelmetal merged 148 commits into
mainfrom
integration
Oct 10, 2026
Merged

lowlevelmetal merged 148 commits into
mainfrom
integration

Conversation

@lowlevelmetal

Copy link
Copy Markdown
Owner

Multiplayer rebuilt around the host (the maintainer's decision: "the server needs to be the authority of everything"), plus cross-platform determinism. Version 0.4.0, network protocol 18. The records are docs/review/multiplayer-desync-*.md and docs/review/multiplayer-determinism.md; the design is in docs/multiplayer.md.

Host authority

  • Players' cars:
    • Clients upload inputs, and the host simulates every car.
    • Clients predict their own car and the 3 nearest cars, using inputs the host already holds for them, then reconcile by rewind and replay at the fixed step.
    • States go out at up to 60 Hz.
    • Collisions between players happen once, on the host.
  • Damage, resets, water and falls: decided by the host; client resets are validated by ResetRules.
  • Shared traffic and police:
    • Presented at the client car's time.
    • Police and knocked cars near a client are simulated on the client from full states.
    • Hit reports are removed.
  • Props:
    • Host-run and replicated, with priority to those near each client.
    • Knocks by the client's car, near cars and companions are predicted.
    • Pieces near the client's car are sent in full.
  • Rules:
    • Checkpoints, laps, finishes and standings come from a host referee.
    • Cops and Robbers gold and limits are decided on the host.
    • Clients predict and the host confirms.

Determinism

  • The simulation's own libm (sin, cos, atan2, exp, log, pow, hypot) and no float contraction on any target, so Linux and Windows simulate the same bits.
  • A new CI determinism test hashes a fixed scenario. The MinGW job runs it under Wine, and this PR's Windows MSVC job is its first MSVC run.
  • The opponent sweep baseline moves to 515/517 finished and 507/517 across the line: correctly rounded sin and cos change one racer.

Measured

Through netprobe relay with internet-like latency and loss:

  • Other players' cars: about 0.5 m → 2-19 cm median.
  • Traffic near a client: 2.6 m → 3 cm median.
  • Props: 0 knocks on one machine only, resting within 3 mm of the host's.
  • Rules: every checkpoint, finish time and gold event equals the host's measure.

Testing

  • Full suite: 1050/1050 with game data and without, and under -D_GLIBCXX_ASSERTIONS.
  • Sweep: 515/517 finished, 507/517 across the line.
  • Two-instance runs on every round.

🤖 Generated with Claude Code

lowlevelmetal and others added 30 commits October 9, 2026 12:43
With OPENMM2_NET_TRACE set, the host writes each shared car near a player
at the session time its state belongs to (the AI's last step for a car on
its rail, the physics step's for a police car or a knocked one), and every
knock and refused hit report; a client writes the received cars where its
car meets them each frame, with the session time its own car reaches in
that frame's steps, and the hits it reports. Comparing the files measures
how far each client's traffic is from the host's at the same moment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…vergence

OPENMM2_NET_TRACE gains three opt-in lines: D for every player's car as
the frame draws it (the machine's own car marked), K for a collision
between two players' cars in this machine's simulation, and C for a
correction of a predicted car. netprobe syncreport reads one trace per
machine and prints how far apart the machines show each car at the same
wall-clock moment, the frames in which a car moves further than its
velocity explains, which collisions between players each machine had and
how far apart every machine drew the two cars at that moment, and the
corrections.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
For the host-authoritative network cars: CarSim and Trailer save and
restore everything a sample changes; World::replaySample runs one sample of
a car (and its trailer) alone, against the city, the instances around it and
the other movers held still, so a client can run its own car's samples again
after the host corrected an earlier one without moving anything else; World
gets hooks around every sample for the players' per-sample inputs; and a car
can draw its wheels' bump numbers from its own stream, so the host and the
car's own machine simulate it alike whatever else each one simulates (MM2
shares one rand() across the game). Single player sets none of these.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… rails

A client drew the host's traffic and police a trip and a playout delay in
the past while its own car was in the present, and its car met them there:
measured through a 60 ms (+-20) relay, a moving traffic car was 2.6 m
(median, 3.7 m at the 90th percentile) from where the host had it at the
same session time, 4.0 m over 150 ms. The host also stamped its messages
with the frame's time while the cars were at the AI's last 30 Hz step,
putting every car another 17 ms behind and making them surge.

Now the host stamps a message with the AI step's time (the police and the
knocked cars moved to it along their velocity) and sends, for a car on its
rail, its acceleration and the heading's turn per metre measured from the
AI's last two steps (protocol 5; 1 bit when both are 0). The client shows
every car at the time its own car reaches in the frame's steps: a rail car
predicted along an arc at its speed and acceleration (stopping at 0), a
police car or a knocked car along its velocity turning with its yaw rate.
Its collisions take the newest prediction; the drawing blends a newer
message's correction away over 100 ms (above 4 m it jumps). The same
relay: 0.02 m median, 0.06 m at the 90th percentile; 150 ms: 0.03 m and
0.14 m. MM2's network cruise has no traffic; MM2 predicts its network
cars forward from the last packet (mmNetObject::PositionUpdate).

OPENMM2_DEBUG_TRAFFIC_LEAD_MS shows the cars that much later, to measure
the prediction over the longer horizon a host-simulated car will need.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Protocol 5 for the host-authoritative cars: PlayerInput (client to host,
unreliable) carries every sample's controls the host has not acknowledged,
numbered and repeated until acknowledged (a frame like the one before it is
one bit), with the resets the client's rules asked for; CarStates (host to
each client, unreliable) carries the last input the host applied to that
client's car, how many of its inputs were waiting, the car's state after it
at full precision, and every other player's car as a snapshot. Both are read
within ranges (no wrapping sequence, positions in the city, a rotation
matrix, bounded values), only during a race, and a joiner's inputs within a
budget of 90 messages a second.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2 sends nothing about props: mmGameMulti and mmNetObject carry the
cars, and every machine knocks its own props with its own simulation of
each car. OpenMM2 0.3 did the same, except that a network car is a
kinematic body there, which can never break a prop (dgImpact::CalcImpact
could not stop it), so every prop a player knocked stood on every other
screen, and the host's traffic knocked props on the host only.

The host now owns them (the maintainer's decision: the host is the
authority for everything):

- PropState (protocol 5): every occupied slot of the host's ring of
  knocked-over props (dgBangerManager), about 20 times a second while
  any moves, every 0.5 s otherwise; complete, so a slot left out is
  empty. A placed prop is named by its index (every machine places the
  same props in the same order; a checksum travels with each message),
  a piece by its prop and BREAKnn part, a thrown car part by its car and
  part.
- PropKnocks event: the placed props the host broke loose, reliable, and
  all of them so far to a machine that has just loaded the race.
- A client shows the host's ring in mirror slots, interpolated a playout
  delay in the past, and breaks a prop when it shows the host's knock.
  Only its own car may touch its props (phys::Instance::acceptsContact):
  what its car knocks moves at once and hands over to the host's piece
  when both rest; a knock the host never makes is undone after 2 s.
- A moving kinematic body breaks a banger as a body of its mass would
  and gives it all of the relative velocity, so the host's copies of the
  other players' cars knock props until they become host-simulated cars.

OPENMM2_NETPROPS=local keeps 0.3's behaviour for comparison, and
OPENMM2_DEBUG_NETPROPS=<file> traces each machine's props for
`netprobe propdiff`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It reads a host's and a client's OPENMM2_NET_TRACE files and prints how far
the client's traffic and police were from the host's at the session time
of the client's car, the cars near the client one machine had and the
other did not, the police targets, the hit reports and knocks, and the cars
the client knocked loose itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sync

A piece the host is simulating was a ghost on a client: its car drove
through a flying cone that hit the host's copy of it. A client's mirror
of a host piece is now an object its car can touch at any time; the
active that takes it over starts from the host's motion
(dgBangerActive::Attach starts a placed or resting prop at rest), and
the piece stays where the client left it until the host's has moved
and come to rest too. Creeping props (a meter sliding down a hill that
phSleep never stops) go out every 200 ms instead of 50.

OPENMM2_DEBUG_FOCUS=prop:<index> frames a placed prop, or its first
piece once knocked, on every machine alike.

Tests: the messages and their limits (test_prop_state.cpp, the fuzz
corpus and decoders), and a host and a client in one process through a
lossy link (test_prop_sync.cpp): the host's knocks and pieces on the
client, a predicted knock handed over and one undone, other cars
passing through a client's props, moving kinematic bodies breaking
props, loss and reordering, the catch-up, thrown car parts, refused
input, and on retail data the same placement on every machine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he ground they cover

A client's car met the received traffic as kinematic instances of infinite
mass: it bounced off a car as off a moving wall, while on the host the same
contact gives the rail car a body and the two cars push each other
(aiVehicleInstance::AttachEntity, aiVehicleActive); the car itself flew off
only a round trip later, when the host's knock came back.

TrafficBodies now takes its traffic through an interface (the AI's for the
host and single player, unchanged), and a shared-traffic client gives it
the received cars on their rails (game::NetTrafficCars). The client's own
car hitting one attaches a body there as on the host, so the collision
runs the host's physics; only that car knocks cars loose (the police and
the other players' cars reach the client in the host's messages). The car
stays the client's until a host message shows it off its rail too, or
still on it half a round trip and 150 ms after the hit (the host did not
knock it); the drawing then blends from the local body to the host's car.
The cars off their rails on the host stay kinematic instances.

The host's trace showed rail cars moving over the ground at 70-80 % of
their AI speed in turns (ai::Traffic moves a car along its curves by their
parameter) and not at all when held at the end of a lane at speed. The
host now sends a rail car's speed over the ground when it differs (12 more
bits), and the client predicts its position with it while its velocity
(what its body takes when hit) stays the AI's.

TrafficClient::poseAt gives a car's physics pose at any time, for a client
that replays its own car's steps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once a wreck standing still on the host was shared with the 6.8 m/s its
body's velocity still said for 40 s, and the client, predicting it to the
present, showed it 1.2 m ahead all that time. A body that has moved less
than 5 cm in 100 ms is now shared without velocity or spin, whatever its
velocity says.

mm2tool nettrace also gives the error at the time the client showed each
car (for OPENMM2_DEBUG_TRAFFIC_LEAD_MS).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A moving kinematic body broke every banger it hit, so with
OPENMM2_NETPROPS=local each machine's kinematic copies of the others'
cars knocked props too, which OpenMM2 0.3 never did. The body now has to
be allowed to (Body::kinematicBreaksBangers): the host sets it on its
copies of the other players' cars, which knock the props for everyone;
everywhere else a banger holds against a kinematic body as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The error of each car the client's car hit, over the second after the
hit: how well the client's view of a collision matches the host's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At traffic density 1 the cars within 200 m of a client did not all fit in
a message: it held at most 96, and the farthest ones were cut. 12 % of the
host's cars within 200 m of the client were missing there, and cars at the
cut came and went (45 times in 100 s, at 170-195 m) as the order shifted.

A message now holds up to 160 cars, a known rail car beyond 160 m gets its
state every fourth message (the client predicts it in between), and when
the cars still do not all fit, the ones the client has count as 25 m
nearer, so the car at the edge of what fits stays until a new one is
clearly nearer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With more cars near a client than fit, the cars at the edge came and went
nearly 2000 times in 100 s at traffic density 1: whether a far car's turn
for its state fell in a message changed its size from 13 bits to 130, and
with it which cars at the edge still fit; a car left out was gone for the
client, and coming back new it needed its state, pushing out others.

The host now chooses the cars by their usual cost (a far known car's state
every second or fourth message, its id in between), then writes them; a
far car whose turn it is goes without its state when it would not fit
with it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host's copy of another player's car broke a banger and gave it all
of the relative velocity, as an infinitely heavy body: a parked car hit
by a client's car flew off at the car's full speed on the host (and so
on every screen), about twice what the client's own car gave it. It now
meets the banger exactly as dgImpact::CalcImpact meets a car of its mass
(the car's phInertialCS): the same break test and the same share of the
motion; only the car's own response is left out, which its owner's
machine simulates. Within 5% of a simulated car's piece in the test.

The props trace lists every placed prop once (P lines), for finding
props and parked cars to drive into.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A car ploughs through 60 props on the host: every knock reaches the
client, the largest message is 425 bytes and the crash costs about 5 KB/s
for a few seconds; the ring of 40 knocked-over props (dgBangerManager)
bounds what any message can carry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
game/net/PlayerCars: NetCarDriver applies one sample's input to a car the
same way on every machine (the gearbox keys, the gold's mass and throttle
cap, the finish brake, the grid hold, mmGame::UpdateSteeringBrakes, Cops
and Robbers' regeneration) and carries out the resets a client's rules ask
for; HostInputQueue takes a client's numbered inputs, starts once a few are
in hand, applies one per sample, repeats a missing one for a quarter of a
second and then lets the car coast, and drops what comes too late or too
far ahead; CarPrediction keeps a client's car's inputs and states, sends
what the host has not acknowledged, and when the host's state for a sample
differs puts the car there and runs the later samples again on their own;
CorrectionBlend draws what a correction moved away over a few frames. The
input frame gains Cops and Robbers' regeneration flag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The maintainer's decision: the host is the authority for every player's
car. In a network race every player's car takes its input once a sample.
A client predicts its own car from its inputs at once and sends them (all
the host has not acknowledged, each frame); the host simulates each
client's car from them as a full car of the level (mmNetObject's vehCar,
polygonal bound, type-3 mover), so collisions between players, with the
traffic and with the props happen there once, and sends each client 20
times a second the last input it applied, the car's state after it and
every other player's car. A client puts its car back to the host's state
when they differ and runs its later inputs again; it draws the other cars
interpolated from the host's states as before, and runs its samples up to
3% faster or 2% slower to keep the host a sample or three of its inputs
ahead. Resets (the water, a fall, a debug respawn, Cops and Robbers'
repairs) travel as commands with the inputs and happen on the host at the
same sample. The host decides every car's damage: it records its
simulated cars' dents, parts and impacts and sends them about each
player's car (VehicleDamage subjects 513 on), and refuses a player's word
on its own car; a client shows its own car's damage from them and keeps
only its predicted sparks and sounds.

OPENMM2_NET_TRACE's divergence lines now use the machine's shared
monotonic clock (each process's own clock made the traces of instances
started apart incomparable), syncreport also reports how close each machine
drew two colliding cars around the collision, and OPENMM2_DEBUG_NETCARS
logs the corrections and what the cars cost.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A function-local struct with default member initialisers made with
std::vector<T>(n), and polymorphic objects (worlds, prop sets) built
inside lambdas in the tests: both have broken MSVC before. The ring's
per-slot state is a namespace-scope struct, the tests' helpers are
functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/multiplayer.md gains a Props section (what MM2 does, the protocol,
the client's rules, the measurements, the deviations) and the props in
the channel, message, untrusted input, event, per-frame contract,
automation and testing notes; docs/bangers.md what BangerSet carries for
network games; docs/parity/openmm2-only.md the extras and deviations.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/multiplayer.md: the message's stamp and rail motion, the prediction
to the present, the client's collisions, the trace lines and mm2tool
nettrace, protocol 5; docs/parity/openmm2-only.md: the new OpenMM2 extras;
the start of the review record (docs/review/multiplayer-desync-traffic.md).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Protocol 7 for the props (5 is the players' cars, 6 the shared
traffic's). The host's players' cars are simulated bodies now, so they
knock its props as single player's cars do: the kinematic copies'
banger breaking (Body::kinematicBreaksBangers, calcBangerImpact's
kinematic branch) is gone again, and dgImpact::CalcImpact is MM2's for
every body. The parts the host throws off a client's car carry that
player's tag, like its own car's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
At traffic density 1 the cars at the edge of what fits still came and went
190 times in 100 s: a near car turning or slipping costs more bits in one
message than in the last, the farthest known car no longer fit, and back as
a new car it needed its state. A car new to the client now waits while the
known ones would leave less than a tenth of the budget, which absorbs those
changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the host simulating every player's car, a client's car runs ahead of
the host's simulation of it by the time its inputs take to reach the host
and wait there. The client measures that lead from the host's states of its
car (the host's session time after sample n against the time it ran sample
n itself) and shows the received traffic at its car's time plus the lead,
so its car meets the cars where the host's simulation of it will. Its own
knocks are confirmed on the host's clock (the host simulates the same hit
at that time), so the half round trip leaves the window.

The hits now happen on the host: the client's hit reports (TrafficHit, the
host's applyNetTrafficHits and TrafficBodies::knock) go. The host chooses
each client's cars round its simulated car, which also fixes the traffic no
longer being sent at all (the clients send no snapshots any more, and the
host looked for them).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the host corrects a client's car, the samples run again now meet the
other players' cars where each sample first met them, not where they stand
in the frame of the correction: pushing another car from behind, replaying
against its newest place made the corrections swing back and forth by up
to 4 m. Also an experiment for the record, OPENMM2_NET_OTHERS: "ahead"
places and draws the other cars where the host will have them when it runs
this machine's sample (extrapolated from its newest states), "ghost" keeps
this machine's car from touching them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The props' lines (placed props, knocks and their causes, undone
predictions, and every 250 ms of session time the props down, the
pieces and parts shown, the cars' damage and the own car's damaging
impacts) go into the session's divergence trace with lowercase tags,
and `netprobe syncreport` compares every pair of machines' props after
the cars. OPENMM2_DEBUG_NETPROPS and `netprobe propdiff` are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A client replays its car's samples when the host corrects it
(World::replaySample), and everything it meets there holds still. A
standing prop held like a wall (calcBangerImpact against a body without
an ICS stops the car after the break limit) and a knocked-over piece
like an infinitely heavy body: a replay through props took up to 6 m/s
off the car that the real sample had not (1.5 m corrections after a hit
on a street light). The held instance now meets the car with the mass a
real hit gives it (Instance::heldInertia: the active dgBangerActive::
Attach would give the prop; a copy of a moving piece's phInertialCS),
still taking nothing itself, and a light one (under a quarter of the
car's mass) gives way after the first hit of a replay, as the real one
flies off (World::beginReplay, which CarPrediction calls). A car
replayed into a meter loses what a real one does (within 1 m/s), not
all of its speed. A client driving alone through props had no
correction in 40 s: the host's simulation of its car through the same
props gives the same samples.

The parts the host's record takes off a client's own car are no longer
thrown again on the client (the host's ring shows them), and the props
trace takes each car's dents from the host's records.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the host simulating the client's car from the same inputs, the car the
client knocked loose moved 3 cm (median) from the host's while the client
simulated it, but handing it to the host's messages as soon as they showed
it knocked jumped it 3.7 m: the host's knocked car, a trip old, predicted
along its velocity while it tumbles. The local body now leads until it
comes to rest, the host's car is 5 m from it, or the host has it back on
its rail; meanwhile the received car is not also a moving instance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lowlevelmetal and others added 29 commits October 9, 2026 21:27
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integration at b33bebc: the simulation's maths through libm::, every
target with -ffp-contract=off -fno-fast-math.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/review/multiplayer-desync-traffic.md: protocol 12 against 782f469 in
the same scenarios plus a second police chase: what was fixed (O1-O3 and
four more found on the way), what stays open, the bandwidth and the edits
outside this area.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/review/multiplayer-desync-rules.md, "Second round: the rough
edges": MM2's water and fall handlers (mmGame::Update,
mmGameMulti::HitWaterHandler, mmMultiCR's), what changed in protocol 14,
the runs (the bay's water in a cruise and in Cops and Robbers, contested
gold) and their results before and after: checkpoints confirmed with the
loss tail gone (99th percentile 767-776 to 417-441 ms at 150 ms and 5 %
loss), the water agreeing at the same samples and now the host's, no
predicted pickup undone (3 of 7 contested runs before), the bandwidth;
R9 and O1-O6. docs/multiplayer.md: a client's predicted water as its own
bullet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a client stalled, or its inputs were lost for longer than the host repeats them, the host coasted its car on and then dropped every input as late, until the client's 3% faster pace made the gap up: a minute for two seconds (one 75 s run here had the host's car of the client parked while the client drove off 500 m). A state acknowledging a sample the client has not run now makes it count the samples up to it, the round trip and the host's margin as run (CarPrediction::skipTo), so its next inputs reach the host in time; the state then corrects the car to where the host coasted it. The players' cars' section of docs/multiplayer.md describes this round's states, near cars and drawing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A car's full state now also holds, when the sample's push has moved the body past it, the bound's matrix as the collisions saw it (the next sweep starts there), and the collider that pushed it hardest in the sample (CopyLastMatrix reads it) as a player's number. Rebuilt without them, a car pressed against another drifted from the host's: a push against a braking car was corrected by 0.93 m; now twice by a centimetre (tests/game/test_player_cars.cpp).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NetGameStart.SlowLoaderIsWaitedForAndBothGoTogether wanted each machine's
countdown and go within 25 ms of the shared times, which a frame under a
loaded ctest run can exceed. NetRaceStart counts down and goes in the first
frame at or after those times on each machine's session clock; the test now
checks exactly that (the frame before was earlier, the frame itself not).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The companions keep both the shared traffic's (police and knocked cars driven or rebased by their own functions) and the players' (each sample's input from the host's inputs ahead, the state's pusher mapped to this machine's colliders); the full traffic states go out with the CarStates after every frame that ran a sample. The players' cars this round take protocol 13 (11 on this branch); a near player's car now counts as a player's in what it hits, so it knocks the shared traffic loose on the client as the simulated police do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Integration at 8e74c96 (the shared traffic's second round, protocol 12):
the rules keep protocol 14, RulesState after TrafficFull.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Before and after on the merged builds: another player's car's knocks now
fall as that car meets the prop (they fell 140-400 ms after it passed;
138 of 155 confirmed by the host), a missed prediction stands again in
0.7-1.8 s instead of 2 s, a client's pieces knock no placed prop, each
client's message carries its own area first; every knock still shared,
the pieces within 3 mm, 0.1-6.4 KB/s to each client.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/multiplayer.md
#	src/net/Protocol.h
…ection

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shared police and knocked traffic cars a client now simulates with its
car (TrafficFull, or knocked loose by it) may knock its props as the near
players' cars do: a prop one of them hits falls as it meets it, the host's
knock confirming it (TrafficBodies::carOfBody tells a traffic car's body).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

# Conflicts:
#	docs/multiplayer.md
#	src/net/Protocol.h
A car's full state carries both the water (its vehSplash and the water handler's time) and the contact's bound and pusher; the host's reset check is the rules' narrowed one. The players' cars of this round take protocol 15.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A client ran the host's braking car into a parked car 1.9 s before the real
one reached it; the prediction was undone at 2 s and the host's knock came
a moment later, so the car stood up and fell again. While the host still
has the knocking car within 6 m of the prop, its prediction now waits up to
4 s. Protocol 16 in the docs (13 on this branch).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the build merged with the traffic's and the rules' second rounds: every
knock shared, the pieces within 3 mm of the host's, all 66 predictions of
the last batch confirmed (own car, other players' cars, the police and
knocked traffic), another player's car's knocks falling as it meets the
prop; 1045 of 1045 tests with and without assertions; protocol 16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The players' cars of this round take protocol 17.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A client drew every near car at its player's present, a lead back on its trail: right for the host's car, whose player's present is the host's, but another client's screen runs ahead of the host's as this one's does (each predicts its car its inputs' trip and the host's margin ahead), so its car was drawn about 150 ms behind it. Another client's car is now drawn at this machine's car's time always (three players ramming, client to client: 0.02 m at the median while the clients were near each other, against 0.38-0.54); the host's car as before. The players' cars' section of docs/multiplayer.md has this round's measurements.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The third round's section of docs/review/multiplayer-desync-cars.md: what the runs showed (the near cars' old inputs, where they were drawn, a contact's bookkeeping, a client behind the host's samples, O8's cause), what changed, every scenario before and after against 0.3.1 and integration before and after the rules' and props' second rounds merged, what drives the corrections (knocked-over props in the rear runs), the traffic (O8, fixed by the traffic's second round), another player's car's knocks for the props (10 of 57 undone on integration, 3 of 43 here), the checkpoint race's start (not reproduced), a race started in the water (the rules' O5, still open) and the bandwidth. O5 dropped, O8 and O9 fixed, C9-C11 and O10-O11 added. The parity and sync notes and two comments carry protocol 17.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he fuzz test

The mutated CarStates the decoder accepts now also keep the near set within three cars, a near car's inputs ahead within 16 (their flags, events and extra mass in range) and every full state's carried forces, pushes, tyre resistances and bound matrix in range.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A client's car pushing knocked-over props and pieces met them where the
host's messages showed them (a playout delay in the past) or as its own
simulation had them, and was corrected at nearly every state. The host
now sends, with the CarStates of each client (about 20 times a second),
the pieces round that client's car in full (net::PropFullMsg): a moving
piece's body as its simulation has it, with the bound where the sample's
collisions saw it and what pushed it hardest (the sweep against that
collider starts where the push left it); a resting one's exact frame.
The client puts them to those states and runs them with its car when its
samples run again (CarPrediction companions), so the replays reproduce
the host's samples exactly.

Also: a piece in full takes this machine's own piece of the same prop
before the slot is shown; a piece in full knocks placed props as the
host's does (a prediction, with a replay ghost); its prop's ghost goes
once the host's state of it is known; up to three datagrams a state,
the nearest pieces in the host's world's order.

Measured in tests/game/test_prop_push.cpp (a car pushing a pile of 18
props through the rest, 60 states a second, 100 ms each way): no
correction with the pieces in full, 269 without.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pieces round a client's car in full (protocol 18) in the props'
section of docs/multiplayer.md, and the round in the review: the chase's
corrections are not the props' second round (the same binary corrects 0
or 112-167 times a minute from run to run, before and after it), what an
exact replay of a pushed pile needed step by step, the rear-end runs
before and after (corrections over 1 cm near a knocked piece 208 -> 87 a
minute, the largest 2.8 m -> 0.77 m), the bandwidth, and another
player's car's knocks on the merged build (31 of 50 predicted, 15 of 50
before the cars' third round).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
src/net/PlayerCars.h and src/game/net/PlayerCars.h shared a name. For a
quoted include MSVC searches the directories of every file up the include
stack before the -I paths, so "net/PlayerCars.h" from a file under
src/game/ found game/net/PlayerCars.h and the wire types were never
declared (PR #5's Windows build: C2039 'CarInputFrame' is not a member of
'mm2::net'). The wire header is now net/PlayerCarState.h, like the other
*State.h messages; no other quoted include in the tree resolves to two
headers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lowlevelmetal
lowlevelmetal merged commit 6ba4c79 into main Oct 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant