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:
co-authored by
Claude Fable 5
parent
02cdfd6576
commit
2e9c78d604
+204
-17
@@ -470,31 +470,218 @@ void
|
||||
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
// LoadLocomotionClipsExt
|
||||
// LoadLocomotionClipsExt (the INTERIOR clip set)
|
||||
//
|
||||
// @004a86c8
|
||||
//
|
||||
// Structurally identical to LoadLocomotionClips, but reads from the *second*
|
||||
// gait-clip suffix table at 0050d94d -- these are 4-character codes (note the
|
||||
// extra uStack_19 byte the decomp materialises in the name buffer) -- and the
|
||||
// optional-set markers also carry the extra character. Used for the mech
|
||||
// configuration whose clip names do not fit the 3-letter scheme. The cached
|
||||
// targets (0x530/0x534/0x538/0x34c/0x52c/0x350, the optional sets, and the
|
||||
// capability flags) are exactly the same as the base loader.
|
||||
//
|
||||
// (Body omitted here for brevity; it differs from LoadLocomotionClips only in
|
||||
// the suffix-string operands and the 4-char name length. See @004a86c8 for
|
||||
// the byte-exact form -- the call/measure/store sequence is line-for-line
|
||||
// identical.)
|
||||
// The 4-character suffix table @0050d94d is the 3-letter codes with a trailing
|
||||
// 'i' (swri, wwri, wwli, ... -- byte-verified from BTL4OPT.EXE @file 0x10d74d).
|
||||
// These are the INTERIOR ('i') gait clips, bound to the LOCAL cockpit MASTER
|
||||
// (the ctor gate @part_012.c:10308-10320): they animate `jointshakey` (the
|
||||
// cockpit shake node) and OMIT the `jointhip` walk-lean channel the exterior
|
||||
// clips carry -- so the pilot's own view stays level while a walking mech
|
||||
// leans into its stride for everyone ELSE (the exterior clips, network
|
||||
// replicants + the L4VIEWEXT external view). The call/measure/store sequence
|
||||
// and the cached targets (0x530/0x534/0x538/0x34c/0x52c/0x350, the optional
|
||||
// sets, the capability flags) are LINE-FOR-LINE identical to the 3-char
|
||||
// loader; only the suffix operands differ (each gains the 'i'). [T1: byte-
|
||||
// exact against @004a86c8 + the suffix-table dump.]
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
void
|
||||
Mech::LoadLocomotionClipsExt(Mech__ModelResource *model)
|
||||
{
|
||||
// identical control flow to LoadLocomotionClips with the 0050d94d table;
|
||||
// see banner. Reconstructed as a thin alias to keep the cached-field
|
||||
// contract documented in one place.
|
||||
LoadLocomotionClips(model); // PLACEHOLDER for the 4-char variant @004a86c8
|
||||
const char *prefix = model->animationPrefix; // param_2 + 0x40
|
||||
|
||||
gyroRumbleTimer = 0.0f; // this+0x5c4
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user