#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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9bb95d8580
commit
a3329a64da
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user