Files
BT411/game/reconstructed/mechsub.cpp
T
Joe DiPrimaandClaude Fable 5 819772f974 the rest of the Myomers wrapper: two live outputs recovered, and one branch that never fires
Follow-on to b70654d, which chained the base sim and stopped there. Decoding
the remaining 0x17c bytes of @004b8b9c turned up four more blocks the port had
dropped along with it. In binary order the registered Performance is:

  @004b8bab  chain PoweredSubsystemSimulation                  (fixed in b70654d)
  @004b8bb9  un-powered self-repair of this myomer's own zone   (dead -- see below)
  @004b8c1e  republish outputVoltage@0x344 from the source
  @004b8c5a  republish speedEffect@0x31C, the drive fed to the mover
  @004b8ceb  run the inner integrator, ONLY when outputVoltage > 0

speedEffect is the interesting one. AvailableOutput scales by the Mech base
speed and the wrapper divides by it again, so the two cancel and what lands in
+0x31C is a 0..1 FRACTION of full drive carrying the gear ratio, the thermal
degradation curve and the accumulated zone damage. Neither it nor outputVoltage
was ever written before: outputVoltage sat wherever the ctor left it and
speedEffect stayed pinned at its ctor 1.0f.

Un-stubbed DamageStructureLevel() while in here. It was `return 0.0f`, which
held AvailableOutput's (1 - damage) factor at 1, so a shot-up myomer drove
exactly as well as a fresh one. Routed to the base's GetSubsystemDamageLevel()
bridge, the same cell sensor.cpp:312 already reads. Measured dmg=0.6 -> speed=0.4.

@004b8bb9 does not work, and did not work in 1995 either. It builds a Damage
(Explosive, amount 0xbc343958 ~= -0.011f, impactPoint from owner+0x100, burst 1)
and calls the ZONE's TakeDamage -- vtable +0x18, not the subsystem's +0x24 -- so
the plain `damageLevel += amount * damageScale[type]`. Read on its own that is
"a myomer you power down slowly heals". But a subsystem's private zone is built
by the 2-arg `new DamageZone(this, 0)`, and DAMAGE.cpp:187-190 zeroes all five
damageScale entries; Reset never touches them and the only other writer is
Mech__DamageZone, the mech's streamed zones. The sum is always `+= amount * 0`.
Seeded a zone to 0.6, held it at NoVoltage for ~1500 ticks: damageLevel never
moved. Reconstructed and deliberately not "fixed" -- a working repair here would
be behavior we invented. The crit path reaches subsystem damage by writing
damageLevel directly, which is why subsystems still die.

That generalises, so it is written up in context/combat-damage.md on its own:
you cannot damage a subsystem's own zone through DamageZone::TakeDamage at all.
Anything reconstructed later that means to hurt a subsystem has to go the way
the crit path goes, or it will silently do nothing.

Verified, self-damage runs across two death/respawn cycles:
  torso    96/96 samples elec=4, zero dips
  myomers  96/97 elec=4 (the odd one is the first tick, before the machine runs)
  healthy  outV=10000 speed=1     un-powered  outV=0 speed=0

BT_MYOMERS_LOG probes the four outputs; BT_MYOMERS_REPAIR_TEST=<level> seeds the
zone and drops the subsystem into Manual so the repair branch can be watched
without the AutoConnect hunt restoring power a frame later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:06:56 -05:00

615 lines
24 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
//
// ConfigureActivePress -> the +0x110 session flag (AUDIO_FIDELITY.md F15).
const MechSubsystem::IndexEntry
MechSubsystem::AttributePointers[]=
{
ATTRIBUTE_ENTRY(MechSubsystem, ConfigureActivePress, configureActivePress) // id 2 @0x110
};
MechSubsystem::AttributeIndexSet&
MechSubsystem::GetAttributeIndex()
{
static MechSubsystem::AttributeIndexSet attributeIndex(
ELEMENTS(MechSubsystem::AttributePointers),
MechSubsystem::AttributePointers,
Simulation::GetAttributeIndex()
);
return attributeIndex;
}
MechSubsystem::SharedData
MechSubsystem::DefaultData(
&MechSubsystem::ClassDerivations,
ReconMessageHandlers,
MechSubsystem::GetAttributeIndex(),
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)
controlDestination = 0; // this[0x3a] = param_7 (ctor pass-through;
controlMessageID = 0; // this[0x3b] = param_8 weapons override -- task #6)
vitalSubsystem = 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]
configureActivePress = -1; // this[0x44] configure-session idle
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)
controlDestination = 0; // this[0x3a]=param_6 (ctor pass-through;
controlMessageID = 0; // this[0x3b]=param_7 weapons override -- task #6)
resource = subsystem_resource; // this[0x3c]
configureActivePress = -1; // this[0x44] configure-session idle
vitalSubsystem = (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;
}
//
// @0049c9a8 (zone-side inline) -- the destroyed-side effects applied when a
// crit drives this subsystem's own zone to 1.0: status Destroyed, the gated
// debug print, and the zone's state valve. (task #2)
//
void MechSubsystem::ForceCriticalFailure()
{
statusAlarm.SetLevel(1); // +0x2C: 1 = Destroyed
if (printSimulationState) // +0x104 gate
{
PrintState(); // vtable slot 0x34 @4ac8c0
}
if (damageZone != 0)
{
((DamageZone *)damageZone)->SetDamageZoneState(1); // zone+0x10
}
}
// issue #22 (PORT): the fresh-mech respawn repair -- the exact inverse of the
// crit-failure path (statusAlarm 1 + zone state 1 + damageLevel), restoring
// the private zone to its ctor state. See mech4.cpp Mech::Reset.
void MechSubsystem::RespawnRepair()
{
statusAlarm.SetLevel(0); // operational
if (damageZone != 0)
{
((DamageZone *)damageZone)->damageLevel = 0.0f;
((DamageZone *)damageZone)->SetDamageZoneState(0);
((DamageZone *)damageZone)->SetGraphicState(0); // ExistsGraphicState
}
}
void MechSubsystem::SetSubsystemDamageLevel(Scalar level)
{
if (damageZone)
{
((DamageZone *)damageZone)->damageLevel = level;
}
}
//#############################################################################
// Subsystem virtual overrides
//
//
// @0x4ac144 -- the DAMAGE TIER, read from the zone's damageLevel (dz+0x158):
// >= 1.0 -> bit 0 (DESTROYED), > 0.0 -> bit 1 (DAMAGED), else 0 (pristine).
// These are TechStatusType bit indices 0/1 -- the low two conditions of the
// MechTech annunciator scan (#47). The old comment read the field as a
// STRUCTURE level ("1.0 = intact / 0 = dead") -- backwards: the engine member
// is damageLevel, which ACCUMULATES to 1.0 = destroyed (mechdmg.cpp's crit
// cascade tests `level >= StructureMax` for exactly that). Behaviour was
// always right; the comment was a leftover of the same structureLevel
// misreading that #46 retired.
//
LWord
MechSubsystem::GetStatusFlags()
{
Scalar structure = ((DamageZone *)damageZone)->damageLevel; // *(this[0x38]+0x158) (engine view; the recon proxy's structureLevel@0 ALIASES THE VPTR)
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 *)damageZone)->damageLevel) // _DAT_004ac140 (engine view)
{
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
if (printSimulationState != 0) // this[0x41]
{
OnAlarmChanged(); // (*this.vtable+0x34)(this)
}
((DamageZone *)damageZone)->damageLevel = 1.0f; // dz+0x158 = 1.0 (engine view)
if (vitalSubsystem != 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 *)damageZone)->damageLevel = 1.0f; // dz+0x158 = 1.0 (engine view)
}
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 *)damageZone)->damageLevel = 0.0f; // dz+0x158 = 0 (engine view)
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 -- the NOVICE-experience lockout predicate (issue #2 [T1]):
// *(*(*(sub+0xd0)+0x190)+0x274) == 0, i.e. owner mech -> playerLink ->
// BTPlayer experienceLevel == 0. NOT a damage/bus query (the old label), and
// the old body raw-read the binary offset chain off `owner` (already the
// Mech, so the extra +0xd0 hop was doubly wrong) -- the databinding trap.
// Routed through the complete-type bridge in btplayer.cpp. NOTE:
// HeatSink::IsDamaged (heat.hpp) deliberately shadows this with a
// simulationState check for the gauge tint (btl4gaug.cpp); the FUN_004ac9c8
// call sites in powersub.cpp call the bridge directly.
//
Logical
MechSubsystem::IsDamaged()
{
extern int BTPlayerRoleLocksAdvanced(void *owner_mech); // btplayer.cpp (FUN_004ac9c8)
return BTPlayerRoleLocksAdvanced(owner) ? True : False;
}
//#############################################################################
// 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 *)damageZone)->damageLevel; // dz+0x158 (engine view)
TakeDamage(damage); // (*this.vtable+0x24)(this, &damage)
return ((DamageZone *)damageZone)->damageLevel - before;
}
//
// Straight to the zone's own virtual -- see the header note. Myomers'
// Performance (@004b8c0f-0x4b8c1b) does exactly:
// eax = this[0xE0]; (*(*eax + 0x18))(eax, &damage)
// i.e. DamageZone::TakeDamage, whose T0 body (engine/MUNGA/DAMAGE.cpp:374) is
// damageLevel += damageAmount * damageScale[damageType];
// Clamp(damageLevel, 0.0f, 1.0f); SetDamageLevelChangedFlag();
// so a NEGATIVE damageAmount walks the zone back toward intact, and the clamp
// is what stops it below 0.
//
void MechSubsystem::ApplyZoneDamage(Damage &damage)
{
if (damageZone != 0)
((DamageZone *)damageZone)->TakeDamage(damage); // zone vtable +0x18
}
//
// @0x4ac274 -- the AMMO-EXPLOSION fan-out (Gitea #46, re-read from the raw
// decomp). The old reconstruction here was wrong in kind: it "re-applied the
// scaled damage to critical subsystems" through the CriticalChain STAND-IN in
// mechrecon.hpp, whose CountOfType/First/Next were stubs -- it iterated
// nothing, divided by a fabricated count, and had no caller anyway (AmmoBin's
// CookOff went to a local no-op). The real body [T1, part_012.c @004ac274]:
//
// 1. statusAlarm pulse 2 (Exploding) -> 1 (Destroyed), each followed by the
// slot-13 state PRINT when printSimulationState is set (this+0x104 gates
// `(*vtbl+0x34)(this)` = @004ac8c0, the "<name> = __Exploding/__Destroyed"
// debug print -- NOT an "explosion notify").
// 2. *(this[0x38] + 0x158) = 1.0f -- the subsystem's OWN DamageZone structure
// pinned to destroyed.
// 3. Iterate the plugs connected to THIS SUBSYSTEM (FUN_004acfa9 walks the
// Node link chain at this+8), collecting every object whose classID@+4 ==
// 0x4E = **DamageZoneClassID** (VDATA.h enum -- index 78; cross-checked:
// index 28 = AudioStateTrigger matches the audio-trace finding) -- i.e.
// the MECH damage zones that list this subsystem as a critical subsystem.
// 4. damage.damageAmount /= zoneCount (evenly split; the binary divides
// UNGUARDED -- every authored bin is in at least one zone).
// 5. Per zone: print "ammo explosion damaging <zoneName>" (@0050df61, name
// from zone+0x15C = damageZoneName) and send the OWNER a full 100-byte
// Entity::TakeDamageMessage (id 0x12) with
// inflictingEntity = the owner mech itself (owner+0x184)
// damageZone = the zone's index (zone+0x13C)
// damageData = the split Damage record
// inflictingSubsystemID = this subsystem's ID (this+0xD8)
// -- the message-manager uses inflictingSubsystemID to bundle the
// explosion resource for the view (ENTITY3.h's own comment).
//
// Steps 3-5 need the Mech/zone complete types, so they live in
// Mech::AmmoExplosionFanOut (mechdmg.cpp) behind the BTAmmoExplosionFanOut
// bridge (databinding rule).
//
void
MechSubsystem::DistributeCriticalHit(Damage &damage)
{
statusAlarm.SetLevel(2); if (printSimulationState) OnAlarmChanged(); // Exploding
statusAlarm.SetLevel(1); if (printSimulationState) OnAlarmChanged(); // Destroyed
SetSubsystemDamageLevel(1.0f); // own private zone gutted (dz+0x158, ENGINE view)
extern void BTAmmoExplosionFanOut( // mechdmg.cpp (Mech-complete TU)
void *owner_mech, void *inflicting_subsystem,
int inflicting_subsystem_id, Damage &total_damage);
BTAmmoExplosionFanOut(owner, this, subsystemID, damage);
}
//
// @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;
}
//#############################################################################
// BTSubsystemControlFeed -- complete-type bridge (task #6)
//
// ConfigMapGauge::Execute (@004c6f1c) reads the subsystem's controls feed
// (*(subsys+0xE8) destination, *(subsys+0xEC) message id) for GetMapState.
// The gauge TU has no complete MechSubsystem (databinding-trap rules: never
// raw-read a compiled object's offsets) -- this bridge reads the NAMED members
// and hands back the Receiver upcast alongside.
//
void
BTSubsystemControlFeed(
void *subsystem_in,
void **destination_out,
int *message_id_out,
void **receiver_out
)
{
MechSubsystem *sub = (MechSubsystem *)subsystem_in;
*destination_out = sub->controlDestination; // @0xE8
*message_id_out = sub->controlMessageID; // @0xEC
*receiver_out = (Receiver *)sub;
}
//
// BTSubsystemDamageLevelOf -- complete-type bridge (PPC-dial fix, 2026-07-12):
// feeds the gauge cluster's OPERATING flag (the "destroyed X" lamps). The
// observable truth: a subsystem is out of action when its own damage level
// saturates (DistributeCriticalHit destroys at >= 1.0, mechdmg.cpp) -- read
// that instead of the unidentified binary +0x40 field the old raw read
// guessed at (layout noise -> X's over healthy panels).
//
Scalar
BTSubsystemDamageLevelOf(void *subsystem_in)
{
return (subsystem_in != 0)
? ((MechSubsystem *)subsystem_in)->GetSubsystemDamageLevel() : 0.0f;
}