Repository navigation
Multiplayer review and fixes, shared traffic in multiplayer cruise (0.2.0) - #2
Merged
Merged
Conversation
file(DOWNLOAD) with EXPECTED_HASH turns a failed download into a CMake
error ("cannot compute hash on failed download"), so a timeout on the
first CTAN mirror ended the configure without trying the others; the
v0.1.0 tag's MinGW build failed that way. The hash is now checked after
the download, a mismatch moves on to the next mirror, and a font that no
mirror provides is a warning again, as the file's comment says.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host cleaned up what joiners sent, but its cleanup dropped every
trailing non-ASCII character ("José" became "Jos") and could cut a name
inside a UTF-8 sequence when it added " (2)". Clients cleaned up nothing
the host sent: names, chat, the session name, kick and reject reasons
reached the UI with control characters and invalid UTF-8, the player list
grew to 255 entries (each one a car in the race), and the city name went
unchecked into file names and, as the last race, into the profile file,
where a newline added arbitrary lines.
sanitizeText keeps well-formed UTF-8 without control characters and cuts
on a character boundary. Car and city names must be plain base names
([A-Za-z0-9_-], up to 32 characters): the host gives a joiner with any
other car the default car and keeps the old one on a request, a client
replaces such a car with the default and ignores such a city. A client
keeps at most kMaxPlayers players and refuses a Welcome that does not
list it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host relayed every chat line, PlayerRequest and game event a joiner sent to every other player on reliable channels, with no limit: one client could make the host send N-1 times its own flood and grow ENet's unbounded outgoing queues. Each joiner now has a budget per kind (chat 2 lines/s with a burst of 8, player updates 10/s with 20, game events 30/s with 60), far above what the game sends. Chat and events beyond it are dropped. A PlayerRequest is always applied and its update relayed once the budget allows, so the last car, colour, team and ready state still reaches everyone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ENet's defaults allow a 32 MiB packet and 32 MiB of undelivered data per peer, and it allocates a fragmented packet's full size when the first fragment arrives. Any connected peer, before it even sends Hello, could make the host allocate tens of megabytes with a couple of datagrams, and with 16 peer slots about a gigabyte. TransportConfig now limits a packet to 64 KiB and a peer's waiting data to 1 MiB on both ends. OpenMM2's largest message, a Welcome with 16 players and 32 extra settings, is about 5 KiB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2's network cruise has no traffic and no police (mmGameMulti::Init zeroes their densities). OpenMM2 adds them as an extra: the host runs them and sends each client the cars near it. This is the wire and the replication logic; the race wiring follows. - net::AmbientStateMsg on a new unreliable, sequenced Ambient channel: per car the slot id, a generation that changes when the slot is reused, the model (catalog index), paint job, position relative to the message's origin (3 cm), orientation, the speed of a car on its rail or the full velocities of a knocked car or a police car, and flags (brake, horn, indicators, off its rail, wreck, siren, pursuit); police add their target, damage, rpm, throttle and gear. A rail car takes 118 bits. - SessionSettings::sharedTraffic (the host's lobby option, on by default) and RaceConfig::netTraffic; protocol version 2. - game::TrafficHost picks each client's cars: the police chasing it, then the nearest within 200 m (kept until 230 m), within one unfragmented datagram. game::TrafficClient follows them with the remote players' interpolation buffer: every message is complete, so a car missing from a newer one has left, an older one never brings a car back, and a new generation is a new car rather than a blend across the city. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The second race of a session bounced everyone back to the lobby as soon as it had loaded. When the host's race ended (a finish, the results, or Quit to Lobby), RaceScreen::leaveRace called NetGame::returnToLobby and switched to the menus; the session's ReturnedToLobby event reached NetGame only on the menus' next update and set a "return pending" flag that nothing in the menus took. The next race's first frame took it as its own, left the race, and the host, still in that race's countdown, sent everyone back again (which set the flag once more, so every later race bounced too). A joiner who had quit a race early met the same stale flag alone. NetGame now counts the races it sees start (raceNumber) and remembers which race the last return to the lobby ended (backToLobby); the race screen keeps the number of its race. A return and a new countdown arriving in one update still end the old race and start the new one. Game events still queued when a countdown starts are from an earlier race and are dropped. The frontend script now survives a race (wait:race, and the commands after the one that started it run when the menus are back), and mp:car changes the lobby car as the garage does, which is how the bounce was reproduced. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The beacon answered every well-formed query from any source with an advert about 17 times its size, up to 64 per frame: a spoofed source turned a host with a reachable discovery port into a reflector, and a spoofed broadcast source made one query reach a whole subnet. The scanner kept every advert it heard, keyed by source and game port, so a flood of spoofed adverts grew the session list without bound, and their text reached the session list as sent. The beacon now answers loopback, private addresses and the subnets of this machine's interfaces (so VPN adapters on public ranges still work), never broadcast, multicast or zero addresses, and at most 20 replies a second with a burst of 40. The scanner lists at most 64 sessions, skips adverts with game port 0 and cleans up their text like the session's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The crash-safe state file's ports were cast to 16 bits unchecked, so a damaged file wrapped them around, and internal port 0 passed: the next start's NAT-PMP or PCP clean-up then asked the router to delete internal port 0, which RFC 6886 and RFC 6887 define as every mapping of this machine, other programs' included. The PCP nonce's hex accepted signs. The external address a UPnP device reported went to the lobby's status line as given, and any device on the LAN can answer as the router. Records with a port outside 1-65535 are ignored, the PCP/NAT-PMP backend refuses to delete internal port 0, the nonce takes only hex digits, and an external address that does not parse is dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2's traffic is written for a list of players (aiMap +0x170, filled by aiMap::AddPlayer): aiMap::Update populates the roads round each player's room with that player's bit in each road's mask (aiPath::AddAmbPlayer / RemAmbPlayer), aiGoalRandomDrive::Update and aiGoalRegainRail::Update walk the list for the first player in a car's way and keep its index (aiVehicleSpline +0xe6) for aiGoalAvoidPlayer, and the pose solver refits a car when any player is within 100 m. OpenMM2 had folded that into one player. Traffic::step now takes the list (one player in slot 0 is the single-player step, unchanged), with 16 slots where aiMap has four, for the shared traffic of a network cruise. ai::World::setOtherPlayers adds the network players to the traffic (the pedestrians keep the local player only). World counts its light steps, and advanceLightsTo lets a network client's light sets follow the host's: they are deterministic from a reset. Traffic publishes a spawn count per pool slot so the network can tell a recycled slot from the car before it, and the police mark hits by the first player's car (the local one) as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Back from a network race, the menus took the race's whole configuration as the driver's: multiplayer set, no traffic, police or opponents, and the host's difficulty. Leaving the session and starting a single-player cruise then ran it with no police (19 before, 0 after, in San Francisco), with the multiplayer rules and start picks, and nothing could be recorded. The menus now keep the driver's state as the profile gives it (configFromProfile), which already holds the network event and car (saveNetEvent, as MM2's MultiStartGame -> BeDone); MM2's network games never touch the single-player traffic and police settings either. The sessions page under the lobby no longer starts the LAN browser, which then searched every second through the races. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A network car is a kinematic body placed at its received position; its collider has no inertia, so every impact saw it standing still. That is MM2's static wall, and stays the default. An instance may now report its motion (Instance::kinematicMotion, Body::kinematicMoves): its collider then carries that velocity and spin into the impact, so a traffic car a remote player hits on the host is knocked away as by a moving car, and a client's car meets the shared traffic and police as moving cars. Nothing in MM2 has such instances (OpenMM2 extra). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
mmNetObject::Init takes a network car through mmVehList::GetVehicleInfo, which answers a name the list does not have with the default vehicle, vpcoop (mmVehList::LoadAll). OpenMM2 tried to load the name as sent and, when this machine lacked it (an add-on car), retried the load every frame, logging a warning each time, while the player's car stayed invisible and could not be hit. netVehicle gives vpcoop for a name the catalog does not have, and a car that still fails to load is tried once per car and paint job. The traffic and pedestrian densities arrive as a percent byte, which allowed up to 255 %; fromSessionSettings now keeps them at most full. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NetArena::AddRosterName cuts a name wider than the column to "%.6s...". OpenMM2 took six bytes, which splits a UTF-8 character (a name such as "aéééé" showed a replacement glyph). It now keeps six whole characters, as the main menu's fit() does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NetGame::raceConfig() left RaceConfig::automatic at its default, so every network race was driven with the automatic gearbox whatever the driver had chosen, and the menus then saved that as the driver's choice. MM2's network session data carries no transmission (mmInterface::GetSessionData) and mmGame::Init sets vehTransmission::Automatic from the player's own state. NetCar now holds the driver's choice: the lobby takes it from the menus, the garage's PREV (applyLobbyCar) updates it, and the race and the saved event (saveNetEvent, BeDone) use it. It never travels. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In Cops vs. Robbers each machine put its own player in the team's car (vpcop for the cops, vpmustang99 for the robbers) but drew and counted the others with the car they had picked in the lobby. A robber who had picked a police car was drawn as one, and RaceScreen's Cops and Robbers took him for a cop on every machine but his own, so the gold rules disagreed. MM2's lobby sets the player's car to the team's, locks the others and sends it to the session (mmInterface, cnr_team, ChangePlayerData), so every machine sees the same car. game::raceCar now holds the rule, and NetGame applies it to every player: raceConfig() for the local car, playerCar() and remoteCars() for the others; the race's Cops and Robbers teams and the lobby roster use it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A fixed-seed fuzz test feeds random and mutated messages, LAN packets, router replies and event payloads to every decoder, checking that what decodes stays within the limits the writer could produce. Two more drive a live host (from a joined peer, a peer that never said Hello and raw datagrams below ENet) and a live client (from a fake host) with mutated traffic and check that the session stays consistent. It found that Transport::send passed a zero-length packet to ENet, whose range coder then reads the packet's null data and crashes the sender. Every message has at least its type byte, so send now refuses an empty one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NetGame keeps the last 64 chat lines. The lobby remembered how many there were when it opened (NetArena::ResetGameChat) and the race how many it had posted, both as indices into that list: once it was full the indices stopped moving, and neither showed a new line again for the rest of the session. The race also started counting at 0, so its first frame posted every line typed in the lobby on the HUD (seen at the start of a cruise), with a network alert for each of the others' lines. Every line now has a serial; the lobby shows the lines from the serial it opened at, and the race posts the lines from the serial it started at. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A wrong password cost a guesser only a reconnect: the host answered each new connection with a fresh challenge, so a lobby password could be guessed online at the rate of round trips, on up to 16 connections at once. The host now counts wrong passwords per address and, after five within a minute, disconnects that address's new connections with BadPassword before sending a challenge until the minute is over. The joiner sees the usual "wrong password". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A client's session kept the last state its race screen had submitted and went on sending it at the snapshot rate all through the lobby; the host stored it and passed it on to the others. The next race then began with that car drawn, and collided with as a kinematic body, where it had stopped in the last race until its new states arrived. The session now replicates cars only during a race: a state submitted in the lobby is ignored, a late VehicleState or WorldState arriving there is dropped, and the countdown and the return to the lobby clear every car's buffer and the local state on the host and the clients alike. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A city that failed to load sent the race screen straight to the menus. For the host of a network race that left the session in the race's countdown or game phase: the lobby's GO DRIVE could never start another race (NetGame::startRace needs the lobby phase) and the joiners waited in a race that would not end. A failed load now leaves through leaveRace, so the host takes everyone back to the lobby (seen with a session set to a city the data does not have: both machines back in the lobby, and the next GO DRIVE starts again); a network race whose own car cannot be loaded leaves the same way. While a network race loads, its session is serviced between the parts, so that a machine that loads more slowly than the others is not timed out by them (ENet gives up on a peer that has not answered for 5 s). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The control options page changes the driver's transmission too, and it can be opened from the lobby (the strip's OPTIONS); only the garage's PREV handed the choice to the lobby car. The menus now give the lobby car the transmission they have when the race starts. NetGame::setLocalCar sends the car to the session only when the car, paint job or team changed, so this costs no message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/review/multiplayer-input.md lists what was read and tested, the fifteen fixed findings with their commits, what was checked and found correct, the open findings and what other areas should pick up. docs/multiplayer.md describes the limits now applied to untrusted input: packet sizes, text cleanup, base-name checks for cars and cities, player counts, relay budgets, the password lockout, who the LAN beacon answers and the checks on the port mapping record. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the host's lobby option on (it is by default), a network cruise keeps the traffic and the police that mmGameMulti::Init takes away: - The host's lobby has a SHARED TRAFFIC panel in cruise (on/off, the single-player cruise's traffic and cop density sliders, defaults 0.5 and 1 as RaceMenuBase::SetStateRace), carried to the clients by the session settings; off gives MM2's network cruise exactly. - The host runs ai::World's traffic at its density with every player in aiMap's player list and the cruise's police posts; the other players' kinematic cars are players to the traffic and suspects to the police, and strike traffic cars as moving cars. Every 50 ms each client gets its cars (game::TrafficHost), with the light sets' step count. - A client runs no traffic or police of its own: it interpolates the received cars (game::TrafficClient), draws them with the traffic renderer and the police as cars with their sirens, plays their engines, horns and sirens (a cop chasing it starts the chase music), shows the police in pursuit on the map, collides with them as moving kinematic instances (game::TrafficProxies), runs its light sets to the host's steps, and its pedestrians shy from the received cars off their rails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The other players' cars are kinematic bodies placed every frame from their snapshots. Their colliders had no velocity, so every impact treated them as walls standing still: closing on a car doing 30 m/s at 1 m/s cost the chaser 12.6 m/s, a rear tap threw the car back and rubbing side by side dragged it as against a barrier. In MM2 those cars were simulated vehCars (mmNetObject) with velocities of their own. A kinematic collider now takes its velocity from its body's ICS (which RaceScreen fills from the snapshot), so impacts work on the relative speed; the network car still does not give way (infinite mass). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The "cars slightly bounce" report, measured with a host and a client on one machine (and through a 100 ms round trip with jitter and loss): - Snapshots were stamped with the frame's time, but the state they carry is the last fixed simulation step's, up to 16.7 ms older. 50 ms of stamps carried 3 or 4 steps of motion, so a car at 140 km/h surged back and forth by up to 0.35 m (0.1 m vertically on a slope) even on loopback. NetGame::submitLocalState now takes the state's age (the fixed step's unstepped remainder) and stamps the session time the state belongs to, measured once a frame (NetGame::frameTime). - Remote cars were sampled at the frame's time while the local car, the camera and every simulated object move in whole 60 Hz steps: a car alongside shook by half a step (29 cm rms at 35 m/s, 144 fps), and at 60 fps 4-10% of frames jumped by a whole step. RaceScreen now samples them once a frame at the time the simulation reaches after the frame's steps (World::remainderAfter) and uses that one sample for the physics body, the drawing, the HUD and Cops and Robbers. - The fixed 100 ms interpolation delay is too short over the Internet: at 100 ms round trip 14-40% of frames extrapolated and snapped back (up to 0.56 m, 0.48 m vertically). Each remote car now has its own playout delay, what its snapshots needed to arrive in time over the last 3 s (SnapshotBuffer::requiredDelay), between 50 and 500 ms, growing quickly and shrinking slowly so the shown time never jumps. - A client's clock estimate stepped whenever the best sample changed, moving every car by its speed times the step. The shown offset now slews toward the estimate at 5% while a race runs (SlewedClock), and the clock has sub-millisecond precision. - Snapshots go out on a steady 20 Hz cadence instead of drifting to the next frame each time, and only new states are sent. - States are ignored in the lobby, and a client forgets its car on the return to the lobby, so the last race's car does not appear in the next one. OPENMM2_NET_TRACE=<file> writes the snapshots taken in and, once a frame, the local car and the remote cars as drawn, for diagnosing replication. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
As the network review asks of every received value: - A received car's paint job is held to the jobs its model can show, and a model always comes from the client's own catalog. - The host's police are shown at most 32 at a time; each model is loaded once per car shown at a time (a car that leaves keeps its parts for the next of its model), a model that fails is not tried again, and a new generation or paint job reuses the car. - The fuzzer feeds AmbientState through every decoder and a live client, and a hostile host's bit-flipped messages never give the client a car out of range. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A client's cars beyond 80 m on their rails now get their state in every other message; in the others they come as 13 bits (id, generation, "still there"), so a missing car still means it has gone and nothing comes back from a lost message. Near cars, cars off their rails, the police and cars new to the client keep their state in every message. In a two-player San Francisco cruise at traffic density 0.5 with 63 cars round the client, the host's stream to it fell from 17.5 to 11.0 KB/s. The client's cars within 120 m of it were 2 cm from the host's (median; 90 % within 10 cm, 27 cm at worst, for far cars between messages). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host settings page remembered the host's mode and city for the menus but not the race or its setup, so the single race menu could hold a race mode with cruise's race and setup. Hosting, choosing a circuit in the host settings and leaving the session, the race menu's GO then started the circuit with no opponents (4 from the menu directly) and could record nothing. The page now also keeps the race and applies its defaults, as the race menu does (RaceMenuBase::SetStateRace); MM2's HostRaceMenu edits the one state pack that the race menu shows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host relayed a joiner's snapshot times unchanged, so a broken or hostile sender could stamp its car minutes ahead: every receiver would drag the car toward that state and hold its playout delay on it. A receiver now ignores states more than 1 s ahead of its session clock or 5 s behind it (more than any clock error or playout delay), and the host relays only the states it took in. Tests for the replication timing: the playout delay's measurement, the slewed client clock, states carrying their simulation time end to end, nothing replicated in the lobby, World::remainderAfter. The multiplayer document describes the time stamps, the playout delay, the clock and OPENMM2_NET_TRACE. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A multiplayer checkpoint race or circuit ends on the host once every player is counted, and the host then takes everyone back to the lobby. The last player to finish is still in its 3 s post-race wait when that return arrives, and its race screen left with the race's bare configuration: no results page for the player whose finish completed the race. MM2's host sends 0x211 when everyone is counted, and every machine goes to its results (mmMultiRace / mmMultiCircuit::UpdateGame state 5). The same race could happen between a Cops and Robbers limit announced on a joiner and the host's return. When the host ends the race (or the session ends), a race this machine has finished or lost, or a Cops and Robbers game whose limit it has announced, now leaves with its results as at its own ending. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host options dialog trims the password it hosts with; the password dialog sent what was typed, so a stray space before or after the right password was refused as wrong. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lobby now shows notices while the session goes on (a race that could not load), so a notice left from an earlier session must not appear in the next one: hosting and joining start with none. The multiplayer notes said the host settings add Snowing to the weather; the page offers Clear to Raining (RaceMenuBase::IncWeather stops there). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The root cause of the lobby bounce (a return to the lobby left pending in the menus ended the next race), the fixes made, what was checked and found correct, and the findings left open, among them MM2's wait for every player to load before the start. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host's view of a client's car lags the client's own by the interpolation delay and the trip, so a client that hits a moving traffic car as it shows it often misses it on the host, where the collision then never happens. The client now reports such a hit (a reliable game event: the car's id and generation and its own velocity before the step, at most once a second per car). If that car is still on its rail on the host and the client's car is near it there, the host knocks it off as the hit would have (aiVehicleInstance::AttachEntity) with the impulse of a car of the client's mass meeting it at that velocity (elasticity 0.25), and the result comes back with the next messages. A hit the host saw itself has already taken the car off its rail and the report is dropped. Also two automation aids: OPENMM2_DEBUG_NETTRAFFIC logs both sides' cars, hits and knocks for comparison, and OPENMM2_DEBUG_NET_SHOT_MS ends a network race at a session time, so every machine's --screenshot shows the same moment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shared traffic follows the cars' replication lifecycle: the host sends it during a race only, a client drops it in the lobby, and the countdown and the return to the lobby clear what was queued. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Measured with a host and two clients, each behind a relay adding 75 ms +-30 ms per hop: - The host held a joiner's state for its own 20 Hz tick and sent only the newest one per player: of two that arrived within one tick the older was lost, a quarter of the client-to-client snapshots at 30 ms of jitter, and every one waited up to a tick longer. Joiners' states now go on to the others as soon as they arrive. - The state channel was unreliable but sequenced: ENet drops a packet that arrives after a newer one, which with packets reordered by the network cost 15-20% of the snapshots. A snapshot that arrives late still fills a hole in the receiver's time-ordered buffer, so the channel is now unsequenced; with reordering every snapshot arrived and no view extrapolated. - ENet's packet throttle drops unreliable packets at the sender after round-trip spikes (0.94 for seconds in these tests). The game's few small packets are not what congests a link; each side's peer now never throttles down. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2's network games have neither racers nor police (mmGameMulti::Init); OpenMM2's shared cruise traffic, when the host has it on, brings the cruise's police posts back for the host to drive, and the races still have none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/multiplayer.md: the Ambient channel, the AmbientState message and its encoding, interest management, the far cars' alternation, the complete-message rule, the client's hit reports, the untrusted-input checks, the lobby panel, the automation hooks, the measured bandwidth and accuracy, and the tests. docs/ai.md: MM2's player list and the traffic's 16 slots. docs/gamemodes.md: a network cruise's traffic with the option on and off. docs/parity/openmm2-only.md records the extra and its deviations from mmGameMulti::Init (the maintainer's decision of 2026-10-09: shared, host-authoritative, a lobby option on by default); the new files get their manifest rows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lobby's host settings box leaves its fourth line empty in cruise; it now says whether the host shares its traffic and police (OpenMM2 extra), so the joiners know before the race. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A race or circuit that waits for every finisher compared the results it had (the player's own and the others') with the players listed now. A player who finished and then left the session still counted, so with three players one could finish, leave, and end the race (and send everyone back to the lobby) while the third was still driving. MM2's host compares its finish count with the players in the session as each finish arrives (mmMultiRace::GameMessage 0x206). OpenMM2 now waits until every player still in the race has finished, or the finish timeout runs out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A host and a client NetGame on loopback start a cruise; the host's cars (two traffic cars and a cop chasing the client), chosen by TrafficHost, go out on the Ambient channel and the client follows them with TrafficClient at the interpolation delay within 35 cm of where they were at that session time; the client's hit report reaches the host; nothing is sent in the lobby. Also keeps the shared traffic's new lines within 110 columns. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A joiner who quits a running race goes back to the lobby and stays in the session. The others went on drawing its car frozen where it stopped (a solid obstacle, held after 250 ms of extrapolation), and a race or circuit waited for its finish until the 60 or 120 s finish timeout ran out. In MM2 the player left the session (mmGameMulti::QuitNetwork), and the others took its car out and stopped counting it (SystemMessage 0x2d, mmNetObject::Deactivate). The quitter now tells the others (GameEventType::LeftRace): they take its car out of the race (drawing, collisions, HUD, Cops and Robbers, which also drops gold it carried), show "has left the game", and leave it out of the racers they wait for. The race screen takes the frame's game events once and hands them to the race and the Cops and Robbers rules. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each machine counted the time limit (and its warnings and lockouts) in frames from the moment its own race screen loaded, through the start countdown: the clocks disagreed by the difference in load times, so one machine showed "Time's up" and its results while the others played on. mmMultiCR::UpdateGame starts the clock when it enables the racers (state 2). OpenMM2's cars go at the shared start time, so the rules now run on the session clock from that time: with a host and a client the remaining time agreed to the millisecond. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The other players' cars were silent. MM2 builds them as vehCars with their vehCarAudioContainer (engine, tyres, horn; a police car's siren) and mmNetObject::PositionUpdate drives it from each packet: the pedals, the horn and siren bits, the gear. Each network car now gets the AI cars' positioned car audio, fed from its kinematic vehCarSim (which turns its engine and gears from the snapshot's speed and pedals: 7000 rpm in first, shifting as the sender did) with the snapshot's throttle, brake, gear, horn, siren and wreck. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The other players' wheels were drawn at their rest positions, spun from the snapshot's speed and turned by a fixed half radian at full lock, so they hung in the air or sank into the road over every bump and kerb. vehCarModel::Draw takes a car's wheels from its vehCarSim. The kinematic network car's sim already runs its wheels each sample against the ground under the placed body (suspension, roll, steering from the snapshot's pedals); the drawing now uses those wheel matrices. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A cruise without shared traffic reads no game events, so whatever a peer sends (up to its relay budget of 30 a second) piled up for the whole session. The queue now keeps the newest 256; the modes that read events take them every frame and never come near it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
netprobe relay <host[:port]> --port N --delay MS --jitter MS --loss PERCENT [--reorder] forwards each player's datagrams to the host and back with a delay of delay +- jitter (uniform) each way, dropping the given share, and keeps them in order unless --reorder. Players join the relay's port: a host and its clients on one machine then see an Internet link, which is how the replication fixes were measured (the system's tc/netem needs root). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shared traffic's kinematic motion (Body::kinematicMoves, reported to the collider per collision) replaces this branch's collider pointer to the body's ICS, and the network players' cars now report their motion in every mode, not only in shared-traffic cruise: as still walls they cost a chaser 12.6 m/s on a 1 m/s tap. The state channel stays unsequenced; the ambient channel keeps ENet's sequencing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The client drew the shared traffic a fixed 100 ms behind the session clock. Its messages come over the host's link on the host's 50 ms cadence, as the host's car does: over a 150 ms round trip with jitter 27% of the logged traffic cars ran past their newest message and were extrapolated. The traffic is now shown at least as far back as the host car's playout delay (15%, the rest being the far cars whose state comes in every other message), on the frame's precise session time. The host's handling of the clients' traffic hit reports read the game events itself; since the race screen takes each frame's events once (for the race and Cops and Robbers rules), it now reads that frame's list, as a client's cruise no longer needs to throw them away. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The jitter the maintainer reported, its four causes in the network cars' timing (stamps, frame-rate sampling, the fixed delay, the stepping clock) and the network's share (sequenced drops, the per-tick relay, ENet's throttle), measured before and after with a host and two clients behind lossy relays; the other in-race findings, what was checked, and what is left open. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SharedTrafficNet.HostAndClientInOneProcess failed on the Windows runner: it picked ports from 51000-63000, inside Windows' dynamic range (49152-65535), where the system reserves and hands out ports, and the bind failed. test_netgame.cpp's range reached 56998 as well, and the lobby and sync tests shared one block. Each file now has its own block below 49152. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Multiplayer review and fixes, plus shared traffic and police in multiplayer cruise. Version 0.2.0. The four reviews are recorded in
docs/review/multiplayer-*.md.Fixes for reported problems
Booted back to the lobby: every race after the first in a session was sent back to the lobby right after loading. A return-to-lobby flag from the previous race was never cleared in the menus. Races are now numbered, and a return ends only its own race.
Remote cars jittering, which had several causes:
With 100 ms round trip and 1% loss, the error went from 22-37 cm rms to about 2 cm.
New
Other fixes
Testing
🤖 Generated with Claude Code