Files
BT411/game/reconstructed/torso.cpp
T
arcattackandClaude Fable 5 cb85517ede Gyro: byte-exact re-enable -- ctor map, integrators, jointeye writers (task #56)
The Gyroscope subsystem is LIVE again (was a stub since the NaN revert).
Reconstructed byte-exact from @004b3778 / @004b2ec0 / @004b30ec / @004b33e0 /
@004b34ec + the byte-verified dispatch in the unexported Mech performance
FUN_004a9b5c (calls @0x4aaf74/0x4aaf83):

- ctor field map corrected: springConstant@0x1E8 / dampingConstant@0x1F4 were
  mislabelled (the old 'eyeOrientation = r->springConstant' poison line), the
  0x254-0x2B3 block (externalPitchPtr, work matrix, placement) was missing so
  everything after was mis-offset, and clamps/accumulators were 0xCD fill --
  every field now initialised per the binary; 33 offsets static_assert-locked,
  sizeof == 0x3D0 exact
- integrators: state-minus-target displacements, componentwise damping
  OVERWRITE (FUN_004086d0 is a component multiply, not a cross), position step
  without dt, clamp to [posSpring, negSpring]; IntegrateBody's X/Z-crossed
  force + integration terms
- writers: WriteMechJoint drives ONE node ('jointeye', type 5): TRANSLATION =
  eyePosition spring, ROTATION = bodyOrientation spring; WriteEyeJoint is a
  multiplicative sway attenuator on 'jointlocal' (post-anim only)
- dispatch moved out of GyroscopeSimulation into the Mech performance tail
  (GyroFrameJointWrite bridge, mech4.cpp) with the binary gates; gyro<->torso
  pitch link wired (gyro+0x258 = &torso currentTwist) via complete-type-TU
  bridges, retiring the SubProxy::linkTarget landmine
- Mech ctor: deathAnimationLatched/legResetLatch were never initialised --
  0xCDCDCDCD gated the writers off silently

Runtime-verified: joints resolve (both BallTranslation), Performance installs,
WriteMechJoint finite from frame 0 (no NaN), Madcat cockpit un-regressed,
combat targeting healthy. Empirical: spring targets are SYMMETRIC -> eye
equilibrium (0,0,0), clamps +/-0.1-0.15u -- the gyro is the hit-BOUNCE
mechanism, not a steady eye offset (hypothesis disproven, recorded in KB).

Pending (task #56 tail): the damage->gyro fan-out FUN_004b2980 (unexported
gap) so hits actually kick the springs; the mech+0x3F0 overspeed sway model;
the torso-pitch EyepointRotation writer (FUN_004b66b4) + glance-look states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 06:47:22 -05:00

914 lines
37 KiB
C++

//===========================================================================//
// File: torso.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Torso subsystem -- torso twist (yaw) and elevation (pitch) aim //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered shard
// part_013.c). No header survived; see torso.hpp. Each non-trivial method
// cites the originating @ADDR. Confidence flags: [CONFIDENT] / [BEST-EFFORT]
// / [EXCLUDED] as in gyro.cpp.
//
// Hex constants converted to decimal (from section_dump.txt):
// _DAT_004b6500 = 0.0f _DAT_004b6504 = 0.0001f (zero-vel epsilon)
// _DAT_004b6508 = 0.02f _DAT_004b650c = 0.05f (settle tolerances)
// _DAT_004b64f8 = 0.5f _DAT_004b64fc = 1.0f (ramp cap)
// _DAT_004b6a14 = 0.0f _DAT_004b6a18 = 0.0001f (recenter epsilon)
// _DAT_004b6b08 = 10.0f (min slew-window, ms->s)
// _DAT_004b6fb8 = 0.0174532925f (PI/180, deg->rad) _DAT_004b6fbc = 0.5f
// _DAT_004b76c4 = -1.0f (resource "unset" sentinel)
// DAT_0052140c = milliseconds-per-second tick scale (runtime-initialised;
// reads 0 in the static image -- used as (now-t0)/scale).
//
// Helper-function name mapping:
// FUN_004b18a4 PowerWatcher base constructor
// FUN_004b198c PowerWatcher::CreateStreamedSubsystem
// FUN_004b1804 PowerWatcher::ResetToInitialState (slot 10)
// FUN_004b179c PowerWatcher slot-9 death/voltage handler
// FUN_004b181c PowerWatcher per-frame watch update
// FUN_0041bd34 Subsystem::WriteUpdateRecord (base)
// FUN_00414b60 Clock::Now() (ms)
// FUN_004081e0 Scalar lerp(dst,a,b,t)
// FUN_0041cfa0/0041d020/0041d0a8 skeleton-node get/set transform
// FUN_00404118 NotationFile::ReadScalar FUN_00404088 ReadString
// FUN_004d4b58 stricmp FUN_004dcd00 fabsf
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(TORSO_HPP)
# include <torso.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp> // complete Mech -- owner->ResolveJoint
#endif
#include <JOINT.hpp> // Joint, JointSubsystem (fwd shim)
#include <ROTATION.hpp> // EulerAngles, Radian, Hinge (fwd shim)
#if !defined(APP_HPP)
# include <app.hpp>
#endif
#if !defined(TESTBT_HPP)
# include <testbt.hpp>
#endif
static const Scalar Zero = 0.0f; // _DAT_004b6500 / _DAT_004b6a14
static const Scalar VelEps = 0.0001f; // _DAT_004b6504 / _DAT_004b6a18
static const Scalar TwistEps = 0.02f; // _DAT_004b6508
static const Scalar SettleEps = 0.05f; // _DAT_004b650c
static const Scalar RampCap = 1.0f; // _DAT_004b64fc
static const Scalar MinSlewMs = 10.0f; // _DAT_004b6b08
static const Scalar DegToRad = 0.0174532925f; // _DAT_004b6fb8
static const Scalar Unset = -1.0f; // _DAT_004b76c4
// DAT_0052140c -- the runtime "ticks-per-second" scale (reads 0 in the static
// image; the engine initialises it from the system clock). TODO: confirm the
// real value once the clock-init is recovered; 1000.0f (ms->s) is consistent
// with the MinSlewMs window above.
static const Scalar MsPerSecond = 1000.0f; // DAT_0052140c
//
// CROSS-FAMILY compile shims for the streamed-subsystem joint validation: the
// engine has no `Skeleton` C++ type with FindNode (only SkeletonClassID streams
// in VDATA.h), so the real joint-existence check could not be recovered. These
// let CreateStreamedSubsystem compile; they conservatively accept any joint.
//
namespace {
struct ReconSkeleton
{
Logical FindNode(const char * /*node_name*/) const { return True; }
};
static ReconSkeleton g_reconSkeleton;
inline ReconSkeleton*
LoadSkeleton(const ResourceDirectories * /*dirs*/, const char * /*name*/)
{ return &g_reconSkeleton; }
// The Torso update record appends three trailing Scalars after the base
// Simulation::UpdateRecord header (recovered offsets +0x10/+0x14/+0x18).
inline Scalar
RecordField(Simulation::UpdateRecord *record, int byte_offset)
{ return *(const Scalar*)((const char*)record + byte_offset); }
}
//###########################################################################
// BASE-CHAIN RE-BASE -- compile-time layout locks (STEP 3).
//
// The Torso is read at RAW absolute offsets externally (the gyro cross-link
// stores (char*)sinkSourceSubsystem+0x1D8 == currentTwist, mech.cpp:740; the
// damage-slice, HUD ctor and radar read the same +0x1D8; MechControlsMapper
// reads +0x1F0/+0x1F4/+0x250/+0x274). These asserts fail the BUILD if the
// re-based layout ever drifts, long before any runtime mis-read.
//###########################################################################
// A friend of Torso so it can offsetof() the protected own-block fields.
struct TorsoLayoutCheck
{
static_assert(sizeof(Torso) == 0x280, "sizeof(Torso) must be 0x280 (factory alloc + gyro cross-link)");
static_assert(offsetof(Torso, currentTwist) == 0x1D8, "Torso currentTwist must be at 0x1D8 (gyro linkTarget +0x1D8)");
static_assert(offsetof(Torso, currentElevation) == 0x1E4, "Torso currentElevation must be at 0x1E4");
static_assert(offsetof(Torso, analogTwistAxis) == 0x1F0, "Torso analogTwistAxis must be at 0x1F0 (mapper +0x1F0)");
static_assert(offsetof(Torso, analogElevationAxis) == 0x1F4, "Torso analogElevationAxis must be at 0x1F4 (mapper +0x1F4)");
static_assert(offsetof(Torso, horizontalEnabled) == 0x250, "Torso horizontalEnabled must be at 0x250 (mapper +0x250)");
static_assert(offsetof(Torso, recenterActive) == 0x274, "Torso recenterActive must be at 0x274 (proves the 0x270 pad)");
static_assert(offsetof(Torso, horizontalShadowJointNode) == 0x27C, "Torso last own field at 0x27C (+4 => sizeof 0x280)");
};
//###########################################################################
//###########################################################################
// Torso
//###########################################################################
//###########################################################################
//#############################################################################
// Shared Data Support (DefaultData @00510af8)
//
Derivation
Torso::ClassDerivations(
PowerWatcher::GetClassDerivations(), // returns Derivation* (no &)
"Torso"
);
Receiver::MessageHandlerSet
Torso::MessageHandlers;
Torso::AttributeIndexSet
Torso::AttributeIndex;
Torso::SharedData
Torso::DefaultData(
&Torso::ClassDerivations,
Torso::MessageHandlers,
Torso::AttributeIndex,
Torso::StateCount
);
//#############################################################################
// Construction / Destruction
//
//
// @004b6b0c [CONFIDENT] -- chains to the PowerWatcher base ctor (FUN_004b18a4)
// with &Torso::DefaultData, installs the Torso vtable (PTR @0051103c). A live
// master segment (flags & 0xC == 0 && flags & 1) gets TorsoSimulation as its
// Performance and isDamagedCopy=0; any other segment gets TorsoCopySimulation
// and isDamagedCopy=1. Angular resource values are converted deg->rad here.
//
Torso::Torso(
Mech *owner,
int subsystem_ID,
SubsystemResource *r,
SharedData &shared_data
):
PowerWatcher(owner, subsystem_ID, r, shared_data)
{
Check(owner);
Check_Pointer(r);
// BASE-CHAIN RE-BASE: the 7 CROSS-FAMILY shim backing fields were deleted
// (they over-sized the object); their accessors now read the real inherited
// base state, so there is nothing to prime here. The master/copy selection
// below already reads the authoritative owner->simulationFlags.
// INTEGRATION (gate reconcile): read OWNER simulationFlags (param_2+0x28) —
// the oracle-verified authoritative source — not the local segment shim.
if ((owner->simulationFlags & SegmentCopyMask) == 0
&& (owner->simulationFlags & MasterHeatSinkFlag) != 0) // owner flags & 0x100 (binary @004b6b0c)
{
isDamagedCopy = 0; // @0x24C
SetPerformance(&Torso::TorsoSimulation); // PTR @00510c10 (-> @004b5cf0)
}
else
{
isDamagedCopy = 1;
SetPerformance(&Torso::TorsoCopySimulation); // PTR @00510c1c (-> @004b65f8)
}
statusFlags = 0; // @0x20C
buttonAccelerationPerSecond = r->buttonAccelerationPerSecond; // @0x210 <- +0x150
buttonAccelerationStart = r->buttonAccelerationStartValue; // @0x214 <- +0x154
baseTwistRate = r->horizontalRotationPerSecond * DegToRad; // @0x23C <- +0xF4
baseElevationRate = r->verticalRotationPerSecond * DegToRad; // @0x240 <- +0xF8
horizontalLimitRight = r->horizontalLimitRight * DegToRad; // @0x1DC <- +0xFC
horizontalLimitLeft = r->horizontalLimitLeft * DegToRad; // @0x1E0 <- +0x100
verticalLimitTop = r->verticalLimitTop * DegToRad; // @0x220 <- +0x104
verticalLimitBottom = r->verticalLimitBottom * DegToRad; // @0x224 <- +0x108
// derived limit copies + half-bottom (used as soft centre / settle band)
twistCenterHigh = verticalLimitTop; // @0x230 = @0x220
twistCenterLow = verticalLimitBottom; // @0x234 = @0x224
elevationCenter = verticalLimitTop; // @0x228 = @0x220
elevationHalfBottom = verticalLimitBottom * 0.5f; // @0x22C (_DAT_004b6fbc)
buttonRampActive = 0; // @0x268
buttonRamp = 0.0f;// @0x26C
horizontalEnabled = r->torsoHorizontalEnabled; // @0x250 <- +0x14C
if (horizontalEnabled)
{
// resolve the two skeleton joints named in the resource:
horizontalJointNode = ResolveJoint(r->torsoHorizontalJoint); // @0x278
horizontalShadowJointNode = ResolveJoint(r->torsoHorizontalShadowJoint); // @0x27C
// bring-up verification (env BT_TORSO_LOG; default OFF): confirm the
// named joints resolved to live nodes and report their joint types.
if (getenv("BT_TORSO_LOG"))
{
DEBUG_STREAM << "[torso] resolve '" << r->torsoHorizontalJoint
<< "' -> " << (void*)horizontalJointNode;
if (horizontalJointNode)
DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
DEBUG_STREAM << " ; shadow '" << r->torsoHorizontalShadowJoint
<< "' -> " << (void*)horizontalShadowJointNode;
if (horizontalShadowJointNode)
DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
DEBUG_STREAM << "\n" << std::flush;
}
}
// ---- BRING-UP DEMO (env BT_FORCE_TORSO; default OFF; NOT faithful) --------
// The Blackhawk 0xBC5 record has TorsoHorizontalEnabled=0 + empty joint names
// (the binary skips torso joints for this mech, verified). To exercise the
// reconstructed twist path end-to-end (ResolveJoint -> TorsoSimulation ->
// UpdateJoints -> PushTwist -> Joint::SetRotation), force-enable and resolve
// the REAL BLH.SKL torso joints ('jointshakey2' torso body / 'jointtshadow'
// = the "apply torso twist to yaw" hinge), widen the limits, and give a slew
// rate. TorsoSimulation drives the sweep (below). Remove after verification.
if (isDamagedCopy == 0 && getenv("BT_FORCE_TORSO"))
{
horizontalEnabled = True; // @0x250
const char *mj = getenv("BT_FORCE_TORSO_JOINT");
if (mj == 0 || *mj == '\0') mj = "jointshakey2";
horizontalJointNode = ResolveJoint(mj); // torso body (ball)
horizontalShadowJointNode = ResolveJoint("jointtshadow"); // shadow twist (hingey)
horizontalLimitLeft = 0.7f; // @0x1E0 ~40 deg
horizontalLimitRight = -0.7f; // @0x1DC
baseTwistRate = 1.0f; // @0x23C rad/s slew
if (getenv("BT_TORSO_LOG"))
{
DEBUG_STREAM << "[torso] FORCE-ENABLE '" << mj << "' -> " << (void*)horizontalJointNode;
if (horizontalJointNode) DEBUG_STREAM << " type=" << (int)horizontalJointNode->GetJointType();
DEBUG_STREAM << " ; shadow 'jointtshadow' -> " << (void*)horizontalShadowJointNode;
if (horizontalShadowJointNode) DEBUG_STREAM << " type=" << (int)horizontalShadowJointNode->GetJointType();
DEBUG_STREAM << "\n" << std::flush;
}
}
// --------------------------------------------------------------------------
effectiveTwistRate = baseTwistRate; // @0x244 = @0x23C
effectiveElevationRate = baseElevationRate; // @0x248 = @0x240
currentTwist = 0.0f; // @0x1D8
currentElevation = 0.0f; // @0x1E4
twistVelocity = 0.0f; // @0x1E8
twistRate = 0.0f; // @0x238
analogTwistAxis = analogElevationAxis = 0.0f; // @0x1F0 / @0x1F4
elevateUpCommand = elevateDownCommand = 0; // @0x1F8 / @0x1FC
twistLeftCommand = twistRightCommand = 0; // @0x200 / @0x204
centerCommand = 0; // @0x208
hitElevTop = hitElevBottom = 0; // @0x258 / @0x25C
hitTwistLeft = hitTwistRight = 0; // @0x260 / @0x264
recenterActive = 0; // @0x274
lastUpdateTime = GetCreationTime(); // @0x254 = this[5]
// bring-up verification (env BT_TORSO_LOG): the gyro cross-links to
// (Torso*)+0x1D8 (== currentTwist) at a RAW offset (mech.cpp:740), so the
// compiled layout MUST place currentTwist at 0x1D8 and the object must fit the
// 0x280 factory alloc. Log both so a mismatch is caught immediately.
if (getenv("BT_TORSO_LOG"))
{
DEBUG_STREAM << "[torso] ctor this=" << (void*)this
<< " sizeof(Torso)=" << (unsigned)sizeof(Torso)
<< " (0x280=" << (unsigned)0x280 << ")"
<< " currentTwist@" << (unsigned)((char*)&currentTwist - (char*)this)
<< " (want 0x1D8=" << (unsigned)0x1D8 << ")"
<< " damagedCopy=" << isDamagedCopy
<< " horizEnabled=" << (int)horizontalEnabled << "\n" << std::flush;
// RESOURCE DIAGNOSTIC: is horizEnabled=0 a genuine content value or a
// mis-read? Dump the resource enable flag + joint names (valid strings =>
// seg is the right record) + the raw int at record+0x14C + the two limit
// scalars (to confirm the resource is a plausible Torso record at all).
DEBUG_STREAM << "[torso] res enabled=" << (int)r->torsoHorizontalEnabled
<< " raw@0x14C=" << *(const int*)((const char*)r + 0x14C)
<< " hJoint='" << r->torsoHorizontalJoint << "'"
<< " sJoint='" << r->torsoHorizontalShadowJoint << "'"
<< " hRotPerSec=" << r->horizontalRotationPerSecond
<< " hLimL=" << r->horizontalLimitLeft
<< "\n" << std::flush;
// Is the record correctly positioned? classID@+0x20 should be 0xBC5 and
// modelSize@+0x24 should be 0x158 for a real Torso record; garbage => the
// seg pointer / stream position is wrong (not a content issue).
DEBUG_STREAM << "[torso] res classID=0x" << std::hex
<< *(const int*)((const char*)r + 0x20)
<< " modelSize=0x" << *(const int*)((const char*)r + 0x24) << std::dec
<< " (want classID=0xBC5 size=0x158)\n" << std::flush;
}
Check_Fpu();
}
//
// @004b6fc0 [BEST-EFFORT, prologue not captured] -- reinstalls the vtable, runs
// the PowerWatcher teardown and frees on the deleting bit. (Mirrors @004b3e88.)
//
Torso::~Torso()
{
Check(this);
Check_Fpu();
}
Logical Torso::TestClass(Mech &) { return True; }
Logical Torso::TestInstance() const { return IsDerivedFrom(ClassDerivations); }
//#############################################################################
// Subsystem virtual overrides
//
//
// @004b5bf8 (slot 10) [CONFIDENT] -- ResetToInitialState. Chains to
// PowerWatcher::ResetToInitialState (FUN_004b1804) first; when (re)powering,
// snaps the effective rates back to the base rates and clears the button ramp.
// Always clears the command inputs / velocity / latches, then re-pushes the
// (centred) twist into the joints via WriteJoints.
//
void
Torso::ResetToInitialState()
{
WatcherResetToInitialState(); // CROSS-FAMILY: PowerWatcher::ResetToInitialState (FUN_004b1804)
// @004b5bf8: on a (re)power (param_2 != 0) restore the effective rates and
// clear all FOUR limit latches (0x258/0x25c/0x260/0x264); always clear the
// command + analog inputs, the aim state and velocities, then re-push the
// centred twist into the joints via WriteJoints.
const Logical powered = True; // bring-up: reset always re-powers
if (powered)
{
effectiveTwistRate = baseTwistRate; // @0x244 = @0x23C
effectiveElevationRate = baseElevationRate; // @0x248 = @0x240
hitElevTop = hitElevBottom = 0; // @0x258 / @0x25C
hitTwistLeft = hitTwistRight = 0; // @0x260 / @0x264
}
elevateUpCommand = elevateDownCommand = 0; // @0x1F8 / @0x1FC
twistLeftCommand = twistRightCommand = 0; // @0x200 / @0x204
centerCommand = 0; // @0x208
analogTwistAxis = analogElevationAxis = 0.0f;// @0x1F0 / @0x1F4
currentTwist = 0.0f; // @0x1D8
currentElevation = 0.0f; // @0x1E4
twistVelocity = 0.0f; // @0x1E8
elevationVelocity = 0.0f; // @0x1EC
twistAtUpdate = 0.0f; // @0x21C
targetTwist = 0.0f; // @0x218
twistRate = 0.0f; // @0x238
recenterActive = 0; // @0x274
Scalar verticalOut;
WriteJoints(verticalOut); // FUN_004b66b4
}
//
// @004b5be0 (slot 9) [BEST-EFFORT, prologue not captured] -- forwards to the
// PowerWatcher slot-9 handler (FUN_004b179c): on a "destroyed" message it clears
// the watched power source's voltage alarm, otherwise chains to the base.
//
Logical
Torso::HandleDeathMessage(Message &message)
{
return WatcherHandleDeathMessage(message); // CROSS-FAMILY: PowerWatcher::HandleDeathMessage (FUN_004b179c)
}
//
// @004b6a78 (slot 6) [CONFIDENT] -- network/replay update record. Samples the
// clock (FUN_00414b60) into lastUpdateTime, biasing it forward by one interval
// when the elapsed window is below MinSlewMs, chains to Subsystem::WriteUpdateRecord
// (FUN_0041bd34), then writes twistAtUpdate / twistVelocity / twistRate from the
// record fields (record +0x10 / +0x14 / +0x18).
//
void
Torso::WriteUpdateRecord(UpdateRecord *message, int update_model)
{
lastUpdateTime = GetCurrentTime(); // @0x254
if ((Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond < MinSlewMs)
{
lastUpdateTime += (lastUpdateTime - GetCreationTime()); // stretch tiny windows
}
Subsystem::WriteUpdateRecord(message, update_model); // FUN_0041bd34
twistAtUpdate = RecordField(message, 0x10); // @0x21C
twistVelocity = RecordField(message, 0x14); // @0x1E8
twistRate = RecordField(message, 0x18); // @0x238
}
//#############################################################################
// Per-frame simulation
//
//
// @004b5cf0 [CONFIDENT] -- the live-master Performance (PTR @00510c10).
//
// 1. PowerWatcher watch update (FUN_004b181c).
// 2. Latch the effective rates from the base rates, then ZERO them if the
// watched power source is dead (this[0x10]==1), not Ready (this @0x198 != 4),
// or in the Failure heat state (this @0x140 == 2); the Degradation heat state
// (==1) instead halves the twist rate.
// 3. Apply the button-acceleration ramp (start value -> cap) while a command is
// held, integrate the per-axis commands into currentTwist / currentElevation,
// clamp to the software limits, set the limit latches, and -- if releasing --
// run Recenter. The "moved" dirty bit (this @0x18 |= 1) is raised when the
// aim changed beyond TwistEps/SettleEps, and statusFlags reflects which limit
// (if any) is being held.
//
void
Torso::TorsoSimulation(Scalar time_slice)
{
Check(this);
WatcherUpdateWatch(); // CROSS-FAMILY: PowerWatcher per-frame watch (FUN_004b181c)
Scalar twist0 = currentTwist; // snapshot for velocity calc
Scalar elev0 = currentElevation;
effectiveTwistRate = baseTwistRate; // @0x244 = @0x23C
effectiveElevationRate = baseElevationRate; // @0x248 = @0x240
if (HeatModelOff()) effectiveTwistRate = 0.0f; // this[0x10]==1
if (ElectricalStateLevel() != PoweredSubsystem::Ready) effectiveTwistRate = 0.0f; // @0x198 != 4
switch (HeatStateLevel()) // @0x140
{
case HeatSink::DegradationHeat: effectiveTwistRate = baseTwistRate * 0.5f; break; // _DAT_004b64f8
case HeatSink::FailureHeat: effectiveTwistRate = 0.0f; break;
default: break;
}
// BRING-UP DEMO (env BT_FORCE_TORSO): un-gate the slew rate (the forced demo
// torso isn't wired to a live power source, so ElectricalStateLevel would zero
// it) and drive a left/right analog sweep so the reconstructed twist path is
// exercised visibly. Faithful behavior is untouched when the env is unset.
static const int s_forceTorso = getenv("BT_FORCE_TORSO") ? 1 : 0;
if (s_forceTorso)
{
effectiveTwistRate = baseTwistRate; // un-gate (ctor set 1.0 rad/s)
static int s_sweep = 0;
analogTwistAxis = ((++s_sweep / 90) & 1) ? -1.0f : 1.0f; // +/- every ~90 frames
}
Scalar twistStep = effectiveTwistRate * time_slice;
Scalar elevStep = effectiveElevationRate * time_slice;
// button-acceleration ramp (forced to RampCap in the shipped build -- see note)
if (buttonRampActive == 0)
{
buttonRamp = buttonAccelerationStart; // @0x26C <- @0x214
}
else
{
buttonRamp += buttonAccelerationPerSecond * time_slice; // ramp up
buttonRamp = Min(buttonRamp, RampCap);
buttonRampActive = 0;
}
buttonRamp = 1.0f; // NB: @004b5cf0 unconditionally overwrites @0x26C with 1.0f
// ---- digital elevation (pitch) commands @0x1F8 / @0x1FC ----
if (elevateUpCommand > 0) // @0x1F8
{
currentElevation += elevStep * buttonRamp;
currentElevation = Min(currentElevation, verticalLimitTop); // @0x220
buttonRampActive = 1;
}
if (elevateDownCommand > 0) // @0x1FC
{
currentElevation -= elevStep * buttonRamp;
currentElevation = Max(currentElevation, verticalLimitBottom); // @0x224
buttonRampActive = 1;
}
// ---- digital twist (yaw) commands @0x200 / @0x204 ----
if (twistLeftCommand > 0) // @0x200
{
currentTwist += twistStep * buttonRamp;
currentTwist = Min(currentTwist, horizontalLimitLeft); // @0x1E0
recenterActive = 0;
buttonRampActive = 1;
}
if (twistRightCommand > 0) // @0x204
{
currentTwist -= twistStep * buttonRamp;
currentTwist = Max(currentTwist, horizontalLimitRight); // @0x1DC
recenterActive = 0;
buttonRampActive = 1;
}
if (centerCommand > 0) // @0x208
{
recenterActive = 1;
buttonRampActive = 0;
}
// ---- analog (RIO/stick) axes @0x1F0 / @0x1F4: proportional, no button ramp ----
if (analogTwistAxis != Zero) // @0x1F0
{
currentTwist += analogTwistAxis * twistStep;
currentTwist = Min(currentTwist, horizontalLimitLeft); // @0x1E0
currentTwist = Max(currentTwist, horizontalLimitRight); // @0x1DC
recenterActive = 0;
}
if (analogElevationAxis != Zero) // @0x1F4
{
currentElevation += analogElevationAxis * elevStep;
currentElevation = Min(currentElevation, verticalLimitTop); // @0x220
currentElevation = Max(currentElevation, verticalLimitBottom); // @0x224
}
if (recenterActive != 0)
{
recenterActive = Recenter(time_slice); // FUN_004b6918
}
// ---- derive angular velocities + settle detection ----
Scalar oldTwistRate = twistRate; // local_90: OLD rate, snapshot before recompute
if (time_slice > Zero)
{
twistVelocity = (currentTwist - twist0) / time_slice; // @0x1E8
elevationVelocity = (currentElevation - elev0) / time_slice; // @0x1EC
twistRate = twistVelocity; // @0x238 (signed)
twistVelocity = fabsf(twistVelocity);
elevationVelocity = fabsf(elevationVelocity);
}
if (fabsf(twistRate) <= VelEps) twistRate = 0.0f;
if (fabsf(oldTwistRate) <= VelEps) oldTwistRate = 0.0f;
ComputeTargetTwist(); // FUN_004b6510 -> targetTwist (@0x218)
// raise the "moved" dirty bit unless we have settled within tolerance. The
// aim error is measured against targetTwist (0x218) and the settle test uses
// the OLD rate snapshot (not the previous twist angle).
Scalar aimError = currentTwist - targetTwist; // 0x1D8 - 0x218
Logical justStopped = (twistRate == Zero) && (oldTwistRate != Zero);
Scalar rateDelta = twistRate - oldTwistRate;
if (!(fabsf(aimError) <= TwistEps // _DAT_004b6508
&& fabsf(rateDelta) <= SettleEps // _DAT_004b650c
&& !justStopped))
{
SetMovedFlag(); // this @0x18 |= 1
}
// ---- limit latches (edge-triggered) + statusFlags ----
// Cleared when off the limit; set (with statusFlags=2) only on the FRAME the
// limit is newly reached. @0x260/@0x264 latch the twist limits, @0x258/@0x25C
// the elevation limits (verified against part_013.c 4658-4692).
statusFlags = 0;
if (fabsf(currentTwist - horizontalLimitLeft) > VelEps) hitTwistLeft = 0; // @0x260
else if (hitTwistLeft == 0) { hitTwistLeft = 1; statusFlags = 2; }
if (fabsf(currentTwist - horizontalLimitRight) > VelEps) hitTwistRight = 0; // @0x264
else if (hitTwistRight == 0) { hitTwistRight = 1; statusFlags = 2; }
if (fabsf(currentElevation - verticalLimitTop) > VelEps) hitElevTop = 0; // @0x258
else if (hitElevTop == 0) { hitElevTop = 1; statusFlags = 2; }
if (fabsf(currentElevation - verticalLimitBottom) > VelEps) hitElevBottom = 0; // @0x25C
else if (hitElevBottom == 0) { hitElevBottom = 1; statusFlags = 2; }
if (recenterActive != 0) statusFlags = 1;
// Per-frame skeleton write: push currentTwist into the resolved horizontal
// joint(s). In the shipped binary this is UpdateJoints (@004b67ec) -- the
// out-param-free twin of WriteJoints -- with no DIRECT caller in the recovered
// decomp: it was dispatched by the engine's generic per-frame joint pass (an
// indirect/virtual call). This port ticks the torso's Performance here, so we
// resolve that dispatch to a direct call at the same per-frame cadence, which
// is what makes the torso visibly track the aim.
UpdateJoints(); // FUN_004b67ec
Check_Fpu();
}
//
// @004b65f8 [CONFIDENT] -- the damaged-copy Performance (PTR @00510c1c). A copy
// segment does not take commands; it just eases its twist toward the master's
// target. ComputeTargetTwist() reports whether the master is still slewing: if
// not, snap currentTwist to the target; otherwise lerp by
// dt / ((now - lastUpdateTime)/scale + dt). Result is clamped to the limits.
//
void
Torso::TorsoCopySimulation(Scalar time_slice)
{
if (!ComputeTargetTwist()) // FUN_004b6510 -> 0 = settled
{
currentTwist = targetTwist; // @0x1D8 = @0x218
}
else
{
Scalar age = (Scalar)(lastUpdateTime - GetCreationTime()) / MsPerSecond;
currentTwist = Lerp(currentTwist, targetTwist, time_slice / (age + time_slice)); // FUN_004081e0
}
targetTwist = Min(targetTwist, horizontalLimitLeft); // @0x1E0
targetTwist = Max(targetTwist, horizontalLimitRight); // @0x1DC
// Per-frame skeleton write for the replicated (damaged-copy) torso -- same
// external joint pass as the master path (see TorsoSimulation for the @004b67ec
// note). Harmless in single-player bring-up (no copies); correct for MP.
UpdateJoints(); // FUN_004b67ec
}
//#############################################################################
// Internal model helpers
//
//
// @004b6510 [CONFIDENT] -- compute the extrapolated twist target. Picks the
// time base (clamped command timestamp vs. clock), forms an elapsed time in
// seconds, predicts targetTwist = twistAtUpdate + twistRate * elapsed, clamps to
// the limits, and returns 1 while still extrapolating (live), 0 once settled.
//
Logical
Torso::ComputeTargetTwist()
{
int base;
Logical slewing;
if (isDamagedCopy == 0 || lastUpdateTime <= GetCurrentTime())
{
base = GetCurrentTime() - GetCreationTime(); // this[0x10] - this[0x14]
slewing = False;
}
else
{
base = lastUpdateTime - GetCreationTime(); // @0x254 - this[0x14]
slewing = True;
}
Scalar elapsed = (Scalar)base / MsPerSecond;
targetTwist = twistAtUpdate + twistRate * elapsed; // @0x218 = @0x21C + @0x238*t
targetTwist = Min(targetTwist, horizontalLimitLeft); // @0x1E0
targetTwist = Max(targetTwist, horizontalLimitRight); // @0x1DC
return slewing;
}
//
// ResolveJoint -- forward to the owning Mech's shared resolver (FUN_00424b60,
// inlined by the Torso ctor @004b6b0c). Out-of-line so the complete Mech type
// (mech.hpp) is visible; returns NULL for an absent node (ctor guards on it).
//
Joint*
Torso::ResolveJoint(const char *joint_name)
{
Check(owner); // inherited MechSubsystem::owner (Mech*)
return owner->ResolveJoint(joint_name);
}
//
// @004b66b4 (inner block) [CONFIDENT] -- write one twist (yaw) scalar into a
// resolved skeleton node, preserving the node's other DOF. Dispatched on the
// joint type exactly like AnimationInstance::Animate (JMOVER.cpp:1518-1567):
// hinge nodes (types 0..2) take a scalar Radian (FUN_0041d0a8 ==
// Joint::SetRotation(Radian)); ball nodes (types 4..5) take an EulerAngles whose
// YAW carries the twist while pitch/roll are read back and kept (FUN_0041cfa0 ==
// GetEulerAngles, FUN_0041d020 == SetRotation(EulerAngles)). SetRotation sets
// jointModified + ModifyJoints() internally, so we write unconditionally.
//
void
Torso::PushTwist(Joint *node, Scalar twist)
{
if (node == NULL) // ctor may have failed to resolve the node;
{ // the binary trusts it -- guard for bring-up
return;
}
Joint::JointType jt = node->GetJointType(); // node+0x10
// bring-up verification (env BT_TORSO_LOG; default OFF): show the first few
// joint writes so the per-frame path can be confirmed in a headless run.
static const int s_log = getenv("BT_TORSO_LOG") ? 1 : 0;
static int s_count = 0;
if (s_log && (s_count % 30) == 0 && s_count < 1800) // sample periodically to show the sweep
{
DEBUG_STREAM << "[torso] PushTwist node=" << (void*)node << " type=" << (int)jt
<< " twist=" << (float)twist << "\n" << std::flush;
}
++s_count;
switch (jt) // node+0x10
{
case Joint::HingeXJointType: // types 0..2
case Joint::HingeYJointType:
case Joint::HingeZJointType:
node->SetRotation(Radian(twist)); // FUN_0041d0a8
break;
case Joint::BallJointType: // types 4..5
case Joint::BallTranslationJointType:
{
EulerAngles angles = node->GetEulerAngles(); // FUN_0041cfa0 (keep pitch/roll)
EulerAngles twisted(angles.pitch, twist, angles.roll); // yaw <- twist (index 1)
node->SetRotation(twisted); // FUN_0041d020
break;
}
default: // StaticJointType(3) / NULLJointType(-1)
break;
}
}
//
// @004b66b4 [CONFIDENT] -- write currentTwist into the two horizontal joints
// (main @0x278 + shadow @0x27C) when horizontalEnabled, and report the current
// elevation through `verticalOut`. Each joint is updated as a scalar channel
// (node type < 3) or a vector channel (types 4..5), matching the gyro pattern.
//
void
Torso::WriteJoints(Scalar &verticalOut)
{
verticalOut = currentElevation; // *param_2 = @0x1E4
if (!horizontalEnabled) // @0x250
{
return;
}
PushTwist(horizontalJointNode, currentTwist); // @0x278
PushTwist(horizontalShadowJointNode, currentTwist); // @0x27C
}
//
// @004b67ec [CONFIDENT] -- identical to WriteJoints minus the elevation output;
// used on the paths that only need to refresh the skeleton.
//
void
Torso::UpdateJoints()
{
if (!horizontalEnabled)
{
return;
}
PushTwist(horizontalJointNode, currentTwist);
PushTwist(horizontalShadowJointNode, currentTwist);
}
//
// @004b6918 [CONFIDENT] -- ease currentTwist back toward 0 by effectiveTwistRate
// * dt, clamping so it does not cross zero, and return True while still off
// centre (|currentTwist| > VelEps).
//
Logical
Torso::Recenter(Scalar time_slice)
{
Scalar step = effectiveTwistRate * time_slice; // @0x244
if (currentTwist > Zero)
{
currentTwist -= step;
if (currentTwist < 0.0f) currentTwist = 0.0f;
}
if (currentTwist < Zero)
{
currentTwist += step;
if (currentTwist > 0.0f) currentTwist = 0.0f;
}
return fabsf(currentTwist - Zero) > VelEps; // _DAT_004b6a18
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- Torso
//
// @004b6fec [CONFIDENT for the field list / classID / size; body linearised].
// Chains to PowerWatcher::CreateStreamedSubsystem (FUN_004b198c), stamps the
// resource (classID 0x0BC5 @+0x20, model size 0x158 @+0x24), reads the optional
// "TorsoHorizontalEnabled" flag (default True; the string "false" -> False), the
// six mandatory angular/limit scalars and the two acceleration scalars, then --
// only when enabled -- the two joint names, which must resolve in the model's
// "skeleton" file.
//
int
Torso::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *r,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (
!PowerWatcher::CreateStreamedSubsystem( // FUN_004b198c
model_file, model_name, subsystem_name,
r, subsystem_file, directories, passes
)
)
{
return False;
}
r->subsystemModelSize = sizeof(*r); // +0x24 = 0x158
r->classID = RegisteredClass::TorsoClassID; // +0x20 = 0x0BC5
if (passes == 1)
{
// "TorsoHorizontalEnabled": absent => True; the literal "false" => False.
const char *flag = 0;
if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalEnabled", &flag))
{
r->torsoHorizontalEnabled = True;
}
else
{
r->torsoHorizontalEnabled =
(stricmp(flag, "false") == 0) ? False : True; // FUN_004d4b58
}
}
#define REQ_SCALAR(NAME, FIELD) \
if (!model_file->GetEntry(subsystem_name, NAME, &r->FIELD) \
&& r->FIELD == Unset) \
{ DebugStream << subsystem_name << " missing " << NAME << "!"; return False; }
REQ_SCALAR("ButtonAccelerationPerSecond", buttonAccelerationPerSecond) // +0x150
REQ_SCALAR("ButtonAccelerationStartValue", buttonAccelerationStartValue)// +0x154
REQ_SCALAR("HorizontalRotationPerSecond", horizontalRotationPerSecond) // +0xF4
REQ_SCALAR("VerticalRotationPerSecond", verticalRotationPerSecond) // +0xF8
REQ_SCALAR("HorizontalLimitRight", horizontalLimitRight) // +0xFC
REQ_SCALAR("HorizontalLimitLeft", horizontalLimitLeft) // +0x100
REQ_SCALAR("VerticalLimitTop", verticalLimitTop) // +0x104
REQ_SCALAR("VerticalLimitBottom", verticalLimitBottom) // +0x108
if (r->torsoHorizontalEnabled)
{
const char *hj = "Unspecified";
if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalJoint", &hj)
&& strcmp(hj, "Unspecified") == 0)
{ DebugStream << subsystem_name << " missing TorsoHorizontalJoint!"; return False; }
if (strcmp(hj, "Unspecified") != 0) strcpy(r->torsoHorizontalJoint, hj); // +0x10C
const char *sj = "Unspecified";
if (!model_file->GetEntry(subsystem_name, "TorsoHorizontalShadowJoint", &sj)
&& strcmp(sj, "Unspecified") == 0)
{ DebugStream << subsystem_name << " missing TorsoHorizontalShadowJoint!"; return False; }
if (strcmp(sj, "Unspecified") != 0) strcpy(r->torsoHorizontalShadowJoint, sj); // +0x12C
const char *skeleton = 0;
if (!model_file->GetEntry("video", "skeleton", &skeleton))
{
DebugStream << model_name << " is missing skeleton file!";
return -1;
}
ReconSkeleton *skl = LoadSkeleton(directories, skeleton);
if (!skl->FindNode(r->torsoHorizontalJoint))
{
DebugStream << r->torsoHorizontalJoint << " not found in " << skeleton;
return -1;
}
// (shadow-joint lookup follows the same pattern)
}
#undef REQ_SCALAR
Check_Fpu();
return True;
}
//===========================================================================//
// WAVE 4 factory bridge -- Torso (factory case 0xBC5, "SinkSource" label).
// The real class at 0xBC5 (ctor @004b6b0c) is Torso; the factory built a
// HeatSinkSource RECON_SUBSYS stub in its place. Constructing the real Torso
// here is what lets the reconstructed twist -> skeleton path run. The object
// is read at RAW offsets externally (the gyro cross-link -> currentTwist@0x1D8,
// mech.cpp:740), so the compiled layout MUST match the binary 0x280 -- now
// compile-time proven by TorsoLayoutCheck (sizeof(Torso)==0x280) above.
//===========================================================================//
Subsystem *CreateTorsoSubsystem(Mech *owner, int id, void *seg)
{
return (Subsystem *) new (Memory::Allocate(0x280))
Torso(owner, id, (Torso::SubsystemResource *)seg, Torso::DefaultData);
}
//
// STEP 6 bridge -- expose the live torso twist (Torso::currentTwist == the binary
// torso+0x1d8) to mech.cpp / the cylinder damage table without pulling the Torso
// header (which collides with mech.cpp's local subsystem stubs) into mech.cpp.
// The roster torso @mech+0x438 is a Torso (ClassID 0xBC5); 0 when absent.
//
Scalar BTGetTorsoTwist(Subsystem *torso)
{
return torso ? ((Torso *)torso)->CurrentTwist() : 0.0f;
}
//
// Task #56 bridge -- ADDRESS of the live torso twist for the gyro's external-
// pitch pointer (binary bt_mech stream tail: gyro+0x258 = torso+0x1D8; the gyro
// damage-response @004b2980 reads it per hit). Same complete-type-TU pattern
// as BTGetTorsoTwist above.
//
Scalar *BTGetTorsoTwistAddr(Subsystem *torso)
{
return torso ? ((Torso *)torso)->CurrentTwistAddr() : 0;
}