diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 6c76b92..b1e3036 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -915,6 +915,10 @@ Mech::Mech( Wword(0xe6) = 0; poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df)) heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline) + angMirrorYaw = 0.0f; // scalar peer-yaw mirror (port addition) + angMirrorRate = 0.0f; + angMirrorValid = 0; + angSyncLatch = 0; // peer heading re-anchor (type-4 receive) Wword(0xfe) = Wword(0xfc) = Wword(0x105) = 0; mechName.Copy(&DAT_004e0f8c); // FUN_00408e90(this+0xd8,"") Wword(0x160) = Wword(0x15f) = 0; @@ -1893,6 +1897,7 @@ void } } bodyTargetSpeed = record->speedDemand; // rec+0x28 + angSyncLatch = 1; // arm the peer heading re-anchor } break; @@ -2114,6 +2119,15 @@ void // master perf; the writer-side re-base is the coherent reconstruction.) projectedOrigin.angularPosition = localOrigin.angularPosition; projectedVelocity.angularMotion = localVelocity.angularMotion; + // SCALAR peer-yaw mirror re-base (see mech.hpp): what the peer will now + // extrapolate -- this yaw, at this rate, from this moment. + { + YawPitchRoll _my; _my = localOrigin.angularPosition; + angMirrorYaw = (Scalar)_my.yaw; + angMirrorRate = (Scalar)localVelocity.angularMotion.y; + angMirrorTime = lastPerformance; + angMirrorValid = 1; + } record->speedDemand = speedDemand; // rec+0x28 bodyTargetSpeed = speedDemand; } diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 31d4415..17281dc 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -618,6 +618,20 @@ public: Scalar updatePositionDeadband; // binary @0x768 -- type-0 pose trigger Scalar updateTurnVelocityDeadband; // binary @0x76c -- type-4 yaw-rate trigger Scalar updateTurnAngleDeadband; // binary @0x770 -- type-4 orientation trigger (rad) + // SCALAR peer-yaw mirror (port addition, replicant-spin work): the master's + // estimate of the yaw the PEER is currently rendering -- re-based on every + // type-4 send (writer case 4), advanced analytically as base + rate*elapsed. + // Replaces the quaternion projectedOrigin mirror for the ANGLE deadband: that + // one is recomputed by the master's own reckoner each frame from lastUpdate + // timing it does not control, and its false pi-drift waves FLOODED type-4 + // resyncs (measured maxAng~=pi bursts). Scalars, wrap-safe, self-timed. + Scalar angMirrorYaw; // yaw at the last type-4 send (rad) + Scalar angMirrorRate; // yaw rate sent with it (rad/s) + Time angMirrorTime; // lastPerformance at that send + int angMirrorValid; // 0 until the first type-4 send + int angSyncLatch; // peer: type-4 arrived -> re-anchor heading (the + // angular analog of poseSyncLatch; consumed by + // the replicant's incremental heading integrator) // AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode; // binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared // after the layout-locked fields so nothing shifts. diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index d0c87ca..ff1a406 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1930,7 +1930,36 @@ void { if (dt > 0.0001f && dt < 0.5f) { - DeadReckon(dt); // engine: reckoner (exact angular) + lerp + // AUTHENTIC PEER HEADING (decomp FUN_004ab188/FUN_00409f58 via FUN_004ab1c8): + // the original replicant integrates its heading INCREMENTALLY from the + // CURRENT pose -- compose an exact rotation of (replicated yaw rate * dt) + // onto the rendered heading each frame -- and re-anchors it when a type-4 + // resync lands. It does NOT slerp toward a projected angular target: the + // engine Mover::DeadReckon lerp uses the shared lastUpdate/nextUpdate + // timebase, which the DENSE type-0 pose stream resets every frame while + // walking while RESTORING a stale orientation (the authentic case-0 strip) + // -- so the angular projection barely advances from a stale base and the + // slerp DRAGS the heading back each frame. Measured: walking+turning, the + // peer yaw advanced at ~40% rate with half the frames stepping BACKWARD + // (the keyboard-steering skip); pure spin/walk never showed it (one stream). + // Save the heading, let DeadReckon own the LINEAR channel, then override + // the angular result with the original's incremental integration. + Quaternion replPrevHeading = localOrigin.angularPosition; + DeadReckon(dt); // engine reckoner: LINEAR position/velocity + if (angSyncLatch) + { + // type-4 arrived: re-anchor on the authoritative orientation + localOrigin.angularPosition = updateOrigin.angularPosition; + angSyncLatch = 0; + } + else + { + Vector3D replAngStep; + replAngStep.Multiply(updateVelocity.angularMotion, dt); + ReconQuatIntegrate(&localOrigin.angularPosition, + &replPrevHeading, &replAngStep); // exact FUN_00409f58 + } + localOrigin.angularPosition.Normalize(); localToWorld = localOrigin; // RENDER-vs-SIM decoupling probe: publish the heading the RENDER will @@ -3615,44 +3644,34 @@ void // UpdateTurnVelocityDiffrence. [T1 expressions; zero-deadband // guard falls back to the old quat-w stand-in] { - // PEER-ESTIMATE MIRROR (2026-07-14, replicant-chop fix): advance the - // projection by the last-SENT angular velocity each frame -- this is - // what the replicant's dead-reckoner is doing with the last record. - // The gate below then measures the peer's TRUE drift, so a steady - // turn (matching velocity) sends almost nothing and the replicant - // extrapolates smoothly, instead of the stale mirror firing a - // re-base record EVERY frame (the stall/snap chop). The type-4 - // writer re-bases this mirror on each send (mech.cpp case 4). - { - Vector3D angStepM; - angStepM.Multiply(updateVelocity.angularMotion, dt); - projectedOrigin.angularPosition.Add( - projectedOrigin.angularPosition, angStepM); - // Adding a scaled angular-velocity VECTOR to a quaternion denormalizes - // it (the reckoner MOVER.cpp:463 does the same); a non-unit quaternion's - // extracted yaw is garbage (~pi), which spiked angDrift and FLOODED angle - // resyncs in bursts -> peer horizon reset -> uneven (max/avg ~2.8x) render. - projectedOrigin.angularPosition.Normalize(); - } + // SCALAR PEER-YAW MIRROR (replaces the quaternion projectedOrigin mirror): + // the peer renders yaw ~= angMirrorYaw + angMirrorRate*(now - angMirrorTime) + // (re-based by the type-4 writer on every send, mech.cpp case 4). The old + // quaternion mirror was ALSO recomputed each frame by the master's own + // reckoner from lastUpdate timing it does not control, so it drifted in + // slow pi-waves and FLOODED angle resyncs in bursts (measured maxAng~=pi) + // -- the walk+turn record churn. Scalars: wrap-safe, self-timed, exact + // for the constant-rate spin the peer's exact integrator reproduces. Scalar angDb = (updateTurnAngleDeadband > 0.0f) ? updateTurnAngleDeadband : -1.0f; Scalar velDb = (updateTurnVelocityDeadband > 0.0f) ? updateTurnVelocityDeadband : -1.0f; Logical resync = False; - int rzn = 0; // 1=angleDrift 2=velDrift 4=cameToRestAng - // TRUE yaw drift in RADIANS (wrap-safe), to match the radian deadband - // angDb. The prior `Abs(localOrigin.angularPosition.y - - // projectedOrigin.angularPosition.y)` compared raw QUATERNION y-components - // (sin(yaw/2)-ish, unitless) against a radian deadband -- both a units - // mismatch and wrap-unsafe: at the +-pi heading wrap the quaternion-y diff - // spikes toward its max (~2) and fired byAngle=56/56 EVERY frame (measured), - // flooding resyncs at every wrap. Take the yaw difference in euler space - // and unwrap it to [-pi,pi] so a wrap is not seen as a ~2pi drift. - YawPitchRoll _yprL, _yprP; - _yprL = localOrigin.angularPosition; - _yprP = projectedOrigin.angularPosition; - Scalar _dyaw = (Scalar)(_yprL.yaw - _yprP.yaw); - while (_dyaw > 3.14159265f) _dyaw -= 6.28318531f; - while (_dyaw < -3.14159265f) _dyaw += 6.28318531f; - const Scalar angDrift = Abs(_dyaw); + int rzn = 0; // 1=angleDrift 2=velDrift 4=cameToRestAng 8=mirrorInvalid + Scalar angDrift = 0.0f; + if (angMirrorValid) + { + YawPitchRoll _yprL; + _yprL = localOrigin.angularPosition; + const Scalar mirrorYaw = angMirrorYaw + + angMirrorRate * (Scalar)(lastPerformance - angMirrorTime); + Scalar _dyaw = (Scalar)_yprL.yaw - mirrorYaw; + while (_dyaw > 3.14159265f) _dyaw -= 6.28318531f; + while (_dyaw < -3.14159265f) _dyaw += 6.28318531f; + angDrift = (_dyaw < 0.0f) ? -_dyaw : _dyaw; + } + else + { + resync = True; rzn |= 8; // no baseline yet -> establish one + } // Compare the live yaw rate against the LAST-SENT rate (updateVelocity, // which the type-4 writer copies straight from localVelocity, mech.cpp:2107) // -- NOT projectedVelocity, which the master's dead-reckoner recomputes in a @@ -3698,17 +3717,6 @@ void { resync = True; // stand-in when unstreamed } - // ANGULAR dense-send: the original refreshed orientation in its FREQUENT - // pose record so the peer's dead-reckon gap stayed tiny (decomp: 7-float - // pose carries the quaternion, refreshed every broadcast). Our orientation - // rides only the type-4 resync, and during a PURE spin the linear dense-send - // never fires (not translating), so the gap balloons (~1.6s) and even the - // exact integrator's projection is far ahead -> the slerp jumps. Send the - // resync every frame while rotating to keep updateOrigin.angular fresh - // (small gap -> smooth), mirroring the original's frequent-orientation model. - static const int s_denseRot = getenv("BT_NO_DENSE_TX") ? 0 : 1; - if (s_denseRot && Abs(localVelocity.angularMotion.y) > 0.1f) - resync = True; if (resync) { ForceUpdate(1 << MechResyncUpdateModelBit); // type 4