From a3329a64da861be68283e52934e4f1f494f1ffc8 Mon Sep 17 00:00:00 2001 From: arcattack Date: Mon, 27 Jul 2026 07:13:07 -0500 Subject: [PATCH] #67 torso replication, part 1: the dead-reckoning clock fix + the bug REPRODUCED on the rig THE CLOCK FIX (landed, verified live). The replicant torso's extrapolation window was hardwired to ZERO: both shim accessors returned the same field (torso.hpp, the "dormant in single-player" note). The engine maintains both clocks exactly as the binary expects -- WriteUpdateRecord stamps lastUpdate=lastPerformance on the master, ReadUpdateRecord stamps lastUpdate=Now() on the replicant (SIMULATE.cpp:276/296, the 1995 "HACK" comment intact) -- so the fix is one mapping: GetCreationTime (misnamed; now GetLastUpdateTime) reads lastUpdate. ComputeTargetTwist's `current - lastUpdate` window is real again. Why legs always replicated while torsos did not: Mover is ENGINE code reading these fields natively; the Torso is our reconstruction with the collapsed shim. Rig-verified: the copy-side log now shows distinct lastUpd/now values. THE FIELD BUG REPRODUCED (2-pod rig, thor vs thor -- thor because the first attempt used FOGDAY's Black Hawk, whose torso is authentically FIXED like the Owens: hEn=0, limits +/-0.01 deg): [torso-copy] cur=-3.31613 target=-3.31613 atUpd=-3750 The replicant renders at EXACTLY its twist limit (thor: +/-3.31613 rad) because twistAtUpdate holds garbage (-3750) and the limit clamp slams it to the stop -- "twisted full right but was not using TT", on demand. An earlier run poisoned it with 2.1e-44 (= int 15 as float, suspiciously the torso's subsystem index). NARROWED: the tx/rx probes added here (BT_TORSO_LOG prints every torso record both directions, header + raw payload dwords) logged ZERO records in the poisoned runs -- Torso::Read/WriteUpdateRecord never executed. The garbage therefore arrives OUTSIDE the torso record path: a raw state write during mech spawn/consolidation spraying the torso's fields (stream-walker framing or a consolidation blit). That writer is part 2's hunt; the probes and the deterministic repro make it a short one. Co-Authored-By: Claude Opus 5 (1M context) --- game/reconstructed/torso.cpp | 40 ++++++++++++++++++++++++++++++------ game/reconstructed/torso.hpp | 20 ++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/game/reconstructed/torso.cpp b/game/reconstructed/torso.cpp index bd304d3..b23a87c 100644 --- a/game/reconstructed/torso.cpp +++ b/game/reconstructed/torso.cpp @@ -341,7 +341,7 @@ Torso::Torso( hitElevTop = hitElevBottom = 0; // @0x258 / @0x25C hitTwistLeft = hitTwistRight = 0; // @0x260 / @0x264 recenterActive = 0; // @0x274 - lastUpdateTime = GetCreationTime(); // @0x254 = this[5] + lastUpdateTime = GetLastUpdateTime(); // @0x254 = this[5] // bring-up verification (env BT_TORSO_LOG): the gyro cross-links to // (Torso*)+0x1D8 (== currentTwist) at a RAW offset (mech.cpp:740), so the @@ -465,9 +465,9 @@ void Torso::ReadUpdateRecord(UpdateRecord *message) { lastUpdateTime = GetCurrentTime(); // @0x254 - if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs) + if ((Scalar)(lastUpdateTime - GetLastUpdateTime()) / MsPerSecond < MinSlewMs) { - lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows + lastUpdateTime += (lastUpdateTime - GetLastUpdateTime()); // stretch tiny windows } Subsystem::ReadUpdateRecord(message); // FUN_0041bd34 @@ -475,6 +475,23 @@ void twistAtUpdate = RecordField(message, 0x10); // @0x21C twistVelocity = RecordField(message, 0x14); // @0x1E8 twistRate = RecordField(message, 0x18); // @0x238 + + // #67 stream forensics (BT_TORSO_LOG): the copy received atUpd=2.1e-44 + // (= int 15 as float) on the rig -- a mis-framed record. Print the raw + // header + payload dwords of every record this torso consumes. + if (getenv("BT_TORSO_LOG")) + { + const unsigned int *raw = (const unsigned int *)message; + DEBUG_STREAM << "[torso-rec-rx] len=" << message->recordLength + << " subID=" << (int)message->subsystemID + << " recID=" << (int)message->recordID + << std::hex + << " p10=0x" << raw[4] << " p14=0x" << raw[5] + << " p18=0x" << raw[6] << std::dec + << " -> atUpd=" << twistAtUpdate + << " vel=" << twistVelocity << " rate=" << twistRate + << "\n" << std::flush; + } } // @@ -503,6 +520,17 @@ void WriteRecordField(message, 0x14, twistVelocity); // @0x1E8 WriteRecordField(message, 0x18, twistRate); // @0x238 twistAtUpdate = currentTwist; // @0x21C snapshot at send + + // #67 stream forensics: what the MASTER actually serialized. + if (getenv("BT_TORSO_LOG")) + { + DEBUG_STREAM << "[torso-rec-tx] len=" << message->recordLength + << " subID=" << (int)message->subsystemID + << " recID=" << (int)message->recordID + << " twist=" << currentTwist + << " vel=" << twistVelocity << " rate=" << twistRate + << "\n" << std::flush; + } } @@ -730,7 +758,7 @@ void } else { - Scalar age = (Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond; + Scalar age = (Scalar)(lastUpdateTime - GetLastUpdateTime()) / MsPerSecond; currentTwist = Lerp(currentTwist, targetTwist, time_slice / (age + time_slice)); // FUN_004081e0 } targetTwist = Min(targetTwist, horizontalLimitLeft); // @0x1E0 @@ -776,12 +804,12 @@ Logical if (isDamagedCopy == 0 || lastUpdateTime <= GetCurrentTime()) { - base = GetCurrentTime() - GetCreationTime(); // this[0x10] - this[0x14] + base = GetCurrentTime() - GetLastUpdateTime(); // this[0x10] - this[0x14] slewing = False; } else { - base = lastUpdateTime - GetCreationTime(); // @0x254 - this[0x14] + base = lastUpdateTime - GetLastUpdateTime(); // @0x254 - this[0x14] slewing = True; } diff --git a/game/reconstructed/torso.hpp b/game/reconstructed/torso.hpp index e989dba..2445e2d 100644 --- a/game/reconstructed/torso.hpp +++ b/game/reconstructed/torso.hpp @@ -155,15 +155,27 @@ class Joint; // engine skeleton node (JOINT.h); the twist target // HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140) // ElectricalState <- PowerWatcher::watchdogAlarm level (this+0x198) // HeatModelOff <- MechSubsystem::simulationState==Destroyed (this+0x40) - // Creation/Current <- engine Simulation::lastPerformance (this+0x14/0x10) - // the net/copy timing is dormant in single-player; - // equal reads give a 0 window == the shipped master path. + // Current <- engine Simulation::lastPerformance (this+0x10) + // LastUpdate <- engine Simulation::lastUpdate (this+0x14) + // #67 (2026-07-27): both accessors used to return + // lastPerformance ("dormant in single-player") -- + // which zeroed the replicant torso's dead-reckoning + // window (ComputeTargetTwist: elapsed = current - + // lastUpdate) and broke remote torso twist in MP + // (mechs stuck twisted / muzzles at mech centre). + // The engine maintains BOTH fields exactly as the + // binary expects: WriteUpdateRecord stamps + // lastUpdate=lastPerformance on the master, + // ReadUpdateRecord stamps lastUpdate=Now() on the + // replicant (SIMULATE.cpp:276/296). Mover -- engine + // code reading these fields natively -- is why LEGS + // replicated fine while the torso did not. // SetMovedFlag -> engine Simulation::ForceUpdate() dirty bit (this+0x18) // (GetSegmentFlags removed -- unused; the ctor reads owner->simulationFlags.) HeatSink::HeatState HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); } Logical HeatModelOff() const { return simulationState == 1; /* Destroyed */ } PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); } - LWord GetCreationTime() const { return (LWord)(long)lastPerformance; } + LWord GetLastUpdateTime() const { return (LWord)(long)lastUpdate; } LWord GetCurrentTime() const { return (LWord)(long)lastPerformance; } // Current horizontal (yaw) torso aim in radians (binary torso+0x1d8); the // damage-table PieSlice wheel reads this when RotateWithTorso is set.