diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index d7456c2..37f5004 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -915,6 +915,7 @@ Mech::Mech( Wword(0xe6) = 0; poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df)) heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline) + mirrorBodyAdv = 0.0f; // master send-mirror gait travel angMirrorYaw = 0.0f; // scalar peer-yaw mirror (port addition) angMirrorRate = 0.0f; angMirrorValid = 0; diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 17281dc..176ccd4 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -625,6 +625,12 @@ public: // 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. + // Master send-mirror gait travel (BT_MASTER_GAITMIRROR): the invisible mj=0 + // body channel's per-frame cycleDistance, stashed in the drive block and + // consumed by the send gate to advance projectedOrigin the way the PEER + // predicts (gait), instead of the constant-velocity deadReckoner. This is the + // authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin. + Scalar mirrorBodyAdv; // body cycleDistance this frame (master) 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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index d316c01..e67da96 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -3167,7 +3167,21 @@ void // while the animation transitioned gradually (the "physics don't // line up with the animation" desync). Signed, the SM decelerates // through stop -> reverse-entry exactly like the leg channel. - if (s_realControls && MappingMapper() != 0) + // BT_MASTER_GAITMIRROR: the invisible mj=0 body channel becomes the + // PEER's predictor, so it must slew toward the LAST-SENT commanded + // speed (which the record writers stamp into bodyTargetSpeed@0x6b4 on + // each send, mech.cpp), NOT the live mapper demand. Leaving + // bodyTargetSpeed at last-sent re-activates the authentic type-2 + // deadband and makes the mirror model exactly what the peer computes. + // The master's VISIBLE motion uses the LEG channel reading live demand + // independently, so this does not change what the local player sees. + // (Default OFF: live overwrite preserved.) + static const int s_gaitMirror = getenv("BT_MASTER_GAITMIRROR") ? 1 : 0; + if (s_gaitMirror) + { + /* leave bodyTargetSpeed at the last-sent value */ + } + else if (s_realControls && MappingMapper() != 0) bodyTargetSpeed = MappingMapper()->speedDemand; else bodyTargetSpeed = ((throttle < 0.0f) ? -1.0f : 1.0f) @@ -3234,6 +3248,7 @@ void // BT_BODY_MJ=1 restores the old double-writer for A/B. static const int s_bodyMj = BTEnvOn("BT_BODY_MJ", 0); adv = AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0) + mirrorBodyAdv = adv; // stash for the send-mirror gait advance legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel } else @@ -3716,7 +3731,43 @@ void // the PEER's event queue busy, so its CreatingMission->LoadingMission // quiet-timeout never fired -- the pods deadlocked pre-launch. The real // system never moves before launch, so the gate is faithful.) - (this->*deadReckoner)(); + // SEND-MIRROR advance: model where the PEER believes this mech is, so the + // deadband fires exactly when the peer would drift. Authentic (binary + // FUN_004a9b5c @0x4aab9c): advance projectedOrigin by IntegrateMotion(mj=0) + // -- the SAME gait predictor the peer runs -- fed the LAST-SENT commanded + // speed. Our default mirror uses the engine constant-velocity deadReckoner + // (AcceleratedDeadReckoner), which does NOT model a gait-driven peer -> the + // deadband under-sends and the coupled peer drifts (measured). With + // BT_MASTER_GAITMIRROR: increment projectedOrigin.linear by the mj=0 body + // channel's travel (mirrorBodyAdv, = the peer's predicted stride) rotated by + // the mirror heading, and integrate the heading by the last-sent angular + // velocity. Re-seeded to localOrigin on each send by the record writers' + // projectedOrigin re-base, so this is incremental-from-last-send, exactly the + // peer's IntegrateMotion. (Default OFF: constant-velocity deadReckoner.) + static const int s_gaitMirrorSend = getenv("BT_MASTER_GAITMIRROR") ? 1 : 0; + if (s_gaitMirrorSend) + { + Vector3D mAng; + mAng.Multiply(updateVelocity.angularMotion, dt); // last-sent yaw rate + ReconQuatIntegrate(&projectedOrigin.angularPosition, + &projectedOrigin.angularPosition, &mAng); + projectedOrigin.angularPosition.Normalize(); + if (mirrorBodyAdv != 0.0f) + { + Vector3D mLocal(0.0f, 0.0f, -mirrorBodyAdv); // forward = -Z, frame distance + Matrix34 mOrient; + Matrix34::FromQuaternion(&mOrient, &projectedOrigin.angularPosition); + Vector3D mWorld; + FUN_00408744(&mWorld, (Scalar *)&mLocal, &mOrient); + projectedOrigin.linearPosition.x += mWorld.x; + projectedOrigin.linearPosition.y += mWorld.y; + projectedOrigin.linearPosition.z += mWorld.z; + } + } + else + { + (this->*deadReckoner)(); + } Vector3D error; error.Subtract( projectedOrigin.linearPosition, @@ -3774,6 +3825,15 @@ void ) { ForceUpdate(); // type 0: pose record + // RE-SEED the gait mirror to the just-sent pose: the peer will now + // extrapolate from THIS localOrigin, so the mirror must too. (The + // type-0 writer only re-bases projectedOrigin in the sim-state-2 + // transition, not the normal walk case, so do it here.) + if (s_gaitMirrorSend) + { + projectedOrigin.linearPosition = localOrigin.linearPosition; + projectedOrigin.angularPosition = localOrigin.angularPosition; + } if (getenv("BT_WIRE")) DEBUG_STREAM << "[tx0] errSq=" << error.LengthSquared() << " posDb=" << posDb