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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`).
|
||||
|
||||
+57
-19
@@ -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;
|
||||
|
||||
|
||||
@@ -930,6 +930,7 @@ protected:
|
||||
void MeasureClipStride(int slot, Scalar *total, Scalar *lastKey); // @004a8054
|
||||
void LoadLocomotionClips(Mech__ModelResource *model); // @004a80d4
|
||||
void LoadLocomotionClipsExt(Mech__ModelResource *model); // @004a86c8
|
||||
void MaintainViewClipSet(); // task #59 port: interior(viewpoint)/exterior(others)
|
||||
|
||||
static int
|
||||
CreateSubsystemStream(
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1662,6 +1662,12 @@ void
|
||||
const Logical isPlayerMech =
|
||||
(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
|
||||
// vital kill it collapses + shuts the subsystems down + settles to disabled;
|
||||
// the drive's movementMode write below is guarded on !IsMechDestroyed so a
|
||||
|
||||
@@ -198,6 +198,22 @@ Scalar
|
||||
int ty = jt ? (int)jt->GetJointType() : 0;
|
||||
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
|
||||
cursor += 8;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user