From 2e9c78d6043a5aaf7dc6d4a97f558439d997422d Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 13 Jul 2026 14:46:22 -0500 Subject: [PATCH] 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 --- context/asset-formats.md | 13 +- context/locomotion.md | 42 +++++++ context/open-questions.md | 12 ++ game/reconstructed/mech.cpp | 76 +++++++++--- game/reconstructed/mech.hpp | 1 + game/reconstructed/mech3.cpp | 221 +++++++++++++++++++++++++++++++--- game/reconstructed/mech4.cpp | 6 + game/reconstructed/seqctl.cpp | 16 +++ 8 files changed, 348 insertions(+), 39 deletions(-) diff --git a/context/asset-formats.md b/context/asset-formats.md index f708c44..b9327ba 100644 --- a/context/asset-formats.md +++ b/context/asset-formats.md @@ -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 diff --git a/context/locomotion.md b/context/locomotion.md index d1111af..836cb2b 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -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]: +- `swr` ramps `jointhip` 0 → **−8°** (stand→walk), `wrl/wrr` −8 → **−11.1°** (walk→run), + `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 diff --git a/context/open-questions.md b/context/open-questions.md index 99bb7b9..46bcdb1 100644 --- a/context/open-questions.md +++ b/context/open-questions.md @@ -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`). diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index b8eda0b..c25b95f 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -246,8 +246,9 @@ inline ReconRegistryT *ReconRegistry() { static ReconRegistryT r; return &r; } // segment/joint API -- it is no longer a free stand-in. template inline Recon LookupVideoObject(A&&...) { return Recon(); } template inline int ResourceFindByName(A&&...) { return 0; } -template inline void LoadLowDetailBody(A&&...) {} -template 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 inline void FinishConstruction(A&&...) {} template inline void ResetPose(A&&...) {} template 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; diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index e25b895..f658141 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -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( diff --git a/game/reconstructed/mech3.cpp b/game/reconstructed/mech3.cpp index 5955911..b04e42b 100644 --- a/game/reconstructed/mech3.cpp +++ b/game/reconstructed/mech3.cpp @@ -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; } diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index bf2cbc6..980447e 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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 diff --git a/game/reconstructed/seqctl.cpp b/game/reconstructed/seqctl.cpp index 63847f2..1a6ab31 100644 --- a/game/reconstructed/seqctl.cpp +++ b/game/reconstructed/seqctl.cpp @@ -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; }