Skip to content

Parity audit round 3: frames, cruise spawn, random streams, order, conventions - #1

Merged
lowlevelmetal merged 44 commits into
mainfrom
integration
Oct 9, 2026
Merged

lowlevelmetal merged 44 commits into
mainfrom
integration

Conversation

@lowlevelmetal

Copy link
Copy Markdown
Owner

Parity audit round 3: cross-cutting checks of OpenMM2 against MM2Recomp (midtown2.exe build 3393), organised by theme instead of function by function. Records are in docs/parity/round3/, and the results table is in docs/parity/README.md.

Fixes

  • Traffic lights: they stand on their CG, as aiTrafficLightInstance places them. The poles had been drawn 3.9 m in the ground under correctly placed glows. They are now props that cars knock over, as in MM2.
  • Cruise start: cruise starts where mmGame::RespawnXYZ puts it. The pick comes from MM2's one global random 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).
  • Race set-up: the race is set up from that stream in mmGame::Init's order (props, the player car, gizmos, aiMap::Init, the resets). Traffic and pedestrians share it.
  • Drawing: semi trailers, knocked traffic cars' wheels and shadows, police headlight sweeps and far pedestrians' stick figures are drawn as MM2 draws them. 68 PKG xref props stand upright.
  • Restart and pause: a restart happens at the start of the next frame. A paused game keeps its camera, sky, rain and HUD message going. AudManager::Update runs twice a frame, and the horn and steering behave as MM2 does with the menu up.
  • Conventions: the player's steering tune is loaded from tune/<car>.asnode; also the police heading factor, cop density and slider stepping.
  • Frontend script: gains time: for automated screenshots.

Testing

  • Full suite with retail game data: 803/803.
  • Suite without game data (as CI runs it): 803/803.
  • Opponent sweep: 516/517 finished, 508/517 across the line. The miss is SF race 11 pro, as in MM2.
  • Smoke screenshots of both cruise starts, SF and London at night, and a London Blitz.

🤖 Generated with Claude Code

lowlevelmetal and others added 30 commits October 8, 2026 19:14
aiTrafficLightInstance::Init places a traffic light at its pole's base
plus R * CG (the model's dgBangerData CG) and Draw draws the body, whose
mesh is centred on that point, at that frame; DrawGlow takes R * CG off
again for the glow and WALK meshes, which are modelled from the base.
OpenMM2 drew the body at the base, so every pole sat 3.9 m in the ground
under glows at the right height and the lights looked misplaced. The
body, visibility, room and draw distance now use the CG frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aiRaceData::aiRaceData stores a [Police] line's heading as heading x
-0.017444445, the same not-quite-pi/180 factor aiRouteRacer::Init uses
for the racers' first .opp row, and aiPoliceOfficer::Reset copies it to
the car's reset rotation. OpenMM2 converted it with pi / 180, turning a
post at 245 degrees about 0.12 degrees too far.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RaceMenuBase::SetStateRace (and the modes' next-race setup) store the
race table's Cops column, a count from 0 to 8, in the cop density as it
is. aiMap::Init clamps it to 0..1 when it places the posts, but the
modes' RegisterFinish compare the setting with the count, so once the
slider has been moved a race with two or more cops is not recorded.
OpenMM2 clamped the count when it applied the table, which made the
moved-and-restored slider count as the race's setting.

The slider steps from its own clamped value, as UISlider::Action steps
mmSlider's (SetValue clamped the bound variable when UISlider::Update
took it): Left from a count of 3 gives 1 - step, not 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the post turned by MM2's own degree factor, the point 70 m behind
London's roam post at (1110, -731) lies a few centimetres off the road,
so the tests found no post and failed. 75 m behind is on the road again;
the suspect is still out of the cop's view (behind it, at the edge of
DetectPerpetrator's 75 m).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aiVehicleInstance::Draw draws a traffic car that has a body
(aiVehicleActive) with WHL0-3 at its vehWheelCheaps' drawing matrices:
unturned, at the pivot moved along the car's up axis by the spring's
travel (Limit below the pivot off the ground) and back by 0.2 of the
sideways and 0.3 of the forward tyre deflection, carried by the body's
matrix; WHL4/5 unturned at their pivots raised by WHL2/3's drawn height
less the wheel radius. OpenMM2 kept turning the wheels at the rail's
pivots, so a car hit into the air kept its wheels tucked in.
aiVehicleInstance::DrawShadow lays such a car's shadow on the ground
under it (lvlInstance::DrawPhysics), and a rail car's on the ground when
it is upside down; OpenMM2 always left it at the body. TrafficBodies now
keeps the drawing matrices and AiRenderer gets a ground probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mmPlayer::Init names the player node after the car and loads it, so
mmPlayer::FileIO reads tune/<car>.asnode: the speed bases, the keyboard
and game pad filter rates (DiscreteSteering*), and the mouse, joystick
and wheel sensitivities, curves and approaches that mmPlayer::Update
blends by speed. Every retail car but vpcentury and vpdune ships one, and
they differ a lot from the constructor's values OpenMM2 used for every
car (vpbug: SpeedBaseHi 44.6 instead of 100, keyboard DeltaOut 2.573 ->
0.8 instead of 3.5 -> 2.5, DeltaIn 5 instead of 2.5 -> 1.5, joystick
approach on). Fields a file lacks keep the constructor's values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The xref loop of lvlLevel::LoadInstances requests each xref's banger
with RequestBanger(name, 0), a dgUnhitYBangerInstance, which keeps only
the X row's x and z of the placed matrix (its CG offset is still turned
by the whole matrix in dgUnhitBangerInstance::Init). OpenMM2 kept the
full matrix. 68 of the 177 retail xrefs were exported Z up, so San
Francisco's shop awnings, the Chinatown awnings and sign, Harrods'
awning and the Golden Gate, Bay Bridge and tower lights were drawn and
collided tipped 90 degrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dgBangerData::Load hands the nested block to asBirthRule::Load, which
skips the block's name, its brace and the first field's name and then
reads the rule by position. OpenMM2 only looked for a block named
BirthRule, so sp_tree1_s_break06, whose block is called asBirthRule,
lost its rule (InitialBlast 25, SpewRate 1.24) and threw no leaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vehTrailerInstance::Draw draws a trailer's body alone below the high
LOD; at H it adds TLIGHT while the tow car brakes harder than 0.1 (an
ordinary lit draw of the object pass, which its black transparent
fxltglowred material leaves invisible) and TWHL0-3 at the trailer's
wheel matrices. It has none of vehCarModel's other parts and keeps
lvlInstance's empty DrawGlow. OpenMM2 drew trailers as cars: the medium
and low TWHL0/1 meshes, which are modelled away from their pivots, at
the front wheels' matrices, a refl_dc reflection, texel damage, and
TLIGHT as an added glow when braking and at night.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ai::PlayerCar::at (mm2tool aisim --drive and the AI tests' player) built
its frame with rotationY(atan2(-x, -z) + pi). rotationY(a) already faces
(-sin a, 0, -cos a), so the extra half turn put the car's -Z axis
against its velocity, and the traffic saw the tool's player driving
backwards. The game's own player car comes from the simulation and was
not affected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pedAnimation::DrawSkeleton offsets each quad's corners in the
pedestrian's own space (the pedestrian's matrix is the world matrix) by
the first row of gfxRenderState's modelview: the pedestrian's X axis in
view space, MM2's view matrix being the camera's inverse with z negated
(sm_FullComposite). That is the camera's right axis while pedestrian and
camera only turn about Y, and tilts under a pitched camera. OpenMM2 used
the camera's right axis in world space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restart Race, F4 in the menu and F4 in the game only set
mmReplayManager's reset flag (+0x19); mmReplayManager::Update acts on it
at the start of the next frame, before any node of the game updates:
mmReplayManager::Reset, then the tree's Reset down to the mode's Reset,
so the first rules, AI and physics after a restart see the cars and the
session back at the start. F4 also ends a pause at once (asRoot::Reset).

OpenMM2 reset the session at once and the cars only from the Restart
event after that frame's physics, so the session's first update after a
restart saw the car where it had been: restarting at a checkpoint showed
it cleared in the new countdown. The race now restarts at the top of the
next frame (applyRestart), and the frame of the request takes the paused
path. The session's events are handled in one place (handleSessionEvents)
for the rules, the restart and later the paused frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aused

asRoot's pause only stops what checks it. mmGameManager::Update runs
cityLevel::Update (lvlSky::Update turns the sky), cityLevel::PreDraw
(asParticles::Update: the rain and snow) and camViewCS::Update (the
camera) in every frame; mmGame::Update skips UpdateGame and
aiMap::Update but still runs its fall and water checks, whose timer
counts datTimeManager::Seconds; and mmHUD::Update, a node of the game,
still counts the HUD message down.

OpenMM2's paused frame froze all of these: the camera stopped where it
was (a camera key pressed in the full-screen map changed nothing until
the game ran on), the rain and the sky stood still behind the menu, a
message outlived the pause and five seconds in the water could not run
out under the menu. The paused frame now updates the camera, the
weather and the city renderer, and Session::updatePaused runs the
hazard checks and the message timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MM2 lights instances with Direct3D 7's fixed function and never turns
on NORMALIZENORMALS (gfxRenderState::Init zeroes the state block and no
code writes that state). Direct3D takes normals through the inverse
transpose of the world matrix and does not renormalise them, so a
building or prop placed with a scaled .inst matrix is lit in
proportion to 1 / its scale along each axis: a petrol station at 0.88
is brighter, a house stretched 1.34 high has darker roofs. OpenMM2
renormalised the world normal and lit all of them at unit strength.
About 1,500 of London's 1,997 and 2,400 of San Francisco's 3,763
instances carry a scale. Rigid matrices (cars, gizmos) are unaffected.
The Direct3D rule is inferred; it is outside midtown2.exe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mmGame::UpdatePaused (the C and V keys whatever the camera actions are
bound to) sits in the game's UpdatePaused slot, which nothing calls in
build 3393: asNode::UpdatePaused is only reached from its own recursion
and AudManager's, and mmReplayManager::Update runs the tree with
asNode::Update whether or not asRoot is paused. A paused game takes its
keys through mmGame::Update's UpdateGameInput instead, which OpenMM2
already runs while paused; the extra raw C and V keys are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the menu up and the game not paused (a network game, a race lost,
the chat line), mmGame::Update skips UpdateDebugInput and
UpdateGameInput but still calls UpdateSteeringBrakes when inputDevice is
above 0, so every controller but the mouse keeps driving the car from
the inputs mmReplayManager::Update recorded that frame; with the mouse
nothing sets the car's inputs and it keeps the last ones. OpenMM2 let
go of every pedal and the wheel instead. The recorded inputs are now
read whatever the menu, and the mouse controller keeps the car's last
ones while the menu is up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vehCarModel::DrawHeadlights keeps its two ltLights' directions in world
space: without the siren it sets them to the car's forward axis, with it
it turns each about Y by +-42.411503 rad/s of the frame's time from
wherever it points (Vector3::RotateY). OpenMM2 turned the car's forward
axis by the siren's accumulated angle, so the sweep followed the car's
turning and jumped each time that angle wrapped at 2 pi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MM2 has one global irand/frand seed. aiMap::Init draws for the ambient
pool (two constructor draws per car, then the type, SetColor twice and
aiGoalRandomDrive per car) and then for the pedestrians (type, then
aiPedestrian::Init's variant) from wherever the race's set-up left it;
aiMap::Reset sets it to 1 and places the traffic (AdjustAmbients) and then
the pedestrians (AdjustPedestrians) before anything updates.

OpenMM2 gave the pedestrians a stream of their own (seed x 7919 + 1), so
their clothes, placement and choices never matched MM2's. ai::World now
draws both from one stream: the caller's (ai::Settings::random, MM2's
global stream as aiMap::Init finds it) or its own, which reset() sets back
to its seed. The first step after a reset populates the traffic and then
the pedestrians before either updates (Traffic::populate,
Pedestrians::populate), as aiMap::Reset orders them.

The ambient array is built (and draws) at traffic density 0 too: MM2
constructs the state's vehicle count before it looks at the density.

AiWorld.DeterministicAndWellBehaved now exempts a car that committed to
an intersection (enterInt) before its light turned red: with the new
draws one such car waits in a queue and turns 2.07 s into the red, which
MM2's committed cars do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MM2 draws a race's set-up from its one global seed in a fixed order, so
what it decides is the same every time in a city: cityLevel::Load sets the
seed to 1 before each road's street props, leaving it as the last road's
walk does (1 in London, whose last road has no props); mmPlayer::Init's
vehCar::Init draws vehSiren::Init's lens flare (20 flares, six frand
each) and vehSplash::Init's 64 random directions (three each, then
overwritten); mmGame::InitGizmos the sailboats' paint and speed, the
ferries' speed and the parked cars' choice and paint; aiMap::Init each
racer's vehCar::Init, the ambient pool, the pedestrians, the cable cars
(one draw each for the array's aiRailSet, then aiCableCar::Init's) and
each police car's vehCar::Init.

OpenMM2 started the gizmos at seed 1, the traffic at seed 1, the
pedestrians at their own seed and the siren flares on a stream of their
own, so the traffic's models and paints, the pedestrians' clothes, the
parked cars and the boats all differed from MM2's. RaceScreen now keeps
one stream (ai::Random) through loadWorldObjects (props, player car,
gizmos, created before the AI now), loadAi (the racers' cars, then the
AI world) and loadEffects (the cable cars, then the police cars), and
hands it to the AI world, whose reset sets it to 1.

- placeStreetProps / placeCityProps report the stream's state after the
  last road; roads without sidewalks are walked too (they draw, but
  nothing stands there: still 10,184 street props).
- CableCars::create takes the array constructors' draws first.
- takeVehCarInitDraws takes vehCar::Init's 312 draws; the player's, the
  racers' and the police cars' siren flares come from their place in the
  stream (VehicleRenderer::setSirenFlares).
- game::fx::Rand is now ai::Random, one class for the generator; the
  particles and force feedback keep their old default seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The waypoint heading convention (0 is -Z, 90 is +X, the start turned by
heading x -0.017453292), the .opp grid row's angle (x 0.017444445, not
negated) and which .opp columns aiRouteRacer reads, the [Police]
columns, and the race tables' cop count. The .sky parameters (dome
height offset and scale, turn rate), why neither retail .lmap is used
(its count is checked against lvlSDL's room count, dummy room 0
included: London 1341 for 1342, SF 1125 for 1172), that .ext and .reset
have no reader in midtown2.exe, and that TGA, BMP and JPEG textures are
now flipped to MM2's row order before upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aiMap::ReadBinary links each <city>_sup.bai shortcut road into its two end
intersections and calls aiIntersection::CreateRoadMap on them, which first
moves the intersection's centre to its room's bound-sphere centre
(cityLevel::GetBoundSphere: the perimeter's area centroid at the mean
corner height) and sorts the road list round that. OpenMM2 kept the file's
centre. The file's centres were within 0.21 m of it, but
mmGame::RespawnXYZ starts a cruise at an intersection's centre, so a
shortcut end must sit where MM2 has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GameLoop calls AudManager::Update before asRoot::Update, and BeginPhase
adds the same manager to asRoot as its first node, so asRoot::Update
calls it again: twice a frame. While the game runs each call updates the
announcer's queue (mmSpeechContainer::Update) and mmGame::Update does a
third time, so the queue's delays pass at three times the clock (the
pre-race line starts 0.5 s after it is queued, not 0.75 s); while it is
paused AudManagerBase::UpdatePaused's two stops both come in the first
paused frame.

OpenMM2 called the manager once a frame. AudioManager::updateFrame makes
both calls and the race uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MM2 picks the cruise start in the mode's InitOtherPlayers, which
mmGame::Init runs after aiMap::Reset. aiMap::Reset sets the one global
random seed to 1 and populates the roads of the player car's room, the car
still at mmSingleRoam::InitGameObjects' place (mmGame's (0, 10, 0) plus its
CG): with the cruise menu's densities that is 73 link choices for the
ambient cars and 4 draws for each of 25 pedestrians in London (room 65),
21 in San Francisco (room 265). RespawnXYZ then draws (counter + 1)
numbers irand() % (n - 1) + 1 and takes the last, again until the
intersection fits: its room, FindRoomId of its centre, neither water of
death nor a terrain instance's (0x24) nor subterranean or covered (0x0A),
no freeway or alley road; the start is the centre 2 m up, angle 0. So the
start is the same every time: London intersection 19 by Marble Arch, San
Francisco intersection 121 on the Embarcadero.

OpenMM2 picked a random intersection at race setup with a glibc-style
generator seeded from the clock, and judged rooms by the .bai's room
field (San Francisco's intersection 187 lies in a terrain instance's room
that RespawnXYZ rejects). Now loadRaceSetup leaves the car at the
InitGameObjects place, ai::World::globalSeedAfterReset replays
aiMap::Reset's draws on one stream, and Session::placeRespawnStart picks
the start with MM2's generator, rules and counter. Multiplayer cruise and
Cops and Robbers draw twice from the player's own seed (mmMultiRoam /
mmMultiCR::Reset, then InitNetworkPlayers) and advance mmGame's run-long
counter, which also changes later single-player picks, as in MM2. The Cops
and Robbers picker uses the same port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every value that crosses from MM2's data files and code into OpenMM2,
followed from MM2's reader to its uses and compared with OpenMM2's
parser and use sites: the race files and spawns, the city files, the
tune files, audio units, the HUD, frontend and option values, the
renderer's conventions and the frames and angles derived in code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 3's cross-cutting check of the cruise start: how mmGame::Init,
aiMap::Reset and mmGame::RespawnXYZ pick it in single player, multiplayer
cruise and Cops and Robbers, the draws aiMap::Reset makes first (by city
and density), the placement chain, and the London and San Francisco
starts with what stands there. The session and game-flow records point
their RespawnXYZ rows at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aiTrafficLightInstance derives from dgUnhitYBangerInstance: Init gives it
its model's banger data and bound and stands it at the pole's base + R *
CG, and aiTrafficLightSet::SetFourWay moves it into the room of that CG
(flags 0x13: a collidable banger). Cars collide with it like any prop and
break it loose into its BREAKnn parts (dgUnhitBangerInstance::Impact);
then it leaves the rooms, so neither its body nor its glows are drawn,
and the parts are ordinary hit bangers. OpenMM2 only drew the poles, so
cars drove through them. The poles now join the BangerSet (in aiMap::
Init's place, before the cable cars, so pedestrians step round them as
aiPath::AddBangersToObsMap lists them); AiRenderer keeps drawing them
with their signals from the props' frames while they stand
(PlacedProp::ownerDrawn).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session record keeps the conventions audit's police row and the
cruise-spawn audit's player start row; the counts combine both, and the
manifest's results table follows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/parity/round3/random-streams.md lists every consumer of MM2's global
irand/frand stream (and of its secondary seed) with the order of draws from
cityLevel::Load's street props to aiMap::Reset at a race start and a
restart, OpenMM2's counterpart and a verdict; the records the fixes touch
(ai-ambient-city, camera-props, vehicle, rendering-fx, the MM2-side
infrastructure and world-objects) now point to it. San Francisco's parked
cars from MM2's stream are pinned too (102).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lowlevelmetal and others added 14 commits October 8, 2026 20:00
mmGame::UpdateHorn opens mmGame::UpdateGameInput, so with the menu up
(neither runs) the horn keeps the state it had: a horn held when the menu
opened over a running game honks on, and one pressed under the menu does
not sound. OpenMM2 read the horn key every frame. The race now keeps the
held state (mmGame +0x274) from its own UpdateGameInput, which still runs
while the game is paused without the menu, as MM2's does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity/round3/frames.md lists every model, part, glow, shadow, decal,
particle emitter and light OpenMM2 draws or positions against the frame
MM2 uses for it (GetMatrix / GetPosition, CG frame or model origin, part
pivots, xrefs, multiplication order and signs), with the fixes of this
round and the screenshots checked. The audits' own rows for the functions
the fixes touched point at it, and the trailer's tail light is marked as
inferred invisible (its material under the object pass's alpha test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity/round3/order.md: MM2's tree and the call order of the load
(mmGame::Init, the modes' Init, mmGameManager's Reset), the restart
(mmReplayManager's reset flag and the modes' Reset), the frame
(GameLoop, asRoot, mmReplayManager, mmGameManager, mmGame::Update), the
paused frame, the menu over a running game, the draw order
(asCullManager) and the end of a race, side by side with RaceScreen,
with each difference's verdict.

Co-Authored-By: Claude Opus 5.5 <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 cruise-spawn work picked the start from a replay of aiMap::Reset's
population on a stream of its own (World::globalSeedAfterReset,
Traffic/Pedestrians::replayResetPopulation). With the traffic and the
pedestrians now drawing from MM2's global stream itself, that replay is
redundant: RaceScreen::placeRespawnStart calls World::resetAndPopulate
(aiMap::Reset of mmGame::Init: seed 1, then AdjustAmbients and
AdjustPedestrians round the car's InitGameObjects place at once), takes the
start from the shared stream as it stands (mmGame::RespawnXYZ), and resets
the world again (mmGame::Reset's aiMap::Reset from mmGameManager's
constructor), so the first step places the traffic and pedestrians round the
start from seed 1, as MM2's second reset does. The draws before the first
reset do not reach it (it starts from seed 1): the starts stay London 19
and San Francisco 121.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… audit

AudManager::Update runs twice a frame, so the announcer's queue counts
three times a frame and the pre-race line starts 0.5 s after it is queued.
mmGame::UpdatePaused is never called in build 3393 and moves from ported
to not needed; the XCam row no longer mentions its C and V keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The traffic light props (frames audit) join aiMap::Init's steps without
moving MM2's stream: nothing in aiTrafficLightSet::SetFourWay or
aiTrafficLightInstance::Init reaches irand or frand (only their DrawGlow,
as the glows are drawn). The restart at the start of the next frame (order
audit) reaches ai::World::reset once, so mmGame::Reset's aiMap::Reset sets
the stream to 1 once per restart before the next step places the traffic
and pedestrians round the start.

Both round 3 records now say why the shared stream leaves the cruise starts
where the cruise-spawn work put them (London 19, San Francisco 121):
aiMap::Reset starts with ResetRandomSeed, so the set-up's earlier draws
cannot reach RespawnXYZ, and the population's draw count does not depend
on the types and clothes they chose; a new retail test checks it on the
whole set-up. The cruise-spawn record's open row (the start's traffic) is
closed: mmGameManager's constructor ends with mmGameManager::Reset, whose
mmGame::Reset resets aiMap again from seed 1 round the start, as OpenMM2
now does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five cross-cutting themes (frames, cruise spawn, random streams,
order, conventions) in one table with what they fixed; ai-ambient-city's
row moves two random-stream rows from deviation to fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MSVC 14.51 cannot default-construct a function-local class with default
member initializers inside std::vector (C2672 in std::construct_at for
std::vector<VertexSweep>(n)), which has failed the Windows build on main
since 32903d6. No change in behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
London and San Francisco, cruise, the races, the crash courses, Cops and
Robbers and online multiplayer all run; point at docs/parity for how
closely each part follows the original and what is still open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MSVC 14.51's optimiser crashed (C1001, access violation) on the
phys::FlatGround made inside planeProbe's lambda. The probe now does
FlatGround::probe's arithmetic itself; the tests are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lowlevelmetal
lowlevelmetal merged commit fed00f7 into main Oct 9, 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