Files
BT412/game/reconstructed/mech2.cpp
T
arcattackandClaude Opus 4.8 bfdd41bb9d Gait: turn-then-walk stutter ROOT-CAUSED + FIXED -- one skeleton writer (task #64)
User repro (100%): turn in place, push forward before the turn stops -> the gait
skips/stutters + visibly reduced bob; standstill starts always clean.  Three-layer
fix, user-verified:

1) THE DISPLAY BUG (the actual visible artifact): v5's "body advances first with
mj=1, leg overwrites last, so body drift can't show" was FALSE.  Whenever the two
gait channels phase-split, the BODY channel's out-of-phase joint writes leaked
into the rendered skeleton (rhythmic leg skips, averaged-down bob) while every
leg-channel trace read clean -- the leg DATA was fine, the RENDERED pose wasn't
the leg's.  v6: AdvanceBodyAnimation(dt, mj=0) -- the body still advances +
projects for replication (records/cycle speeds unchanged) but no longer touches
the skeleton.  One writer, structurally; matches the binary's own observable
("body phase drift is locally INVISIBLE in the binary").  BT_BODY_MJ=1 = old A/B.

2) THE SPLIT SEED: the bring-up trn trigger armed only the LEG channel, so a
turn-in-place entry guaranteed the channels re-entered walk frames apart and the
walk cycles ran permanently out of phase.  Lockstep: the Standing trn entry arms
BOTH channels the same frame (body case-4 twin added, same rate/keying), gated on
both Standing.  The authentic dispatcher (un-decompiled master-perf gap 0x4a9b5c-
0x4ab188, the sole reader of turnDemand/turnCapable) armed both -- the body's
case-4 machinery is dead code otherwise [T1].

3) THE POSE-MATCH INVARIANT: the engine has NO pose blending; transitions avoid
pops purely by authored pose-matched boundaries.  The old trn exits cut the pivot
clip MID-STEP (legFrm 7->1 teleport).  Now: entry gated on near-zero speed
(turn-IN-place); on forward command the pivot FAST-FORWARDS to completion (x4)
and the authentic finish callback lands Standing at the stand pose -> the normal
pose-matched stand->walk runs.  Decompiled reverse abort kept; the standSpeed
mid-clip abort subsumed (it WAS the pose cut).  [T3: 0.25*standSpeed threshold +
4x rate stand in for the gap's constants.]

Harness: BT_FORCE_TURN now reaches gBTDrive.turn (was silently inert for the
gait), BT_WALK_DELAY=<s> holds forced throttle then ramps (the turn-first repro),
BT_GAIT_TRACE=1 per-frame gait trace.  Regressions: standstill start, turn-entry,
pure pivot loops, run cycle -- all clean, bob full amplitude (1.33/1.32).
KB: locomotion.md v6 section + trn reconstruction + symptom-family closure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:52:35 -05:00

1450 lines
59 KiB
C++

//===========================================================================//
// 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 <bt.hpp>
#pragma hdrstop
#if !defined(MECH_HPP)
# include <mech.hpp> // Mech class -- owned by mech.cpp slice
#endif
#if !defined(MECHMPPR_HPP)
# include <mechmppr.hpp> // MechControlsMapper -- the leg channel's LIVE speed source
#endif
#if !defined(APP_HPP)
# include <app.hpp>
#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. The authentic entry DISPATCHER lives in the
// un-decompiled master-perf gap (0x4a9b5c-0x4ab188), the sole reader of
// turnDemand(+0x12c) + turnCapable(+0x588); the decompiled lifecycle is
// case-4 advance/exit (below) + the clip-finish drop-to-stand. The
// authentic gate is turn-IN-PLACE == NEAR-ZERO commanded speed. The old
// stand-in gated on `ZeroSpeed <= commandedSpeed` -- i.e. it re-armed trn
// for ANY forward speed in [0, standSpeed~6.83], so pushing forward mid-
// turn SWALLOWED the throttle (the mech kept pivoting) until spd crossed
// standSpeed, then lurched to walk (task #64 user-reported stutter). Gate
// on near-zero speed instead: the instant real throttle is applied the trn
// re-arm stops, the in-flight pivot finishes to Standing, and case 0 hands
// off to walk normally. turnEntrySpeed = 0.25*standSpeed is a STAND-IN for
// the gap's threshold constant [T3]; the deadband mirrors the replicant's.
const Scalar turnEntrySpeed = standSpeed * 0.25f;
if (hasCrashSet != 0 && mppr != 0
&& commandedSpeed >= ZeroSpeed && commandedSpeed < turnEntrySpeed
&& (mppr->turnDemand > 0.05f
|| mppr->turnDemand < -0.05f)
&& bodyAnimationState == StandingAnimation) // weld: enter only when BOTH can
{
// 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
SetBodyAnimation(4); // channel B, same frame [lockstep]
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). THE POSE-SNAP RULE (task #64, root-caused
// from the user's BT_GAIT_TRACE): this engine has NO pose blending -- every
// authored transition is pose-MATCHED at its boundary (swr starts from the
// stand pose; the walk clips alternate at matched extremes). Any exit that
// abandons the pivot MID-CLIP (SetLevel(0) or SetLegAnimation(5)) hard-resets
// the next clip to frame 0 while the legs are mid-step -> the legs TELEPORT
// (legFrm 7->1 in the trace) = the 100%-repro turn-then-walk stutter. Three
// prior fixes all failed because each still cut the clip mid-step, just at a
// different speed threshold.
//
// The reconstruction: when forward is commanded past the turn-in-place band,
// FAST-FORWARD the pivot to completion (accelerated advance) -- the feet
// hurry down to the plant, the clip's own finished-callback (case 4 ->
// SetLevel(0), the DECOMPILED one-shot tail) drops to Standing AT THE STAND
// POSE, and the normal pose-matched stand->walk path takes over. No mid-clip
// cut anywhere. trn has zero root translation, so the fast-forward moves no
// distance. [T3: the release threshold (0.25*standSpeed) + hurry rate (4x)
// stand in for the un-decompiled master-perf dispatcher constants (gap
// 0x4a9b5c); the completion->Standing tail itself is T1 decompiled.]
//
// The decompiled REVERSE abort is kept verbatim; the decompiled standSpeed
// abort is deliberately SUBSUMED by the hurry (it was the mid-clip pose cut
// itself -- any spd above the band fast-forwards to the plant instead, and
// Standing picks up the walk one frame after the callback). [T3 deviation
// from the literal case-4 exit, in service of the pose-match invariant.]
if (commandedSpeed < ZeroSpeed)
{
legStateAlarm.SetLevel(0);
ForceUpdate(8); // type-3 record
distance = 0.0f;
break;
}
{
Scalar trnRate = 1.0f;
if (standSpeed * 0.25f < commandedSpeed)
trnRate = 4.0f; // forward commanded: hurry the plant [T3]
distance = legAnimation.Advance( // same advance advance_normally does,
time_slice * globalTimeScale * idleStrideScale * trnRate, 1); // rate-scaled
legCycleSpeed = distance / time_slice; // trn z=0 -> stays 0 (no travel)
}
break;
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 -- the body's pose flashes through on the leg's
// clip-boundary frames (body advances mj=1 FIRST, leg overwrites LAST;
// boundary frames the leg doesn't write) = the rhythmic gait skip + halved
// bob the user reproduced 100% from turn-then-walk (standstill starts weld
// the phases, which is why they never stuttered). [T3 rates/threshold; the
// per-channel case-4 machinery itself is T1 decompiled.]
{
MechControlsMapper *bm = MappingMapper();
const Scalar bspd = (bm != 0) ? bm->speedDemand : 0.0f;
if (bspd < ZeroSpeed) // reverse abort (leg-symmetric)
{
bodyStateAlarm.SetLevel(0);
ForceUpdate(8);
distance = 0.0f;
break;
}
Scalar trnRate = 1.0f;
if (standSpeed * 0.25f < bspd)
trnRate = 4.0f; // hurry the plant (leg-symmetric)
distance = bodyAnimation.Advance(
time_slice * globalTimeScale * idleStrideScale * trnRate, 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.
//===========================================================================//