Files
BT411/game/reconstructed/torso.hpp
T
Joe DiPrimaandClaude Opus 5 4ccc2a7eec control mode: Basic re-centre used the STICKY held-button cell -- and the elevation-limit swap was never ported
Sauron: "toggled through advanced controls from standard to advanced and back
to standard -- lost torso control."

The cycle is 0 Basic -> 1 Standard -> 2 Veteran -> WRAPS TO BASIC, so getting
from "advanced" back to Standard PASSES THROUGH BASIC, whose arm re-centres the
torso.  @004afbe0 is a complete spec and the port got three things wrong:

    iVar1 = mech+0x438 (TORSO)        iVar2 = mech+0x5b4 (HUD)
    if (mode == 0) {                                 // BASIC
        *(iVar1 + 0x1f0) = 0;                        // analogTwistAxis
        *(iVar1 + 0x274) = 1;                        // recenterActive
        *(iVar1 + 0x220) = *(iVar1 + 0x228);         // vertLimitTop
        *(iVar1 + 0x224) = *(iVar1 + 0x22c);         // vertLimitBottom
        *(iVar2 + 0x2a0) = 1;                        // HUD flickerActive
    } else if (mode - 1U < 2) {                      // STANDARD/VETERAN
        *(iVar1 + 0x220) = *(iVar1 + 0x230);
        *(iVar1 + 0x224) = *(iVar1 + 0x234);
    }

1. WRONG CELL.  Basic called CommandRecenter() -> centerCommand (@0x208), the
   HELD-BUTTON cell: TorsoSimulation re-arms recenterActive from it EVERY frame
   it is non-zero, and only the input path clears it -- a mode switch has no
   button release to follow.  Digital twist commands are processed BEFORE the
   centerCommand block, so while it is set they are overridden as fast as they
   are applied: the torso stops responding.  The binary sets recenterActive
   (@0x274) directly -- a ONE-SHOT that self-clears on settle
   (`recenterActive = Recenter(dt)`) and is cancelled by any twist input.
2. THE ELEVATION-LIMIT SWAP WAS MISSING ENTIRELY.  Two authored pairs exist --
   BASIC @0x228/@0x22C (full top, HALF bottom) vs STANDARD/VETERAN @0x230/@0x234
   (the full pair) -- and all four members were ctor-written and read by NOTHING.
   Basic never restricted downward travel; the assisted modes never restored it.
3. Basic also raises the HUD's flickerActive (@0x2A0) so the horizon re-settles
   with the torso it just re-centred.  Not ported.  (New BTSetHudFlickerActive
   bridge in hud.cpp -- mechmppr sees Subsystem*, not HUD.)
Also removed an invented SetAnalogElevationAxis(0); the binary zeroes only 0x1F0.

MEASURED A/B (scratchpad/night13/modecycle.sh, LEGACY=1 for the old path;
BT_LEGACY_MODE_RECENTER=1 is the revert switch):

    ctrCmd=1 samples   legacy 26   fixed 0
    vLim pairs         fixed run shows BOTH -- (-0.698..0.349) = -40..20 deg
                       assisted, and (-0.349..0.349) = -20..20 deg Basic.
                       Before this commit only the ctor pair ever appeared.

WHAT IS *NOT* PROVEN.  I did not reproduce Sauron's PERMANENT loss.  In this
bench the legacy latch is periodic, not sticky:

    ..........LLLL......LLLL......LLLL......LLLL......LLLL......LL

because the desktop key bridge writes centerCommand every frame and zeroes it
when no button is held, so it self-recovers.  The torso IS locked while the cell
is set, which is the symptom -- but whether it stays locked depends on the input
path OWNING that cell.  On the glass/pad route (Sauron's) nothing may clear it,
which would make it permanent.  So: mechanism fixed and binary-grounded, exact
field persistence unverified.  Field-verify by cycling modes on a pad build.

Probe: the BT_TORSO_LOG gate line now carries ctrCmd / recen / vLim.
Bench hook: BT_MODECYCLE_EVERY=<n> cycles the mode from the mapper (the pod's
own route is console key 0x13d -- not a RIO button, so BT_BTNTEST cannot press
it, and mech4's BT_MODECYCLE_TEST counter did not advance in a solo run).
NOTE the bench needs BT_KEY_BRIDGE=1: with a PadRIO present the key-bridge
block that consumes the cycle is skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
2026-08-08 10:36:36 -05:00

421 lines
22 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;
// (AUDIO_FIDELITY F16) the binary Torso attribute table [T1, rows
// keyed on the SpeedOfTorsoHorizontal string]: dense ids 3..15
// starting at MechSubsystem::NextAttributeID (HeatWatcher/PowerWatcher
// publish nothing -- binary parity). The authored torso-twist servo
// whir reads SpeedOfTorsoHorizontal (scale: pitch over 0.5..0.9 +
// start/stop trigger at 0.25 -- the |yaw rate| the binary abs's) and
// MotionState (match ==2 = limit-hit frame -> the stop clunk).
enum {
RotationOfTorsoVerticalAttributeID = MechSubsystem::NextAttributeID, // 3 @0x1E4 currentElevation
RotationOfTorsoHorizontalAttributeID, // 4 @0x1D8 currentTwist
HorizontalLimitRightAttributeID, // 5 @0x1DC
HorizontalLimitLeftAttributeID, // 6 @0x1E0
SpeedOfTorsoVerticalAttributeID, // 7 @0x1EC elevationVelocity (|rate|)
SpeedOfTorsoHorizontalAttributeID, // 8 @0x1E8 twistVelocity (|rate|)
StickPositionAttributeID, // 9 @0x1F0 analogTwistAxis
TorsoUpAttributeID, // 10 @0x1F8 elevateUpCommand
TorsoDownAttributeID, // 11 @0x1FC elevateDownCommand
TorsoLeftAttributeID, // 12 @0x200 twistLeftCommand
TorsoRightAttributeID, // 13 @0x204 twistRightCommand
TorsoCenterAttributeID, // 14 @0x208 centerCommand
MotionStateAttributeID, // 15 @0x20C statusFlags
NextAttributeID
};
private:
static const IndexEntry AttributePointers[];
public:
static AttributeIndexSet& GetAttributeIndex();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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)
// Current <- engine Simulation::lastPerformance (this+0x10)
// LastUpdate <- engine Simulation::lastUpdate (this+0x14)
// #67 (2026-07-27): both accessors used to return
// lastPerformance ("dormant in single-player") --
// which zeroed the replicant torso's dead-reckoning
// window (ComputeTargetTwist: elapsed = current -
// lastUpdate) and broke remote torso twist in MP
// (mechs stuck twisted / muzzles at mech centre).
// The engine maintains BOTH fields exactly as the
// binary expects: WriteUpdateRecord stamps
// lastUpdate=lastPerformance on the master,
// ReadUpdateRecord stamps lastUpdate=Now() on the
// replicant (SIMULATE.cpp:276/296). Mover -- engine
// code reading these fields natively -- is why LEGS
// replicated fine while the torso did not.
// 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 GetLastUpdateTime() const { return (LWord)(long)lastUpdate; }
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; }
// Current vertical (pitch) torso aim in radians (binary torso+0x1e4) --
// elevation-axis bring-up diagnostics (2026-07-18).
Scalar CurrentElevation() const { return currentElevation; }
Scalar ElevationVelocity() const { return elevationVelocity; }
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 HELD button -- writer MUST clear it
void ClearRecenterCommand() { centerCommand = 0; } // button released (writer-owned state)
// ⚠ centerCommand (@0x208) is a HELD-BUTTON cell: TorsoSimulation re-arms
// `recenterActive` from it EVERY frame it is non-zero, and only the input
// path clears it. Do NOT use CommandRecenter() for a one-shot recenter --
// nothing releases it and the torso re-centres forever, which reads to the
// pilot as "lost torso control" (Sauron, control-mode cycle).
//
// The one-shot the mode switch actually wants is recenterActive (@0x274)
// itself: TorsoSimulation runs `recenterActive = Recenter(dt)`, so it
// SELF-CLEARS on settle, and any twist input cancels it. This is exactly
// what the binary writes -- `*(torso + 0x274) = 1` @004afbe0.
void BeginRecenterOnce() { recenterActive = 1; } // @0x274 one-shot (@004afbe0)
// The TWO authored elevation-limit pairs the control mode swaps between
// (@004afbe0). BASIC gets @0x228/@0x22C (full top, HALF bottom -- reduced
// downward travel); STANDARD/VETERAN get @0x230/@0x234 (the full authored
// pair). Before 2026-08-08 all four were written by the ctor and never
// read by anything -- the port simply never implemented the swap.
void ApplyBasicElevationLimits()
{ verticalLimitTop = elevationCenter; verticalLimitBottom = elevationHalfBottom; }
void ApplyAssistedElevationLimits()
{ verticalLimitTop = twistCenterHigh; verticalLimitBottom = twistCenterLow; }
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
// #55 step 4: the respawn sweep (Mech::Reset -> DeathReset) never
// reached this class -- it fell through to the empty Subsystem base, so
// this subsystem kept its previous life's state. Chains our own
// authentic slot-10 body.
void DeathReset(int reset_command);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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