Reconstructs the binary's five-state LOOK machine (controls mapper tail, part_013.c:396-459) as proper members/methods: - MECHMPPR: LookState enum (None/Left/Right/Behind/Down) + lookState/ previousLookState (out of reserved[24] -> [22]). InterpretControls picks the state from the look buttons each frame and on a CHANGE calls Mech::CommitLookState. BT_FORCE_LOOK=<1..4> dev hook. - MECH: authored look angles lookLeft/Right/Front/BackAngle (deg->rad from the GameModel resource, guarded) + lookPitch/lookYaw + CommitLookState(int): side looks yaw by the authored angle, look-behind = yaw pi + lookBackAngle pitch, look-down = lookFrontAngle pitch, forward = identity. The per-frame eyepoint compose in Simulate adds the live Torso elevation on top. (reservedState [208] -> [202].) - Deferred inside the commit (needs MechWeapon viewFireEnable/rearFiring): per-view weapon fire re-arm + HUD pip group-mask flip. Verified headlessly: model reads clean authored angles (L=60 R=-60 F=-10 B=0 deg -- ModelResource 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=pi with eyePitch=0.349066 (lookBackAngle + the 20deg-clamped elevation). Zero Fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
19 KiB
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 /MakeAndLinkViewpointEntitycalls.- The static shared data (ClassDerivations / DefaultData) — from the earlier staging.
- A staged Mech ctor that chains the real
JointedMoverbase 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:
- 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. - 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.
- 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)
- Derive the Mech member layout. Turn
reserved[331]into named members. Sources: BT411's offset comments (this[0x14a]gyroSubsystem,+0x388target 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'snew Mechallocation. - 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).
- Reconstruct the Mech ctor against the named layout: embedded-member construction, the segment-table walk, subsystem instantiation + wiring.
- 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:
- 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.
- Watcher / capability-chain / plug wiring in the Mech ctor (controllable/ heatable/weaponRoster/damageable) so PerformAndWatch has valid targets.
- The per-frame simulation itself: mech2 (animation SM), mech3, mech4 (locomotion/targeting) -- ~9K lines, currently all staged Fail.
- 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):
- 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; - heading integration (rotate
localOrigin.angularPositionby the turn rate); - terrain-height drop (
BoundingBoxTreeNode::FindBoundingBoxUnder) resting the feet on the ground; - cockpit telemetry
FilteredScalars (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
currentBodySpeedtoward the demand (bounded bymaxBodyAcceleration); - computes the authentic per-mech turn rate
authTurnRate= lerp(walkingTurnRate, runningTurnRate) by ground speed with arunTR/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 setsworldLinearVelocity = 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 intoInterpretControls): 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_DUMPper-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 pushescurrentTwistonto it viaJoint::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):LookStateenum (None/Left/Right/Behind/Down),lookState/previousLookStatemembers; InterpretControls picks the state from the look buttons each frame and, ON A CHANGE, callsmech->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 π +lookBackAnglepitch, look-down =lookFrontAnglepitch, 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).