Files
BT411/game/reconstructed/mechdmg.cpp
T
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

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

820 lines
32 KiB
C++

//==========================================================================//
// File: mechdmg.cpp //
// Project: BattleTech //
// Contents: Mech::DamageZone -- maps incoming damage to mech locations / //
// subsystems (the dz_* zones streamed from the .BGF SV_SPECIAL //
// tokens), critical-hit selection, LOD redirection, and the //
// descend-on-destruction segment walk. //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// 06/05/95 JM Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. //
// All Rights reserved worldwide //
// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary (Ghidra pseudo-C, recovered/all/
// part_012.c). Class shape is the SURVIVING MECHDMG.HPP; bodies follow the
// decompiled object at the cited @ADDRs. The companion Mech-side loop that
// streams every zone, Mech::CreateDamageZoneStream @004a474c, lives in
// mech.cpp; this file is the per-zone class.
//
// Float constants recovered from the code section (section_dump.txt):
// _DAT_0049c99c = 1.0f _DAT_0049c9a0 = 0.5f _DAT_0049c9a4 = 0.0f
// _DAT_0049cacc = 0.0f _DAT_0049cad0 = 1.0f
// _DAT_0049ce48 = 0.5f _DAT_0049ce4c = 1.0f
// _DAT_0049c50c = 0.25f _DAT_0049c510 = 10.0f (LOD reuse-window seconds)
// _DAT_0049c5f8 = 0.0f _DAT_0049c5fc = 1.0f (structure clamp)
// _DAT_0049d1c4 = 1.0f _DAT_0049d1c8 = 1.0e-4f _DAT_0049d1cc = 0.5f (leg halving)
//
// Helper-function name mapping (engine internals referenced by the decomp):
// FUN_0041df5c DamageZone::DamageZone (base ctor, vtable @004e3cf0)
// FUN_0041e394 DamageZone::~DamageZone (base dtor)
// FUN_0041e4e0 DamageZone::TakeDamage (base armor/structure model)
// FUN_0041e590 DamageZone::CreateStreamedDamageZone (base streamer)
// FUN_00424767 DamageZoneIndexTable::TableOf(0,1)
// FUN_0042478a DamageZoneIndexTable::~TableOf
// FUN_00424830 DamageZoneIndexTableIterator::TableIteratorOf(&table)
// FUN_0042484f DamageZoneIndexTableIterator::~TableIteratorOf
// FUN_00424716 IntegerPlug::PlugOf(&int)
// FUN_00424811 ...IteratorOf(&list) (segment child iterators)
// FUN_00417ab4 SlotOf<>::Resolve() / SharedData::Resolve()
// FUN_00417858 SlotOf<>::Release
// FUN_0041a1a4 IsDerivedFrom(classDerivations)
// FUN_0041b9ec AlarmIndicator(levels) FUN_0041bbd8 AlarmIndicator::SetLevel(n)
// FUN_00420ea4 EntityID::operator=(copy) FUN_00420ef4 EntityID::operator=
// FUN_0042104c EntityID::operator==
// FUN_00414b60 TheTime.CurrentTick() (/ DAT_0052140c == TicksPerSecond)
// FUN_00408050 (Scalar)Random -> [0,1) FUN_0040807c(rng,n) Random(n)
// FUN_004ac07c Subsystem::ApplyDamage(damage) -> Scalar
// FUN_004b11bc Generator::ForceShortRecovery (class derivations @0050f4bc)
// FUN_0049fb54 Mech::<query torso/leg destroyed state>
// FUN_0040485c/00404720/00404088/00404190/004040d8 NotationFile readers
// FUN_00402298/004022b0/004022d0 operator new / array new / delete
// FUN_004dbb24/004db92c/004d9c38/004dbd4c DebugStream << / endl
//
#include <bt.hpp>
#pragma hdrstop
#if !defined(MECHDMG_HPP)
# include <mechdmg.hpp>
#endif
#if !defined(MECH_HPP)
# include <mech.hpp>
#endif
#if !defined(POWERSUB_HPP)
# include <powersub.hpp>
#endif
#include <stdio.h> // sscanf (offline streamer)
#if !defined(RANDOM_HPP)
# include <random.hpp> // the engine's global RandomGenerator Random
#endif
//===========================================================================//
// Reconstruction stand-ins LOCAL to this translation unit.
// The per-zone damage code reaches engine clock / RNG / segment-tree / NULL
// subsystem facets through an invented name set; these behaviour-neutral
// stand-ins (and proxy casts onto the real Subsystem* / NotationFile* /
// ResourceFile*) carry that surface so the recovered logic compiles.
//===========================================================================//
static struct ReconClock { Scalar CurrentTick() { return 0; } } TheTime; // FUN_00414b60
static const Scalar TicksPerSecond = 1.0f; // DAT_0052140c
struct DZRef { int index; };
struct SegmentRecord { int siblings; int children; }; // mech segment-tree node
struct SegmentIterator { template<class A> SegmentIterator(const A &) {} DZRef *Next() { return 0; } };
struct SegTableX { SegmentRecord *operator[](int) { return 0; } }; // cast of EntitySegment::SegmentTable
// Subsystem facets the recovered code reaches that the engine Subsystem does
// not expose under these names (cast of Subsystem*).
struct SubProxy2
{
Scalar ApplyDamage(Damage &) { return 0; } // FUN_004ac07c
void OnDestroyed() {} // vtable slot 0x34
int isVital; // +0x39
AlarmIndicator failureAlarm; // +0x2c
int hasDestructor; // +0x41
void *damageZone; // +0xE0
};
// Offline .dmg/.skl notation readers (cast of NotationFile* / ResourceFile*).
struct NotationEntry { const char *name; const char *text; NotationEntry *Next() { return 0; } };
struct NotationList { int Count() { return 0; } NotationEntry *First() { return 0; } };
struct NoteX2 // cast of NotationFile*
{
template<class...A> int ReadLogical(A&&...) { return 0; }
template<class...A> int ReadString(A&&...) { return 0; }
template<class...A> NotationList FindList(A&&...) { return NotationList(); }
void Close() {}
};
struct RFileX2 { template<class A> NotationFile *Open(A) { return 0; } }; // cast of ResourceFile*
inline int Str_Equal(const char *a, const char *b) { return a && b && strcmp(a, b) == 0; }
template<class...A> inline int FindSegment(A&&...) { return -1; } // FUN_004274f8
//
// Sentinel for "no zone". Returned by GetSegmentIndex/RandomRedirect on a miss.
//
const int Mech__DamageZone::NullDamageZone = -1;
// LOD reuse window: within [0.25s, 10.0s] of the previous hit, the artifact
// zone tends to keep routing to the same child (damage clustering).
static const Scalar LODReuseWindowMin = 0.25f; // _DAT_0049c50c
static const Scalar LODReuseWindowMax = 10.0f; // _DAT_0049c510
// Structure level is always clamped to [0,1].
static const Scalar StructureMin = 0.0f; // _DAT_0049c5f8 / _DAT_0049c9a4 / _DAT_0049cacc
static const Scalar StructureMax = 1.0f; // _DAT_0049c5fc / _DAT_0049c99c / _DAT_0049cad0
// A leg whose zone is half-gone already trips the partial leg-failure graphic
// when the mech is not in a normal standing/walking stance (the oracle reads
// _DAT_0049c9a0 == 0.5 in that branch, vs StructureMax (1.0) for the full
// "leg destroyed -> fall" case).
static const Scalar LegHalfStructure = 0.5f; // _DAT_0049c9a0
// CriticalHit passes only half of the inflicted damage on to the armour model.
static const Scalar CriticalDamageFraction = 0.5f; // _DAT_0049ce48
static const Scalar CriticalDamageMax = 1.0f; // _DAT_0049ce4c
// Probability of re-using the previous child zone inside the reuse window.
// (Ghidra read the operand at @0049c514; exact value not cleanly recoverable
// from the pseudo-C -- it behaves as a high "stay on the same spot" bias.)
static const Scalar LODReuseHysteresis = 0.82f; // _DAT_0049c514 (TODO: verify)
//###########################################################################
//###########################################################################
// MechCriticalSubsystem
//###########################################################################
//###########################################################################
//
// @0049dd18 -- owner is the DamageZone that will destroy this subsystem when
// the zone's structure runs out. damagePercentage / damagePercentageUsed and
// criticalWeight are filled in afterwards by the streaming ctor of the zone.
//
MechCriticalSubsystem::MechCriticalSubsystem(DamageZone *owner)
: subsystemPlug(owner) // FUN_0049dd7e (SlotOf<Subsystem*>)
{
criticalWeight = 0; // Wword(6) @0x18
damagePercentageUsed = 0; // Wword(5) @0x14
}
//
// @0049dd44 -- releases the subsystem slot (FUN_0049dd9d).
//
MechCriticalSubsystem::~MechCriticalSubsystem()
{
}
//
// @0049dd74
//
Logical
MechCriticalSubsystem::TestInstance() const
{
return True;
}
//###########################################################################
//###########################################################################
// Mech::DamageZone
//###########################################################################
//###########################################################################
//#############################################################################
// Construction -- @0049ce50
//
// Chains to the base DamageZone (which parses armour/structure from the .dmg
// record and allocates the redirect table), then reads this class's own
// stream fields, normalises the per-facing armour scalars, builds the
// critical-subsystem array and finally the LOD redirect table.
//
Mech__DamageZone::Mech__DamageZone(
Mech *mech,
int damage_zone_index,
MemoryStream *stream
)
: DamageZone(mech, damage_zone_index, stream) // FUN_0041df5c
{
// vtable @0050bb90
redirectTable.Construct(0, 1); // Wword(0x58) = FUN_00424767(,0,1)
// last-redirect bookkeeping
lastDamageTime = (Scalar)TheTime.CurrentTick() / TicksPerSecond; // Wword(0x5f)
lastInflicting = mech->entityID; // Wword(0x60) = FUN_00420ea4(,mech+0x184)
parentArtifactZone.Construct(0); // Wword(0x62) = FUN_0049ddc9(,0)
criticalWeightSum = 0; // Wword(0x6c)
//
// Read the streamed scalar/flag block. (Stream order set by
// CreateStreamedDamageZone below.)
//
stream->ReadBytes(&descendOnDestruction, 4); // Wword(0x67)
stream->ReadBytes(&destroySiblingsOnDestruction, 4); // Wword(0x68)
stream->ReadBytes(&segmentIndex, 4); // Wword(0x65)
stream->ReadBytes(&leftLeg, 4); // Wword(0x69)
stream->ReadBytes(&rightLeg, 4); // Wword(0x6a)
stream->ReadBytes(&vitalDamageZone, 4); // Wword(0x66)
stream->ReadBytes(&criticalSubsystemCount, 4); // Wword(0x6b)
//
// Normalise the damage-scale vector damageScale[Damage::DamageTypeCount]
// (this[0x51..0x55], == base DamageZone::damageScale[5]). These are NOT
// per-facing: the base TakeDamage indexes them by Damage::damageType
// (it reads this[0x51 + damage.damageType]; see DamageZone::TakeDamage
// @0041e4e0), so the 5 cells are the {Collision,Ballistic,Explosive,Laser,
// Energy} per-type armour scale for this zone. Each cell is rewritten so a
// stored "armour points" figure becomes a damage-per-point fraction
// (1/(points*count)); a leg zone then halves every type's scale.
//
// damageScale[5] and defaultArmorPoints are the INHERITED engine DamageZone base
// members (read raw from the stream by the base ctor, DAMAGE.cpp:301-307); we
// normalise them in place here -- not a raw 0x144/0x140 offset (which would be the
// binary layout, wrong when compiled against the engine base).
for (int type = 0; type < Damage::DamageTypeCount; ++type) // 5 damage types
{
Scalar even = 1.0f / defaultArmorPoints; // _DAT_0049d1c4 / count
if (fabsf(damageScale[type] - even) > 1.0e-4f) // _DAT_0049d1c8
damageScale[type] = 1.0f / (damageScale[type] * defaultArmorPoints);
if (rightLeg || leftLeg)
damageScale[type] *= 0.5f; // _DAT_0049d1cc
}
//
// Critical-subsystem table.
//
if (criticalSubsystemCount == 0)
{
criticalSubsystems = 0; // Wword(0x6d)
}
else
{
criticalSubsystems =
(MechCriticalSubsystem **)Memory::AllocateArray(criticalSubsystemCount * 4);
for (int i = 0; i < criticalSubsystemCount; ++i)
{
criticalSubsystems[i] =
new (Memory::Allocate(0x1c)) MechCriticalSubsystem(this); // FUN_0049dd18
}
int subsystemIndex = -1;
for (int i = 0; i < criticalSubsystemCount; ++i)
{
stream->ReadBytes(&criticalSubsystems[i]->criticalWeight, 4); // +0x18
stream->ReadBytes(&criticalSubsystems[i]->damagePercentage, 4); // +0x10
criticalWeightSum += criticalSubsystems[i]->criticalWeight;
stream->ReadBytes(&subsystemIndex, 4); // subsystem-table index
// Wire the subsystem plug to the live subsystem unless this is the
// pure-LOD / cosmetic build (mech flags 0x28 bits).
if ((mech->flags & 0xc) == 0 && (mech->flags & 0x100) != 0)
criticalSubsystems[i]->subsystemPlug.Resolve(); // FUN_00417ab4
}
}
//
// LOD redirect table: <count> followed by <count> child zone indices.
//
int redirectCount = 0;
stream->ReadBytes(&redirectCount, 4);
for (int i = 0; i < redirectCount; ++i)
{
int childZone = 0;
stream->ReadBytes(&childZone, 4);
redirectTable.Add(new (Memory::Allocate(0x10)) IntegerPlug(&childZone)); // FUN_00424716
}
lastHitZone = (redirectCount == 0) ? 0 : RandomRedirect(); // Wword(0x5e)
}
//#############################################################################
// SetLODParentPointers -- @0049d1d0
//
// Run after every zone of the mech exists. For each child index in this
// (artifact) zone's redirect table, point that real child zone's
// parentArtifactZone slot back at us.
//
void
Mech__DamageZone::SetLODParentPointers()
{
Mech *mech = (Mech *)GetOwningSimulation();
DamageZoneIndexTableIterator it(&redirectTable); // FUN_00424830
for (IntegerPlug *plug; (plug = it.Next()) != 0; ) // slot 0x28
{
Mech__DamageZone *child = mech->Zone(plug->value); // inherited Entity::damageZones[idx], typed
child->parentArtifactZone = this; // slot @0x188 +4
}
}
//#############################################################################
// Destruction -- @0049d23c (vtable slot 0)
//
Mech__DamageZone::~Mech__DamageZone()
{
*(void **)this = &vtable_0050bb90;
if (criticalSubsystems != 0)
{
for (int i = 0; i < criticalSubsystemCount; ++i)
delete criticalSubsystems[i];
Memory::Free(criticalSubsystems);
}
{
DamageZoneIndexTableIterator it(&redirectTable); // FUN_00424830
it.DeleteContents(); // FUN_00417858(,1)
}
// (binary tail: FUN_0049dde8 parentArtifactZone.~DZSlot, FUN_0042478a
// redirectTable.~DamageZoneIndexTable, then base ~DamageZone FUN_0041e394 --
// all COMPILER-EMITTED dtor epilogue glue; C++ re-emits member + base
// destruction implicitly at the closing brace, so writing them as source runs
// them twice. See the reconstruction rule in mech.cpp ~Mech.)
}
//#############################################################################
// TakeDamage -- @0049c690 (vtable slot 6)
//
// If this zone is an artifact (has a redirect table) the hit is handed to the
// LOD router so it lands on a real child zone. Otherwise the base DamageZone
// applies it to armour/structure, the parent artifact (if any) recomputes its
// displayed level, special damage may short an attached generator, and the
// zone updates its graphic / leg-failure state.
//
// The base armour model (DamageZone::TakeDamage @0041e4e0, == the WinTesla
// engine DAMAGE.cpp body we link against) is:
//
// damageLevel += damage.damageAmount * damageScale[damage.damageType];
// Clamp(damageLevel, 0.0f, 1.0f); // _DAT_0041e57c / _DAT_0041e580
// SetDamageLevelChangedFlag(); // flags |= 4
// if (damageLevel >= 1.0f) { // zone destroyed
// SetDamageZoneState(BurningState); // state indicator -> 1
// SetGraphicState(DestroyedGraphicState);// vtable slot 3 -> 1
// SetGraphicStateChangedFlag(); // flags |= 8
// }
//
// i.e. there is no separate "armour then structure" pool -- a single 0..1
// damageLevel accumulates, scaled per damage TYPE by damageScale[5] (the
// vector normalised in the ctor above); 1.0 == destroyed. (damageLevel in
// this reconstruction names that same base damageLevel cell @0x158.)
//
void
Mech__DamageZone::TakeDamage(Damage& damage)
{
Mech *mech = (Mech *)GetOwningSimulation();
DamageZoneIndexTableIterator it(&redirectTable); // FUN_00424830
if (it.Count() != 0) // slot 0x14 -> artifact zone
{
EntityID inflicting = mech->lastInflictingID; // mech[0x43c]
Damage local = damage; // copy 10 words
LODDamageRouter(inflicting, local); // FUN_0049c40c
return;
}
// ---- real zone: apply to armour/structure ----
DamageZone::TakeDamage(damage); // FUN_0041e4e0
Mech__DamageZone *parent = parentArtifactZone.Resolve(); // FUN_00417ab4(this+0x62)
if (parent != 0)
parent->UpdateLODDamageLevel(); // FUN_0049c51c
// ---- ShortAttachedGenerators (inlined): special damage type 4 ----
if (damage.damageType == 4 && criticalSubsystemCount > 0)
{
int n = Random(criticalSubsystemCount); // FUN_0040807c
Subsystem *s = criticalSubsystems[n]->subsystemPlug.Resolve();
if (s->IsDerivedFrom(*Generator::GetClassDerivations())) // FUN_0041a1a4(,0x50f4bc)
((Generator *)s)->ForceShortRecovery(); // FUN_004b11bc
}
// ---- graphic / failure state from current structure level ----
if (vitalDamageZone == 0 || damageLevel < StructureMax)
{
if (leftLeg == 0 && rightLeg == 0)
{
if (damageLevel >= StructureMax)
RecurseSegmentTable(mech); // FUN_0049cad4
}
else if ((unsigned)(mech->stance - 3) < 2) // standing/walking
{
if (rightLeg != 0 && damageLevel >= StructureMax)
mech->graphicAlarm.SetLevel(9); // fall / leg gone
else if (leftLeg != 0 && damageLevel >= StructureMax)
mech->graphicAlarm.SetLevel(9);
}
else if (mech->IsAirborne() == 0) // FUN_0049fb54
{
// Non-standing stance, still grounded: a half-destroyed leg zone
// trips the partial-failure graphic (right -> 4, left -> 3). The
// oracle compares against _DAT_0049c9a0 (0.5), NOT StructureMax.
if (rightLeg != 0 && damageLevel >= LegHalfStructure)
mech->graphicAlarm.SetLevel(4);
else if (leftLeg != 0 && damageLevel >= LegHalfStructure)
mech->graphicAlarm.SetLevel(3);
}
}
else
{
// vital zone destroyed -> mech death
SetDamageZoneState(1); // this+4
mech->graphicAlarm.SetLevel(9);
}
damageLevel = Clamp(damageLevel, StructureMin, StructureMax);
}
//#############################################################################
// SetGraphicState -- @0049c66c (vtable slot 3)
//
// Drives the secondary "graphic state" alarm (this+0x64) and flags the zone
// as needing a visual refresh (Wword(0xb8) |= 8).
//
// Mech__DamageZone::SetGraphicState removed -- byte-identical to the engine base
// DamageZone::SetGraphicState (damageZoneGraphicState.SetState(new_state) +
// SetGraphicStateChangedFlag(), the latter == the old `flags |= 8`). Calls below
// resolve to the inherited base override.
//#############################################################################
// LODDamageRouter -- @0049c40c
//
// An artifact zone decides which real child takes the hit. If the same
// attacker hit recently (within [0.25s, 10.0s]) and a random roll succeeds
// the previous child is reused; otherwise a fresh RandomRedirect() is chosen.
// A brand-new attacker always re-rolls.
//
void
Mech__DamageZone::LODDamageRouter(EntityID inflicting, Damage damage)
{
int zone;
if (inflicting != lastInflicting) // FUN_0042104c
{
zone = RandomRedirect(); // new attacker
}
else
{
Scalar dt = (Scalar)TheTime.CurrentTick() / TicksPerSecond - lastDamageTime;
if (dt < LODReuseWindowMin)
zone = lastHitZone; // too soon: same spot
else if (dt > LODReuseWindowMax)
zone = RandomRedirect(); // stale: re-roll
else if ((Scalar)Random <= LODReuseHysteresis) // _DAT_0049c514
zone = lastHitZone;
else
zone = RandomRedirect();
}
lastHitZone = zone; // Wword(0x5e)
lastDamageTime = (Scalar)TheTime.CurrentTick() / TicksPerSecond;
lastInflicting = inflicting; // FUN_00420ef4
Mech *mech = (Mech *)GetOwningSimulation();
mech->Zone(zone)->TakeDamage(damage); // slot 6 on the real child
UpdateLODDamageLevel(); // FUN_0049c51c
}
//#############################################################################
// RandomRedirect -- @0049c600
//
// Uniformly pick one of the redirect table's child zone indices.
//
int
Mech__DamageZone::RandomRedirect()
{
DamageZoneIndexTableIterator it(&redirectTable); // FUN_00424830
int n = Random(it.Count()); // FUN_0040807c(rng, slot 0x14)
IntegerPlug *plug = it[n]; // slot 0x34
return plug->value; // +0xc
}
//#############################################################################
// UpdateLODDamageLevel -- @0049c51c
//
// An artifact zone's displayed structure is the mean of its real children's
// structure levels, clamped to [0,1].
//
void
Mech__DamageZone::UpdateLODDamageLevel()
{
Mech *mech = (Mech *)GetOwningSimulation();
unsigned count = 0;
Scalar sum = 0.0f;
DamageZoneIndexTableIterator it(&redirectTable); // FUN_00424830
for (IntegerPlug *plug; (plug = it.Next()) != 0; ) // slot 0x28
{
sum += mech->Zone(plug->value)->damageLevel; // +0x158
++count;
}
damageLevel = (count != 0) ? sum / (Scalar)count : StructureMin;
damageLevel = Clamp(damageLevel, StructureMin, StructureMax);
}
//#############################################################################
// CriticalHit -- @0049ccc4
//
// Apply (half of) a critical hit. Half the damage amount flows through the
// normal armour model (TakeDamage); the remainder rolls one critical
// subsystem, weighted by criticalWeight, and damages it. Returns the chosen
// subsystem (or 0 if none was selected).
//
Subsystem*
Mech__DamageZone::CriticalHit(Damage &damage_data)
{
Scalar bite = damage_data.damageAmount * CriticalDamageFraction; // damage+4 * 0.5
if (bite > CriticalDamageMax)
bite = CriticalDamageMax;
damage_data.damageAmount -= bite;
TakeDamage(damage_data); // slot 6 (this+0x18)
Scalar roll = (Scalar)Random; // FUN_00408050
if (criticalSubsystems != 0)
{
for (int i = 0; i < criticalSubsystemCount; ++i)
{
Scalar cum = criticalSubsystems[i]->criticalWeight;
for (int j = 0; j < i; ++j)
cum += criticalSubsystems[j]->criticalWeight;
if (roll <= cum / criticalWeightSum)
{
Subsystem *s = criticalSubsystems[i]->subsystemPlug.Resolve();
MechCriticalSubsystem *cs = criticalSubsystems[i];
if (cs->damagePercentageUsed < cs->damagePercentage)
cs->damagePercentageUsed += ((SubProxy2 *)s)->ApplyDamage(damage_data); // FUN_004ac07c
return criticalSubsystems[i]->subsystemPlug.Resolve();
}
}
}
return 0;
}
//#############################################################################
// SendSubsystemDamage -- @0049c9a8
//
// When this zone is destroyed it forces full structure (1.0) on itself and
// pushes each critical subsystem's outstanding damage fraction onto the zone
// that physically carries that subsystem; any zone driven to destruction
// there raises its alarms and fails the subsystem.
//
void
Mech__DamageZone::SendSubsystemDamage()
{
SetDamageZoneState(1); // this+0x10
damageLevel = StructureMax; // this+0x158 = 1.0
for (int i = 0; i < criticalSubsystemCount; ++i)
{
Subsystem *s = criticalSubsystems[i]->subsystemPlug.Resolve(); // +4
MechCriticalSubsystem *cs = criticalSubsystems[i];
Mech__DamageZone *zone = (Mech__DamageZone *)((SubProxy2 *)s)->damageZone; // subsystem[0x38] @0xE0
zone->damageLevel += (cs->damagePercentage - cs->damagePercentageUsed);
zone->damageLevel = Clamp(zone->damageLevel, StructureMin, StructureMax);
if (zone->damageLevel >= StructureMax) // destroyed
{
if (((SubProxy2 *)s)->isVital) // subsystem[0x39]
((Mech *)GetOwningSimulation())->graphicAlarm.SetLevel(9);
((SubProxy2 *)s)->failureAlarm.SetLevel(1); // subsystem+0xb
if (((SubProxy2 *)s)->hasDestructor) // subsystem[0x41]
((SubProxy2 *)s)->OnDestroyed(); // slot 0x34
zone->SetDamageZoneState(1); // zone+0x10
}
}
}
//#############################################################################
// RecurseSegmentTable -- @0049cad4
//
// Walks the model's segment tree from this zone. First pushes this zone's
// subsystem damage out (SendSubsystemDamage), then -- per the two streamed
// flags -- destroys sibling zones (destroySiblingsOnDestruction) and/or
// descends into child segments (descendOnDestruction), recursing on each.
//
void
Mech__DamageZone::RecurseSegmentTable(Mech *my_mech)
{
SendSubsystemDamage(); // FUN_0049c9a8
SetGraphicState(1); // slot 0xc
SegmentRecord *seg = ((SegTableX &)my_mech->segmentTable)[segmentIndex]; // mech+0x300, Wword(0x65)
if (destroySiblingsOnDestruction) // Wword(0x68)
{
SegmentIterator sibs(&seg->siblings); // seg+0xd0
for (DZRef *r; (r = sibs.Next()) != 0; ) // slot 0x28
{
Mech__DamageZone *sib = my_mech->Zone(r->index); // inherited damageZones[..], typed
if (sib != this && sib->GetGraphicState() != 1)
{
sib->RecurseSegmentTable(my_mech);
sib->SetGraphicState(2); // slot 0xc
}
}
}
if (descendOnDestruction) // Wword(0x67)
{
SegmentIterator kids(&seg->children); // seg+0xe8
for (DZRef *r; (r = kids.Next()) != 0; )
{
SegmentRecord *child = ((SegTableX &)my_mech->segmentTable)[r->index];
SegmentIterator czones(&child->siblings); // child+0xd0
for (DZRef *cr; (cr = czones.Next()) != 0; )
{
Mech__DamageZone *cz = my_mech->Zone(cr->index);
cz->RecurseSegmentTable(my_mech);
cz->SetGraphicState(2);
}
}
}
}
//#############################################################################
// CreateStreamedDamageZone -- @0049d304
//
// Parse ONE zone out of the .dmg notation file into the memory stream that
// the ctor above will later replay. Called once per entry in the .dmg
// "DamageZones" list by Mech::CreateDamageZoneStream (@004a474c).
//
// Token stream written, in order:
// (base fields via DamageZone::CreateStreamedDamageZone @0041e590)
// DescendOnDestruction (Logical)
// DestroySiblingsOnDestruction (Logical)
// GetSegmentIndex(name, skl) (int)
// leftLeg, rightLeg (from "LegDamageZone" == LeftLeg/RightLeg)
// vitalDamageZone (from "VitalDamageZone" == True/False, required)
// criticalSubsystemCount, then per "CriticalSubsystem <name> <%f> <%f>":
// subsystem index ("ControlsMapper"->0, "Power"->1, else Subsystems list),
// the two percentages
// redirectCount, then per "LODRedirect" entry: GetSegmentIndex of the name.
//
Logical
Mech__DamageZone::CreateStreamedDamageZone(
ResourceFile *resource_file,
NotationFile *model_file,
const char *model_name,
NotationFile *skl_file,
const char *damage_zone_name,
NotationFile *dmg_file,
const ResourceDirectories *directories,
MemoryStream *damage_zone_stream
)
{
CString name(damage_zone_name);
// base armour/structure record
if (!DamageZone::CreateStreamedDamageZone( // FUN_0041e590
model_file, model_name, skl_file, name, damage_zone_stream, dmg_file, directories))
return (Logical)-1;
Logical descend = 0;
((NoteX2 *)dmg_file)->ReadLogical(damage_zone_name, "DescendOnDestruction", &descend);
damage_zone_stream->WriteBytes(&descend, 4);
Logical destroySibs = 0;
((NoteX2 *)dmg_file)->ReadLogical(damage_zone_name, "DestroySiblingsOnDestruction", &destroySibs);
damage_zone_stream->WriteBytes(&destroySibs, 4);
int segIndex = GetSegmentIndex(name, skl_file); // FUN_0049db20
damage_zone_stream->WriteBytes(&segIndex, 4);
Logical isLeftLeg = 0, isRightLeg = 0;
const char *leg;
if (((NoteX2 *)dmg_file)->ReadString(damage_zone_name, "LegDamageZone", &leg))
{
if (Str_Equal(leg, "RightLeg")) isRightLeg = 1;
if (Str_Equal(leg, "LeftLeg")) isLeftLeg = 1;
}
damage_zone_stream->WriteBytes(&isLeftLeg, 4);
damage_zone_stream->WriteBytes(&isRightLeg, 4);
const char *vital;
if (!((NoteX2 *)dmg_file)->ReadString(damage_zone_name, "VitalDamageZone", &vital))
{
DebugStream << dmg_file << damage_zone_name << " missing VitalDamageZone!";
return False;
}
Logical isVital;
if (Str_Equal(vital, "True")) isVital = 1;
else if (Str_Equal(vital, "False")) isVital = 0;
else
{
DebugStream << model_name << " " << damage_zone_name
<< " Improper format for LegDamageZone";
return False;
}
damage_zone_stream->WriteBytes(&isVital, 4);
//
// CriticalSubsystem list ("<name> <%f> <%f>").
//
NotationList crits = ((NoteX2 *)dmg_file)->FindList(damage_zone_name, "CriticalSubsystem");
int critCount = crits.Count();
damage_zone_stream->WriteBytes(&critCount, 4);
for (NotationEntry *e = crits.First(); e != 0; e = e->Next())
{
char subName[64];
Scalar pctA, pctB;
if (sscanf(e->text, "%s %f %f", subName, &pctA, &pctB) == 0) // s__s__f__f
{
DebugStream << model_name << " error in CriticalSubsystem Format!";
return (Logical)-1;
}
damage_zone_stream->WriteBytes(&pctA, 4);
damage_zone_stream->WriteBytes(&pctB, 4);
int subIndex;
if (Str_Equal(subName, "ControlsMapper"))
subIndex = 0;
else if (Str_Equal(subName, "Power"))
subIndex = 1;
else
{
// search the model's "Subsystems" list, indices start at 2
const char *subFile;
((NoteX2 *)model_file)->ReadString("gamedata", "Subsystems", &subFile);
NotationFile *sf = ((RFileX2 *)resource_file)->Open(subFile);
NotationList list = ((NoteX2 *)sf)->FindList(0);
subIndex = 2;
NotationEntry *se;
for (se = list.First(); se != 0; se = se->Next(), ++subIndex)
if (Str_Equal(se->name, subName))
break;
((NoteX2 *)sf)->Close();
if (se == 0)
{
DebugStream << model_name << ':' << subName << " does not exist!";
return (Logical)-1;
}
}
damage_zone_stream->WriteBytes(&subIndex, 4);
}
//
// LODRedirect list: each redirect names a child zone resolved via the
// segment table (TableOf::Find @004274f8); -1 == not found is an error.
//
NotationList redirects = ((NoteX2 *)dmg_file)->FindList(damage_zone_name, "LODRedirect");
int redirectCount = redirects.Count();
damage_zone_stream->WriteBytes(&redirectCount, 4);
for (NotationEntry *e = redirects.First(); e != 0; e = e->Next())
{
CString childName(e->text);
if (childName.Length() == 0)
{
DebugStream << model_name << " error in LODRedirect Format!";
return (Logical)-1;
}
int childIndex = FindSegment(childName, skl_file); // FUN_004274f8
if (childIndex == -1)
{
DebugStream << resource_file << ' ' << childName << " Not Found!";
return (Logical)-1;
}
damage_zone_stream->WriteBytes(&childIndex, 4);
}
return True;
}
//#############################################################################
// GetSegmentIndex -- @0049db20
//
// Walk the skeleton (.skl) notation file's segment groups in order. Skipping
// the "LAB_ONLY" and "DamageZones" pseudo-groups, count real segments; for
// each segment read its "dzone" sub-list and, if any dzone name matches
// damage_zone_name, return the running segment index. Returns -1 on miss.
//
int
Mech__DamageZone::GetSegmentIndex(CString damage_zone_name, NotationFile *skl_file)
{
int index = -1;
NotationList segments = ((NoteX2 *)skl_file)->FindList(0); // FUN_0040485c
for (NotationEntry *seg = segments.First(); seg != 0; seg = seg->Next())
{
if (Str_Equal(seg->name, "LAB_ONLY")) continue;
if (Str_Equal(seg->name, "DamageZones")) continue;
++index;
NotationList dzones = ((NoteX2 *)skl_file)->FindList(seg->name, "dzone"); // FUN_00404720
for (NotationEntry *dz = dzones.First(); dz != 0; dz = dz->Next())
{
CString dzName(dz->text);
if (dzName == damage_zone_name) // FUN_004027d8
return index;
}
}
return -1;
}