Files
BT411/game/reconstructed/gyro.hpp
T
arcattackandClaude Opus 5 ef8e449a17 #55 steps 0/1/2/4: the 1995 DEATH pass restored, the arg gate made real, and 5 subsystems the respawn sweep never reached
THE HEADLINE: the port's death sweep was a total no-op, and fixing it required
fixing the arg gate in the same commit -- otherwise corpses refill their ammo.

STEP 2 -- the authentic dispatch shape:
  * engine/MUNGA/SUBSYSTM.h: Subsystem::DeathShutdown gains the binary's
    universal base body { DeathReset(c) } (@004ad10e).  It was empty, and NO
    port class overrode it, so everything the 1995 game does at death was
    skipped entirely.
  * mech4.cpp death sweep now passes 0, not 1 (the binary's @0049fe0c arg) --
    the wreck shape: alarms/state settle, nothing refills.
  * ARG PROPAGATION (mandatory once the sweep runs): AmmoBin::DeathReset is now
    arg-gated exactly as @004bd26c -- refill only when arg != 0, ammoAlarm
    unconditional.  Ignoring the arg was harmless only while the sweep was
    dead; with it restored, every corpse would have re-armed.  Also forwarded
    in PoweredSubsystem / HeatSink / Condenser / HeatableSubsystem / Generator
    (each binary body forwards it -- verified addresses in the plan).

STEP 4 -- coverage for 5 classes that had an authentic slot-10 body but no
DeathReset, so the respawn sweep fell through to the empty base:
  Torso (this is Gitea #70 -- "loosing torso twist function after a death"),
  Gyroscope, Myomers, HUD, Seeker.

STEPS 0/1 -- observability + the David chain:
  * Three unconditional matchlog rows: DEAD_NOTIFY (mech + resolved link),
    PLAYER_LINK (player <-> vehicle), RESPAWN (mode/alive/zones/subsys/pos).
    A respawn was previously INVISIBLE in the matchlog -- night 3's analysis
    had to infer them from ammo arithmetic.
  * Mech::PlayerLinkMessageHandler + the death dispatch site: when the engine's
    one-shot registry lookup misses (no null check, no retry -> the whole
    death/respawn cycle silently swallowed), recover the SAME object via the
    reverse link the binary's own respawn branch walks (player+0x1FC ==
    playerVehicle).  Complete-type TU, no raw offsets.
  * deathPending cleared in the first-spawn branch (a latch carried in would
    permanently kill every later respawn -- the #57 class).

VERIFIED on the 2-pod rig (madcat vs thor, forced kills, 5 death/respawn cycles
per pod): build clean, ZERO new /FORCE unresolved externs from the 5 new
overrides; refill lines appear ONLY immediately before a Mech::Reset and NEVER
between a death and the next respawn (the corpse-refill regression this commit
had to pre-empt); all three probe rows present in both pods' matchlogs; every
DEAD_NOTIFY carried a non-null link.

DELIBERATELY DEFERRED (documented, not forgotten): the mechsub.cpp rename pair
(ResetToInitialState -> GenerateFault, ClearStatus -> the root reset @004ac22c),
deleting HeatableSubsystem::ResetToInitialState, and deleting RespawnRepair.
Those need the HeatableSubsystem vtable-slot-10 pre-flight the plan calls for,
and we have no binary image here to dump the vtable from -- guessing at it
risks the vptr-alias trap.  Next session with the decomp shards open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 08:09:31 -05:00

362 lines
18 KiB
C++

//===========================================================================//
// File: gyro.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: Gyroscope subsystem -- balance / orientation / tip-over model //
//---------------------------------------------------------------------------//
// 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 for this class; every declaration below
// is inferred from the decompiled bodies and -- crucially -- from the
// resource field-name string pool parsed by Gyroscope::CreateStreamedSubsystem
// (@004b3eb4). Those "missing <Name>!" diagnostics are GROUND TRUTH for the
// resource layout; the in-object member names are best-effort (flagged).
//
// Recovered binary facts (see gyro.cpp for per-method @ADDR evidence):
// - Gyroscope IS-A PowerWatcher (powersub.hpp); the ctor @004b3778 chains to
// the PowerWatcher base ctor (FUN_004b18a4) passing &Gyroscope::DefaultData
// (&DAT_0050fdb0). The full chain is therefore
// Gyroscope : PowerWatcher : HeatWatcher : MechSubsystem : Subsystem
// (NOT HeatableSubsystem -- the Watcher branch, re-based; see CLAUDE.md §10d)
// - vtable @00510abc ctor @004b3778 dtor @004b3e88
// - DefaultData @0050fdb0
// - RegisteredClass classID 0x0BC4 (resource +0x20), streamed model size
// 0x21C (resource +0x24) -- both stamped by CreateStreamedSubsystem.
//
// WHAT THE GYROSCOPE DOES (from the recovered simulation @004b275c + helpers):
// It is the mech's balance/orientation model. It carries a damage-dependent
// idle "sway" (the PercentageOn{Normal,Degradation,Destruction,Failure}
// targets, ramped at RotationPerSecond and clamped to the [MinAnimationNoise,
// MaxAnimationNoise] band), a full pitch/yaw/roll spring-damper that tips the
// body when off balance, a separate spring-damper that drives the cockpit
// "eye" joint, and a set of per-damage-type response curves (Trans / PitchRoll
// / Yaw / Vibration) that perturb the gyro when the mech is hit. The result
// is written into two skeleton joints (the "EyeJoint" and the "MechJoint").
// When the watched power source is dead (PowerWatcher), the heat model is off,
// or the subsystem is in the Failure heat state, it snaps to the destruction
// target instead of the normal one (-> visible tip / collapse).
//
#if !defined(GYRO_HPP)
# define GYRO_HPP
#if !defined(POWERSUB_HPP)
# include "powersub.hpp"
#endif
//##################### Forward Class Declarations #######################
class Mech;
class Joint; // engine skeleton node (JOINT.h); eye/mech joint targets
class Damage; // engine damage record (DAMAGE.h); ApplyDamageResponse input
//###########################################################################
//################# Gyroscope Model Resource ##########################
//###########################################################################
//
// Extends the PowerWatcher resource (which adds MinVoltagePercent @+0xF0).
// Field offsets and names are exactly those read by
// Gyroscope::CreateStreamedSubsystem @004b3eb4. Every Scalar below is primed
// to -1.0f on pass 1 and is mandatory (the parser fails with
// "<subsystem> missing <Name>!" if absent). The Rotation*Spring* angular
// fields are additionally multiplied by PI/180 (deg->rad, _DAT_004b5b24).
//
// Per-damage-type gyro response curve (4 named Scalars, +0x10 each). The
// recon first modelled this as a Vector4D; the engine Vector4D is incomplete
// here and the fields are addressed by name (Trans/PitchRoll/Yaw/Vibration),
// so a dedicated POD captures the layout exactly.
struct Gyroscope__DamageResponse
{
Scalar trans; // +0x0
Scalar pitchRoll; // +0x4
Scalar yaw; // +0x8
Scalar vibration; // +0xC
};
struct Gyroscope__SubsystemResource:
public PowerWatcher::SubsystemResource // ends 0xF4 after RESOURCE_AUDIT.md fix B
{
// +0xF4: a 4-byte record slot between the PowerWatcher base and "Exageration".
// The Gyroscope parser/ctor do not read it (verified: ctor reads 0xF8/0xFC/0x100/
// 0x104), but the record reserves it -- omitting it slid every field 4 bytes low
// (exageration -> 0xF4 instead of 0xF8). Keep the slot to match the binary.
Scalar _reserved_f4; // +0xF4 (unread record slot)
Scalar exageration; // +0xF8 "Exageration"
Scalar maxAnimationNoise; // +0xFC "MaxAnimationNoise"
Scalar minAnimationNoise; // +0x100 "MinAnimationNoise"
Scalar rotationPerSecond; // +0x104 "RotationPerSecond"
Scalar percentageOnNormal; // +0x108 "PercentageOnNormal"
Scalar percentageOnDestruction; // +0x10C "PercentageOnDestruction"
Scalar percentageOnDegradation; // +0x110 "PercentageOnDegradation"
Scalar percentageOnFailure; // +0x114 "PercentageOnFailure"
Vector3D springConstant; // +0x118 "SpringConstantX/Y/Z"
Vector3D dampingConstant; // +0x124 "DampingConstantX/Y/Z"
Vector3D posSpring; // +0x130 "PosSpringX/Y/Z"
Vector3D negSpring; // +0x13C "NegSpringX/Y/Z"
// angular spring/damping, ordered Roll(+0x148), Yaw(+0x14C), Pitch(+0x150)
Vector3D rotationSpringConstant; // +0x148 "RotationSpringConstantRoll/Yaw/Pitch"
Vector3D rotationDampingConstant;// +0x154 "RotationDampingConstantRoll/Yaw/Pitch"
// rotation limits, ordered Pitch(.x) / Yaw(.y) / Roll(.z), each *PI/180.
// RECON NOTE: originally guessed Point4D (quaternion-ish); the parser only
// reads three named components (Pitch/Yaw/Roll) so a Vector3D is exact.
Vector3D rotationPosSpring; // +0x160 "RotationPosSpringPitch/Yaw/Roll"
Vector3D rotationNegSpring; // +0x16C "RotationNegSpringPitch/Yaw/Roll"
char eyeJoint[32]; // +0x178 "EyeJoint" (skeleton node name)
char mechJoint[32]; // +0x198 "MechJoint" (skeleton node name)
// per-damage-type response: scalar multiplier + {Trans,PitchRoll,Yaw,Vibration}
Scalar collisionDamageMultiplier; // +0x1B8 "CollisionDamageMultiplier"
Scalar ballisticDamageMultiplier; // +0x1BC "BallisticDamageMultiplier"
Scalar explosiveDamageMultiplier; // +0x1C0 "ExplosiveDamageMultiplier"
Scalar laserDamageMultiplier; // +0x1C4 "LaserDamageMultiplier"
Scalar energyDamageMultiplier; // +0x1C8 "EnergyDamageMultiplier"
Gyroscope__DamageResponse collisionDamageResponse;// +0x1CC Trans/PitchRoll/Yaw/Vibration
Gyroscope__DamageResponse ballisticDamageResponse;// +0x1DC
Gyroscope__DamageResponse explosiveDamageResponse;// +0x1EC
Gyroscope__DamageResponse laserDamageResponse; // +0x1FC
Gyroscope__DamageResponse energyDamageResponse; // +0x20C
}; // sizeof == 0x21C (model size stamp)
static_assert(offsetof(Gyroscope__SubsystemResource, exageration) == 0xF8, "Gyroscope exageration must be at 0xF8");
static_assert(offsetof(Gyroscope__SubsystemResource, energyDamageResponse)== 0x20C, "Gyroscope energyDamageResponse must be at 0x20C");
static_assert(sizeof(Gyroscope__SubsystemResource) == 0x21C, "Gyroscope record must be 0x21C");
//###########################################################################
//############################# Gyroscope ###############################
//###########################################################################
class Gyroscope:
public PowerWatcher
{
friend struct GyroLayoutCheck; // compile-time offset lock (gyro.cpp)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support (DefaultData @0050fdb0, vtable @00510abc)
//
public:
static Derivation ClassDerivations;
static Receiver::MessageHandlerSet MessageHandlers;
static AttributeIndexSet AttributeIndex;
static SharedData DefaultData;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BT segment / base-state compatibility shims.
//
// CROSS-FAMILY: these accessors and segment-flag constants logically belong
// on the shared bases (Mech/Subsystem::GetSegmentFlags, HeatSink::HeatStateLevel
// / HeatModelOff, PoweredSubsystem::ElectricalStateLevel). Gyroscope derives
// from PowerWatcher (-> Subsystem), which in these headers exposes none of
// them, so they are backed locally here so this module compiles; the backing
// fields are written by the (cross-family) base simulation once the real
// accessors land. See report "CROSS-FAMILY NEEDS".
//
#if !defined(BT_SEGMENT_FLAG_CONSTS)
# define BT_SEGMENT_FLAG_CONSTS
enum {
SegmentCopyMask = 0x0C, // (flags & 0xC): 0 == master, 4 == damaged copy
SegmentLiveFlag = 0x01,
MasterHeatSinkFlag = 0x0100
};
#endif
public:
// BASE-CHAIN RE-BASE: the 4 shim BACKING FIELDS are removed (they over-sized
// the object + duplicated engine-base state); these accessors now read the
// REAL inherited base state, exactly like Torso (see CLAUDE.md §10d).
// (GetSegmentFlags removed -- unused; the ctor reads owner->simulationFlags.)
HeatSink::HeatState
HeatStateLevel() const { return (HeatSink::HeatState)heatAlarm.GetLevel(); } // inherited HeatWatcher
Logical
HeatModelOff() const { return simulationState == 1; /* Destroyed */ } // inherited MechSubsystem
PoweredSubsystem::ElectricalState
ElectricalStateLevel() const { return (PoweredSubsystem::ElectricalState)watchdogAlarm.GetLevel(); } // inherited PowerWatcher
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical TestClass(Mech&);
Logical TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Simulation Support
//
public:
typedef void
(Gyroscope::*Performance)(Scalar time_slice);
void
SetPerformance(Performance performance)
{
Check(this);
activePerformance = (Simulation::Performance)performance;
}
// @004b275c -- the registered Performance (PTR @0050fe08). Advances the
// idle sway toward the damage-state target then runs the two integrators.
void
GyroscopeSimulation(Scalar time_slice);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Damage / impulse hooks (called by the Mech damage path to kick the gyro)
//
public:
void ApplyDamageImpulse( // @004b2d8c
Scalar x, Scalar y, Scalar z, Scalar magnitude);
void ApplyDamageTorque( // @004b2de4
Scalar x, Scalar y, Scalar z, Scalar magnitude);
void ApplyVerticalImpulse( // @004b2e50
Scalar pitch, Scalar /*y*/, Scalar /*z*/, Scalar magnitude);
// @004b2980 -- the damage->gyro fan-out (task #56): normalize the hit
// direction (random horizontal if ~zero), rotate into the torso-twist
// frame, scale by the per-damage-type multiplier/response curves, clamp
// at 1.3, fire the four Apply* kicks. The binary passes the whole
// Damage BY VALUE (12 dwords, caller add esp,0x34) but never mutates it
// and never reads surfaceNormal/impactPoint -- const-ref is byte-equal.
void ApplyDamageResponse(const Damage &damage);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Per-frame joint-write dispatch (called from the MECH master performance
// tail, NOT from GyroscopeSimulation -- byte-verified: the binary calls
// WriteEyeJoint @0x4aaf74 / WriteMechJoint @0x4aaf83 inside the unexported
// Mech performance FUN_004a9b5c, after the animation pass, gated on
// !deathLatched (+ legAnimState!=0 for the eye writer), preceded by
// swayBias = mech sway accumulator). Bridge fns in gyro.cpp forward here.
//
public:
void SetSwayBias(Scalar v) { swayBias = v; }
void BindExternalPitch(Scalar *p) { if (p) externalPitchPtr = p; } // bt_mech tail: &torso pitch
void WriteEyeJoint(); // @004b33e0 (@004b2eac thunks here)
void WriteMechJoint(); // @004b34ec
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @00510abc)
//
public:
Logical HandleDeathMessage(Message &message); // slot 9, @004b2660 -> @004b179c
void ResetToInitialState(); // slot 10, @004b2678
// #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 Gyroscope__SubsystemResource SubsystemResource;
Gyroscope( // @004b3778
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~Gyroscope(); // @004b3e88
static int
CreateStreamedSubsystem( // @004b3eb4
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:
void IntegrateEyeJoint(Scalar time_slice); // @004b2ec0
void IntegrateBody(Scalar time_slice); // @004b30ec
Logical UpdateAnimationNoise(Scalar time_slice);// @004b357c (DEAD in the binary: zero callers -- kept unwired)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data. Byte offsets are those observed in the shipped object
// (this[n] == @0x(4n)). Names are best-effort from usage; the resource
// provenance (where a field is copied straight out of the resource) is
// noted. PowerWatcher/HeatWatcher/HeatableSubsystem base data occupies the
// low offsets and is NOT redeclared here.
//
protected:
// BINARY-EXACT layout (ctor @004b3778 field map, task #56 — every byte
// 0x1D8..0x3CF is ctor-initialised in the binary; the old recon layout
// mislabelled 0x1DC/0x1E8/0x1F4 and omitted 0x254..0x2B3 entirely, which
// mis-offset everything after it AND left the integrator state as 0xCD fill
// (the NaN-to-root-joint revert). See context/cockpit-view.md + the
// wf_f52abaff synthesis for the per-line decomp evidence.
Scalar exageration; // @0x1D8 res +0xF8 (impulse scale)
Vector3D eyePosition; // @0x1DC eye TRANSLATION spring state -> jointeye
// translation (steady offset + hit bounce); ZERO
Vector3D springConstant; // @0x1E8 res +0x118 SpringConstantX/Y/Z
Vector3D dampingConstant; // @0x1F4 res +0x124 DampingConstantX/Y/Z
Vector3D eyeClampUpper; // @0x200 = posSpring (min-clamped against)
Vector3D eyeClampLower; // @0x20C = negSpring (max-clamped against)
Vector3D posSpring; // @0x218 res +0x130
Vector3D negSpring; // @0x224 res +0x13C
Vector3D eyeForce; // @0x230 accumulator (damage-impulse target; NOT
// cleared per frame -- carries by design)
Vector3D eyeVelocity; // @0x23C
Vector3D eyeWork; // @0x248 scratch
Scalar spare0; // @0x254 = 0
Scalar *externalPitchPtr; // @0x258 ctor: &spare0; post-stream re-pointed to
// &torso pitch (bt_mech tail); read by the
// damage-response @004b2980
Scalar workMatrix[12]; // @0x25C 3x4 work matrix (identity; damage frame)
Vector3D placePos; // @0x28C placement pos (zero)
Scalar placeQuat[4]; // @0x298 placement quat {0,0,0,1}
Vector3D placeRot; // @0x2A8 placement rot scratch (zero)
Vector3D bodyOrientation; // @0x2B4 body ROTATION spring state -> jointeye rotation; ZERO
Vector3D rotationSpringConstant; // @0x2C0 res +0x148 (Roll,Yaw,Pitch order)
Vector3D rotationDampingConstant;// @0x2CC res +0x154
Vector3D bodyClampUpper; // @0x2D8 = 2.0 * rotationPosSpring
Vector3D bodyClampLower; // @0x2E4 = 2.0 * rotationNegSpring
Vector3D rotationPosSpring; // @0x2F0 res +0x160 (Pitch,Yaw,Roll; radians)
Vector3D rotationNegSpring; // @0x2FC res +0x16C
Vector3D bodyForce; // @0x308 accumulator (torque/vertical impulse target)
Vector3D bodyVelocity; // @0x314
Vector3D bodyWork; // @0x320 scratch
Scalar damageMultiplier[5]; // @0x32C res +0x1B8..0x1C8 (Coll/Ball/Expl/Laser/Energy)
Gyroscope__DamageResponse
damageResponse[5]; // @0x340 res +0x1CC..0x21B (5 x {trans,pitchRoll,yaw,vibration})
// @0x390 (this[0xE4..0xE6]) -- the VIBRATION AXIS: ctor init (0,1,0) =
// straight up. Read as ONE Vector3D by ApplyDamageResponse @4b2d2b (the
// second ApplyDamageImpulse = the vibration shake). [T1 re-disassembled]
// (Was mis-split as scalars animationOffset/animationScale/animationPhase
// -- the 0/1/0 "init pattern" was this unit vector all along.)
Vector3D vibrationDirection; // @0x390 init (0.0f, 1.0f, 0.0f)
Scalar maxAnimationNoise; // @0x39C resource +0xFC
Scalar minAnimationNoise; // @0x3A0 resource +0x100
Scalar rotationPerSecond; // @0x3A4 resource +0x104
Scalar swayBias; // @0x3A8 (this[0xEA]) added to target, init 0
Scalar percentageOnNormal; // @0x3AC resource +0x108
Scalar percentageOnDestruction;// @0x3B0 resource +0x10C
Scalar percentageOnDegradation;// @0x3B4 resource +0x110
Scalar percentageOnFailure; // @0x3B8 resource +0x114
Scalar swayAngle; // @0x3BC (this[0xEF]) current idle sway value
Scalar swayVelocity; // @0x3C0 (this[0xF0]) noise ramp timer
int swayActive; // @0x3C4 (this[0xF1]) noise state flag
Joint *eyeJointNode; // @0x3C8 (this[0xF2]) resolved EyeJoint
Joint *mechJointNode; // @0x3CC (this[0xF3]) resolved MechJoint
};
#endif