//===========================================================================// // File: mech2.cpp // // Project: BattleTech Brick: Entity Manager // // Contents: Mech locomotion / gait animation -- second implementation slice // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // --/--/95 ?? Initial coding. // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved // // PROPRIETARY AND CONFIDENTIAL // //===========================================================================// // // RECONSTRUCTED from the shipped binary (Ghidra pseudo-C in // all/part_012.c, cluster 0x4a5028-0x4a7400) cross-referenced with the // surviving animation-name string table at .data:0050cfe8 and the // CLASSMAP shared-base offset layout. The decompiler tagged exactly four // functions in this window as file=bt/mech2.cpp: // // @004a5028 Mech::AdvanceLegAnimation (1543 bytes) // @004a5678 Mech::AdvanceBodyAnimation (1333 bytes) // @004a5bf8 Mech::AdvanceBodyAnimationAirborne(1792 bytes) // @004a71f4 Mech::AdvanceLegAnimationAirborne (1840 bytes) // // The embedded assert path on every one of them is // "d:\tesla\bt\bt\MECH2.CPP" // (strings @0050d7e4 / 0050d81a / 0050d850 / 0050d886 / 0050d8bc) at source // lines 0xD3, 0x13B, 0x206, 0x2E8 and 0x672 respectively -- confirming the // attribution and the original source ordering. // // These four functions are Mech METHODS. The Mech class declaration is owned // by mech.cpp / mech.hpp (the first slice); this file therefore declares no // header of its own. The member offsets it touches are documented in the // "Mech animation member map" block below so the mech.hpp owner can fold them // into the class definition (see report). Field offsets noted in comments are // the byte offsets observed in the decompiled object (e.g. "@0x348"). // //---------------------------------------------------------------------------// // THE TWO ANIMATION CHANNELS // // A Mech carries two parallel gait animators, each a SequenceController plus a // small state machine driven by an AlarmIndicator that stores the current // animation enumerant: // // Channel A ("leg" / locally-simulated gait) // legAnimation @0x65c SequenceController // legStateAlarm @0x39c AlarmIndicator (current state readable @0x3b0) // legCycleSpeed @0x348 smoothed leg-cycle speed // Input: the *live* commanded speed from the controls subsystem // ( *(*(this+0x128)) + 0x128 ). // // Channel B ("body" / displayed-motion gait, integrated by Mech::IntegrateMotion // @004ab1c8 to advance the world transform) // bodyAnimation @0x6bc SequenceController // bodyStateAlarm @0x714 AlarmIndicator (current state readable @0x728) // bodyCycleSpeed @0x6b8 smoothed body-cycle speed // bodyTargetSpeed @0x6b4 target speed snapshot (dead-reckoned / net) // // Each channel ships in two flavours selected per-frame by movementMode @0x40 // (see Mech::IntegrateMotion @004ab1c8, which picks the airborne body updater // when (movementMode==3 || movementMode==4) && jumpActive @0x580): // "ground" -- 0x5028 (leg) / 0x5678 (body) // "airborne" -- 0x71f4 (leg) / 0x5bf8 (body): adds the FallForward / // FallBackward jump-jet states 0x18/0x19 and clamps the // commanded speed to the jump-speed limits. // // The exact A=leg / B=body role split is best-effort; what is certain from the // decomp is the (channelA,channelB) x (ground,airborne) 2x2 grouping and the // data each reads. Flagged inline as TODO where uncertain. // //---------------------------------------------------------------------------// // Helper / engine routine name mapping used below: // FUN_0042790c SequenceController::Advance(increment, loop) -> Scalar // (advances the active clip, returns cycle distance covered) // FUN_004277a8 SequenceController::SelectSequence(clip, dbg...) (via setters) // FUN_004283b8 SequenceController::Reset(loop) // FUN_0041bbd8 AlarmIndicator::SetLevel(n) (== heat.cpp mapping) // FUN_00408440 Vector/string Assign(dst, src) (clears @0x598 to "") // FUN_0040385c Verify()/assert(msg,file,line) (== heat.cpp mapping) // FUN_004dbb24 DebugStream::operator<<(stream,str) (error message build) // FUN_004d9c38 DebugStream::flush/emit // FUN_0049fb54 Mech::IsDisabled() (true => mask action-request bits) // FUN_004a7fc4 Mech::SetLegAnimation(state) (cluster helper, see below) // FUN_004a800c Mech::SetBodyAnimation(state) (cluster helper, see below) // FUN_004a4c54 Mech::RequestActionFlags(bits) (cluster helper, see below) // // Read-only constants resolved from CODE literal pools (all == 0.0f): // _DAT_004a5674 = _DAT_004a5bf4 = _DAT_004a6340 = _DAT_004a796c = 0.0f // &DAT_004e0f74 = "" (empty string; 0050cfe0 byte at 0x4e0f74 == 0) // #include #pragma hdrstop #if !defined(MECH_HPP) # include // Mech class -- owned by mech.cpp slice #endif #if !defined(MECHMPPR_HPP) # include // MechControlsMapper -- the leg channel's LIVE speed source #endif #if !defined(APP_HPP) # include #endif // // Speed comparisons in the original use a literal-pool 0.0f as a "moving at // all" threshold; reverse/gimp cycle ratios are reflected through it so a // backward cycle plays its clip with a positive increment. // static const Scalar ZeroSpeed = 0.0f; // _DAT_004a5674 / 5bf4 / 6340 / 796c //########################################################################### //##################### Mech gait animation enum ######################## //########################################################################### // // Recovered verbatim from the 0x3c-byte-stride name table at .data:0050cfe8 // (used by the "Unsupported mech animation" assert). This enum belongs in // mech.hpp; reproduced here for clarity of the switches below. // enum MechAnimationState { StandingAnimation = 0x00, RightStandToWalkAnimation = 0x01, RightWalkForwardAnimation = 0x02, LeftWalkForwardAnimation = 0x03, RightWalkToStandAnimation = 0x04, LeftWalkToStandAnimation = 0x05, RightWalkToRunAnimation = 0x06, LeftWalkToRunAnimation = 0x07, RightRunAnimation = 0x08, LeftRunAnimation = 0x09, RightRunToWalkAnimation = 0x0a, LeftRunToWalkAnimation = 0x0b, RightStandToReverseAnimation = 0x0c, LeftStandToReverseAnimation = 0x0d, RightReverseAnimation = 0x0e, LeftReverseAnimation = 0x0f, RightReverseToStandAnimation = 0x10, LeftReverseToStandAnimation = 0x11, LeftWalkToGimpAnimation = 0x12, RightWalkToGimpAnimation = 0x13, LeftGimpAnimation = 0x14, RightGimpAnimation = 0x15, LeftGimpToStandAnimation = 0x16, RightGimpToStandAnimation = 0x17, FallForwardAnimation = 0x18, FallBackwardAnimation = 0x19, FallLeftAnimation = 0x1a, FallRightAnimation = 0x1b, CrashAnimation = 0x1c, // 0x1d-0x20 are valid clip slots (death variants) handled by the // "advance normally" group but are past the named table; AnimationCount // is the table sentinel at index 0x1d. AnimationCount = 0x1d }; //########################################################################### //################# Mech animation member map (offsets) ################# //########################################################################### // // For the mech.hpp owner. Names below are used in the bodies; "?" flags an // uncertain semantic. All are Scalar unless noted. // // @0x18 actionRequestFlags (Word) pending-action bitfield (RequestActionFlags) // @0x40 movementMode (int) gait/death selector: 3=Run,4=Walk,5-8=fall/death? (?) // @0x128 controlSource (ptr) handle; *(*(this+0x128))+0x128 == commandedSpeed // @0x344 forwardCycleRate leg/body speed slew rate (set from 0x5b8/0x5bc) // @0x348 legCycleSpeed channel A current cycle speed // @0x34c reverseStrideLength clip length for the Reverse cycle // @0x350 gimpStrideLength clip length for the Gimp cycle (stored negative) // @0x39c legStateAlarm (AlarmIndicator) // @0x3b0 legAnimationState (int) == legStateAlarm.level // @0x52c gimpSpeedMax speed cap for the Gimp cycle (?) // @0x530 standSpeed "at rest" / minimum move speed threshold // @0x534 walkStrideLength forward walk/run clip length (also speed cap) // @0x538 reverseSpeedMax speed cap while decelerating into Reverse (?) // @0x53c jumpRunSpeedMax airborne speed cap, movementMode==3 // @0x540 jumpWalkSpeedMax airborne speed cap, otherwise // @0x544 jumpRunStrideLength airborne clip length, movementMode==3 // @0x548 jumpWalkStrideLength airborne clip length, otherwise // @0x598 motionEventName (string) cleared to "" on fall/reset // @0x5a4 motionEventArmed (int) reset to 0 on fall/reset // @0x5a8 globalTimeScale multiplies every clip increment // @0x5ac idleStrideScale extra scale used only in the idle/transition group // @0x5b0 gimpCycleRate speed slew rate while in a Gimp cycle // @0x5cc animationClips[] (ptr[]) clip handle per MechAnimationState (this+0x5cc + state*4) // @0x650 deathAnimationLatched(int) one-shot latch for movementMode 5-8 death anims // @0x654 legResetLatch (int) cleared on fall/reset (channel A) // @0x658 bodyResetLatch (int) cleared on fall/reset (channel B) // @0x65c legAnimation (SequenceController) // @0x6b4 bodyTargetSpeed channel B target cycle speed // @0x6b8 bodyCycleSpeed channel B current cycle speed // @0x6bc bodyAnimation (SequenceController) // @0x714 bodyStateAlarm (AlarmIndicator) // @0x728 bodyAnimationState (int) == bodyStateAlarm.level // @0x7a0 reverseSpeedMax2 speed cap while accelerating into Reverse (?) // //########################################################################### //########################################################################### // Cluster helpers (attribution "?" in the decomp, but they // live inside the mech2 window and are required to read the // four methods). BEST-EFFORT; the mech.cpp owner should // reconcile their final home. //########################################################################### //########################################################################### // // @004a7fc4 -- bind the channel-A (leg) SequenceController to the clip for // 'state' and record the new state in the leg alarm. // void Mech::SetLegAnimation(int state) { legAnimation.SelectSequence( // FUN_004277a8(this+0x65c, ...) animationClips[state], // *(this+0x5cc + state*4) // The real leg finished-callback PTR_LAB_0050d6f0 == FUN_004a6928 // (resolved from .data + disassembled; == Mech::LegClipFinished below). // cbArg2/3 = 0 (the binary's DAT_0050d6f4/d6f8). (void *)&Mech::LegClipFinished, 0, 0); legStateAlarm.SetLevel(state); // FUN_0041bbd8(this+0x39c, state) } // // @004a800c -- channel-B (body) equivalent of SetLegAnimation. // void Mech::SetBodyAnimation(int state) { bodyAnimation.SelectSequence( // FUN_004277a8(this+0x6bc, ...) animationClips[state], // The real body finished-callback PTR_LAB_0050d6fc == FUN_004a6d8c (resolved from the // binary + reconstructed as Mech::BodyClipFinished): the gait-state TRANSITION + leg // alternation. SequenceController::Advance calls it at end-of-clip; it re-arms the next // state's clip (with this same callback) + advances the carryover. cbArg2/3 = 0 (the // binary's DAT_0050d700/704). (void *)&Mech::BodyClipFinished, 0, 0); bodyStateAlarm.SetLevel(state); // FUN_0041bbd8(this+0x714, state) } // // (@004a4c54 lives inline in mech.hpp as Mech::ForceUpdate -- the "action // request bits" ARE the updateModel record-request mask; the old // RequestActionFlags name wrote a dead side-member the update emitter never // read. The call sites below keep their binary-exact masks: 8 = 1<<3 the // leg-state/stability record.) //########################################################################### //########################################################################### // BodyTransition / BodyClipFinished (end-of-clip gait transitions) // // The real body finished-callback FUN_004a6d8c (== PTR_LAB_0050d6fc, resolved from the // binary .data at 0x50d6fc). SequenceController::Advance invokes it when a body clip // finishes; it dispatches on bodyAnimationState (the jump table @0x4a6e0a), compares the // commanded speed (bodyTargetSpeed) to the loaded caps (standSpeed/walkStrideLength/ // reverseSpeedMax) to pick the next gait state, re-arms it (SetBodyAnimation -> re-binds the // clip with THIS same callback so the cycle keeps transitioning), and recursively advances // the leftover (carryover) time -- returning the extra distance covered. Reconstructed // byte-for-byte from the disassembly (handlers 0x4a6f11 walk-R / 0x4a6e36 walk-L / 0x4a7041 / // 0x4a6fc7 run, shared tail 0x4a6e66 / 0x4a6ed1 / 0x4a7001). //########################################################################### //########################################################################### // Shared tail (0x4a6e66 etc.): bind the next state's clip, advance the carryover, return dist. Scalar Mech::BodyTransition(int next_state, Scalar adv_time, int move_joints) { SetBodyAnimation(next_state); // call 0x4a800c return bodyAnimation.Advance(adv_time, move_joints); // call 0x42790c } Scalar Mech::BodyClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj) { // airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a6344 (deferred; safe // no-op while grounded -- the test mech never jumps). if ((m->MovementMode() == 3 || m->MovementMode() == 4) && m->jumpCapable) return 0.0f; const Scalar fcr = m->forwardCycleRate; // 0x344 const Scalar gts = m->globalTimeScale; // 0x5a8 const Scalar cyc = m->bodyCycleSpeed; // 0x6b8 const Scalar tgt = m->bodyTargetSpeed; // 0x6b4 const Scalar Tscale = carryover * gts; // 0x4a6e66 tail time switch (m->bodyAnimationState) // 0x728 (jump table @0x4a6e0a) { // slot0 (0x4a71e9): standing / idle / reset-idle -- no transition, distance 0. case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27: return 0.0f; // state 2 (0x4a6fb1): bodyStateAlarm.SetLevel(1). case 2: m->bodyStateAlarm.SetLevel(1); return 0.0f; // slot9 state 4 (0x4a71d8) + slot2 (0x4a6f85) + state 32 (0x4a6f9b): SetLevel(0) // (transition-END clips fall back to standing). case 3: case 4: case 8: case 9: case 20: case 21: case 28: case 29: case 30: case 31: case 32: m->bodyStateAlarm.SetLevel(0); return 0.0f; // -- walk-R handler (0x4a6f11): states 5 (swr-end),6 (wwr),14 -- case 5: case 6: case 14: { bool cont = (tgt >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed); if (!cont) // 0x4a6f11 -> next 9 (wsl, walk->stand) return m->BodyTransition(9, Tscale, mj); bool up = (tgt > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength); if (up) // 0x4a6f46 -> next 0xb (11, toward run) return m->BodyTransition(0xb, Tscale, mj); return m->BodyTransition(7, carryover * cyc * gts / m->walkStrideLength, mj); // 0x4a6f7b alt -> wwl } // -- walk-L handler (0x4a6e36): states 7 (wwl),15 -- case 7: case 15: { bool cont = (tgt >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed); if (!cont) // 0x4a6e36 -> next 8 (wsr) return m->BodyTransition(8, Tscale, mj); bool up = (tgt > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength); if (up) // 0x4a6e9a -> next 0xa (10) return m->BodyTransition(0xa, Tscale, mj); return m->BodyTransition(6, carryover * cyc * gts / m->walkStrideLength, mj); // 0x4a6ecc alt -> wwr } // -- run/reverse-A handler (0x4a7041): states 10,12 -- case 10: case 12: { bool cont = (tgt >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax); if (!cont) // 0x4a7041 -> next 0xf (15) return m->BodyTransition(0xf, Tscale, mj); return m->BodyTransition(0xd, carryover * cyc * gts / m->reverseStrideLength, mj); // 0x4a7076 alt -> 13 } // -- run/reverse-B handler (0x4a6fc7): states 11,13 -- case 11: case 13: { bool cont = (tgt >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax); if (!cont) // 0x4a6fc7 -> next 0xe (14) return m->BodyTransition(0xe, Tscale, mj); return m->BodyTransition(0xc, carryover * cyc * gts / m->reverseStrideLength, mj); // 0x4a6ffc alt -> 12 } // -- REVERSE handlers (0x4a707d states 16,18 / 0x4a712c states 17,19): the body // mirror of the leg's reverse cases (0x4a6c17/0x4a6cc4). The earlier "gimp, // not decoded -> fall back to standing" reading was wrong twice over: these // are the REVERSE gait (16/17 = sbr/sbl entry, 18/19 = bbr/bbl back cycle, // 0x14/0x15 = bsr/bsl back->stand exit -- the slot map is binary-verified), // and the stand fallback made the body loop stand->reverse-entry forever // ("lingers in 16": slow reverse + the screwy backward->forward exit). // The alt tails 0x70b2/0x7161 are the BodyTransition(0x13/0x12) cycle folds, // by exact structural symmetry with the leg jump table (every previously // decoded body case mirrors its leg twin). While reversing (demand below // gimpSpeedMax) the cycle alternates 0x12<->0x13; a forward demand exits // through 0x15/0x14 (back->stand), then Standing self-arms the forward walk. case 16: case 18: { bool up = (tgt > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax); if (up) return m->BodyTransition(0x15, Tscale, mj); Scalar t = carryover * cyc * gts / m->gimpStrideLength; // gimpStride stored NEGATIVE if (t <= 0.0f) t = -t; // sign fold (leg: 0x4a6c6e/0x4a6d3d) return m->BodyTransition(0x13, t, mj); } case 17: case 19: { bool up = (tgt > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax); if (up) return m->BodyTransition(0x14, Tscale, mj); Scalar t = carryover * cyc * gts / m->gimpStrideLength; if (t <= 0.0f) t = -t; return m->BodyTransition(0x12, t, mj); } default: // 0x4a71e9 (state > 0x20 or unmapped) return 0.0f; } } // Bring-up loop for the inline cutover path (BT_GAIT_CUTOVER without BT_GAIT_SM): re-arm the // current body clip at frame 0 and advance the carryover (the SequenceController's own clip // keeps playing). NOT the authentic transition path -- that is BodyClipFinished above. Scalar Mech::LoopBodyClip(Mech *m, unsigned /*a2*/, Scalar carryover, int move_joints) { m->bodyAnimation.currentFrame = 0; m->bodyAnimation.currentTime = 0.0f; m->bodyAnimation.keyframeCursor = m->bodyAnimation.keyframeBase; return m->bodyAnimation.Advance(carryover, move_joints); } //########################################################################### //########################################################################### // LegTransition / LegClipFinished (LEG-channel end-of-clip) // // The real leg finished-callback FUN_004a6928 (== PTR_LAB_0050d6f0, resolved from // the binary .data at 0x50d6f0 and capstone-disassembled: jump table byte idx // @0x4a6989, dword targets @0x4a69aa -- the same 33-state shape as the body's). // Differences from BodyClipFinished, all verified in the disassembly: // - the speed compared is the LIVE commanded speed *(subsystemArray[0])+0x128 // == the controls mapper's speedDemand (typed mirror: controlsMapper), not // the snapshot bodyTargetSpeed; // - the cycle speed slewed/scaled is legCycleSpeed@0x348 (not bodyCycleSpeed); // - re-arm via SetLegAnimation + legAnimation.Advance (0x65c, alarm@0x39c); // - the GIMP cycle alternates 0x12<->0x13 with |ratio| (gimpStrideLength is // stored negative; the 0x4a6c6e sign fold takes the magnitude). //########################################################################### //########################################################################### // Shared tail (0x4a6a06 / 0x4a6a6f / 0x4a6b9d): bind next state's clip, advance carryover. Scalar Mech::LegTransition(int next_state, Scalar adv_time, int move_joints) { SetLegAnimation(next_state); // call 0x4a7fc4 return legAnimation.Advance(adv_time, move_joints); // call 0x42790c } Scalar Mech::LegClipFinished(Mech *m, unsigned /*a2*/, Scalar carryover, int mj) { // airborne branch (movementMode 3/4 && jumpCapable) -> FUN_004a7970 (deferred). if ((m->MovementMode() == 3 || m->MovementMode() == 4) && m->jumpCapable) return 0.0f; // The binary reads edx = *(mech+0x128) then [edx]+0x128: subsystemArray[0] // (the roster's ControlsMapper slot 0) -> speedDemand; null (no mapper) // reads demand 0 -> the mech idles. (task #7: read the REAL slot-0 mapper.) MechControlsMapper *mppr2 = m->MappingMapper(); const Scalar spd = (mppr2 != 0) ? mppr2->speedDemand : 0.0f; const Scalar fcr = m->forwardCycleRate; // 0x344 const Scalar gts = m->globalTimeScale; // 0x5a8 const Scalar cyc = m->legCycleSpeed; // 0x348 const Scalar T = carryover * gts; // 0x4a6a06 tail time switch (m->legAnimationState) // 0x3b0 (jump table @0x4a69aa) { // slot0 (0x4a6d7f): standing / idle -- no transition, distance 0. case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27: return 0.0f; // slot10 state 2 (0x4a6b37): SetLevel(1). case 2: m->legStateAlarm.SetLevel(1); return 0.0f; // slot2 (0x4a6b21) + slot1 state 32 (0x4a6b4d) + slot9 state 4 (0x4a6d6e): SetLevel(0). case 3: case 4: case 8: case 9: case 20: case 21: case 28: case 29: case 30: case 31: case 32: m->legStateAlarm.SetLevel(0); return 0.0f; // -- walk-R handler (0x4a6aad): states 5,6,14 -- case 5: case 6: case 14: { bool cont = (spd >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed); if (!cont) // -> 9 (walk->stand L) return m->LegTransition(9, T, mj); bool up = (spd > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength); if (up) // 0x4a6ae2 -> 0xb (toward run) return m->LegTransition(0xb, T, mj); return m->LegTransition(7, carryover * cyc * gts / m->walkStrideLength, mj); // alt -> walk-L } // -- walk-L handler (0x4a69d6): states 7,15 -- case 7: case 15: { bool cont = (spd >= m->standSpeed) || ((cyc - fcr * carryover) >= m->standSpeed); if (!cont) // -> 8 (walk->stand R) return m->LegTransition(8, T, mj); bool up = (spd > m->walkStrideLength) && ((cyc + fcr * carryover) > m->walkStrideLength); if (up) // 0x4a6a38 -> 0xa return m->LegTransition(0xa, T, mj); return m->LegTransition(6, carryover * cyc * gts / m->walkStrideLength, mj); // alt -> walk-R } // -- run handler (0x4a6bdb): states 10,12 -- case 10: case 12: { bool cont = (spd >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax); if (!cont) // -> 0xf (15) return m->LegTransition(0xf, T, mj); return m->LegTransition(0xd, carryover * cyc * gts / m->reverseStrideLength, mj); // alt -> 13 } // -- run handler (0x4a6b63): states 11,13 -- case 11: case 13: { bool cont = (spd >= m->reverseSpeedMax) || ((cyc - fcr * carryover) >= m->reverseSpeedMax); if (!cont) // -> 0xe (14) return m->LegTransition(0xe, T, mj); return m->LegTransition(0xc, carryover * cyc * gts / m->reverseStrideLength, mj); // alt -> 12 } // -- gimp handler (0x4a6c17): states 16,18 -> alt 0x13; up 0x15 -- case 16: case 18: { bool up = (spd > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax); if (up) return m->LegTransition(0x15, T, mj); Scalar t = carryover * cyc * gts / m->gimpStrideLength; // gimpStride stored NEGATIVE if (t <= 0.0f) t = -t; // 0x4a6c6e/0x4a6d3d sign fold return m->LegTransition(0x13, t, mj); } // -- gimp handler (0x4a6cc4): states 17,19 -> alt 0x12; up 0x14 -- case 17: case 19: { bool up = (spd > m->gimpSpeedMax) && ((m->gimpCycleRate * carryover + cyc) > m->gimpSpeedMax); if (up) return m->LegTransition(0x14, T, mj); Scalar t = carryover * cyc * gts / m->gimpStrideLength; if (t <= 0.0f) t = -t; return m->LegTransition(0x12, t, mj); } default: // state > 0x20 / unmapped return 0.0f; } } //########################################################################### //########################################################################### // AdvanceLegAnimation (channel A, ground) // // @004a5028 (MECH2.CPP:0xD3, 0x13B) // // Per-frame update of the locally-simulated leg gait. Reads the live // commanded speed from the controls subsystem, slews legCycleSpeed toward it, // drives the walk/run/reverse/gimp state machine, advances the leg clip and // returns the cycle distance covered this frame. //########################################################################### //########################################################################### Scalar Mech::AdvanceLegAnimation(Scalar time_slice) { // commandedSpeed = *(*(this+0x128)) + 0x128 in the binary: subsystemArray[0] // (the roster's ControlsMapper slot) -> speedDemand, read LIVE each frame. // RECONCILED: the old draft double-deref'd the never-initialized controlSource // alias (an AV); controlsMapper is the typed mirror of roster slot 0. A mech // with no mapper reads demand 0 (idles) -- matching a zeroed binary roster. MechControlsMapper *mppr = MappingMapper(); // roster slot 0 (task #7) Scalar commandedSpeed = (mppr != 0) ? mppr->speedDemand : 0.0f; Scalar distance = 0.0f; // binary: legAnimationState@0x3b0 IS legStateAlarm's level (one field; the // recon split them) -- re-sync so SetLegAnimation's level reaches the switch. legAnimationState = (int)legStateAlarm.GetLevel(); // // One-shot: when movementMode selects a death/fall (5..8), latch the // matching crash clip (0x1c..0x1f) exactly once. // if (!deathAnimationLatched) { switch (MovementMode()) // this+0x40 = simulationState { case 5: SetLegAnimation(0x1c); deathAnimationLatched = 1; break; case 6: SetLegAnimation(0x1d); deathAnimationLatched = 1; break; case 7: SetLegAnimation(0x1e); deathAnimationLatched = 1; break; case 8: SetLegAnimation(0x1f); deathAnimationLatched = 1; break; } } // // Once the leg cycle has wound down (legCycleSpeed <= 0) while in a // run/run-to-walk transition, drop the gait alarm to "standing". // { int state = legAnimationState; // this+0x3b0 if (legCycleSpeed <= ZeroSpeed // this+0x348 && (state == 6 || state == 7 || state == 8 || state == 9)) { legStateAlarm.SetLevel(0); // FUN_0041bbd8(this+0x39c,0) legResetLatch = 1; // this+0x654 } } switch (legAnimationState) // this+0x3b0 { case StandingAnimation: // 0 // STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed // 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop // gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full // reverse speed, and several stand-entry paths (turn exit, terminal // poses) never touch the cycle -- Standing could be entered with a // stale legCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master // LOOKS still (case 0 never advances the clip) but the stale cycle // REPLICATES and the peer's replicant marches in place. A standing // mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8). if (legCycleSpeed != 0.0f) { legCycleSpeed = 0.0f; ForceUpdate(8); // type-3 record: legs stopped } // RAW (part_012.c FUN_004a5028 case 0): standSpeed < commandedSpeed -> // begin WALKING (state 5); 0 <= commanded < standSpeed -> stay standing; // commanded < 0 -> stand-to-reverse (0x10). (The earlier draft had the // first comparison INVERTED -> a commanded mech never left Standing.) if (standSpeed < commandedSpeed) // this+0x530 < live demand { SetLegAnimation(5); // stand -> walk } else { distance = 0.0f; // TURN-IN-PLACE entry -- AUTHENTIC, decoded from the master-perf disasm // (0x4aa505-0x4aa588, task #64b). The dispatcher lives in the master perf // FUN_004a9b5c (Ghidra never decompiled it -> objdump); it arms trn from // Standing when: the mech is TURNING (|angularVelocity| > 1e-4, ds:0x4ab16c // -- angularVelocity = turnDemand * turnRate, so turnDemand outside a tiny // deadband) AND 0 <= speedDemand <= standSpeed (the FULL sub-walk range, // ds:0x4ab178=0 .. mech[0x530]) AND turnCapable(mech[0x588]) AND the // legResetLatch(mech[0x654]) debounce is clear. CORRECTS the task-#64a // stand-in `commandedSpeed < 0.25*standSpeed` (that near-zero gate was a // guess; the binary allows the full [0,standSpeed] range -- trn is gated on // TURNING, not on being slow). [T1 from disasm] The turnDemand deadband // (0.05) is the port proxy for the |angVel|>1e-4 test. NOTE: the disasm's // legResetLatch gate is a master-perf ONE-FRAME debounce (cleared every // frame at 0x4a9bff, set on wind-down/turn-stop) -- it does NOT map onto // the port's split leg-SM (wind-down + trn-entry run in different frames/ // cases and the port never per-frame-clears the latch), so gating entry on // it here would wrongly block trn after every walk. Omitted by design; // see locomotion.md "turn-in-place dispatcher". // REPLICANT accommodation (regression fix, 2026-07-14): the dispatcher // above is MASTER-perf logic; a replicant feeds this SM DERIVED signals // (speed/turn from the dead-reckon stream, mech4.cpp:1948) and runs ONLY // the leg channel. Two master rules break it: // (1) the body weld -- a replicant never runs the body SM, so arming the // body to 4 sticks bodyAnimationState there forever and BLOCKS every // later trn entry (the peer "rotates as a statue"); // (2) the full [0,standSpeed] entry -- the derived speed sweeps that band // on every dead-reckoned start/stop with turnDemand pinned +-1, so // trn keeps arming mid-locomotion and speed-exiting (jerky walking). // Replicants: no body weld / no body arm (channel is inert, mj=0) and the // narrow near-zero entry gate (the pre-#64b accommodation). [T3] const int trnIsRepl = (GetInstance() == ReplicantInstance); const Scalar trnEntryMax = trnIsRepl ? standSpeed * 0.25f : standSpeed; if (hasCrashSet != 0 && mppr != 0 && commandedSpeed >= ZeroSpeed && commandedSpeed <= trnEntryMax && (mppr->turnDemand > 0.05f || mppr->turnDemand < -0.05f) && (trnIsRepl || bodyAnimationState == StandingAnimation)) // weld: masters only { // LOCKSTEP (task #64): arm BOTH channels on the same frame. Arming // only the leg let the body enter walk on its own schedule ~7-20 // frames apart -> the two walk cycles ran permanently out of phase // and the body's pose flashed through on leg clip-boundary frames // (the rhythmic gait skip + reduced bob). Both SequenceControllers // bind the same trn clip at the same frame + advance at the same // rate (case 4 twins), so they complete + re-enter walk together -- // the same phase-weld a standstill start gets for free. This is the // workflow plan's "master perf arms both channels" reconstruction // (the authentic dispatcher in the un-decompiled 0x4a9b5c gap arms // both -- the body's case-4/finish machinery is dead code otherwise). SetLegAnimation(4); // turn-in-place (trn), channel A if (!trnIsRepl) SetBodyAnimation(4); // channel B, same frame [lockstep, masters] goto advance_normally; } if (ZeroSpeed <= commandedSpeed) { break; // truly at rest } SetLegAnimation(0x10); // reverse entry } // FALLTHROUGH into the "advance normally" group (state has just been // changed away from Standing by the setters above). case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b: case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15: case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20: advance_normally: // // Standing must never reach here -- the fallthrough above always // retargets the alarm first. // if (legAnimationState == StandingAnimation) { Verify(False, "Standing Not Supported", "d:\\tesla\\bt\\bt\\MECH2.CPP", 0xD3); } distance = legAnimation.Advance( // FUN_0042790c(this+0x65c, ...) time_slice * globalTimeScale * idleStrideScale, // 0x5a8 * 0x5ac 1); legCycleSpeed = distance / time_slice; // this+0x348 break; case 1: distance = 0.0f; break; case 4: // TURN-IN-PLACE (trn clip) // state 4 is the turn-in-place animation (animationClips[4] = the "trn" clip, // loaded under turnCapable@0x588). AUTHENTIC exits, restored VERBATIM from the // decompiled leg SM (part_012.c:12013 == FUN_004a5028 case 4 [T1]): // * standSpeed < commandedSpeed -> SetLevel(0)+ForceUpdate(8): walk takes over // * commandedSpeed < ZeroSpeed -> SetLevel(0)+ForceUpdate(8): reverse // * else -> goto advance_normally: advance the pivot at idleStrideScale. // PLUS the master-perf turn-STOP exit (disasm 0x4aa5c6-0x4aa5e6 [T1], which the // leg SM does NOT contain -- it lives in the un-decompiled master perf that the // port has no separate frame for, so it folds in here): when the turn stops // (|angularVelocity| <= 1e-4, i.e. turnDemand back inside the deadband) -> // SetLevel(0) + legResetLatch(mech[0x654])=1. Without it the trn clip would // shuffle in place forever after the stick re-centers. // // The task-#64 fast-forward (4x) + 0.25*standSpeed early-release were STAND-INS // invented to dodge a turn->walk stutter that was ACTUALLY the body channel // leaking joints into the rendered skeleton (mj=1); root-caused + fixed by the // body-channel mj=0 change (AdvanceBodyAnimation, mech4.cpp). With the body no // longer writing joints the authentic mid-clip cut renders cleanly, so the // inventions are removed. trn has zero root translation -> distance stays 0. if (standSpeed < commandedSpeed || commandedSpeed < ZeroSpeed) { legStateAlarm.SetLevel(0); // -> Standing (leg-SM exit) ForceUpdate(8); // type-3 record distance = 0.0f; break; } // MASTER ONLY (regression fix): the master-perf turn-stop exit is master-perf // logic (FUN_004a9b5c runs on MasterInstance mechs, NOT replicants). A replicant // derives turnDemand from the REPLICATED yaw rate (mech4.cpp:1968) -- a noisy // proxy that dips into the deadband between dead-reckon updates; running this // exit on it kicked the peer out of trn every few frames -> the peer "rotated as // a statue" + jerky (user-reported). Gate to masters; a replicant leaves trn via // the speed exits + its own turnDemand-driven re-entry, exactly as it did before // this exit was added. if (GetInstance() != ReplicantInstance && (mppr == 0 || (mppr->turnDemand <= 0.05f && mppr->turnDemand >= -0.05f))) { legStateAlarm.SetLevel(0); // turn stopped -> Standing (master-perf exit) ForceUpdate(8); legResetLatch = 1; // mech[0x654] (master perf sets it here, 0x4aa5e1) distance = 0.0f; break; } goto advance_normally; // still turning, sub-walk -> advance the pivot case 6: case 7: // WalkToRun // // Slew legCycleSpeed toward commandedSpeed at forwardCycleRate, // clamped into [standSpeed .. walkStrideLength]. // if (commandedSpeed <= legCycleSpeed) { if (commandedSpeed < legCycleSpeed) { legCycleSpeed -= forwardCycleRate * time_slice; // 0x344 if (legCycleSpeed < commandedSpeed) { legCycleSpeed = commandedSpeed; } if (legCycleSpeed < standSpeed) // 0x530 { legCycleSpeed = standSpeed; } } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > commandedSpeed) { legCycleSpeed = commandedSpeed; } if (legCycleSpeed > walkStrideLength) // 0x534 (used as cap) { legCycleSpeed = walkStrideLength; } } distance = legAnimation.Advance( time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale, 1); break; case 0x0c: case 0x0d: // StandToReverse if (commandedSpeed <= legCycleSpeed) { if (commandedSpeed < legCycleSpeed) { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < commandedSpeed) { legCycleSpeed = commandedSpeed; } if (legCycleSpeed < reverseSpeedMax) // 0x538 { legCycleSpeed = reverseSpeedMax; } } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > commandedSpeed) { legCycleSpeed = commandedSpeed; } if (legCycleSpeed > reverseSpeedMax2) // 0x7a0 { legCycleSpeed = reverseSpeedMax2; } } distance = legAnimation.Advance( time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale, // 0x34c 1); break; case 0x12: case 0x13: // WalkToGimp if (commandedSpeed <= legCycleSpeed) { if (commandedSpeed < legCycleSpeed) { legCycleSpeed -= gimpCycleRate * time_slice; // 0x5b0 if (legCycleSpeed < commandedSpeed) { legCycleSpeed = commandedSpeed; } if (legCycleSpeed < gimpStrideLength) // 0x350 { legCycleSpeed = gimpStrideLength; } } } else { legCycleSpeed += gimpCycleRate * time_slice; if (legCycleSpeed > commandedSpeed) { legCycleSpeed = commandedSpeed; } if (legCycleSpeed > gimpSpeedMax) // 0x52c { legCycleSpeed = gimpSpeedMax; } } { // gimpStrideLength is stored negative; reflect the ratio through // ZeroSpeed so the clip plays with a positive increment. Scalar ratio = legCycleSpeed / gimpStrideLength; // 0x350 if (ratio <= ZeroSpeed) { ratio = -ratio; } distance = legAnimation.Advance( ratio * time_slice * globalTimeScale, 1); } break; case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b: // // Gimp-to-stand and the four fall directions: terminal poses. Clear // the motion event, drop both reset latches, reset the leg clip. // Assign(this->motionEventName, ""); // FUN_00408440(this+0x598, &DAT_004e0f74) motionEventArmed = 0; // this+0x5a4 legResetLatch = 0; // this+0x654 deathAnimationLatched = 0; // this+0x650 legStateAlarm.SetLevel(0); // this+0x39c legAnimation.Reset(1); // FUN_004283b8(this+0x65c, 1) break; default: // name table @0050cfe8, 0x3c-byte stride, indexed by state DebugStream << (AnimationNames + legAnimationState * 0x3c); DebugStream.Emit(); Verify(False, "Unsupported mech animation!", "d:\\tesla\\bt\\bt\\MECH2.CPP", 0x13B); } return distance; } //########################################################################### //########################################################################### // AdvanceBodyAnimation (channel B, ground) // // @004a5678 (MECH2.CPP:0x206) // // Channel-B counterpart used by Mech::IntegrateMotion to advance the world // transform. Identical state machine to AdvanceLegAnimation except it slews // bodyCycleSpeed toward the snapshot bodyTargetSpeed (not the live controls // value), takes an explicit loop flag, and has no death-latch / "Standing Not // Supported" guard. //########################################################################### //########################################################################### Scalar Mech::AdvanceBodyAnimation(Scalar time_slice, int loop) { Scalar distance = 0.0f; // In the binary `bodyAnimationState`@0x728 IS `bodyStateAlarm`'s level (one field); // the reconstruction split them, so SetBodyAnimation's `bodyStateAlarm.SetLevel(state)` // would not update the int the switch reads. Re-sync from the alarm each frame (the // switch below dispatches on the pre-transition state exactly as the binary does). bodyAnimationState = (int)bodyStateAlarm.GetLevel(); if (!deathAnimationLatched) // this+0x650 { switch (MovementMode()) // this+0x40 = simulationState { case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break; case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break; case 7: SetBodyAnimation(0x1e); deathAnimationLatched = 1; break; case 8: SetBodyAnimation(0x1f); deathAnimationLatched = 1; break; } } switch (bodyAnimationState) // this+0x728 { case StandingAnimation: // 0 // STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed // 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop // gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full // reverse speed, and several stand-entry paths (turn exit, terminal // poses) never touch the cycle -- Standing could be entered with a // stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master // LOOKS still (case 0 never advances the clip) but the stale cycle // REPLICATES and the peer's replicant marches in place. A standing // mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8). if (bodyCycleSpeed != 0.0f) { bodyCycleSpeed = 0.0f; ForceUpdate(8); // type-3 record: legs stopped } // RAW (FUN_004a5678 case 0): standSpeed < bodyTargetSpeed -> begin WALKING // (5); 0 <= target < standSpeed -> stay standing; target < 0 -> reverse // (0x10). (The earlier draft had the comparison INVERTED.) distance = 0.0f; if (standSpeed < bodyTargetSpeed) // 0x530 < 0x6b4 { SetBodyAnimation(5); } else { if (ZeroSpeed <= bodyTargetSpeed) { break; } SetBodyAnimation(0x10); } // FALLTHROUGH case 4: // TURN-IN-PLACE, LOCKSTEP twin (task #64) // The body channel runs trn in LOCKSTEP with the leg: armed together at // entry (leg Standing cross-arms both), advanced at the SAME rate keyed on // the SAME live speedDemand, so both clips complete on the same frame and // both channels re-enter walk on the same frame. Without this the body // entered walk ~7-20 frames apart from the leg and the two walk cycles ran // permanently out of phase. Exits mirror the leg twin VERBATIM // (part_012.c:12013 [T1]): speed exits + the master-perf turn-stop exit. // The body does NOT set legResetLatch (leg-channel/master-perf state, set // once by the leg twin). Since the body no longer writes joints (mj=0, // AdvanceBodyAnimation) this channel only tracks state so both channels // re-enter walk on the same frame. { MechControlsMapper *bm = MappingMapper(); const Scalar bspd = (bm != 0) ? bm->speedDemand : 0.0f; if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric) { bodyStateAlarm.SetLevel(0); ForceUpdate(8); distance = 0.0f; break; } if (GetInstance() != ReplicantInstance // MASTER only (see leg twin) && (bm == 0 || (bm->turnDemand <= 0.05f && bm->turnDemand >= -0.05f))) { bodyStateAlarm.SetLevel(0); // turn stopped (leg-symmetric) ForceUpdate(8); distance = 0.0f; break; } distance = bodyAnimation.Advance( // still turning -> advance the pivot time_slice * globalTimeScale * idleStrideScale, loop); bodyCycleSpeed = distance / time_slice; } break; case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b: case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15: case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20: distance = bodyAnimation.Advance( // FUN_0042790c(this+0x6bc, ...) time_slice * globalTimeScale * idleStrideScale, loop); bodyCycleSpeed = distance / time_slice; // this+0x6b8 break; case 1: distance = 0.0f; break; case 6: case 7: // WalkToRun if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < standSpeed) bodyCycleSpeed = standSpeed; } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength; } distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop); break; case 0x0c: case 0x0d: // StandToReverse if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < reverseSpeedMax) bodyCycleSpeed = reverseSpeedMax; } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2; } distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop); break; case 0x12: case 0x13: // WalkToGimp if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= gimpCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < gimpStrideLength) bodyCycleSpeed = gimpStrideLength; } } else { bodyCycleSpeed += gimpCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > gimpSpeedMax) bodyCycleSpeed = gimpSpeedMax; } { Scalar ratio = bodyCycleSpeed / gimpStrideLength; if (ratio <= ZeroSpeed) ratio = -ratio; distance = bodyAnimation.Advance( ratio * time_slice * globalTimeScale, loop); } break; case 0x16: case 0x17: case 0x18: case 0x19: case 0x1a: case 0x1b: Assign(this->motionEventName, ""); // FUN_00408440(this+0x598, "") motionEventArmed = 0; // this+0x5a4 bodyResetLatch = 0; // this+0x658 deathAnimationLatched = 0; // this+0x650 bodyStateAlarm.SetLevel(0); // this+0x714 bodyAnimation.Reset(loop); // FUN_004283b8(this+0x6bc, loop) break; default: DebugStream << (AnimationNames + bodyAnimationState * 0x3c); DebugStream.Emit(); Verify(False, "Unsupported mech animation!", "d:\\tesla\\bt\\bt\\MECH2.CPP", 0x206); } return distance; } //########################################################################### //########################################################################### // AdvanceBodyAnimationAirborne (channel B, jump-capable) // // @004a5bf8 (MECH2.CPP:0x2E8) // // Airborne variant of AdvanceBodyAnimation selected by Mech::IntegrateMotion // when jump jets are active. Adds a pre-clamp of bodyTargetSpeed to the jump // speed limits and handles the FallForward / FallBackward jump cycles // (states 0x18/0x19); gimp-to-stand (0x16/0x17) and the lateral falls // (0x1a/0x1b) join the normal advance group here rather than resetting. //########################################################################### //########################################################################### Scalar Mech::AdvanceBodyAnimationAirborne(Scalar time_slice, int loop) { Scalar distance = 0.0f; // // While in any forward/reverse/gimp *moving* cycle, clamp the target to // the jump speed limit for the current gait, then floor at zero. // { int state = bodyAnimationState; // this+0x728 if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2 || (unsigned)(state - 0x12) < 2) { if (MovementMode() == 3) // run jump { if (bodyTargetSpeed > jumpRunSpeedMax) bodyTargetSpeed = jumpRunSpeedMax; // 0x53c } else // walk jump { if (bodyTargetSpeed > jumpWalkSpeedMax) bodyTargetSpeed = jumpWalkSpeedMax; // 0x540 } if (bodyTargetSpeed < ZeroSpeed) bodyTargetSpeed = ZeroSpeed; } } switch (bodyAnimationState) { case StandingAnimation: // 0 // STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed // 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop // gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full // reverse speed, and several stand-entry paths (turn exit, terminal // poses) never touch the cycle -- Standing could be entered with a // stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master // LOOKS still (case 0 never advances the clip) but the stale cycle // REPLICATES and the peer's replicant marches in place. A standing // mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8). if (bodyCycleSpeed != 0.0f) { bodyCycleSpeed = 0.0f; ForceUpdate(8); // type-3 record: legs stopped } if (bodyTargetSpeed <= standSpeed) // 0x6b4 <= 0x530 { distance = 0.0f; break; } SetBodyAnimation(5); // FALLTHROUGH case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0x0b: case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15: case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20: distance = bodyAnimation.Advance( time_slice * globalTimeScale * idleStrideScale, loop); bodyCycleSpeed = distance / time_slice; break; case 1: distance = 0.0f; break; case 6: case 7: // WalkToRun if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < standSpeed) bodyCycleSpeed = standSpeed; } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength; } distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / walkStrideLength) * globalTimeScale, loop); break; case 0x0c: case 0x0d: // StandToReverse if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < reverseSpeedMax) bodyCycleSpeed = reverseSpeedMax; } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > reverseSpeedMax2) bodyCycleSpeed = reverseSpeedMax2; } distance = bodyAnimation.Advance( time_slice * (bodyCycleSpeed / reverseStrideLength) * globalTimeScale, loop); break; case 0x12: case 0x13: // WalkToGimp if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= gimpCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < gimpStrideLength) bodyCycleSpeed = gimpStrideLength; } } else { bodyCycleSpeed += gimpCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > gimpSpeedMax) bodyCycleSpeed = gimpSpeedMax; } { Scalar ratio = bodyCycleSpeed / gimpStrideLength; if (ratio <= ZeroSpeed) ratio = -ratio; distance = bodyAnimation.Advance( ratio * time_slice * globalTimeScale, loop); } break; case 0x18: case 0x19: // FallForward / FallBackward (jump) { Scalar ratio; if (MovementMode() == 3) // run jump: caps 0x53c / 0x544 { if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < jumpRunSpeedMax) bodyCycleSpeed = jumpRunSpeedMax; // 0x53c } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > jumpRunStrideLength) bodyCycleSpeed = jumpRunStrideLength; // 0x544 } ratio = bodyCycleSpeed / jumpRunStrideLength; // 0x544 } else // walk jump: caps 0x540 / 0x548 { if (bodyTargetSpeed <= bodyCycleSpeed) { if (bodyTargetSpeed < bodyCycleSpeed) { bodyCycleSpeed -= forwardCycleRate * time_slice; if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed < jumpWalkSpeedMax) bodyCycleSpeed = jumpWalkSpeedMax; // 0x540 } } else { bodyCycleSpeed += forwardCycleRate * time_slice; if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed; if (bodyCycleSpeed > jumpWalkStrideLength) bodyCycleSpeed = jumpWalkStrideLength; // 0x548 } ratio = bodyCycleSpeed / jumpWalkStrideLength; // 0x548 } distance = bodyAnimation.Advance( time_slice * ratio * globalTimeScale, loop); } break; default: DebugStream << (AnimationNames + bodyAnimationState * 0x3c); DebugStream.Emit(); Verify(False, "Unsupported mech animation!", "d:\\tesla\\bt\\bt\\MECH2.CPP", 0x2E8); } return distance; } //########################################################################### //########################################################################### // AdvanceLegAnimationAirborne (channel A, jump-capable) // // @004a71f4 (MECH2.CPP:0x672) // // Airborne variant of AdvanceLegAnimation. Like the ground version it reads // the live commanded speed from the controls subsystem, but here it also // CLAMPS that source value to the jump speed limit (writing it back), and // handles the FallForward / FallBackward jump cycles (0x18/0x19). No death // latch / footstep block; gimp-to-stand and lateral falls join the normal // advance group. //########################################################################### //########################################################################### Scalar Mech::AdvanceLegAnimationAirborne(Scalar time_slice) { ReconMotionSource *motionSource = *(ReconMotionSource **)(this->controlSource); // **(this+0x128) Scalar distance = 0.0f; int mode = MovementMode(); // this+0x40 = simulationState int state = legAnimationState; // this+0x3b0 // // Clamp the source commandedSpeed (motionSource->commandedSpeed, +0x128) // to the jump speed cap while in a moving cycle, then floor at zero. // if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2 || (unsigned)(state - 0x12) < 2) { if (mode == 3) { if (motionSource->commandedSpeed > jumpRunSpeedMax) motionSource->commandedSpeed = jumpRunSpeedMax; // 0x53c } else { if (motionSource->commandedSpeed > jumpWalkSpeedMax) motionSource->commandedSpeed = jumpWalkSpeedMax; // 0x540 } if (motionSource->commandedSpeed < ZeroSpeed) motionSource->commandedSpeed = ZeroSpeed; } switch (legAnimationState) { case StandingAnimation: // 0 // STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed // 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop // gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full // reverse speed, and several stand-entry paths (turn exit, terminal // poses) never touch the cycle -- Standing could be entered with a // stale legCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master // LOOKS still (case 0 never advances the clip) but the stale cycle // REPLICATES and the peer's replicant marches in place. A standing // mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8). if (legCycleSpeed != 0.0f) { legCycleSpeed = 0.0f; ForceUpdate(8); // type-3 record: legs stopped } if (motionSource->commandedSpeed <= standSpeed) // +0x128 <= 0x530 { distance = 0.0f; break; } SetLegAnimation(5); // FALLTHROUGH case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b: case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15: case 0x16: case 0x17: case 0x1a: case 0x1b: case 0x20: advance_normally: distance = legAnimation.Advance( time_slice * globalTimeScale * idleStrideScale, 1); legCycleSpeed = distance / time_slice; break; case 1: distance = 0.0f; break; case 4: // WalkToStand if (standSpeed < motionSource->commandedSpeed) { legStateAlarm.SetLevel(0); ForceUpdate(8); // type-3 record break; } goto advance_normally; case 6: case 7: // WalkToRun if (motionSource->commandedSpeed <= legCycleSpeed) { if (motionSource->commandedSpeed < legCycleSpeed) { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed < standSpeed) legCycleSpeed = standSpeed; } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed > walkStrideLength) legCycleSpeed = walkStrideLength; } distance = legAnimation.Advance( time_slice * (legCycleSpeed / walkStrideLength) * globalTimeScale, 1); break; case 0x0c: case 0x0d: // StandToReverse if (motionSource->commandedSpeed <= legCycleSpeed) { if (motionSource->commandedSpeed < legCycleSpeed) { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed < reverseSpeedMax) legCycleSpeed = reverseSpeedMax; } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed > reverseSpeedMax2) legCycleSpeed = reverseSpeedMax2; } distance = legAnimation.Advance( time_slice * (legCycleSpeed / reverseStrideLength) * globalTimeScale, 1); break; case 0x12: case 0x13: // WalkToGimp if (motionSource->commandedSpeed <= legCycleSpeed) { if (motionSource->commandedSpeed < legCycleSpeed) { legCycleSpeed -= gimpCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed < gimpStrideLength) legCycleSpeed = gimpStrideLength; } } else { legCycleSpeed += gimpCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed > gimpSpeedMax) legCycleSpeed = gimpSpeedMax; } { Scalar ratio = legCycleSpeed / gimpStrideLength; if (ratio <= ZeroSpeed) ratio = -ratio; distance = legAnimation.Advance(ratio * time_slice * globalTimeScale, 1); } break; case 0x18: case 0x19: // FallForward / FallBackward (jump) { Scalar ratio; if (mode == 3) // run jump { if (motionSource->commandedSpeed <= legCycleSpeed) { if (motionSource->commandedSpeed < legCycleSpeed) { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed < jumpRunSpeedMax) legCycleSpeed = jumpRunSpeedMax; } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed > jumpRunStrideLength) legCycleSpeed = jumpRunStrideLength; } ratio = legCycleSpeed / jumpRunStrideLength; // 0x544 } else // walk jump { if (motionSource->commandedSpeed <= legCycleSpeed) { if (motionSource->commandedSpeed < legCycleSpeed) { legCycleSpeed -= forwardCycleRate * time_slice; if (legCycleSpeed < motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed < jumpWalkSpeedMax) legCycleSpeed = jumpWalkSpeedMax; } } else { legCycleSpeed += forwardCycleRate * time_slice; if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed; if (legCycleSpeed > jumpWalkStrideLength) legCycleSpeed = jumpWalkStrideLength; } ratio = legCycleSpeed / jumpWalkStrideLength; // 0x548 } distance = legAnimation.Advance(time_slice * ratio * globalTimeScale, 1); } break; default: DebugStream << (AnimationNames + legAnimationState * 0x3c); DebugStream.Emit(); Verify(False, "Unsupported mech animation!", "d:\\tesla\\bt\\bt\\MECH2.CPP", 0x672); } return distance; } //===========================================================================// // End of recovered mech2.cpp slice. //===========================================================================//