Files
BT411/game/reconstructed/mech2.cpp
T
Joe DiPrimaandClaude Opus 5 6a96fb6420 #52 the peer STANDING-LOCK: case 0's fallthrough was intercepted
A replicant could not start walking between gait-change records.  The port's
body case 4 (the task-#64 lockstep twin) is an INSERTION sitting between case 0
and the advance group; in the binary case 4 is a MEMBER of that group
(FUN_004a5678 @004a5678: case 2,3,4,5,8,... -- no turn block, no speed exit),
so case 0's fallthrough is meant to land on Advance().  The insertion caught it.

On a replicant that is not a race but an identity: case 0 arms walk iff
standSpeed < bodyTargetSpeed, and the inserted block resets iff standSpeed <
bspd -- where bspd IS bodyTargetSpeed for a replicant.  Same expression, so arm
and reset fire on the same frame, forever, and a peer parked at Standing with a
live replicated demand never cycles.  bodyCycleSpeed stays 0 while position
advances from dead reckoning: the skate.

This is the sequel to e91d447 (#82).  Before it the replicant branch read the
dead local mapper cell (0 forever), the exit never fired, and the fallthrough
worked BY ACCIDENT.  Fixing the dead cell closed the escape hatch.

Fix: case 0 -> goto advance_body_normally, the leg twin's own idiom, restoring
the binary's structure without touching the #64/#82 turn logic.
BT_NO_BODY_FALLTHRU=1 reverts.

Measured (2-node, scratchpad/night13/skatelock.sh):
  legacy  336 consecutive locked seconds, bspd=39.2324 bts=39.2324 every line
  fixed   0 locks, every pass
  master body-Standing samples 52 -> 21 (it locked too, invisibly at mj=0)
  turn-in-place intact: pivoter body state 4 x9 / leg state 4 x8, in lockstep

Diagnostics (both keepers): [skate] now carries bstate= -- the field lines
proved "both channels idle" but never named the state, which was the whole
answer; [bodySM]/[peergait] under BT_BODY_SM_LOG instrument the arm->reset pair
and a moving replicant's body channel.

NOT claimed: that this accounts for the night-13 field episodes.  That link is
inference -- locked + translating IS the skate signature by construction, but no
bench caught the two together.  bstate= settles it next playtest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 00:50:54 -05:00

1983 lines
85 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::AdvanceBodyAnimationGimp(1792 bytes)
// @004a71f4 Mech::AdvanceLegAnimationGimp (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>
#include <intrin.h> // _ReturnAddress -- the #82 trn-armer trap
#include <AUDCMP.hpp> // AudioComponent -- the foot-plant step-intensity broadcast
#include <AUDSRC.hpp> // AudioSource -- footstep-source identification
#include <AUDLVL.hpp> // AudioResource::GetAudioLevelOfDetail
#include <L4AUDLVL.hpp> // PatchLevelOfDetail -- bank/patch of the target source
#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 gimpLeftSpeedMax airborne speed cap, movementMode==3
// @0x540 gimpRightSpeedMax airborne speed cap, otherwise
// @0x544 gimpLeftStrideLength airborne clip length, movementMode==3
// @0x548 gimpRightStrideLength 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)
{
// DIAG (BT_DUCK_LOG): every leg re-arm with the caller's return address --
// the crouch-clobber hunt (who re-arms after the squat parks?).
if (getenv("BT_DUCK_LOG"))
{
char dbuf[96];
sprintf(dbuf, "[duck] SetLegAnimation(%d) ra=btl4+0x%lx", state,
(unsigned long)_ReturnAddress()
- (unsigned long)GetModuleHandleA(0));
DEBUG_STREAM << dbuf << std::endl << std::flush;
}
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)
// Drive the AnimationState indicators so the audio subsystem's state-watchers
// (footstep/gait/transition sounds) fire on every animation change. Guarded to
// the constructed range (stateCount 0x21) so an out-of-range clip can't trip
// StateIndicator::SetState's Verify(state<stateCount). [T2]
if (state >= 0 && state < 0x21)
{
// DIAG (BT_TRNTRAP): who arms state 4 on a REPLICANT? Module-relative
// ra (symbolize: tools/symcrash.py) -- the #82 trn-lock armer hunt.
if (state == 4 && getenv("BT_TRNTRAP")
&& GetInstance() == ReplicantInstance)
{
static int s_tt = 0;
if (s_tt++ < 60)
{
char ttbuf[96];
sprintf(ttbuf, "[trntrap] mech=%d ra=btl4+0x%lx",
(int)GetEntityID(),
(unsigned long)_ReturnAddress()
- (unsigned long)GetModuleHandleA(0));
DEBUG_STREAM << ttbuf << std::endl << std::flush;
}
}
// #78 audio-flake diag: print the LIVE indicator address once per mech
// so a session's [attrbind] ptr can be checked against it (stale-bind
// hypothesis: watchers bound to a recreated mech's dead indicator).
if (getenv("BT_AUDIO_SPATIAL")) { static int s_ai=0;
// BT_ANIMIND_CAP: the 200-print budget hid every post-1-minute state
// (two #82 investigations mis-read the silence as "never entered") --
// raise it for state-timeline diagnosis.
static int s_aiCap = -1;
if (s_aiCap < 0) { const char *c = getenv("BT_ANIMIND_CAP"); s_aiCap = (c && *c) ? atoi(c) : 200; }
if (s_ai++ < 6 || (state >= 22 && state <= 27 && s_ai < s_aiCap)
|| (s_aiCap > 200 && s_ai < s_aiCap))
DEBUG_STREAM << "[animind] mech=" << (int)GetEntityID()
<< " &animationState=" << (void*)&animationState
<< " inst=" << (int)(GetInstance() == ReplicantInstance)
<< " state=" << state
<< " audioWatchers=" << animationState.DebugAudioWatcherCount()
<< "\n" << std::flush; }
animationState.SetState(state);
replicantAnimationState.SetState(state);
}
// FootStep pulse: every locomotion-clip transition is one foot plant (the
// stride R<->L alternation -- MEASURED at runtime: forward walking alternates
// body states 12<->13, so the enum-name numbering above does NOT match the
// runtime clip ids; key on the locomotion RANGE instead). Clips 1..0x17 are
// the gait cycle (stand-to-walk through gimp); 0 = standing, >= 0x18 = falls/
// knockdown/death (those get collision/fall audio, not a step). Raise the
// Logical the FootStep AudioLogicalTrigger polls; IntegrateMotion decays it
// back to 0 a few frames later so each step is a clean rising edge. [T2]
// (AUDIO_FIDELITY F5) the old per-transition footStep PULSE is GONE:
// footStep is the authored CONTACT LEVEL, evaluated per frame in
// Mech::PerformAndWatch from jointlocal.y vs the clip's authored
// threshold (ANI hdr[2]) -- steps now fire at actual root-height
// contact crossings, not at clip boundaries with a fixed width.
// (F19) the old STEP-INTENSITY SEND is also GONE. Its premise ("the
// mixer feed was lost game code") was wrong: the feed is WHOLLY
// AUTHORED -- LocalAcceleration |linear| [0,10] -> ctl100 and
// LocalVelocity |linear| [0,0.6] -> ctl101 scale watchers drive the
// footstep volume mixer (0.4 base while moving + the per-stride
// acceleration kick). The port's gap was never publishing
// localAcceleration; mech4.cpp derives it exactly as the binary does
// (d(averaged velocity)/dt, part_012.c:15186-15195), and the invented
// broadcast fought that live chain.
}
//
// (@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)
{
// GIMP branch (FUN_004a6d8c top): a limping mech's body transitions run the
// gimp machine instead. Mode = the graphicAlarm level (the binary's real
// mech+0x40: 3=left-leg gimp, 4=right) read via the mechdmg bridge --
// NEVER graphicAlarm directly here (AlarmIndicator ODR split, gotcha #23).
{
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (the TU-safe read)
const int gl = BTMechGimpLevel(m);
if ((gl == 3 || gl == 4) && m->hasGimpClips)
return m->GimpBodyClipFinished(carryover, mj);
}
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)
{
// GIMP branch (FUN_004a6928 top): route a limping mech's leg transitions
// into the gimp machine (mode = graphicAlarm level via bridge, gotcha #23).
{
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (the TU-safe read)
const int gl = BTMechGimpLevel(m);
if ((gl == 3 || gl == 4) && m->hasGimpClips)
return m->GimpLegClipFinished(carryover);
}
// 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:
if (getenv("BT_DUCK_LOG"))
DEBUG_STREAM << "[duck] squat clip parked (case 2 -> leg alarm 1)\n" << std::flush;
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;
}
}
//###########################################################################
//###########################################################################
// GimpBodyClipFinished / GimpLegClipFinished (#78 visible limp)
//
// @004a6344 (body) / @004a7970 (leg) -- the GIMP transition machines, entered
// from the normal finished-callbacks when (gimpLevel 3|4) && hasGimpClips.
// Same walk/run/reverse alternation as the normal cbs, PLUS:
// - a top clamp of the demand to the gimped leg's speed cap (body: clamps
// bodyTargetSpeed@0x6b4; leg: clamps the LIVE mapper speedDemand and
// writes it back -- the binary's own "you can't outrun a shot leg");
// - PHASE-CORRECT limp entry from the walk cases: left-gimp (mode 3) enters
// 0x16/wgl only from a RIGHT step (5/6/0xe) -- the L case forces one more
// normal R step first; right-gimp (mode 4) mirrors into 0x17/wgr from the
// L case (7/0xf). The limp always starts on the correct foot.
// - the gg cycles 0x16/0x18 (left, ggr clip, stride @0x544) and 0x17/0x19
// (right, ggl, @0x548), continuing at gimp cadence or exiting through the
// gs transitions 0x1a/0x1b when the demand dies;
// - NO standing->reverse entry exists in the gimp machines (or drivers):
// the binary itself refuses REVERSE while gimped.
// Mode is the graphicAlarm level via the mechdmg bridge (gotcha #23).
//###########################################################################
//###########################################################################
Scalar
Mech::GimpBodyClipFinished(Scalar carryover, int mj)
{
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (TU-safe)
const int mode = BTMechGimpLevel(this); // binary: this+0x40
const int state = bodyAnimationState; // 0x728
const Scalar fcr = forwardCycleRate; // 0x344
const Scalar gts = globalTimeScale; // 0x5a8
// Top clamp (0x4a6352): while in a moving cycle, cap the body target speed
// to the gimped side's entry-clip speed, then floor at zero.
if ((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2
|| (unsigned)(state - 0x12) < 2)
{
const Scalar cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax; // 0x53c / 0x540
if (bodyTargetSpeed > cap) bodyTargetSpeed = cap;
if (bodyTargetSpeed < ZeroSpeed) bodyTargetSpeed = ZeroSpeed;
}
const Scalar cyc = bodyCycleSpeed; // 0x6b8
const Scalar tgt = bodyTargetSpeed; // 0x6b4 (post-clamp)
const Scalar T = carryover * gts; // plain end-tail time
int next;
switch (state)
{
default: // 0/1 + unmapped: parked
return 0.0f;
case 2:
bodyStateAlarm.SetLevel(1); return 0.0f;
case 3: case 4: case 8: case 9: case 0x14: case 0x15:
case 0x1a: case 0x1b: case 0x20:
bodyStateAlarm.SetLevel(0); return 0.0f;
// -- walk-R (0x4a6440): states 5,6,0xe -- the left-gimp entry point --
case 5: case 6: case 0xe:
{
bool cont = (tgt >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed);
if (!cont) { next = 9; break; } // walk->stand L
bool up = (tgt > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength);
if (up) { next = 0xb; break; } // toward run (dead once clamped)
int alt = 7; // normal alternation -> wwl
if (mode == 4) alt = 7; // right-gimp: one more normal step
if (mode == 3) alt = 0x16; // left-gimp: enter wgl on this R step
return BodyTransition(alt, carryover * cyc * gts / walkStrideLength, mj); // LAB_004a6505
}
// -- walk-L (0x4a63ff): states 7,0xf -- the right-gimp entry point --
case 7: case 0xf:
{
bool cont = (tgt >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed);
if (!cont) { next = 8; break; } // walk->stand R
bool up = (tgt > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength);
if (up) { next = 0xa; break; }
int alt = 6; // normal alternation -> wwr
if (mode == 3) alt = 6; // left-gimp: force back to the R step
if (mode == 4) alt = 0x17; // right-gimp: enter wgr on this L step
return BodyTransition(alt, carryover * cyc * gts / walkStrideLength, mj);
}
// -- run cycles (0x4a65f7 / 0x4a6663): 10/12 <-> 11/13 --
case 0xa: case 0xc:
{
bool cont = (tgt >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax);
if (cont) // LAB_004a6648 run-cadence tail
return BodyTransition(0xd, carryover * cyc * gts / reverseStrideLength, mj);
next = 0xf; break; // run->walk R
}
case 0xb: case 0xd:
{
bool cont = (tgt >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax);
if (cont)
return BodyTransition(0xc, carryover * cyc * gts / reverseStrideLength, mj);
next = 0xe; break;
}
// -- back cycles (0x4a66d5 / 0x4a6754): 0x10/0x12 <-> 0x11/0x13 --
case 0x10: case 0x12:
{
bool up = (tgt > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax);
if (up) { next = 0x15; break; } // back->stand L
Scalar t = carryover * cyc * gts / gimpStrideLength;
if (t <= 0.0f) t = -t; // 0x350 stored negative
return BodyTransition(0x13, t, mj);
}
case 0x11: case 0x13:
{
bool up = (tgt > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax);
if (up) { next = 0x14; break; }
Scalar t = carryover * cyc * gts / gimpStrideLength;
if (t <= 0.0f) t = -t;
return BodyTransition(0x12, t, mj);
}
// -- GIMP cycles (0x4a67cf / 0x4a6836): left 0x16/0x18, right 0x17/0x19 --
case 0x16: case 0x18:
{
bool cont = (tgt >= gimpLeftSpeedMax) || ((cyc - fcr * carryover) >= gimpLeftSpeedMax);
if (cont) // keep limping (ggr cycle)
return BodyTransition(0x18, carryover * cyc * gts / gimpLeftStrideLength, mj);
next = 0x1a; break; // demand died -> gsl exit
}
case 0x17: case 0x19:
{
bool cont = (tgt >= gimpRightSpeedMax) || ((cyc - fcr * carryover) >= gimpRightSpeedMax);
if (cont)
return BodyTransition(0x19, carryover * cyc * gts / gimpRightStrideLength, mj);
next = 0x1b; break; // -> gsr exit
}
}
return BodyTransition(next, T, mj); // shared plain tail
}
Scalar
Mech::GimpLegClipFinished(Scalar carryover)
{
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (TU-safe)
const int mode = BTMechGimpLevel(this); // binary: this+0x40
const int state = legAnimationState; // 0x3b0
MechControlsMapper *mppr = MappingMapper(); // **(this+0x128)
// Top clamp (0x4a7995): cap the LIVE COMMANDED speedDemand itself while in
// a moving cycle, writing the clamp back -- this is the binary's authentic
// gimp slowdown (the mapper re-derives demand each tick; this cb re-caps
// it every clip end, and the gimp cycle cadence below enforces it anyway).
if (((unsigned)(state - 6) < 2 || (unsigned)(state - 0x0c) < 2
|| (unsigned)(state - 0x12) < 2) && mppr != 0)
{
const Scalar cap = (mode == 3) ? gimpLeftSpeedMax : gimpRightSpeedMax; // 0x53c / 0x540
if (mppr->speedDemand > cap) mppr->speedDemand = cap;
if (mppr->speedDemand < ZeroSpeed) mppr->speedDemand = ZeroSpeed;
}
const Scalar spd = (mppr != 0) ? mppr->speedDemand : 0.0f; // (binary derefs; port guards null)
const Scalar fcr = forwardCycleRate; // 0x344
const Scalar gts = globalTimeScale; // 0x5a8
const Scalar cyc = legCycleSpeed; // 0x348
const Scalar T = carryover * gts;
int next;
switch (state) // all leg tails mj=1 (binary)
{
default:
return 0.0f;
case 2:
legStateAlarm.SetLevel(1); return 0.0f;
case 3: case 4: case 8: case 9: case 0x14: case 0x15:
case 0x1a: case 0x1b: case 0x20:
legStateAlarm.SetLevel(0); return 0.0f;
// -- walk-R (0x4a7a52): states 5,6,0xe -- left-gimp entry --
case 5: case 6: case 0xe:
{
bool cont = (spd >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed);
if (!cont) { next = 9; break; }
bool up = (spd > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength);
if (up) { next = 0xb; break; }
int alt = 7;
if (mode == 4) alt = 7; // right-gimp: one more normal step
if (mode == 3) alt = 0x16; // left-gimp: enter wgl on this R step
return LegTransition(alt, carryover * cyc * gts / walkStrideLength, 1); // LAB_004a7b38
}
// -- walk-L (0x4a7bb5): states 7,0xf -- right-gimp entry --
case 7: case 0xf:
{
bool cont = (spd >= standSpeed) || ((cyc - fcr * carryover) >= standSpeed);
if (!cont) { next = 8; break; }
bool up = (spd > walkStrideLength) && ((fcr * carryover + cyc) > walkStrideLength);
if (up) { next = 0xa; break; }
int alt = 6;
if (mode == 3) alt = 6; // left-gimp: force back to the R step
if (mode == 4) alt = 0x17; // right-gimp: enter wgr on this L step
return LegTransition(alt, carryover * cyc * gts / walkStrideLength, 1);
}
// -- run cycles (0x4a7c33 / 0x4a7c93): --
case 0xa: case 0xc:
{
bool cont = (spd >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax);
if (cont) // LAB_004a7c79
return LegTransition(0xd, carryover * cyc * gts / reverseStrideLength, 1);
next = 0xf; break;
}
case 0xb: case 0xd:
{
bool cont = (spd >= reverseSpeedMax) || ((cyc - fcr * carryover) >= reverseSpeedMax);
if (cont)
return LegTransition(0xc, carryover * cyc * gts / reverseStrideLength, 1);
next = 0xe; break;
}
// -- back cycles: 0x10/0x12 <-> 0x11/0x13 --
case 0x10: case 0x12:
{
bool up = (spd > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax);
if (up) { next = 0x15; break; }
Scalar t = carryover * cyc * gts / gimpStrideLength;
if (t <= 0.0f) t = -t;
return LegTransition(0x13, t, 1);
}
case 0x11: case 0x13:
{
bool up = (spd > gimpSpeedMax) && ((gimpCycleRate * carryover + cyc) > gimpSpeedMax);
if (up) { next = 0x14; break; }
Scalar t = carryover * cyc * gts / gimpStrideLength;
if (t <= 0.0f) t = -t;
return LegTransition(0x12, t, 1);
}
// -- GIMP cycles: left 0x16/0x18, right 0x17/0x19 --
case 0x16: case 0x18:
{
bool cont = (spd >= gimpLeftSpeedMax) || ((cyc - fcr * carryover) >= gimpLeftSpeedMax);
if (cont)
return LegTransition(0x18, carryover * cyc * gts / gimpLeftStrideLength, 1);
next = 0x1a; break;
}
case 0x17: case 0x19:
{
bool cont = (spd >= gimpRightSpeedMax) || ((cyc - fcr * carryover) >= gimpRightSpeedMax);
if (cont)
return LegTransition(0x19, carryover * cyc * gts / gimpRightStrideLength, 1);
next = 0x1b; break;
}
}
return LegTransition(next, T, 1); // shared plain tail (mj=1)
}
//###########################################################################
//###########################################################################
// 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)
// REPLICANT DEMAND FEED (#82 final root, 2026-07-30): a replicant's LOCAL
// mapper cell is a dead 0 (input never drives it), so every demand
// threshold in this machine failed on peers -- from STAND the walk-begin
// could never fire and the trn state churned on the turn signal forever
// ("lifting legs in an alternating turning fashion"). It never mattered
// while a peer was mid-cycle (clip-end chains keep cycles going, which is
// why straight-line/grass limpers replicated fine) -- it bit the moment a
// KNOCKDOWN recovery dropped the replicant to STAND. The binary replicant
// reads a LIVE demand here (its mapper cell replicates with the subsystem
// records); the port's replicated equivalent is bodyTargetSpeed (@0x6b4 --
// every update record's speedDemand writes it on RX). [gimpfeed]-probe
// proof: AdvanceLegAnimationGimp never even runs on replicants, so THIS
// driver is the peer's one and only leg machine.
Scalar commandedSpeed =
(GetInstance() == ReplicantInstance) ? bodyTargetSpeed
: (mppr != 0) ? mppr->speedDemand : 0.0f;
// DIAG (BT_GIMPFEED): 1 Hz -- the NORMAL driver is the only leg machine a
// replicant runs; print every threshold operand it sees.
if (getenv("BT_GIMPFEED") && GetInstance() == ReplicantInstance)
{
static Scalar s_gfAcc = 0.0f; s_gfAcc += time_slice;
if (s_gfAcc >= 1.0f) { s_gfAcc = 0.0f;
DEBUG_STREAM << "[gimpfeed] cmd=" << commandedSpeed
<< " bts=" << bodyTargetSpeed
<< " standSpeed=" << standSpeed
<< " state=" << (int)legStateAlarm.GetLevel()
<< " gl=" << ([](Mech *m){ extern int BTMechGimpLevel(void*); return BTMechGimpLevel(m); })(this)
<< std::endl << std::flush; }
}
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 (turnCapable != 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;
// #52 probe (BT_BODY_SM_LOG): case 0 and the inserted turn block below run
// in the SAME invocation, so a plain local proves the arm->reset pair --
// no cross-frame state, no per-mech bookkeeping.
int armedFromStanding = 0;
// 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 -- into the ADVANCE GROUP, which is where the binary
// sends it. #52 SKATE ROOT CAUSE (2026-08-07): in FUN_004a5678 case 4
// is a MEMBER of the advance list (case 2,3,*4*,5,8,...), so a state
// just armed away from Standing lands on Advance(). The port's turn
// block below is an INSERTION (task #64 lockstep twin) and, sitting
// between case 0 and the advance group, it intercepted that fallthrough.
// On a REPLICANT that is fatal and not a race: case 0 arms walk iff
// `standSpeed < bodyTargetSpeed`, and the inserted block's exit tests
// `standSpeed < bspd` where bspd IS bodyTargetSpeed for a replicant --
// the SAME expression. Arm and reset therefore fire on the same frame,
// every frame, and a peer parked at Standing with a live replicated
// demand can never start cycling (reverse likewise: both sides test
// `< ZeroSpeed`). It cycles again only when a record sets the state
// directly (ReadUpdateRecord, mech.cpp) -- the observed self-recovery.
// Masters escape because their two tests read DIFFERENT cells
// (bodyTargetSpeed = last-sent vs the live mapper speedDemand) and
// because the body channel is mj=0 there, so its stall is invisible.
// Introduced by e91d447 (#82): before it the replicant branch read the
// dead mapper cell (0 forever), so the exit never fired and this
// fallthrough worked BY ACCIDENT. Fixing the dead cell closed the
// accidental escape hatch and the trn-lock skate came back as a
// Standing-lock skate. BT_NO_BODY_FALLTHRU=1 restores the old path.
armedFromStanding = (int)bodyStateAlarm.GetLevel();
{
static const int s_bodyFallthru = getenv("BT_NO_BODY_FALLTHRU") ? 0 : 1;
if (s_bodyFallthru)
goto advance_body_normally;
}
// FALLTHROUGH (legacy path only)
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();
// REPLICANT: the local mapper's speedDemand is a dead cell (nothing
// writes it on a peer -- reads 0 forever, so a peer that entered trn
// could NEVER take the speed exit = the #82 trn-lock). The peer's
// commanded speed is the REPLICATED demand (bodyTargetSpeed@0x6b4,
// stamped by every record RX) -- same source the peer Standing case
// walks on, so entry and exit judge the same number.
const Scalar bspd = (GetInstance() == ReplicantInstance)
? bodyTargetSpeed
: (bm != 0) ? bm->speedDemand : 0.0f;
if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric)
{
// #52 probe: when this fires on a state case 0 JUST armed, the
// mech is being pushed straight back to Standing on the same
// frame it tried to leave it -- the Standing-lock. On a
// replicant `bspd` IS the same cell case 0 tested, so the pair
// is unconditional, not a race.
if (armedFromStanding != 0 && getenv("BT_BODY_SM_LOG"))
{
static float s_bsm = 0.0f; s_bsm += time_slice;
if (s_bsm >= 1.0f) { s_bsm = 0.0f;
DEBUG_STREAM << "[bodySM] " << (GetInstance() == ReplicantInstance
? "REPLICANT " : "master ")
<< GetEntityID() << " case0 armed " << armedFromStanding
<< " -> turn-block RESET to Standing bspd=" << (float)bspd
<< " bts=" << (float)bodyTargetSpeed
<< " standSpeed=" << (float)standSpeed
<< " (STANDING-LOCK)\n" << std::flush; }
}
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:
advance_body_normally: // case 0's fallthrough target (leg twin: advance_normally)
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;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < standSpeed) ? standSpeed
: ((peerMirrorSpeed > walkStrideLength) ? walkStrideLength : peerMirrorSpeed);
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;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < reverseSpeedMax) ? reverseSpeedMax
: ((peerMirrorSpeed > reverseSpeedMax2) ? reverseSpeedMax2 : peerMirrorSpeed);
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;
}
//###########################################################################
//###########################################################################
// AdvanceBodyAnimationGimp (channel B, limping)
//
// @004a5bf8 (MECH2.CPP:0x2E8)
//
// GIMP (limp-gait) variant of AdvanceBodyAnimation, selected when
// (gimpLevel 3|4) && hasGimpClips ("Airborne"/jump-jet was a misread -- the
// clip set is wg/gg/gs, #78). Adds a pre-clamp of bodyTargetSpeed to the
// gimped side's speed cap and drives the gg limp cycles (0x18 left / 0x19
// right) at gimp cadence; the wg entries (0x16/0x17) and gs exits (0x1a/0x1b)
// join the plain advance group. Standing (case 0) only ever enters FORWARD
// walk -- the binary refuses reverse while gimped.
//###########################################################################
//###########################################################################
Scalar
Mech::AdvanceBodyAnimationGimp(Scalar time_slice, int loop)
{
Scalar distance = 0.0f;
// RE-SYNC alarm -> state member (the binary's one cell is split in the
// recon, same as the ground drivers at :831/:1181). Without this the
// member freezes at its pre-gimp value the moment this driver takes over
// and the whole machine pins in one state (live-diagnosed 2026-07-30).
bodyAnimationState = (int)bodyStateAlarm.GetLevel();
//
// While in any forward/reverse/gimp *moving* cycle, clamp the target to
// the gimp 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)
{
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23)
if (BTMechGimpLevel(this) == 3) // left leg gimped
{
if (bodyTargetSpeed > gimpLeftSpeedMax) bodyTargetSpeed = gimpLeftSpeedMax; // 0x53c
}
else // walk jump
{
if (bodyTargetSpeed > gimpRightSpeedMax) bodyTargetSpeed = gimpRightSpeedMax; // 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;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < standSpeed) ? standSpeed
: ((peerMirrorSpeed > walkStrideLength) ? walkStrideLength : peerMirrorSpeed);
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;
}
if (peerMirrorSpeed >= 0.0f) // peer: cadence == actual mirrored ground speed
bodyCycleSpeed = (peerMirrorSpeed < reverseSpeedMax) ? reverseSpeedMax
: ((peerMirrorSpeed > reverseSpeedMax2) ? reverseSpeedMax2 : peerMirrorSpeed);
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;
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23)
if (BTMechGimpLevel(this) == 3) // left-gimp cycle: caps 0x53c / 0x544
{
if (bodyTargetSpeed <= bodyCycleSpeed)
{
if (bodyTargetSpeed < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed < gimpLeftSpeedMax) bodyCycleSpeed = gimpLeftSpeedMax; // 0x53c
}
}
else
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed > gimpLeftStrideLength) bodyCycleSpeed = gimpLeftStrideLength; // 0x544
}
ratio = bodyCycleSpeed / gimpLeftStrideLength; // 0x544
}
else // walk jump: caps 0x540 / 0x548
{
if (bodyTargetSpeed <= bodyCycleSpeed)
{
if (bodyTargetSpeed < bodyCycleSpeed)
{
bodyCycleSpeed -= forwardCycleRate * time_slice;
if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed < gimpRightSpeedMax) bodyCycleSpeed = gimpRightSpeedMax; // 0x540
}
}
else
{
bodyCycleSpeed += forwardCycleRate * time_slice;
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
if (bodyCycleSpeed > gimpRightStrideLength) bodyCycleSpeed = gimpRightStrideLength; // 0x548
}
ratio = bodyCycleSpeed / gimpRightStrideLength; // 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;
}
//###########################################################################
//###########################################################################
// AdvanceLegAnimationGimp (channel A, limping)
//
// @004a71f4 (MECH2.CPP:0x672)
//
// GIMP variant of AdvanceLegAnimation (see the body variant's note). Like
// the ground version it reads the live commanded speed from the controls
// subsystem, but here it also CLAMPS that source value to the gimped side's
// speed cap (writing it back -- the authentic "can't outrun a shot leg"),
// and drives the gg limp cycles (0x18/0x19). No death latch / footstep
// block; wg entries and gs exits join the normal advance group.
//###########################################################################
//###########################################################################
Scalar
Mech::AdvanceLegAnimationGimp(Scalar time_slice)
{
// Binary: **(this+0x128) + 0x128 = the mapper's live speedDemand. The
// port's controlSource@0x128 is NOT wired (null -> the first live gimp
// engagement crashed here, 2026-07-30 bench) -- use the roster idiom the
// ground driver + LegClipFinished use (MappingMapper), pointing the shim
// straight at the speedDemand cell.
MechControlsMapper *gimpMppr = MappingMapper();
static Scalar s_nullDemand = 0.0f; // no mapper -> demand 0, writes inert
// REPLICANT DEMAND FEED (#82 final root, 2026-07-30): a replicant's LOCAL
// mapper demand is a dead 0 (no input ever drives it), so every speed
// threshold in this machine read 0 on peers -- in particular the trn exit
// (standSpeed < commandedSpeed) could NEVER fire, and a gimped replicant
// that staggered through stand->trn was LOCKED in turn-in-place forever
// (the observed "lifting legs in an alternating turning fashion" skate;
// unblinded [animind] timeline: SIX transitions in 3 minutes, ending at
// state 4). The binary's replicant reads a LIVE demand here because the
// mapper's speedDemand cell replicates with the subsystem records; the
// port's replicated equivalent of that same value is bodyTargetSpeed
// (@0x6b4 -- every update record's speedDemand writes it on RX). Feed
// the machine from it on replicants; the gimp caps that write back
// through the source are transient there (the next record rewrites it),
// and masters keep the authentic live mapper cell.
ReconMotionSource *motionSource =
(GetInstance() == ReplicantInstance)
? (ReconMotionSource *)&bodyTargetSpeed
: gimpMppr
? (ReconMotionSource *)&gimpMppr->speedDemand
: (ReconMotionSource *)&s_nullDemand;
Scalar distance = 0.0f;
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23)
int mode = BTMechGimpLevel(this); // binary this+0x40 = gimp level
// DIAG (BT_GIMPFEED): 1 Hz -- what demand does this machine actually see?
if (getenv("BT_GIMPFEED") && GetInstance() == ReplicantInstance)
{
static Scalar s_gfAcc = 0.0f; s_gfAcc += time_slice;
if (s_gfAcc >= 1.0f) { s_gfAcc = 0.0f;
DEBUG_STREAM << "[gimpfeed] repl cmd=" << motionSource->commandedSpeed
<< " bts=" << bodyTargetSpeed << " state=" << legAnimationState
<< " standSpeed=" << standSpeed << " mode=" << mode << std::endl << std::flush; }
}
// RE-SYNC alarm -> state member (see AdvanceBodyAnimationGimp note).
legAnimationState = (int)legStateAlarm.GetLevel();
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 > gimpLeftSpeedMax)
motionSource->commandedSpeed = gimpLeftSpeedMax; // 0x53c
}
else
{
if (motionSource->commandedSpeed > gimpRightSpeedMax)
motionSource->commandedSpeed = gimpRightSpeedMax; // 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
{
// TURN-IN-PLACE while GIMPED (#78 field find, 2026-07-29: "rotating
// statue"). The binary's trn dispatcher (FUN_004a9b5c, master perf)
// runs OUTSIDE the driver selection, so it arms the turn step for
// gimped mechs too -- 71f4's case 4 exists to advance it. The port
// relocated that dispatcher into the NORMAL driver's Standing case,
// which stops running the moment this driver takes over. Mirror it
// (same gates + lockstep body arm; no reverse entry here -- the gimp
// machines refuse reverse).
const int trnIsRepl2 = (GetInstance() == ReplicantInstance);
const Scalar trnEntryMax2 = trnIsRepl2 ? standSpeed * 0.25f : standSpeed;
if (turnCapable != 0 && gimpMppr != 0
&& motionSource->commandedSpeed >= ZeroSpeed
&& motionSource->commandedSpeed <= trnEntryMax2
&& (gimpMppr->turnDemand > 0.05f
|| gimpMppr->turnDemand < -0.05f)
&& (trnIsRepl2 || bodyAnimationState == StandingAnimation))
{
SetLegAnimation(4); // trn, channel A
if (!trnIsRepl2)
SetBodyAnimation(4); // lockstep, masters
goto advance_normally;
}
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 < gimpLeftSpeedMax) legCycleSpeed = gimpLeftSpeedMax;
}
}
else
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed;
if (legCycleSpeed > gimpLeftStrideLength) legCycleSpeed = gimpLeftStrideLength;
}
ratio = legCycleSpeed / gimpLeftStrideLength; // 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 < gimpRightSpeedMax) legCycleSpeed = gimpRightSpeedMax;
}
}
else
{
legCycleSpeed += forwardCycleRate * time_slice;
if (legCycleSpeed > motionSource->commandedSpeed) legCycleSpeed = motionSource->commandedSpeed;
if (legCycleSpeed > gimpRightStrideLength) legCycleSpeed = gimpRightStrideLength;
}
ratio = legCycleSpeed / gimpRightStrideLength; // 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.
//===========================================================================//