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:
+514
-398
@@ -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 <bt.hpp>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(TORSO_HPP)
|
||||
# include <torso.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(JOINT_HPP)
|
||||
# include <joint.hpp>
|
||||
#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 <bt.hpp>
|
||||
#pragma hdrstop
|
||||
|
||||
#if !defined(TORSO_HPP)
|
||||
# include <torso.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(MECH_HPP)
|
||||
# include <mech.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(JOINT_HPP)
|
||||
# include <joint.hpp>
|
||||
#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();
|
||||
}
|
||||
|
||||
+232
-209
@@ -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 <powersub.hpp>
|
||||
# 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<int>::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<StateIndicator>
|
||||
// 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 <powersub.hpp>
|
||||
# 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<int>::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<StateIndicator>
|
||||
// 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
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user