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
+57 -19
View File
@@ -246,8 +246,9 @@ inline ReconRegistryT *ReconRegistry() { static ReconRegistryT r; return &r; }
// segment/joint API -- it is no longer a free stand-in.
template<class...A> inline Recon LookupVideoObject(A&&...) { return Recon(); }
template<class...A> inline int ResourceFindByName(A&&...) { return 0; }
template<class...A> inline void LoadLowDetailBody(A&&...) {}
template<class...A> inline void LoadHighDetailBody(A&&...) {}
// (LoadLowDetailBody / LoadHighDetailBody removed task #59 -- they were no-op
// stubs mislabeling the FUN_004a80d4/86c8 GAIT-CLIP loaders as a body-LOD
// pair; the authentic clip-set gate now calls the real loaders.)
template<class...A> inline void FinishConstruction(A&&...) {}
template<class...A> inline void ResetPose(A&&...) {}
template<class...A> inline void SetStatusState(A&&...) {}
@@ -1354,21 +1355,19 @@ Mech::Mech(
}
//
// Choose the LOD / view variant of the body model.
// CLIP-SET / VIEW gate (@part_012.c:10308-10320). CORRECTION (task #59):
// the two branch targets here are FUN_004a80d4 / FUN_004a86c8 -- which are
// the GAIT-CLIP loaders (LoadLocomotionClips / LoadLocomotionClipsExt), NOT
// a "body LOD" pair (the old LoadLowDetailBody/LoadHighDetailBody names were
// no-op stubs, so this authentic gate did NOTHING and every mech silently
// fell back to the unconditional exterior load below -> the local cockpit
// mech played the EXTERIOR clips and leaned -8deg into every walk). The
// only real side effect at THIS point is the L4VIEWEXT flag; the clip LOAD
// itself is deferred to the gait-clip site below (after animationClips[] is
// zeroed), where the same three-way selection is applied.
//
if (getenv("L4VIEWEXT") != 0) // FUN_004dee74
{
Wword(0x15f) = 1;
LoadLowDetailBody(model); // FUN_004a80d4 [mech2]
}
else if ((instanceFlags & 0xc) == 4)
{
LoadLowDetailBody(model); // FUN_004a80d4 [mech2]
}
else
{
LoadHighDetailBody(model); // FUN_004a86c8 [mech2]
}
//
// Tell every heatable subsystem to attach to the body model.
@@ -1449,12 +1448,49 @@ Mech::Mech(
animationClips[ci] = 0;
// P3 STEP 7 (state-machine path): load the full gait clip set + speed caps into
// animationClips[5..26] via LoadLocomotionClips (mech3.cpp @004a80d4 -- real). GATED
// (BT_GAIT_CUTOVER) so the default STEP-1/2 path is untouched while the AdvanceBody
// Animation state machine is brought up (it derefs ResolveAnimationClip unconditionally
// -> will fault on any clip this mech's content lacks; observe under the gate first).
// animationClips[5..26]. GATED (BT_GAIT_CUTOVER) so the default STEP-1/2 path is
// untouched while the AdvanceBodyAnimation state machine is brought up (it derefs
// ResolveAnimationClip unconditionally -> will fault on any clip this mech's content
// lacks; observe under the gate first).
//
// AUTHENTIC clip-set gate (@part_012.c:10308-10320, task #59): the local cockpit
// MASTER gets the INTERIOR clip set (LoadLocomotionClipsExt @004a86c8, 4-char 'i'
// suffix) whose clips shake `jointshakey` and OMIT the `jointhip` walk lean; network
// REPLICANTS and the forced L4VIEWEXT external view get the EXTERIOR set
// (LoadLocomotionClips @004a80d4, 3-char) carrying the authored -8deg lean. This is
// the SINGLE loader call the decomp ctor makes (the earlier no-op body-LOD gate was
// this same decomp branch mis-reconstructed -- see the L4VIEWEXT block above).
//
// PORT NOTE (task #59): the authentic discriminator is the replicant COPY bit
// (instanceFlags & 0xC == 4), which the 1995 game sets when it constructs a
// network replicant. The port does NOT set that bit at construction (all MP
// mechs are built as local masters; replication rides the mech registry +
// update-records, not the copy-performance path) -- so BOTH the local mech
// and the peer's replica currently take the ELSE (interior) branch. That is
// correct for the LOCAL cockpit (level, the reported bug) but means the peer's
// mech does not carry the exterior -8deg lean in the OTHER pod's view yet;
// that lights up automatically once the replicant copy bit is set at
// construction (tracked in open-questions -- do NOT fake it with a viewpoint
// test here: the clips load once at ctor, before the viewpoint is assigned).
int loadedInterior = 0;
if (BTEnvOn("BT_GAIT_CUTOVER", 1)) // default ON (=0 to disable)
LoadLocomotionClips(model);
{
if (getenv("L4VIEWEXT") != 0 || (instanceFlags & 0xc) == 4)
{
LoadLocomotionClips(model); // exterior (3-char): replicant / forced-ext view
}
else
{
LoadLocomotionClipsExt(model); // interior (4-char 'i'): the local cockpit master
loadedInterior = 1;
}
}
// Register this mech's model + which clip set it currently holds, so the
// per-frame MaintainViewClipSet() (mech4 PerformAndWatch) can flip it to the
// authentic set once the viewpoint is assigned (the copy bit is never set in
// the port, so the ctor gate lands everyone on interior -- see the note above).
extern void BTStashClipState(const Mech *m, Mech__ModelResource *model, int interior);
BTStashClipState(this, model, loadedInterior);
Wword(0xe5) = 0;
Wword(0xdd) = Wword(0xde) = 1;
@@ -1622,6 +1658,8 @@ Mech::~Mech()
extern void BTDeregisterMech(Entity *m); // task #46 live-mech registry
BTDeregisterMech((Entity *)this);
extern void BTUnstashClipState(const Mech *m); // task #59 clip-set registry
BTUnstashClipState(this);
vtable = &PTR_FUN_0050cfa8;