Extended mission comparisons
This pass starts at native commit 8925045. It combines coherent authored mission launches with player controls, extends several runs, and checks native execution with AddressSanitizer and UndefinedBehaviorSanitizer. Analysis changes remain uncommitted; native fixes and regression fixtures are committed in earthsiege-cpp.
What runs
The simulator receives original GO handoff files, initial RNG state, a frame ratio and a repeatable sequence of player inputs. Original instructions run first to generate expected results. Native code receives no expected actor, mission, random or damage state.
The sequence exercises movement, torso aiming, weapons, target changes, auto-tracking, weapon pairing and interface commands. It keeps each mission's actual factions, waiting groups, triggers and objectives. The checks compare HERCs, flyers, buildings, controllers, animation, damage, power, sensors, target relationships, counters, outcomes and shared RNG after every captured frame.
| Run | Launch seed / simulator warm-up | Ratio | Cases and consecutive frames |
|---|---|---|---|
| player-pilot-valid | 42 / 83 | 128 | Four missions, 256 each: 1,024 matching frames |
| player-all-450 | 42 / 83 | 450 | All 75 supported sources, 512 each: 38,400 frames; nine initial differences explained below |
| long-worlds | 42 / 131 | 256 | B0_C1_61, B0_C3_2A, B0_C5_70, neutral input, 2,048 each: 6,144 matching frames |
| initialized-aim | 42 / 83 | 450 | Two missions, 512 each, with the unwritten aim field initialized: all 1,024 frames match |
| corrected-offset | 42 / 83 | 450 | B0_C5_70, 512 frames, with one arithmetic instruction repaired: all match |
| player-all-128 | 17 / 131 | 128 | All 75 sources, 512 each: 38,400 matching frames after the ramming fix |
| player-career | Saved journey handoffs / 151 | 256 | Ten later-career starts, 1,024 each: 10,240 matching frames after the reverse-animation fix |
The reviewed sweep compares 752,640 HERC/flyer observations and 353,280 ground observations. player-all-450-reviewed.json indexes every selected reference and records its hash and any repairs. The two corrected-reference runs replace three cases in the 38,400-frame sweep; they are not additional unique mission runs. Full reports, raw captures, comparison fixtures and logs are under work/research/missions/autonomous-validation/.
Missing native player-disable transition
Six cases first differed when damage disabled the player's HERC: B0_C2_20, B0_C2_70, B0_C7_6A, SM_ASS01, SM_PTL02 and SM_SCT02. Original 13250 composes the camera-part transform with the actor placement, extracts its yaw, assigns that heading to the chassis, zeros the torso angle/channel and refreshes the pose. The transition preserves the cockpit's direction when the legs fail.
Native code had omitted that operation. Adding it makes all six complete 512-frame captures agree, including subsequent follower steering and collision damage. The calculation must use the evaluated camera matrix and original fixed-point angle extraction. Simply adding the torso angle misses the animation offsets and rounding. Elevation and angular rates are not directly reset. Construction damage does not execute this player-only transition before the controlled-player pointer is installed.
Native commit c35aeb5 contains the initial alignment fix; the reverse-playback case below completes its animation-time behavior. Three complete traces, 1,536 frames, are retained in mission_player_disabled_frames.bin. The other three remain in the analysis tree. rechecked-player-disable.json preserves the comparison against all 75 untouched captures without overwriting the initial failure report.
Reverse animation during disable
A later-career B0_C2_70 capture exposed a two-unit movement difference at frame 92. The argument 1 in the original call to 215c is an unscaled animation time, not a redraw flag. The body channel keeps its cached rate of -155, so signed Q8 scaling advances it by -1: elapsed 79 becomes 78, moving Y from -40,033 to -40,034 before the ordinary frame advance. The changed channel interpolation accounts for the second unit by the end of the frame.
The native disable path now advances and consumes root motion for that one unit before evaluating the pose (commit a6aac9a). It preserves the cached rate. All 1,024 frames of this later-career capture agree. The instruction trace is in disable-step-trace, and mission_disabled_reverse_frames.bin retains the full case.
Ramming damage uses a live centre
At B0_C5_50 frame 410, original 18278 passes the rammer's position by address to the area-damage loop at 1f75c. The rammer destroys itself during its own iteration and is immediately moved to Z -100,000. Subsequent distance checks therefore use that changed centre. The native implementation had copied the position, causing five armour sections of the nearby player HERC to receive damage the original never applied.
Keeping a reference to the live position reproduces the original order without any extra emulation state (commit a6aac9a). The original pointer and removal write are captured in ramming-trace/blast-centre.json. All 512 mission frames now match; mission_ramming_frames.bin retains the complete case.
Completed extended sweeps
After these fixes, saved-reference rechecks agree on all 75 sources at ratio 450, all 75 at ratio 128, and all ten later-career starts at ratio 256. The two source sweeps use different launch seeds and simulator warm-ups. Combined with the four-case pilot and three longer neutral-input runs, this is 94,208 world frames across 167 case/rate combinations. Corrected references remain explicit: every case in the second sweep and career run initializes the unwritten aim word and applies the multiply repair; the first sweep uses those only in its three identified affected cases. Formation and effect-queue repairs are also recorded.
The recheck helper preserves the original reports, including their historical failures, and records both native commit/changes and executable hash. The original reference JSON and exported fixture have independent hashes. The initial raw results are 66/75, 74/75 and 9/10 respectively; the final rechecks are 75/75, 75/75 and 10/10.
Original unwritten aim component
B0_C1_60 and B0_C4_00 first differ in an AI HERC's elevation aiming. Actor word +0x238 selects a target component. Original constructor 11330 never writes it; pool setup 637e4 only initializes allocation links. The first strategy selection normally replaces it, but travelling actors can aim before that call. At the tested range, a zero left in this field selects component 0 instead of the target's centre.
check_herc_aim_initialization.py fills that word before original construction. Both -12,345 and 23,456 survive unchanged in all fifteen constructed HERCs. A separate write trace finds no assignment before B0_C1_60 frame 212. The observed aim point changes from target-centre Z 981 to component Z 1,481.
The native state already initializes the field to -1, the original routines' centre-target sentinel. This is an explicit deterministic fallback. It does not attempt to reproduce whatever happened to be in DOS allocation memory. Seeding only that word to -1 before original construction makes both 512-frame runs agree without changing native behavior. The capture flag is --repair-aim-initialization, and every seeded pool entry is recorded. mission_initialized_aim_frames.bin retains both full corrected-reference cases.
Original fixed-point multiply overflow
B0_C5_70's first remaining difference occurs during a long obstacle detour at frame 354. Both versions have identical position, obstacle, heading, timer and distance. Original 63d8c offsets the obstacle position using helper 5e838. That helper multiplies a signed long by a signed Q14 word, then executes:
imul ecx
sbb ecx, ecx
xor ecx, 0x2000
add eax, ecx
adc edx, 0
shrd eax, edx, 14
IMUL sets carry when the product does not fit a signed 32-bit value. SBB then makes the rounding addend 0xffffdfff, rather than 8192. ADD/ADC add it as an unsigned low word, producing a spurious carry into the high half. This is a deterministic arithmetic defect, separate from the allocation defects above.
The observed distance is 214,005 and obstacle is (-36,563, -39,375, 0). Original execution produces (404,958, -156,082, 0); correct wide Q14 arithmetic produces (142,815, -156,082, 0). That extra 262,143 on X changes the steering request from 242 to 623 and affects the world from that frame onward.
check_fixed_offset_overflow.py checks 153 products, including signs and signed 32-bit boundaries. Its instruction-equivalent calculation matches all original results; 55 differ from correct wide arithmetic. Replacing SBB ECX,ECX with XOR ECX,ECX at 5e845 makes all 153 products agree with (product + 8192) >> 14. The two-byte patch exists only in the explicitly labelled reference emulator. The installed DOS executable is untouched.
The native implementation already uses wide products and keeps that behavior. It does not add machinery to reproduce this confirmed original bug. The --repair-offset-overflow capture makes all 512 B0_C5_70 frames agree, including RNG, damage and outcomes. mission_corrected_offset_frames.bin retains that corrected-reference case, with patch bytes and capture hash in its provenance.
Test harness corrections
The first player pilot used a weapon-slot selection that was valid for the recorded demos but not every authored starting chassis. The scripted input now selects the last valid slot when the requested slot is absent. No game code was changed for this harness error. The initial failed captures remain available.
The sweep now rejects an unknown requested mission instead of silently omitting it. Consequently long-worlds contains three cases, not the four originally requested. Launch selection also retains one context per requested launch seed.
Sanitizers and reproduction
The initial complete 263-test suite passed both normally and under ASan/UBSan, including leak checking. The sanitized run took 446.6 seconds. The four newly added fixtures pass a further sanitized run after the player-disable change (26.36 seconds). The intermediate rebuilt native suite passed 267/267 tests (53.52 seconds). The final two gameplay fixes add two more fixtures. The complete final suite passes 269/269 tests in 38.66 seconds; all 19 selected movement/world suites pass ASan/UBSan in 31.12 seconds, including the six new regressions. Normal and sanitizer build/test logs are retained under earthsiege-cpp/build.
.venv/bin/python -B tools/check_herc_aim_initialization.py
.venv/bin/python -B tools/check_fixed_offset_overflow.py
.venv/bin/python -B tools/check_authored_mission_frames.py --launch-seed 42 --seed 83 --ratio 450 --steps 512 --player-input --repair-formations --repair-effect-queues --output work/research/missions/autonomous-validation/player-all-450
.venv/bin/python -B tools/check_authored_mission_frames.py --mission b0_c1_60 --mission b0_c4_00 --launch-seed 42 --seed 83 --ratio 450 --steps 512 --player-input --repair-formations --repair-effect-queues --repair-aim-initialization --output work/research/missions/autonomous-validation/initialized-aim
.venv/bin/python -B tools/check_authored_mission_frames.py --mission b0_c5_70 --launch-seed 42 --seed 83 --ratio 450 --steps 512 --player-input --repair-formations --repair-effect-queues --repair-offset-overflow --output work/research/missions/autonomous-validation/corrected-offset
The original formation-index and effect-queue repairs remain explicit, as documented in MISSION_FIDELITY.md. These are finite simulation intervals with controlled input, not complete manual playthroughs of every launch variation. The existing sampled-image and PCM completion-timing limits are unchanged.
The completed machine-readable coverage index is work/research/missions/autonomous-validation/summary.json. No unexplained difference remains in these captured runs. Broader interactive playtesting and the existing PCM-completion timing boundary remain outside this pass.