Repository navigation
Fix 0.2.0's known limitations: smooth drawing, shared race start, network damage (0.3.0) - #3
Merged
Merged
Conversation
OPENMM2_DEBUG_DRAW_TRACE=<file> (off unless set) writes, once a frame, the frame's time, the physics samples it ran, the player's car, a point by the road ahead and the nearest other car as drawn, in the camera's frame. Differencing those positions from frame to frame measures how smoothly they move at a given frame rate, which is what drawing between the fixed simulation steps is meant to fix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2's network races do not start on a timer. After loading, each machine sits on the grid with every car held (mmMultiRace / mmMultiCircuit / mmMultiBlitz::InitNetworkPlayers), counts 5 s of frames, then sends RaceReady (0x1f6, mmGameMulti::SendRaceReady) to everyone, and shows "Waiting for N players" (strings 31-37) while some other player's has not arrived (UpdateGame state 0). A player who leaves meanwhile is taken off the count (SystemMessage 0x2d). When the host's count reaches 0 it sends the start (0x20f) and every machine counts Ready... and Set..., 1.25 s each, then Go!. Protocol 3 does the same on a shared clock. GO DRIVE sends RaceLoad (the host's race number and the order time) in place of Countdown; each machine reports RaceLoaded with the race number, which the host relays; once every player still in the session has reported, the host sends RaceStart with the session time of the Go, a lead (twice the slowest round trip plus 100 ms, 200-1000 ms) plus the mode's countdown ahead, so every countdown ends at the same moment. Reports for another race, repeats and reports in the lobby are dropped. MM2 has no limit on the wait; OpenMM2's host starts without the players still loading 60 s after the order, and such a machine counts down on its own when it has loaded and joins the running race, as an MM2 machine does with a start message that waited in its queue. game::NetRaceStart runs the machine's side (the 5 s, the report, the waiting text, the countdown on the shared start); the race session's countdown follows the time left (Session::setNetStart). Cops and Robbers reports at once and says "Go!" at the shared start (OpenMM2 waits for everyone there too, since its rules run on that clock); cruise reports at once and is never held, showing "<name> has joined" as the others' reports come (mmGameMulti::GameMessageCB 0x1fa). A network car is shown once its player has reported, as MM2 activates it on RaceReady. A joiner that quits or cannot load the race no longer holds the start, and a machine still loading leaves when the host takes everyone back to the lobby. OPENMM2_DEBUG_LOAD_DELAY_MS stalls this machine's loading for tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2 sends a car's vehCarDamage::CurrentDamage in every position packet (mmNetObject::SetPositionData) and sets the network car's to it (mmNetObject::PositionUpdate, clearing its damage when it drops to 0); every machine simulates the network car, so its own collisions paint the dents (fxTexelDamage) and break the parts there. OpenMM2 draws those cars kinematically, so nothing showed: no smoke, no fire, no dents, no parts. Now the damage level from the snapshot (and the police's from the shared traffic, at 10 bits) is the network car's CurrentDamage, and vehCarDamage::Update's smoke, fire, exhaust and tyre wobble follow it. What the owner's vehCarDamage::ApplyImpact painted and broke travels as a reliable game event (net/VehicleDamage.h): each texel damage patch with the point (painted at its wire precision on the owner too) and the random state it started from, so the receivers' fxTexelDamage paints the same patches; the parts broken off since the reset; the damaging impacts' sparks, shards and sound; a new epoch on every reset (vehCar::ClearDamage). The receivers (game/net/DamageSync) keep one record per car and apply each entry when the car is drawn at the time it happened. The host relays a joiner's damage events with a budget of their own. A knocked traffic car with a body on the host now carries its wheels' drawing offsets (vehWheelCheap), so a client draws it on them as the host does (aiVehicleInstance::Draw). Protocol version 4. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2 ran its game once a frame with the frame's time (datTimeManager:: Update, mmGame::Update, dgPhysManager::Update's samples inside it), so every frame showed the state its own update reached. OpenMM2 steps the physics in fixed 1/60 s samples and the AI in fixed 1/30 s steps, and drew the last step's state: above 60 fps the cars, the camera and the props moved in steps against the frame, and the traffic and pedestrians in 30 Hz steps at any frame rate. The drawing now blends each simulated object from its state when the last step began to its current one by the share of a step the simulation has not run yet (game::StepHistory, fed by step observers on phys::World and ai::World): the player's car, trailer and wheels (spun by their accumulated turns, so a fast wheel does not turn backwards), the cameras that follow it, the mirror, the opponents, the police, the traffic with a body, the props an active simulates, the traffic on its rails and the pedestrians, and the HUD's icons and map. The network players' cars and a client's shared traffic, which are sampled from snapshots, are sampled a step further back for drawing, where the rest of the scene is. A car's reset count (vehCar::Reset), a recycled traffic slot, a pedestrian put on a road, a race restart and any move too long for one step are drawn where they now are, never blended. Nothing the simulation, the rules, the audio or the snapshots read changes; the scene is drawn up to one step (16.7 ms, 33 ms for the AI's objects) behind the frame. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Session tests on loopback: three machines report in all six orders, and the race starts only after the last report, at the same session time on every machine, the countdown and a lead after it; every machine starts within a few milliseconds of the host; a player ejected, leaving or timed out while the others wait no longer holds the start; after the load wait the host starts without a player still loading, which has the start when it loads and whose late report shows it to the others without changing it; a report for an earlier race and repeats are dropped. Hostile input: a joiner's reports count only for itself, for the race being started, once; a host's race messages out of turn (in the lobby, for another race, a second start, unknown players, the client itself, an order that numbers no new race) leave the client's race alone; a Welcome during a race keeps only known loaded players. The fuzz corpus has the new messages, the live fuzz tests check the race state, and a joiner sending mutated race messages can neither start the race nor make it start twice. NetGame: the machine side (the 5 s, the report, the waiting count, the countdown on the shared start, a late machine's own countdown, cruise and Cops and Robbers), a slow loader that both machines' countdowns wait for and end together, a loader that never finishes and joins later, and a joiner leaving the race. NetOptions::loadWaitMs lets the tests shorten the wait. A Welcome's list of loaded players no longer marks the joiner itself, which reports on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The blends (half way is half the way and half the turn, a wheel that turns more than half a revolution a step still turns forwards), when an object is drawn where it is instead (nothing recorded at the last step, another generation, a move too long for a step, a restart), the physics observer's one call per sample, and the shared traffic's draw-time sample. With game data: a car driven with fixed pedals takes the same state at every sample at 60, 90, 144 and 240 fps and with uneven frames of 2 to 42 ms, with the drawing's history kept or not, and London's traffic and pedestrians do the same step for step. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Unit tests for the damage event's encoding (round trip, the owner's point arriving bit for bit, non-finite values, a record overflow), the recorder (batching, splitting, resets, its own budget), the receivers' records (each entry at its time, the hold, a reset clearing the car, convergence after a lost event, cars not drawn, hostile events), the same texel damage on two renderers from the replayed events (retail data), two NetGames, and the knocked traffic cars' wheels. The fuzz corpus and the hostile-input tests cover the new event and its relay budget. Events piling up for one car now go into its record at once instead of being dropped, and the car shows its whole record when next drawn. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rendering.md gets the record: what MM2 did (one update a frame with the frame's time, datTimeManager::Update, dgPhysManager::Update's samples), why OpenMM2 keeps its fixed physics and AI steps, what is blended and how, what is never blended (resets, recycled slots, a pedestrian put on a road, restarts, jumps) and what stays stepped (the particles and tracks), the latency it costs and why the other options were rejected, and the measured judder before and after at 60, 90, 144 and 240 fps, in a race and in a network cruise. physics.md says the fixed step stays and how the observer reads the samples; multiplayer.md how the remote cars and the shared traffic are sampled for drawing; the sync review's S1 is fixed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/multiplayer.md gains a "Race start" section: what MM2 does in state 0 of its race modes (RaceReady 5 s after loading, "Waiting for N players", 0x20f, Ready / Set / Go; no wait in Cops and Robbers, "has joined" in a cruise), what protocol 3 does instead (RaceLoad, RaceLoaded, RaceStart, a shared start a lead ahead, the 60 s wait and a late machine's own countdown, which reports are dropped), and the measured countdowns of two processes with a slow loader, also through netprobe's lossy relay. The messages table, the race flow, the race screen's contract, the automation hooks and the tests follow it. OPENMM2_DEBUG_NET_SHOT_MS=+<ms> takes each machine's screenshot that long after the race was ordered, also before the start (the waiting screen). The parity and game mode records that described the old fixed start, and Cops and Robbers' seed (now the order time), are updated; the lobby review's O1 is fixed, and the sync review's note on it follows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The race screen made its NetRaceStart only with a race session. A network race whose rules could not be set up (a race index the city lacks) then never reported itself loaded, held its car for good and kept the others waiting until the host's load wait ran out; it now reports and goes as any other. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Count the players still loading with an explicit test, and compare the screenshot hook's session times as long long throughout, so that no compiler sees a bool added to an int or a mixed-sign comparison. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MM2 updated its particle systems once a frame with the frame's time (asParticles::Update); OpenMM2 runs those updates at a fixed 60 Hz (fx::FixedTicker), so the rain (35 m/s, 58 cm an update), the wheels' dust and smoke, the engine smoke and the props' debris moved in 60 Hz steps against a scene that is now drawn smoothly. An update moves a particle by its new velocity, so drawing it back along that velocity by the time since the last update was due (FixedTicker::behind) puts it where it was between its last two updates, with the rest of the scene. The tracks, shards and sparks (which MM2 also steps at 1/30 s) stay as they are updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Welcome carried the race only during a race, so a player who joined in the lobby after some races counted from 0 until the next order, unlike everyone else. It now always carries the host's number (the last race's in the lobby) and its order time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tGame Through two NetGames: the client reporting first and then the host, race after race, starts once the second has reported, at the same time on both; a report sent for a race the host has already left does not count for the next one, which waits for the client's report of it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The same two-process run on 0.2.0 (with only the load delay and the countdown's logs added): a joiner loading 4 s more slowly began its countdown 0.5 s late and went 560 ms after the host; 6 s slower, 2.55 s after it. With the handshake both go at the start. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The client's view of the host's car through netprobe relay (50 +- 15 ms each way, 1% loss, reordering) moves by 1.3 mm a frame (median second difference) instead of 114 mm, as on a clean link. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sendLocalState read the flag from the simulated pose, which never carries the lights (only the drawn poses get them from carLights), so the other players' cars never showed their headlights at night. Found by the render interpolation work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/multiplayer.md's race screen contract keeps both: the cars drawn from a second sample between the steps, and no state for a player who has not reported the race loaded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
OPENMM2_DEBUG_FOCUS=net:<id> frames a network player's car (this machine's own or another's as drawn) and police:<id> a shared police car, so both machines can take the same view of one car; OPENMM2_DEBUG_RESPAWN_MS puts the player's car back at its reset position at given session times (as the water does); OPENMM2_DEBUG_INPUT takes several inputs separated by '/' in turn (OPENMM2_DEBUG_INPUT_MS each), to ram a wall again and again; OPENMM2_DEBUG_NETDAMAGE also logs the cars' damage levels every 2 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The network cars' damage smoke and replayed impacts come from where the cars are drawn, a physics step behind their kinematic bodies; a client's knocked traffic cars stand on their wheels as drawn. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Protocol 4 on top of the race start's 3. OPENMM2_DEBUG_RESPAWN_MS takes "+<ms>" from the race's order as OPENMM2_DEBUG_NET_SHOT_MS does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
OPENMM2_DEBUG_NET_SHOT_MS takes a list of times, saving a numbered picture at each but the last; the focus frames a shared police car, a traffic car by id or the knocked car nearest a player; OPENMM2_DEBUG_START_NEAR_POLICE and OPENMM2_DEBUG_START place a cruise's car; OPENMM2_DEBUG_POLICE_TOUGHNESS scales the host's police damage limits; the input phases count from the start of a network race. The host's nettraffic statistics give the share of the police damage bits and the knocked cars' wheels, the client's how many cars stand on their bodies' wheels. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/multiplayer.md gains "Damage": what MM2 sends (vehCarDamage's CurrentDamage in the position packet) and simulates on every machine, what OpenMM2 replays instead (the level, the owner's texel patches with their random state, parts, impacts and resets as reliable events), the police's damage and the knocked traffic cars' wheels in AmbientState, the bounds, the budgets, the hooks and the measured bandwidth. S5 of the in-race review is fixed; the shared-traffic parity notes and the effects page say what a client now shows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The recorder returned straight from its loop when its tokens ran out, skipping the bookkeeping after it; it now stops the loop and carries on from the same place. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Fixes the three known limitations of 0.2.0. Version 0.3.0, network protocol 4.
Drawing between simulation steps
Network race start
mmMulti*::UpdateGamestate 0,SendRaceReady0x1f6 / 0x20f). Every machine reports when it has loaded, the host waits for every player still in the session, and all countdowns end at one shared session time.Damage in multiplayer
Testing
🤖 Generated with Claude Code