Earthsiege Research archive

World loading and starting actors

tools/native_world.py runs the original DBSIM resource initialization and world loader, followed by actor construction. It supplies read-only extracted-resource streams, heap allocation and the DOS extender's 64 KiB texture selector. Original data readers, model constructors, weapon assembly, initial damage, group assignment and placement execute unchanged. Temporary allocations live for the fixture's lifetime; this is not a DOS allocator reconstruction.

Initialization and ownership

The original static-constructor table registers phase-2 subsystem initialization in this order:

46ac, 55b8, 5764, 607c, 66ec, 6d9c, 73d0, 78c0, 7bb0, 8230, bf48, 10e10, 198f0, 1c558, 20c2c, 27e10.

These initialize ground definitions, lighting/effects/projectiles, weapon definitions, HERC/flyer profiles and renderer resources. Effect pools matter before the first simulation tick: actors with authored initial damage can create effects in their constructors.

0x1d850 reads the shell handoff and allocates the referenced actor pools. 0x1e638 rereads the compiled script, constructs those actors, equips HERCs, applies starting conditions and assigns groups. Group zero uses the separately appended player roster. Referenced source records are compacted to live pool slots. Preload membership does not mean that an actor is currently activated or visible.

The group record is 40 bytes. Its fields include kind at +0, formation at +2, argument at +4, route cursor at +6, route pointer at +8, heading at +12, member array at +14, member count at +18, side/mode byte at +20, trigger pointer at +22, target kind at +26 and target pointer at +28. Object group ownership is at +0x3f and its authored member ordinal at +0x43. A source actor may occur in multiple group member arrays; the last assignment supplies its owning-group pointer. Earlier arrays still retain their reference.

Starting positions

An actor with its own point uses that point. HERCs add the profile's height term (disk offset 20, memory offset 22 after the profile's added prefix). Otherwise the actor starts with a negative-height sentinel and group assignment resolves its location from the group's point or first route point. An unspecified orientation uses the group's heading. With no explicit heading, a route with at least two points supplies its direction.

Non-leading members receive a formation offset rotated using the leader's orientation. HERC offsets come from MFORMS (seven pairs of shorts per formation); flyer offsets from FFORMS (three triples of shorts); ground-object offsets from BFORMS (seven pairs of 32-bit coordinates). The authored member ordinal matters, including holes in a group's member list. A flyer whose final height is exactly zero is raised to 5,000 native units.

The independent tools/actor_placement.py reconstruction matches the four recorded demos' 128 actors, including headings, equipment and group membership. All 300 compiled variants also pass, covering 8,048 actors and 4,315 groups. Their explicitly labelled player fixture and comparisons are in work/research/missions/constructed. The mission map can display captured constructed positions; the tape browser exposes actor tables and model links. These captures end before the first simulation tick and do not assert deterministic replay or simulate activation, movement or combat.

Native edge cases

Some authored spider groups use formation -1, but the member-placement routine still calls 0x1a098 for non-leading members. Its unchecked expression indexes before the MFORMS allocation. Eighty-nine actor positions across the compiled fixtures involve this case. Nine controlled original-code probes show that changing the preceding memory changes the resulting offsets. See work/research/missions/formation-bounds.json. Such positions are annotated as belonging to the capture's allocation fixture; they are not treated as stable authored offsets or an arbitrary final formation. The independent decoder does not use Python's negative indexing to fabricate an answer.

Route headings use the original table-assisted integer arctangent (0x734a0 through 0x63ca0 / 0x63cc8), not floating-point atan2. 4,096 independent comparisons include large signed coordinate differences. The original rounds the low half of a 64-bit numerator with ADD but no ADC; a carry can therefore be lost for sufficiently large coordinates. This behavior is retained in the analytical reconstruction and is not silently replaced with a mathematically cleaner formula.

Next runtime boundary

Group dispatch, activation triggers, locomotion, collision/damage and mission-outcome decisions run after construction. RUNTIME.md maps their frame ordering. The construction captures provide controlled starting objects for probing those consumers; they are not a replacement for those probes.

Native C++ placement

The C++ placement catalogue now reads the original MECHS.NAM resource order, HERC height profiles, GL equipment slots and all three formation tables. Its pre-tick result matches all 304 captures: 8,176 actors and 4,368 groups. It preserves compact source order, group-zero roster substitution, route-derived headings, authored member ordinals, repeated group references and last-group ownership. The 89 allocator-dependent negative-formation positions are represented as absent positions; no fabricated coordinate is passed off as original output. Controller activation and simulation are integrated and checked separately in CONTROLLERS.md and RUNTIME.md.

Complete HERC starting state in the native session

capture_mission_hercs.py records all 56 HERCs across the four recorded mission starts, with the initial/final shared RNG, source/group identity, side, activation gates, pose, damage, cells and actor flags. The native session now reconstructs these complete HERC starts. It applies NPC damage and player condition restoration before final group placement, preserving original effect positions during loading. A waiting gate is the owning group's trigger pointer, not an actor flag. Visible and collision rosters exclude those groups. Separate player-roster rendering is retained for the existing mixed-scene pixel comparisons. Flyer and fixed-object ownership and active controllers are now integrated; the later captures below and CONTROLLERS.md document their separate comparisons.

Fixed ground actors and destruction

capture_ground_actors.py executes all nine types in both side banks, including initial wrecks and partial starting damage: 72 states and 288 untouched original VGA views. capture_ground_combat.py captures 1,332 lifecycle operations and 1,728 rays through intact, collapsing and wrecked bases. Effects, kill credit, mission counter updates and attached smoke are explicit boundaries. Turret acquisition is stubbed to no target in these idle/background tests; active firing is separate.

BASES.DAT supplies static/animated model and wreck indices, channels, capacity, explosion centres/extents, weak point/radius, channel rates, cell clock and contact radius/target height. BASECOL.DAT is nine consecutive counted COL blocks, without an additional outer count. Static buildings use their model heightfield; articulated bases use the single damage section in BASECOL until the final wreck switch. The sphere consumer and base actor each add one to articulated impact depth.

0x3978 destroys only when accumulated signed-short damage exceeds capacity. 0x2df8 then executes six random local bursts (effect 15), the final centre blast (effect 16), a 2,000-unit wait and the wreck/attached-smoke transition. Burst waits are 300; background cell clocks use the global frame ratio, while radar animation advances a fixed 100 animation units per dispatch. Gun turret cell animation stops on its unlit frame until firing restarts it. The native session now renders and collides with bases and routes hits through this behavior.

Invalid distant radar partition transforms

BASES_AN and BASES_AE root 0 each have two animation poses but BSP references 4, 3 and five references to 2 in detail branches 0, 1 and 2. These are actual out-of-bounds references: an original-renderer probe observes the instance matrix pointer and index 3 resolving to the following animation-channel allocation. The native catalogue changes these seven references to -1 (model-local planes). Sixteen explicitly labelled extra distant views patch the same indices before running the original renderer, to verify the safe, deterministic result. The 288 ordinary frame captures remain untouched. Static instances' unused articulated fields are not captured as valid matrices: DOS leaves them uninitialized.

A further controlled replay probe establishes the visible consequence without changing authored data, camera, matrices, or clocks. In DEMO2 frame 720, the invalid transform 2 begins with the following allocation's ABLK header; one purported matrix coefficient is an allocator pointer. Changing that pointer by 0x1000 in the original process changes eleven rendered pixels. The original projected polygon vertices and material intensities already agree with the native renderer: it is the partition order that differs.

Evidence: tools/probe_radar_allocation.py and work/research/tapes/replays/image-differences/radar-allocation-dependence.json. This is a heap-history-dependent original defect. A fixed replacement matrix cannot reproduce it universally. Exact accidental pixels would require the original DOS address space and allocation history; the deterministic local plane repair remains an explicit compatibility departure.

The repair is intentionally retained in the authentic C++ build. This is a narrow exception to the usual policy of leaving fixes to Redux: the original out-of-bounds read is confirmed, and emulating the relevant DOS allocation layout/history solely to reproduce it would add unnecessary machinery. The local-space replacement is a documented deterministic choice; the evidence does not recover the artists' intended replacement.

Gun turret tracking and firing

capture_turret_tracking.py runs 0x3058 for 1,536 sequential targets, retaining the returned pre-turn local pitch/yaw errors, rates, control angles and every matrix. It transforms the world target through the base and barrel inverses, clamps error/8 to +/-256, squares the signed input in Q8, and approaches maximum rates 2,000/2,500 with steps 200/800. Pitch is limited to +/-4,000; yaw wraps as a signed word. The resulting unsigned angles shifted by two select channel phases.

capture_turret_firing.py covers 3,072 actual 0x3364 ticks. Reacquisition resets a 10,000-unit timer. Target lead uses projectile record 2's speed and the target's heading alone. Firing requires range below 40,000 and both pre-turn errors in [-1000,1000). Alternating burst windows each last 10,000; refire is 1,500. Both bullets use subtype 2, from barrel-local (+/-300,400,0), in that order. Their animated muzzle cell clock restarts at 1. Burst/refire clocks freeze when there is no target; the target and cell clocks continue. These verified native consumers await the mission scheduler's full perception/target context.

Skimmer ownership and flight

capture_flyer_actors.py runs the original flyer constructor and damage consumers (0x1ac58, 0x1b290, 0x1b370). The native owner loads SKIMMER.DAT/DTS/DMG/COL and ENEMY.DBA. Its one section has 300 armour and 200 internal points, with no shield. Construction fills its presence word with 0x0101; all seven cells start at zero. A lethal hit emits the section explosion/debris before kill credit and mission notifications, clears detectability, marks removal and moves Z to -100000. Projectile impact then emits its separate hit explosion/debris. There is no falling or persistent wreck phase for this actor. Locked-player kills also request speech 3 before kill credit. Ten cumulative damage steps and 768 rays check damage, flags, position, cells, effects and RNG. Thirty-two complete original VGA frames verify the texture, lighting, projection and distance-shading path. capture_mission_flyers.py checks all 18 flyer starts in the four demo worlds.

capture_flyer_motion.py exercises 2,304 steering/movement steps across 192 paths: pitch approach 0x1b6c0, bank control 0x1b794, bank hold 0x1b76c, turn control 0x1b8c4, altitude hold 0x1b708, and integration 0x1b084. Original profile fields now identified are pitch approach step 500, bank rate 1200, forward step 1000, bank acceleration 150, bank-to-yaw shift 4 and maximum bank 14000. Position and Euler changes are per dispatch; the frame ratio only ticks the avoidance clock. The forward obstacle probe is 15000 units, lowered by 500. An original quirk is retained: object distances only replace a smaller ground intersection distance; without a ground intersection, positive scenery/base distances compare against zero and do not start avoidance. The capture records all 4,608 probe endpoints and results, using explicit collision-query boundaries. Active route/attack controller scheduling is integrated and covered by the complete replays in RUNTIME.md.

capture_flyer_navigation.py verifies 600 route and 600 formation-control cases. Flyers approach the next route point within 15000 horizontal units, loop a closed route, or continue circling its final point. Followers aim 8192 units ahead of the leader plus their FFORMS offset. The horizontal formation matrix includes the leader's pitch/roll, while the stored altitude offset remains unrotated. Requested speed is 180 for route flight, otherwise a bounded 140..256 formation correction; the movement consumer currently overwrites local forward step from the profile.

capture_flyer_attack.py verifies 3072 actual 0x1bba4 leader attack updates, with 78 projectile requests and full RNG. Phases depart, approach, circle and level; departure turns toward the target after 80000 horizontal units. Approach begins its close pass below 45000; bearing outside [-8192,8192) or altitude below 5500 aborts it. Shots require pitch/yaw errors in [-1000,1000), pitch step in [-10,10), and an expired 1500-unit refire clock (which only ticks inside those gates). The first eligible shot below 30000 is rocket subtype 0; later shots are paired bullet subtype 2 from local (+/-500,200,-100). Lead uses the flyer's reported speed, except close approach uses projectile record 2's speed. Actual rocket spawn 0x7748 adds the flyer's local forward speed to 500, verified with six launch probes including negative and wrapping input. Target selection, followers and world scheduling are separate consumers, not replaced in these policy captures.

Live flyer integration and target geometry

capture_mission_flyer_frames.py executes 960 world navigation frames in demo3 and demo4, at ratios 64/256/450. All groups are activated on side zero; HERC controller bodies and sound are explicit boundaries. Bases, existing effects, flyer controller dispatch, route/formation motion, both obstacle queries, area triggers and perception run as original instructions. The native session matches all 8,640 flyer states and the shared RNG after every frame. This isolates navigation and does not assert a complete hostile combat replay.

The world harness now initializes the original controller tables with 0xfee4 and 0x105b0 before construction. Omitting those static constructors leaves zero callback pointers and would make an apparent live-controller trace inert. Previous explicit state-entry/selector/dispatch probes initialized those tables separately; pre-tick deciding-state constructor captures were unaffected.

capture_target_geometry.py checks 1,536 original virtual target selectors and 9,180 component positions. HERC missile selection (0x15884) visits present sections in ascending order, scores saturated absolute pitch and roll error, and retains the first tie. Although yaw error is computed, it is not scored. The selected component index is fixed at missile creation; 0x15990 resolves its current animated centre on subsequent updates. For component -1, 0x6eb4 uses the HERC camera-node position, a base's authored target-height offset, or the flyer's origin (its ordinary virtual +0x28 returns null).

The native comparisons found that HERC equipment setup 0x12c00 evaluates animation channels at time zero through 0x215c before binding weapons. Some enemy chassis, including MONGOOSE, therefore start with articulated aiming poses that differ from raw model defaults. Native actor construction now does this.

Negative chassis reference in fresh C6 examples

SM_SCT50 compiled for C6 steps 1, 4 or 6 with the inspection scenario's fresh history produces two active records with chassis -1. DBSIM 0x19a30 reads 0xbc430 + chassis, so -1 aliases 0xbc42f, the high byte of its loaded-type count. With fewer than 256 types this is zero: the first loaded profile. The original loader and constructors complete and produce unarmed Roadrunners in these six seed-17/42 cases. This is an original out-of-range type-map read. The independent placement decoder and browser capture preserve both the authored_chassis: -1 and resolved chassis: 0; negative weapon IDs construct no weapon and are represented by the existing empty-slot ID 17. Actual campaign history can choose different source branches. None of this establishes that a normal playthrough necessarily encounters this state.

Complete replay validation of the radar repair

All six remaining timed whole-image discrepancies have now been independently redrawn from their captured original states, unchanged and then with only the same seven-reference repair. The affected sets are exactly 36/33/30 pixels in DEMO1 frames 120/1020/1440, 46/54 in DEMO2 frames 660/720 and 31 in DEMO4 frame 0. Every changed pixel matches the native result; no other pixel changes. See tools/probe_replay_radar_repair.py, the radar-repair-six-scenes.json report, and the complete pixel investigation.

Later authored-mission fidelity pass

Live native sessions now use formation zero for negative formation IDs, allowing the affected missions to run without indexing before their allocations. Inspection placement still defaults to explicit undefined positions. The fallback is a documented choice, not recovered authorial intent. Starting damage/controller ordering and objective source-map behavior were also corrected from original instructions; see MISSION_FIDELITY.md.