From a8eb8a427fdc7eb8fd7a07f39569e30fe3208176 Mon Sep 17 00:00:00 2001 From: arcattack Date: Tue, 14 Jul 2026 17:05:16 -0500 Subject: [PATCH] MP: FIX peer spin hang/divergence -- exact quaternion integration + frequent orientation (task #50) Answers 'how did the original handle this?' from the decomp (subagent hunt): the 1995 binary's replicant reckoner (FUN_004ab1c8 -> FUN_00409f58, part_000.c:9359) integrates heading EXACTLY: build a unit axis-angle rotation quaternion from angularVelocity*dt ({axis*sin(t/2), cos(t/2)}) and Hamilton-multiply it onto the heading (FUN_00409d9c) -- exact for any timestep, stays on the unit sphere. It further carries the full orientation quaternion in the FREQUENT pose record (FUN_0040a938, 7-float pose), so the dead-reckon gap stays tiny. Our reconstruction diverged two ways, both fixed: 1. ReconQuatIntegrate (mechrecon.hpp) -- the reconstruction of FUN_00409f58 -- was STUBBED as , a crude small-angle VECTOR add. Restored to the real exact axis-angle composition. (A 'no stand-ins' violation: the comment even wrongly claimed Quaternion::Add == FUN_00409f58.) 2. The engine Mover reckoner (MOVER.cpp AcceleratedDeadReckoner/LinearDeadReckoner) also did the vector Add on projectedOrigin.angularPosition -> over a long peer record gap it diverged to ~180deg then snapped (the reported spin HANG/hesitation). Routed both through a new ExactAngularProject() helper (same exact math). 3. Orientation only rode the sparse type-4 resync; during a PURE spin the linear dense-send never fires (not translating), so the gap ballooned (~1.6s) and the now- exact projection sat far ahead -> the slerp jumped. Added an ANGULAR dense-send (resync every frame while |yawRate|>0.1), mirroring the original's frequent- orientation model -> gap stays tiny -> smooth. Verified live-autonomous (BT_AUTODRIVE+BT_FORCE_TURN + BT_RENDHDG render-rate probe): the ~180deg divergence + multi-radian snaps are GONE (rendered maxStep 0.05-0.10 rad, no jumps). User confirms: no frame hang/hesitation. MOVER.cpp change is strictly more correct (exact==crude for the small per-frame master case; only large-gap peer extrapolation changes), so walking is unaffected. KNOWN REMAINING (separate, smaller): the turn-STEP leg animation (trn clip, mech2.cpp advance_normally) runs at a FIXED idleStrideScale cadence that does not scale with the rotation rate, so the legs lag/skip vs the (now-correct) body rotation. Next. Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/MUNGA/MOVER.cpp | 40 +++++++++++++++++++++++++------- game/reconstructed/mech4.cpp | 13 ++++++++++- game/reconstructed/mechrecon.hpp | 28 +++++++++++++++++++--- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/engine/MUNGA/MOVER.cpp b/engine/MUNGA/MOVER.cpp index cb1a062..b1b3b33 100644 --- a/engine/MUNGA/MOVER.cpp +++ b/engine/MUNGA/MOVER.cpp @@ -11,6 +11,34 @@ #include "line.h" #include "app.h" #include "notation.h" +#include + +// +// EXACT axis-angle rotation composition -- matches the 1995 BT binary's angular +// integrator (FUN_00409f58): build a unit rotation quaternion from the rotation +// VECTOR `rotVec` (angle = |rotVec|, axis = rotVec/angle) as { axis*sin(angle/2), +// cos(angle/2) } and Hamilton-multiply it onto `base`. The dead-reckoner previously +// did `out.Add(base, rotVec)` -- adding a scaled angular-velocity vector to the heading +// quaternion. That is only a small-angle approximation: fine per-frame (tiny angle), +// but over a long replicant dead-reckon gap it DIVERGES (the heading drifts to ~180deg +// then snaps -- the spinning-peer hesitation). This composition is exact for any angle +// and stays on the unit sphere. +// +static void ExactAngularProject(Quaternion &out, const Quaternion &base, const Vector3D &rotVec) +{ + const Scalar ang = rotVec.Length(); + if (ang > 1.0e-6f) + { + const Scalar h = 0.5f * (Scalar)fmodf((float)ang, 6.2831853f); // half of angle mod 2pi + const Scalar s = (Scalar)(sinf((float)h) / ang); // sin(angle/2)/angle + const Quaternion dq(rotVec.x * s, rotVec.y * s, rotVec.z * s, (Scalar)cosf((float)h)); + out.Multiply(base, dq); // base (X) dq + } + else + { + out = base; + } +} //############################################################################# //############################### Mover ################################# @@ -395,10 +423,8 @@ Logical //------------------------------- // position_delta.Multiply(updateVelocity.angularMotion, time_slice); - projectedOrigin.angularPosition.Add( - updateOrigin.angularPosition, - position_delta - ); + ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add) + updateOrigin.angularPosition, position_delta); projectedVelocity = updateVelocity; Check_Fpu(); @@ -460,10 +486,8 @@ Logical updateVelocity.angularMotion, time_slice ); - projectedOrigin.angularPosition.Add( - updateOrigin.angularPosition, - position_delta - ); + ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add) + updateOrigin.angularPosition, position_delta); // //----------------------------------- diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 0d8f15f..a56b001 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1930,7 +1930,7 @@ void { if (dt > 0.0001f && dt < 0.5f) { - DeadReckon(dt); // engine: reckoner + lerp + DeadReckon(dt); // engine: reckoner (exact angular) + lerp localToWorld = localOrigin; // RENDER-vs-SIM decoupling probe: publish the heading the RENDER will @@ -3672,6 +3672,17 @@ 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 diff --git a/game/reconstructed/mechrecon.hpp b/game/reconstructed/mechrecon.hpp index 26f4d06..def87fb 100644 --- a/game/reconstructed/mechrecon.hpp +++ b/game/reconstructed/mechrecon.hpp @@ -435,11 +435,33 @@ namespace Vector // // P3 GAIT CUTOVER: backed by the real engine Quaternion (were no-op stubs). // ReconQuatIdentity(q, &id) -> q = Quaternion::Identity -// ReconQuatIntegrate(out,in,d) -> out = in integrated by angular delta d -// (engine Quaternion::Add(source, Vector3D) == FUN_00409f58) +// ReconQuatIntegrate(out,in,d) -> out = in composed with an EXACT rotation by +// angular delta d (= the real FUN_00409f58). // All callers pass (Quaternion*,Quaternion*) / (Quaternion*,Quaternion*,Vector3D*). inline void ReconQuatIdentity(Quaternion *q, const Quaternion * /*id*/) { *q = Quaternion::Identity; } -inline void ReconQuatIntegrate(Quaternion *out, const Quaternion *in, const Vector3D *delta) { out->Add(*in, *delta); } +// EXACT axis-angle quaternion integration -- the real FUN_00409f58 (part_000.c:9359), +// verified from the decomp. A prior stand-in wrongly used `out->Add(*in,*delta)` (a +// small-angle VECTOR add): correct every-frame on the master (tiny angle) but it +// DIVERGES over a long dead-reckon gap -- the spinning-peer heading drifting to ~180deg +// then snapping. The binary builds the unit rotation quaternion from the rotation vector +// `delta` (angle=|delta|, axis=delta/angle) as { axis*sin(angle/2), cos(angle/2) } and +// Hamilton-multiplies it onto `in` (FUN_00409d9c: out = in (X) deltaQuat). +inline void ReconQuatIntegrate(Quaternion *out, const Quaternion *in, const Vector3D *delta) +{ + const Scalar ang = delta->Length(); + if (ang > 1.0e-6f) + { + const Scalar half = (Scalar)(fmodf((float)ang, 6.2831853f) * 0.5f); // reduce2pi, then halve + const Scalar s = (Scalar)(sinf((float)half) / ang); // sin(angle/2)/angle + const Quaternion dq((Scalar)(delta->x * s), (Scalar)(delta->y * s), + (Scalar)(delta->z * s), (Scalar)cosf((float)half)); + out->Multiply(*in, dq); // in (X) dq + } + else + { + *out = *in; + } +} template inline Recon ReconQuatSlerp(A&&...) { return Recon(); } template inline Recon ComputeImpactDamage(A&&...) { return Recon(); }