From 6374efc26a4d6f55ca758f66bfd5b34544166034 Mon Sep 17 00:00:00 2001 From: arcattack Date: Tue, 14 Jul 2026 14:08:22 -0500 Subject: [PATCH] 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) --- game/reconstructed/mech4.cpp | 88 ++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 560bb82..c6082d6 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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,