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>
396 lines
23 KiB
Markdown
396 lines
23 KiB
Markdown
# 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.
|