The master limped clean (218x gimp-cycle records, zero trn) -- a _ReturnAddress trap on SetAnimationState(4) proved ALL peer trn arming came from the port's own mech4 turn-step block, not the type-3 reader. Two dead ends, one root: - mech4 peer arm: gated on !wantsWalk (standSpeed < bodyTargetSpeed), exits trn when walking demand appears -- the leg twin's authentic precedence (part_012.c:12013, the Standing speed test outranks the turn test). - mech2 body case 4: bspd reads bodyTargetSpeed on ReplicantInstance; the local mapper's speedDemand is a dead cell on a peer (reads 0 forever -> no exit). Verified 2-node timeline: arena circle replicant 218x state-24 / 0x state-4 (was 116x trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths. Diagnostics kept: BT_TRNTRAP ra-trap, BT_ANIMIND_CAP, [gimpfeed], [replgimp] extension. KB: locomotion.md trn-lock entry + the dead-mapper-cell lesson. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QRjrTQpJd6u9XyfnUbTB3v
1921 lines
82 KiB
C++
1921 lines
82 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)
|
|
{
|
|
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:
|
|
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;
|
|
|
|
// In the binary `bodyAnimationState`@0x728 IS `bodyStateAlarm`'s level (one field);
|
|
// the reconstruction split them, so SetBodyAnimation's `bodyStateAlarm.SetLevel(state)`
|
|
// would not update the int the switch reads. Re-sync from the alarm each frame (the
|
|
// switch below dispatches on the pre-transition state exactly as the binary does).
|
|
bodyAnimationState = (int)bodyStateAlarm.GetLevel();
|
|
|
|
if (!deathAnimationLatched) // this+0x650
|
|
{
|
|
switch (MovementMode()) // this+0x40 = simulationState
|
|
{
|
|
case 5: SetBodyAnimation(0x1c); deathAnimationLatched = 1; break;
|
|
case 6: SetBodyAnimation(0x1d); deathAnimationLatched = 1; break;
|
|
case 7: SetBodyAnimation(0x1e); deathAnimationLatched = 1; break;
|
|
case 8: SetBodyAnimation(0x1f); deathAnimationLatched = 1; break;
|
|
}
|
|
}
|
|
|
|
switch (bodyAnimationState) // this+0x728
|
|
{
|
|
case StandingAnimation: // 0
|
|
// STANDING ZEROES THE CYCLE (reverse-stop desync, live-diagnosed
|
|
// 2026-07-13): a REVERSE cadence is NEGATIVE, so the walk-family stop
|
|
// gate (cycleSpeed <= ZeroSpeed) passes while still cycling at full
|
|
// reverse speed, and several stand-entry paths (turn exit, terminal
|
|
// poses) never touch the cycle -- Standing could be entered with a
|
|
// stale bodyCycleSpeed = -2.507 ([gaitSM] state=0 evidence). The master
|
|
// LOOKS still (case 0 never advances the clip) but the stale cycle
|
|
// REPLICATES and the peer's replicant marches in place. A standing
|
|
// mech's cycle is 0 (the clean forward-stop log: legSum ~3e-8).
|
|
if (bodyCycleSpeed != 0.0f)
|
|
{
|
|
bodyCycleSpeed = 0.0f;
|
|
ForceUpdate(8); // type-3 record: legs stopped
|
|
}
|
|
// RAW (FUN_004a5678 case 0): standSpeed < bodyTargetSpeed -> begin WALKING
|
|
// (5); 0 <= target < standSpeed -> stay standing; target < 0 -> reverse
|
|
// (0x10). (The earlier draft had the comparison INVERTED.)
|
|
distance = 0.0f;
|
|
if (standSpeed < bodyTargetSpeed) // 0x530 < 0x6b4
|
|
{
|
|
SetBodyAnimation(5);
|
|
}
|
|
else
|
|
{
|
|
if (ZeroSpeed <= bodyTargetSpeed)
|
|
{
|
|
break;
|
|
}
|
|
SetBodyAnimation(0x10);
|
|
}
|
|
// FALLTHROUGH
|
|
|
|
case 4: // TURN-IN-PLACE, LOCKSTEP twin (task #64)
|
|
// The body channel runs trn in LOCKSTEP with the leg: armed together at
|
|
// entry (leg Standing cross-arms both), advanced at the SAME rate keyed on
|
|
// the SAME live speedDemand, so both clips complete on the same frame and
|
|
// both channels re-enter walk on the same frame. Without this the body
|
|
// entered walk ~7-20 frames apart from the leg and the two walk cycles ran
|
|
// permanently out of phase. Exits mirror the leg twin VERBATIM
|
|
// (part_012.c:12013 [T1]): speed exits + the master-perf turn-stop exit.
|
|
// The body does NOT set legResetLatch (leg-channel/master-perf state, set
|
|
// once by the leg twin). Since the body no longer writes joints (mj=0,
|
|
// AdvanceBodyAnimation) this channel only tracks state so both channels
|
|
// re-enter walk on the same frame.
|
|
{
|
|
MechControlsMapper *bm = MappingMapper();
|
|
// 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)
|
|
{
|
|
bodyStateAlarm.SetLevel(0);
|
|
ForceUpdate(8);
|
|
distance = 0.0f;
|
|
break;
|
|
}
|
|
if (GetInstance() != ReplicantInstance // MASTER only (see leg twin)
|
|
&& (bm == 0
|
|
|| (bm->turnDemand <= 0.05f && bm->turnDemand >= -0.05f)))
|
|
{
|
|
bodyStateAlarm.SetLevel(0); // turn stopped (leg-symmetric)
|
|
ForceUpdate(8);
|
|
distance = 0.0f;
|
|
break;
|
|
}
|
|
distance = bodyAnimation.Advance( // still turning -> advance the pivot
|
|
time_slice * globalTimeScale * idleStrideScale, loop);
|
|
bodyCycleSpeed = distance / time_slice;
|
|
}
|
|
break;
|
|
|
|
case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b:
|
|
case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
|
|
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
|
|
distance = bodyAnimation.Advance( // FUN_0042790c(this+0x6bc, ...)
|
|
time_slice * globalTimeScale * idleStrideScale, loop);
|
|
bodyCycleSpeed = distance / time_slice; // this+0x6b8
|
|
break;
|
|
|
|
case 1:
|
|
distance = 0.0f;
|
|
break;
|
|
|
|
case 6: case 7: // WalkToRun
|
|
if (bodyTargetSpeed <= bodyCycleSpeed)
|
|
{
|
|
if (bodyTargetSpeed < bodyCycleSpeed)
|
|
{
|
|
bodyCycleSpeed -= forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed < bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
|
if (bodyCycleSpeed < standSpeed) bodyCycleSpeed = standSpeed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bodyCycleSpeed += forwardCycleRate * time_slice;
|
|
if (bodyCycleSpeed > bodyTargetSpeed) bodyCycleSpeed = bodyTargetSpeed;
|
|
if (bodyCycleSpeed > walkStrideLength) bodyCycleSpeed = walkStrideLength;
|
|
}
|
|
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.
|
|
//===========================================================================//
|