# P3 — Locomotion Cutover (procedural slide → animation-driven gait)

Plan from the `p3-locomotion-cutover-scope` workflow (4 maps, source-verified). Goal: make BT movement
ANIMATION-DRIVEN (the walk/run clip's `[RootTranslation].z` IS the speed; feet plant by construction),
replacing the bring-up procedural slide + disconnected `gBodyAnim`.

## Ground truth (verified)
The real two-channel gait is reconstructed but **bracketed by no-op stubs on BOTH ends**, so it can't just
be "called on":
- **INPUT stubs:** `legAnimation@0x65c` / `bodyAnimation@0x6bc` are `ReconSeq` (mech.hpp:508-509);
  `ReconSeq::Advance()` returns 0, `SelectSequence`/`Reset` no-op, `keyframeData==NULL` (mechrecon.hpp:225-236).
- **OUTPUT stub:** `Matrix34 == ReconMatrix` no-op (mechrecon.hpp:460); its `SetRotation/FromQuaternion` write
  nothing → `IntegrateMotion`'s world-step (mech4.cpp:206-285) commits nothing even with nonzero input.
- `IntegrateMotion` is REAL logic (not a stub) but dead (input 0, output stub); its only caller `Mech::Simulate`
  (mech4.cpp:307) has 0 call sites. The live tick is bring-up `Mech::PerformAndWatch` (mech4.cpp:549).
- `LoadLocomotionClips` (mech3.cpp:325) would CRASH today (reads `keyframeData[..]` on the NULL stub).
- **The lever that already works:** `gBodyAnim->Animate(dt,True)` (mech4.cpp) runs the REAL engine
  `AnimationInstance::Animate` (JMOVER.cpp:1474-1700), cycling joints AND returning the clip's per-frame
  root-translation distance — which the stand-in threw away. That is the smallest safe cutover.

## Steps (each independently build+test; mech keeps moving throughout)
- **✅ STEP 1 — DONE (commit below): forward travel animation-driven via the working `AnimationInstance`.**
  Use the `adv` that `gBodyAnim->Animate(dt,True)` returns as the forward step (`localOrigin.linearPosition
  += facing * adv`); removed the `kDriveMaxSpeed * throttle * dt` slide (mech4.cpp). VERIFIED: mech walks at
  the clip's authentic ~60 u/s (was a guessed 30), FORWARD (−Z), feet plant (travel==stride); combat +
  damage un-regressed; no crash.
- **✅ STEP 2 — DONE: gait clip selection by throttle** (walk/run/reverse). `|throttle| >= 0.5` → run
  (`blhrrl` id 904), else walk (`blhwwl` id 898); throttle sign → direction (negative backs up); SetAnimation
  only on gait change (tracked by `gCurrentGait`). Added a forced-throttle VALUE (`BT_FORCE_THROTTLE=<v>`,
  btl4main.cpp) for headless testing. VERIFIED: run ~60 u/s, walk ~22 u/s (walk is ~1/3 of run — authentic
  per-clip speeds, NOT a scalar of one guess), reverse backs up (pos.z increases); combat un-regressed (enemy
  DESTROYED at walk speed, 8 hits). NOTE: the blh set has no dedicated reverse CYCLE clip (stand/walk/run +
  transitions only) → reverse plays the forward clip and drives the body backward (bring-up; real reverse in STEP 7).
- **STEP 3 — back the embedded controllers with real `AnimationInstance`** (`ReconSeq`→`AnimationInstance`,
  0x60 bytes each at 0x65c/0x6bc; adapters SelectSequence→SetAnimation, Advance→Animate, Reset, keyframe
  accessors). Infrastructure only; live path unchanged.
- **STEP 4 — call `LoadLocomotionClips(model)` at model build** (mech3.cpp:325) to populate
  `animationClips[]`/`namedClip[]` + stride caps (`standSpeed`, `walkStrideLength`, ...). Safe after Step 3.
- **STEP 5 — bring-up feed for `movementMode@0x40` + `bodyTargetSpeed@0x6b4` + `commandedSpeed`** from
  `gBTDrive.throttle` (the real `MechControlsMapper::InterpretControls` is deferred — reads unsafe App offsets).
- **STEP 6 — back `Matrix34`/`ReconMatrix` with the real engine affine matrix + base-region audit** of
  `IntegrateMotion`'s transform offsets (`+0x260` dual-use pos/quat suspected mislabel; `+0x100` labeled
  maxSpeed but used as `localToWorld`) vs `part_012.c`, anchored from `damageZoneCount@0x11c`.
- **STEP 7 — THE FULL CUTOVER:** run the real two-channel gait (`AdvanceLegAnimation`/`AdvanceBodyAnimation`
  + `IntegrateMotion`) in `PerformAndWatch`, retiring the Step-1/2 stand-in translation + free-standing
  `gBodyAnim`. Gives the authentic locally-simulated + displayed-motion gait.

**Deferred polish (post-core):** airborne/fall gaits (AdvanceBody/LegAnimationAirborne), torso-twist-to-target
(Torso is FULLY reconstructed — TorsoSimulation twist/elevation @004b5cf0 + WriteJoints; needs wiring), gyro sway.

**First-milestone recommendation:** Steps 1-2 give a visibly correct animation-driven walk/run (real speeds,
feet plant) on the already-proven engine path, with near-zero risk. Steps 3-7 are the deeper "authentic
two-channel gait" and can follow once the milestone is banked.

---

## ⭐ BASE-REGION RECONCILIATION (the STEP-6 audit — ground truth, the shared P3/P5/gyro de-risk)

This is THE root cause behind the P5 teardown crash, the gyro `+4` cross-link landmine, the dead `Mech::Simulate`
stomps, AND why `IntegrateMotion` can't be revived: the reconstructed `IntegrateMotion`/`Simulate` (mech4.cpp) use
**1995 raw binary offsets** (`this+0xNN`) that, in the 2007 RP411 engine base layout, land ON live engine fields.

**Ground-truth compiled layout (cdb `dt btl4!Mover` / `!JointedMover` / `!Mech`, from btl4.pdb):**
```
engine Mover/JointedMover:                          reconstructed Mech own region:
  +0x0C8 localToWorld (LinearMatrix)                  +0x3D4 torsoAimCurrent
  +0x0F8 localOrigin (Origin)                         +0x3E0 torsoAimTarget
  +0x114 damageZoneCount  +0x11C subsystemCount       +0x3EC netOrientation (EulerAngles)
  +0x124 updateOrigin (Origin)                        +0x458 movementMode   +0x45C movementFlags
  +0x194 creationTime (Time)                          +0x468 airborneSelect +0x46C forwardCycleRate
  +0x250 projectedOrigin (Origin)                     +0x4A4 groundCycleRate +0x4A8 airborneCycleRate
  +0x26C previousOrigin (Origin)                      +0x4C8 deathAnimationLatched
  +0x288 projectedVelocity (Motion)                   +0x4D4 legAnimation  +0x4E0 bodyAnimation (ReconSeq)
  +0x2A0 updateAcceleration (Motion)                  +0x500 arrivalTime  +0x504 simTime  +0x508 spinRate
  +0x2B8 updateVelocity (Motion)                      +0x598 motionEventName  +0x5A4 motionEventArmed
  +0x2D0 nextUpdate (Time)
  +0x2D4..0x2EC collision cluster (Count/Volume/Template/containedByNode/Lists/last/Assistant)
  +0x2F0 segmentTable  +0x308 segmentCount  +0x30C jointSubsystem   (JointedMover ends 0x318)
```

**The stomp table — every `IntegrateMotion` raw offset vs the engine field it corrupts, and the correct target:**
| raw offset (1995) | intended 1995 field | 2007 engine field it STOMPS | correct target (declared member) |
|---|---|---|---|
| `this+0x100` | motion payload A | `localOrigin` (0xF8-0x114) | (relocate) `motionPayloadA` |
| `this+0x12c` | motion payload B | `updateOrigin` (0x124-0x140) | (relocate) `motionPayloadB` |
| `this+0x260` | motionDelta (Quat) | `projectedOrigin` (0x250-0x26c) | **declare** `motionDelta` |
| `this+0x26c` | worldPose (Quat) | `previousOrigin` (0x26c-0x288) | **declare** `worldPose` |
| `this+0x298`/`0x29c` | angular accum | `projectedVelocity` (0x288-0x2a0) | **declare** `angularAccum` |
| `this+0x2a4` | torsoAimTarget | `updateAcceleration` (0x2a0-0x2b8) | ✅ existing `torsoAimTarget`@0x3E0 |
| `this+0x2d4` | netOrientation | `collisionVolumeCount` | ✅ existing `netOrientation`@0x3EC |
| `this+0x598`/`0x5a4` | motionEventName/Armed | (past base, in Mech region) | ✅ existing `motionEventName`/`motionEventArmed` |

So `torsoAimTarget`, `netOrientation`, `motionEventName/Armed`, `arrivalTime`, `simTime`, `spinRate`, `movementMode/
Flags`, cycle rates ARE already relocated as declared Mech members — but `IntegrateMotion` still reads the RAW
offsets for them AND for the 3 un-declared motion fields (`motionDelta`/`worldPose`/`angularAccum`) + the 2 motion
payloads. Reviving `IntegrateMotion` as-is would corrupt `projectedOrigin`/`previousOrigin`/`projectedVelocity`/
`updateAcceleration`/the collision cluster → exactly the P5 teardown heap corruption.

## STEP-6/7 attack (revised, concrete)
1. **Lock the ground truth** (compile-time): `friend struct MechBaseLayoutCheck` with `static_assert(offsetof(...))`
   on the engine-base fields (localOrigin@0xF8, projectedOrigin@0x250, previousOrigin@0x26c, collision cluster
   @0x2d4-0x2ec, segmentTable@0x2f0) + the relocated members (netOrientation@0x3EC, torsoAimTarget@0x3E0,
   arrivalTime@0x500, spinRate@0x508). Any future raw-offset stomp then fails the build.
2. **Declare the 3 missing motion members** (`motionDelta`/`worldPose` Quaternion, `angularAccum` Vector3D) +
   the 2 motion payloads in the Mech's OWN region (offset > 0x318), so nothing lands on the engine base.
3. **Convert every `IntegrateMotion` raw `this+0xNN` (0xNN < 0x318) to the declared member** per the table above.
   Same pass for the dead `Simulate` (so it's revivable) — this is the base-region audit HARD_PROBLEMS wanted.
4. **Back the 3 stub families with real engine types:** `ReconMatrix`→`AffineMatrix` ops (Identity/FromQuaternion/
   transform-vector); `ReconQuat*`→real `Quaternion`; `ReconSeq`→real `AnimationInstance` (the two-channel gait
   input, 0x60 bytes each @legAnimation/bodyAnimation).
5. **Revive `IntegrateMotion` in `PerformAndWatch`** (the cutover), retiring the Step-1/2 stand-in translation.
   Verify: mech walks via the real gait, no engine-base stomp, no teardown crash, combat/heat un-regressed.

Payoff: this ALSO fixes the gyro cross-link (write to the correct relocated field, not gyro+4), makes the dead
`Simulate` revivable, and removes the P5 teardown corruptor — the single highest-leverage de-risk in the port.

### PROGRESS (this pass)
- ✅ **STEP 1 — layout locked:** `MechBaseLayoutCheck` (mech4.cpp, friend of Mech) static_asserts the engine-base
  offsets (localOrigin@0xF8, projectedOrigin@0x250, previousOrigin@0x26c, collision cluster@0x2d4-0x2ec,
  collisionLists@0x2e4, segmentTable@0x2f0) + the relocated members (torsoAimTarget@0x3E0, netOrientation@0x3EC,
  arrivalTime@0x500, spinRate@0x508). All pass — the ground truth is proven in code; any stomp now fails the build.
- ✅ **STEP 2/3 (partial) — declared the missing motion members** (mech.hpp, appended so no locked offset shifts):
  `motionDelta`/`worldPose`/`worldPoseBase`/`angularAccum` (Quaternion) + `motionSourceA/B`/`motionEventVector`
  (Vector3D).  **Converted the LIVE-PATH functions** `IntegrateMotion` (@004ab1c8) and `DeadReckonPose` (@004ab188)
  — every raw `this+0xNN` (<0x318) now uses the declared member (0x260→motionDelta, 0x26c→worldPose, 0x298→
  angularAccum, 0x100/0x12c→motionSourceA/B, 0x598→motionEventVector, 0x2a4→torsoAimTarget, 0x2d4→netOrientation,
  0x138→worldPoseBase).  Build green, combat un-regressed (TARGET DESTROYED, 0 crashes).  The dead `Simulate`
  (@004ab430) still holds raw offsets — DEFERRED (off the cutover path; convert when/if reviving it).
- ✅ **STEP 4 (partial) — `ReconMatrix` backed** with the real engine `AffineMatrix` (mechrecon.hpp): `Identity`→
  `BuildIdentity()`, `FromQuaternion`/`SetRotation`→`operator=(const Quaternion&)`.  SAFE: its only live caller
  (IntegrateMotion) uses a LOCAL `bodyFrame`; other callers are the dead Simulate.
- ✅ **STEP 4 — `ReconQuat` backed** (mechrecon.hpp): `ReconQuatIdentity`→`Quaternion::Identity`,
  `ReconQuatIntegrate`→`Quaternion::Add(source, Vector3D delta)` (the exact FUN_00409f58 integrate).  SAFE: all its
  callers are dead (Simulate + the dead-until-cutover IntegrateMotion/DeadReckonPose).  Also relocated the last
  live-adjacent raw offset: the `this+0x1dc` "aim/torso" clear → a declared `aimRate` (Quaternion) member.  (NOTE: I
  earlier miscalled mech4.cpp:447 a LIVE PerformAndWatch call — it is actually in the dead `Simulate` telemetry tail
  (<0x551 where PerformAndWatch starts), so ReconQuat had no live caller after all.)  Build green, combat un-regressed.
- ⚠ **STEP 4 remaining — the GAIT (`ReconSeq`) is the hard final piece, NOT a drop-in.**  Verified: the leg/body
  controllers use `SelectSequence`/`Advance`/`Reset` (FUN_004277a8/0042790c/004283b8) — a BT-specific **SequenceController**
  keyframe player that **does NOT exist in the RP411 engine** (grep of MUNGA found no SelectSequence/Sequence/Controller).
  So "ReconSeq→AnimationInstance" (the STEP-3 note) is inaccurate.  TWO approaches, a real fork:
    (A) **Reconstruct the SequenceController** from the binary (FUN_004277a8/0042790c/004283b8 + its keyframe-table
        struct) and keep the authentic two-channel gait (AdvanceLeg/BodyAnimation + IntegrateMotion).  Most faithful;
        biggest.  NOTE: retyping legAnimation/bodyAnimation to the full controller GROWS the Mech (they are ~0xC now vs
        the binary's 0x60 each), shifting the relocated members after them (arrivalTime/spinRate) — update those
        MechBaseLayoutCheck locks (they are by-name relocations, safe to move; the engine-base locks must NOT move).
    (B) **Rewrite AdvanceLeg/BodyAnimation onto the engine `AnimationInstance`** (which STEP 1-2 already proved can
        play the gait clips).  Less faithful to the exact controller, reuses the engine, no SequenceController rebuild.
  Also still a stub: **`FUN_00408744`** (mechrecon.hpp:391) — the world-step transform (transform angularAccum by the
  bodyFrame matrix) IntegrateMotion needs; back it (AffineMatrix·Vector3D) as part of the cutover.
- **STEP 7 (revive) is unchanged** but gated on the gait decision above.

---

## ⭐ SequenceController RECONSTRUCTION SPEC (chosen path A — from the binary, part_003.c)

The BT gait controller (embedded `legAnimation`@0x65c / `bodyAnimation`@0x6bc, 0x60 bytes each). It is a BT-specific
keyframe animation player (NOT in the RP411 engine). Full decomp read: ctor @00427768, SelectSequence @004277a8,
Advance @0042790c, Reset @004283b8, dtor @004278d4. It parses the SAME animation-clip resource format the engine
`AnimationInstance` uses and writes joints through the SAME `Joint` API already backed for the gyro/torso.

**Object layout (0x60; offsets are byte offsets into the controller):**
```
+0x00 vtable (Plug base, FUN_00415e90 ctor)      +0x34 rootTranslation[] table (0xC/frame; .z@+8 = stride)
+0x0c (base refcount region)                      +0x38 jointSubsystem  (resolved from owner Mech +0x31c)
+0x14 frameCount   (resource hdr[0])              +0x3c currentFrame (int)
+0x18 jointCount   (resource hdr[1])              +0x40 owner (Mech*)
+0x1c jointIndices[] (jointCount ints)            +0x44 clipResource (ref-counted handle)
+0x20 hdr+2 (metadata ptr)                        +0x48 finishedCallback  (Mech method ptr)
+0x24 frameTimes[]  (frameCount floats)           +0x4c cbArg2   +0x50 cbArg3
+0x28 keyframeBase   +0x2c keyframeCursor         +0x54 currentTime (Scalar)
```
**Methods (behaviour + engine mapping):**
- **ctor(mech):** Plug base; store owner@0x40; `jointSubsystem@0x38 = mech->GetJointSubsystem()` (binary resolves
  mech+0x31c); clipResource@0x44 = 0.
- **SelectSequence(clipID, cb, a2, a3):** store cb@0x48/a2@0x4c/a3@0x50; reset currentFrame@0x3c + currentTime@0x54;
  release old clipResource; `FindResourceDescription(clipID)` → clipResource@0x44; parse the clip: frameCount@0x14,
  jointCount@0x18, jointIndices@0x1c, frameTimes@0x24, keyframe pointers@0x28/0x2c; compute the per-frame keyframe
  stride@0x34 by summing joint DOF sizes (hinge<3 →8B, ball==4 →0xC, balltrans==5 →0x18).
- **Advance(dt, moveJoints) -> Scalar distance:** `t = currentTime + dt`.  Walk keyframes while `frameTimes[cur] <= t`:
  for each joint (jointSubsystem->GetJoint(jointIndices[i])) snap to the keyframe pose (hinge→SetRotation(Radian),
  ball→SetRotation(EulerAngles), balltrans→ + SetTranslation) IF moveJoints; accumulate `distance += (frameTimes[cur]
  - currentTime) * rootTranslation[cur].z`; advance cur/currentTime.  If clip ended (cur==frameCount): call the
  finishedCallback (owner->*cb)(clipResource, carryover, moveJoints) and add its distance.  Else interpolate the
  partial frame (ratio = (t - currentTime)/(frameTimes[cur]-currentTime)); per joint LERP/SLERP between keyframes
  (FUN_00409390 slerp / FUN_00408848 translation-lerp / FUN_00408dd4 hinge-lerp) and write; accumulate the partial
  distance.  Return distance.  Engine helper map: joint iter→`JointSubsystem::GetJoint`; GetEulerAngles=FUN_0041cfa0;
  SetRotation(Radian)=FUN_0041d0a8; SetRotation(EulerAngles)=FUN_0041d020; SetTranslation=FUN_0041d11c;
  SetHinge/direct=FUN_0041cfc8; close-enough=FUN_00408d78(radian)/FUN_004091f4(euler)/FUN_004084fc(point);
  slerp=FUN_00409390; fabsf=FUN_004dcd00.
- **Reset(loop):** reset each joint to a default pose (identity quat + default euler/translation) via the per-type set.
- **dtor:** reinstall vtable + release clipResource.

**Wiring:** replace the `ReconSeq` stub (mechrecon.hpp) with this class (keep a `typedef ... ReconSeq` so mech2's
`legAnimation.SelectSequence/Advance/Reset` calls are unchanged); retype is automatic.  The Mech GROWS (0xC→0x60 per
controller = +0xA8) → move + update the by-name locks after them (arrivalTime/simTime/spinRate); engine-base locks
UNCHANGED.  The Mech ctor must construct legAnimation/bodyAnimation with `this` (owner).  Then STEP 7 cutover.

### ✅ BUILT (seqctl.cpp) — the SequenceController is reconstructed, linked, integrated (inert until the cutover)
- `SequenceController` class in mechrecon.hpp (extends the old ReconSeq: keeps keyframeCount/keyframeTimes/keyframeData
  accessors for mech3; adds the real fields; `typedef SequenceController ReconSeq`).  `seqctl.cpp` implements Init
  (ctor logic — resolves `owner->GetJointSubsystem()`), SelectSequence (find via `application->GetResourceFile()->
  SearchList(clip_id, 16)` + Lock + parse frameCount/jointCount/jointIndices/frameTimes/pose-base/rootTranslations),
  Advance (snap full keyframes + interpolate the partial frame via the engine Joint API, accumulate the
  rootTranslation.z distance, loop at end-of-clip), Reset (neutral pose per joint type), dtor (Unlock the clip).
  `Hinge`==8B confirmed (`{int axisNumber; Radian rotationAmount}`) → keyframe hinge snap `SetHinge(*(Hinge*)cursor)`
  is byte-faithful.  Joint I/O reuses the same engine API the gyro/torso use (GetJointType/GetEulerAngles/GetTranslation
  / SetHinge/SetRotation(Radian|EulerAngles)/SetTranslation).
- Added seqctl.cpp to btbuild/CMakeLists.txt.  Full build GREEN; combat un-regressed (TARGET DESTROYED, 0 crashes).
  Inert for now: its callers (mech2 AdvanceLeg/BodyAnimation via IntegrateMotion, mech3 LoadLocomotionClips) are all
  DEAD until the cutover, so the retype + link changes no live behavior.
- **Two known simplifications to revisit at the cutover (STEP 7):** (a) end-of-clip currently loops directly (reset to
  frame 0) instead of invoking the stored member-fn-ptr finished-callback @0x48 — faithful in INTENT (that callback is
  Mech::OnBodyAnimFinished's re-arm), refine if a non-looping callback is needed; (b) partial-frame rotation uses a
  component LERP of the euler/hinge (the binary FUN_00409390 slerps) — visually identical for small gait deltas, upgrade
  to slerp if needed.
- **Remaining for STEP 7 cutover (task 13):** (1) Mech ctor must call `legAnimation.Init(this)` + `bodyAnimation.Init(this)`
  (currently default-constructed with jointSubsystem=0 — fine while inert, REQUIRED before use); (2) call
  `LoadLocomotionClips(model)` at model build to populate the clips; (3) back the `FUN_00408744` world-step transform;
  (4) revive `IntegrateMotion` in `PerformAndWatch`, retiring the STEP-1/2 stand-in.  First point where it all
  runtime-verifies together.

### ✅ STEP 7 CUTOVER — DONE & VERIFIED LIVE (gated `BT_GAIT_CUTOVER=1`; the real controller drives locomotion)
Rather than reviving the FULL `IntegrateMotion` (leg+body channels + world-step + DeadReckonPose, whose position-apply
lives in the dead `Simulate`/`MoveAndCollide`) in one leap, the cutover swaps the STEP-1/2 forward-step SOURCE to the
real `SequenceController` on the PROVEN position path (`localOrigin += facing*adv`).  This runtime-verifies the
reconstructed controller with minimal risk and is the meaningful milestone (the authentic gait controller drives the
mech).  Implemented + verified:
- **(1) DONE** — Mech ctor (`mech.cpp`, after the body-load `ResolveJoint("jointlocal")`, where `jointSubsystem` is
  valid) calls `legAnimation.Init(this)` + `bodyAnimation.Init(this)`.  Runs always (harmless when the cutover is off).
- **cutover branch** — `PerformAndWatch` (mech4.cpp), gated `static int s_gaitCutover = getenv("BT_GAIT_CUTOVER")`.
  On a gait CHANGE it `bodyAnimation.SelectSequence(ResolveAnimationClip("blh",suffix), (void*)1 /*loop*/, 0, 0)`; each
  frame `adv = bodyAnimation.Advance(dt,1)` (the real controller animates the skeleton AND returns the clip forward
  step) → `localOrigin.linearPosition += facing*adv*dir`.  STEP-1/2 `gBodyAnim` path preserved as the `else` (default).
- **KEY BUG (found via cdb av in `ResourceFile::SearchList+0x77`, RESOURCE.cpp:498):** `SelectSequence` fetched the
  clip with `rf->SearchList(clip_id, 16)` — but `SearchList(list_id,type)` treats its arg as a resource LIST (reads the
  resource's data as an ID array + iterates) → it walked clip 904's animation bytes as garbage resource IDs → crash.
  The `clip_id` from `ResolveAnimationClip` is already a DIRECT resource ID → fixed to `rf->FindResourceDescription(clip_id)`
  + `Lock`, which mirrors the engine's own clip load `AnimationInstance::SetAnimation` (JMOVER.cpp:1406) EXACTLY (same
  header layout too: hdr[0]=frameCount, hdr[1]=jointCount, hdr[2]=footStepThreshold skipped, hdr[3]=jointIndices).
- **Verified live under cdb** (`BT_FORCE_THROTTLE=1 BT_GAIT_CUTOVER=1`): `[gait] SequenceController -> blhrrl id=904
  (run)`, mech walks (adv≈1.07, matching the STEP-1/2 baseline; pos advances), 0 crashes.  Combat regression
  (`+BT_SPAWN_ENEMY +BT_FORCE_FIRE`): identical to a same-harness BASELINE (no-cutover) run — both fire/heat/damage the
  same (`FIRED #1..#141`, `structure` climbs 0.133/hit) and both walk PAST the stationary dummy before 8 hits (no
  DESTROY in-window) — i.e. NOT a regression, just harness geometry.  `SequenceController::Advance` runs live for the
  first time with no crash.
- **DECOMP FACT (`Advance@0042790c` raw, part_003.c:6722):** distance is LUMPY BY DESIGN — whole keyframes add
  `(frameTimes[cur]-currentTime)*rootTrans[cur].z` (matches seqctl.cpp:217); the partial-frame else-branch (6821)
  interpolates joints but adds NO distance; end-of-clip (6814) CALLS the finished-callback `@0x48` recursively (passing
  the carryover time) and folds its return into distance.  Smoothness in the real game comes from VELOCITY integration
  in `IntegrateMotion`, not per-Advance — so applying `adv` directly to position (this cutover) is inherently lumpy;
  that's expected, faithful to the distance fn, and superseded once the velocity path lands.
- **STILL GATED (not default) pending fidelity refinements:** (a) end-of-clip `carryover*stride[last]` in place of the
  recursive finished-callback `@0x48` (source of the occasional adv spike at loop boundaries — needs `OnBodyAnimFinished`
  wired as the real member-fn-ptr callback that re-arms + recursively continues `Advance`); (b) component-LERP → slerp
  (`FUN_00409390`); (c) the deep win — the FULL `IntegrateMotion` velocity path (leg+body via mech2 `AdvanceLeg/
  BodyAnimation`, the `FUN_00408744` world-step, `DeadReckonPose`) which smooths the lumpy step and enables MP dead-reckon.

### ✅ STEP 7 DEEPER — the AUTHENTIC world-step (real velocity model) drives locomotion LIVE
Took the "harder path": replaced the cutover's direct position slide with the real `IntegrateMotion`
motion-tail (`@004ab1c8`), so the mech now moves through the authentic velocity→rotate→integrate model.
- **`FUN_00408744` BACKED** (mechrecon.hpp; was a no-op template stub): the world-step matrix×vector
  (part_000.c:8331, `out[i]=Σ_j v[j]·M(i,j)`). Backed via the engine `AffineMatrix::GetFromAxis` column
  basis — `out = v.x·colX + v.y·colY + v.z·colZ` reproduces the raw row-dot EXACTLY, and it is the SAME
  `GetFromAxis` convention the drive facing uses, so sign-consistent. Only real caller = mech4.cpp:304
  (gyro only name-drops it in comments).
- **KEY MAPPING (verified):** the 1995 motion transform `{ Point3D @0x260; Quaternion @0x26c }` IS the
  engine `localOrigin` — `Origin = { Point3D linearPosition; Quaternion angularPosition }` (ORIGIN.h:15);
  raw `FUN_0040ab44` builds the matrix from BOTH halves (rot from 0x26c, translation from 0x260). So the
  motion state maps directly onto `localOrigin`, NOT the parallel `motionDelta`/`worldPose` placeholders.
- **Cutover branch now (mech4.cpp):** `adv = bodyAnimation.Advance` → `localVel = {0,0,-adv/dt}` →
  `Matrix34::FromQuaternion(orient, localOrigin.angularPosition)` → `FUN_00408744(worldVel, localVel,
  orient)` → `localOrigin.linearPosition += worldVel*dt`. == `facing*adv`, through the real machinery.
- **Latent bug fixed** in the (still-dead) full `IntegrateMotion`: it set only `spinRate`@0x508 and left
  `angularAccum[2]` (velocity.z the world-step reads) stale → now sets `angularAccum[2] = -adv/dt`.
- **Verified:** walk-only `BT_GAIT_CUTOVER=1` walks straight fwd ~60 u/s, 0 crashes; combat fire/damage
  identical to STEP-1/2. ⚠ A combat run faulted in the ENGINE BGF loader (`bld08.bgf`, `Builder::~Builder`)
  walking into a building's range — pre-existing heap fragility, position-dependent (STEP-1/2 fired 600+
  clean), NOT the world-step (zero heap ops). Track separately.

### Remaining full-`IntegrateMotion` work (the rest of the harder path)
1. **`AdvanceBodyAnimation` gait STATE MACHINE** (mech2.cpp:506, real — stand→walk→run transitions via
   `SetBodyAnimation(state)` → `animationClips[state]`) in place of the inline `ResolveAnimationClip`+
   `SelectSequence`. Needs #2.
2. **`LoadLocomotionClips`** (mech3.cpp:326, real) called at model build (Mech ctor) to populate
   `namedClip[]`/`animationClips[]` + gait-speed caps (standSpeed/walkStride/reverseSpeedMax/gimp…) —
   measured via `legAnimation.SelectSequence` + `keyframeData` (now that the SequenceController is real).
   OPEN: reconcile `namedClip[]`@0x5e0 vs `animationClips[]`@0x5cc + the state→clip index mapping.
3. **Orientation INTEGRATION** — the real turn is angular-rate integration into `localOrigin.angular
   Position` (raw `FUN_00409f58(0x26c, 0x26c, angRate*dt)`), driven by control input, not the bring-up
   `gDriveHeading`. (Bring-up keeps `gDriveHeading` until the controls mapper is un-bypassed.)
4. **Velocity STORAGE + `DeadReckonPose`** — store the world velocity (raw @0x298) for MP dead-reckon;
   `DeadReckonPose(fraction)` projects the render pose forward between sim ticks (the true render smoothing).
5. Then call the real `Mech::IntegrateMotion` (retargeted to `localOrigin`) from the cutover, retiring the
   inline world-step.

### ✅ STEP 2 (steps 1-2 of the full IntegrateMotion) — LoadLocomotionClips + the gait STATE MACHINE drive the walk LIVE
Gated `BT_GAIT_SM` (requires `BT_GAIT_CUTOVER`).  The real `Mech::AdvanceBodyAnimation` now drives locomotion.
- **Step 1 — `LoadLocomotionClips`** called in the Mech ctor (gated), completes cleanly for the Blackhawk:
  resolves the full gait set + measures the speed caps (verified `walkStride=22.02`, `standSpeed=6.83`).
  **RECONCILIATION FIX:** `namedClip[]`@0x5e0 and `animationClips[]`@0x5cc were declared as SEPARATE arrays
  but in the binary are ONE (`namedClip[i]==animationClips[i+5]`, 0x5e0==0x5cc+0x14) -> the loader's writes
  weren't visible to `SetBodyAnimation`/`MeasureClipStride`.  Fixed: `namedClip` is a pointer aliased to
  `&animationClips[5]` (ctor).  Same class of bug as the field-shadowing systemic issue.
- **Step 2 — `AdvanceBodyAnimation` state machine** drives the walk: case 6/7 slews `bodyCycleSpeed` toward
  `bodyTargetSpeed` with the loaded caps, Advances `animationClips[6]=wwr`, returns the cycle distance ->
  world-step.  **FIX:** `bodyAnimationState`@0x728 == `bodyStateAlarm.level` (also declared-separate) -> re-synced
  from the alarm at the top of AdvanceBodyAnimation so `SetBodyAnimation`'s level reaches the switch.
- Verified live: mech walks forward (z 1600->-975), `adv~0.35-0.42` (authentic walk cadence, < run's 1.07),
  clip loops its 15 keyframes, 0 crashes.
- **Bring-up substitute (MARKED, mech2.cpp):** `SetBodyAnimation` passes a non-null loop sentinel `(void*)1`
  where the binary passes `PTR_LAB_0050d6fc` = the real body finished-callback (gait TRANSITION stand->walk->run
  + leg alternation, @0x48).  Without a non-null cb the SequenceController leaves the cycle stuck at its last
  keyframe (`adv=0`).  Reconstruct `PTR_LAB_0050d6fc` for authentic transitions.

### Remaining for the FULL gait (after steps 1-2)
1. **Reconstruct `PTR_LAB_0050d6fc`** (+ the leg `PTR_LAB_0050d6f0`) -- the finished-callback that transitions
   gait states (stand->walk->run->reverse) + alternates legs.  Replaces the `(void*)1` loop sentinel.
2. **LEG channel** `AdvanceLegAnimation` for visible leg stepping (body channel already drives world motion).
3. **Gait SELECTION** from throttle -> `bodyTargetSpeed`/`movementMode` (currently forces walk state 6);
   wire walk/run/reverse via the real commanded speed (the `MechControlsMapper` is still bypassed).
4. Then orientation integration + velocity storage + DeadReckonPose (the later IntegrateMotion pieces).

### ✅ TRANSITION CALLBACK — reconstructed + wired (walk/run leg alternation + walk->run transition)
The real gait finished-callback (was a `(void*)1` loop sentinel).
- **Resolved the .data fn ptr by PE-parsing** BTL4OPT.EXE: `PTR_LAB_0050d6fc`@0x50d6fc -> body cb **0x4a6d8c**;
  `PTR_LAB_0050d6f0`@0x50d6f0 -> leg cb **0x4a6928**; cbArg2/3 = 0.  Neither in the assert-anchored decomp, so
  **disassembled with capstone** + decoded the jump table (byte idx @0x4a6de9, dword targets @0x4a6e0a; 33 states
  -> 10 handlers).  Reusable: `/tmp/readptr.py` (PE VA->DWORD) + `/tmp/disas.py`, `/tmp/jt.py`, `/tmp/dh*.py`.
- **Reconstructed `Mech::BodyClipFinished` (= FUN_004a6d8c)** byte-for-byte (mech2.cpp): dispatch on
  bodyAnimationState, compare bodyTargetSpeed to the caps (standSpeed/walkStrideLength/reverseSpeedMax), pick the
  next state, `SetBodyAnimation(next)` (re-binds with THIS callback) + `bodyAnimation.Advance(carryover-derived)`
  via the shared tail `Mech::BodyTransition`.  Walk 6<->7, run 12<->13, stop->8/9, walk->run 6->11->12.
- **Plumbing:** `SequenceController::Advance` now CALLS `cbCode` as `Scalar(*)(Mech*,unsigned,Scalar,int)` at
  end-of-clip (matches the binary `**(code**)(this+0x48)`), folding the returned distance.  `SetBodyAnimation`
  passes `&Mech::BodyClipFinished`; the inline cutover passes `&Mech::LoopBodyClip` (bring-up loop).
- **Verified live (cdb):** walk alternates 6<->7 (adv~0.42), run alternates 12<->13 (adv~0.5-0.85), walk->run
  transition fires (6->11->12), pos advances, 0 crashes; inline cutover (LoopBodyClip) + combat un-regressed.
- **Fixed a bring-up gap:** `reverseSpeedMax2`@0x7a0 (run bodyCycleSpeed clamp) unset by LoadLocomotionClips ->
  0xCDCDCDCD -> run exploded; set to reverseStrideLength (case-12 divisor -> ratio~1).
- **Remaining:** LEG callback FUN_004a6928 (state@0x3b0 + motion source@0x128); airborne FUN_004a6344; gimp
  handlers (16-19; targets 0x70b2/0x7161 undecoded); gait SELECTION from real throttle (mapper still bypassed).

### ✅ PILLAR A COMPLETE — leg channel + orientation + velocity storage + DEFAULT-ON
- **LEG finished-callback** FUN_004a6928 (== PTR_LAB_0050d6f0) reconstructed (`Mech::LegClipFinished`,
  mech2.cpp): jump table @0x4a69aa decoded (same 33-state shape as the body's); compares the LIVE mapper
  speedDemand (*(subsystemArray[0])+0x128 -> typed mirror controlsMapper), slews legCycleSpeed@0x348,
  re-arms via SetLegAnimation; gimp alternates 0x12<->0x13 with |ratio| (gimpStrideLength stored negative).
- **Two-channel split LIVE** (real-controls): leg = joints from live demand, body = motion only
  (Advance(dt, 0)).  Natural Standing entry verified: legState 0 -> 5 -> 11 -> 13<->12 @ 61.5 u/s.
- **RAW FIX: Standing case INVERTED in BOTH channels** (raw case 0: standSpeed < commanded -> walk(5);
  commanded < 0 -> reverse(0x10); else stand).  Also: AdvanceLegAnimation controlSource double-deref AV ->
  controlsMapper->speedDemand; legAnimationState==legStateAlarm.level re-sync.
- **Orientation**: yaw rate composed via Quaternion::Add (FUN_00409f58 form) -- spawn orientation kept
  authentic (turn-rate constant still bring-up).
- **Velocity storage every frame**: worldLinearVelocity + localVelocity (fwd -Z + yaw rate) == the
  Mover::WriteUpdateRecord publish set = the MP dead-reckon writer data.
- **DEFAULTS FLIPPED** (BTEnvOn): BT_GAIT_CUTOVER / BT_GAIT_SM / BT_COLLISION / BT_REAL_CONTROLS default
  ON; "=0" opts back to the bring-up paths (verified).  BT_SPAWN_ENEMY now places the dummy along the
  spawn FACING (the authentic orientation exposed the old fixed-offset assumption).
- Verified: env-free default run = full authentic chain, combat damage -> 0.933, 0 crashes; all =0
  fallbacks work.  P3 is CLOSED for multiplayer purposes (the update-writer data is maintained).

### v5 (2026-07-09, task #49) — CHANNEL ROLES CORRECTED FROM RAW DISASM: display+travel = LEG
User-reported recurrence of "legs stutter / lose sync" (live [sync]: advSum/legSum drifted 5->92 during
walk-up churn then locked).  Reproduced headlessly (BT_FORCE_OSC feathering + BT_FORCE_TURN; per-frame
BT_SYNC_LOG probe): the SEED is the bring-up trn trigger arming only the LEG channel (walk re-entry lands
1 frame apart -> clip phase offset); the AMPLIFIER is a demand change landing between the two channels'
end-of-clip callbacks -> DIVERGENT next-state picks (one winds down, one keeps walking) -> opposite-phase
churn (observed bs=6 ls=7).  Under v4 (display+travel = body) the out-of-phase LEG pose showed through on
every frame the body didn't write (Standing/wind-down) = the visible stutter.
DISASM GROUND TRUTH (both Advance fns have NO static decomp caller; found by byte-scanning CODE for e8
calls): the master perf (0x4a9b5c gap region) calls AdvanceLegAnimation @0x4aa399 (airborne @0x4aa388) and
stores -dist/dt into localVelocity (+0x1cc) == the LEG drives local travel (and pose, writing last);
IntegrateMotion (0x4ab1c8; body advance @0x4ab312; caller FUN_004ab430 = the projected-origin updater)
stores -dist/dt into projectedVelocity (+0x2a0) == the BODY is the dead-reckoning PROJECTOR, locally
invisible.  v4's reading of 0x4ab430 as the travel source was WRONG.
FIX (mech4.cpp): advance body FIRST (projection; writes overwritten), leg LAST (displayed pose); travel =
legAdv under the two-channel split (travelAdv selector; single-channel path keeps adv).  Display == travel
through the LEG by construction; body drift can no longer show.  VERIFIED: trn+feather repro clean; straight
drive locked (adv==proj); solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill intact
(hdlr=241, DESTROYED).  FOLLOW-UPS: wire the body adv into the named projectedVelocity member (the +0x2a0
store, currently diagnostic-only); locate the authentic trn entry dispatcher (would also arm... TBD).
New gated instruments: BT_SYNC_LOG (per-frame channel divergence), BT_FORCE_OSC=<s> (throttle feathering).
