Files
BT412/game/reconstructed/mechsub.cpp
T
arcattackandClaude Opus 4.8 12c4254aae gauge-complete P4a: heat-leaf data fixes (radar heat-degradation authentic)
The gauge-complete-decode workflow found the "deep heat-leaf byte-exact re-base" is
STALE -- the leaf layout already shipped byte-exact (alarm-unification/P7), and the
real work is a small misidentified-field data fix (highest value, lowest risk):

- Sensor radarPercent read the INHERITED heatEnergy (~1.3e7) -> hugely negative ->
  needed a bring-up guard forcing it to 1.0.  The binary (@004b1c4c:1829) reads
  *(this[0x38]+0x158) = the subsystem's OWN DamageZone::damageLevel [0,1] (0 intact ..
  1 destroyed).  Fixed: radarPercent = RadarBaseline - GetSubsystemDamageLevel(); guard
  DELETED (damageLevel is engine-clamped).  So the radar authentically degrades with
  sensor structural damage (RadarBaseline-damage, x0.5 on DegradationHeat, 0 on Failure).
- New databinding-SAFE accessor MechSubsystem::GetSubsystemDamageLevel()/Set... reads the
  ENGINE DamageZone::damageLevel NAMED member (NOT the ReconDamageZone proxy, whose
  offset-0 structureLevel aliases the vtable ptr as a float = garbage).
- HeatSink::HandleMessage msg==1: same misidentification -> SetSubsystemDamageLevel(0.5)
  (was linkedSinks->heatEnergy=0.5, wrong object+field).
- MyomerCluster seek lamp: subsys+0x800 (OOB for 0x358 Myomers) -> currentSeekVoltageIndex
  @0x320 (the Emitter+0x3f0 analog; INFERRED -- ctor not in the assert-anchored export).
- numericSpeed (~225 vs 61.5): verified NOT A BUG (NumericDisplaySpeed applies x3.6; the
  feed is correct u/s) -- closed, no code.

No layout change (already byte-exact); pure data-read retargets.  Verified DBASE+dev
gauges: no crash, combat un-regressed (TARGET DESTROYED).  (radarPercent degrade is only
OBSERVABLE once per-subsystem damage routes to each engine DamageZone -- a separate task;
today the sensor is undamaged so radarPercent = full 1.0, which is correct.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:03:07 -05:00

472 lines
17 KiB
C++

//===========================================================================//
// File: mechsub.cpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: MechSubsystem -- BT damage-coupled base for all mech subsystems //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (BEST-EFFORT). Behaviour follows the
// Ghidra pseudo-C for the cluster @0x4ac07c..@0x4ac9ec. No function in the
// cluster is linker-tagged (all file=?); the module name "mechsub.cpp" is taken
// from the surviving MECHTECH.HPP `#include <mechsub.hpp>`. This cluster was
// previously attributed to heat.cpp's "Subsystem/HeatableSubsystem base"; the
// classID 0xBBB / model 0xE4 streamer @0x4ac9ec, the "MechSubsystem"
// Derivation name (@0x50def5), the DamageZone coupling, and the absence of any
// thermal field access show it is MechSubsystem. See mechsub.hpp for the full
// reconciliation note and the CLASSMAP correction. Each method cites its @ADDR.
//
// Float constants converted to decimal:
// _DAT_004ac140 = 0x3f800000 = 1.0f (status threshold)
// _DAT_004ac18c = 0x3f800000 = 1.0f (GetStatusFlags upper threshold)
// _DAT_004ac190 = 0.0f (GetStatusFlags lower threshold)
// _DAT_004ac860 = 0x3f800000 = 1.0f (armour-inversion numerator)
// _DAT_004ac864 = 0x38d1b717 ~= 1e-4f (armour-inversion epsilon)
// DAT_004e0f80 = identity ResourceID
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_0041c5c0 Subsystem ctor (name + classID overload)
// FUN_0041c52c Subsystem ctor (resource overload)
// FUN_0041c648 Subsystem dtor FUN_0041c684 Subsystem::CreateStreamed
// FUN_0041bbd8 AlarmIndicator::SetLevel(n)
// FUN_0041de1c DamageZone ctor (0x160) FUN_004023f4 RefCount ctor
// FUN_00402298 operator new FUN_004022d0 operator delete
// FUN_00402a98 TextString connection bind
// FUN_00408440 ResourceID copy
// FUN_0041c800 (assert / GenerateFault default)
// FUN_004dcd00 fabsf()
// FUN_004dbb24 DebugStream::operator<< FUN_004d9c38 endl
// FUN_00404088/0118/0190 NotationFile::Read(char**/float*/int*)
// FUN_004274f8 Get_Segment_Index FUN_004d4b58 Strcmp()
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECHSUB_HPP)
# include <mechsub.hpp>
#endif
//
// Tuning constants observed as read-only globals in the decomp.
//
static const Scalar StatusThreshold = 1.0f; // _DAT_004ac140 / _DAT_004ac18c
static const Scalar StatusFloor = 0.0f; // _DAT_004ac190
static const Scalar ArmourNumerator = 1.0f; // _DAT_004ac860
static const Scalar ArmourEpsilon = 1.0e-4f; // _DAT_004ac864
static const Scalar ResourceUnset = -1.0f; // 0xbf800000
//
// Status-state name table @0x50de74 walked by LookupStatusType (@0x4ac194).
//
struct StatusNameEntry { const char *name; int value; };
static const StatusNameEntry StatusNameTable[] =
{
{ "Destroyed", 0 },
{ "Damaged", 1 },
{ "CoolantLeaking", 2 },
{ "Overheating", 3 },
{ "AmmoBurning", 4 },
{ "Jammed", 5 },
{ "BadPower", 6 },
{ 0, 0 }
};
//
// Status-state name table @0x50de74 -> {char* name, int value} pairs, walked by
// LookupStatusType (@0x4ac194). Names @0x50df17.
//
// PTR_s_Destroyed_0050de74 = {
// "Destroyed",0, "Damaged",1, "CoolantLeaking",2, "Overheating",3,
// "AmmoBurning",4, "Jammed",5, "BadPower",6, 0
// };
//#############################################################################
// Shared Data Support
//
MechSubsystem::SharedData
MechSubsystem::DefaultData(
&MechSubsystem::ClassDerivations,
ReconMessageHandlers,
ReconAttributeIndex,
MechSubsystem::StateCount
);
Derivation
MechSubsystem::ClassDerivations(
Subsystem::GetClassDerivations(),
"MechSubsystem" // @0x50def5
);
//#############################################################################
// Construction / Destruction
//
//
// @0x4ac530 -- light constructor (name + classID). Chains the MUNGA Subsystem
// name/classID ctor, lays the vtable @0x50e210, allocates the status memory
// block and a DamageZone (0x160 bytes) named "None", and primes flags.
//
MechSubsystem::MechSubsystem(
Mech *owner,
int subsystem_ID,
const char *subsystem_name,
RegisteredClass::ClassID class_id,
SharedData &shared_data
):
Subsystem(owner, subsystem_ID, subsystem_name, class_id, shared_data) // FUN_0041c5c0
{
refCount = NewRefCount(0x10); // this[0x3d]
this->owner = owner; // this[0x34] -- the owning Mech (canonical)
hostEntity = owner; // this[0x3a] = param_7
subsystemId2 = subsystem_ID; // this[0x3b] = param_8
videoObjectFlag = 0; // this[0x39]
BindName(refCount, "None"); // FUN_00402a98(.,this+0x3d,"None") @0x50df7a
alarmModel = 0; // identity ResourceID // FUN_00408440(this+0x3e, identity)
criticalReference = 0; // this[0x42]
collisionCriticalHitWeight = 0; // this[0x43]
printSimulationState = 0; // this[0x41]
vitalSubsystemIndex = -1; // this[0x44]
damageZone = (ReconDamageZone *)new DamageZone(this, 0); // this[0x38] = FUN_0041de1c(new 0x160, this, 0)
}
//
// @0x4ac644 -- resource constructor. Chains the MUNGA Subsystem resource ctor,
// lays the same vtable @0x50e210, builds the DamageZone, and seeds it from the
// resource: structureReference -> DamageZone+0x140, armorByFacing[5] ->
// DamageZone+0x144..+0x154. Each armour scalar is then inverted to a
// per-facing absorption coefficient (1.0 / (armour * reference)) where the
// reciprocal exceeds the epsilon.
// (heat.cpp mislabels this ctor as "HeatableSubsystem"; the thermal fields it
// claims at this+0x114 are never written here -- they belong to HeatSink.)
//
MechSubsystem::MechSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data
):
Subsystem(owner, subsystem_ID, subsystem_resource, shared_data) // FUN_0041c52c
{
refCount = NewRefCount(0x10); // this[0x3d]
this->owner = owner; // this[0x34] -- the owning Mech. (The recon
// ctor only set hostEntity; subclasses read
// `owner` widely -> must be set or it is garbage.)
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0)
hostEntity = (Mech *)subsystem_resource->segmentIndex; // this[0x3a]=param_6
subsystemId2 = subsystem_ID; // this[0x3b]=param_7
resource = subsystem_resource; // this[0x3c]
vitalSubsystemIndex = -1; // this[0x44]
videoObjectFlag = (subsystem_resource->vitalSubsystemIndex == 1); // res+0x48==1
BindName(refCount, subsystem_resource->videoObjectName); // res+0x4c
alarmModel = subsystem_resource->alarmModel; // res+0xcc -> this[0x3e]
printSimulationState = subsystem_resource->printSimulationState; // res+0xd8
criticalReference = subsystem_resource->criticalReference; // res+0xe0
collisionCriticalHitWeight = subsystem_resource->collisionCriticalHitWeight; // res+0xdc
damageZone = (ReconDamageZone *)new DamageZone(this, 0); // this[0x38]
BindName((char *)damageZone + 0x15c, GetName()); // FUN_00402a98(., dz+0x15c, name)
// structure reference and per-facing armour into the DamageZone
damageZone->structureReference = subsystem_resource->structureReference; // dz+0x140 = res+0x44
for (int i = 0; i < 5; ++i)
{
damageZone->armour[i] = subsystem_resource->armorByFacing[i]; // dz+0x144+i = res+0x30+i
}
// invert each facing into an absorption coefficient
for (int i = 0; i < 5; ++i)
{
Scalar a = damageZone->armour[i] - ArmourNumerator / damageZone->structureReference;
if (ArmourEpsilon < fabsf(a)) // FUN_004dcd00
{
damageZone->armour[i] =
ArmourNumerator / (damageZone->armour[i] * damageZone->structureReference);
}
}
}
//
// @0x4ac868 -- releases the DamageZone (virtual slot0, arg 3), drops the status
// memory block, then chains the MUNGA Subsystem dtor.
//
MechSubsystem::~MechSubsystem()
{
// *this = &PTR_FUN_0050e210; damageZone->~DamageZone(); release(refCount);
// FUN_0041c648(this, 0) (Subsystem dtor)
}
Logical MechSubsystem::TestClass(Mech &) { return True; }
Logical MechSubsystem::TestInstance() const { return IsDerivedFrom(ClassDerivations); }
//
// gauge-complete wave: this subsystem's OWN structural damage level [0,1] (0=intact,
// 1=destroyed). The zone at this+0xE0 is an ENGINE DamageZone (new DamageZone(this,0)
// above); read/write its public NAMED damageLevel (DAMAGE.h) via the ENGINE type --
// NOT the ReconDamageZone proxy (whose offset-0 structureLevel aliases the vtable ptr
// as a float = garbage). The binary reads/writes *(this[0x38]+0x158) for exactly this
// (SensorSimulation @004b1c4c, UpdateCoolant @004adbf8, HeatSink::HandleMessage @004add6c).
//
Scalar MechSubsystem::GetSubsystemDamageLevel() const
{
return damageZone ? ((DamageZone *)damageZone)->damageLevel : 0.0f;
}
void MechSubsystem::SetSubsystemDamageLevel(Scalar level)
{
if (damageZone)
{
((DamageZone *)damageZone)->damageLevel = level;
}
}
//#############################################################################
// Subsystem virtual overrides
//
//
// @0x4ac144 -- damage tier from the DamageZone structure level (dz+0x158):
// >= 1.0 -> 1 (intact / nominal), > 0.0 -> 2 (damaged), else 0 (dead).
//
LWord
MechSubsystem::GetStatusFlags()
{
Scalar structure = damageZone->structureLevel; // *(this[0x38]+0x158)
if (StatusThreshold <= structure) return 1; // _DAT_004ac18c
if (StatusFloor < structure) return 2; // _DAT_004ac190
return 0;
}
//
// @0x4ac0bc -- per-frame structure watchdog. Forwards the message to the base
// (vtable+0x18), and when the DamageZone structure has recovered to >= 1.0,
// raises the status alarm to level 1, fires the alarm-changed callback, pins
// structure to exactly 1.0, and (if a model is attached) raises alarm 9 on the
// owner's status block.
//
Logical
MechSubsystem::HandleMessage(int message)
{
(*BaseSlot0x18())(this, message); // *(this[0x38].vtable+0x18)
if (StatusThreshold <= damageZone->structureLevel) // _DAT_004ac140
{
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
if (printSimulationState != 0) // this[0x41]
{
OnAlarmChanged(); // (*this.vtable+0x34)(this)
}
damageZone->structureLevel = 1.0f; // dz+0x158 = 1.0
if (videoObjectFlag != 0) // this[0x39]
{
((Mech *)owner)->RaiseStatusAlarm(9); // FUN_0041bbd8(owner+0x2c, 9)
}
}
return True;
}
//
// @0x4ac1d4 -- reset. On powered==0: restore structure to 1.0, set status
// alarm level 1 and fire the alarm-changed callback. Otherwise delegate to the
// fault/assert path (FUN_0041c800).
//
void
MechSubsystem::ResetToInitialState(Logical powered)
{
if (!powered)
{
if (damageZone != 0)
{
damageZone->structureLevel = 1.0f; // dz+0x158 = 1.0
}
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
if (printSimulationState != 0)
{
OnAlarmChanged(); // (*this.vtable+0x34)(this)
}
}
else
{
GenerateFaultDefault(); // FUN_0041c800
}
}
//
// @0x4ac22c -- ClearStatus: zero the structure and both alarms (the status
// alarm and the DamageZone's own alarm), then fire the alarm-changed callback.
//
void
MechSubsystem::ClearStatus()
{
damageZone->structureLevel = 0.0f; // dz+0x158 = 0
damageZone->alarm.SetLevel(0); // FUN_0041bbd8(dz+0x10, 0)
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0)
if (printSimulationState != 0)
{
OnAlarmChanged();
}
}
//
// @0x4ac8c0 -- prints "<name> = <SimulationState>". Status @this+0x40.
//
void
MechSubsystem::PrintState()
{
switch (simulationState) // *(this+0x40)
{
case DefaultState: DebugStream << GetName() << " = DefaultState" << endl; break; // 0050df99
case DestroyedState: DebugStream << GetName() << " = Destroyed" << endl; break; // 0050df7f
case ExplodingState: DebugStream << GetName() << " = Exploding" << endl; break; // 0050df8c
default: DebugStream << GetName() << " = UnknownState!!! : "
<< simulationState << endl; break; // 0050dfa9
}
}
//
// @0x4ac9c8 -- IsDamaged: true (dead) when the owning mech's electrical bus is
// down -- *(*(*(owner+0xd0)+0x190)+0x274) == 0.
//
Logical
MechSubsystem::IsDamaged()
{
return (*(int *)(*(int *)(*(int *)((char *)owner + 0xd0) + 0x190) + 0x274) == 0);
}
//#############################################################################
// Damage / critical-hit helpers
//
//
// @0x4ac07c -- run TakeDamage (virtual slot+0x24) and return the change in the
// DamageZone structure level it produced.
//
Scalar
MechSubsystem::ApplyDamageAndMeasure(Damage &damage)
{
Scalar before = damageZone->structureLevel; // dz+0x158
TakeDamage(damage); // (*this.vtable+0x24)(this, &damage)
return damageZone->structureLevel - before;
}
//
// @0x4ac274 -- distribute a critical hit across the contained critical
// subsystems. Drives the status alarm through level 2 then 1, pins structure
// to 1.0, then iterates the critical-subsystem chain (ClassID 0x4E entries),
// divides the incoming damage amount by the critical count, and re-applies the
// scaled damage to each, logging "ammo explosion damaging " per hit.
//
void
MechSubsystem::DistributeCriticalHit(Damage &damage)
{
statusAlarm.SetLevel(2); if (printSimulationState) OnAlarmChanged();
statusAlarm.SetLevel(1); if (printSimulationState) OnAlarmChanged();
damageZone->structureLevel = 1.0f;
CriticalChain criticals(this); // FUN_004acfa9
int count = criticals.CountOfType(0x4E); // entries whose +4 == 0x4E
damage.damageAmount /= (Scalar)count; // per-critical share
for (CriticalEntry *c = criticals.First(); c != 0; c = criticals.Next())
{
DebugStream << "ammo explosion damaging " << c->Subsystem()->GetName(); // 0050df61
c->Subsystem()->TakeDamage(damage); // (*owner.vtable+0xc)(...)
}
}
//
// @0x4ac194 -- map a status-state name to its TechStatusType value via the
// table @0x50de74. Returns False if not found.
//
Logical
MechSubsystem::LookupStatusType(const char *name, int *out_value)
{
for (const StatusNameEntry *e = StatusNameTable; e->name != 0; ++e) // PTR @0x50de74
{
if (Strcmp(name, e->name) == 0) // FUN_004d4b58
{
*out_value = e->value;
return True;
}
}
return False;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CreateStreamedSubsystem -- MechSubsystem (@0x4ac9ec)
//
// Stamps classID 0xBBB / model size 0xE4 and parses the damage/critical fields
// on top of the base Subsystem record.
//
int
MechSubsystem::CreateStreamedSubsystem(
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes
)
{
if (!Subsystem::CreateStreamedSubsystem( // FUN_0041c684
model_file, model_name, subsystem_name,
subsystem_resource, subsystem_file, directories))
{
return False;
}
subsystem_resource->subsystemModelSize = 0xE4; // res+0x24
subsystem_resource->classID = (RegisteredClass::ClassID)0x0BBB; // MechSubsystemClassID, res+0x20
if (passes == 1)
{
// first pass: prime to "unset"
subsystem_resource->criticalReference = ResourceUnset; // res+0xe0 = -1.0f
for (int i = 0; i < 5; ++i)
subsystem_resource->armorByFacing[i] = ResourceUnset; // res+0x30.. = -1.0f
subsystem_resource->structureReference = ResourceUnset; // res+0x44
subsystem_resource->vitalSubsystemIndex = -1; // res+0x48
memset(subsystem_resource->videoObjectName, 0, 128); // res+0x4c
strcpy(subsystem_resource->videoObjectName, "None"); // DAT_0050dfc0
subsystem_resource->alarmModel = 0; // identity ResourceID // res+0xcc
subsystem_resource->printSimulationState = 0; // res+0xd8
subsystem_resource->collisionCriticalHitWeight = 0; // res+0xdc
}
model_file->GetEntry(subsystem_name, "PrintSimulationState",
&subsystem_resource->printSimulationState); // 0050dfc5
model_file->GetEntry(subsystem_name, "CollisionCriticalHitWeight",
&subsystem_resource->collisionCriticalHitWeight); // 0050dfda
const char *videoName = "Unspecified"; // 0050dff5
model_file->GetEntry(subsystem_name, "VideoObjectName", &videoName); // 0050e001
if (strcmp(videoName, "Unspecified") != 0) // 0050e011
{
strcpy(subsystem_resource->videoObjectName, videoName); // res+0x4c
}
const char *vitalName = "Unspecified"; // 0050e01d
if (!model_file->GetEntry(subsystem_name, "VitalSubsystem", &vitalName) // 0050e029
&& subsystem_resource->vitalSubsystemIndex == -1)
{
// VitalSubsystem optional; resolved to a segment index when present
}
if (strcmp(vitalName, "Unspecified") != 0)
{
subsystem_resource->vitalSubsystemIndex =
Get_Segment_Index(model_file, model_name, directories, vitalName); // FUN_004274f8
}
Check_Fpu();
return True;
}