Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
312 lines
15 KiB
C++
312 lines
15 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
|
|
|
|
//###########################################################################
|
|
//################# 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);
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// Subsystem virtual overrides (slots on vtable @00510abc)
|
|
//
|
|
public:
|
|
Logical HandleDeathMessage(Message &message); // slot 9, @004b2660 -> @004b179c
|
|
void ResetToInitialState(); // slot 10, @004b2678
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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
|
|
void WriteEyeJoint(); // @004b33e0 (@004b2eac thunks here)
|
|
void WriteMechJoint(); // @004b34ec
|
|
Logical UpdateAnimationNoise(Scalar time_slice);// @004b357c
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
// 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:
|
|
Scalar exageration; // @0x1D8 resource +0xF8 (impulse scale)
|
|
|
|
Vector3D eyeOrientation; // @0x1DC integrated cockpit "eye" angles
|
|
Vector3D eyeVelocity; // @0x1E8 d(eyeOrientation)/dt
|
|
Vector3D eyeLimitLow; // @0x200 = posSpring (clamp lo, +0x200/204/208)
|
|
Vector3D eyeLimitHigh; // @0x20C = negSpring (clamp hi, +0x20C/210/214)
|
|
Vector3D eyePosSpring; // @0x218 resource posSpring (+0x130)
|
|
Vector3D eyeNegSpring; // @0x224 resource negSpring (+0x13C)
|
|
Vector3D eyeSpringConstant; // @0x1F4? eyeSpring work (see ctor 0408744)
|
|
Vector3D eyeAccel; // @0x230 spring-damper accumulator
|
|
Vector3D eyeForce; // @0x23C scaled by time_slice
|
|
Vector3D eyeWork; // @0x248 scratch (cross/normalise)
|
|
|
|
Vector3D bodyOrientation; // @0x2B4 integrated body tip angles
|
|
Vector3D rotationSpringConstant; // @0x2C0 resource +0x148 (roll/yaw/pitch)
|
|
Vector3D rotationDampingConstant;// @0x2CC resource +0x154
|
|
Vector3D bodyLimitLow; // @0x2D8 = rotationPosSpring
|
|
Vector3D bodyLimitHigh; // @0x2E4 = rotationNegSpring
|
|
Vector3D bodyPosSpring; // @0x2F0 resource rotationPosSpring (+0x160)
|
|
Vector3D bodyNegSpring; // @0x2FC resource rotationNegSpring (+0x16C)
|
|
Vector3D bodyAccel; // @0x308 spring-damper accumulator
|
|
Vector3D bodyForce; // @0x314 scaled by time_slice
|
|
Vector3D bodyWork; // @0x320 scratch
|
|
|
|
Scalar animationOffset; // @0x390 (this[0xE4]) idle-noise phase, init 0
|
|
Scalar animationScale; // @0x394 (this[0xE5]) init 1.0f
|
|
Scalar animationPhase; // @0x398 (this[0xE6]) init 0
|
|
|
|
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
|