Files
BT411/game/reconstructed/mechdmg.cpp
T
arcattackandClaude Opus 4.8 160b78e38d Respawn: swirl warp, cockpit-not-black on respawn, observer sees peer un-wreck
Three respawn-visual bugs the user saw, each grounded in the engine/decomp:

1. Warp = flat blue BLOB, not the demo's swirly blue/light-blue/white shimmer.
   Ground truth (content/VIDEO/MAT/DAY/BTFX.VMF): tsphere_mtl = a SCROLLING bintA
   texture (tsphere_scr_tex, SPECIAL SCROLL) + EMISSIVE {0.7,0.5,1} + RAMP "sky"
   (dark-blue->white).  The swirl IS the scrolling texture; the colour is the
   emissive/ramp.  Our draw did COLOROP=SELECTARG1/ARG1=TFACTOR, which REPLACES
   every texel with one flat colour -> the blob.  Fix: MODULATE the (bound,
   scrolling) texture by a TFACTOR set to the authentic EMISSIVE hue (0xB380FF)
   so the swirl survives and reads blue-white.  DrawMesh's cached SetTexture
   (L4D3D.cpp:1215) leaves our MODULATE ops standing since textured meshes drew
   first.  Additive glow, all state saved/restored.

2. First-person view BLACK on the dying/respawning mech until V.  SetViewInside's
   body-hide + '_cop' canopy suppression + viewSkeleton update were gated !wrecked,
   and RebuildMechRenderables (respawn un-wreck) restored the full OUTSIDE torso
   with no '_cop' rule -> the cockpit eyepoint ended up wrapped in opaque geometry.
   Fix: factor the per-segment mesh selection into ApplyViewSkeleton(viewpoint,
   inside) shared by SetViewInside AND RebuildMechRenderables, so respawn re-asserts
   the inside skeleton + '_cop' hide; record viewSkeleton even while wrecked.  Only
   the mech the local camera views FROM gets the inside treatment (a replicant is
   always outside).

3. OBSERVER never saw the peer respawn -- the peer's wreck sat forever.  The wreck
   appears incidentally via rising damage-zone replication -> MechDeathHandler::Tick
   -> BTRemakeMechModel (one-way).  The un-wreck+warp ran master-only in Mech::Reset.
   Fix (reuses the existing damage channel, no stream-framing change): Mech::Reset
   also ForceUpdate(DamageZoneUpdateModelFlag) so healed zones cross; Tick handles
   the FALLING edge -- on a ReplicantInstance whose zone heals from destroyed, call
   BTRebuildMechModel + BTStartWarpEffect once (wasWrecked latch).  This is the port
   analog of the binary's type-0 graphic-state -> ResetPose un-wreck hook
   (Mech::ReadUpdateRecord case 0); the full Mech::WriteUpdateRecord death record
   (type 6) is deferred (would touch the netcode framing).

Smoke-verified headless (2-node): victim respawns intact + warp; observer logs
"replicant un-wrecked + warp" at the peer's spawn point; no crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 08:09:27 -05:00

1060 lines
42 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), wasWrecked(0)
{
// 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];
// FALLING edge = the master's Mech::Reset healed + re-broadcast this zone
// (ForceUpdate(DamageZoneUpdateModelFlag)). The rising-damage swap below is
// one-way; on a REPLICANT observer, reverse it here -- drop the destroyed
// segment meshes (BTRebuildMechModel) and play the recovery warp -- so the
// peer's wreck un-wrecks + warps back on the observer's screen. Gated to
// ReplicantInstance: the master already un-wrecks + warps locally in
// Mech::Reset (mech4.cpp), so it must NOT double-fire here. The authentic
// engine drives this off the type-0 graphic-state transition in
// Mech::ReadUpdateRecord (mech.cpp case 0 -> ResetPose); our port has no
// ported RemakeEntity state, so we react to the replicated zone level.
if (level < prev)
{
lastLevel[i] = level;
if (wasWrecked && prev >= 1.0f && level < 1.0f)
{
wasWrecked = 0; // one un-wreck per death->respawn
if (owner->GetInstance() == Entity::ReplicantInstance)
{
extern void BTRebuildMechModel(Entity *entity);
extern void BTStartWarpEffect(float x, float y, float z);
BTRebuildMechModel((Entity *)owner);
BTStartWarpEffect((float)owner->localOrigin.linearPosition.x,
(float)owner->localOrigin.linearPosition.y,
(float)owner->localOrigin.linearPosition.z);
if (getenv("BT_DEATH_LOG"))
DEBUG_STREAM << "[respawn] replicant un-wrecked + warp at ("
<< owner->localOrigin.linearPosition.x << ","
<< owner->localOrigin.linearPosition.z << ")\n" << std::flush;
}
}
continue;
}
if (level <= prev) // unchanged -- no new damage
continue;
lastLevel[i] = level;
const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor
{
wasWrecked = 1; // latch: a wreck the observer must later reverse
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;
}
}
}