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 ≈ Object=part.bgf, dzone=, tranx/y/z, pitch/yaw/roll`. Build a **DCS tree**; neutral pose ≈
translations (pyr≈0). [T1] translations (pyr≈0). [T1]
- **`.ANI`** — INI keyframe animation. `[HEADER] framecount/framerate(30)/skeletonfile`; `[frameN]` - **`.ANI`** — INI keyframe animation. `[HEADER] framecount/framerate(30)/skeletonfile`; `[frameN]`
joint→`rx ry rz` radians (balltranslate→translate); `[RootTranslation]` per-keyframe `x y z` joint→**`pitch yaw roll` radians for EVERY joint type** (ANIMTOOL.cpp:140-142 parses the triple as
where **`.z` = forward root SPEED** (units/s). Walk anims have no baked root translation — forward EulerAngles unconditionally — the old "balltranslate→translate" note was wrong [T0]); a
motion is animation-driven from `.z` ([[locomotion]]). [T1] 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 ## Materials / textures
- **`.BMF`** — `DIV-BIZ2` (FILETYPE=1) material+texture LIBRARY (no pixels). Material → DIFFUSE color - **`.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 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] 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 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 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 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. destroyed-subsystem dark-panel look.
## Locomotion / combat polish (non-gating) ## 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). - 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); - Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
airborne callbacks (`FUN_004a6344`/`FUN_004a7970`). airborne callbacks (`FUN_004a6344`/`FUN_004a7970`).
+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. // segment/joint API -- it is no longer a free stand-in.
template<class...A> inline Recon LookupVideoObject(A&&...) { return Recon(); } template<class...A> inline Recon LookupVideoObject(A&&...) { return Recon(); }
template<class...A> inline int ResourceFindByName(A&&...) { return 0; } template<class...A> inline int ResourceFindByName(A&&...) { return 0; }
template<class...A> inline void LoadLowDetailBody(A&&...) {} // (LoadLowDetailBody / LoadHighDetailBody removed task #59 -- they were no-op
template<class...A> inline void LoadHighDetailBody(A&&...) {} // 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 FinishConstruction(A&&...) {}
template<class...A> inline void ResetPose(A&&...) {} template<class...A> inline void ResetPose(A&&...) {}
template<class...A> inline void SetStatusState(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 if (getenv("L4VIEWEXT") != 0) // FUN_004dee74
{
Wword(0x15f) = 1; 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. // Tell every heatable subsystem to attach to the body model.
@@ -1449,12 +1448,49 @@ Mech::Mech(
animationClips[ci] = 0; animationClips[ci] = 0;
// P3 STEP 7 (state-machine path): load the full gait clip set + speed caps into // 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 // animationClips[5..26]. GATED (BT_GAIT_CUTOVER) so the default STEP-1/2 path is
// (BT_GAIT_CUTOVER) so the default STEP-1/2 path is untouched while the AdvanceBody // untouched while the AdvanceBodyAnimation state machine is brought up (it derefs
// Animation state machine is brought up (it derefs ResolveAnimationClip unconditionally // ResolveAnimationClip unconditionally -> will fault on any clip this mech's content
// -> will fault on any clip this mech's content lacks; observe under the gate first). // 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) 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(0xe5) = 0;
Wword(0xdd) = Wword(0xde) = 1; Wword(0xdd) = Wword(0xde) = 1;
@@ -1622,6 +1658,8 @@ Mech::~Mech()
extern void BTDeregisterMech(Entity *m); // task #46 live-mech registry extern void BTDeregisterMech(Entity *m); // task #46 live-mech registry
BTDeregisterMech((Entity *)this); BTDeregisterMech((Entity *)this);
extern void BTUnstashClipState(const Mech *m); // task #59 clip-set registry
BTUnstashClipState(this);
vtable = &PTR_FUN_0050cfa8; vtable = &PTR_FUN_0050cfa8;
+1
View File
@@ -930,6 +930,7 @@ protected:
void MeasureClipStride(int slot, Scalar *total, Scalar *lastKey); // @004a8054 void MeasureClipStride(int slot, Scalar *total, Scalar *lastKey); // @004a8054
void LoadLocomotionClips(Mech__ModelResource *model); // @004a80d4 void LoadLocomotionClips(Mech__ModelResource *model); // @004a80d4
void LoadLocomotionClipsExt(Mech__ModelResource *model); // @004a86c8 void LoadLocomotionClipsExt(Mech__ModelResource *model); // @004a86c8
void MaintainViewClipSet(); // task #59 port: interior(viewpoint)/exterior(others)
static int static int
CreateSubsystemStream( CreateSubsystemStream(
+204 -17
View File
@@ -470,31 +470,218 @@ void
//########################################################################### //###########################################################################
//########################################################################### //###########################################################################
// LoadLocomotionClipsExt // LoadLocomotionClipsExt (the INTERIOR clip set)
// //
// @004a86c8 // @004a86c8
// //
// Structurally identical to LoadLocomotionClips, but reads from the *second* // The 4-character suffix table @0050d94d is the 3-letter codes with a trailing
// gait-clip suffix table at 0050d94d -- these are 4-character codes (note the // 'i' (swri, wwri, wwli, ... -- byte-verified from BTL4OPT.EXE @file 0x10d74d).
// extra uStack_19 byte the decomp materialises in the name buffer) -- and the // These are the INTERIOR ('i') gait clips, bound to the LOCAL cockpit MASTER
// optional-set markers also carry the extra character. Used for the mech // (the ctor gate @part_012.c:10308-10320): they animate `jointshakey` (the
// configuration whose clip names do not fit the 3-letter scheme. The cached // cockpit shake node) and OMIT the `jointhip` walk-lean channel the exterior
// targets (0x530/0x534/0x538/0x34c/0x52c/0x350, the optional sets, and the // clips carry -- so the pilot's own view stays level while a walking mech
// capability flags) are exactly the same as the base loader. // leans into its stride for everyone ELSE (the exterior clips, network
// // replicants + the L4VIEWEXT external view). The call/measure/store sequence
// (Body omitted here for brevity; it differs from LoadLocomotionClips only in // and the cached targets (0x530/0x534/0x538/0x34c/0x52c/0x350, the optional
// the suffix-string operands and the 4-char name length. See @004a86c8 for // sets, the capability flags) are LINE-FOR-LINE identical to the 3-char
// the byte-exact form -- the call/measure/store sequence is line-for-line // loader; only the suffix operands differ (each gains the 'i'). [T1: byte-
// identical.) // exact against @004a86c8 + the suffix-table dump.]
//########################################################################### //###########################################################################
//########################################################################### //###########################################################################
void void
Mech::LoadLocomotionClipsExt(Mech__ModelResource *model) Mech::LoadLocomotionClipsExt(Mech__ModelResource *model)
{ {
// identical control flow to LoadLocomotionClips with the 0050d94d table; const char *prefix = model->animationPrefix; // param_2 + 0x40
// see banner. Reconstructed as a thin alias to keep the cached-field
// contract documented in one place. gyroRumbleTimer = 0.0f; // this+0x5c4
LoadLocomotionClips(model); // PLACEHOLDER for the 4-char variant @004a86c8
// --- stand -> walk : standSpeed is that clip's final-keyframe stride ----
namedClip[ 0] = *ResolveAnimationClip(prefix, "swri"); // 0x5e0
legAnimation.SelectSequence(namedClip[0], PTR_LAB_0050d714, DAT_0050d718, DAT_0050d71c);
standSpeed = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x530
// --- forward walk/run : stride = (s6+s7)/(d6+d7) ------------------------
Scalar s0, d0, s1, d1;
namedClip[ 1] = *ResolveAnimationClip(prefix, "wwri"); // 0x5e4
MeasureClipStride(6, &s0, &d0);
namedClip[ 2] = *ResolveAnimationClip(prefix, "wwli"); // 0x5e8
MeasureClipStride(7, &s1, &d1);
walkStrideLength = (s0 + s1) / (d0 + d1); // 0x534
namedClip[ 3] = *ResolveAnimationClip(prefix, "wsri"); // 0x5ec walk->stand R
namedClip[ 4] = *ResolveAnimationClip(prefix, "wsli"); // 0x5f0 walk->stand L
// --- reverse base : reverseSpeedMax is that clip's final-keyframe stride -
namedClip[ 5] = *ResolveAnimationClip(prefix, "wrri"); // 0x5f4
legAnimation.SelectSequence(namedClip[5], PTR_LAB_0050d720, DAT_0050d724, DAT_0050d728);
reverseSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x538
namedClip[ 6] = *ResolveAnimationClip(prefix, "wrli"); // 0x5f8
// --- reverse cycle : stride = (s_c+s_d)/(d_c+d_d) -----------------------
namedClip[ 7] = *ResolveAnimationClip(prefix, "rrri"); // 0x5fc
MeasureClipStride(0x0c, &s0, &d0);
namedClip[ 8] = *ResolveAnimationClip(prefix, "rrli"); // 0x600
MeasureClipStride(0x0d, &s1, &d1);
reverseStrideLength = (s0 + s1) / (d0 + d1); // 0x34c
// REVERSE + BUMP set (same slot map as the 3-char loader; see its banner) --
animationClips[14] = *ResolveAnimationClip(prefix, "rwri"); // 0x604 state 0xE run->walk R
animationClips[15] = *ResolveAnimationClip(prefix, "rwli"); // 0x608 state 0xF run->walk L
animationClips[0x20] = *ResolveAnimationClip(prefix, "bmpi");// 0x64c bump/stagger clip
gimpBaseClip = animationClips[0x20];
animationClips[16] = *ResolveAnimationClip(prefix, "sbri"); // 0x60c state 0x10 stand->back R
legAnimation.SelectSequence(animationClips[16], PTR_LAB_0050d72c, DAT_0050d730, DAT_0050d734);
gimpSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x52c
animationClips[17] = *ResolveAnimationClip(prefix, "sbli"); // 0x610 state 0x11 stand->back L
animationClips[20] = *ResolveAnimationClip(prefix, "bsri"); // 0x61c state 0x14 back->stand R
animationClips[21] = *ResolveAnimationClip(prefix, "bsli"); // 0x620 state 0x15 back->stand L
// --- reverse cycle : stride = -(s+s)/(d+d), stored negative (carried s1/d1)
animationClips[18] = *ResolveAnimationClip(prefix, "bbri"); // 0x614 state 0x12 back cycle R
MeasureClipStride(0x12, &s0, &d0);
animationClips[19] = *ResolveAnimationClip(prefix, "bbli"); // 0x618 state 0x13 back cycle L
MeasureClipStride(0x13, &s0, &d0);
gimpStrideLength = -((s0 + s1) / (d0 + d1)); // 0x350
//
// ---- OPTIONAL clip set 1: JUMP JETS (probe "wgli") ----
//
jumpCapable = 0; // this+0x580
if (ResolveAnimationClip(prefix, "wgli") != 0)
{
jumpCapable = 1;
animationClips[22] = *ResolveAnimationClip(prefix, "wgli"); // 0x624 state 0x16
legAnimation.SelectSequence(animationClips[22], PTR_LAB_0050d738, DAT_0050d73c, DAT_0050d740);
jumpRunSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x53c
animationClips[23] = *ResolveAnimationClip(prefix, "wgri"); // 0x628 state 0x17
legAnimation.SelectSequence(animationClips[23], PTR_LAB_0050d744, DAT_0050d748, DAT_0050d74c);
jumpWalkSpeedMax = legAnimation.keyframeData[legAnimation.keyframeCount].stride; // 0x540
animationClips[24] = *ResolveAnimationClip(prefix, "ggri"); // 0x62c state 0x18
MeasureClipStride(0x18, &s0, &d0);
jumpRunStrideLength = s0 / d0; // 0x544
animationClips[25] = *ResolveAnimationClip(prefix, "ggli"); // 0x630 state 0x19
MeasureClipStride(0x19, &s0, &d0);
jumpWalkStrideLength = s0 / d0; // 0x548
animationClips[26] = *ResolveAnimationClip(prefix, "gsli"); // 0x634 state 0x1A
animationClips[27] = *ResolveAnimationClip(prefix, "gsri"); // 0x638 state 0x1B
}
//
// ---- OPTIONAL clip set 2: SQUAT (probe "squi") ----
//
hasReverseGimpSet = 0; // this+0x584 (squatCapable)
if (ResolveAnimationClip(prefix, "squi") != 0)
{
hasReverseGimpSet = 1;
animationClips[3] = *ResolveAnimationClip(prefix, "squi"); // 0x5d8
animationClips[2] = *ResolveAnimationClip(prefix, "sqdi"); // 0x5d4
}
//
// ---- OPTIONAL clip set 3: TURN-IN-PLACE (probe "trni") ----
//
hasCrashSet = 0; // this+0x588 (turnCapable)
if (ResolveAnimationClip(prefix, "trni") != 0)
{
hasCrashSet = 1;
animationClips[4] = *ResolveAnimationClip(prefix, "trni"); // 0x5dc
}
DEBUG_STREAM << "[loadclips] INTERIOR ('i') set: fScale=" << forwardThrottleScale
<< " walkStride=" << walkStrideLength << " jumpCapable=" << jumpCapable
<< "\n" << std::flush;
}
//###########################################################################
//###########################################################################
// MaintainViewClipSet (port adaptation, task #59)
//
// The authentic clip-set gate (@part_012.c:10308-10320) keys on the replicant
// COPY bit (instanceFlags & 0xC == 4), which the port never sets -- ALL MP mechs
// are built as local masters (replication rides the mech registry + update
// records, not the copy-performance path; the heat sim / scoring / torso
// watcher-connect all run on both, by design). So the ctor gate lands every
// mech on the INTERIOR set. We restore the authentic RESULT by selecting the
// set by VIEWPOINT instead, maintained per frame: the mech you PILOT (== the
// viewpoint entity) keeps the INTERIOR set (level cockpit -- you never see your
// own mech externally in the real pod); every OTHER mech gets the EXTERIOR set
// (the authored -8deg jointhip walk lean, which you DO see from your cockpit /
// the chase cam). Reloads only on a viewpoint-status CHANGE (no per-frame
// thrash), and the two sets share identical stride data so the swap is seamless.
// The model pointer is stashed at ctor (the GameModel resource persists for the
// mission). Fixed array -- mech counts are tiny, no allocator in the tick path.
//###########################################################################
//###########################################################################
namespace {
struct BTClipEntry { const Mech *mech; Mech__ModelResource *model; int interior; };
BTClipEntry s_clipReg[32];
int s_clipRegCount = 0;
}
void BTStashClipState(const Mech *m, Mech__ModelResource *model, int interior)
{
for (int i = 0; i < s_clipRegCount; ++i)
if (s_clipReg[i].mech == m)
{
s_clipReg[i].model = model;
s_clipReg[i].interior = interior;
return;
}
if (s_clipRegCount < 32)
{
s_clipReg[s_clipRegCount].mech = m;
s_clipReg[s_clipRegCount].model = model;
s_clipReg[s_clipRegCount].interior = interior;
++s_clipRegCount;
}
}
void BTUnstashClipState(const Mech *m)
{
for (int i = 0; i < s_clipRegCount; ++i)
if (s_clipReg[i].mech == m)
{
s_clipReg[i] = s_clipReg[--s_clipRegCount];
return;
}
}
void
Mech::MaintainViewClipSet()
{
if (application == 0 || !BTEnvOn("BT_GAIT_CUTOVER", 1))
return;
if (getenv("L4VIEWEXT") != 0) // forced-external debug view: leave EXTERIOR everywhere
return;
Entity *vp = application->GetViewpointEntity();
if (vp == 0) // viewpoint not assigned yet -- keep the current set
return;
BTClipEntry *e = 0;
for (int i = 0; i < s_clipRegCount; ++i)
if (s_clipReg[i].mech == this) { e = &s_clipReg[i]; break; }
if (e == 0 || e->model == 0) // not registered / no model -- nothing to flip
return;
const int wantInterior = ((Entity *)this == vp) ? 1 : 0;
if (wantInterior == e->interior) // already holding the right set
return;
if (wantInterior)
LoadLocomotionClipsExt(e->model); // interior: level cockpit (your own mech)
else
LoadLocomotionClips(e->model); // exterior: the authored walk lean (every other mech)
e->interior = wantInterior;
DEBUG_STREAM << "[clipfix] mech " << (void *)this << " -> "
<< (wantInterior ? "INTERIOR (level)" : "EXTERIOR (lean)") << "\n" << std::flush;
} }
+6
View File
@@ -1662,6 +1662,12 @@ void
const Logical isPlayerMech = const Logical isPlayerMech =
(application != 0 && (Entity *)this == application->GetViewpointEntity()); (application != 0 && (Entity *)this == application->GetViewpointEntity());
// task #59: keep this mech on the authentic gait clip set for its role --
// INTERIOR (level cockpit) if it's the mech you pilot, EXTERIOR (the -8deg
// walk lean) for every other mech. One-time flip per viewpoint change; the
// ctor lands everyone on interior because the port never sets the copy bit.
MaintainViewClipSet();
// DEATH consumer -- runs for EVERY mech (player + spawned targets). On a // DEATH consumer -- runs for EVERY mech (player + spawned targets). On a
// vital kill it collapses + shuts the subsystems down + settles to disabled; // vital kill it collapses + shuts the subsystems down + settles to disabled;
// the drive's movementMode write below is guarded on !IsMechDestroyed so a // the drive's movementMode write below is guarded on !IsMechDestroyed so a
+16
View File
@@ -198,6 +198,22 @@ Scalar
int ty = jt ? (int)jt->GetJointType() : 0; int ty = jt ? (int)jt->GetJointType() : 0;
if (ty < 3) // hinge if (ty < 3) // hinge
{ {
// task #59 discriminator (BT_HIP_LOG): log hinge writes so the
// jointhip walk-lean can be seen. EXTERIOR clips ramp jointhip
// to ~-8deg on stand->walk and hold it; INTERIOR ('i') clips
// never bind jointhip, so it stays silent. Sampled by joint
// slot so the stream stays readable.
if (jt && move_joints)
{
static const int s_hipLog = getenv("BT_HIP_LOG") ? 1 : 0;
if (s_hipLog)
{
const Radian ang = ((const Hinge *)cursor)->rotationAmount;
if (fabsf((float)ang) > 0.02f) // only the leaning ones
DEBUG_STREAM << "[hip] jointIdx=" << jointIndices[j]
<< " deg=" << ((float)ang * 57.2958f) << "\n" << std::flush;
}
}
if (jt && move_joints) jt->SetHinge(*(const Hinge *)cursor); // FUN_0041cfc8 if (jt && move_joints) jt->SetHinge(*(const Hinge *)cursor); // FUN_0041cfc8
cursor += 8; cursor += 8;
} }