Files
TeslaRel410/restoration/source410/BT/MISSILE.NOTES.md
T
CydandClaude Fable 5 e682f64acb BT410 5.3.126: the enemy had no hands -- the lifecycle kill-cycle crash was a missing controls mapper, and the whole mission lifecycle is green again
The crash that has killed lifecycle2.conf since somewhere in 5.3.26..120
was never in the missile code.  The map named Missile::MoveAndCollide
because it resolves the nearest preceding symbol; the instruction bytes
named the truth: mov eax,[ebx+0x128]; mov eax,[eax]; mov eax,[eax+0xFC]
= mech->subsystemArray[0]->member through a NULL slot 0.

Roster slot 0 is the CONTROLS MAPPER, and the BT_SPAWN_ENEMY dev enemy
never got one -- SetMappingSubsystem is only called on the player's
viewpoint mech.  The binary never NULL-checks that slot (mech2's
advancers, the master perf's duck evaluation and the effectiveness
census all deref it bare, correctly: a real mech always has its mapper),
so the enemy faulted the first frame it reached one of those reads.
Nothing dereferenced a non-player mech's mapper before the gait arc,
which is exactly why it lay hidden.

The object was proven by the fault-address rule this project already
learned: the faulting member offset MOVED 0xFC -> 0x100 across the
5.3.125 build, which added ejectLatch to MechControlsMapper -- a 4-byte
layout shift in the very class being dereferenced.

The dev enemy now installs a plain MechControlsMapper (inert without
device input), preserving the binary's contract.  VERIFIED end to end:
kill -> respawn -> second kill -> OUT OF LIVES -> mission end -> clean
DOS exit, zero exceptions -- the full lifecycle is green for the first
time since 5.3.25.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:13:29 -05:00

7.3 KiB

MISSILE.CPP / SEEKER.CPP / MISTHRST.CPP / PROJTILE.CPP — the flying rounds

Status: LIVE and DEFAULT (5.3.19) — the full entity lifecycle is verified: spawn → guided flight → proximity detonation → death-row teardown, 20/20/20 over a 110s mutual fight with zero faults. BT_MISSILE_INSTANT=1 keeps the 5.3.18a instant-hit delivery as an A/B dev opt-out.

Interfaces

  • SEEKER.HPP + MISTHRST.HPP are AUTHENTIC survivors (CODE/BT/BT/). The Subsystem base chain is the NAME ctor (binary FUN_0041c52c) with the shipped strings "Seeker" @00512e1e and "MissleThruster" @00512e25 (the 1995 misspelling is authentic — keep it).
  • MISSILE.HPP / PROJTILE.HPP are STAGED (only MISSILE.TCP / PROJTILE.TCP survive). Binary chain: Entity → Projectile (@004be1bc, 0x340) → Missile (@004bf5b4, vtable @00512f2c, 0x368). Our staged chain matches: Projectile : Mover (make-message path), Missile : Projectile.

What is verified live (BT_MISSILE_FLIGHT=1 fight run)

ONE cluster Missile entity per SRM salvo, spawned through the real Mover make machinery mid-weapon-sim (the Entity ctor self-registers — the engine tolerates mid-roster-walk creation, as the binary did); flight telemetry shows thruster acceleration (150→153 u/s) and closing range; the proximity fuse (rangeToTarget <= 20u staged) delivers damageData (salvo-split amount, burstCount=missileCount) exactly once via TakeDamageMessage with the LAUNCHER as inflictor; lifetime fizzle retires unexploded rounds. Guidance constants are binary-decoded (steer deadband 1e-4, turn gain 4.0, seeker loft 200/50/300/0.1 — .rdata @004bec18/@004bf594).

The teardown crash — SOLVED (5.3.19)

The dtor-chain bisect (logs in ~Missile/~Projectile/~Seeker/~MissileThruster) showed the whole chain COMPLETING, then the fault. The killer was the third strike of the uninitialized-zone-array trap: the staged spawn uses the MECH FAMILY's resourceID, whose DamageZoneStream makes the Entity base ctor allocate the 20-pointer damageZones[] array (entity.cpp:1032, slots UNINITIALIZED) — the Missile never fills it, and ~Entity's teardown loop if (damageZones[i]) delete damageZones[i] called 20 virtual dtors through trash vtables (EIP-in-heap AFTER the subsystem dtors — exactly the bisect signature). FIX: the Projectile ctor NULLs every inherited zone slot. RULE (three crashes now): any entity subclass that does not FILL the allocated zone array must NULL it — the Entity base never initializes the slots, on construction OR destruction. Also fixed en route: the Subsystem NAME ctor left damageZone uninitialized (the resource ctor NULLs it). Ruled out along the way: AlwaysExecute flags, interestZoneID (message base NULLs it), the subsystemArray idiom, Sensor caching, FryDeathRow timing (an app task, one delete per frame, not mid-walk).

Engine truths learned (load-bearing)

  • ResourceFile::SearchList(id, type) CRASHES (@0x414938) when id is not in the top-level directory — it derefs its inner FindResourceDescription unconditionally. FindResourceDescription(int) is the NULL-safe probe.
  • Resource IDs in MakeMessages must be FAMILY ids: the Mover base ctor runs its own SearchList(resourceID, GameModelResourceType), and member resources resolve only through their family head.
  • The streamed explosionModelFile IS a resource FAMILY id after all (5.3.20 decode): SRM6's 17 = 'mslhit' — a 3-member family: VideoModel (id 225, 100 bytes), AudioStreamList (id 227), and an 8-byte GameModel (id 228) = { int 498, Scalar maxTimeOfFlight } — SRM reads 5.0 s (range 800 at ~160 u/s checks out). Resource TYPE numbering (RESOURCE.HPP): 1 ModelList, 9 BoxedSolidStream, 10 VideoModel, 15 GameModel, 17 SubsystemModelStream, 18 GaugeImageStream, 20 DamageZoneStream, 21 SkeletonStream, 30 ExplosionTableStream. A family's raw stream = int count + count member resource ids (what SearchList walks).
  • NoCollisionVolumeFlag is LOAD-BEARING on projectile spawns: collision volumes are the Mover DEFAULT (IsCollisionVolume() = the flag NOT set), and the ctor then demands a BoxedSolidStream member — which a projectile family doesn't carry (SearchList NULL → Lock() page fault; the mech family works because bhk1 carries blh_cv.sld). The round's collision is the world query / proximity fuse.
  • The original SearchList crash (@0x414938) is the missing-MEMBER case: Check(res) on a member id that doesn't resolve is compiled out in opt and the walk derefs NULL. Probe families manually (FindResourceDescription per member) before handing them to engine paths.
  • The spawn keeps the resolved family LOCKED (no Unlock) — the launcher holds its projectile model resident, as the binary does.

Staged / deferred

  • Muzzle = mech origin; the authored MuzzleVelocity arc through the MOUNT segment world frame (fire builder @0x4bcc60:8758-64, z NEGATED + mech velocity) is the muzzle wave.
  • Lifetime is AUTHORED now (5.0 s from the mslhit GameModel, sanity-banded [0.5, 30] so the owner-family fallback's mech record keeps the default); burn (2s @120 u/s²), launch speed (150 u/s) and fuse radius (20u) remain staged — BT411's model +0x44/+0x48/+0x50 offsets describe a bigger record than the 8-byte 4.10 GameModel, so thrust/detonation-mode live elsewhere (candidates: the VideoModel record's tail, or per-launcher resources).
  • Cluster SPLASH + explosion entity (ClassID 0x5C @004be078), world-geometry collision (@0042291c), target-velocity intercept lead, Missile WriteUpdateRecord (tag 0x78) for MP replication.

SOLVED (5.3.126): the lifecycle kill-cycle crash was a MISSING MAPPER

Symptom: lifecycle2.conf died seconds after the SRM salvos with Exception 0E ... illegal address 000000FC, EBX = the spawned enemy mech. The map put the EIP inside Missile::MoveAndCollide -- a red herring: the map resolves the nearest PRECEDING symbol, and the real faulting code was a mech-side inline.

The instruction settled it: mov eax,[ebx+0x128]; mov eax,[eax]; mov eax,[eax+0xFC] = mech->subsystemArray[0]->member with subsystemArray[0] NULL. Roster slot 0 is the controls mapper, and the BT_SPAWN_ENEMY dev enemy never got one -- SetMappingSubsystem is called only from BTL4APP's viewpoint-entity path, i.e. for the PLAYER's mech. The binary never NULL-checks slot 0 (mech2's advancers, the master perf's duck evaluation and the census all deref it bare, correctly: a real mech always has its mapper), so the first frame the enemy reached one of those reads, it faulted.

CONFIRMING EVIDENCE (the fault-address rule, used properly this time): the faulting member offset MOVED 0xFC -> 0x100 across the 5.3.125 build, which added ejectLatch to MechControlsMapper -- a 4-byte layout shift in exactly the class being dereferenced. That named the object beyond doubt.

Why it lay hidden since ~5.3.26: before the gait arc nothing dereferenced a non-player mech's mapper. Missile::MoveAndCollide was never involved; the missile flight path is exonerated.

FIX: the dev enemy spawn now installs a plain MechControlsMapper (inert without device input) in slot 0, preserving the binary's contract. VERIFIED: full lifecycle green -- kill -> respawn -> second kill -> OUT OF LIVES -> mission end -> clean DOS exit, zero exceptions.