MP: FIX peer gait jitter -- feed the leg channel the REPLICATED commanded speed (task #50)

Decomp-verified via multi-agent investigation (workflow wv1km7lvc, adversarially
checked against reference/decomp/all/part_012.c + engine T0).

ROOT CAUSE (D3 CONFIRMED): the peer's leg channel FUN_004a5028 reads its speed
demand from mapper->speedDemand (**(mech+0x128)+0x128, part_012.c:11947/11975/
12028). The reconstruction FABRICATED that input from the noisy dead-reckoned
velocity magnitude and PINNED it flat with a standSpeed*1.05 floor across the
walk threshold -- the leg stutter/skip + cadence-vs-travel desync, worst on
accel/decel. The authentic input is the replicated commanded speed
bodyTargetSpeed@0x6b4 -- the master's own throttle-commanded speedDemand
(mechmppr.cpp:749), stamped into every record (mech.cpp:2036) and read back on
the peer (mech.cpp:1833/1841/1871). It is the IDENTICAL value the master's leg
channel consumes, so the peer clears the stand->walk gate exactly when the master
does and ramps cadence continuously through accel/decel.

Confirms the 07-13/07-14 regression: bb795e2 (smooth, 07-12) fed raw derived
speed with NO floor; the floor added in the task-#64/#50 spiral is what pinned
the cadence.

CHANGES (mech4.cpp replicant branch):
 - Change 1 (D3): replMppr->speedDemand = bodyTargetSpeed (was derived velocity
   + standSpeed*1.05 floor). Old path kept behind BT_REPL_VEL for A/B.
 - Turning: authentic peer turning is net-driven (angular slerp in DeadReckon,
   MOVER.cpp:521-525, D8). Removed the flickery bare +-0.02 yawRate->turnDemand.
   The visible turn-STEP (authentically from the body channel replaying the
   type-3 turn state; we run the leg channel per D2) is reproduced by arming the
   trn clip from the replicated yaw rate with HYSTERESIS (enter >0.08, hold until
   <0.03) so the clip no longer chatters. BT_REPL_NOTURN = decomp-strict, no step.

REFUTED (kept as-is): D1/D5 -- peer POSITION authentically comes from the engine
velocity dead-reckoner (Mover::DeadReckon, T0), NOT the gait travel; the
body-channel switch DEVIATES (double-authors position + T4 leg-bone risk) and was
NOT taken. D4 -- bodyTargetSpeed is non-zero tracking throttle (0 only on coast,
which authentically winds down to stand).

Verified live: walking + accel/decel + in-place pivot all smooth (user-confirmed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 14:08:22 -05:00
co-authored by Claude Opus 4.8
parent d5d512e087
commit 6374efc26a
+59 -29
View File
@@ -1984,21 +1984,28 @@ void
MechControlsMapper *replMppr = MappingMapper(); // roster slot 0 (task #7)
if (!IsMechDestroyed() && replMppr != 0)
{
// Feed the leg SM the REPLICATED COMMANDED speed (bodyTargetSpeed):
// the master's OWN mppr->speedDemand, carried in every pose (type-0)
// and speed (type-2) record (mech.cpp:1833/1841). This is the
// IDENTICAL input the master's own leg SM consumes, so the replicant
// selects the same gait state and RAMPS its cadence through accel /
// decel exactly as the master does -- the legs stay in sync with the
// motion. The earlier feed derived the ACTUAL velocity from
// updateVelocity, but a mech's forward walk velocity (~walkStrideLength)
// sits BELOW standSpeed, so it never cleared the stand->walk gate; the
// floor hack that fixed THAT then pinned the cadence flat during
// accel/decel -- the reported stutter/skip glitch. bodyTargetSpeed is
// signed (negative == reverse), so reverse needs no special case.
// (BT_REPL_VEL forces the old derived-velocity feed for A/B compare.)
static const int s_useCmd = getenv("BT_REPL_CMD") ? 1 : 0;
if (!s_useCmd) // default: derive from replicated velocity (bodyTargetSpeed is 0 -- master walks thr=0)
// AUTHENTIC LEG-CHANNEL DEMAND (decomp-verified, workflow wv1km7lvc D3):
// the leg channel FUN_004a5028 reads its speed demand from
// mapper->speedDemand == **(mech+0x128)+0x128 (part_012.c:11947/11975/
// 12028) and slews cadence + selects the stand->walk gate from it. On a
// peer this MUST be the REPLICATED COMMANDED speed bodyTargetSpeed@0x6b4 --
// the master's own throttle-commanded speedDemand (mechmppr.cpp:749),
// stamped into every record (mech.cpp:2036, record->speedDemand) and read
// back on the peer (mech.cpp type-0 @1833, type-2 @1841/1848, type-3 @1871).
// It is the IDENTICAL value the master's leg channel consumes, so the peer
// clears the stand->walk gate exactly when the master does (the commanded
// speed is what makes the master walk, so it clears standSpeed by
// construction) and ramps cadence CONTINUOUSLY through accel/decel --
// cadence tracks travel like the master. bodyTargetSpeed is signed
// (negative == reverse); it is 0 only while COASTING (thr=0), which
// authentically winds the gait down to stand, matching the master.
//
// The prior default DERIVED a noisy speed from the dead-reckoned velocity
// and PINNED it flat with a standSpeed*1.05 floor across the walk threshold
// -- the CONFIRMED leg-stutter/skip + cadence-vs-travel desync. Retained
// behind BT_REPL_VEL for A/B comparison only.
static const int s_replVel = getenv("BT_REPL_VEL") ? 1 : 0;
if (s_replVel)
{
const Vector3D &wv = updateVelocity.linearMotion;
float spd = sqrtf((float)(wv.x * wv.x + wv.z * wv.z));
@@ -2013,22 +2020,45 @@ void
}
else
{
replMppr->speedDemand = bodyTargetSpeed;
replMppr->speedDemand = bodyTargetSpeed; // authentic replicated commanded speed
}
// REPLICANT TURN-STEP (user-reported: a turning peer
// statue-rotates while the local mech steps): the leg SM's
// Standing case arms the turn-in-place "trn" clip (state 4,
// mech2.cpp:590) from mppr->turnDemand -- which nothing fed on
// a replicant, so pivots never stepped. Feed it from the
// REPLICATED yaw rate (updateVelocity.angularMotion.y, stored
// by Mover::ReadUpdateRecord from the master's localVelocity
// -- the same stream DeadReckon rotates the body with). Only
// the +-0.05 threshold matters to the SM (the trn clip
// advances at fixed cadence), so a signed unit demand
// suffices; the deadband ignores dead-reckon jitter.
// AUTHENTIC PEER TURNING is NET-DRIVEN (decomp-verified D8): the angular
// slerp inside DeadReckon (MOVER.cpp:521-525) rotates
// localOrigin.angularPosition toward projectedOrigin.angularPosition from
// the replicated yaw rate (decomp angular path part_012.c:15001/15010).
// The leg SM's turn-in-place 'trn' clip is a MASTER-ONLY construct fed by
// the LIVE controls mapper; the old synthetic yawRate->turnDemand
// (quantized +-0.02) flickered the trn clip in/out as the noisy replicated
// rate crossed the band -> the jerky pivot / statue-rotate. Do NOT
// synthesize it -- let the whole body rotate via the slerp.
// TURN-STEP (presentation of the authentic pivot): the authentic peer
// shows a stepping pivot because its BODY channel replays the master's
// replicated turn body-STATE (type-3 SetBodyAnimation, mech.cpp:1869).
// We run the LEG channel (D2 not switched), which has no replicated turn
// state, so we reproduce the visible turn-step by arming the leg SM's
// 'trn' clip from the replicated yaw rate. The old feed used a bare
// +-0.02 threshold that CHATTERED the trn clip in/out as the noisy rate
// wandered near zero (the "jerky pivot", decomp-noted D8). HYSTERESIS
// fixes that: enter the turn only above +-0.08 rad/s, hold it (the
// mapper's turnDemand persists frame-to-frame -- we are its only writer on
// a peer) until the rate clearly collapses below 0.03. A steady turn sits
// well above both, so the clip never flickers. (BT_REPL_NOTURN =
// decomp-strict net-driven rotation with NO step.)
const float yawRate = (float)updateVelocity.angularMotion.y;
replMppr->turnDemand = (yawRate > 0.02f) ? 1.0f
: (yawRate < -0.02f) ? -1.0f : 0.0f;
static const int s_noTurn = getenv("BT_REPL_NOTURN") ? 1 : 0;
if (s_noTurn)
{
replMppr->turnDemand = 0.0f;
}
else
{
const float prevTd = (float)replMppr->turnDemand; // last frame (our own write)
if (prevTd != 0.0f) // turning: hold until the rate collapses
replMppr->turnDemand = (fabsf(yawRate) < 0.03f) ? 0.0f : prevTd;
else // idle: start only on a clear turn
replMppr->turnDemand = (yawRate > 0.08f) ? 1.0f
: (yawRate < -0.08f) ? -1.0f : 0.0f;
}
// Prime the same clip-advance scalars the master's gait block sets
// each frame -- uninitialized on a replicant they read 0, freezing
// the clip at advance-time dt*0 (observed: legState engaged at 11,