Found while auditing today's vtable change before release. The binary's
HeatSink::GetStatusFlags @004add30 OPENS with `call 0x4ac144` --
MechSubsystem::GetStatusFlags, the damage tier (damageLevel >= 1.0 -> bit 0
Destroyed; > 0 -> bit 1 Damaged). There is no zero-returning
HeatableSubsystem override anywhere in the image.
Our port routed that call through `HeatableSubsystem::GetStatusFlags()
{ return 0; }` -- a "neutral default" that silently dropped bits 0 and 1 for
the ENTIRE heat branch (every weapon, sensor, emitter, PPC, generator,
powered subsystem). Two live consequences:
* Destroyed / Damaged could never reach MechTech's annunciator scan --
gutting half the shipped mechalrm table (Destroyed -> gotoEngineering +
engCooling + engBusMode) hours after #47 shipped the flash;
* PoweredSubsystem's AutoConnect gate (`GetStatusFlags() == 0`,
powersub.cpp:358) read a DAMAGED subsystem as pristine.
One-line fix: chain the real tier, reproducing @004add30 exactly. The call
site's own comment already said `// FUN_004ac144` -- the implementation had
simply never matched it.
Also corrected a stale comment on MechSubsystem::GetStatusFlags that read the
field as a STRUCTURE level ("1.0 = intact, 0 = dead") -- backwards. The
engine member is damageLevel and ACCUMULATES to 1.0 = destroyed (mechdmg's
crit cascade tests `level >= StructureMax` for exactly that), which is what
makes bits 0/1 line up with TechStatusType Destroyed/Damaged. Behaviour was
always right; the comment was a leftover of the same structureLevel misreading
#46 retired.
VERIFIED (3-pod combat sim, BT_LAMP_LOG): the annunciator is genuinely live --
Jammed x7, AmmoBurning x5, BadPower x20, Overheating x207 (Overheating
correctly maps to NO lamp: the shipped table has no entry for it). The lamp
resolution is per-subsystem as designed -- different weapons flash their own
panel buttons (lamps 0xf/0x5/0xb/0x3 under distinct mode masks), 32 flash
writes across a whole battle, no thrash. Zero crashes, zero plane/OOB
tripwire hits across all three pods.
FILED, deliberately NOT fixed tonight (open-questions.md): vtable slot +0x40
is misattributed. The binary calls it `(this, 0)` in AutoConnect's outer gate
and `(this, candidateGenerator)` in the roster walk -- a one-arg voltage query
(HasVoltage(source)), not GetStatusFlags(). Modelling both as the no-arg
GetStatusFlags makes the outer gate demand "no flags" and the inner "some
flag" with nothing between them, so AutoConnect's loop body can never execute.
Pre-existing before and after this change; it is a live power-bus behaviour
change and deserves its own playtest cycle, not a release-eve patch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
599 lines
23 KiB
C++
599 lines
23 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;
|
|
}
|
|
|
|
//
|
|
// @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;
|
|
}
|