MP: FIX replicant 'slides before it steps' -- walk-entry floor on derived demand

Root-caused from a live per-frame [replmov] trace (user's session, where replication
works -- the headless rig won't spawn a replicant): the replicant's leg SM sat in
Standing (legState 0, legFrm pinned at 19) while dead-reckoning forward at spd=6.13,
until the master's speed rose to 6.99, then it snapped to StandToWalk.

Cause: the replicant derives speedDemand from the master's ACTUAL velocity, but a
mech's forward walk velocity == walkStrideLength, which sits BELOW standSpeed (bhk1:
walk 6.13 < stand ~6.8). The leg SM's stand->walk gate is ,
so a steadily-walking peer's derived demand NEVER crosses it -> the peer slides in the
standing pose until the master happens to accelerate past standSpeed. The master never
hits this: it feeds the leg SM its COMMANDED throttle speed (>> standSpeed), not the
actual velocity.

Fix: when the peer is clearly moving forward, floor the derived demand just past
standSpeed so it enters the walk cycle immediately; the walk case clamps legCycleSpeed
back to walkStrideLength, so cadence still matches travel (no foot-slip). Reverse
needs no floor (its gate is commandedSpeed < ZeroSpeed, tripped by any negative).

Diag: BT_REPL_MOV (per-frame replicant pos/vel/speed/legState/legFrm + master twin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EOF
This commit is contained in:
arcattack
2026-07-14 10:50:39 -05:00
parent a3b735d5da
commit 936c34c997
+57 -2
View File
@@ -1952,7 +1952,25 @@ void
localToWorld.GetFromAxis(Z_Axis, &zAxR);
const float fdot = -((float)wv.x * (float)zAxR.x
+ (float)wv.z * (float)zAxR.z); // mech faces -Z
replMppr->speedDemand = (fdot < 0.0f) ? -spd : spd;
Scalar sd = (fdot < 0.0f) ? -spd : spd;
// WALK-ENTRY FLOOR (user-reported "slides forward before it steps",
// root-caused 2026-07-14 from a per-frame [replmov] trace): the leg
// SM enters walk only when `standSpeed < commandedSpeed`, but a mech's
// forward WALK velocity == walkStrideLength which sits BELOW standSpeed
// (bhk1: walk 6.13 < stand ~6.8). The master clears the gate because
// it feeds the leg SM its COMMANDED throttle speed (>> standSpeed); the
// replicant feeds the DERIVED actual velocity, which never crosses the
// gate -- so a steadily-walking peer dead-reckons forward frozen in the
// standing pose (legState 0, legFrm pinned) until the master happens to
// accelerate past standSpeed. When the peer is clearly moving forward,
// floor the demand just past standSpeed so it enters the walk cycle at
// once; the walk case (6/7) clamps legCycleSpeed back to walkStrideLength,
// so the CADENCE still matches the actual travel (no foot-slip). Reverse
// needs no floor -- its entry gate is `commandedSpeed < ZeroSpeed`, which
// any negative demand already trips.
if (sd > walkStrideLength * 0.5f && sd < standSpeed * 1.1f)
sd = standSpeed * 1.1f;
replMppr->speedDemand = sd;
// 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,
@@ -1997,7 +2015,11 @@ void
DEBUG_STREAM << "[repltrn] yawRate=" << yawRate
<< " turnDemand=" << replMppr->turnDemand
<< " legState=" << (int)legStateAlarm.GetLevel()
<< " spd=" << replMppr->speedDemand << "\n" << std::flush;
<< " bodyState=" << (int)bodyStateAlarm.GetLevel()
<< " legFrm=" << legAnimation.currentFrame
<< " bodyFrm=" << bodyAnimation.currentFrame
<< " spd=" << replMppr->speedDemand
<< " tgt=" << bodyTargetSpeed << "\n" << std::flush;
}
}
// PER-FRAME heading trace (BT_REPL_HDG, choppy-spin diagnosis): the
@@ -2019,6 +2041,29 @@ void
<< " yawRate=" << yawRate << " dt=" << dt << "\n" << std::flush;
}
// PER-FRAME travel/animation coupling probe (BT_REPL_MOV, user:
// "slides forward before it steps"): position vs the dead-reckon target
// vs the derived speed vs the leg SM state, every frame there is
// replicated motion. Reveals travel-vs-animation lag + lerp-gap jitter.
if (getenv("BT_REPL_MOV"))
{
const Scalar uvx = (Scalar)updateVelocity.linearMotion.x;
const Scalar uvz = (Scalar)updateVelocity.linearMotion.z;
const Scalar umag = (Scalar)sqrtf(uvx*uvx + uvz*uvz);
if (umag > 0.01f || replMppr->speedDemand != 0.0f)
DEBUG_STREAM << "[replmov]"
<< " pos=(" << localOrigin.linearPosition.x << ","
<< localOrigin.linearPosition.z << ")"
<< " proj=(" << projectedOrigin.linearPosition.x << ","
<< projectedOrigin.linearPosition.z << ")"
<< " uvel=" << umag
<< " spd=" << replMppr->speedDemand
<< " legState=" << (int)legStateAlarm.GetLevel()
<< " legFrm=" << legAnimation.currentFrame
<< " nu-lp=" << (nextUpdate.ticks - lastPerformance.ticks)
<< " dt=" << dt << "\n" << std::flush;
}
// REPLICANT BEAMS (task #51): the emitters carry live replicated
// discharge state (Emitter::ReadUpdateRecord); draw them with the
// same per-weapon walk the master uses.
@@ -3136,6 +3181,16 @@ void
localVelocity.linearMotion = Vector3D(0.0f, worldLinearVelocity.y, -fwdSpeed);
localVelocity.angularMotion = Vector3D(0.0f, turn * authTurnRate, 0.0f);
// MASTER twin of [replmov]: the velocity this mech PUBLISHES vs its own
// leg state -- compare the master ramp to the replicant response.
if (getenv("BT_REPL_MOV") && (fwdSpeed > 0.01f || throttle != 0.0f))
DEBUG_STREAM << "[mastervel] fwdSpeed=" << fwdSpeed
<< " thr=" << throttle
<< " legState=" << (int)legStateAlarm.GetLevel()
<< " pos=(" << localOrigin.linearPosition.x << ","
<< localOrigin.linearPosition.z << ")"
<< " dt=" << dt << "\n" << std::flush;
// CRASH motion suppression (spec: the binary zeroes localVelocity while
// crashed). While the stagger clip (legState 0x20) plays, freeze the
// velocity so a knocked-down mech does not creep back into the obstacle and