Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b9f0f7d
Stand traffic lights on their CG as aiTrafficLightInstance does
lowlevelmetal Oct 9, 2026
1b803d7
Let the frontend script pick the time of day
lowlevelmetal Oct 9, 2026
dc9f9cb
Turn race police posts by MM2's own degree factor
lowlevelmetal Oct 9, 2026
67e02bc
Keep a race's cop count in the cop density as RaceMenuBase does
lowlevelmetal Oct 9, 2026
ffd80d6
Start the police chase tests' suspect 75 m behind the cop
lowlevelmetal Oct 9, 2026
cd820fc
Draw knocked traffic cars' wheels and shadows as aiVehicleInstance does
lowlevelmetal Oct 9, 2026
52b505e
Load the player's steering tune from tune/<car>.asnode as mmPlayer does
lowlevelmetal Oct 9, 2026
3d11dff
Stand PKG xref bangers upright as lvlLevel::LoadInstances does
lowlevelmetal Oct 9, 2026
cc92314
Read a banger's birth rule whatever its block is called
lowlevelmetal Oct 9, 2026
ba78c68
Draw semi trailers as vehTrailerInstance does
lowlevelmetal Oct 9, 2026
71c0608
Face the stand-in player car along its velocity
lowlevelmetal Oct 9, 2026
5eb4cd1
Widen far pedestrians' stick figures along MM2's modelview row
lowlevelmetal Oct 9, 2026
9d2c313
Restart the race at the start of the next frame as mmReplayManager does
lowlevelmetal Oct 9, 2026
d6310f6
Keep the camera, sky, rain, HUD message and water check going while p…
lowlevelmetal Oct 9, 2026
dc95635
Light scaled instances with Direct3D's unnormalised normals
lowlevelmetal Oct 9, 2026
c124f3c
Drop the C and V keys of the unreachable mmGame::UpdatePaused
lowlevelmetal Oct 9, 2026
6b444ac
Drive the car with the menu up as mmGame::Update does
lowlevelmetal Oct 9, 2026
657657e
Sweep police headlight beams in world space as DrawHeadlights does
lowlevelmetal Oct 9, 2026
bf32be5
Draw the traffic and the pedestrians from one stream as aiMap does
lowlevelmetal Oct 9, 2026
abed91c
Set a race up from MM2's global random stream in mmGame::Init's order
lowlevelmetal Oct 9, 2026
186760f
Document the meaning of the race and environment file values
lowlevelmetal Oct 9, 2026
ef670f6
Centre shortcut ends on their room as aiIntersection::CreateRoadMap does
lowlevelmetal Oct 9, 2026
7bcdb41
Update the audio manager twice a frame as GameLoop and asRoot do
lowlevelmetal Oct 9, 2026
d77b611
Start a cruise where mmGame::RespawnXYZ does
lowlevelmetal Oct 9, 2026
648e384
Record the round 3 conventions audit
lowlevelmetal Oct 9, 2026
89bcfbd
Keep the round 3 conventions changes within 110 columns
lowlevelmetal Oct 9, 2026
763bdca
Record the cruise-spawn audit
lowlevelmetal Oct 9, 2026
abf27b8
Make traffic lights props the cars knock over, as MM2's are
lowlevelmetal Oct 9, 2026
1fc480f
Merge the round 3 cruise-spawn audit
lowlevelmetal Oct 9, 2026
98b960b
Record round 3's audit of MM2's random streams
lowlevelmetal Oct 9, 2026
3798bb8
Hold the horn as mmGame::UpdateHorn last set it while the menu is up
lowlevelmetal Oct 9, 2026
289f28c
Record the round 3 frames audit
lowlevelmetal Oct 9, 2026
9f26930
Wrap the order audit's lines at 110 columns
lowlevelmetal Oct 9, 2026
53e1e5a
Record the round-3 order audit
lowlevelmetal Oct 9, 2026
9058cc9
Merge the round 3 frames audit
lowlevelmetal Oct 9, 2026
40a2d24
Merge the round 3 order audit
lowlevelmetal Oct 9, 2026
e50754a
Merge integration's cruise spawn onto MM2's one random stream
lowlevelmetal Oct 9, 2026
95500ee
Bring the audio, game-flow and session records in line with the order…
lowlevelmetal Oct 9, 2026
d4ed050
Merge integration's frames and order audits onto the one random stream
lowlevelmetal Oct 9, 2026
b06dd0f
Merge integration's order-audit record updates
lowlevelmetal Oct 9, 2026
135cb26
Record round 3's results in the parity README
lowlevelmetal Oct 9, 2026
2960cc6
Move testBoundPolyTerrain's edge and vertex state to namespace scope
lowlevelmetal Oct 9, 2026
b0a6e24
Say in the README that OpenMM2 is playable
lowlevelmetal Oct 9, 2026
fed00f7
Work out the game-flow tests' plane probe without a FlatGround
lowlevelmetal Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ of that, OpenMM2 adds:
- online multiplayer over UDP with automatic port forwarding (UPnP)
- a Windows installer, and native Linux builds

> **Status:** early development. OpenMM2 reads the original game data and its
> subsystems are being built up; it is not playable yet.
> **Status:** early but playable. London and San Francisco, cruise, Blitz,
> checkpoint and circuit races, the crash courses, Cops and Robbers and online
> multiplayer all run from the original game data. Behaviour is checked
> against the original game function by function; the results, and the gaps
> still open (mostly in network play), are in [docs/parity/](docs/parity/README.md).

## You need the original game

Expand Down
16 changes: 12 additions & 4 deletions docs/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ Poles (`aiTrafficLightInstance::Init`, `World::signals()`): at
`trafficLightAxis` (which points away from the road on retail data), Z =
(-x.z, 0, x.x), so the glows on +Z face the arriving traffic and the SF
models' arms (-X) reach over the road. The dual model for approaches with
two or more lanes. `DrawGlow` draws the light's glow together with the
pedestrian signal (WALK in state 4, NOWALK otherwise) and only when the
model has both; NIGHT glows from the evening on (time of day > 1). Stop
signs are static instances in `city/<map>_ai.inst`, drawn with the city.
two or more lanes. Each pole is an unhit Y banger of its model's banger
data (`aiTrafficLightInstance` derives from `dgUnhitYBangerInstance`): its
frame (GetMatrix) is the base + R * CG, at which the CG-centred body is
drawn; `SetFourWay` puts it in the room of its CG (GetPosition). Cars
collide with it as with any prop and knock it over into its BREAKnn parts
(`BangerSet`; the parts are ordinary hit bangers), after which neither its
body nor its glows are drawn; the lights keep cycling. `DrawGlow` draws
the light's glow together with the pedestrian signal (WALK in state 4,
NOWALK otherwise), modelled from the base (GetMatrix less R * CG), and
only when the model has both; NIGHT glows from the evening on (time of
day > 1). Stop signs are static instances in `city/<map>_ai.inst`, drawn
with the city.

## Ambient traffic (`Traffic`, `AmbientRoute`)

Expand Down
4 changes: 2 additions & 2 deletions docs/audio.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ a time. Ported from MM2 `Aud3DObject` and `Aud3DObjectManager`:
| Pre-race: r = RandomizeNumber(11.5); ≤ 7.5 a random row of the mode's pre-race range, ≤ 8.5 time of day, ≤ 9.5 weather (nothing if that table was not loaded), else the car's line (else the mode's); it starts 1.5 s later | MM2 (`mmRaceSpeech::PlayPreRace`) |
| Results: 1st win; a place above half the field mid; else poor. Win/mid use the car's line when r > 5 of 10, poor when r > 8; without a "mid" table (blitz) nothing is said | MM2 (`PlayResults`, `PlayResultsWin` / `Mid` / `Poor`) |
| Final checkpoint, final lap, damage penalty, race progress, texture unlock and results stop the line playing and empty the queue; unlock-race and unlock-vehicle lines wait 0.1 s in the queue. The queue starts with one slot and gains one for every unlock-vehicle line loaded (and every unlock header after the first in a table); a play that finds no free slot is dropped; the first due play starts once nothing is playing | MM2 (`AudSpeech::Play`, `PutInQueue`, `Update`, `Stop`, `AllocateQueuePlayData`) |
| The queue is updated twice a frame while the game runs (GameLoop's `AudManager::Update`, then `mmGame::Update`), so its delays pass at twice the clock's speed: the pre-race line starts 0.75 s after it is queued, not 1.5 s. While the game is paused only `mmGame::Update`'s update is left: the queue counts at the clock's speed and a due line starts during the pause (after the pause has stopped every sound, below) | MM2 (`AudManager::Update`, `mmGame::Update`, `mmSpeechContainer::Update`); `AudioManager` |
| The queue is updated three times a frame while the game runs (`AudManager::Update` twice, by GameLoop and again as asRoot's first node, then `mmGame::Update`), so its delays pass at three times the clock's speed: the pre-race line starts 0.5 s after it is queued, not 1.5 s. While the game is paused only `mmGame::Update`'s update is left: the queue counts at the clock's speed and a due line starts during the pause (after the pause has stopped every sound, below) | MM2 (`AudManager::Update`, `mmGame::Update`, `mmSpeechContainer::Update`); `AudioManager` |
| What calls them: the pre-race line at the start (`mmGame::Reset`), the final checkpoint when a checkpoint race has only the finish left and when a circuit's final lap reaches its last checkpoint (`mmWaypoints::Update`, the race's FinalCheckpoint event), the damage penalty and blitz results (`mmSingleBlitz::UpdateGame`), the results and unlock lines (`mmGameSingle::UpdateRewards`). Nothing in build 3393 calls the final lap, race progress or unlock-race lines (`mmRaceSpeech::PlayFinalLap`, `PlayRaceProgress`, `PlayUnlockRace`), the crash course's unlock line or the Cops & Robbers commentary (`mmCNRSpeech::Play`): their tables load but are never heard, and OpenMM2 does not play them either | MM2 (callers of the Play methods) |
| File names `<announcer><prefix>NN` | verified against the sample names |
| Cops & Robbers: `bullshit.csv` in SF, `cnrlondon.csv` elsewhere; an event draws one of its rows, cuts the line playing and starts after 0.01 s; a fourth "num used" column smaller than the row's range picks a random window of that many lines. The London rows' names end in a space ("AL1\AL1ROBROB "), which MM2 keeps, so those files are never found and London's Cops & Robbers commentary is silent | MM2 (`mmSpeechContainer::InitCNR`, `mmCNRSpeech::LoadGroup`, `SetReadState`, `Play`); the missing files **inferred** from `AudStream::PlayOnce`'s path |
Expand Down Expand Up @@ -236,7 +236,7 @@ a time. Ported from MM2 `Aud3DObject` and `Aud3DObjectManager`:

| Behaviour | Evidence |
|---|---|
| While the game is paused (the popup menu in single player, the full-screen map) every wave sound stops, on the first two paused frames: engines, skids, sirens, ambience, rain, voices and the announcer's line. Loops start again from the beginning once the game runs on, as their owners find them stopped; one-shots are cut. The music (DirectMusic) plays on; the popup switches it to its pause segment (`mmPopup::PlayPauseMusic`) | MM2 (`AudManager::Update`, `AudManagerBase::UpdatePaused` with its counter limit of 1, `StopAllSounds`); `audio/game/AudioManager.h`, the race's hook at the start of its frame |
| While the game is paused (the popup menu in single player, the full-screen map) every wave sound stops, on the first two paused updates (both in the first paused frame, `AudManager::Update` running twice a frame): engines, skids, sirens, ambience, rain, voices and the announcer's line. Loops start again from the beginning once the game runs on, as their owners find them stopped; one-shots are cut. The music (DirectMusic) plays on; the popup switches it to its pause segment (`mmPopup::PlayPauseMusic`) | MM2 (`AudManager::Update`, `AudManagerBase::UpdatePaused` with its counter limit of 1, `StopAllSounds`); `audio/game/AudioManager.h`, the race's hook at the start of its frame |

## Not implemented

Expand Down
9 changes: 6 additions & 3 deletions docs/bangers.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@ name. A mass of 0 or less becomes 1 in OpenMM2 (no retail file has one).
xrefs place (lvlLevel::LoadInstances, the "xrefs" chunk read by
lvlInstance::EndGeom): each xref's matrix times the record's, dropped
when a row is zero or two rows have a dot product above 0.01, rows
outside 0.97..1.03 squared length normalised, kept as a full matrix with
the record's variant, in the room FindRoomId finds from the record's
room. An xref whose model has no banger data is not placed at all (MM2
outside 0.97..1.03 squared length normalised, then requested as Y
bangers (RequestBanger(name, 0), dgUnhitYBangerInstance: only the turn
about Y is kept, the CG offset is turned by the whole matrix) with the
record's variant, in the room FindRoomId finds from the record's room.
68 retail xrefs were exported Z up (awnings, the Chinatown sign, the
bridge and tower lights): MM2 stands them upright. An xref whose model has no banger data is not placed at all (MM2
reports it as not exported): cl10's trees. 33 retail models have xrefs
(tower and bridge lights, awnings, doors, windows): 65 bangers in London,
368 in San Francisco. They are not drawn with their parent model.
Expand Down
10 changes: 6 additions & 4 deletions docs/formats/bai.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ for its end-1 and its end-0 intersection, in that order):
* each end intersection lists it last, then sorts its list by
`atan2(dx, dz)` from its centre to each road's far end (the road's last
centre vertex when its end 0 is the intersection, else its first),
smallest first, and rewrites every listed road's index there. MM2 first
moves the centre to the bound-sphere centre of the intersection's room;
OpenMM2 keeps the file's centre (**inferred**: every main-file list is
already sorted round it);
smallest first, and rewrites every listed road's index there. First the
intersection's centre moves to the bound-sphere centre of its room
(`cityLevel::GetBoundSphere`: the room perimeter's area centroid at the
mean height of its corners); the file's centres of these intersections are
already within 0.21 m of it (66 in San Francisco), and `mmGame::RespawnXYZ`
starts a cruise at the moved centre;
* `aiMap::MapRoadToRooms(road, 2)` lists it, as component type 2, for the
rooms of its centre vertices 1 to n - 2 and for every room its two curbs
cross, sampled trunc(section length) times per section (the second
Expand Down
8 changes: 4 additions & 4 deletions docs/formats/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ Parsers: `src/city/Environment.{h,cpp}`; loaded by `loadCity`.
|------|--------|---------|
| `<map>.lt00`..`.lt15` | Angel "type: a" | Lighting per time of day and weather. Index = timeOfDay × 4 + weather (time: morning, noon, evening, night; weather: clear, cloudy, foggy, rainy; verified from the block names). Fields `KeyHeading/KeyPitch/KeyColor`, `Fill1*`, `Fill2*` (radians, 0..1 colours), `Ambient` (signed int, ARGB). |
| `<map>_fog.csv` | CSV | 16 rows in the same order: fog colour (0..255), start, end. The description column is ignored per its header. |
| `<map>.sky` | text | `model a b c` (`sky_dome_l 0 0.95 0.005`). Parameter meanings unknown. |
| `<map>.sky` | text | `model a b c` (`sky_dome_l 0 0.95 0.005`), read by `lvlSky::AutoInit` (`%s %f %f %f`): the dome sits at the camera with its height = camera height x b + a (`lvlSky::DrawHat`) and turns about +Y at c radians per second (`lvlSky::Update`). |
| `<map>.water` | text | water plane height, then room ids (inferred: rooms that use it). |
| `<map>.ext` | text | `minX minZ maxX maxZ`, a world rectangle inside the PSDL bounds (inferred: HUD map extent). |
| `<map>.reset` | text | `x y z # comment` recovery points ("GG fallthru" = Golden Gate fall-through). |
| `<map>.lmap` | binary | `"LMP0"`, `u32 n`, `u32 argb[n]`. London: n = rooms - 1, entry 0 = 0xCDCDCDCD. SF: n = 1,125 for 1,171 rooms. Likely per-room ambient colours (mm2hook `lvlRoomInfo::Color`). Indexing is unconfirmed and SF's count does not match its PSDL. |
| `<map>.ext` | text | `minX minZ maxX maxZ`, a world rectangle inside the PSDL bounds. midtown2.exe has no reader for it (an editor file). |
| `<map>.reset` | text | `x y z # comment` recovery points ("GG fallthru" = Golden Gate fall-through). midtown2.exe has no reader for it: the game respawns at checkpoints or intersections instead. |
| `<map>.lmap` | binary | `"LMP0"`, `u32 n`, `u32 argb[n]`: one colour per room id (entry 0, the dummy room, is 0xCDCDCDCD). `cityLevel::Load` takes it only when n equals the PSDL's room count (dummy room 0 included); then room i gets argb[i] (`lvlRoomInfo` +0x1c, the street colour and the instance light tint), else every room is white. Neither retail file matches (London 1,341 for 1,342, SF 1,125 for 1,172), so the retail game draws every room white. |
| `materials.mtl` | text | Physics materials: `mtl name { elasticity: f friction: f effect: s sound: n drag: f width: f height: f depth: f ptxindex: a b ptxthreshold: a b }`. |
| `materials.csv` | CSV | `texture,physics`: texture name → material name (`none`, `cobblestone`, `grass`, `water`, `deepwater`, `sand`, `mud`, `ash`). |

Expand Down
8 changes: 4 additions & 4 deletions docs/formats/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ rectangle.
MM2 stores these images the other way round: its TGA, BMP and JPEG readers
all put the picture's top row first (only `.tex` files are copied in file
order). Its 2D drawing blits them upright, as OpenMM2's UI does. On a 3D mesh
MM2's v = 0 is the top of a TGA picture, while OpenMM2 uploads row 0 (the
bottom) as v = 0, so a TGA on a mesh is upside down relative to MM2 unless
the renderer flips it. The only retail case is the HUD map dots, which are
symmetric.
MM2's v = 0 is the first stored row, so the top of a TGA, BMP or JPEG picture
and the bottom of a `.tex` one: `game::TextureLibrary::readImage` flips
TGA, BMP and JPEG images to the top row first before it uploads them, so
meshes sample every format as MM2 does.
21 changes: 18 additions & 3 deletions docs/formats/race.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,21 @@ editor files or crash-course event lists.
Header plus one row per race: `Description`, then two blocks of 10 columns:
amateur then professional. Each block is `CarType, TimeofDay (0..3),
Weather (0..3), Opponents, Cops, Ambient (density 0..1), Peds (density),
NumLaps, TimeLimit (seconds), Difficulty`.
NumLaps, TimeLimit (seconds), Difficulty`. `Cops` is a count (0 to 8 in
the retail tables) that the race menu keeps as the cop density unchanged;
the police placement clamps it to 0..1 (all posts from 1 up).

### Waypoints: `<race>Nwaypoints.csv`

`x,y,z,a,radius,...`: checkpoint position, heading in degrees, checkpoint
radius. Some files label column 5 `poly count`. The remaining columns
(`frame rate`, `state changes`, `texture changes`, `msg`) are editor profiling
fields and are kept verbatim. Crash-course event lists, `copchase.csv` and
fields and are kept verbatim, except that column 6 is the waypoint's hit
flag. Heading h faces (sin h, 0, -cos h): 0 is -Z and 90 is +X. The game
turns the car at the start by h x -0.017453292 about +Y
(`mmWaypoints::GetStartAngle`); a heading of exactly 0 on a waypoint after
the first means "towards the next waypoint" (`mmWaypoints::LoadCSV`). A
radius of 0 means 15. Crash-course event lists, `copchase.csv` and
`multicopwaypoints.csv` use the same layout.

### Opponent lines: `*.opp`
Expand All @@ -55,6 +62,14 @@ CSV `x,y,z,brake,forward offset,side offset,target speed,speed start,side start`
Named `<race>-a-<n>.opp` (amateur) and `<race>-p-<n>.opp` (professional) and
referenced from the `[Opponent]` section of the `.aimap`.

MM2's `aiRouteRacer::Init` reads only the first four numbers of a row
(`atof`). The first row is the racer's grid place: its fourth column (headed
`brake`) is the start angle in degrees, stored as the car's reset rotation
times 0.017444445 (MM2's own degree factor) and, unlike a waypoint heading,
not negated: 90 faces -X. The rows in between become route waypoints (the
first intersection of the room each point lies in) and the last row is the
destination. The other columns are not read.

### AI setup: `*.aimap`, `*.aimap_p`

`[Section]` headers, `#` comments. A section is a *list* when its first line is
Expand All @@ -71,7 +86,7 @@ reproduces it for every retail file.
| Ambients Drive On The Left | value | 0/1 |
| Traffic Lights | value | two model names |
| Exceptions | list | `road density speedLimit` per AI path id |
| Police | list | `car x y z heading mode lane ? ?` (the file's comment header is stale) |
| Police | list | `car x y z heading mode behaviours chance range` (the file's comment header is stale): heading in degrees, stored as the reset rotation heading x -0.017444445 (`aiRaceData::aiRaceData`); `mode` is read and never used; `behaviours` (default 15) are the apprehend bits; `chance` (default 0.5) and `range` (default 50 m) govern pursuing opponents |
| Opponent | list | `car oppFile <numbers>` |
| Ambient Types/Density | list | `model cumulativeProbability ?` |
| GoodWeatherPedName / BadWeatherPedName | list | `goodModel badModel` |
Expand Down
38 changes: 34 additions & 4 deletions docs/parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ record's summary counts them:
| phys-bounds | 159 | 101 | 36 | 13 | 1 | 0 | 8 |
| vehicle | 141 | 76 | 42 | 6 | 2 | 1 | 14 |
| ai-vehicles | 170 | 71 | 67 | 3 | 2 | 1 | 26 |
| ai-ambient-city | 236 | 116 | 61 | 13 | 22 | 4 | 20 |
| session | 205 | 89 | 86 | 6 | 4 | 1 | 19 |
| ai-ambient-city | 236 | 116 | 63 | 11 | 22 | 4 | 20 |
| session | 205 | 87 | 89 | 5 | 4 | 1 | 19 |
| camera-props | 195 | 107 | 57 | 11 | 2 | 0 | 18 |
| rendering-fx | 197 | 67 | 95 | 8 | 3 | 4 | 20 |
| audio | 265 | 69 | 155 | 11 | 10 | 0 | 20 |
| frontend-ui | 151 | 80 | 39 | 11 | 3 | 2 | 16 |
| formats | 119 | 23 | 44 | 11 | 7 | 0 | 34 |
| OpenMM2-only | 34 | 6 | 8 | 5 | 4 | 1 | 10 |
| **Total** | **2078** | **880** | **786** | **107** | **70** | **15** | **220** |
| **Total** | **2078** | **878** | **791** | **104** | **70** | **15** | **220** |

The open rows and each record's "Missing" table list what still differs
from MM2 and what porting it needs. The larger missing features are the
Expand All @@ -105,7 +105,7 @@ placement.
| Subsystem | Record | Reachable | Ported (new) | Replaced | Not needed | Open |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| world objects | [world-objects.md](mm2/world-objects.md) | 297 | 187 (124) | 30 | 75 | 5 |
| game flow and spawns | [game-flow.md](mm2/game-flow.md) | 608 | 382 (62) | 66 | 151 | 9 |
| game flow and spawns | [game-flow.md](mm2/game-flow.md) | 608 | 382 (62) | 65 | 152 | 9 |
| AI | [ai.md](mm2/ai.md) | 507 | 409 (17) | 6 | 92 | 0 |
| vehicle and physics | [vehicle-physics.md](mm2/vehicle-physics.md) | 736 | 574 (0) | 13 | 146 | 3 |
| props and effects | [props-fx.md](mm2/props-fx.md) | 169 | 126 (1) | 0 | 43 | 0 |
Expand All @@ -127,6 +127,36 @@ record's Open section; the largest are late joins for network cruise and
Cops and Robbers, the network tuning checksum, the water respawn sync and
remote cars' sounds and damage.

## Round 3: cross-cutting themes (2026-10-13)

Two differences the maintainer saw after both audits (traffic lights out of
line with their glows, and where cruise mode starts) were classes of bug
that a function-by-function audit misses, because each function looked
right on its own. Round 3 followed data across systems, one theme at a
time, with records under [round3/](round3/):

| Theme | Record | Cases | Verified | Fixed | Deviation | Inferred | Open | Other |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| frames: where models, parts, glows and shadows are drawn | [frames.md](round3/frames.md) | 68 | 56 | 6 | 2 | 1 | 0 | 3 no retail data |
| the cruise start (`mmGame::RespawnXYZ`) | [cruise-spawn.md](round3/cruise-spawn.md) | 22 | 5 | 11 | 4 | 1 | 0 | 1 openmm2 |
| MM2's global random stream and its consumers | [random-streams.md](round3/random-streams.md) | 41 | 7 | 16 | 12 | 0 | 0 | 6 not needed |
| init, reset, update and draw order | [order.md](round3/order.md) | 51 | 35 | 10 | 5 | 0 | 1 | |
| units, signs, axes and conventions in data | [conventions.md](round3/conventions.md) | 104 | 83 | 8 | 6 | 3 | 0 | 4 openmm2 |
| **Total** | | **286** | **186** | **51** | **29** | **5** | **1** | **14** |

The fixes that show most: traffic lights stand on their CG as
`aiTrafficLightInstance` does (the poles were 3.9 m in the ground under
correctly placed glows) and are props cars knock over; cruise starts where
MM2 does, from the one global stream right after `aiMap::Reset` (London
intersection 19 at Marble Arch, San Francisco intersection 121 on the
Embarcadero, with the cruise menu's default densities); a race is set up
from that stream in `mmGame::Init`'s order; semi trailers, knocked traffic
cars' wheels and shadows, and police headlight beams are drawn as MM2 draws
them; a restart happens at the start of the next frame; a paused game keeps
its camera, sky, rain and HUD message going; 68 PKG xref props stand
upright; the player's steering tune comes from `tune/<car>.asnode`. The one
open case is F2's pause (OpenMM2's F2 is the fly camera).

## Manifest

Lines as of the start of the audit (files the audit added: as of when
Expand Down
Loading
Loading