Files
BT411/game/reconstructed/torso.hpp
T
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00

349 lines
18 KiB
C++

//===========================================================================//
// File: torso.hpp //
// 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; declarations are inferred from the
// decompiled bodies and from the resource field-name string pool parsed by
// Torso::CreateStreamedSubsystem (@004b6fec) -- the "missing <Name>!"
// diagnostics are GROUND TRUTH for the resource layout. In-object member names
// are best-effort (flagged in torso.cpp).
//
// Recovered binary facts (see torso.cpp for per-method @ADDR evidence):
// - Torso IS-A PowerWatcher (powersub.hpp); the ctor @004b6b0c chains to the
// PowerWatcher base ctor (FUN_004b18a4) passing &Torso::DefaultData
// (&DAT_00510af8). Full chain:
// Torso : PowerWatcher : HeatWatcher : MechSubsystem : Subsystem
// (NOT HeatableSubsystem -- the Watcher branch is separate from the
// HeatSink/PoweredSubsystem heat leaves; they share only MechSubsystem.)
// - vtable @0051103c ctor @004b6b0c dtor @004b6fc0
// - DefaultData @00510af8
// - RegisteredClass classID 0x0BC5 (resource +0x20), streamed model size
// 0x158 (resource +0x24).
//
// WHAT THE TORSO DOES (from the recovered simulation @004b5cf0 + helpers):
// It models torso twist (horizontal / yaw) and torso elevation (vertical /
// pitch) aim. Player turn/elevate commands ramp an angular rate (with a
// button-acceleration curve from a start value up to a cap), integrate into
// horizontal/vertical aim angles that are clamped to per-axis software limits
// (HorizontalLimitLeft/Right, VerticalLimitTop/Bottom), auto-recenter the
// twist when released, and write the result into two skeleton joints
// (TorsoHorizontalJoint and its TorsoHorizontalShadowJoint). All angular
// resource values are stored in radians (deg * PI/180). When the watched
// power source is dead, not Ready, or in the Failure heat state, the effective
// rate is zeroed (the torso freezes).
//
#if !defined(TORSO_HPP)
# define TORSO_HPP
#if !defined(POWERSUB_HPP)
# include "powersub.hpp"
#endif
//##################### Forward Class Declarations #######################
class Mech;
class Joint; // engine skeleton node (JOINT.h); the twist target
//###########################################################################
//################# Torso Model Resource ##############################
//###########################################################################
//
// Extends the PowerWatcher resource (MinVoltagePercent @+0xF0). Field offsets
// and names are exactly those read by Torso::CreateStreamedSubsystem @004b6fec.
// The angular Scalars are stored in radians (the ctor multiplies the parsed
// degree values by PI/180); they are primed to -1.0f on pass 1 and mandatory.
//
struct Torso__SubsystemResource:
public PowerWatcher::SubsystemResource
{
Scalar horizontalRotationPerSecond; // +0xF4 "HorizontalRotationPerSecond"
Scalar verticalRotationPerSecond; // +0xF8 "VerticalRotationPerSecond"
Scalar horizontalLimitRight; // +0xFC "HorizontalLimitRight"
Scalar horizontalLimitLeft; // +0x100 "HorizontalLimitLeft"
Scalar verticalLimitTop; // +0x104 "VerticalLimitTop"
Scalar verticalLimitBottom; // +0x108 "VerticalLimitBottom"
char torsoHorizontalJoint[32]; // +0x10C "TorsoHorizontalJoint"
char torsoHorizontalShadowJoint[32]; // +0x12C "TorsoHorizontalShadowJoint"
Logical torsoHorizontalEnabled; // +0x14C "TorsoHorizontalEnabled" (default True)
Scalar buttonAccelerationPerSecond; // +0x150 "ButtonAccelerationPerSecond"
Scalar buttonAccelerationStartValue; // +0x154 "ButtonAccelerationStartValue"
}; // sizeof == 0x158 (model size stamp)
static_assert(offsetof(Torso__SubsystemResource, horizontalRotationPerSecond) == 0xF4, "Torso horizontalRotationPerSecond must be at 0xF4");
static_assert(offsetof(Torso__SubsystemResource, buttonAccelerationStartValue)== 0x154, "Torso buttonAccelerationStartValue must be at 0x154");
static_assert(sizeof(Torso__SubsystemResource) == 0x158, "Torso record must be 0x158");
//###########################################################################
//############################# Torso ###################################
//###########################################################################
class Torso:
public PowerWatcher
{
friend struct TorsoLayoutCheck; // compile-time offset locks (torso.cpp STEP 3)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support (DefaultData @00510af8, vtable @0051103c)
//
public:
static Derivation ClassDerivations;
static SharedData DefaultData;
static Receiver::MessageHandlerSet MessageHandlers;
static AttributeIndexSet AttributeIndex;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BT segment / base-state / joint compatibility shims.
//
// CROSS-FAMILY: these accessors, the segment-flag constants, the joint
// helpers and the PowerWatcher base-method forwards logically belong on the
// shared bases (Mech::GetSegmentFlags / Mech::ResolveJoint, HeatSink::
// HeatStateLevel / HeatModelOff, PoweredSubsystem::ElectricalStateLevel, the
// skeleton-node writer, the Simulation dirty bit + creation/current time
// accessors, and PowerWatcher::ResetToInitialState / HandleDeathMessage /
// UpdateWatch). Until those families expose them they are backed locally so
// this module compiles; see report "CROSS-FAMILY NEEDS".
//
#if !defined(BT_SEGMENT_FLAG_CONSTS)
# define BT_SEGMENT_FLAG_CONSTS
enum {
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == primary, 4 == damaged copy
SegmentLiveFlag = 0x01,
MasterHeatSinkFlag = 0x0100
};
#endif
public:
// BASE-CHAIN RE-BASE: the 7 shim BACKING FIELDS are removed so the object
// matches the binary 0x280 layout (currentTwist@0x1D8); these accessors now
// read the REAL inherited base state:
// HeatStateLevel <- HeatWatcher::heatAlarm level (this+0x140)
// ElectricalState <- PowerWatcher::watchdogAlarm level (this+0x198)
// HeatModelOff <- MechSubsystem::simulationState==Destroyed (this+0x40)
// Creation/Current <- engine Simulation::lastPerformance (this+0x14/0x10)
// the net/copy timing is dormant in single-player;
// equal reads give a 0 window == the shipped master path.
// SetMovedFlag -> engine Simulation::ForceUpdate() dirty bit (this+0x18)
// (GetSegmentFlags removed -- unused; the ctor reads owner->simulationFlags.)
HeatSink::HeatState HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); }
Logical HeatModelOff() const { return simulationState == 1; /* Destroyed */ }
PoweredSubsystem::ElectricalState ElectricalStateLevel() const{ return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); }
LWord GetCreationTime() const { return (LWord)(long)lastPerformance; }
LWord GetCurrentTime() const { return (LWord)(long)lastPerformance; }
// Current horizontal (yaw) torso aim in radians (binary torso+0x1d8); the
// damage-table PieSlice wheel reads this when RotateWithTorso is set.
Scalar CurrentTwist() const { return currentTwist; }
// Address of the twist scalar (task #56: the binary bt_mech stream tail
// re-points gyro+0x258 to torso+0x1D8; the gyro damage-response reads it).
Scalar* CurrentTwistAddr() { return &currentTwist; }
protected:
// Resolve a named skeleton node to its live Joint* via the owning Mech
// (the ctor @004b6b0c inlines Mech::ResolveJoint == FUN_00424b60). Defined
// out-of-line in torso.cpp -- needs the complete Mech type.
Joint* ResolveJoint(const char *joint_name);
// Write one twist (yaw) scalar into a resolved skeleton node, preserving its
// other DOF -- the @004b66b4 inner block (hinge -> SetRotation(Radian);
// ball -> SetRotation(EulerAngles) with yaw <- twist).
void PushTwist(Joint *node, Scalar value);
// Simulation dirty / replicate bit -> real engine Simulation::ForceUpdate().
void SetMovedFlag() { ForceUpdate(); }
// Local helper: "is value pinned at this software limit?".
Logical AtLimit(Scalar value, Scalar limit) const { return fabsf(value - limit) <= 0.0001f; }
// CROSS-FAMILY delegate shims (STEP-4): UpdateWatch now chains to the real
// PowerWatcher per-tick watch body (@004b181c, task #57) -- this is what
// raises watchdogAlarm to Ready and un-gates the twist rate. The other
// two remain no-ops (no layout impact).
void WatcherResetToInitialState() {}
Logical WatcherHandleDeathMessage(Message & /*m*/) { return False; }
void WatcherUpdateWatch() { UpdateWatch(); } // FUN_004b181c (body: powersub.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical TestClass(Mech&);
Logical TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Command interface (set by MechControlsMapper each frame -- inputs @0x1F8)
//
public:
// Command roles verified against the raw decomp @004b5cf0 (part_013.c
// 4522-4571): @0x1F8/@0x1FC drive ELEVATION (0x1e4, clamp vertTop/Bottom);
// @0x200/@0x204 drive TWIST (0x1d8, clamp horizLimitLeft/Right); @0x208
// recenters twist. (The earlier draft had elevation/twist swapped.)
enum {
ElevateUpCommand, // @0x1F8 elevation + (clamp verticalLimitTop)
ElevateDownCommand, // @0x1FC elevation - (clamp verticalLimitBottom)
TwistLeftCommand, // @0x200 twist + (clamp horizontalLimitLeft)
TwistRightCommand, // @0x204 twist - (clamp horizontalLimitRight)
CenterCommand // @0x208 recenter twist
};
// Analog (RIO/stick) aim demand written by MechControlsMapper::Interpret-
// Controls (@0x1F0 twist, @0x1F4 elevation); proportional, no button ramp.
void SetAnalogTwistAxis(Scalar v) { analogTwistAxis = v; }
void SetAnalogElevationAxis(Scalar v) { analogElevationAxis = v; }
void CommandRecenter() { centerCommand = 1; } // @0x208 (Basic-mode re-center)
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
Logical GetHorizontalEnabled() const { return horizontalEnabled; } // @0x250 (mapper free-aim gate @004afd10)
// Reachable horizontal (yaw) half-arc the guns can be brought to bear by
// twisting the torso: the wider of the two software limits (radians), or 0
// when horizontal twist is disabled (e.g. the Blackhawk, TorsoHorizontalEnabled=0).
// This is the AUTHENTIC, data-driven weapon traverse -- weapons carry no arc
// field; the torso mount is what lets a mech point its guns off dead-ahead.
Scalar GetHorizontalReach() const
{
if (!horizontalEnabled) return 0.0f;
Scalar l = fabsf(horizontalLimitLeft), r = fabsf(horizontalLimitRight);
return (l > r) ? l : r;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
typedef void
(Torso::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
void TorsoSimulation(Scalar time_slice); // @004b5cf0 (master, PTR @00510c10)
void TorsoCopySimulation(Scalar time_slice); // @004b65f8 (damaged copy, PTR @00510c1c)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @0051103c)
//
public:
// Update-record pair (task #57 direction fix): the ENGINE's semantics
// [T0 SIMULATE.cpp] are Write = PRODUCE the outgoing record (master),
// Read = APPLY the incoming record (replicant). @004b6a78 (slot 6,
// base chain 41bd34 = ReadUpdateRecord) CONSUMES record+0x10/14/18 --
// it is the READ side, previously mislabeled Write; the real WRITE
// side is @004b6a1c (slot 7, base chain 41c500, 3 args), which Ghidra
// missed -- recovered from raw disasm. With the old flip the master
// never serialized twist and clobbered its own fields from the
// uninitialized stream buffer (the replicant's 0xCDCDCDCD twist pin).
void ReadUpdateRecord(UpdateRecord *message); // slot 6, @004b6a78 (apply)
void WriteUpdateRecord(UpdateRecord *message, int update_model); // slot 7, @004b6a1c (serialize)
Logical HandleDeathMessage(Message &message); // slot 9, @004b5be0 -> @004b179c
void ResetToInitialState(); // slot 10, @004b5bf8
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef Torso__SubsystemResource SubsystemResource;
Torso( // @004b6b0c
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~Torso(); // @004b6fc0
static int
CreateStreamedSubsystem( // @004b6fec
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Internal model helpers
//
protected:
Logical ComputeTargetTwist(); // @004b6510 (returns "still slewing")
void WriteJoints(Scalar &verticalOut); // @004b66b4
void UpdateJoints(); // @004b67ec
Logical Recenter(Scalar time_slice); // @004b6918 (returns "still off-centre")
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data. Byte offsets are those observed in the shipped object
// (this[n] == @0x(4n)); names are best-effort. Angular values are radians.
// PowerWatcher/HeatWatcher/MechSubsystem base data occupies the low
// offsets and is NOT redeclared here.
//
protected:
Scalar currentTwist; // @0x1D8 current horizontal (yaw) aim
Scalar horizontalLimitRight; // @0x1DC resource +0xFC (rad)
Scalar horizontalLimitLeft; // @0x1E0 resource +0x100 (rad)
Scalar currentElevation; // @0x1E4 current vertical (pitch) aim
Scalar twistVelocity; // @0x1E8 d(currentTwist)/dt
Scalar elevationVelocity; // @0x1EC d(currentElevation)/dt
Scalar analogTwistAxis; // @0x1F0 RIO/stick twist demand
Scalar analogElevationAxis; // @0x1F4 RIO/stick elevation demand
int elevateUpCommand; // @0x1F8 digital elevation-up (>0 = active)
int elevateDownCommand; // @0x1FC digital elevation-down
int twistLeftCommand; // @0x200 digital twist-left
int twistRightCommand; // @0x204 digital twist-right
int centerCommand; // @0x208 digital recenter
int statusFlags; // @0x20C 0/1/2 (limit/active state)
Scalar buttonAccelerationPerSecond;// @0x210 resource +0x150
Scalar buttonAccelerationStart; // @0x214 resource +0x154
Scalar targetTwist; // @0x218 ComputeTargetTwist output
Scalar twistAtUpdate; // @0x21C twist snapshot at last net update
Scalar verticalLimitTop; // @0x220 resource +0x104 (rad)
Scalar verticalLimitBottom; // @0x224 resource +0x108 (rad)
Scalar elevationCenter; // @0x228 derived limit copy
Scalar elevationHalfBottom; // @0x22C = verticalLimitBottom * 0.5f
Scalar twistCenterHigh; // @0x230 derived limit copy
Scalar twistCenterLow; // @0x234 derived limit copy
Scalar twistRate; // @0x238 net-synced angular rate
Scalar baseTwistRate; // @0x23C resource +0xF4 (rad/s)
Scalar baseElevationRate; // @0x240 resource +0xF8 (rad/s)
Scalar effectiveTwistRate; // @0x244 baseTwistRate, gated by power/heat
Scalar effectiveElevationRate; // @0x248 baseElevationRate, gated by power/heat
int isDamagedCopy; // @0x24C 0 = live master, 1 = copy
Logical horizontalEnabled; // @0x250 resource +0x14C
LWord lastUpdateTime; // @0x254 ms timestamp of last command sample
int hitElevTop; // @0x258 elevation-top limit latch (vs verticalLimitTop)
int hitElevBottom; // @0x25C elevation-bottom limit latch (vs verticalLimitBottom)
int hitTwistLeft; // @0x260 twist-left limit latch (vs horizontalLimitLeft)
int hitTwistRight; // @0x264 twist-right limit latch (vs horizontalLimitRight)
int buttonRampActive; // @0x268
Scalar buttonRamp; // @0x26C current button-accel multiplier
int _reserved0x270; // @0x270 binary reserves a 4-byte slot here
// (cleared by ResetToInitialState @004b5bf8);
// present so recenterActive lands at 0x274.
int recenterActive; // @0x274
Joint *horizontalJointNode; // @0x278 resolved TorsoHorizontalJoint
Joint *horizontalShadowJointNode; // @0x27C resolved TorsoHorizontalShadowJoint
}; // sizeof == 0x280 (own block 0x1D8..0x280); locked by static_asserts below.
//
// BASE-CHAIN RE-BASE: the 7 CROSS-FAMILY shim backing fields that used to sit
// here (segmentFlags/heatModelOff/heatStateLevel/electricalState/creationTime/
// currentTime/movedFlag) are DELETED -- they over-sized the object by 28 bytes
// and duplicated engine-base state. Their accessors now read the real inherited
// members (HeatWatcher::heatAlarm, PowerWatcher::watchdogAlarm, MechSubsystem::
// simulationState, engine Simulation::lastPerformance/ForceUpdate). See above.
//
#endif