MP: FIX phantom walk-in-place -- master sends a pose record on came-to-rest (task #50)

The linear pose send-gate watched only POSITION drift + the 2s heartbeat.
When a mech stops, its localOrigin is pinned and the master's own dead-reckon
projection re-bases to it each frame, so error->0 and NO pose record fires --
leaving the peer holding the last WALKING velocity (updateVelocity). The
replicant then dead-reckons + animates a phantom walk-in-place until the 2s
heartbeat (stretched to ~30s when the master window is OS-throttled to ~8fps
while backgrounded during solo two-window testing).

Add the symmetric linear 'came-to-rest' trigger -- the exact analog of the
angular (live yaw-rate==0 && replicated yaw-rate!=0) resync trigger already
present in the type-4 gate: fire a pose record the frame the live horizontal
speed collapses while the last-SENT speed was still non-zero, so the replicant
receives velocity=0 immediately instead of waiting for the heartbeat.

Diagnosis established via BT_WIRE tx0/rx0 probes: records deliver 1:1
(239 tx =~ 240 rx), velocities match -- NOT a replication stall; the master
simply never transmitted the stop. Also confirmed the dominant choppiness is
OS window-throttling (master 8fps backgrounded vs observer 63fps), a
two-windows-on-one-box test artifact absent on real pod hardware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 11:30:18 -05:00
co-authored by Claude Opus 4.8
parent 2aba89f962
commit 94647dd378
2 changed files with 33 additions and 0 deletions
+7
View File
@@ -1810,6 +1810,13 @@ void
updateVelocity.angularMotion = savedAngV; // FUN_00408440
poseSyncLatch = 1; // @0x77c -- arm the dead-reckon re-base
if (getenv("BT_WIRE"))
DEBUG_STREAM << "[rx0] ent=" << GetEntityID()
<< " uvLin=(" << updateVelocity.linearMotion.x << ","
<< updateVelocity.linearMotion.z << ")"
<< " uPos=(" << updateOrigin.linearPosition.x << ","
<< updateOrigin.linearPosition.z << ")\n" << std::flush;
if (bodyAnimationState == 2 || bodyAnimationState == 3) // @0x728
{
projectedVelocity.linearMotion = ZeroVector; // FUN_0040a7f4(0x298, zeroMotion)
+26
View File
@@ -3414,12 +3414,38 @@ void
{
Scalar posDb = (updatePositionDeadband > 0.0f)
? updatePositionDeadband : 0.04f;
// LINEAR "came-to-rest" trigger -- the symmetric analog of the
// angular (live yaw-rate==0 && replicated yaw-rate!=0) resync
// trigger in the type-4 gate below. The position deadband alone
// does NOT catch a stop: once the master halts, its localOrigin is
// pinned and its OWN dead-reckon projection is re-based to it each
// frame, so error->0 and no pose record fires -- leaving the peer
// holding the last WALKING velocity (updateVelocity), which the
// replicant keeps dead-reckoning + animating as a phantom walk
// until the 2s heartbeat. Fire a pose record the frame the live
// horizontal speed collapses while the last-SENT speed was still
// non-zero, so the replicant gets velocity=0 immediately.
const Scalar liveSpdSq =
(Scalar)(localVelocity.linearMotion.x * localVelocity.linearMotion.x
+ localVelocity.linearMotion.z * localVelocity.linearMotion.z);
const Scalar sentSpdSq =
(Scalar)(updateVelocity.linearMotion.x * updateVelocity.linearMotion.x
+ updateVelocity.linearMotion.z * updateVelocity.linearMotion.z);
const Logical cameToRest = (liveSpdSq < 0.01f && sentSpdSq > 0.01f);
if (
error.LengthSquared() > posDb
|| lastPerformance - lastUpdate > 2.0f
|| cameToRest
)
{
ForceUpdate(); // type 0: pose record
if (getenv("BT_WIRE"))
DEBUG_STREAM << "[tx0] errSq=" << error.LengthSquared()
<< " posDb=" << posDb
<< " hb=" << (float)(lastPerformance - lastUpdate)
<< (cameToRest ? " REST" : "")
<< " lvLin=(" << localVelocity.linearMotion.x << ","
<< localVelocity.linearMotion.z << ")\n" << std::flush;
}
}
// ORIENTATION rides ONLY the type-4 resync record (the authentic