BT410 5.3.114: the replicant torso aims -- TorsoCopySimulation + ComputeTargetTwist, decomp-verified, two donor drifts corrected

The damaged-copy twist path (@004b65f8 + @004b6510), reconstructed from the
raw decomp rather than the BT411 donor -- which drifted twice: its ease aged
by lastUpdate (+0x14) where the binary reads lastPerformance (+0x10), and it
calls the joint-write helper from the copy sim where 1995 does not (port-era
addition, dropped). The extrapolator predicts targetTwist = twistAtUpdate +
twistRate * elapsed on two time bases (settled = this frame's window, slewing
= the command window), and the copy sim snaps when settled or eases by the
remaining-time form dt/((stamp - now) + dt) -- landing exactly when the clock
reaches the command stamp. The decomp's DAT_0052140c divide is just
Time::operator- inlined; no hand conversion existed.

Members carved from the dynamicsState reserve (16->12): targetTwist @0x218,
twistAtUpdate @0x21C, twistRate @0x238, lastUpdateTime @0x254. The ctor now
registers the copy Performance on replicants (PTR @00510c1c); [T2] the gate
mirrors the binary's simulationFlags 0xC/0x100 pair with the instance test,
same as the whole watcher family, until the stream builders that seed those
bits are reconstructed. Feeders (Read/WriteUpdateRecord) stay staged --
zero-init eases a copy to centre, correct-by-vacuity in single-pod.

Stubs: 16 across 10 files. Regression: clean mission soak on the rig
(no faults, wheel turning, pacing steady).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-04 07:55:14 -05:00
co-authored by Claude Fable 5
parent 0e763fda1f
commit ea99e8987f
3 changed files with 839 additions and 662 deletions
+514 -398
View File
@@ -1,398 +1,514 @@
//===========================================================================// //===========================================================================//
// File: torso.cpp // // File: torso.cpp //
// Project: BattleTech Brick: Mech subsystems // // Project: BattleTech Brick: Mech subsystems //
// Contents: Torso -- torso twist / weapon elevation // // Contents: Torso -- torso twist / weapon elevation //
//---------------------------------------------------------------------------// //---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. // // Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide // // All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================// //===========================================================================//
#include <bt.hpp> #include <bt.hpp>
#pragma hdrstop #pragma hdrstop
#if !defined(TORSO_HPP) #if !defined(TORSO_HPP)
# include <torso.hpp> # include <torso.hpp>
#endif #endif
#if !defined(MECH_HPP) #if !defined(MECH_HPP)
# include <mech.hpp> # include <mech.hpp>
#endif #endif
#if !defined(JOINT_HPP) #if !defined(JOINT_HPP)
# include <joint.hpp> # include <joint.hpp>
#endif #endif
Derivation Derivation
Torso::ClassDerivations( Torso::ClassDerivations(
PowerWatcher::ClassDerivations, PowerWatcher::ClassDerivations,
"Torso" "Torso"
); );
// //
// The full 13-entry authentic table (ids 3..15) -- donor torso.cpp:152 with // The full 13-entry authentic table (ids 3..15) -- donor torso.cpp:152 with
// binary offsets, cross-confirmed by BTL4.RES's streamed control mappings // binary offsets, cross-confirmed by BTL4.RES's streamed control mappings
// binding ids 12/13 on subsystem 17. The five command members and // binding ids 12/13 on subsystem 17. The five command members and
// analogTwistAxis are DIRECT WRITE DESTINATIONS for device polls // analogTwistAxis are DIRECT WRITE DESTINATIONS for device polls
// (L4CTRL.CPP:1936), which is why every id from 9 up must exist and be the // (L4CTRL.CPP:1936), which is why every id from 9 up must exist and be the
// right kind of storage: id 9 short of this table landed the RIO's analog // right kind of storage: id 9 short of this table landed the RIO's analog
// stick on the motionState StateIndicator. // stick on the motionState StateIndicator.
// //
// SpeedOf* still point at the base rates rather than the donor's live // SpeedOf* still point at the base rates rather than the donor's live
// velocity accumulators (elevationVelocity/twistVelocity @0x1EC/0x1E8) -- // velocity accumulators (elevationVelocity/twistVelocity @0x1EC/0x1E8) --
// those members arrive with TorsoSimulation; the authored audio watcher on // those members arrive with TorsoSimulation; the authored audio watcher on
// SpeedOfTorsoHorizontal just reads a constant until then. // SpeedOfTorsoHorizontal just reads a constant until then.
// //
const Torso::IndexEntry const Torso::IndexEntry
Torso::AttributePointers[]= Torso::AttributePointers[]=
{ {
ATTRIBUTE_ENTRY(Torso, RotationOfTorsoVertical, currentElevation), ATTRIBUTE_ENTRY(Torso, RotationOfTorsoVertical, currentElevation),
ATTRIBUTE_ENTRY(Torso, RotationOfTorsoHorizontal, currentTwist), ATTRIBUTE_ENTRY(Torso, RotationOfTorsoHorizontal, currentTwist),
ATTRIBUTE_ENTRY(Torso, HorizontalLimitRight, horizontalLimitRight), ATTRIBUTE_ENTRY(Torso, HorizontalLimitRight, horizontalLimitRight),
ATTRIBUTE_ENTRY(Torso, HorizontalLimitLeft, horizontalLimitLeft), ATTRIBUTE_ENTRY(Torso, HorizontalLimitLeft, horizontalLimitLeft),
ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoVertical, baseElevationRate), ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoVertical, baseElevationRate),
ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoHorizontal, baseTwistRate), ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoHorizontal, baseTwistRate),
ATTRIBUTE_ENTRY(Torso, StickPosition, analogTwistAxis), ATTRIBUTE_ENTRY(Torso, StickPosition, analogTwistAxis),
ATTRIBUTE_ENTRY(Torso, TorsoUp, elevateUpCommand), ATTRIBUTE_ENTRY(Torso, TorsoUp, elevateUpCommand),
ATTRIBUTE_ENTRY(Torso, TorsoDown, elevateDownCommand), ATTRIBUTE_ENTRY(Torso, TorsoDown, elevateDownCommand),
ATTRIBUTE_ENTRY(Torso, TorsoLeft, twistLeftCommand), ATTRIBUTE_ENTRY(Torso, TorsoLeft, twistLeftCommand),
ATTRIBUTE_ENTRY(Torso, TorsoRight, twistRightCommand), ATTRIBUTE_ENTRY(Torso, TorsoRight, twistRightCommand),
ATTRIBUTE_ENTRY(Torso, TorsoCenter, centerCommand), ATTRIBUTE_ENTRY(Torso, TorsoCenter, centerCommand),
ATTRIBUTE_ENTRY(Torso, MotionState, motionState) ATTRIBUTE_ENTRY(Torso, MotionState, motionState)
}; };
Torso::AttributeIndexSet Torso::AttributeIndexSet
Torso::AttributeIndex( Torso::AttributeIndex(
ELEMENTS(Torso::AttributePointers), ELEMENTS(Torso::AttributePointers),
Torso::AttributePointers, Torso::AttributePointers,
MechSubsystem::AttributeIndex MechSubsystem::AttributeIndex
); );
Torso::SharedData Torso::SharedData
Torso::DefaultData( Torso::DefaultData(
Torso::ClassDerivations, Torso::ClassDerivations,
Subsystem::MessageHandlers, Subsystem::MessageHandlers,
Torso::AttributeIndex, Torso::AttributeIndex,
Subsystem::StateCount Subsystem::StateCount
); );
Torso::Torso( Torso::Torso(
Mech *owner, Mech *owner,
int subsystem_ID, int subsystem_ID,
SubsystemResource *r, SubsystemResource *r,
SharedData &shared_data SharedData &shared_data
): ):
PowerWatcher(owner, subsystem_ID, r, shared_data) PowerWatcher(owner, subsystem_ID, r, shared_data)
{ {
Check(owner); Check(owner);
Check_Pointer(r); Check_Pointer(r);
isDamagedCopy = (owner->GetInstance() == Entity::ReplicantInstance); isDamagedCopy = (owner->GetInstance() == Entity::ReplicantInstance);
statusFlags = 0; statusFlags = 0;
buttonAccelerationPerSecond = r->buttonAccelerationPerSecond; buttonAccelerationPerSecond = r->buttonAccelerationPerSecond;
buttonAccelerationStart = r->buttonAccelerationStartValue; buttonAccelerationStart = r->buttonAccelerationStartValue;
baseTwistRate = r->horizontalRotationPerSecond * RAD_PER_DEG; baseTwistRate = r->horizontalRotationPerSecond * RAD_PER_DEG;
baseElevationRate = r->verticalRotationPerSecond * RAD_PER_DEG; baseElevationRate = r->verticalRotationPerSecond * RAD_PER_DEG;
horizontalLimitRight= r->horizontalLimitRight * RAD_PER_DEG; horizontalLimitRight= r->horizontalLimitRight * RAD_PER_DEG;
horizontalLimitLeft = r->horizontalLimitLeft * RAD_PER_DEG; horizontalLimitLeft = r->horizontalLimitLeft * RAD_PER_DEG;
verticalLimitTop = r->verticalLimitTop * RAD_PER_DEG; verticalLimitTop = r->verticalLimitTop * RAD_PER_DEG;
verticalLimitBottom = r->verticalLimitBottom * RAD_PER_DEG; verticalLimitBottom = r->verticalLimitBottom * RAD_PER_DEG;
twistCenterHigh = verticalLimitTop; twistCenterHigh = verticalLimitTop;
twistCenterLow = verticalLimitBottom; twistCenterLow = verticalLimitBottom;
elevationCenter = verticalLimitTop; elevationCenter = verticalLimitTop;
elevationHalfBottom = verticalLimitBottom * 0.5f; elevationHalfBottom = verticalLimitBottom * 0.5f;
buttonRampActive = 0; buttonRampActive = 0;
buttonRamp = 0.0f; buttonRamp = 0.0f;
horizontalEnabled = r->torsoHorizontalEnabled; horizontalEnabled = r->torsoHorizontalEnabled;
// //
// The device-poll write destinations (published ids 9..14). Zero = // The device-poll write destinations (published ids 9..14). Zero =
// stick centred, no button held -- their state before the first poll. // stick centred, no button held -- their state before the first poll.
// //
elevateUpCommand = 0; elevateUpCommand = 0;
elevateDownCommand = 0; elevateDownCommand = 0;
twistLeftCommand = 0; twistLeftCommand = 0;
twistRightCommand = 0; twistRightCommand = 0;
centerCommand = 0; centerCommand = 0;
recenterActive = 0; recenterActive = 0;
// //
// Resolve the named torso-twist skeleton joints (twist body + shadow) from // Resolve the named torso-twist skeleton joints (twist body + shadow) from
// the mech skeleton (now live). A mech with a fixed torso, or one whose // the mech skeleton (now live). A mech with a fixed torso, or one whose
// joint name is absent from the skeleton, resolves NULL and simply never // joint name is absent from the skeleton, resolves NULL and simply never
// twists. // twists.
// //
horizontalJointNode = owner->ResolveJoint(r->torsoHorizontalJoint); horizontalJointNode = owner->ResolveJoint(r->torsoHorizontalJoint);
horizontalShadowJointNode = owner->ResolveJoint(r->torsoHorizontalShadowJoint); horizontalShadowJointNode = owner->ResolveJoint(r->torsoHorizontalShadowJoint);
if (getenv("BT_MECH_LOG")) if (getenv("BT_MECH_LOG"))
{ {
DEBUG_STREAM << "[torso] horizJoint '" << r->torsoHorizontalJoint DEBUG_STREAM << "[torso] horizJoint '" << r->torsoHorizontalJoint
<< "' -> " << (void *)horizontalJointNode; << "' -> " << (void *)horizontalJointNode;
if (horizontalJointNode != NULL) if (horizontalJointNode != NULL)
{ {
DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType(); DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
} }
DEBUG_STREAM << " enabled=" << (int)horizontalEnabled << endl << flush; DEBUG_STREAM << " enabled=" << (int)horizontalEnabled << endl << flush;
} }
currentTwist = 0.0f; currentTwist = 0.0f;
currentElevation = 0.0f; currentElevation = 0.0f;
analogElevationAxis = 0.0f; analogElevationAxis = 0.0f;
analogTwistAxis = 0.0f; analogTwistAxis = 0.0f;
// //
// ELEMENTS(), never a literal. This loop said 22 while the array was // ELEMENTS(), never a literal. This loop said 22 while the array was
// shrunk to 16 by carving the command members out of its front (5.3.68 / // shrunk to 16 by carving the command members out of its front (5.3.68 /
// 5.3.71) -- 24 bytes written past the end of every Torso, on the heap, // 5.3.71) -- 24 bytes written past the end of every Torso, on the heap,
// every mission. It compiled and ran; C++ does not check. The macro // every mission. It compiled and ran; C++ does not check. The macro
// makes the next carve harmless. // makes the next carve harmless.
// //
int int
i; i;
for (i = 0; i < (int)ELEMENTS(dynamicsState); ++i) for (i = 0; i < (int)ELEMENTS(dynamicsState); ++i)
{ {
dynamicsState[i] = 0.0f; dynamicsState[i] = 0.0f;
} }
// //
// Install the per-frame twist/elevation Performance (the replicant copy is // Twist-replication state (binary @0x218/@0x21C/@0x238/@0x254) -- see the
// driven by console updates via TorsoCopySimulation instead, still staged). // header note. Zero until an update record stamps it.
// //
if (owner->GetInstance() != Entity::ReplicantInstance) targetTwist = 0.0f;
{ twistAtUpdate = 0.0f;
SetPerformance(&Torso::TorsoSimulation); twistRate = 0.0f;
} lastUpdateTime = 0L;
Check_Fpu(); //
} // Install the per-frame Performance. The binary's gate @004b6b0c is
// (owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100)
Torso::~Torso() // != 0 -- the master-segment flag pair seeded by the mech stream builders,
{ // which are not reconstructed yet. [T2] Until they land, the whole
} // watcher family mirrors that gate with the instance test (same sites:
// heat.cpp / powersub.cpp / gyro.cpp registrations); revisit together.
Logical //
Torso::TestClass(Mech &) if (owner->GetInstance() != Entity::ReplicantInstance)
{ {
return True; SetPerformance(&Torso::TorsoSimulation);
} }
else
Logical {
Torso::TestInstance() const SetPerformance(&Torso::TorsoCopySimulation); // binary PTR @00510c1c
{ }
return IsDerivedFrom(ClassDerivations);
} Check_Fpu();
}
//
//############################################################################# Torso::~Torso()
// TorsoSimulation -- per-frame torso twist / weapon-elevation integration. {
// }
// Slews the current elevation/twist toward the analog aim axes (written by
// MechControlsMapper::InterpretControls) at the resource base rates, clamped to Logical
// the resource limits. Authentic core (torso.cpp @004b6xxx): current += axis * Torso::TestClass(Mech &)
// baseRate * dt. APPLYING the resolved angles onto the skeleton torso/gun {
// joints (horizontalJointNode / the elevation joint) is deferred with the return True;
// skeleton-link + render wave; here we advance the scalar aim state (read by the }
// HUD reticle / weapon aim and, once wired, the joints).
//############################################################################# Logical
// Torso::TestInstance() const
void {
Torso::TorsoSimulation(Scalar time_slice) return IsDerivedFrom(ClassDerivations);
{ }
Check(this);
//
// //#############################################################################
// The BUTTON COMMANDS (donor torso.cpp:557, binary @004b5cf0). The five // TorsoSimulation -- per-frame torso twist / weapon-elevation integration.
// command members are the streamed direct mappings' write destinations -- //
// the RIO/TM twist and elevation buttons land in them as raw // Slews the current elevation/twist toward the analog aim axes (written by
// ControlsButton ints (positive = held). The donor's ramp machinery is // MechControlsMapper::InterpretControls) at the resource base rates, clamped to
// carried verbatim, including its punchline: the shipped build // the resource limits. Authentic core (torso.cpp @004b6xxx): current += axis *
// unconditionally overwrites the ramp with 1.0f, so the acceleration // baseRate * dt. APPLYING the resolved angles onto the skeleton torso/gun
// ramp is authored dead weight. Kept because it is the binary's shape. // joints (horizontalJointNode / the elevation joint) is deferred with the
// // skeleton-link + render wave; here we advance the scalar aim state (read by the
Scalar // HUD reticle / weapon aim and, once wired, the joints).
twist_step = baseTwistRate * time_slice, //#############################################################################
elev_step = baseElevationRate * time_slice; //
void
// Torso::TorsoSimulation(Scalar time_slice)
// DEV HOOK BT_FORCE_TORSO=1: sweep the twist BUTTONS left/right without a {
// hand on the RIO. This drives the exact members the streamed direct Check(this);
// mappings write (twistLeftCommand / twistRightCommand), so it exercises
// the real button path end to end -- table binding, this integrator, the //
// skeleton joint, and the cockpit eye that rides the twist chain -- and // The BUTTON COMMANDS (donor torso.cpp:557, binary @004b5cf0). The five
// makes the result visible on the render bridge. The donor carries the // command members are the streamed direct mappings' write destinations --
// same hook for the same reason (torso.cpp:583). // the RIO/TM twist and elevation buttons land in them as raw
// // ControlsButton ints (positive = held). The donor's ramp machinery is
// Held for ~90 frames each way, matching the donor's period. // carried verbatim, including its punchline: the shipped build
// // unconditionally overwrites the ramp with 1.0f, so the acceleration
if (getenv("BT_FORCE_TORSO") != NULL) // ramp is authored dead weight. Kept because it is the binary's shape.
{ //
static int Scalar
sweep = 0; twist_step = baseTwistRate * time_slice,
elev_step = baseElevationRate * time_slice;
sweep++;
twistLeftCommand = ((sweep / 90) & 1) ? 1 : 0; //
twistRightCommand = ((sweep / 90) & 1) ? 0 : 1; // DEV HOOK BT_FORCE_TORSO=1: sweep the twist BUTTONS left/right without a
} // hand on the RIO. This drives the exact members the streamed direct
// mappings write (twistLeftCommand / twistRightCommand), so it exercises
if (buttonRampActive == 0) // the real button path end to end -- table binding, this integrator, the
{ // skeleton joint, and the cockpit eye that rides the twist chain -- and
buttonRamp = buttonAccelerationStart; // makes the result visible on the render bridge. The donor carries the
} // same hook for the same reason (torso.cpp:583).
else //
{ // Held for ~90 frames each way, matching the donor's period.
buttonRamp += buttonAccelerationPerSecond * time_slice; //
if (buttonRamp > 1.0f) if (getenv("BT_FORCE_TORSO") != NULL)
{ {
buttonRamp = 1.0f; static int
} sweep = 0;
buttonRampActive = 0;
} sweep++;
buttonRamp = 1.0f; // @004b5cf0 unconditionally overwrites with 1.0f twistLeftCommand = ((sweep / 90) & 1) ? 1 : 0;
twistRightCommand = ((sweep / 90) & 1) ? 0 : 1;
if (elevateUpCommand > 0) }
{
currentElevation += elev_step * buttonRamp; if (buttonRampActive == 0)
if (currentElevation > verticalLimitTop) {
{ buttonRamp = buttonAccelerationStart;
currentElevation = verticalLimitTop; }
} else
buttonRampActive = 1; {
} buttonRamp += buttonAccelerationPerSecond * time_slice;
if (elevateDownCommand > 0) if (buttonRamp > 1.0f)
{ {
currentElevation -= elev_step * buttonRamp; buttonRamp = 1.0f;
if (currentElevation < verticalLimitBottom) }
{ buttonRampActive = 0;
currentElevation = verticalLimitBottom; }
} buttonRamp = 1.0f; // @004b5cf0 unconditionally overwrites with 1.0f
buttonRampActive = 1;
} if (elevateUpCommand > 0)
{
// currentElevation += elev_step * buttonRamp;
// Elevation (weapon pitch): slew toward the analog axis, clamp to limits. if (currentElevation > verticalLimitTop)
// {
currentElevation += analogElevationAxis * baseElevationRate * time_slice; currentElevation = verticalLimitTop;
{ }
Scalar lo = (verticalLimitBottom < verticalLimitTop) buttonRampActive = 1;
? verticalLimitBottom : verticalLimitTop; }
Scalar hi = (verticalLimitBottom < verticalLimitTop) if (elevateDownCommand > 0)
? verticalLimitTop : verticalLimitBottom; {
if (currentElevation < lo) currentElevation = lo; currentElevation -= elev_step * buttonRamp;
if (currentElevation > hi) currentElevation = hi; if (currentElevation < verticalLimitBottom)
} {
currentElevation = verticalLimitBottom;
// }
// Twist (horizontal torso rotation): only mechs with an enabled torso joint. buttonRampActive = 1;
// }
if (horizontalEnabled)
{ //
// // Elevation (weapon pitch): slew toward the analog axis, clamp to limits.
// Digital twist commands, then recenter, then the analog axis -- //
// the donor's order. Twist buttons cancel a pending recenter; currentElevation += analogElevationAxis * baseElevationRate * time_slice;
// the centre button arms it and it slews home across frames {
// (donor Recenter, binary @004b6918). Scalar lo = (verticalLimitBottom < verticalLimitTop)
// ? verticalLimitBottom : verticalLimitTop;
if (twistLeftCommand > 0) Scalar hi = (verticalLimitBottom < verticalLimitTop)
{ ? verticalLimitTop : verticalLimitBottom;
currentTwist += twist_step * buttonRamp; if (currentElevation < lo) currentElevation = lo;
if (currentTwist > horizontalLimitLeft) if (currentElevation > hi) currentElevation = hi;
{ }
currentTwist = horizontalLimitLeft;
} //
recenterActive = 0; // Twist (horizontal torso rotation): only mechs with an enabled torso joint.
buttonRampActive = 1; //
} if (horizontalEnabled)
if (twistRightCommand > 0) {
{ //
currentTwist -= twist_step * buttonRamp; // Digital twist commands, then recenter, then the analog axis --
if (currentTwist < horizontalLimitRight) // the donor's order. Twist buttons cancel a pending recenter;
{ // the centre button arms it and it slews home across frames
currentTwist = horizontalLimitRight; // (donor Recenter, binary @004b6918).
} //
recenterActive = 0; if (twistLeftCommand > 0)
buttonRampActive = 1; {
} currentTwist += twist_step * buttonRamp;
if (centerCommand > 0) if (currentTwist > horizontalLimitLeft)
{ {
recenterActive = 1; currentTwist = horizontalLimitLeft;
buttonRampActive = 0; }
} recenterActive = 0;
if (recenterActive != 0) buttonRampActive = 1;
{ }
if (currentTwist > 0.0f) if (twistRightCommand > 0)
{ {
currentTwist -= twist_step; currentTwist -= twist_step * buttonRamp;
if (currentTwist < 0.0f) currentTwist = 0.0f; if (currentTwist < horizontalLimitRight)
} {
else if (currentTwist < 0.0f) currentTwist = horizontalLimitRight;
{ }
currentTwist += twist_step; recenterActive = 0;
if (currentTwist > 0.0f) currentTwist = 0.0f; buttonRampActive = 1;
} }
if (currentTwist == 0.0f) if (centerCommand > 0)
{ {
recenterActive = 0; recenterActive = 1;
} buttonRampActive = 0;
} }
if (recenterActive != 0)
if (getenv("BT_TORSO_LOG") != NULL) {
{ if (currentTwist > 0.0f)
static int {
torso_tick = 0; currentTwist -= twist_step;
if (currentTwist < 0.0f) currentTwist = 0.0f;
if ((torso_tick++ % 240) == 0) }
{ else if (currentTwist < 0.0f)
DEBUG_STREAM << "[torso] twist=" << currentTwist {
<< " elev=" << currentElevation currentTwist += twist_step;
<< " cmdL=" << twistLeftCommand if (currentTwist > 0.0f) currentTwist = 0.0f;
<< " cmdR=" << twistRightCommand }
<< " axis=" << analogTwistAxis if (currentTwist == 0.0f)
<< " rate=" << baseTwistRate {
<< " limits=(" << horizontalLimitRight recenterActive = 0;
<< ".." << horizontalLimitLeft << ")" }
<< " joint=" << (horizontalJointNode != NULL ? 1 : 0) }
<< endl << flush;
} if (getenv("BT_TORSO_LOG") != NULL)
} {
static int
currentTwist += analogTwistAxis * baseTwistRate * time_slice; torso_tick = 0;
Scalar lo = (horizontalLimitLeft < horizontalLimitRight)
? horizontalLimitLeft : horizontalLimitRight; if ((torso_tick++ % 240) == 0)
Scalar hi = (horizontalLimitLeft < horizontalLimitRight) {
? horizontalLimitRight : horizontalLimitLeft; DEBUG_STREAM << "[torso] twist=" << currentTwist
if (currentTwist < lo) currentTwist = lo; << " elev=" << currentElevation
if (currentTwist > hi) currentTwist = hi; << " cmdL=" << twistLeftCommand
<< " cmdR=" << twistRightCommand
// << " axis=" << analogTwistAxis
// Push the twist onto the skeleton torso joint so the upper body rotates << " rate=" << baseTwistRate
// on the model. Hinge joints take the scalar angle about their axis; << " limits=(" << horizontalLimitRight
// ball joints take it as the yaw component. << ".." << horizontalLimitLeft << ")"
// << " joint=" << (horizontalJointNode != NULL ? 1 : 0)
if (horizontalJointNode != NULL) << endl << flush;
{ }
Joint::JointType jt = horizontalJointNode->GetJointType(); }
if (jt == Joint::HingeXJointType
|| jt == Joint::HingeYJointType currentTwist += analogTwistAxis * baseTwistRate * time_slice;
|| jt == Joint::HingeZJointType) Scalar lo = (horizontalLimitLeft < horizontalLimitRight)
{ ? horizontalLimitLeft : horizontalLimitRight;
horizontalJointNode->SetRotation(Radian(currentTwist)); Scalar hi = (horizontalLimitLeft < horizontalLimitRight)
} ? horizontalLimitRight : horizontalLimitLeft;
else if (jt == Joint::BallJointType) if (currentTwist < lo) currentTwist = lo;
{ if (currentTwist > hi) currentTwist = hi;
horizontalJointNode->SetRotation(EulerAngles(0.0f, currentTwist, 0.0f));
} //
} // Push the twist onto the skeleton torso joint so the upper body rotates
} // on the model. Hinge joints take the scalar angle about their axis;
// ball joints take it as the yaw component.
Check_Fpu(); //
} if (horizontalJointNode != NULL)
{
void Joint::JointType jt = horizontalJointNode->GetJointType();
Torso::TorsoCopySimulation(Scalar) if (jt == Joint::HingeXJointType
{ || jt == Joint::HingeYJointType
Fail("Torso::TorsoCopySimulation -- torso.cpp not yet reconstructed"); || jt == Joint::HingeZJointType)
} {
horizontalJointNode->SetRotation(Radian(currentTwist));
}
else if (jt == Joint::BallJointType)
{
horizontalJointNode->SetRotation(EulerAngles(0.0f, currentTwist, 0.0f));
}
}
}
Check_Fpu();
}
//
//#############################################################################
// ComputeTargetTwist -- binary @004b6510 (232 bytes), verified against the
// raw decomp (part_013.c).
//
// Extrapolate where the master's torso should be by now. Two time bases:
//
// settled (not a copy, or the command stamp is not ahead of the clock):
// elapsed = lastPerformance - lastUpdate (this frame's window)
// slewing (a copy with a command stamp still in the future):
// elapsed = lastUpdateTime - lastUpdate (the command window)
//
// then targetTwist = twistAtUpdate + twistRate * elapsed, clamped to the
// twist limits (left = upper bound, right = lower -- same convention as the
// master sim). Returns True while slewing. Time-Time subtraction is the
// engine's operator- (ticks delta / SystemClock::ticksPerSecond) -- the
// decomp's DAT_0052140c divide is that operator inlined.
//#############################################################################
//
Logical
Torso::ComputeTargetTwist()
{
Check(this);
Scalar
elapsed;
Logical
slewing;
if (isDamagedCopy == 0 || lastUpdateTime <= lastPerformance)
{
elapsed = lastPerformance - lastUpdate;
slewing = False;
}
else
{
elapsed = lastUpdateTime - lastUpdate;
slewing = True;
}
targetTwist = twistAtUpdate + twistRate * elapsed;
if (targetTwist > horizontalLimitLeft)
{
targetTwist = horizontalLimitLeft;
}
if (targetTwist < horizontalLimitRight)
{
targetTwist = horizontalLimitRight;
}
return slewing;
}
//
//#############################################################################
// TorsoCopySimulation -- binary @004b65f8 (188 bytes), verified against the
// raw decomp. The damaged-copy Performance (replicant registration in the
// ctor): no commands, no dynamics -- just ease currentTwist onto the
// extrapolated target.
//
// settled: snap currentTwist = targetTwist
// slewing: ease currentTwist = Lerp(current, target,
// dt / ((lastUpdateTime - lastPerformance) + dt))
// -- the remaining-time form: the ease lands exactly when the
// clock reaches the command stamp.
//
// The binary then re-clamps targetTwist (not currentTwist) with the same
// limit pair. NOTE two donor drifts corrected here against the decomp: the
// ease's age term reads lastPerformance (+0x10), not lastUpdate (+0x14); and
// the binary does NOT call the joint-write helper (@004b67ec) from the copy
// sim -- that call in the BT411 port is a port-era addition, not 1995 code.
//#############################################################################
//
void
Torso::TorsoCopySimulation(Scalar time_slice)
{
Check(this);
if (!ComputeTargetTwist())
{
currentTwist = targetTwist;
}
else
{
currentTwist =
Lerp(
currentTwist,
targetTwist,
time_slice / ((lastUpdateTime - lastPerformance) + time_slice)
);
}
if (targetTwist > horizontalLimitLeft)
{
targetTwist = horizontalLimitLeft;
}
if (targetTwist < horizontalLimitRight)
{
targetTwist = horizontalLimitRight;
}
Check_Fpu();
}
+232 -209
View File
@@ -1,209 +1,232 @@
//===========================================================================// //===========================================================================//
// File: torso.hpp // // File: torso.hpp //
// Project: BattleTech Brick: Mech subsystems // // Project: BattleTech Brick: Mech subsystems //
// Contents: Torso -- the torso twist / weapon-elevation subsystem // // Contents: Torso -- the torso twist / weapon-elevation subsystem //
//---------------------------------------------------------------------------// //---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. // // Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide // // All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================// //===========================================================================//
#if !defined(TORSO_HPP) #if !defined(TORSO_HPP)
# define TORSO_HPP # define TORSO_HPP
# if !defined(POWERSUB_HPP) # if !defined(POWERSUB_HPP)
# include <powersub.hpp> # include <powersub.hpp>
# endif # endif
//##################### Forward Class Declarations ####################### //##################### Forward Class Declarations #######################
class Mech; class Mech;
class Joint; class Joint;
//########################################################################### //###########################################################################
//##################### Torso Model Resource ########################### //##################### Torso Model Resource ###########################
//########################################################################### //###########################################################################
struct Torso__SubsystemResource: struct Torso__SubsystemResource:
public PowerWatcher::SubsystemResource public PowerWatcher::SubsystemResource
{ {
Scalar horizontalRotationPerSecond; Scalar horizontalRotationPerSecond;
Scalar verticalRotationPerSecond; Scalar verticalRotationPerSecond;
Scalar horizontalLimitRight; Scalar horizontalLimitRight;
Scalar horizontalLimitLeft; Scalar horizontalLimitLeft;
Scalar verticalLimitTop; Scalar verticalLimitTop;
Scalar verticalLimitBottom; Scalar verticalLimitBottom;
char torsoHorizontalJoint[32]; char torsoHorizontalJoint[32];
char torsoHorizontalShadowJoint[32]; char torsoHorizontalShadowJoint[32];
Logical torsoHorizontalEnabled; Logical torsoHorizontalEnabled;
Scalar buttonAccelerationPerSecond; Scalar buttonAccelerationPerSecond;
Scalar buttonAccelerationStartValue; Scalar buttonAccelerationStartValue;
}; };
//########################################################################### //###########################################################################
//############################### Torso ################################ //############################### Torso ################################
//########################################################################### //###########################################################################
// //
// Drives torso twist (horizontal) and weapon elevation (vertical). The // Drives torso twist (horizontal) and weapon elevation (vertical). The
// tuning rates/limits come from the resource; the live twist/elevation state // tuning rates/limits come from the resource; the live twist/elevation state
// and the resolved skeleton joints are advanced by the per-frame // and the resolved skeleton joints are advanced by the per-frame
// TorsoSimulation (staged) once the Mech skeleton link is live. // TorsoSimulation (staged) once the Mech skeleton link is live.
// //
class Torso: class Torso:
public PowerWatcher public PowerWatcher
{ {
public: public:
static Derivation ClassDerivations; static Derivation ClassDerivations;
static SharedData DefaultData; static SharedData DefaultData;
typedef void typedef void
(Torso::*Performance)(Scalar time_slice); (Torso::*Performance)(Scalar time_slice);
void void
SetPerformance(Performance performance) SetPerformance(Performance performance)
{ {
Check(this); Check(this);
activePerformance = (Simulation::Performance)performance; activePerformance = (Simulation::Performance)performance;
} }
static Logical static Logical
TestClass(Mech &); TestClass(Mech &);
Logical Logical
TestInstance() const; TestInstance() const;
Scalar Scalar
CurrentTwist() const { Check(this); return currentTwist; } CurrentTwist() const { Check(this); return currentTwist; }
Scalar Scalar
CurrentElevation() const { Check(this); return currentElevation; } CurrentElevation() const { Check(this); return currentElevation; }
Logical Logical
GetHorizontalEnabled() const{ Check(this); return horizontalEnabled; } GetHorizontalEnabled() const{ Check(this); return horizontalEnabled; }
// //
// Analog aim inputs (written by MechControlsMapper::InterpretControls; // Analog aim inputs (written by MechControlsMapper::InterpretControls;
// consumed by TorsoSimulation to slew the twist/elevation each frame). // consumed by TorsoSimulation to slew the twist/elevation each frame).
// //
void void
SetAnalogElevationAxis(Scalar axis) { Check(this); analogElevationAxis = axis; } SetAnalogElevationAxis(Scalar axis) { Check(this); analogElevationAxis = axis; }
void void
SetAnalogTwistAxis(Scalar axis) { Check(this); analogTwistAxis = axis; } SetAnalogTwistAxis(Scalar axis) { Check(this); analogTwistAxis = axis; }
void void
TorsoSimulation(Scalar time_slice); TorsoSimulation(Scalar time_slice);
void //
TorsoCopySimulation(Scalar time_slice); // The extrapolation helper (binary @004b6510): predict targetTwist
// from the last update snapshot, clamp to the twist limits, return
public: // True while the copy is still slewing toward a future command time.
typedef Torso__SubsystemResource SubsystemResource; //
Logical
Torso( ComputeTargetTwist();
Mech *owner,
int subsystem_ID, void
SubsystemResource *subsystem_resource, TorsoCopySimulation(Scalar time_slice);
SharedData &shared_data = DefaultData
); public:
~Torso(); typedef Torso__SubsystemResource SubsystemResource;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Torso(
// Attribute publication. The shipped string pool lists this class's Mech *owner,
// attribute names contiguously after the class name "Torso": int subsystem_ID,
// SubsystemResource *subsystem_resource,
// RotationOfTorsoVertical RotationOfTorsoHorizontal SharedData &shared_data = DefaultData
// HorizontalLimitRight HorizontalLimitLeft );
// SpeedOfTorsoVertical SpeedOfTorsoHorizontal ~Torso();
//
// The FULL authentic table, ids 3..15, confirmed twice over: the BT411 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// donor's decompiled enum carries these names WITH binary member offsets // Attribute publication. The shipped string pool lists this class's
// (torso.hpp 111-124, @0x1E4..@0x20C), and BTL4.RES's own streamed // attribute names contiguously after the class name "Torso":
// control mappings bind ids 12/13 as DIRECT button destinations on //
// subsystem 17 = Torso (the [map] audit). // RotationOfTorsoVertical RotationOfTorsoHorizontal
// // HorizontalLimitRight HorizontalLimitLeft
// An earlier revision declared "TorsoUp/Down/Left/Right/Center are // SpeedOfTorsoVertical SpeedOfTorsoHorizontal
// MESSAGES, not attributes" and stopped at 7 entries with MotionState at //
// id 9. That was WRONG in a specifically nasty way: the ids are the // The FULL authentic table, ids 3..15, confirmed twice over: the BT411
// binding key for the streamed mappings, so Thrustmaster's buttons // donor's decompiled enum carries these names WITH binary member offsets
// (12/13) resolved NULL -- the boot write-fault at // (torso.hpp 111-124, @0x1E4..@0x20C), and BTL4.RES's own streamed
// ControlsInstanceDirectOf<int>::Update+0xE -- and the RIO's ANALOG // control mappings bind ids 12/13 as DIRECT button destinations on
// StickPosition (9) resolved to our motionState StateIndicator, letting // subsystem 17 = Torso (the [map] audit).
// every analog update from a live RIO smash a watcher-socketed object //
// with raw floats. // An earlier revision declared "TorsoUp/Down/Left/Right/Center are
// // MESSAGES, not attributes" and stopped at 7 entries with MotionState at
// The chain -- PowerWatcher -> HeatWatcher -> MechSubsystem -- publishes // id 9. That was WRONG in a specifically nasty way: the ids are the
// nothing of its own, so these start at MechSubsystem::NextAttributeID. // binding key for the streamed mappings, so Thrustmaster's buttons
// // (12/13) resolved NULL -- the boot write-fault at
public: // ControlsInstanceDirectOf<int>::Update+0xE -- and the RIO's ANALOG
enum { // StickPosition (9) resolved to our motionState StateIndicator, letting
RotationOfTorsoVerticalAttributeID = MechSubsystem::NextAttributeID, // every analog update from a live RIO smash a watcher-socketed object
RotationOfTorsoHorizontalAttributeID, // with raw floats.
HorizontalLimitRightAttributeID, //
HorizontalLimitLeftAttributeID, // The chain -- PowerWatcher -> HeatWatcher -> MechSubsystem -- publishes
SpeedOfTorsoVerticalAttributeID, // nothing of its own, so these start at MechSubsystem::NextAttributeID.
SpeedOfTorsoHorizontalAttributeID, //
StickPositionAttributeID, public:
TorsoUpAttributeID, enum {
TorsoDownAttributeID, RotationOfTorsoVerticalAttributeID = MechSubsystem::NextAttributeID,
TorsoLeftAttributeID, RotationOfTorsoHorizontalAttributeID,
TorsoRightAttributeID, HorizontalLimitRightAttributeID,
TorsoCenterAttributeID, HorizontalLimitLeftAttributeID,
MotionStateAttributeID, SpeedOfTorsoVerticalAttributeID,
NextAttributeID SpeedOfTorsoHorizontalAttributeID,
}; StickPositionAttributeID,
TorsoUpAttributeID,
static const IndexEntry AttributePointers[]; TorsoDownAttributeID,
static AttributeIndexSet AttributeIndex; TorsoLeftAttributeID,
TorsoRightAttributeID,
protected: TorsoCenterAttributeID,
int isDamagedCopy; MotionStateAttributeID,
int statusFlags; NextAttributeID
Logical horizontalEnabled; };
Scalar baseTwistRate;
Scalar baseElevationRate; static const IndexEntry AttributePointers[];
Scalar horizontalLimitRight; static AttributeIndexSet AttributeIndex;
Scalar horizontalLimitLeft;
Scalar verticalLimitTop; protected:
Scalar verticalLimitBottom; int isDamagedCopy;
Scalar twistCenterHigh; int statusFlags;
Scalar twistCenterLow; Logical horizontalEnabled;
Scalar elevationCenter; Scalar baseTwistRate;
Scalar elevationHalfBottom; Scalar baseElevationRate;
Scalar buttonAccelerationPerSecond; Scalar horizontalLimitRight;
Scalar buttonAccelerationStart; Scalar horizontalLimitLeft;
int buttonRampActive; Scalar verticalLimitTop;
Scalar buttonRamp; Scalar verticalLimitBottom;
Joint *horizontalJointNode; Scalar twistCenterHigh;
Joint *horizontalShadowJointNode; Scalar twistCenterLow;
Scalar currentTwist; Scalar elevationCenter;
Scalar currentElevation; Scalar elevationHalfBottom;
Scalar analogElevationAxis; Scalar buttonAccelerationPerSecond;
Scalar analogTwistAxis; Scalar buttonAccelerationStart;
// int buttonRampActive;
// MUST BE A StateIndicator. The authored watcher for a *State Scalar buttonRamp;
// name is an AudioStateWatcher, which is AudioWatcherOf<StateIndicator> Joint *horizontalJointNode;
// and whose ctor immediately runs Joint *horizontalShadowJointNode;
// Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this) Scalar currentTwist;
// (AUDWTHR.CPP:891). Point it at a plain int and that call goes Scalar currentElevation;
// through garbage -- the pod dies on the extender. Scalar analogElevationAxis;
// Scalar analogTwistAxis;
StateIndicator motionState; // authored watcher //
// // MUST BE A StateIndicator. The authored watcher for a *State
// The five BUTTON COMMAND destinations (donor torso.hpp @0x1F8-0x208). // name is an AudioStateWatcher, which is AudioWatcherOf<StateIndicator>
// The streamed control mappings bind these BY ID as DIRECT WRITE // and whose ctor immediately runs
// targets -- LBE4ControlsManager::CreateStreamedMappings registers // Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this)
// GetAttributePointer(attributeID) as the destination a device poll // (AUDWTHR.CPP:891). Point it at a plain int and that call goes
// writes through (L4CTRL.CPP:1936). Carved from the front of the // through garbage -- the pod dies on the extender.
// dynamicsState reserve so the class size is unchanged. //
// StateIndicator motionState; // authored watcher
int elevateUpCommand; //
int elevateDownCommand; // The five BUTTON COMMAND destinations (donor torso.hpp @0x1F8-0x208).
int twistLeftCommand; // The streamed control mappings bind these BY ID as DIRECT WRITE
int twistRightCommand; // targets -- LBE4ControlsManager::CreateStreamedMappings registers
int centerCommand; // GetAttributePointer(attributeID) as the destination a device poll
// // writes through (L4CTRL.CPP:1936). Carved from the front of the
// Recenter latch (donor @0x268-adjacent family): armed by the centre // dynamicsState reserve so the class size is unchanged.
// button, cleared by a twist button or on arrival at centre. //
// int elevateUpCommand;
int recenterActive; int elevateDownCommand;
// int twistLeftCommand;
// Twist/elevation dynamics accumulators advanced by TorsoSimulation. int twistRightCommand;
// Reserved until the per-frame sim is reconstructed (phase 5). int centerCommand;
// //
Scalar dynamicsState[16]; // Recenter latch (donor @0x268-adjacent family): armed by the centre
}; // button, cleared by a twist button or on arrival at centre.
//
#endif int recenterActive;
//
// TWIST REPLICATION (binary @0x218/@0x21C/@0x238/@0x254) -- the
// damaged-copy dead-reckoning state. A console update stamps a twist
// snapshot (twistAtUpdate) + rate (twistRate) + its command time
// (lastUpdateTime); ComputeTargetTwist extrapolates targetTwist from
// them and TorsoCopySimulation eases currentTwist onto it. The
// UPDATE-RECORD feeders (Read/WriteUpdateRecord, torso.cpp census
// remainder) are still staged -- zero-init means a copy eases to
// centre, which is correct-by-vacuity until MP updates arrive.
// Carved from the dynamicsState reserve; class size unchanged.
//
Scalar targetTwist;
Scalar twistAtUpdate;
Scalar twistRate;
Time lastUpdateTime;
//
// Twist/elevation dynamics accumulators advanced by TorsoSimulation.
// Reserved until the per-frame sim is reconstructed (phase 5).
//
Scalar dynamicsState[12];
};
#endif
+93 -55
View File
@@ -1,55 +1,93 @@
# TORSO.CPP / .HPP — reconstruction notes # TORSO.CPP / .HPP — reconstruction notes
`Torso` (: PowerWatcher) drives torso twist (horizontal) and weapon elevation `Torso` (: PowerWatcher) drives torso twist (horizontal) and weapon elevation
(vertical). Tuning rates/limits stream from the resource (vertical). Tuning rates/limits stream from the resource
(`horizontal/verticalRotationPerSecond`, `horizontal/verticalLimit*`, (`horizontal/verticalRotationPerSecond`, `horizontal/verticalLimit*`,
`torsoHorizontalEnabled`). `torsoHorizontalEnabled`).
## Reconstructed ## Reconstructed
- Ctor: streams the base rates (deg→rad) + limits + `horizontalEnabled`, primes - Ctor: streams the base rates (deg→rad) + limits + `horizontalEnabled`, primes
`currentTwist`/`currentElevation`/analog axes to 0, and installs `currentTwist`/`currentElevation`/analog axes to 0, and installs
`TorsoSimulation` as the per-frame Performance (the replicant copy is `TorsoSimulation` as the per-frame Performance (the replicant copy is
console-driven via `TorsoCopySimulation`, still staged). console-driven via `TorsoCopySimulation`, still staged).
- `analogElevationAxis` / `analogTwistAxis` members + `SetAnalogElevationAxis` / - `analogElevationAxis` / `analogTwistAxis` members + `SetAnalogElevationAxis` /
`SetAnalogTwistAxis` setters (written by `MechControlsMapper::InterpretControls`). `SetAnalogTwistAxis` setters (written by `MechControlsMapper::InterpretControls`).
- `CurrentTwist()` / `CurrentElevation()` / `GetHorizontalEnabled()` accessors. - `CurrentTwist()` / `CurrentElevation()` / `GetHorizontalEnabled()` accessors.
- `TorsoSimulation(Scalar)` — per-frame aim integration (authentic core): - `TorsoSimulation(Scalar)` — per-frame aim integration (authentic core):
- `currentElevation += analogElevationAxis · baseElevationRate · dt`, clamped - `currentElevation += analogElevationAxis · baseElevationRate · dt`, clamped
to [verticalLimitBottom, verticalLimitTop]; to [verticalLimitBottom, verticalLimitTop];
- if `horizontalEnabled`: `currentTwist += analogTwistAxis · baseTwistRate · - if `horizontalEnabled`: `currentTwist += analogTwistAxis · baseTwistRate ·
dt`, clamped to [horizontalLimitLeft, horizontalLimitRight]. dt`, clamped to [horizontalLimitLeft, horizontalLimitRight].
**VERIFIED** (`BT_MECH_LOG` `[mppr] torsoElev=`, forced-input `BT_FORCE_ELEV`): **VERIFIED** (`BT_MECH_LOG` `[mppr] torsoElev=`, forced-input `BT_FORCE_ELEV`):
stick pitch 0.8 slews the elevation up and clamps at the authentic stick pitch 0.8 slews the elevation up and clamps at the authentic
`verticalLimitTop` = 0.349 rad = **20°**. Zero Fail. `verticalLimitTop` = 0.349 rad = **20°**. Zero Fail.
## Skeleton-joint binding (2026-07-21) ## Skeleton-joint binding (2026-07-21)
The twist joints are now RESOLVED and BOUND: the ctor calls The twist joints are now RESOLVED and BOUND: the ctor calls
`owner->ResolveJoint(torsoHorizontalJoint / torsoHorizontalShadowJoint)` (the `owner->ResolveJoint(torsoHorizontalJoint / torsoHorizontalShadowJoint)` (the
skeleton is live — see MECH.NOTES.md), stores them as `Joint*`, and skeleton is live — see MECH.NOTES.md), stores them as `Joint*`, and
`TorsoSimulation` pushes `currentTwist` onto `horizontalJointNode` via `TorsoSimulation` pushes `currentTwist` onto `horizontalJointNode` via
`Joint::SetRotation` (hinge → `Radian`; ball → `EulerAngles` yaw). `Joint::SetRotation` (hinge → `Radian`; ball → `EulerAngles` yaw).
The bring-up TEST.EGG mech has a FIXED torso (`horizJoint=''`, `enabled=0`), so The bring-up TEST.EGG mech has a FIXED torso (`horizJoint=''`, `enabled=0`), so
its twist path is inert — correctly guarded (null joint → no apply, no crash). A its twist path is inert — correctly guarded (null joint → no apply, no crash). A
torso-twist mech drives the joint. Verified headlessly; the VISUAL rotation torso-twist mech drives the joint. Verified headlessly; the VISUAL rotation
needs the renderer. needs the renderer.
## Elevation does NOT drive a gun joint (corrected 2026-07-21) ## Elevation does NOT drive a gun joint (corrected 2026-07-21)
Checked BT411 before reconstructing a "gun-elevation joint": no such joint Checked BT411 before reconstructing a "gun-elevation joint": no such joint
binding exists anywhere in the authentic engine. `currentElevation` instead binding exists anywhere in the authentic engine. `currentElevation` instead
composes into `Mech::eyepointRotation` (the cockpit-eye / weapon-boresight composes into `Mech::eyepointRotation` (the cockpit-eye / weapon-boresight
pitch) — see MECH.NOTES.md "PHASE 5.3 INCREMENT 5". `TorsoSimulation` only pitch) — see MECH.NOTES.md "PHASE 5.3 INCREMENT 5". `TorsoSimulation` only
needed to keep advancing the scalar state, which it already did; the missing needed to keep advancing the scalar state, which it already did; the missing
piece was the CONSUMER (`Mech::Simulate`'s eyepoint composition), now piece was the CONSUMER (`Mech::Simulate`'s eyepoint composition), now
reconstructed and verified (`torsoElev` == `eyePitch` every frame). reconstructed and verified (`torsoElev` == `eyePitch` every frame).
## Deferred ## TorsoCopySimulation + ComputeTargetTwist (2026-08-04, 5.3.114)
- HUD free-aim slew, the look-button state machine (`BTCommitLookState` / The replicant (damaged-copy) twist path is real, verified line-by-line against
`gBTLookPitch`/`gBTLookYaw` — composes with elevation into the full the raw decomp (part_013.c):
eyepoint), `TorsoCopySimulation` (replicant console-driven aim) — still
staged. - `ComputeTargetTwist` @004b6510 (232B): two time bases — settled uses
`lastPerformance - lastUpdate` (the engine `Simulation` fields at +0x10/+0x14),
slewing (a copy whose command stamp `lastUpdateTime` @0x254 is ahead of the
clock) uses `lastUpdateTime - lastUpdate`. `targetTwist = twistAtUpdate +
twistRate * elapsed`, clamped (left = upper, right = lower). Returns the
slewing flag. The decomp's `DAT_0052140c` divide is `Time::operator-`
(ticks / SystemClock::ticksPerSecond) inlined — NOT a hand-written
MsPerSecond conversion.
- `TorsoCopySimulation` @004b65f8 (188B): settled → snap to target; slewing →
`Lerp(current, target, dt / ((lastUpdateTime - lastPerformance) + dt))` — the
remaining-time ease that lands exactly when the clock reaches the command
stamp. Then the same clamp pair applied to `targetTwist` again.
**Two BT411 donor drifts corrected against the decomp**: the donor's ease aged
by `lastUpdate` (+0x14) where the binary reads `lastPerformance` (+0x10); and
the donor calls the joint-write helper (@004b67ec) from the copy sim — the
binary does not (port-era addition; our copy sim leaves joints to the master
path exactly as 1995 shipped).
Members carved from `dynamicsState[16]` → `[12]`: `targetTwist` @0x218,
`twistAtUpdate` @0x21C, `twistRate` @0x238, `lastUpdateTime` @0x254 (`Time`,
init `0L` — plain `0` is ambiguous between the long/float assignment operators
under BC4.52). Ctor now registers the copy Performance on replicants
(binary PTR @00510c1c). [T2] The registration gate mirrors the binary's
`(owner->simulationFlags & 0xC) == 0 && (flags & 0x100) != 0` with the
instance test, consistent with the whole watcher family, until the mech
stream builders (which seed those flag bits) are reconstructed.
**Staged feeders**: `Read/WriteUpdateRecord` (the census remainder in this TU)
stamp `twistAtUpdate`/`twistRate`/`lastUpdateTime` from console updates.
Zero-init means a copy eases to centre — correct-by-vacuity in single-pod,
inert until MP replication lands.
## Deferred
- HUD free-aim slew, the look-button state machine (`BTCommitLookState` /
`gBTLookPitch`/`gBTLookYaw` — composes with elevation into the full
eyepoint) — still staged.
- `Torso::Read/WriteUpdateRecord` — the copy sim's update feeders (see above).