Gait: the AUTHENTIC walk lean -- interior/exterior clip sets, level cockpit + leaning mechs (task #59)

The MadCat (and every leg-mech) leans -8deg into a walk / -11deg into a run --
an authored jointhip (hingex) pose in the EXTERIOR gait clips that the walk/run
CYCLE clips never rebind, so it HOLDS.  The 1995 game keeps a separate INTERIOR
('i'-suffix) clip set for the mech you PILOT: those shake jointshakey (cockpit
rattle) and OMIT jointhip, so your own view stays level while everyone else sees
you lean.  The port had this entirely dead -- the local cockpit mech pitched
-8deg into the ground (user-reported "staring at the ground").

ROOT CAUSE (two stacked bugs):
- The authentic ctor clip-set gate (@part_012.c:10308-10320) was reconstructed
  as no-op stubs: LoadLowDetailBody/LoadHighDetailBody MISLABELED the
  FUN_004a80d4/86c8 GAIT-CLIP loader addresses as a "body LOD" pair, so the gate
  did nothing and a separate unconditional LoadLocomotionClips always loaded
  EXTERIOR clips.
- LoadLocomotionClipsExt (the interior loader) was a stub aliased to the
  exterior loader.

FIX:
- Reconstructed the real 4-char INTERIOR loader (byte-exact vs @004a86c8; the
  'i'-suffix table dumped from the exe @0x10d74d: swri/wwri/wwli/...; all 18
  base clips confirmed present in BTL4.RES for mad/blh/ava).
- Fixed the ctor gate: getenv("L4VIEWEXT") || (instanceFlags&0xC)==4 -> exterior
  (replicant / forced external view); else -> interior (local cockpit master).
  Removed the mislabeling no-op stubs.
- PORT ADAPTATION (MaintainViewClipSet, per-frame in PerformAndWatch): the port
  never sets the replicant COPY bit (all MP mechs build as local masters --
  heat-sim/scoring/torso-watcher-connect all run on both, by design), so the
  ctor gate lands everyone on interior.  Pick the set by VIEWPOINT instead: the
  mech you pilot keeps interior (level cockpit), every other mech flips once to
  exterior (the lean).  Reloads only on a viewpoint-status change; the sets
  share stride data so the swap is seamless.  Model pointers stashed at ctor.

VERIFIED live (BT_HIP_LOG probe): your walking mech writes jointhip=0 (level);
the peer's replica in the other pod's view leans at exactly -8.0/-11.1deg --
the authored clip values.  A/B control: L4VIEWEXT=1 forces the walking master
back to 74 sustained -8/-11 writes.

KB: locomotion.md ("NO walk lean" audit conclusion CORRECTED -- the lean is
authored + sustained + clip-set-specific), asset-formats.md (.ANI parses
pitch/yaw/roll for all joint types; KeyJointPos translation; cycle clips carry
no pitch), open-questions.md (item closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-13 14:46:22 -05:00
co-authored by Claude Fable 5
parent 02cdfd6576
commit 2e9c78d604
8 changed files with 348 additions and 39 deletions
+10 -3
View File
@@ -20,9 +20,16 @@ spec is `docs/ASSET_PIPELINE.md`. Full list: `docs/PROGRESS_LOG.md §5`.
Object=part.bgf, dzone=, tranx/y/z, pitch/yaw/roll`. Build a **DCS tree**; neutral pose ≈
translations (pyr≈0). [T1]
- **`.ANI`** — INI keyframe animation. `[HEADER] framecount/framerate(30)/skeletonfile`; `[frameN]`
joint→`rx ry rz` radians (balltranslate→translate); `[RootTranslation]` per-keyframe `x y z`
where **`.z` = forward root SPEED** (units/s). Walk anims have no baked root translation — forward
motion is animation-driven from `.z` ([[locomotion]]). [T1]
joint→**`pitch yaw roll` radians for EVERY joint type** (ANIMTOOL.cpp:140-142 parses the triple as
EulerAngles unconditionally — the old "balltranslate→translate" note was wrong [T0]); a
balltranslate joint ADDITIONALLY gets a per-frame translation from the separate **`[KeyJointPos]`**
section (ANIMTOOL.cpp:478-483 packs EulerAngles+Vector3D; JMOVER/seqctl apply SetRotation +
SetTranslation). For the root `jointlocal`, KeyJointPos.y = the gait vertical bob;
`[RootTranslation]` per-keyframe `x y z` where **`.z` = forward root SPEED** (units/s). Walk anims
have no baked root translation — forward motion is animation-driven from `.z` ([[locomotion]]).
Walk/run CYCLE clips author NO body pitch — root keys are a ≤1.5° roll sway only; the authored
LEAN lives in the transition (`rwl/rwr/wrl/wrr` jointhip ~7-12°) + `bmp` stagger (~25°) clips
(2026-07-13 sweep of all 331 source ANIs). [T1]
## Materials / textures
- **`.BMF`** — `DIV-BIZ2` (FILETYPE=1) material+texture LIBRARY (no pixels). Material → DIFFUSE color
+42
View File
@@ -22,6 +22,48 @@ engine `AnimationInstance::Animate(dt, move_joints)` integrates `movement += Δt
moves the body that far → feet plant by construction (no stride constant; k=1). Idle anims have
z=0. The `.MOD` physics governed thrust VEHICLES, NOT walk gait. [T1]
## The walk lean IS authored — and INTERIOR clips omit it (2026-07-13, task #59 CORRECTION) [T1/T2]
**A walking mech DOES lean forward — it's authored, sustained, and clip-set-specific.** (This
supersedes the earlier "NO walk lean" audit conclusion, which was right that no CODE writes body
pitch and that the CYCLE clips carry no pitch keys, but wrong about the net behavior: the
TRANSITION clips ramp `jointhip` (hingex, parent of `jointtorso` → the whole upper body + cockpit)
to a steady pose that the cycle clips never rebind, so it HOLDS.) Decoded from BTL4.RES + live
`[hip]` probe [T2]:
- `<pfx>swr` ramps `jointhip` 0 → **8°** (stand→walk), `<pfx>wrl/wrr` 8 → **11.1°** (walk→run),
`<pfx>wsl/wsr` 8 → 0 (walk→stand). The dedicated return clip only exists because the lean
persists. The BLH has NO `jointhip` channel in any clip → it stays level (both views).
- **Two clip sets per mech.** The ctor gate (@part_012.c:10308-10320) binds the **INTERIOR** set
(4-char `'i'` suffix, `LoadLocomotionClipsExt` @004a86c8) to the **local cockpit master** — those
clips animate `jointshakey` (cockpit shake, a BALL joint) and OMIT `jointhip`, so the PILOT's own
view stays level. The **EXTERIOR** set (3-char, `LoadLocomotionClips` @004a80d4) goes to network
REPLICANTS and the forced `L4VIEWEXT` external view — it carries the `jointhip` lean, so everyone
ELSE sees the mech lean into its stride.
- **Port fix (task #59):** the interior loader was a stub aliased to the exterior loader, and the
authentic ctor gate was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the
FUN_004a80d4/86c8 clip-loader addresses as a body-LOD pair) — so the local cockpit mech played
exterior clips and pitched 8°. Now `LoadLocomotionClipsExt` is the real 4-char interior loader
and the ctor gate selects it (`getenv("L4VIEWEXT") || (instanceFlags&0xC)==4` → exterior; else
interior). Verified live: default (interior) → 4 stray `jointhip` writes over a continuous walk;
`L4VIEWEXT=1` (exterior) → 74 sustained writes at 8/11°.
- **Copy-flag → VIEWPOINT adaptation (the other half, DONE):** the port never sets the replicant
COPY bit (`instanceFlags & 0xC == 4`) — all MP mechs build as local masters (replication rides
the registry + update-records; heat-sim/scoring/torso-watcher-connect all run on both by design,
so flipping the bit would break them). The ctor gate therefore lands EVERY mech on interior. To
restore the authentic RESULT, `Mech::MaintainViewClipSet()` (mech3.cpp, called each frame from
`PerformAndWatch`) picks the set by VIEWPOINT: the mech you pilot (== `GetViewpointEntity()`)
keeps INTERIOR (level cockpit — you never see your own mech externally in the real pod); every
other mech flips ONCE to EXTERIOR (the 8/11° lean). Reloads only on a viewpoint-status change;
the two sets share stride data so the swap is seamless. Verified live: your mech `jointhip`=0
while walking; the peer's replica leans at 8°/11.1° in your view. (Model pointers stashed at
ctor via `BTStashClipState`; the GameModel resource persists for the mission.)
Other pitch sources (unchanged, still correct): `bmp` stagger (≈25° — THE knockdown pitch); JAK's
four directional fall clips (`JAKFB/FF/FL/FR`, root pitch to 1.79 rad — the only mech with them);
`IntegrateMotion`@0x4ab1c8 rotates orientation by `angularVelocity`@0x2d4·dt (yaw only); ground
snap is Y-only (terrain pitch/roll → `jointshadow`). The gyro's `bodyOrientation` spring
(@004b30ec) writes only `jointeye` (cockpit hit-bounce, not a lean). `fallDirection`@0x4a8/
`fallScalar`@0x4b4 are record-payload only. [T1]
## The MARCHING-GHOST desync — ROOT-CAUSED + FIXED (2026-07-13, live diagnosis) [T2]
The long-parked "replicant walks in place while its master stands still" reproduced live with
telemetry running: the master's `[gaitSM]` showed `state=0 cycleSpeed=-2.50725 kfCur=19` — BOTH
+12
View File
@@ -344,6 +344,18 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
destroyed-subsystem dark-panel look.
## Locomotion / combat polish (non-gating)
- **✅ Interior vs exterior gait CLIP SET — DONE 2026-07-13 (task #59) [T2].** The local cockpit
mech was leaning 8° into every walk because the authentic ctor clip-set gate
(@part_012.c:10308-10320) was a no-op (`LoadLowDetailBody`/`LoadHighDetailBody` mislabeled the
FUN_004a80d4/86c8 clip-loader addresses) and `LoadLocomotionClipsExt` was a stub aliased to the
exterior loader. Reconstructed the real 4-char INTERIOR (`'i'`-suffix) loader (omits `jointhip`,
shakes `jointshakey`). Because the port never sets the replicant copy bit the ctor gate lands
everyone on interior, so `Mech::MaintainViewClipSet()` picks the set by VIEWPOINT each frame
(your mech → interior/level; every other mech → exterior/lean). Both verified live: cockpit
level, peer replica leans 8/11°. See [[locomotion]] "The walk lean IS authored".
- FOLLOW-UP (non-blocking): if the copy bit ever gets set at construction for other reasons,
the ctor gate already handles it and MaintainViewClipSet becomes a no-op agreement — no
conflict, but revisit whether the per-frame maintenance is still needed.
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
- Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).