Files
BT411/game/reconstructed/mechdmg.cpp
T
arcattackandClaude Opus 4.8 247e51e1e1 Reset-based respawn: reuse+heal the mech, not create-a-new-one (task #52)
The respawn glitches (2 mechs, on-fire respawn, camera-inside, can't-control,
wreck-never-disappears) all traced to one architectural divergence: our respawn
SEVERED playerVehicle on death and CREATED a new mech, leaving the old as a
permanent wreck and building a duplicate viewpoint whose old render tree was
never torn down.

The authentic engine (FUN_0049fb74 + RPPlayer) REUSES the same mech entity: on
respawn Mech::Reset heals it and moves it in place. Implemented faithfully,
adapted to our layout (the 1995 raw offsets map to different 2007 engine fields,
so reset the equivalent named members, not the offsets):
- Mech::Reset (real, was a reposition-only stub): reposition + kill dead-reckon
  (projectedOrigin/projectedVelocity/updateVelocity + our relocated gait
  accumulators) so the replicant stops lerping to the death spot; clear the
  death latch (movementMode=1, graphicAlarm=0); Heal every damage zone
  (new Mech__DamageZone::Heal: full structure, intact skin); DeathReset
  (vtable+0x28) every subsystem; ForceUpdate to broadcast.
- btplayer.cpp: VehicleDead no longer severs playerVehicle; the respawn re-post
  gates on the mech still being dead; DropZoneReply resets the EXISTING mech in
  place (heal+move) instead of creating a new one, then fires the warp. Warp
  moved to the shared placement (initial drop-in + respawn).

Verified 2-node: mech entity ID stays 3:22 across 3 deaths (reused, not a new
3:32); each Reset logs alive=1, 20 zones healed, 33 subsystems reset; A sees ONE
mech (no wreck+new pair). Warp fires each respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:13:44 -05:00

1026 lines
40 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];
// UNBOUND-PLUG GUARD. Resolve() (FUN_00417ab4) returns the subsystem bound
// to the DZSlot, or 0 if plug+8 was never wired. In the 1995 binary every
// critical subsystem exists and is bound, so it dereferences s with no check.
// In this reconstruction a critical subsystem can be UNBOUND (its type is not
// yet built, or its slot never connected), leaving s == 0 -> the original
// code AV'd here (mov [edx+0xc], edx=0) as soon as a zone with such a plug was
// destroyed. Skip the unbound entry rather than crash; log it so the missing
// binding can be tracked. NOT a behavioural stand-in: a bound plug takes the
// authentic path below unchanged. [T3 -- see open-questions: crit-subsys binding]
if (s == 0)
{
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[deathfx] crit-subsys " << i << "/" << criticalSubsystemCount
<< " UNBOUND (plug not wired) -- skipped\n" << std::flush;
continue;
}
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;
}
//#############################################################################
// Damage-state descriptor table -- @0041e4a8 / @0042a748 / @0042a2c8
//
// Streams this zone's damage-state descriptor table from the type-0x1e resource
// (binary this+0xd4). Each entry (0x1C bytes) is [f32 DamageLevel][i32
// EffectResource][i32 GraphicState][f32 TimeDelay], written ascending by
// DamageLevel. (Was a no-op stub -> the table was never built, so
// MechDeathHandler had nothing to walk and death was invisible.)
//#############################################################################
void
Mech__DamageZone::LoadCriticalSubsystems(MemoryStream *stream) // @0041e4a8
{
int count = 0;
stream->ReadBytes(&count, 4); // FUN_0042a748: entry count
damageDescriptors.reserve(count > 0 ? count : 0);
for (int i = 0; i < count; ++i) // FUN_0042a2c8 per entry
{
DamageDescriptor d;
stream->ReadBytes(&d.damageLevel, 4); // +0xc
stream->ReadBytes(&d.effectResource, 4); // +0x14
stream->ReadBytes(&d.graphicState, 4); // +0x10
stream->ReadBytes(&d.timeDelay, 4); // +0x18
damageDescriptors.push_back(d);
}
if (count != 0 && getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[deathfx] zone descriptor table loaded: " << count
<< " entries (first threshold=" << (count > 0 ? damageDescriptors[0].damageLevel : -1.0f)
<< " effect=" << (count > 0 ? damageDescriptors[0].effectResource : -1) << ")\n" << std::flush;
}
//
// @0042a664 -- first entry whose threshold is strictly greater than 'level'
// (0 if none / the zone is past the last threshold, i.e. fully destroyed).
//
const Mech__DamageZone::DamageDescriptor *
Mech__DamageZone::DescriptorForLevel(Scalar level) const
{
for (unsigned i = 0; i < damageDescriptors.size(); ++i)
if (damageDescriptors[i].damageLevel > level)
return &damageDescriptors[i];
return 0;
}
//
// @0042a5f4 -- true iff some entry's threshold lies in (oldLevel, newLevel]
// (a damage-state threshold was crossed this tick).
//
Logical
Mech__DamageZone::DescriptorCrossed(Scalar oldLevel, Scalar newLevel) const
{
for (unsigned i = 0; i < damageDescriptors.size(); ++i)
{
Scalar t = damageDescriptors[i].damageLevel;
if (t > oldLevel && t <= newLevel)
return True;
}
return False;
}
//
// @0042a6c4 -- first entry whose GraphicState enum == gstate (0 if none).
//
const Mech__DamageZone::DamageDescriptor *
Mech__DamageZone::DescriptorForGraphicState(int gstate) const
{
for (unsigned i = 0; i < damageDescriptors.size(); ++i)
if (damageDescriptors[i].graphicState == gstate)
return &damageDescriptors[i];
return 0;
}
// Inherited engine DamageZone::damageLevel (binary this+0x158) -- the [0,1]
// structural damage MechDeathHandler compares against the descriptor thresholds.
Scalar
Mech__DamageZone::GetStructureDamageLevel() const
{
return damageLevel;
}
void
Mech__DamageZone::ApplyDamageGraphicState(int gstate)
{
SetGraphicState(gstate); // engine DamageZone::SetGraphicState -> destroyed skin
}
void
Mech__DamageZone::Heal()
{
// Respawn heal: restore this zone to the undamaged/intact state the loader
// builds -- full structure, intact skin, no burning/destroyed flag. The
// authentic Mech::Reset (@0049fb74) heals via each subsystem's
// ResetToInitialState; the zone's structural state is restored here.
damageLevel = StructureMin; // 0.0 -- full structure (this+0x158)
ApplyDamageGraphicState(ExistsGraphicState); // 0 -- intact skin (undo Destroyed/Gone)
SetDamageZoneState(0); // clear burning / vital-destroyed state
}
//#############################################################################
// MechDeathHandler -- @0042a984 / @0042aa2c / @0042a9f4
//#############################################################################
// Effect-spawn bridge (mech4.cpp -- where Explosion::Make + the mech transform
// live): spawns the descriptor's explosion resource at the mech. The authentic
// path dispatches a class-5 message to the effect manager (app+0x38 -> the 0xBD3
// SubsystemMessageManager, unreconstructed); we use the established Explosion port.
extern void BTSpawnDamageEffect(Mech *mech, int effect_resource, int segment_index);
// Render bridge (btl4vid.cpp): swap a mech's wrecked segment meshes onto its
// already-built render tree when a damage zone's graphic state changes (the
// unported "RemakeEntity" render state). Entity* param -> Mech* binds fine.
extern void BTRemakeMechModel(Entity *entity);
MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
: owner(mech)
{
// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
lastLevel.assign(count, 0.0f);
}
MechDeathHandler::~MechDeathHandler() // @0042a9f4
{
}
//
// @0042aa2c -- each tick, walk the zones; where a zone's damageLevel rose since
// last tick, fire the current damage-band descriptor's explosion (binary +0xb8 &
// 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 &
// 8), applying that descriptor's GraphicState (the destroyed skin) to the zone.
//
void
MechDeathHandler::Tick() // @0042aa2c
{
if (owner == 0)
return;
int n = owner->damageZoneCount;
for (int i = 0; i < n && i < (int)lastLevel.size(); ++i)
{
Mech__DamageZone *zone = owner->Zone(i);
if (zone == 0)
continue;
Scalar level = zone->GetStructureDamageLevel();
Scalar prev = lastLevel[i];
if (level <= prev) // no new damage on this zone
continue;
lastLevel[i] = level;
const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor
{
const Mech__DamageZone::DamageDescriptor *dd =
zone->DescriptorForGraphicState(DamageZone::DestroyedGraphicState); // enum 1
if (dd != 0)
d = dd;
}
if (d != 0)
{
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
zone->segmentIndex); // segment (world position)
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin (graphic state)
// A graphic-state change means the segment's MODEL changed (intact ->
// destroyed variant, keyed by GetVideoObjectName(skl, gstate)). Fire
// the engine's model-rebuild flag so the renderer re-runs
// MakeMechRenderables and loads the destroyed segment geometry -- the
// same mechanism CulturalIcons use to visibly deform on damage
// (CULTURAL.cpp:91; EXPTBL.cpp:549 does this per zone in the engine's
// ExplosionTable). Without this the tree stays as built at spawn (intact).
// The render's "RemakeEntity" state (which consumes this flag) was
// NOT ported -- the mech tree is built once at spawn -- so we drive the
// equivalent directly: set the flag (authentic signal; also flags the
// zone for net damage replication) AND call the render bridge, which
// swaps the wrecked segment mesh onto the already-built tree in place.
// The single-threaded frame loop makes the immediate swap safe.
if (d->graphicState != DamageZone::ExistsGraphicState)
{
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
}
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
<< "\n" << std::flush;
}
}
}