Files
TeslaRel410/restoration/source410/BT/MECH.NOTES.md
T
CydandClaude Fable 5 d4f0aef6e1 BT410 Phase 5.3.17: the destruction cascade -- zone deaths DESTROY things
Mech::DamageZone::TakeDamage is the full binary cascade (@0049c690): LOD
artifact router with same-attacker clustering (@0049c40c, hysteresis 0.33),
artifact parent level = mean of children, Energy-hit generator shorts
through the crit plug binding (novice-exempt), weighted CriticalHit
(@0049ccc4), zone-death allotment push (SendSubsystemDamage @0049c9a8) and
the segment-table destruction descent (RecurseSegmentTable @0049cad4 --
SIBS + DESCEND via the engine EntitySegment iterators).

Supporting bricks: Mech's OWN handler table with the TakeDamage override
(latches lastInflictingID @0x43c; gyro feed staged; cylinder table
deferred); friend class Mech__DamageZone (authentic); subsystem private
zones ARMED (structureReference + armorByFacing[5] normalized -- crits are
measurable); ApplyDamageAndMeasure; ForceCriticalFailure sets
DestroyedState so the weapon FSMs' GetSimulationState()==1 hard gate went
live; Generator::ForceShort + PoweredSubsystem::ForceShortRecovery.

Fight-verified (mutual BT_SPAWN_ENEMY + BT_FORCE_ZONE=8 concentrated fire):
arm zone to 1.0 -> sibling searchlight zone dies (Searchlight2/ThermalSight
crits destroyed, HUD degrading) -> DESCEND kills the gun zone -> PPC_2 +
ERMLaser_2 + Condenser6 DESTROYED and FALL SILENT (0 shots after death;
undamaged PPC_1 keeps firing); vital-zone hit = MECH KILL; PPC hit shorted
GeneratorD. This art has zero artifact zones (redirect=0 across all 20 --
router verified dormant-correct). Smoke + novice regressions clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:13:28 -05:00

396 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MECH.CPP / .HPP — reconstruction notes
**Status: `Mech::Make` (factory) reconstructed; the Mech constructor and the
entire mech subsystem are the CURRENT FRONTIER — the largest remaining
milestone in the reconstruction.**
## What is reconstructed
- `Mech::Make(MakeMessage *)` = `return new Mech(creation_message);` — the
factory the registry / `MakeAndLinkViewpointEntity` calls.
- The static shared data (ClassDerivations / DefaultData) — from the earlier
staging.
- A staged Mech ctor that chains the real `JointedMover` base ctor (so the
model skeleton/segments stream from BTL4.RES) and then Fails.
The spawn factory path is now validated live: `BTPlayer::CreatePlayerVehicle`
builds the `Mech::MakeMessage`, `Application::MakeAndLinkViewpointEntity` routes
it through the registry to `Mech::Make` -> `new Mech` -> the JointedMover base
ctor (real engine code, streams the mech model) -> the Mech ctor Fail. Boot
reaches `mech.cpp:66`.
## Why the Mech ctor is a milestone, not a brick
`Mech::Mech` is `@004a1674`, **5690 bytes** — the largest single function in the
game. It walks the model's segment table and instantiates the full subsystem
roster (power, heat, weapons, actuators, controls, tech, damage zones), and
primes ~150 members. Reconstructing it requires three things the tree does not
yet have:
1. **The complete 1995 Mech member layout as named fields.** MECH.HPP currently
carries `int reserved[331]` (1324 bytes) as a placeholder. BT411's Ghidra
reconstruction sidesteps the layout entirely by writing raw offset pokes
(`Wword(0x104)`, `*(void**)((char*)this + 0x388)`, `this[0x14a]`) — a
WinTesla-port technique that CANNOT compile into a fresh 1995 build, where
BC4.52 assigns offsets from the header declarations. So the layout must be
derived as real members first.
2. **The subsystem class hierarchy.** MechSubsystem + derived: sensor, generator
(gnrator), gyro, torso, HUD, myomers, plus the weapon subsystems (mechweap,
ppc, gauss, projweap, mislanch, ammobin...). Of these, only GAUSS, PPC,
SENSOR survive as source in the 4.10 archive (CODE/BT/BT); GNRATOR is a
partial .TCP. The rest are part of the measured "917 missing functions" and
come only from BT411's decomp.
3. **The segment-table walk** that reads the model resource and instantiates +
wires each subsystem.
BT411's mech reconstruction is ~12,255 lines across mech.cpp / mech2 / mech3 /
mech4 / mechsub alone, before the subsystem TUs — this is the bulk of the
remaining game.
## Reconstruction plan (for the dedicated mech milestone)
1. **Derive the Mech member layout.** Turn `reserved[331]` into named members.
Sources: BT411's offset comments (`this[0x14a]` gyroSubsystem, `+0x388`
target entity, etc.) give the binary offsets; the embedded member types
(NameFilter, AlarmIndicator, Slot/Socket, Reticle, Filter, CString,
StateIndicator) come from the surviving MUNGA headers. Cross-check total size
against the binary's `new Mech` allocation.
2. **Reconstruct MechSubsystem base + the subsystem roster**, backdating BT411
mechsub.cpp + the per-subsystem TUs; fold in the surviving 4.10 GAUSS/PPC/
SENSOR source where it exists (authoritative over the decomp).
3. **Reconstruct the Mech ctor** against the named layout: embedded-member
construction, the segment-table walk, subsystem instantiation + wiring.
4. Then the per-frame path (mech2 animation SM, mech3, mech4 locomotion/
targeting) and the damage/weapon handlers.
Until then the ctor Fails cleanly at the frontier; everything below it (engine,
app, network, mission, player, factory, jointed-mover base) runs real
reconstructed code.
## PHASE 4 DONE + PHASE 5 FRONTIER (2026-07-20)
**The Mech constructor WORKS.** Live boot: `new Mech` -> segment walk instantiates
the full subsystem roster into the base Entity subsystemArray (dispatch on the
real VDATA classIDs; unknown IDs -> base MechSubsystem so no NULL slots) ->
caches sensor/gyro/sinkSource/hud -> mech links as viewpoint -> SetMappingSubsystem
installs the MechControlsMapper in slot 0 -> mission startup. All structural.
**Phase-5 frontier (the behavioral integration):** boot now crashes (real NULL
deref, not a staged Fail) in `Simulation::GetAttributePointer` reached via
`Simulation::PerformAndWatch` (the per-frame execute + watcher update). Root:
the reconstructed subsystems reuse the base `Subsystem::AttributeIndex`, so they
publish NONE of their real attributes (Sensor RadarPercent/SelfTest/BadVoltage,
Generator OutputVoltage, Emitter ChargeLevel, HUD/Torso/Gyro readouts, ...). The
watcher/gauge binding + per-frame PerformAndWatch resolve those attribute IDs and
hit a bad/NULL activeAttributeIndex path.
Phase-5 work, in order:
1. Per-subsystem AttributeIndex: define each subsystem's AttributePointers[]
(ATTRIBUTE_ENTRY(class, Name, member)) + a real AttributeIndex chained to the
parent, and pass it to that subsystem's DefaultData (instead of the base).
The surviving CODE headers (SENSOR.HPP, PPC.HPP, GAUSS.HPP) list the attribute
enums; the decomp AttributePointers[] tables give the rest.
2. Watcher / capability-chain / plug wiring in the Mech ctor (controllable/
heatable/weaponRoster/damageable) so PerformAndWatch has valid targets.
3. The per-frame simulation itself: mech2 (animation SM), mech3, mech4
(locomotion/targeting) -- ~9K lines, currently all staged Fail.
4. Rendering integration (the DPL renderer bridge) + playable verification.
Everything through the Mech ctor + viewpoint link runs real reconstructed code.
## PHASE-5 CRASH LOCALIZED (2026-07-20)
Trace-confirmed: the Mech ctor FULLY constructs on the real pod TEST.EGG mech --
`subsystemCount=33 weaponCount=7` (BT_MECH_LOG). The crash is AFTER construction,
in BTL4Application::MakeViewpointEntity's "Load the control mappings" block
(btl4app.cpp ~420): it SearchLists the ControlMappingsListResourceType resource
and binds each streamed mapping to a subsystem attribute -- via an AttributeWatcher
-> Simulation::GetAttributePointer(attributeID). The reconstructed subsystems
publish NONE of their real attributes (they reuse the base Subsystem::AttributeIndex),
so the streamed attribute IDs don't resolve.
Phase-5 step 1 = per-subsystem AttributeIndex, and it's a precision task:
- BT411 has the AttributePointers[] tables (mechsub 1, heat/HeatSink 12, powersub 4,
Generator, sensor 3, torso 13, weapons, ...) -- but ATTRIBUTE_ENTRY(class,Name,member)
needs each member NAMED + accessible (several of ours sit in reserved dynamics
pads -- Gyroscope/Torso/HUD -- and must be broken out).
- The attribute-ID enums chain Simulation::NextAttributeID -> Entity -> Mover ->
... -> subsystem, and MUST match the 4.10 binary IDs (the streamed control
mappings reference them by number).
Then: watcher/plug/capability-chain wiring, then the mech2/3/4 per-frame sim
(~9K lines, staged), then rendering. This is the behavioral half.
## PHASE-5.1 CRASH SOLVED + BOOT REACHES THE GAME LOOP (2026-07-20)
The `GetAttributePointer` fault was localized (per-slot probe over the roster)
to roster **slot 22 = `PPC_1`**, an energy weapon: its `GetAttributePointer`
read `entryCount` off a **NULL `activeAttributeIndex`**. Root cause: our
reconstructed `EMITTER.HPP` DECLARES `static AttributeIndexSet AttributeIndex`
(with a `ChargeLevelAttributeID`) but `EMITTER.CPP` never DEFINED it — and the
surviving 4.10 `PPC.CPP` binds `PPC::DefaultData` to the inherited
`PPC::AttributeIndex` (= `Emitter::AttributeIndex`), so the SharedData ctor's
`activeAttributeIndex(&attribute_index)` captured a null/zero symbol. The
surviving `GAUSS.CPP` also *chains* `Emitter::AttributeIndex` as its
inheritance, so the same hole corrupts GaussRifle. (Sensor et al. never
crashed: their `DefaultData` uses the defined `Subsystem::AttributeIndex`.)
**Fix (EMITTER.CPP):** define `Emitter::AttributePointers[]` (one entry,
`ATTRIBUTE_ENTRY(Emitter, ChargeLevel, chargeLevel)`) and `Emitter::AttributeIndex`
chained to `Subsystem::AttributeIndex`, and point `Emitter::DefaultData` at it.
This is the correct 1995 reconstruction (Emitter publishes the beam charge
level for the HUD gauge; PPC binds it; Gauss chains it). Static-init order is
satisfied by ORDER_bt (`... emitter ppc ... gauss`) with `subsystm` in
munga.lib (constructs first). After the fix the per-slot probe walks all 33
slots (PPC_1/2, ERMLaser_1..3, AmmoBinSRM6_1/2, SRM6_1/2, MessageManager,
MechTech, ...) cleanly, and the control-mapping binding block completes.
**Result:** with the Emitter fix + BTPlayer::InitializePlayerLink +
VehicleDeadMessageHandler(non-death), the reconstructed tree now boots with
**zero `Fail()`** all the way into the live per-frame game loop
(`LBE4ControlsManager::Execute` spinning). See BTPLAYER.NOTES.md. The remaining
gap to "playable" is behavioral (mech2/3/4 per-frame sim + renderer + real
control input), not structural — the boot path is complete.
## PHASE 5.3 INCREMENT 1: Mech::Simulate MOTION CORE (2026-07-21)
`Mech::Simulate(Scalar)` is reconstructed and installed as the mech's per-frame
Performance (`SetPerformance(&Mech::Simulate)` at the end of the ctor). Until now
the mech ran the base `DoNothingOnce`; it now runs real body code every frame
(dispatched via Mover→Entity→`Simulation::PerformAndWatch` once RunningMission).
This increment is the **load-bearing spine** of the 1995 Simulate
(mech4.cpp @004ab430): integrate the body velocity into `localOrigin`
(`linearPosition.AddScaled(linearPosition, worldLinearVelocity, dt)`) and commit
the Origin to the world transform with the engine's own idiom
(`localToWorld = localOrigin`, per ENTITY.CPP:988 / MOVER.CPP:850). BT411's RE
was decisive here: the mech's motion operates on the ENGINE BASE Mover fields
(`localOrigin`/`projectedOrigin`/`previousOrigin`/`projectedVelocity`/
`localToWorld`) — the raw `this+0x100/0x260/0x26c` offsets in the WinTesla decomp
actually *stomp* those base fields (their MechBaseLayoutCheck static_asserts).
So the clean reconstruction uses the NAMED base members directly.
**Verified headlessly** (BT_MECH_LOG `[sim] mech pos=` 1 Hz): with
`BT_DRIVE="5,0,10"` the mech advances +5.0/s in x and +10.0/s in z (y fixed),
exactly the injected world velocity; with no BT_DRIVE `worldLinearVelocity` is
zero and the mech holds its spawn pose — no regression, zero Fail. `BT_DRIVE`
is a retained DEV hook (world-velocity injection) for headless motion tests.
**Deferred to the next increments (the locomotion wave):**
1. gait-cycle self-propulsion that FEEDS `worldLinearVelocity` — the authentic
model derives forward speed from the walk/run animation cycle
(`IntegrateMotion``AdvanceBodyAnimation`→cycleDistance) steered by the
control-mapper throttle/turn demands, NOT a raw velocity;
2. heading integration (rotate `localOrigin.angularPosition` by the turn rate);
3. terrain-height drop (`BoundingBoxTreeNode::FindBoundingBoxUnder`) resting the
feet on the ground;
4. cockpit telemetry `FilteredScalar`s (head/aim/leg/torso angular rates).
## PHASE 5.3 INCREMENT 2: DRIVABLE via authentic control interpretation (2026-07-21)
The mech is now **drivable**: `Mech::Simulate` consumes the control-mapper
demands and drives locomotion with the authentic model. Chain:
`MechControlsMapper::InterpretControls` (roster slot 0, ticks before the mech's
Performance each frame — see MECHMPPR.NOTES.md) reads the pushed raw inputs and
publishes `speedDemand` (world u/s = topSpeed·throttle·fwdScale) and `turnDemand`
([-1..1]). `Mech::Simulate` then:
- reads speedDemand/turnDemand from `subsystemArray[0]`;
- accelerates `currentBodySpeed` toward the demand (bounded by `maxBodyAcceleration`);
- computes the authentic per-mech turn rate `authTurnRate` =
lerp(walkingTurnRate, runningTurnRate) by ground speed with a `runTR/t²`
over-run falloff (mech4.cpp master-perf @0x4aa3d3);
- integrates heading: `localOrigin.angularPosition.Add(prevPose,
(0, turnDemand·authTurnRate·dt, 0))` — the engine rotation-integrate op;
- takes the world Z basis (`localToWorld.GetFromAxis(Z_Axis,…)`, negated) as the
facing and sets `worldLinearVelocity = facing · currentBodySpeed`;
- integrates position (the increment-1 core).
New named Mech members (out of `reservedState`, now [211]): `walkingTurnRate`,
`runningTurnRate`, `reverseStrideLength` (top speed), `walkStrideLength`,
`reverseSpeedMax`, `forwardThrottleScale`, `maxBodyAcceleration`,
`bodyTargetSpeed`, `currentBodySpeed`.
**Model-resource sourcing (2026-07-21, VERIFIED).** The ctor now sources the
authentic per-mech `walkingTurnRate` / `runningTurnRate` / `maxBodyAcceleration`
/ `forwardThrottleScale` from the GameModel resource
(`SearchList(GameModelResourceType)` → `ModelResource`), replacing those
bring-up defaults. Each read is SANITY-GUARDED (out-of-band → keep the default)
because BT411 flags parts of the ModelResource layout as mis-decoded — but the
live values came back clean and authentic: **walkTR=75°/s, runTR=50°/s,
maxAcc=30 u/s², throttleAdj=1.0** (walking turns faster than running; maxAcc
matches BT411's madcat note), confirming the struct layout is correct for these
fields. Verified drive: at walk speed the authentic 75°/s tightens the turn
circle to r≈6.9 (=speed/turnRate). Still on bring-up defaults:
`reverseStrideLength` (top speed) / `walkStrideLength` / `reverseSpeedMax` —
their authentic source is `LoadLocomotionClips` (measured from the walk/run
animation clips), a later wave.
**VERIFIED headlessly** (`BT_MECH_LOG` `[sim]`/`[mppr]`; forced-input dev hooks
`BT_FORCE_THROTTLE`/`BT_FORCE_TURN` in InterpretControls):
- `throttle=1.0` → speedDemand=30, mech accelerates to 30 u/s and walks straight
along its heading (per-second position delta magnitude = 30.1);
- `throttle=0.3, turn=1.0` → spd=9, mech walks a CIRCLE of radius ≈ 10 =
speed/turnRate (9 / 0.87 rad·s⁻¹) — heading integrates continuously;
- neutral (RIO, no hardware) → speedDemand=0, mech holds pose;
- zero Fail/Exception throughout.
Still deferred: gait-clip-exact advance + leg animation (needs the animation
subsystem + renderer), model-resource/LoadLocomotionClips sourcing, terrain
drop, torso/free-look aiming (Torso/HUD analog axes), telemetry filters.
## PHASE 5.3 INCREMENT 3+: skeleton-joint resolver + torso aim wiring (2026-07-21)
- Torso weapon-elevation aim reconstructed (`Torso::TorsoSimulation`, wired into
`InterpretControls`): stick pitch → elevation, slew + clamp to the resource
limit (verified: clamps at the authentic 20°). See TORSO.NOTES.md.
- **`Mech::ResolveJoint(name)`** reconstructed (mech.cpp @00424b60): `GetSegment
(name)` → segment joint index → `GetJointSubsystem()->GetJoint(idx)`. The
shared skeleton-joint resolver the subsystems use to bind their animated
joints.
- **The skeleton is live headlessly** (verified, `BT_MECH_LOG` `[skel]` summary +
`BT_SKEL_DUMP` per-segment list): the JointedMover base streams the full
segment/joint tables — `jointCount=19`, 40 named segments including the gun
joints (`jointlgun`/`jointrgun`), shoulders, hip, and all leg joints
(`jointlthigh`…`jointrankle`). So joint-driven aim / gait / damage has real
joints to bind to.
- The Torso now resolves + binds its twist joint (`ResolveJoint(torsoHorizontal
Joint)`) and pushes `currentTwist` onto it via `Joint::SetRotation` (hinge →
Radian, ball → EulerAngles yaw). The bring-up TEST.EGG mech has a FIXED torso
(`horizJoint=''`, `enabled=0`) so its twist path is inert — correct and
guarded; a torso-twist mech would drive the joint.
## PHASE 5.3 INCREMENT 5: eyepointRotation -- elevation is NOT a gun-joint binding (2026-07-21)
Before reconstructing "elevation → gun joints," checked BT411 for how the
authentic engine actually consumes `Torso::currentElevation` — and it does NOT
drive any skeleton joint. BT411's own history records the discovery (mech4.cpp
@~5219, user report "pitch does not work"): the Torso sim integrated the R/F
aim into `currentElevation` but nothing consumed it — no joint search ever
existed for a gun/arm elevation joint. **The pod's stick-Y aim pitches the
COCKPIT EYE, not the torso geometry**; `eyepointRotation` (an `EulerAngles`
Mech member) is composed each frame from the look-state pitch/yaw (buttons)
plus the Torso elevation, and `DPLEyeRenderable` reads it for both the camera
view AND the weapon boresight (so a low target can still be hit — the fire ray
re-applies the elevation onto the leveled boresight). BT411's sign convention
was pixel-calibrated against real screenshots; our composition matches it
verbatim (mirroring the additive pitch, zero look terms until that wave lands).
**Reconstructed:** `Mech::eyepointRotation` (`EulerAngles`, out of `reservedState`,
now `[208]`) + `GetEyepointRotation()` accessor. Composed in `Simulate` each
frame: `eyepointRotation = EulerAngles(Radian(Normalize(lookPitch + elevation)),
Radian(Normalize(lookYaw)), Radian(0))`, reading `Torso::CurrentElevation()` via
`sinkSourceSubsystem`. `lookPitch`/`lookYaw` are 0 until the look/eyepoint button
wave (`BTCommitLookState`) is reconstructed.
**VERIFIED headlessly:** `BT_FORCE_ELEV=0.8` → `torsoElev=0.349066` (the 20°
resource limit) and `eyePitch=0.349066` — an exact match, every frame; neutral →
`eyePitch=0`. Zero Fail.
## PHASE 5.3 INCREMENT 6: the look-button state machine (2026-07-21)
The five-state LOOK machine (the binary's controls-mapper tail,
part_013.c:396-459) is reconstructed as proper members/methods (not BT411's
global bridges):
- **Mapper** (`MECHMPPR`): `LookState` enum (None/Left/Right/Behind/Down),
`lookState`/`previousLookState` members; InterpretControls picks the state
from the look buttons each frame and, ON A CHANGE, calls
`mech->CommitLookState(state)`. `BT_FORCE_LOOK=<1..4>` dev hook.
- **Mech**: authored look angles (`lookLeft/Right/Front/BackAngle`, deg→rad
from the GameModel resource, guarded like the other reads) + `lookPitch`/
`lookYaw` (the committed eye component) + **`CommitLookState(int)`** — side
looks yaw by the authored angle, look-behind = yaw π + `lookBackAngle` pitch,
look-down = `lookFrontAngle` pitch, forward = identity. The per-frame
eyepoint compose in Simulate now adds the live Torso elevation on top of the
committed look terms.
- Deferred inside the commit (needs the MechWeapon viewFireEnable/rearFiring
members): re-arming the per-view weapon fire enables (forward = non-rear
weapons, look-back = rear-mounted 'b'-port weapons, side/down = none) and the
HUD pip group-mask flip.
**VERIFIED headlessly:** the model reads clean authored angles (L=60° R=60°
F=10° B=0° — layout confirmed again); `BT_FORCE_LOOK=3` fires ONE commit
(`[look] state=3 yaw=3.14159 pitch=0`) and the per-frame compose holds
`eyeYaw=π` with `eyePitch=0.349066` (lookBackAngle 0 + the 20°-clamped
elevation). Zero Fail.
Next: the gait leg-clip animation / terrain drop / weapon wave. These become
VISIBLE only under the renderer; headlessly the composed angles are loggable
(as here).
## PHASE 5.3 INCREMENT 7: weapon view-fire gating (2026-07-21)
Completes the weapon-side half of the look commit:
- **MechWeapon** gains the real `rearFiring` / `viewFireEnable` members (out of
the 1995 binary's @0x334/@0x3E0) + accessors. The ctor resolves REAR-FIRING
authentically (binary ctor tail @004b99a8): the weapon's MOUNT SEGMENT site
name is tested for the 'b' (back) marker — the back gun ports
(`sitelbgunport`/`siterbgunport`) are the only port names carrying it. A
weapon spawns armed for the forward view (`viewFireEnable = !rearFiring`).
- **Mech::CommitLookState** now re-arms every roster weapon on a view change
(forward = non-rear weapons, LOOK-BACK = rear-mounted, side/down = none) and
flips the HUD reticle pip group with the authentic 1995 enum flags
(`Reticle::FrontFiringWeaponsOn`/`RearFiringWeaponsOn` — BT411's raw "bits
1/2" resolved to their real names).
**Discovery:** the TEST.EGG mech genuinely mounts TWO REAR LASERS — ERMLaser_2
on `siterbgunport`, ERMLaser_3 on `sitelbgunport` (PPCs on the u-ports, the
third laser + SRM6s on the torso ports).
**VERIFIED headlessly** (`BT_FORCE_LOOK=3`): the look-behind commit arms exactly
the two rear lasers (`rear=1 armed=1`) and disarms the five forward weapons;
`pipMask=0xfffffffe` (front pips off, rear on). Zero Fail.
### The stale-obj layout-skew trap (build410.sh hardened)
The first test runs showed the rear flags mysteriously zeroed after
construction. Root cause was NOT game code: `build410.sh cc()` skipped
recompiles purely on obj-vs-.cpp timestamps, so after MECHWEAP.HPP grew two
members, `emitter.obj` (+ ppc/gauss/projweap/...) kept the OLD MechWeapon
layout — their `Emitter::chargeLevel = 0.0f` write landed exactly on the new
`rearFiring` offset. A silent cross-TU struct-layout skew. Fix:
`build410.sh` now keeps header stamps (newest source410 engine-header mtime
invalidates every bucket; newest BT/BT_L4 header mtime additionally invalidates
the game buckets) so header edits force the dependent recompiles. CODE/ is
immutable and needs no stamp. When in doubt: purge `build410/obj/*` for a
clean baseline.
## PHASE 5.3.12: the missing SetValidFlag (2026-07-22)
The Mech ctor tail now calls `SetValidFlag()` -- every 1995 entity ctor does
(CamShip/DoorFrame/DropZone/...), and ours didn't. LOAD-BEARING:
`Entity::Dispatch` routes messages to an INVALID entity into the deferred
event queue, so the mech never received a directly-dispatched message -- the
PlayerLink bind silently never landed and every player-experience gate read a
NULL player (dev-permissive heat-always-on) until this line was restored.
Found while verifying the novice-mode heat lockout end-to-end.
## 5.3.16 — the target slot + hull zone fill (Pass 3)
- `Entity *targetEntity` @mech+0x388 (Get/SetTargetEntity; ctor NULLs it;
reservedState [202]→[201]). The authentic WRITER is the reticle pick
(render/mech4 wave); the BT_SPAWN_ENEMY harness (BTPLAYER.CPP) sets it for
headless fights.
- Hull damage-zone fill after the model-param block: re-open the type-0x14
DamageZoneStream, skip the count word the Entity base ctor consumed, and
construct `new DamageZone(this, dz, &stream)` per zone — the CLASS-SCOPE
typedef = `Mech__DamageZone` (authentic CODE/BT/BT/MECHDMG.HPP), whose ctor
parses the full BT per-zone record. Then the SetLODParentPointers pass over
all zones (binary Pass-3 order, FUN_004a1674). Full forensics in
MECHDMG.NOTES.md — the Entity ctor ONLY allocates the pointer array; an
unfilled or base-class fill is the 5.3.16 crash pair.
## 5.3.17 — the Mech damage entry + friend
- `Mech::MessageHandlerEntries/MessageHandlers` now DEFINED (chained to
JointedMover's set; Build overlays the TakeDamage slot by ID — no gap risk,
the parent chain registered it). DefaultData switched onto it.
- `TakeDamageMessageHandler` override (binary hub @004a0230): latches
`lastInflictingID` (mech+0x43c, keys the zone LOD router's same-attacker
clustering), gyro-bounce feed STAGED (log marker), cylinder unaimed-zone
resolution DEFERRED (type-0x1d table), chains to Entity's handler.
- `friend class Mech__DamageZone` (BT411 mech.hpp:301 — authentic): the
per-zone damage code walks the roster / segment table / statusAlarm.
- `IsMechDestroyed()` = statusAlarm (the @0x714 body-graphic alarm) >= 9.