Follow-on tob70654d, 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 inb70654d) @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>
615 lines
24 KiB
C++
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;
|
|
}
|