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

128 lines
7.3 KiB
Markdown

# 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.