the visible limp (#78): there was never a jump-jet clip set -- the 'Airborne' drivers ARE the gimp gait machines

The port had FUN_004a5bf8/71f4 fully reconstructed as AdvanceBody/Leg-
AnimationAirborne, gated on (MovementMode()==3||4) && jumpCapable@0x580 and
believed dead ("the test mech never jumps").  The binary says otherwise:
mech+0x40 in that gate is the graphicAlarm LEVEL (3=left-leg gimp, 4=right)
and +0x580 is hasGimpClips, set by the conditional loader block that probes
'wgl' and fills clip slots 22-27 (wgl/wgr/ggr/ggl/gsl/gsr) plus the four
measurements at 0x53c-0x548 (wg entry strides = speed caps, gg cycle strides).
Renamed the five jump* members + both drivers accordingly.

New: GimpBodyClipFinished @004a6344 / GimpLegClipFinished @004a7970 -- the
gimp transition machines, branched from the normal finished-callbacks.  Phase-
correct limp entry (left-gimp enters 0x16/wgl only from a RIGHT step, right-
gimp 0x17/wgr from a LEFT step), gg cycles at gimp cadence, gs exits, and the
demand clamp to the gimped side's speed cap (leg cb writes it back into the
mapper -- the binary's authentic slowdown; the T3 x0.5 stand-in in mechmppr is
retired, BT_GIMP_SPEED now defaults 1.0).  The binary's gimp machines have no
reverse entry -- the "reverse disabled" behavior is now binary-proven.

Reviving the dead drivers replayed two port-glue bugs (gotcha #24): the raw
*(controlSource) mapper read (null -> crash at first engagement) and the
missing alarm->member state re-sync (machine pinned in one run state).  Both
fixed; bench harness gained BT_SELF_DAMAGE_TICKS=<n> to hold a zone past
LegHalfStructure without destroying it.

Bench-verified (madcat, novice, zone 16): crossing -> alarm 4 -> wgr entry
from a left step -> 8k+ frames stable in the ggl limp cycle at cadence 14.77
(vs 18.5 walk / 22+ run) with raw demand still 50.  [T2]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-29 15:49:39 -05:00
co-authored by Claude Fable 5
parent 520f6eecd3
commit 36f68718c2
9 changed files with 506 additions and 127 deletions
+53 -15
View File
@@ -67,7 +67,7 @@
// Helper / engine routine name mapping used below:
// FUN_0049fb54 Mech::IsDisabled() -> Logical
// FUN_004a5678 Mech::AdvanceBodyAnimation(dt, loop) [mech2.cpp]
// FUN_004a5bf8 Mech::AdvanceBodyAnimationAirborne(dt, loop) [mech2.cpp]
// FUN_004a5bf8 Mech::AdvanceBodyAnimationGimp(dt, loop) [mech2.cpp]
// FUN_004ab188 Mech::DeadReckonPose(dt)
// FUN_004ab1c8 Mech::IntegrateMotion(dt, loop) -> Logical (disabled?)
// FUN_004086ac Vector::Scale(out, in, scalar)
@@ -239,7 +239,7 @@ struct MechBaseLayoutCheck
// @0x4b8 templateBottomLift (mech.hpp) ctor: 0.05 x volume X width
// @0x518 standingTemplateMaxY (mech.hpp) CORRECTED (was "heatLevel")
// @0x51c duckedTemplateMaxY (mech.hpp) CORRECTED (was "heatCapacity"); 0.6 x standing
// @0x580 jumpCapable (mech3)
// @0x580 hasGimpClips (mech3)
// @0x598 motionEventName (mech2)
// @0x5a4 motionEventArmed (mech2)
// @0x5b8 groundCycleRate (mech3)
@@ -285,7 +285,7 @@ void
//
// Advance the *displayed-motion* (channel-B) body gait one frame and integrate
// the resulting cycle distance into the world transform. Picks the airborne
// body updater when (movementMode==3||4) && jumpCapable, else the ground one.
// body updater when (movementMode==3||4) && hasGimpClips, else the ground one.
// Also fires the queued footstep / motion event when the mech "arrives".
// Returns the IsDisabled() result captured at entry (the caller branches on it).
//###########################################################################
@@ -337,12 +337,19 @@ Logical
}
}
// Advance the body gait (airborne flavour while jumping).
// Advance the body gait (GIMP flavour while limping, #78). The binary's
// mech+0x40 here is the graphicAlarm level (3=left gimp, 4=right) -- read
// via the mechdmg bridge, never MovementMode()/graphicAlarm directly
// (simulationState split-brain + AlarmIndicator ODR, gotcha #23).
Scalar cycleDistance;
if ((MovementMode() == 3 || MovementMode() == 4) && jumpCapable) // 0x40, 0x580
cycleDistance = AdvanceBodyAnimationAirborne(time_slice, loop); // FUN_004a5bf8
else
cycleDistance = AdvanceBodyAnimation(time_slice, loop); // FUN_004a5678
{
extern int BTMechGimpLevel(void *mech_v);
const int gl = BTMechGimpLevel(this);
if ((gl == 3 || gl == 4) && hasGimpClips) // 0x40, 0x580
cycleDistance = AdvanceBodyAnimationGimp(time_slice, loop); // FUN_004a5bf8
else
cycleDistance = AdvanceBodyAnimation(time_slice, loop); // FUN_004a5678
}
// The raw velocity vector (0x298) = {0, 0, -cycleDistance/dt}; the world-step
// FUN_00408744 reads all three components, so 0x2a0 (== angularAccum[2]) MUST be
@@ -2427,9 +2434,16 @@ void
else if (!turning && bs == 4) // turn stopped -> back to stand
SetBodyAnimation(0);
}
// GIMP routing (#78): a limping PEER runs the gimp machines too
// (the graphicAlarm level replicates with the damage state).
extern int BTMechGimpLevel(void *mech_v);
const int replGl = BTMechGimpLevel(this);
const bool replGimp = (replGl == 3 || replGl == 4) && hasGimpClips;
const Scalar replLegAdv = s_peerLegCh
? AdvanceLegAnimation(dt) // old: re-derived leg SM
: AdvanceBodyAnimation(dt, 1); // authentic: body channel, mj=1 poses skeleton
? (replGimp ? AdvanceLegAnimationGimp(dt) // FUN_004a71f4
: AdvanceLegAnimation(dt)) // old: re-derived leg SM
: (replGimp ? AdvanceBodyAnimationGimp(dt, 1) // FUN_004a5bf8
: AdvanceBodyAnimation(dt, 1)); // authentic: body channel, mj=1 poses skeleton
// AUTHENTIC coupled position (mirror of the master's world-step at
// mech4.cpp:3325-3336 == Mech::IntegrateMotion tail @004ab1c8): between
// records the body advances by the CLIP'S OWN travel rotated by the
@@ -3897,13 +3911,25 @@ void
// INVISIBLE in the binary" (locomotion.md) -- now it is here too.
// BT_BODY_MJ=1 restores the old double-writer for A/B.
static const int s_bodyMj = BTEnvOn("BT_BODY_MJ", 0);
adv = AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0)
// GIMP routing (#78): route BOTH channels through the gimp
// drivers while limping -- (gimpLevel 3|4) && hasGimpClips,
// level via the mechdmg bridge (gotcha #23).
extern int BTMechGimpLevel(void *mech_v);
const int gl2 = BTMechGimpLevel(this);
const bool gimpy = (gl2 == 3 || gl2 == 4) && hasGimpClips;
adv = gimpy ? AdvanceBodyAnimationGimp(dt, s_bodyMj) // FUN_004a5bf8
: AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0)
mirrorBodyAdv = adv; // stash for the send-mirror gait advance
legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel
legAdv = gimpy ? AdvanceLegAnimationGimp(dt) // FUN_004a71f4
: AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel
}
else
{
adv = AdvanceBodyAnimation(dt, 1); // single-channel: body does both
extern int BTMechGimpLevel(void *mech_v);
const int gl2 = BTMechGimpLevel(this);
const bool gimpy = (gl2 == 3 || gl2 == 4) && hasGimpClips;
adv = gimpy ? AdvanceBodyAnimationGimp(dt, 1)
: AdvanceBodyAnimation(dt, 1); // single-channel: body does both
}
// [syncF] per-frame divergence probe (task #49): log every frame
// where the two channels return different distances or either
@@ -5608,10 +5634,21 @@ void
&& !IsMechDestroyed() && damageZoneCount > 0)
{
static float s_sdAccum = 0.0f;
static int s_sdTicks = 0;
// BT_SELF_DAMAGE_TICKS=<n>: stop after n hits (bench: ramp a leg
// PAST LegHalfStructure then hold, so the limp window is unbounded
// instead of the 1 s between half and destroyed at high dps).
static int s_sdMaxTicks = -1;
if (s_sdMaxTicks < 0)
{
const char *te = getenv("BT_SELF_DAMAGE_TICKS");
s_sdMaxTicks = (te != 0 && *te != '\0') ? atoi(te) : 0; // 0 = unlimited
}
s_sdAccum += dt;
if (s_sdAccum >= 1.0f)
if (s_sdAccum >= 1.0f && (s_sdMaxTicks <= 0 || s_sdTicks < s_sdMaxTicks))
{
s_sdAccum = 0.0f;
++s_sdTicks;
Damage dmg;
dmg.damageType = Damage::ExplosiveDamageType;
float want = (float)atof(getenv("BT_SELF_DAMAGE"));
@@ -5634,7 +5671,8 @@ void
sizeof(Entity::TakeDamageMessage),
GetEntityID(), sdZone /*-1 -> cylinder lottery*/, dmg);
DEBUG_STREAM << "[selfdmg] " << dmg.damageAmount
<< " to own mech (unaimed)" << std::endl << std::flush;
<< " to own mech (zone " << sdZone << ", -1=lottery)"
<< std::endl << std::flush;
Dispatch(&td);
}
}