Pure module game/sim/techgraph: PrereqsMet (AND of ORs, empty group fails),
SetResearched (stamps, child-cost sweep, sticky turnAvailable, zero-cost
recursion) and the newly-available collector, all read out of 0x00581e10,
0x0057d8e0 and 0x00587cc3.
Wired into the B3 hook in compare mode only, over the scratch node copies:
four more node write-backs, the EVENT_TECHS_UNLOCKED list (still an input,
still nullptr when it could not be computed), the de-duplicating observed-tech
append and the one RNG word RollResearchEvent draws.
docs/U-unlock.md section 4 is the prediction, written before the build was staged.
The behavioural compare found 8 of 45 StrategyServer::MoveFleet calls diverging by
one ULP on a position component. Read off the instruction stream, the cause is that
the engine's vector normalise narrows to float32 four separate times and we kept
everything in double:
delta.c = f32(dest.c - pos.c) stored back to a float32 slot before normalising
sumsq = f32(x*x + y*y + z*z) products/adds in 53-bit regs, only the SUM stored
len = f32(sqrt(sumsq))
inv = f32(1.0 / len) a reciprocal, MULTIPLIED through, not three divides
dir.c = f32(delta.c * inv)
and the same call returns the leg distance, so it is never recomputed in a wider
precision either. The position tail was already right, which is why the error was a
constant absolute ~1.2e-7 (half an ULP of the inputs) rather than a formula error.
Adds NormalizeVec3 / StraightLeg / StraightLegDistance / AdvanceAlongUnitDirection
and rebuilds AdvanceAlongDirection on them; the movement hook now takes both the
direction and the distance from one StraightLeg call, as the original does. The
arrival test is an exact float compare, so the distance has to be that same float32.
Tests pin float32 BIT PATTERNS, not tolerances: one case per narrowing plus four
independent legs component by component. A CHECK_NEAR would pass against the old
arithmetic.
sim::Distance is left in double on purpose and flagged at its declaration: it now
serves only the node-line/stutter geometry, which very likely needs the same
treatment but has zero behavioural coverage to correct it against.
Live, same VM/save/workload, run twice by this lane:
control recap-7584bad-20260908T0615Z 45 calls, 45 compared, 8 diverged, exit 1
fixed mf-45bdf7d-dirty-20260908T0721Z 45 calls, 45 compared, 0 diverged, exit 0
with identical arguments, identical pos.before and identical ORIGINAL pos.after on
all 45 calls. The control reproduced the eight divergent call_ids exactly.
Coverage unchanged and still thin: all 15 moving calls are the same straight-run
waypoint type; types 2-5 were attempted and could not be reached (the only player
that would travel a node line has no ships on this save). See docs/M-movefleet.md.
ctest 32/32; tools/clean_room_check.sh OK.