diff --git a/restoration/source410/BT/TORSO.CPP b/restoration/source410/BT/TORSO.CPP index bdb9bd6f..da3ca778 100644 --- a/restoration/source410/BT/TORSO.CPP +++ b/restoration/source410/BT/TORSO.CPP @@ -1,398 +1,514 @@ -//===========================================================================// -// File: torso.cpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: Torso -- torso twist / weapon elevation // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(TORSO_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -#if !defined(JOINT_HPP) -# include -#endif - -Derivation - Torso::ClassDerivations( - PowerWatcher::ClassDerivations, - "Torso" - ); - -// -// 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 -// binding ids 12/13 on subsystem 17. The five command members and -// analogTwistAxis are DIRECT WRITE DESTINATIONS for device polls -// (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 -// stick on the motionState StateIndicator. -// -// SpeedOf* still point at the base rates rather than the donor's live -// velocity accumulators (elevationVelocity/twistVelocity @0x1EC/0x1E8) -- -// those members arrive with TorsoSimulation; the authored audio watcher on -// SpeedOfTorsoHorizontal just reads a constant until then. -// -const Torso::IndexEntry - Torso::AttributePointers[]= -{ - ATTRIBUTE_ENTRY(Torso, RotationOfTorsoVertical, currentElevation), - ATTRIBUTE_ENTRY(Torso, RotationOfTorsoHorizontal, currentTwist), - ATTRIBUTE_ENTRY(Torso, HorizontalLimitRight, horizontalLimitRight), - ATTRIBUTE_ENTRY(Torso, HorizontalLimitLeft, horizontalLimitLeft), - ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoVertical, baseElevationRate), - ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoHorizontal, baseTwistRate), - ATTRIBUTE_ENTRY(Torso, StickPosition, analogTwistAxis), - ATTRIBUTE_ENTRY(Torso, TorsoUp, elevateUpCommand), - ATTRIBUTE_ENTRY(Torso, TorsoDown, elevateDownCommand), - ATTRIBUTE_ENTRY(Torso, TorsoLeft, twistLeftCommand), - ATTRIBUTE_ENTRY(Torso, TorsoRight, twistRightCommand), - ATTRIBUTE_ENTRY(Torso, TorsoCenter, centerCommand), - ATTRIBUTE_ENTRY(Torso, MotionState, motionState) -}; - -Torso::AttributeIndexSet - Torso::AttributeIndex( - ELEMENTS(Torso::AttributePointers), - Torso::AttributePointers, - MechSubsystem::AttributeIndex - ); - -Torso::SharedData - Torso::DefaultData( - Torso::ClassDerivations, - Subsystem::MessageHandlers, - Torso::AttributeIndex, - Subsystem::StateCount - ); - -Torso::Torso( - Mech *owner, - int subsystem_ID, - SubsystemResource *r, - SharedData &shared_data -): - PowerWatcher(owner, subsystem_ID, r, shared_data) -{ - Check(owner); - Check_Pointer(r); - - isDamagedCopy = (owner->GetInstance() == Entity::ReplicantInstance); - - statusFlags = 0; - buttonAccelerationPerSecond = r->buttonAccelerationPerSecond; - buttonAccelerationStart = r->buttonAccelerationStartValue; - - baseTwistRate = r->horizontalRotationPerSecond * RAD_PER_DEG; - baseElevationRate = r->verticalRotationPerSecond * RAD_PER_DEG; - horizontalLimitRight= r->horizontalLimitRight * RAD_PER_DEG; - horizontalLimitLeft = r->horizontalLimitLeft * RAD_PER_DEG; - verticalLimitTop = r->verticalLimitTop * RAD_PER_DEG; - verticalLimitBottom = r->verticalLimitBottom * RAD_PER_DEG; - - twistCenterHigh = verticalLimitTop; - twistCenterLow = verticalLimitBottom; - elevationCenter = verticalLimitTop; - elevationHalfBottom = verticalLimitBottom * 0.5f; - - buttonRampActive = 0; - buttonRamp = 0.0f; - horizontalEnabled = r->torsoHorizontalEnabled; - - // - // The device-poll write destinations (published ids 9..14). Zero = - // stick centred, no button held -- their state before the first poll. - // - elevateUpCommand = 0; - elevateDownCommand = 0; - twistLeftCommand = 0; - twistRightCommand = 0; - centerCommand = 0; - recenterActive = 0; - - // - // 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 - // joint name is absent from the skeleton, resolves NULL and simply never - // twists. - // - horizontalJointNode = owner->ResolveJoint(r->torsoHorizontalJoint); - horizontalShadowJointNode = owner->ResolveJoint(r->torsoHorizontalShadowJoint); - - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[torso] horizJoint '" << r->torsoHorizontalJoint - << "' -> " << (void *)horizontalJointNode; - if (horizontalJointNode != NULL) - { - DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType(); - } - DEBUG_STREAM << " enabled=" << (int)horizontalEnabled << endl << flush; - } - - currentTwist = 0.0f; - currentElevation = 0.0f; - analogElevationAxis = 0.0f; - analogTwistAxis = 0.0f; - // - // 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 / - // 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 - // makes the next carve harmless. - // - int - i; - for (i = 0; i < (int)ELEMENTS(dynamicsState); ++i) - { - dynamicsState[i] = 0.0f; - } - - // - // Install the per-frame twist/elevation Performance (the replicant copy is - // driven by console updates via TorsoCopySimulation instead, still staged). - // - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&Torso::TorsoSimulation); - } - - Check_Fpu(); -} - -Torso::~Torso() -{ -} - -Logical - Torso::TestClass(Mech &) -{ - return True; -} - -Logical - Torso::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// 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 -// the resource limits. Authentic core (torso.cpp @004b6xxx): current += axis * -// baseRate * dt. APPLYING the resolved angles onto the skeleton torso/gun -// joints (horizontalJointNode / the elevation joint) is deferred with the -// skeleton-link + render wave; here we advance the scalar aim state (read by the -// HUD reticle / weapon aim and, once wired, the joints). -//############################################################################# -// -void - Torso::TorsoSimulation(Scalar time_slice) -{ - Check(this); - - // - // The BUTTON COMMANDS (donor torso.cpp:557, binary @004b5cf0). The five - // command members are the streamed direct mappings' write destinations -- - // the RIO/TM twist and elevation buttons land in them as raw - // ControlsButton ints (positive = held). The donor's ramp machinery is - // carried verbatim, including its punchline: the shipped build - // unconditionally overwrites the ramp with 1.0f, so the acceleration - // ramp is authored dead weight. Kept because it is the binary's shape. - // - Scalar - twist_step = baseTwistRate * time_slice, - elev_step = baseElevationRate * 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 - // 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 - // makes the result visible on the render bridge. The donor carries the - // same hook for the same reason (torso.cpp:583). - // - // Held for ~90 frames each way, matching the donor's period. - // - if (getenv("BT_FORCE_TORSO") != NULL) - { - static int - sweep = 0; - - sweep++; - twistLeftCommand = ((sweep / 90) & 1) ? 1 : 0; - twistRightCommand = ((sweep / 90) & 1) ? 0 : 1; - } - - if (buttonRampActive == 0) - { - buttonRamp = buttonAccelerationStart; - } - else - { - buttonRamp += buttonAccelerationPerSecond * time_slice; - if (buttonRamp > 1.0f) - { - buttonRamp = 1.0f; - } - buttonRampActive = 0; - } - buttonRamp = 1.0f; // @004b5cf0 unconditionally overwrites with 1.0f - - if (elevateUpCommand > 0) - { - currentElevation += elev_step * buttonRamp; - if (currentElevation > verticalLimitTop) - { - currentElevation = verticalLimitTop; - } - buttonRampActive = 1; - } - if (elevateDownCommand > 0) - { - currentElevation -= elev_step * buttonRamp; - if (currentElevation < verticalLimitBottom) - { - currentElevation = verticalLimitBottom; - } - buttonRampActive = 1; - } - - // - // Elevation (weapon pitch): slew toward the analog axis, clamp to limits. - // - currentElevation += analogElevationAxis * baseElevationRate * time_slice; - { - Scalar lo = (verticalLimitBottom < verticalLimitTop) - ? verticalLimitBottom : verticalLimitTop; - Scalar hi = (verticalLimitBottom < verticalLimitTop) - ? verticalLimitTop : verticalLimitBottom; - if (currentElevation < lo) currentElevation = lo; - if (currentElevation > hi) currentElevation = hi; - } - - // - // Twist (horizontal torso rotation): only mechs with an enabled torso joint. - // - if (horizontalEnabled) - { - // - // Digital twist commands, then recenter, then the analog axis -- - // the donor's order. Twist buttons cancel a pending recenter; - // the centre button arms it and it slews home across frames - // (donor Recenter, binary @004b6918). - // - if (twistLeftCommand > 0) - { - currentTwist += twist_step * buttonRamp; - if (currentTwist > horizontalLimitLeft) - { - currentTwist = horizontalLimitLeft; - } - recenterActive = 0; - buttonRampActive = 1; - } - if (twistRightCommand > 0) - { - currentTwist -= twist_step * buttonRamp; - if (currentTwist < horizontalLimitRight) - { - currentTwist = horizontalLimitRight; - } - recenterActive = 0; - buttonRampActive = 1; - } - if (centerCommand > 0) - { - recenterActive = 1; - buttonRampActive = 0; - } - if (recenterActive != 0) - { - if (currentTwist > 0.0f) - { - currentTwist -= twist_step; - if (currentTwist < 0.0f) currentTwist = 0.0f; - } - else if (currentTwist < 0.0f) - { - currentTwist += twist_step; - if (currentTwist > 0.0f) currentTwist = 0.0f; - } - if (currentTwist == 0.0f) - { - recenterActive = 0; - } - } - - if (getenv("BT_TORSO_LOG") != NULL) - { - static int - torso_tick = 0; - - if ((torso_tick++ % 240) == 0) - { - DEBUG_STREAM << "[torso] twist=" << currentTwist - << " elev=" << currentElevation - << " cmdL=" << twistLeftCommand - << " cmdR=" << twistRightCommand - << " axis=" << analogTwistAxis - << " rate=" << baseTwistRate - << " limits=(" << horizontalLimitRight - << ".." << horizontalLimitLeft << ")" - << " joint=" << (horizontalJointNode != NULL ? 1 : 0) - << endl << flush; - } - } - - currentTwist += analogTwistAxis * baseTwistRate * time_slice; - Scalar lo = (horizontalLimitLeft < horizontalLimitRight) - ? horizontalLimitLeft : horizontalLimitRight; - Scalar hi = (horizontalLimitLeft < horizontalLimitRight) - ? horizontalLimitRight : horizontalLimitLeft; - if (currentTwist < lo) currentTwist = lo; - if (currentTwist > hi) currentTwist = hi; - - // - // 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. - // - if (horizontalJointNode != NULL) - { - Joint::JointType jt = horizontalJointNode->GetJointType(); - if (jt == Joint::HingeXJointType - || jt == Joint::HingeYJointType - || jt == Joint::HingeZJointType) - { - horizontalJointNode->SetRotation(Radian(currentTwist)); - } - else if (jt == Joint::BallJointType) - { - horizontalJointNode->SetRotation(EulerAngles(0.0f, currentTwist, 0.0f)); - } - } - } - - Check_Fpu(); -} - -void - Torso::TorsoCopySimulation(Scalar) -{ - Fail("Torso::TorsoCopySimulation -- torso.cpp not yet reconstructed"); -} +//===========================================================================// +// File: torso.cpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Torso -- torso twist / weapon elevation // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(TORSO_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#if !defined(JOINT_HPP) +# include +#endif + +Derivation + Torso::ClassDerivations( + PowerWatcher::ClassDerivations, + "Torso" + ); + +// +// 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 +// binding ids 12/13 on subsystem 17. The five command members and +// analogTwistAxis are DIRECT WRITE DESTINATIONS for device polls +// (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 +// stick on the motionState StateIndicator. +// +// SpeedOf* still point at the base rates rather than the donor's live +// velocity accumulators (elevationVelocity/twistVelocity @0x1EC/0x1E8) -- +// those members arrive with TorsoSimulation; the authored audio watcher on +// SpeedOfTorsoHorizontal just reads a constant until then. +// +const Torso::IndexEntry + Torso::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Torso, RotationOfTorsoVertical, currentElevation), + ATTRIBUTE_ENTRY(Torso, RotationOfTorsoHorizontal, currentTwist), + ATTRIBUTE_ENTRY(Torso, HorizontalLimitRight, horizontalLimitRight), + ATTRIBUTE_ENTRY(Torso, HorizontalLimitLeft, horizontalLimitLeft), + ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoVertical, baseElevationRate), + ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoHorizontal, baseTwistRate), + ATTRIBUTE_ENTRY(Torso, StickPosition, analogTwistAxis), + ATTRIBUTE_ENTRY(Torso, TorsoUp, elevateUpCommand), + ATTRIBUTE_ENTRY(Torso, TorsoDown, elevateDownCommand), + ATTRIBUTE_ENTRY(Torso, TorsoLeft, twistLeftCommand), + ATTRIBUTE_ENTRY(Torso, TorsoRight, twistRightCommand), + ATTRIBUTE_ENTRY(Torso, TorsoCenter, centerCommand), + ATTRIBUTE_ENTRY(Torso, MotionState, motionState) +}; + +Torso::AttributeIndexSet + Torso::AttributeIndex( + ELEMENTS(Torso::AttributePointers), + Torso::AttributePointers, + MechSubsystem::AttributeIndex + ); + +Torso::SharedData + Torso::DefaultData( + Torso::ClassDerivations, + Subsystem::MessageHandlers, + Torso::AttributeIndex, + Subsystem::StateCount + ); + +Torso::Torso( + Mech *owner, + int subsystem_ID, + SubsystemResource *r, + SharedData &shared_data +): + PowerWatcher(owner, subsystem_ID, r, shared_data) +{ + Check(owner); + Check_Pointer(r); + + isDamagedCopy = (owner->GetInstance() == Entity::ReplicantInstance); + + statusFlags = 0; + buttonAccelerationPerSecond = r->buttonAccelerationPerSecond; + buttonAccelerationStart = r->buttonAccelerationStartValue; + + baseTwistRate = r->horizontalRotationPerSecond * RAD_PER_DEG; + baseElevationRate = r->verticalRotationPerSecond * RAD_PER_DEG; + horizontalLimitRight= r->horizontalLimitRight * RAD_PER_DEG; + horizontalLimitLeft = r->horizontalLimitLeft * RAD_PER_DEG; + verticalLimitTop = r->verticalLimitTop * RAD_PER_DEG; + verticalLimitBottom = r->verticalLimitBottom * RAD_PER_DEG; + + twistCenterHigh = verticalLimitTop; + twistCenterLow = verticalLimitBottom; + elevationCenter = verticalLimitTop; + elevationHalfBottom = verticalLimitBottom * 0.5f; + + buttonRampActive = 0; + buttonRamp = 0.0f; + horizontalEnabled = r->torsoHorizontalEnabled; + + // + // The device-poll write destinations (published ids 9..14). Zero = + // stick centred, no button held -- their state before the first poll. + // + elevateUpCommand = 0; + elevateDownCommand = 0; + twistLeftCommand = 0; + twistRightCommand = 0; + centerCommand = 0; + recenterActive = 0; + + // + // 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 + // joint name is absent from the skeleton, resolves NULL and simply never + // twists. + // + horizontalJointNode = owner->ResolveJoint(r->torsoHorizontalJoint); + horizontalShadowJointNode = owner->ResolveJoint(r->torsoHorizontalShadowJoint); + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[torso] horizJoint '" << r->torsoHorizontalJoint + << "' -> " << (void *)horizontalJointNode; + if (horizontalJointNode != NULL) + { + DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType(); + } + DEBUG_STREAM << " enabled=" << (int)horizontalEnabled << endl << flush; + } + + currentTwist = 0.0f; + currentElevation = 0.0f; + analogElevationAxis = 0.0f; + analogTwistAxis = 0.0f; + // + // 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 / + // 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 + // makes the next carve harmless. + // + int + i; + for (i = 0; i < (int)ELEMENTS(dynamicsState); ++i) + { + dynamicsState[i] = 0.0f; + } + + // + // Twist-replication state (binary @0x218/@0x21C/@0x238/@0x254) -- see the + // header note. Zero until an update record stamps it. + // + targetTwist = 0.0f; + twistAtUpdate = 0.0f; + twistRate = 0.0f; + lastUpdateTime = 0L; + + // + // Install the per-frame Performance. The binary's gate @004b6b0c is + // (owner->simulationFlags & 0xC) == 0 && (owner->simulationFlags & 0x100) + // != 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. + // + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Torso::TorsoSimulation); + } + else + { + SetPerformance(&Torso::TorsoCopySimulation); // binary PTR @00510c1c + } + + Check_Fpu(); +} + +Torso::~Torso() +{ +} + +Logical + Torso::TestClass(Mech &) +{ + return True; +} + +Logical + Torso::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// 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 +// the resource limits. Authentic core (torso.cpp @004b6xxx): current += axis * +// baseRate * dt. APPLYING the resolved angles onto the skeleton torso/gun +// joints (horizontalJointNode / the elevation joint) is deferred with the +// skeleton-link + render wave; here we advance the scalar aim state (read by the +// HUD reticle / weapon aim and, once wired, the joints). +//############################################################################# +// +void + Torso::TorsoSimulation(Scalar time_slice) +{ + Check(this); + + // + // The BUTTON COMMANDS (donor torso.cpp:557, binary @004b5cf0). The five + // command members are the streamed direct mappings' write destinations -- + // the RIO/TM twist and elevation buttons land in them as raw + // ControlsButton ints (positive = held). The donor's ramp machinery is + // carried verbatim, including its punchline: the shipped build + // unconditionally overwrites the ramp with 1.0f, so the acceleration + // ramp is authored dead weight. Kept because it is the binary's shape. + // + Scalar + twist_step = baseTwistRate * time_slice, + elev_step = baseElevationRate * 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 + // 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 + // makes the result visible on the render bridge. The donor carries the + // same hook for the same reason (torso.cpp:583). + // + // Held for ~90 frames each way, matching the donor's period. + // + if (getenv("BT_FORCE_TORSO") != NULL) + { + static int + sweep = 0; + + sweep++; + twistLeftCommand = ((sweep / 90) & 1) ? 1 : 0; + twistRightCommand = ((sweep / 90) & 1) ? 0 : 1; + } + + if (buttonRampActive == 0) + { + buttonRamp = buttonAccelerationStart; + } + else + { + buttonRamp += buttonAccelerationPerSecond * time_slice; + if (buttonRamp > 1.0f) + { + buttonRamp = 1.0f; + } + buttonRampActive = 0; + } + buttonRamp = 1.0f; // @004b5cf0 unconditionally overwrites with 1.0f + + if (elevateUpCommand > 0) + { + currentElevation += elev_step * buttonRamp; + if (currentElevation > verticalLimitTop) + { + currentElevation = verticalLimitTop; + } + buttonRampActive = 1; + } + if (elevateDownCommand > 0) + { + currentElevation -= elev_step * buttonRamp; + if (currentElevation < verticalLimitBottom) + { + currentElevation = verticalLimitBottom; + } + buttonRampActive = 1; + } + + // + // Elevation (weapon pitch): slew toward the analog axis, clamp to limits. + // + currentElevation += analogElevationAxis * baseElevationRate * time_slice; + { + Scalar lo = (verticalLimitBottom < verticalLimitTop) + ? verticalLimitBottom : verticalLimitTop; + Scalar hi = (verticalLimitBottom < verticalLimitTop) + ? verticalLimitTop : verticalLimitBottom; + if (currentElevation < lo) currentElevation = lo; + if (currentElevation > hi) currentElevation = hi; + } + + // + // Twist (horizontal torso rotation): only mechs with an enabled torso joint. + // + if (horizontalEnabled) + { + // + // Digital twist commands, then recenter, then the analog axis -- + // the donor's order. Twist buttons cancel a pending recenter; + // the centre button arms it and it slews home across frames + // (donor Recenter, binary @004b6918). + // + if (twistLeftCommand > 0) + { + currentTwist += twist_step * buttonRamp; + if (currentTwist > horizontalLimitLeft) + { + currentTwist = horizontalLimitLeft; + } + recenterActive = 0; + buttonRampActive = 1; + } + if (twistRightCommand > 0) + { + currentTwist -= twist_step * buttonRamp; + if (currentTwist < horizontalLimitRight) + { + currentTwist = horizontalLimitRight; + } + recenterActive = 0; + buttonRampActive = 1; + } + if (centerCommand > 0) + { + recenterActive = 1; + buttonRampActive = 0; + } + if (recenterActive != 0) + { + if (currentTwist > 0.0f) + { + currentTwist -= twist_step; + if (currentTwist < 0.0f) currentTwist = 0.0f; + } + else if (currentTwist < 0.0f) + { + currentTwist += twist_step; + if (currentTwist > 0.0f) currentTwist = 0.0f; + } + if (currentTwist == 0.0f) + { + recenterActive = 0; + } + } + + if (getenv("BT_TORSO_LOG") != NULL) + { + static int + torso_tick = 0; + + if ((torso_tick++ % 240) == 0) + { + DEBUG_STREAM << "[torso] twist=" << currentTwist + << " elev=" << currentElevation + << " cmdL=" << twistLeftCommand + << " cmdR=" << twistRightCommand + << " axis=" << analogTwistAxis + << " rate=" << baseTwistRate + << " limits=(" << horizontalLimitRight + << ".." << horizontalLimitLeft << ")" + << " joint=" << (horizontalJointNode != NULL ? 1 : 0) + << endl << flush; + } + } + + currentTwist += analogTwistAxis * baseTwistRate * time_slice; + Scalar lo = (horizontalLimitLeft < horizontalLimitRight) + ? horizontalLimitLeft : horizontalLimitRight; + Scalar hi = (horizontalLimitLeft < horizontalLimitRight) + ? horizontalLimitRight : horizontalLimitLeft; + if (currentTwist < lo) currentTwist = lo; + if (currentTwist > hi) currentTwist = hi; + + // + // 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. + // + if (horizontalJointNode != NULL) + { + Joint::JointType jt = horizontalJointNode->GetJointType(); + if (jt == Joint::HingeXJointType + || jt == Joint::HingeYJointType + || 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(); +} diff --git a/restoration/source410/BT/TORSO.HPP b/restoration/source410/BT/TORSO.HPP index d293e2e7..a1134315 100644 --- a/restoration/source410/BT/TORSO.HPP +++ b/restoration/source410/BT/TORSO.HPP @@ -1,209 +1,232 @@ -//===========================================================================// -// File: torso.hpp // -// Project: BattleTech Brick: Mech subsystems // -// Contents: Torso -- the torso twist / weapon-elevation subsystem // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#if !defined(TORSO_HPP) -# define TORSO_HPP - -# if !defined(POWERSUB_HPP) -# include -# endif - -//##################### Forward Class Declarations ####################### - class Mech; - class Joint; - -//########################################################################### -//##################### Torso Model Resource ########################### -//########################################################################### - - struct Torso__SubsystemResource: - public PowerWatcher::SubsystemResource - { - Scalar horizontalRotationPerSecond; - Scalar verticalRotationPerSecond; - Scalar horizontalLimitRight; - Scalar horizontalLimitLeft; - Scalar verticalLimitTop; - Scalar verticalLimitBottom; - char torsoHorizontalJoint[32]; - char torsoHorizontalShadowJoint[32]; - Logical torsoHorizontalEnabled; - Scalar buttonAccelerationPerSecond; - Scalar buttonAccelerationStartValue; - }; - -//########################################################################### -//############################### Torso ################################ -//########################################################################### -// -// Drives torso twist (horizontal) and weapon elevation (vertical). The -// tuning rates/limits come from the resource; the live twist/elevation state -// and the resolved skeleton joints are advanced by the per-frame -// TorsoSimulation (staged) once the Mech skeleton link is live. -// - class Torso: - public PowerWatcher - { - public: - static Derivation ClassDerivations; - static SharedData DefaultData; - - typedef void - (Torso::*Performance)(Scalar time_slice); - void - SetPerformance(Performance performance) - { - Check(this); - activePerformance = (Simulation::Performance)performance; - } - - static Logical - TestClass(Mech &); - Logical - TestInstance() const; - Scalar - CurrentTwist() const { Check(this); return currentTwist; } - Scalar - CurrentElevation() const { Check(this); return currentElevation; } - Logical - GetHorizontalEnabled() const{ Check(this); return horizontalEnabled; } - - // - // Analog aim inputs (written by MechControlsMapper::InterpretControls; - // consumed by TorsoSimulation to slew the twist/elevation each frame). - // - void - SetAnalogElevationAxis(Scalar axis) { Check(this); analogElevationAxis = axis; } - void - SetAnalogTwistAxis(Scalar axis) { Check(this); analogTwistAxis = axis; } - - void - TorsoSimulation(Scalar time_slice); - void - TorsoCopySimulation(Scalar time_slice); - - public: - typedef Torso__SubsystemResource SubsystemResource; - - Torso( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data = DefaultData - ); - ~Torso(); - - //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Attribute publication. The shipped string pool lists this class's - // attribute names contiguously after the class name "Torso": - // - // RotationOfTorsoVertical RotationOfTorsoHorizontal - // HorizontalLimitRight HorizontalLimitLeft - // SpeedOfTorsoVertical SpeedOfTorsoHorizontal - // - // The FULL authentic table, ids 3..15, confirmed twice over: the BT411 - // donor's decompiled enum carries these names WITH binary member offsets - // (torso.hpp 111-124, @0x1E4..@0x20C), and BTL4.RES's own streamed - // control mappings bind ids 12/13 as DIRECT button destinations on - // subsystem 17 = Torso (the [map] audit). - // - // An earlier revision declared "TorsoUp/Down/Left/Right/Center are - // 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 - // binding key for the streamed mappings, so Thrustmaster's buttons - // (12/13) resolved NULL -- the boot write-fault at - // ControlsInstanceDirectOf::Update+0xE -- and the RIO's ANALOG - // StickPosition (9) resolved to our motionState StateIndicator, letting - // every analog update from a live RIO smash a watcher-socketed object - // with raw floats. - // - // The chain -- PowerWatcher -> HeatWatcher -> MechSubsystem -- publishes - // nothing of its own, so these start at MechSubsystem::NextAttributeID. - // - public: - enum { - RotationOfTorsoVerticalAttributeID = MechSubsystem::NextAttributeID, - RotationOfTorsoHorizontalAttributeID, - HorizontalLimitRightAttributeID, - HorizontalLimitLeftAttributeID, - SpeedOfTorsoVerticalAttributeID, - SpeedOfTorsoHorizontalAttributeID, - StickPositionAttributeID, - TorsoUpAttributeID, - TorsoDownAttributeID, - TorsoLeftAttributeID, - TorsoRightAttributeID, - TorsoCenterAttributeID, - MotionStateAttributeID, - NextAttributeID - }; - - static const IndexEntry AttributePointers[]; - static AttributeIndexSet AttributeIndex; - - protected: - int isDamagedCopy; - int statusFlags; - Logical horizontalEnabled; - Scalar baseTwistRate; - Scalar baseElevationRate; - Scalar horizontalLimitRight; - Scalar horizontalLimitLeft; - Scalar verticalLimitTop; - Scalar verticalLimitBottom; - Scalar twistCenterHigh; - Scalar twistCenterLow; - Scalar elevationCenter; - Scalar elevationHalfBottom; - Scalar buttonAccelerationPerSecond; - Scalar buttonAccelerationStart; - int buttonRampActive; - Scalar buttonRamp; - Joint *horizontalJointNode; - Joint *horizontalShadowJointNode; - Scalar currentTwist; - Scalar currentElevation; - Scalar analogElevationAxis; - Scalar analogTwistAxis; - // - // MUST BE A StateIndicator. The authored watcher for a *State - // name is an AudioStateWatcher, which is AudioWatcherOf - // and whose ctor immediately runs - // Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this) - // (AUDWTHR.CPP:891). Point it at a plain int and that call goes - // through garbage -- the pod dies on the extender. - // - StateIndicator motionState; // authored watcher - // - // The five BUTTON COMMAND destinations (donor torso.hpp @0x1F8-0x208). - // The streamed control mappings bind these BY ID as DIRECT WRITE - // targets -- LBE4ControlsManager::CreateStreamedMappings registers - // GetAttributePointer(attributeID) as the destination a device poll - // writes through (L4CTRL.CPP:1936). Carved from the front of the - // dynamicsState reserve so the class size is unchanged. - // - int elevateUpCommand; - int elevateDownCommand; - int twistLeftCommand; - int twistRightCommand; - int centerCommand; - // - // Recenter latch (donor @0x268-adjacent family): armed by the centre - // button, cleared by a twist button or on arrival at centre. - // - int recenterActive; - // - // Twist/elevation dynamics accumulators advanced by TorsoSimulation. - // Reserved until the per-frame sim is reconstructed (phase 5). - // - Scalar dynamicsState[16]; - }; - -#endif +//===========================================================================// +// File: torso.hpp // +// Project: BattleTech Brick: Mech subsystems // +// Contents: Torso -- the torso twist / weapon-elevation subsystem // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#if !defined(TORSO_HPP) +# define TORSO_HPP + +# if !defined(POWERSUB_HPP) +# include +# endif + +//##################### Forward Class Declarations ####################### + class Mech; + class Joint; + +//########################################################################### +//##################### Torso Model Resource ########################### +//########################################################################### + + struct Torso__SubsystemResource: + public PowerWatcher::SubsystemResource + { + Scalar horizontalRotationPerSecond; + Scalar verticalRotationPerSecond; + Scalar horizontalLimitRight; + Scalar horizontalLimitLeft; + Scalar verticalLimitTop; + Scalar verticalLimitBottom; + char torsoHorizontalJoint[32]; + char torsoHorizontalShadowJoint[32]; + Logical torsoHorizontalEnabled; + Scalar buttonAccelerationPerSecond; + Scalar buttonAccelerationStartValue; + }; + +//########################################################################### +//############################### Torso ################################ +//########################################################################### +// +// Drives torso twist (horizontal) and weapon elevation (vertical). The +// tuning rates/limits come from the resource; the live twist/elevation state +// and the resolved skeleton joints are advanced by the per-frame +// TorsoSimulation (staged) once the Mech skeleton link is live. +// + class Torso: + public PowerWatcher + { + public: + static Derivation ClassDerivations; + static SharedData DefaultData; + + typedef void + (Torso::*Performance)(Scalar time_slice); + void + SetPerformance(Performance performance) + { + Check(this); + activePerformance = (Simulation::Performance)performance; + } + + static Logical + TestClass(Mech &); + Logical + TestInstance() const; + Scalar + CurrentTwist() const { Check(this); return currentTwist; } + Scalar + CurrentElevation() const { Check(this); return currentElevation; } + Logical + GetHorizontalEnabled() const{ Check(this); return horizontalEnabled; } + + // + // Analog aim inputs (written by MechControlsMapper::InterpretControls; + // consumed by TorsoSimulation to slew the twist/elevation each frame). + // + void + SetAnalogElevationAxis(Scalar axis) { Check(this); analogElevationAxis = axis; } + void + SetAnalogTwistAxis(Scalar axis) { Check(this); analogTwistAxis = axis; } + + void + TorsoSimulation(Scalar time_slice); + // + // The extrapolation helper (binary @004b6510): predict targetTwist + // from the last update snapshot, clamp to the twist limits, return + // True while the copy is still slewing toward a future command time. + // + Logical + ComputeTargetTwist(); + + void + TorsoCopySimulation(Scalar time_slice); + + public: + typedef Torso__SubsystemResource SubsystemResource; + + Torso( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data = DefaultData + ); + ~Torso(); + + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Attribute publication. The shipped string pool lists this class's + // attribute names contiguously after the class name "Torso": + // + // RotationOfTorsoVertical RotationOfTorsoHorizontal + // HorizontalLimitRight HorizontalLimitLeft + // SpeedOfTorsoVertical SpeedOfTorsoHorizontal + // + // The FULL authentic table, ids 3..15, confirmed twice over: the BT411 + // donor's decompiled enum carries these names WITH binary member offsets + // (torso.hpp 111-124, @0x1E4..@0x20C), and BTL4.RES's own streamed + // control mappings bind ids 12/13 as DIRECT button destinations on + // subsystem 17 = Torso (the [map] audit). + // + // An earlier revision declared "TorsoUp/Down/Left/Right/Center are + // 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 + // binding key for the streamed mappings, so Thrustmaster's buttons + // (12/13) resolved NULL -- the boot write-fault at + // ControlsInstanceDirectOf::Update+0xE -- and the RIO's ANALOG + // StickPosition (9) resolved to our motionState StateIndicator, letting + // every analog update from a live RIO smash a watcher-socketed object + // with raw floats. + // + // The chain -- PowerWatcher -> HeatWatcher -> MechSubsystem -- publishes + // nothing of its own, so these start at MechSubsystem::NextAttributeID. + // + public: + enum { + RotationOfTorsoVerticalAttributeID = MechSubsystem::NextAttributeID, + RotationOfTorsoHorizontalAttributeID, + HorizontalLimitRightAttributeID, + HorizontalLimitLeftAttributeID, + SpeedOfTorsoVerticalAttributeID, + SpeedOfTorsoHorizontalAttributeID, + StickPositionAttributeID, + TorsoUpAttributeID, + TorsoDownAttributeID, + TorsoLeftAttributeID, + TorsoRightAttributeID, + TorsoCenterAttributeID, + MotionStateAttributeID, + NextAttributeID + }; + + static const IndexEntry AttributePointers[]; + static AttributeIndexSet AttributeIndex; + + protected: + int isDamagedCopy; + int statusFlags; + Logical horizontalEnabled; + Scalar baseTwistRate; + Scalar baseElevationRate; + Scalar horizontalLimitRight; + Scalar horizontalLimitLeft; + Scalar verticalLimitTop; + Scalar verticalLimitBottom; + Scalar twistCenterHigh; + Scalar twistCenterLow; + Scalar elevationCenter; + Scalar elevationHalfBottom; + Scalar buttonAccelerationPerSecond; + Scalar buttonAccelerationStart; + int buttonRampActive; + Scalar buttonRamp; + Joint *horizontalJointNode; + Joint *horizontalShadowJointNode; + Scalar currentTwist; + Scalar currentElevation; + Scalar analogElevationAxis; + Scalar analogTwistAxis; + // + // MUST BE A StateIndicator. The authored watcher for a *State + // name is an AudioStateWatcher, which is AudioWatcherOf + // and whose ctor immediately runs + // Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this) + // (AUDWTHR.CPP:891). Point it at a plain int and that call goes + // through garbage -- the pod dies on the extender. + // + StateIndicator motionState; // authored watcher + // + // The five BUTTON COMMAND destinations (donor torso.hpp @0x1F8-0x208). + // The streamed control mappings bind these BY ID as DIRECT WRITE + // targets -- LBE4ControlsManager::CreateStreamedMappings registers + // GetAttributePointer(attributeID) as the destination a device poll + // writes through (L4CTRL.CPP:1936). Carved from the front of the + // dynamicsState reserve so the class size is unchanged. + // + int elevateUpCommand; + int elevateDownCommand; + int twistLeftCommand; + int twistRightCommand; + int centerCommand; + // + // Recenter latch (donor @0x268-adjacent family): armed by the centre + // button, cleared by a twist button or on arrival at centre. + // + 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 diff --git a/restoration/source410/BT/TORSO.NOTES.md b/restoration/source410/BT/TORSO.NOTES.md index 60decba5..055d6efb 100644 --- a/restoration/source410/BT/TORSO.NOTES.md +++ b/restoration/source410/BT/TORSO.NOTES.md @@ -1,55 +1,93 @@ -# TORSO.CPP / .HPP — reconstruction notes - -`Torso` (: PowerWatcher) drives torso twist (horizontal) and weapon elevation -(vertical). Tuning rates/limits stream from the resource -(`horizontal/verticalRotationPerSecond`, `horizontal/verticalLimit*`, -`torsoHorizontalEnabled`). - -## Reconstructed - -- Ctor: streams the base rates (deg→rad) + limits + `horizontalEnabled`, primes - `currentTwist`/`currentElevation`/analog axes to 0, and installs - `TorsoSimulation` as the per-frame Performance (the replicant copy is - console-driven via `TorsoCopySimulation`, still staged). -- `analogElevationAxis` / `analogTwistAxis` members + `SetAnalogElevationAxis` / - `SetAnalogTwistAxis` setters (written by `MechControlsMapper::InterpretControls`). -- `CurrentTwist()` / `CurrentElevation()` / `GetHorizontalEnabled()` accessors. -- `TorsoSimulation(Scalar)` — per-frame aim integration (authentic core): - - `currentElevation += analogElevationAxis · baseElevationRate · dt`, clamped - to [verticalLimitBottom, verticalLimitTop]; - - if `horizontalEnabled`: `currentTwist += analogTwistAxis · baseTwistRate · - dt`, clamped to [horizontalLimitLeft, horizontalLimitRight]. - -**VERIFIED** (`BT_MECH_LOG` `[mppr] torsoElev=`, forced-input `BT_FORCE_ELEV`): -stick pitch 0.8 slews the elevation up and clamps at the authentic -`verticalLimitTop` = 0.349 rad = **20°**. Zero Fail. - -## Skeleton-joint binding (2026-07-21) - -The twist joints are now RESOLVED and BOUND: the ctor calls -`owner->ResolveJoint(torsoHorizontalJoint / torsoHorizontalShadowJoint)` (the -skeleton is live — see MECH.NOTES.md), stores them as `Joint*`, and -`TorsoSimulation` pushes `currentTwist` onto `horizontalJointNode` via -`Joint::SetRotation` (hinge → `Radian`; ball → `EulerAngles` yaw). - -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 -torso-twist mech drives the joint. Verified headlessly; the VISUAL rotation -needs the renderer. - -## Elevation does NOT drive a gun joint (corrected 2026-07-21) - -Checked BT411 before reconstructing a "gun-elevation joint": no such joint -binding exists anywhere in the authentic engine. `currentElevation` instead -composes into `Mech::eyepointRotation` (the cockpit-eye / weapon-boresight -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 -piece was the CONSUMER (`Mech::Simulate`'s eyepoint composition), now -reconstructed and verified (`torsoElev` == `eyePitch` every frame). - -## Deferred - -- HUD free-aim slew, the look-button state machine (`BTCommitLookState` / - `gBTLookPitch`/`gBTLookYaw` — composes with elevation into the full - eyepoint), `TorsoCopySimulation` (replicant console-driven aim) — still - staged. +# TORSO.CPP / .HPP — reconstruction notes + +`Torso` (: PowerWatcher) drives torso twist (horizontal) and weapon elevation +(vertical). Tuning rates/limits stream from the resource +(`horizontal/verticalRotationPerSecond`, `horizontal/verticalLimit*`, +`torsoHorizontalEnabled`). + +## Reconstructed + +- Ctor: streams the base rates (deg→rad) + limits + `horizontalEnabled`, primes + `currentTwist`/`currentElevation`/analog axes to 0, and installs + `TorsoSimulation` as the per-frame Performance (the replicant copy is + console-driven via `TorsoCopySimulation`, still staged). +- `analogElevationAxis` / `analogTwistAxis` members + `SetAnalogElevationAxis` / + `SetAnalogTwistAxis` setters (written by `MechControlsMapper::InterpretControls`). +- `CurrentTwist()` / `CurrentElevation()` / `GetHorizontalEnabled()` accessors. +- `TorsoSimulation(Scalar)` — per-frame aim integration (authentic core): + - `currentElevation += analogElevationAxis · baseElevationRate · dt`, clamped + to [verticalLimitBottom, verticalLimitTop]; + - if `horizontalEnabled`: `currentTwist += analogTwistAxis · baseTwistRate · + dt`, clamped to [horizontalLimitLeft, horizontalLimitRight]. + +**VERIFIED** (`BT_MECH_LOG` `[mppr] torsoElev=`, forced-input `BT_FORCE_ELEV`): +stick pitch 0.8 slews the elevation up and clamps at the authentic +`verticalLimitTop` = 0.349 rad = **20°**. Zero Fail. + +## Skeleton-joint binding (2026-07-21) + +The twist joints are now RESOLVED and BOUND: the ctor calls +`owner->ResolveJoint(torsoHorizontalJoint / torsoHorizontalShadowJoint)` (the +skeleton is live — see MECH.NOTES.md), stores them as `Joint*`, and +`TorsoSimulation` pushes `currentTwist` onto `horizontalJointNode` via +`Joint::SetRotation` (hinge → `Radian`; ball → `EulerAngles` yaw). + +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 +torso-twist mech drives the joint. Verified headlessly; the VISUAL rotation +needs the renderer. + +## Elevation does NOT drive a gun joint (corrected 2026-07-21) + +Checked BT411 before reconstructing a "gun-elevation joint": no such joint +binding exists anywhere in the authentic engine. `currentElevation` instead +composes into `Mech::eyepointRotation` (the cockpit-eye / weapon-boresight +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 +piece was the CONSUMER (`Mech::Simulate`'s eyepoint composition), now +reconstructed and verified (`torsoElev` == `eyePitch` every frame). + +## TorsoCopySimulation + ComputeTargetTwist (2026-08-04, 5.3.114) + +The replicant (damaged-copy) twist path is real, verified line-by-line against +the raw decomp (part_013.c): + +- `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).