Earthsiege 2Excavated · Lost Engines

Working research

Complete original-process mission traces

A retained analysis note. Addresses and capture filenames refer to the original research workspace. Download the Markdown.

The C++ simulation agrees with the captured state after every tick of forty controlled v1.1 runs through the documented comparison limits. The first drives and fires through 1,024 ticks, including an enemy returning fire. The second combines driving, turning, turret twist, pitch and firing for 512 ticks. At delta 81, these cover 40.5 and 20.25 seconds of simulation time. They start from the first campaign mission's original shell handoff, retained in analysis/missions/captured/new-campaign.

The third run adds the initial career's three available squadmates using the original shell's own serializer. It drives, turns the torso and fires for 768 ticks (30.375 seconds), including building destruction and squadmates reversing out of obstacles. tools/prepare_squad_trace.py creates that handoff in analysis/missions/captured/four-member-squad. Its capture also records every debris piece and the player's firing-line points.

What runs in the original

tools/native_tick_probe.c launches a private copy of DBSIM under Wine and breaks at entry and return of Sim_MainTick. It fixes the timestep at 81 and supplies a recorded input block after the game's device-input builder. Loading, terrain, animation, movement, collision, AI, sensors, weapons, damage, rendering and the rest of the game still execute inside that process. No simulation subsystem is replaced with a callback. Breakpoint instructions are restored, single-stepped and rearmed. The separately labelled training-8-extended, training-5-assault-death-guarded, training-5-protected-guarded and training-8-complete-guarded, campaign-c2_10-guarded and campaign-2-carried-guarded runs add one bounds guard for the confirmed original bug described below.

tools/trace_original_mission.py builds the debugger, creates a private install, selects the supplied handoff and input stream, and starts an isolated display. It stops only its own Wine prefix. Raw records, node matrices, weapon records, projectile pool entries, flags and RNG bytes are retained in ticks.jsonl. Completed large traces are stored as ticks.jsonl.gz. Compression preserves every original JSON byte; the complete decoded SHA-256 is checked before deleting its uncompressed duplicate. analysis/reference/trace-storage.json records both sizes and hashes. Analysis readers accept either form, and frame sidecar names remain unchanged. Active captures stay uncompressed until completion.

Optional aim, RNG and terrain-ray breakpoints provide instruction-level evidence when a comparison first diverges. Those logs are independent of the C++ trace.

tools/compare_mission_traces.py interprets the raw records and compares them with the C++ mission_trace diagnostic. The regression fixture exporter reads only the original captures. Its expected values never come from the port.

Current regression coverage

Original run Compared ticks Snapshots
v1.1 campaign combat 0-1024 1,025
v1.1 movement and turret control 0-512 513
v1.0 shared campaign prefix 0-328 329
v1.1 four-member squad and destruction 0-768 769
v1.1 SARH firing without acquisition 0-768 769
v1.1 SARH acquisition and fire 0-1024 1,025
v1.1 ARH acquisition and fire 0-1024 1,025
v1.1 ARM acquisition and fire 0-1024 1,025
v1.1 EO steering, release and impact 0-1024 1,025
v1.1 Razor flight and squad formation 0-768 769
v1.1 first training mission through success 0-4341 4,342
v1.1 data-link training through success 0-7689 7,690
v1.1 third training mission through success 0-7317 7,318
v1.1 flight training through success 0-3969 3,970
v1.1 flight training through boundary warning 0-4000 4,001
v1.1 defence training before invalid aim read 0-1936 1,937
v1.1 defence training with explicit aim bounds guard 0-6000 6,001
v1.1 assault combat 0-2400 2,401
v1.1 escort failure 0-5209 5,210
v1.1 reconnaissance death 0-4110 4,111
v1.1 reconnaissance with full effect pools 0-3866 3,867
v1.1 long assault before invalid aim read 0-5923 5,924
v1.1 long assault death with aim bounds guard 0-6055 6,056
v1.1 protected assault before invalid aim read 0-2766 2,767
v1.1 protected assault success with aim bounds guard 0-7317 7,318
v1.1 complete defence death with aim bounds guard 0-7917 7,918
v1.1 C2_10 before invalid aim read 0-890 891
v1.1 C2_10 with explicit aim bounds guard 0-1536 1,537
v1.1 C4_10 opening 0-1536 1,537
v1.1 C5_10 lunar finale opening 0-1536 1,537
v1.1 C5_04 deployment through disablement 0-3349 3,350
v1.1 opening campaign patrol through success 0-10417 10,418
v1.1 C1_02 carried campaign before invalid aim read 0-4173 4,174
v1.1 C1_02 carried campaign with aim bounds guard 0-11533 11,534
v1.1 C1_03 recovery after the actual C1_02 failure 0-5209 5,210
v1.1 C1_04 armed Razor reconnaissance 0-3969 3,970
v1.1 C1_05 rescue in the carried Razor 0-6697 6,698
v1.1 hard nighttime Ogre escort 0-3421 3,422
v1.1 C1_08 data-link attempt through disablement 0-3969 3,970
v1.1 C1_06 carried Outlaw through incomplete return 0-6000 6,001
v1.1 C5_10 finale through player death 0-6369 6,370
DEMO1 complete playback 0-921 922
DEMO2 unchanged prefix 0-669 670
DEMO2 complete guarded playback 0-857 858
DEMO3 complete playback, with two documented timer departures 0-3350 3,351

Together these 45 cases contain 165,525 snapshots, 192,104,802 named fields and 1,357,011,355 observed integer values. The complete moving-world comparisons include the documented original bounds and timer defects; each fixture's provenance records its exact boundary.

The initial original-missions.bin fixture checks 43,533 complete snapshots, 28,455,637 named field observations and 211,794,709 integer values. Repeated fields are stored as deltas in the fixture, but every observed field is checked on every tick. The missile and flight runs also retain firing lines and debris. The Razor adds body/world velocity, angular rates, throttle, inverse transform and bank rate. The comparisons include actor order, position and orientation, AI state and timers, every captured HERC node matrix, torso controls, components, shields, power, weapon supply/refire, locks, selected weapon, radar, contacts, LOS caches and timers, the projectile pool, all mission flags and the simulation RNG. The squad run additionally checks firing-line points, debris positions, angles, velocities, radii, timers, child groups, death effects and pending deletion.

The retained report is cpp-original-missions.json. The full older-build run remains available even though it is not a v1.1 oracle past its first genuine version difference.

The first training mission also matches its success dialog at tick 4,341 (171.69 simulated seconds) and the exact 80 bytes written to RESULTS.DAT. Data-link training succeeds at tick 7,689 with 92 identical result bytes. The third training mission succeeds at tick 7,317 with 128 identical result bytes, including 12 recovered weapons and four credited HERC kills. The other runs cover specific battle scenarios; additional mission outcomes and campaign branches still need complete-process comparisons. Two hundred and twenty-three idle, moving, view-change, assault, campaign and death-camera frames now match exactly, as detailed below. Further battle presentation and map interactions can extend that coverage. Host audio remains a separate check. Native subsystem fixtures and broader C++ integration tests cover many of those areas, but their evidence has different boundaries.

Corrections found by comparing the whole process

The startup-order and height corrections are reflected in the analysis scene exports and regenerated C++ scene/frame/movement fixtures.

Squad, collision and debris corrections

The 768-tick squad run exposed integration errors that the isolated routines could not reveal with controlled collision replies:

First observed tick Correction
170 Avoidance probes use one ground height sampled beneath the HERC. Structure rays reconstruct their endpoint from the quantised angle and approximate length, then shorten it in object order. The original right-hand hit was 8,695 units; direct endpoint tracing gave 9,333.
213 Collapsing buildings throw debris from the component's fire_point at record offset +0x0a. Using position at +0x10 placed this burst 100 units too high and changed its first child burst at tick 274.
226 A building-volume hit at distance zero is still a hit. Collision presence and distance must be represented separately.
593 The same applies to terrain: a projectile beginning on the surface can intersect exactly at its starting point.
667 Avoidance receives the controller's updated backoff timer and reversing flag. Reading the walking state's old copy added a steering bias for one extra tick.

The firing-line producer is independently checked with 4,096 cases and 62,017 points in each of the four executable families. It is now connected between player firing and the following tick's squad decisions. Building and terrain recipient fixtures now include zero-distance intersections explicitly; all four builds agree on those expanded cases. See navigation and hit detection.

Missile and flight integration

prepare_missile_trace.py serializes the original starting squad with the player carrying an MSL10 on its normal shoulder hardpoint. The four longer runs change only its ammunition and supplied controls. SARH, ARH and ARM cover acquisition and unguided fire; EO covers held steering, release to unguided flight, impact and another launch. prepare_flight_trace.py substitutes a repaired Razor with two wing-mounted Laser 300s. The mission, terrain and other actors stay original.

These captures exposed four further integration mistakes:

The initial Razor flight cache is identity because it is constructed before assignment of the mission pose. The first flight-control pass replaces it.

Missile component indices are compared only while a target exists. Rocket_Fire leaves that word untouched for targetless launches, and guidance never reads it in that state. Reused bullet-pool bytes can appear there. The raw captures retain those bytes; the semantic fixture omits the unused word.

The supplied controls are already-built device packets. They overwrite the original device builder's output, after button-latch filtering. The C++ replay therefore bypasses its device trigger latch too. Effects can still clear the trigger during that tick. Original device/latch handling has separate fixtures; these runs test simulation under identical supplied packets.

First retail version divergence

At tick 329, building type 21 checks its visibility of HERC object 3. The actual ray captured at the original terrain routine is:

from (2198682, 2392195, 14556)
to   (2215981, 2505102, 25752)
radius 0, mode 0

Version 1.0 reports an obstruction without writing an intersection point. Both v1.1 builds report no obstruction. The April demo agrees with v1.0. This is the previously identified failed-plane-solve difference, now observed in an ordinary running mission. It changes building object 34's LOS cache and radar flag at tick 329. On tick 330, v1.0 makes nine sensor RNG calls while v1.1 makes eight. Later trajectories cannot be compared by assuming the builds are interchangeable.

tools/check_original_trace_versions.py checks the first differing state, reads the exact endpoints from both process logs, and replays that ray through all four executable families over the captured campaign terrain. Its report retains the ray, answers, terrain digest and random-call stacks. The reconstruction follows the selected v1.1 behaviour. See terrain collision for the underlying instruction difference and wider ray fixtures.

Reproduction

Run the capture from the analysis directory, using its Python environment:

PYTHONPATH=tools .venv/bin/python tools/trace_original_mission.py \
  --version cd_v1_1_1997_09_win95_e --name combat-poses --ticks 1024 \
  --handoff analysis/missions/captured/new-campaign \
  --inputs analysis/reference/tick-traces/controls/combat.bin
PYTHONPATH=tools .venv/bin/python tools/export_cpp_mission_traces.py

The C++ repository then needs only its bundled data and binary fixture:

ctest --test-dir build -R '^original_mission$' --output-on-failure

Training route, aliases and result file

tools/prepare_training_trace.py asks the original shell to create a new training pilot and export its launch files. The diagnostic mission_trace --route records ordinary steering packets towards the next waypoint. Both programs then replay the same saved packets, with no changes to mission content. The Windows probe presses Tab and Enter once on the success dialog, lets its original modal loop close, and allows the original shutdown to write results. It rejects short captures and removes any old result file before launching.

This exposed a missing connection in the port: source group-zero members are aliases for the squad. TRAIN1's objective resolves HERC reference 0 to the player's group and route. The first campaign mission uses reference 3 instead. Resolving only ordinary authored HERCs prevented these objectives completing. The corrected map matches the original loader, independently checked for 122 compiled previews and five squad sizes in each of four executable families. See mission reference mapping.

The ES2RUN02 regression retains ending status and the original result bytes, as well as every simulation snapshot. Expected data comes exclusively from original process captures. The complete run's result hash is c2809312d8c03189402215cbc1d8d58b8ca0c1f6a26b5926d409bfa79bbe0738.

Defence training and the destroyed-building aim bug

TRAIN8 uses the original shell handoff with chassis 3 and idle player controls. Its two Landskimmers exposed two integration mistakes. A flyer's zero-altitude fallback belongs after its formation offset, leaving the second craft at 400 units rather than 5,400. Formation XY uses the leader's full pitch/roll/heading matrix; the slot's Z offset remains unrotated. Both corrections are reflected in the analysis scenes for C4_10, TRAIN5 and TRAIN8.

The unmodified process agrees through tick 1,936. On tick 1,937, object 14 loses its last live component. Base_FirstLiveComponent (0x406868) returns -1; Ai_AimAndFire forwards it to Base_ComponentPosition (0x406808), which reads three signed words 14 bytes before the component array. In this capture those words are (0, 0, -2571), shifting the aim below the building's origin at (1021677, 1099730, 39168). The resulting differences are 21 turret observations and one joint-pose observation. No other captured state differs in 2,048 ticks.

check_structure_aim_bounds.py reproduces the unchecked read in all four builds and changes the returned point by changing the bytes before the supplied array. Its report is native-structure-aim-bounds.json.

The reconstruction retains its bounded lookup and aims at the building origin when no component remains. To verify that this accounts for the discrepancy, trace_original_mission.py --safe-component guards negative component indices in a separately named original process. It records the caller, index, invalid point and replacement origin. In the initial 2,048-tick capture, this guard fires exactly once, at tick 1,937; all 2,049 states then agree. The regression includes the unmodified prefix and the longer, explicitly labelled guarded run described below. All raw captures are retained.

Extended defence battle and update ordering

The training-8-extended capture runs 6,000 ticks (237.3 simulated seconds) with idle player controls and the same explicit negative-component guard. All 6,001 captured states agree, including further structure collapses, missile flights, targeting, sensors and the complete simulation RNG. It has not reached an ending dialog at the end of the capture.

Three integration details became visible beyond the shorter run:

The debugger runner now saves and compiles a private probe.c for each new capture, and hashes that exact source. Editing the working probe during a long capture can no longer misidentify the binary that produced its evidence.

Complete cockpit frame captures

The cockpit-frames-fixed capture runs the original first-campaign handoff for 129 ticks. It records the complete 640-by-480 indexed buffer and RGB6 palette after ticks 1, 2, 16, 64 and 128. Inputs are zero, delta is 81, and the original coarse-clock function returns 1 + floor(completed_ticks * 81 / 32) from simulation-clock initialization onward. All loading, simulation, rendering, widgets and message processing otherwise run in the original process.

The C++ original_cockpit regression runs the live SDL compositor through the same launch inputs and recorded commands. Five idle, twenty-five movement, forty-two view-change, thirty-three map-control and thirty-two squad-command samples match all 42,086,400 captured indices and every palette entry, with no masks or ignored pixels. The fixture comes only from original captures. tools/export_cpp_cockpit_frames.py exports it, and cpp-cockpit-frames.json records executable, probe, preference, frame and fixture hashes.

The full frames revealed errors not caught by isolated layout and raster tests:

Optional --draw-window captures retain the original buffer before and after each structure draw, along with detail-level and palette-ramp observations. Raw frame-state captures also preserve camera/context blocks, message state and animated structure matrices. These observations located the errors without adjusting the port by visual trial and error. The movement captures below extend this check; side-view restoration and view transitions are covered below.

Movement, throttle and vehicle fog

cockpit-frames-moving and cockpit-moving-details use the same 512-record movement input stream. Their duplicate frame captures agree byte for byte. The exported regression combines their 25 distinct sample ticks, including turns, turret pitch/twist, reverse travel and consecutive throttle changes.

The original coarse clock is sampled inside Sim_MainTick when a footstep or hit starts. At that point the capture's completed-tick count has not advanced. The C++ host queues the resulting event, so it must preserve the pre-step clock instead of stamping it after the simulation step returns. This corrected one-pixel world offsets at the footstep curve boundaries.

The slider's delay is visible in both instructions and captured state. Sim_RenderFrame calls CockpitView_PerFrameUpdate at 0x45fc43, then Player_PerFrameCockpitUpdate at 0x45fcb3. The former paints the slider; the latter calls ThrottleGauge_SetValues. At frame 1 its drawn rectangle is (268,393,296,405) while its newly set bottom is 412. At frame 2 the drawn bottom becomes 412 while the pending bottom is 418. Sixteen full captures, ticks 1-8 and 257-264, match the C++ compositor after retaining this update order.

Ground vehicles also revealed a classification error in scene assembly. The original object's tag at +4 is 10, although the vehicle remains in the structure pool. ObjList_DrawCellObjects raises only tags 5 and 7 by their aim or camera node. At tick 511 the supply transport's render entry is therefore (2303036,2184729,11776), with distance 125438 and fog bias 8192. Giving it the building tag added 1500 to Z, reduced the distance to 125063 and selected bias 0. The 95 affected pixels now match. Native scene-harness classification has also been corrected and its normal/missile fixtures regenerated.

The per-frame diagnostic now saves the throttle gauge and slider blocks. --draw-window records render-entry position, distance and active fog bias in addition to the before/after image. These observations remain in the analysis captures; no original rendering code was replaced to obtain the frame oracle.

Side views and heads-down transitions

The cockpit-view-presented original-process run records 42 displayed frames while entering and leaving both side glances, the tactical map and the damage page. The probe captures the last Screen_PresentFrame call in each simulation tick. Transition loops can present several intermediate frames; a buffer read at the next tick entry can instead contain an unpresented viewport change. The fixture records this capture boundary in its provenance.

These comparisons established the following details:

The capture inputs use ordinary view commands and leave the mission data unchanged. All captured indices and palette entries match without masks. Additional captures below cover map controls and squad orders.

Map keys, warning prompts and button flashes

A further 33 complete frames in cockpit-map-controls match the original after XMIT with no selected pilot, zooming both ways, panning, recentering, and changing pages. The warning row starts empty. An invalid XMIT request calls HddCommandScreen_SetMessageRow (0x44dc44) with six refresh periods remaining; HddCommandScreen_BlinkTick (0x44d348) counts those down. The captured SELECT PILOT label and its background disappear on the same refresh in both programs.

A keyboard press calls WidgetRoot_FlashPress (0x453078) with a deadline ten coarse ticks ahead. The flash service follows widget painting, so the pressed image appears on the following frame. The arrows, magnifiers and XMIT now keep that order. Mouse presses still follow the pointer's held state.

Keypad 5 only zeroes the pan offsets in HddCommandScreen_KeyDispatch (0x44cc40). It does not request a repaint. Forcing an immediate repaint moved a convoy marker one frame earlier than the original. The next timed map update now installs the new offsets. Squad-order selection and transmission are covered by the next comparison.

Lost-target timing and salvage order

The third training mission exposed two differences that shorter battles had not reached. Player_PerFrameCockpitUpdate at 0x41b130 arms the 4,500-unit lost-target timer when the selected target changes to none while automatic turret tracking remains enabled. It decrements that timer after the input pass and requests centring for the following input pass. Decrementing it inside steering instead made the turret start returning one frame early. The corrected run matches all 7,318 snapshots, including the resulting model-node matrices.

Mission_TotalSalvage at 0x423e88 walks the HERC pool backwards. The pool's allocation links prepend new objects, whereas the live-object array records creation order. The training-3-list original capture retains those links. Supplying the live-object array directly to a reverse walk changed the order of recovered weapons despite identical damage and salvage totals. The runtime now supplies pool order. These corrections use the same v1.0 reference addresses as the instruction fixtures, mapped to the v1.1 executable by the harness.

Squad orders and radio portraits

The cockpit-squad-controls capture adds 32 exact full frames. Its ordinary keyboard recording selects Jones and Ortega, designates the player as a defend target, transmits DEFEND POSITION and SCAN FOR HOSTILES, changes pages during replies, and returns to the forward cockpit while Ortega is talking. Together with the earlier captures, the fixture compares 137 frames and 42,086,400 palette indices, plus every RGB6 palette component.

The original instructions explain the differences found in this sequence:

Every captured pixel and palette entry matches. The final image of each slide is part of this comparison; intermediate slide presentations are not yet a separate pixel fixture.

Reused mission text files

FileRWStream_Open does not truncate existing files. The original shell opens DATA/MISSION.STR with flags 0x8102, writes a length-prefixed resource, and leaves any older tail in place. For example, the captured flight-training text occupies 163 bytes of an 866-byte file. DBSIM reads the declared extent, so the remaining 703 bytes do not belong to that mission's text.

The C++ mission-text reader now accepts that original handoff. It still checks that the declared resource is present and complete; a shorter physical file is rejected. The shared string-bank decoder keeps its strict extent check. A regression writes a short mission over a longer one and verifies both this tail handling and rejection of a truncated resource.

Razor training and cockpit regions

The unmodified fourth training handoff now succeeds at tick 3,969 with every captured simulation field and result byte matching the original process. A second input recording flies out of bounds and continues through tick 4,000. The boundary dialog has one Continue button; the diagnostic helper previously selected a nonexistent second button. The live game window already handled it correctly. The two recordings add 7,971 snapshots to the regression fixture.

Eight complete Razor cockpit frames, sampled from tick 1 through tick 3,968, match every index and palette component. These extend the fixture to 145 frames and 44,544,000 indexed pixels. The checks exposed several rendering details:

Addresses above refer to the v1.0 Win95 reference and are mapped to v1.1 for process captures. Fixed timing removes presentation-clock drift from this comparison. Input recordings enter after native device and trigger-latch filtering; the C++ diagnostic uses the same boundary. Ordinary input retains its normal latch handling. These samples cover flight, scenery, messages and ammunition readouts; they do not establish all battle-effect ordering.

Assault, escort and reconnaissance training

The fifth training recording now matches all 2,401 simulation snapshots. The sixth recording reaches the escort failure at tick 5,209, with all 5,210 snapshots matching. Reconnaissance reaches the player-death ending at tick 4,110, with all 4,111 snapshots and the result file matching. These comparisons exposed several integration errors that the isolated subsystem checks could not catch:

The cockpit comparison also establishes the following ordering:

All thirteen training-5-panels frames now match every pixel and RGB6 palette entry. Their presentation RNG also agrees. Moving target brackets retain the previous aim block and use the rebuilt camera-angle matrix. The HUD arrow requires winding detection; its generated triangle otherwise disappeared. The last differences were two patches of missing HERC shadows, now connected to the cell-based ground-shape draw pass. See ground shapes.

The reconnaissance comparison also distinguishes death from a disabled HERC. Sim_ProcessPlayerInput (0x460764) gates pilot control on object +0x39, not its destroyed byte. Sim_UpdateDeathCamera (0x45f978) changes input routing on the following tick: device axes go to the camera, but the pilot still runs with zero axes and the stored throttle. Later collision can clear that throttle, which must stay cleared despite held device input. Skipping the pilot update or continuing to send it live axes gives different wreck motion.

cpp-original-training.json records the three additional processes: 11,722 snapshots, 9,531,602 named fields and 52,139,195 values. Together with the main process fixture, coverage is 55,255 snapshots and 263,933,904 values. The two result-producing recordings match all RESULTS.DAT bytes. They cover failure and death outcomes; successful assault, escort and reconnaissance completion remain separate targets for wider coverage.

Explosion and fire pool captures

The optional --effect-pools probe records both pools before the render-phase flush, retaining raw objects, cell frames, shape radius and pending deletions. The training-7-effect-pools capture contains 3,867 snapshots through lethal player damage. It matches every previously interpreted gameplay field and all new explosion/fire fields: owner, position, render class, radius, animation frame, timer, effect type, fire loop count and pending removal.

The final EXPLOS.DAT type word selects class 8 when nonzero and class 2 when zero, as Explosion_Construct explicitly writes at object offset +4. The C++ code previously used the raw flag as the class; that mapping is corrected.

original-effects.bin retains these independent expected values. Together with original-missions.bin and original-training.bin, the corpus covers 21 runs (20 v1.1 and the existing v1.0 prefix), 59,122 snapshots, 40,535,703 field observations and 279,168,059 values. The new capture repeats a reconnaissance scenario with more state exposed; it does not add a distinct mission.

Full recorded-demo processes

The three retail TAP recordings now add 5,801 snapshots, 11,030,867 fields and 93,992,696 values through the live mission window. Their original process builds its own inputs and uses the tape's variable delta. Live preferences, KEYJOY and absent joystick hardware are held equal. Ending dialogs are acknowledged explicitly. DEMO2 has an unchanged prefix and a separately guarded complete run for the existing negative component-index bug.

Across the fixed-input and recorded-demo fixtures, the retained corpus now has 64,923 snapshots, 51,566,570 field observations and 373,160,755 values. Two DEMO3 rocket timers deliberately differ from native pointer-derived garbage; the fixture preserves the original values and verifies the documented safe values separately. All other observed fields agree. This is 25 capture cases, including the v1.0 prefix and repeated scenarios with different probe scope. See recorded demos for the outcomes, instruction findings and exact exception.

Long assault through death

A separately planned ordinary input stream drives TRAIN5 through sustained combat and its final death dialog. The original destroys the player on tick 5,809 and ends on tick 6,055. Both programs write the same 184-byte result file (SHA-256 5a82bcc685058ef01d5ea4f98e38855947ad0a99c97bd9ab191df3b0f7ab407e). The planner had predicted success before the collision correction; the retained capture is named for its actual death outcome. Planning is independent of the native expectations and never supplies expected gameplay values.

This run exposed the live building collision state and distinct AI probe filters described in hit detection. It also showed that the cockpit republishes its retained target after the player's destruction clears the HERC's target field.

The unchanged original agrees through tick 5,923. On tick 5,924 it reaches the already documented destroyed-building component under-read: component -1 reads local (0, 0, -2571) before the array. Sixteen turret observations on ticks 5,924-5,939 consequently differ; every other interpreted field agrees throughout the complete run, and the turret state converges again on tick 5,940.

training-5-assault-death-guarded repeats the same input stream with only that invalid lookup replaced by the building origin. The guard logs calls on ticks 5,869 and 5,924; the earlier call produces no observable difference in captured state. All 6,056 snapshots match. The regression keeps both the unchanged 5,924-snapshot prefix and the complete guarded run, totaling 11,980 snapshots, 12,244,101 named fields and 62,892,401 values in original-outcomes.bin.

The current combined totals are listed in the coverage table above. Two documented DEMO3 rocket timers remain explicit safe departures. The guarded assault agrees in all fields and preserves the original result file. Reports: unchanged assault, guarded assault, fixture provenance.

Assault success and actual debrief records

The fifth training mission now has a complete successful run as well. The original VSHELL creates its handoff with the normal training invulnerability option enabled. Ordinary movement, targeting and firing inputs were planned separately, then replayed unchanged by the original v1.1 simulator. Both runs reach success on tick 7,317 and write the same 212-byte RESULTS.DAT: bb22c96a840d9e3cd8f56eb8b580fc2528adfafdfdc766517026df855ab41a01.

The unmodified original differs only while actor 8 briefly aims at two destroyed buildings. Its turret and posed nodes differ at ticks 2,767-2,794 and 6,792-6,809: 78 field observations in total. Both calls pass component -1 to the existing out-of-bounds position getter and read local Z=-2571 before the component array. Every other captured field agrees, including RNG, damage, projectiles and all effect pools. The separately labelled guarded original run matches all 7,318 snapshots. It also observes invalid reads at 5,595 and 5,629 that produce no captured state difference. No original result values are replaced in the fixture. The unmodified regression prefix ends at tick 2,766; the guarded run is retained in full. The outcome fixture now contains four cases and 22,065 snapshots.

tools/export_played_debriefs.py carries the actual result files and mission flags from eleven completed original battles into the original shell. It recreates and checks each launch handoff first, then captures the complete post-battle save and shell RNG. These cases cover four initial training victories, assault death, protected assault success, escort failure, reconnaissance death, defence death and campaign C5_04 disablement and the opening campaign patrol victory. C++ produces identical saves and RNG in every case. File transport and shell presentation are harness boundaries. The older 100-mission shell journeys retain their separate scope: their outcomes are supplied rather than played battles.

The retained reports are analysis/combat/cpp-training-5-protected.json, analysis/combat/cpp-training-5-protected-guarded.json and analysis/campaign/native-played-debriefs.json. Together with the earlier cases, the regression corpus contains 30 captures, 94,906 snapshots, 83,568,542 fields and 532,716,112 observed values. The two original DEMO3 timer defects and the explicit bounds guards remain documented separately.

Complete defence outcome and aircraft destruction

training-8-complete-guarded replays ordinary radar, targeting, weapon-change, movement and firing inputs through the death dialog on tick 7,917. It uses the same explicit negative building-component guard established by the earlier unmodified TRAIN8 capture. All 7,918 snapshots and the 112-byte RESULTS.DAT match after two integration corrections:

tools/check_flyer_death.py independently checks 48 destruction cases in each of the four executable families, including the ordered reporting, defeat action, behaviour installation and sound stop. Its component-damage answer and external effect/report sinks are explicit boundaries. The complete defence capture separately exercises the joined game. Expected values come from original memory.

The 34,231,759-byte original-defence.bin adds 9,233,367 named field observations and 43,375,858 integer values. Its result hash is a55c6f18dcc27bc5b23fb6365359cb33e06f6881548d3d4086f9561693902648. The original shell also consumes these results in the ninth played-debrief case. Reports: whole-process comparison, fixture provenance, death endpoint.

Later campaign openings

tools/prepare_campaign_trace.py advances the original shell through the career with supplied successful results, then exports C2_10, C4_10 and C5_10. The earlier battles were not played. Each new original simulator process receives a retained 1,536-packet control stream. These checks cover the mission openings, not their complete objectives or a complete played campaign.

The captures exposed two integration differences:

C2_10 also reaches the existing negative building-component read: the probe records component -1 with local coordinates (0, 0, -2571) at ticks 891 and 1535. The unmodified regression ends at tick 890; the separately labelled guarded run checks every tick through 1536. The guard returns the building origin only for that invalid lookup. The other two executables run unchanged.

The new fixture adds 5,502 snapshots, 6,989,972 fields and 43,247,838 values. All 34 process cases together cover 100,408 snapshots, 90,558,514 fields and 575,963,950 values. Reports retain the original discrepancies as well as the corrected comparisons.

Thirteen additional complete cockpit frames match all indices and palette entries, bringing that corpus to 187 frames and 57,446,400 pixels. Two more frames, both at tick 128, show the already documented native full-queue message corruption. They remain recorded as deliberate departures, outside the exact fixture. No image uses a pixel mask. See frame provenance and queue evidence.

Lunar base deployment through campaign debrief

campaign-c5_04-deployment starts from the original shell's C5_04 handoff. prepare_campaign_trace.py reaches it after 43 supplied successful outcomes. Those earlier battles are setup, not additional played-game evidence. The handoff retains 61 live actors and 12 groups. A separately generated control plan supplies ordinary route inputs; expected state comes only from the original process.

The unmodified simulator activates enemy groups 1 and 5 on ticks 612 and 1,238. Group 8, the approached base, activates on tick 1,154. Three other waiting bases are not reached. The player is disabled on tick 3,349. All 3,350 snapshots, 6,232,881 fields and 48,224,053 values match C++. Both programs write the same 80 result bytes, SHA-256 d3758102441d33113f4ac4671f92eb71d3bb327a030e4c3fe50ff33483a103f3. No negative-component guard or other gameplay patch is needed for this run.

Frames 600, 1,153, 1,154 and 2,500 match in full, including their palettes. These bring the exact cockpit corpus to 191 frames and 58,675,200 pixels. The overall gameplay corpus now contains 35 cases, 103,758 snapshots, 96,791,395 fields and 624,188,003 values.

The played-debrief capture recreates the same prior shell progression, checks each supplied result hash, and verifies the final launch handoff before applying the actual C5_04 result and mission flags. Its complete saved state and RNG match C++, including 44 recorded player missions. This is the tenth played-debrief case and the first in campaign mode. ES2POST2 therefore records that mode in each fixture case; the earlier nine training cases still compare unchanged.

Reports: gameplay comparison, fixture provenance, full frames, debriefs.

Sanitizer coverage

The retained AddressSanitizer and UndefinedBehaviorSanitizer runs cover nine complete gameplay fixtures, 203 exact cockpit frames, twelve played-debrief cases and both positional-sound comparisons, with leak detection enabled. These are selected integration checks recorded over several revisions, not a sanitizer run of the whole suite. Original-demo playback was not included. The latest checks cover the continued campaign, sound-enabled patrol and silent death camera. Two frame checks exceeded their release-speed CTest limits; direct reruns completed with the same instrumented binaries. The retained report records build flags, revisions, binary hashes, boundaries and both attempts.

Opening campaign patrol through victory

The initial career's three available squadmates join the player through the original shell's squad assignment and mission export. No earlier battle results are supplied. A separate route-following plan supplies ordinary controls; the player does not fire or use invulnerability. Expected states come from the original simulator, which reaches success on tick 10,417 with all four squad members alive. The player reaches the final waypoint before the mission succeeds; the squad is still engaged during the intervening ticks.

All 10,418 snapshots, 17,920,495 named fields and 149,729,811 integer values match C++. The complete 308-byte RESULTS.DAT also matches, SHA-256 5ef2c605997e7c50da2809ea65b5f561e1a5cde68e0ba9bbd1190580c1170555. The original uses no component-lookup guard or gameplay patch in this capture. Frames 1,000, 3,100, 6,000 and 10,400 match every pixel and palette entry.

Returning these actual results and final flags to the original shell produces an identical 4,698-byte C++ save and RNG state. The save hash is eddb9762ea4be74f413d1c071374c271481bd2a718e255c7423be27aecf54654. This is the eleventh actual-battle debrief fixture. The earlier 100-mission shell journeys remain separate checks with supplied battle outcomes.

The combined corpus now covers 36 process cases, 114,176 snapshots, 114,711,890 named fields and 773,917,814 integer values. The 195 exact full-frame comparisons cover 59,904,000 pixels, alongside two separately documented message-queue departures. Both new patrol checks and the updated eleven-outcome debrief check pass. Together with the preceding passes, all 159 configured checks have passed. No gameplay changes were needed for the patrol.

Reports: whole-process comparison, fixture provenance, full frames and debriefs.

Two consecutive campaign battles

campaign-2-carried starts C1_02 by returning the actual C1_01 patrol result and flags to the original shell, accepting its debrief and launching the next mission with the same four-member squad. The saved handoff and preceding result hashes are checked. No earlier success, ammunition use or damage is invented. The retained squad records happen to start this mission with the same ammunition and condition values as the first launch.

The unchanged original reaches the known negative building-component lookup on tick 4,174. Its local component coordinates come from bytes before the array, so its later aiming and battle outcome cannot be used as an oracle for the port's documented bounds repair. The regression retains that unchanged prefix through tick 4,173 and a separately labelled guarded original process. The guard only substitutes the building origin for that invalid component lookup.

The guarded mission reaches failure alert 6 on tick 11,533. The two fixtures compare 15,708 snapshots, 28,072,959 named fields and 227,100,324 integer values. The complete guarded capture also writes the same 300 result bytes, SHA-256 28c4367dbae1e2efd33a45f4136987499ec7ef7de5567c0cdd7bac5ac297ce28. Frames 1,000, 4,000, 8,000 and 11,000 match in full, including their palettes. Returning that result and the final flags to the original shell produces the same 4,716-byte save and RNG state as C++, with two recorded player missions. The save SHA-256 is ed2690cd17b082a20e89ce13247aa13f1a25553da3ac340c1ad9e14be1196c2b.

This exposed an integration error in contact damage. The walking controller calculated collisions at the attempted step position, but damage still read the actor's preceding position. Five shield observations therefore selected the wrong front/rear side. The original walking routine writes the trial pose before invoking contact response. C++ now publishes that pose at the same boundary and refreshes the damage position before applying the blast. The walking caller publishes the final pose again after any rejected-step rollback. The correction removes all five differences without changing the observations.

At that checkpoint the gameplay corpus contained 38 cases, 129,884 snapshots, 142,784,849 named fields and 1,001,018,138 integer values. This includes 33 fixed-input v1.1 cases, one v1.0 prefix and four recorded-demo cases. Twelve actual battle debriefs compare complete saves and RNG; these remain separate from the broader shell journeys that supply synthetic battle outcomes.

Reports: fixture provenance, pre-fix shield differences, full frames and played debriefs.

Sound-enabled campaign patrol

original_sound_patrol replays 10,416 ticks of the same four-member opening patrol with sound enabled in the original process. It stops one tick before the already verified victory dialog. All 5,612 positional calls match their order, IDs, source positions, listener transforms and transformed points. Four complete frames and palettes at ticks 1,000, 3,100, 6,000 and 10,400 match exactly, along with the full presentation RNG at those boundaries. This adds four independent sound-enabled frame observations, bringing the complete-frame total to 203 and 62,361,600 indexed pixels. The two message-queue departures remain separate.

Three adapter errors emerged from this longer capture and the existing silent regressions:

The fixture stores every observed call and both original/world transforms. Its test supplies the final sample-output boundary without playing PCM, and reports no voices still playing. It checks the game-facing requests and selected render/RNG results; it does not compare HMI mixing, voice duration or resampling. The separate 68-event view-changing fixture still checks the documented repair for the heads-down map leaking its camera into sound placement.

Continued campaign: recovery and the Razor loan

The four-mission sequence now carries the actual C1_01 success, C1_02 failure, C1_03 success and C1_04 success through the original shell. C1_03 finishes on tick 5,209; C1_04 finishes on tick 3,969. The last two captures use no gameplay guard. Their 9,180 states and six full cockpit frames match C++, as do both complete result files, debrief saves and final RNG states.

The C1_04 Razor is initially unarmed. Before launching it, the original fitting routine installs the two earned plasma cannons in slots 4 and 5, matching ARM_RAZR.DAT socket ids 6 and 7. The original launch guards accept the result. Both fitting operations also have full-save C++ comparisons. This preserves the actual inventory rather than substituting the training loadout.

The first diagnostic launch accidentally omitted that fitting step. The normal shell would have rejected it. With that check bypassed, the original attempted to paint the weapon console before any weapon gauge had loaded PWEAPONS and faulted at v1.1 00442cf8, before tick zero. That attempt is explicitly excluded in campaign-4-razor-recon/excluded.json. Its log is retained losslessly; no production workaround was introduced for it.

Reproduction:

PYTHONPATH=tools .venv/bin/python tools/prepare_played_campaign_trace.py \
  --prior campaign-1-squad-success campaign-2-carried-guarded campaign-3-after-loss \
  --name campaign-c1_04-armed --fit 4 25 --fit 5 25
PYTHONPATH=tools .venv/bin/python tools/plan_mission_trace.py \
  --handoff analysis/missions/captured/campaign-c1_04-armed \
  --name campaign-4-armed-razor-recon --ticks 14000 --recon
PYTHONPATH=tools .venv/bin/python tools/trace_original_mission.py \
  --version cd_v1_1_1997_09_win95_e --name campaign-4-armed-razor-recon \
  --handoff analysis/missions/captured/campaign-c1_04-armed \
  --inputs analysis/reference/control-plans/campaign-4-armed-razor-recon/controls.bin \
  --ticks 3969 --fixed-clock --effect-pools --presented-frames \
  --capture-frames 1000 2000 3900 --accept-alert

export_cpp_mission_traces.py and export_cpp_cockpit_frames.py accept named captures through --capture, with a separate --fixture output name. The reconnaissance planner generates ordinary inputs; all expectations come from the independently captured original. The new reports are states, frames, weapon fits and debriefs.

Current totals are 40 cases, 139,064 snapshots, 150,880,588 named fields and 1,051,040,653 integer values. There are 209 complete exact frames and fourteen actual battle debriefs. The first four campaign missions now have one consecutive played history; other branches and player choices remain open to further checks.

Rescue victory and difficult nighttime escort

The actual campaign now reaches C1_05 through five consecutive played battles. Its rescue succeeds on tick 6,697 with the original pilot and armed Razor intact. All 6,698 snapshots, 11,057,749 fields and 91,626,018 values agree, along with the complete 80-byte result and the subsequent 4,766-byte shell save and RNG. Frames 1,200, 4,000 and 6,600 match every index and palette entry. The original needs no gameplay guard for this capture.

A separate practice escort uses the Ogre, difficulty 3, night conditions and the original AI squadmate. It ends in player death on tick 3,421. Its 3,422 snapshots, 2,795,191 fields, 15,144,678 values, 156 result bytes and complete debrief save match. The three frames at ticks 300, 1,600 and 3,300 also match without masks or an original-code guard.

The crew fixture now retains a legal switch from the loaned Razor to the existing Outlaw using the original crew-screen assignment routine. Both saves around the change agree with C++. The earlier two earned plasma-cannon fits remain in the same fixture. Only the crew-label refresh is a supplied boundary.

These additions bring the retained process corpus to 42 cases and 149,184 snapshots, with 215 exact complete frames and sixteen actual-battle debriefs. This includes prefixes and repeated missions under different conditions.

Reports: rescue states, rescue frames, night escort states, night escort frames, debriefs.

The trace exporter now scans large original captures twice instead of retaining all decoded snapshots in memory. Field ordering, observation counts and stored values are unchanged. Raw captures are losslessly compressed only after their decompressed SHA-256 is checked.

Broader campaign and difficulty verification

Five further captures extend the earlier 40-case corpus: C1_05 rescue, hard nighttime Ogre escort, C1_08 through disablement, C1_06 through an incomplete return and C5_10 through player death. All captured states and result bytes agree; fourteen additional full images match every indexed pixel and palette entry. The debrief fixture now contains nineteen actual battle outcomes.

The complete C1_06 comparison exposed an interrupted pivot-stop rate error. The finale exposed incorrect Arachnitron blast exclusion, attacker identity and publication of its limbo position. The disabled C1_08 cockpit exposed a missing offline gauge branch. Each correction follows the original instructions; the detailed report gives the first differing ticks, addresses, fixture boundaries and remaining useful coverage.