Files
BT412/game/reconstructed/mechdmg.hpp
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

363 lines
14 KiB
C++

//==========================================================================//
// File: mechdmg.hpp //
// Project: BattleTech //
// Contents: Mech::DamageZone //
//---------------------------------------------------------------------------//
// 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 in part_012.c)
// cross-referenced with the SURVIVING header MECHDMG.HPP (ground truth --
// declarations below are reproduced verbatim from it). The only additions
// over the surviving header are the byte-offset annotations ("@0x..") and
// the per-method @ADDR evidence, which live in comments. See mechdmg.cpp.
//
// Object layout (size 0x1b8, ctor @0049ce50):
// [0x000] DamageZone base (vtable @0050bb90, base ctor @0041df5c):
// owner Mech @0x00c, graphic alarms @0x010 / @0x064, flags @0x0b8,
// 5 armor/structure scalars @0x144..0x154, structureLevel @0x158,
// ref-counted name @0x15c.
// [0x160] Mech::DamageZone members (see field comments below).
//
#if !defined(MECHDMG_HPP)
# define MECHDMG_HPP
#include <vector>
# if !defined(DAMAGE_HPP)
# include <damage.hpp>
# endif
# if !defined(SLOT_HPP)
# include <slot.hpp>
# endif
# if !defined(TABLE_HPP)
# include <table.hpp>
# endif
# if !defined(ENTITYID_HPP)
# include <entityid.hpp>
# endif
# include "mechrecon.hpp" // reconstruction shim
class Mech;
class Subsystem;
class DamageZone;
//======================================================================//
// Reconstruction proxies for the per-zone containers/slots.
// The recovered code drives the redirect table, integer plugs and the
// parent/subsystem slots through a verb set (Construct / Add / Resolve /
// Next / Count / value ...) that the modern engine TableOf / PlugOf /
// SlotOf spell differently; these behaviour-neutral stand-ins carry that
// surface so the recovered bodies compile. (Reconstruction only.)
//======================================================================//
struct DZIntegerPlug // PlugOf<int> stand-in
{
int value;
DZIntegerPlug() : value(0) {}
DZIntegerPlug(const int *p) : value(p ? *p : 0) {}
};
struct DZIndexTable // TableOf<IntegerPlug*,int> stand-in
{
void Construct(int, int) {}
void Add(DZIntegerPlug *) {}
};
struct DZIndexTableIterator // TableIteratorOf<...> stand-in
{
DZIndexTableIterator(DZIndexTable *) {}
int Count() { return 0; }
DZIntegerPlug *Next() { return 0; }
DZIntegerPlug *operator[](int) { return 0; }
void DeleteContents() {}
};
template<class T> struct DZSlot // SlotOf<T> stand-in
{
DZSlot() {}
DZSlot(const void *) {} // ctor(owner)
void Construct(int) {}
T Resolve() { return T(); }
DZSlot &operator=(const DZSlot &) { return *this; }
template<class A> DZSlot &operator=(const A &) { return *this; }
};
//##########################################################################
//###################### MechCriticalSubsystem #################
//##########################################################################
//
// One critical subsystem that a damage zone can destroy. Streamed from the
// "CriticalSubsystem" tokens in the .dmg file ("<name> <pct> <pct>").
// Object size 0x1c (28 bytes). ctor @0049dd18, dtor @0049dd44,
// vtable @0050bb8c (single slot: the destructor).
//
class
MechCriticalSubsystem SIGNATURED
{
public:
MechCriticalSubsystem(DamageZone *owner); // @0049dd18
virtual ~MechCriticalSubsystem(); // @0049dd44 (vtable slot 0)
DZSlot<Subsystem*>
subsystemPlug; // @0x04 ctor @0049dd7e / dtor @0049dd9d (vtable @0050bb84)
Scalar
damagePercentage, // @0x10 streamed (1st "%f")
damagePercentageUsed; // @0x14 streamed (2nd "%f"); init 0
Scalar
criticalWeight; // @0x18 init 0; summed into owner.criticalWeightSum
Logical
TestInstance() const; // @0049dd74 (returns 1)
};
//##########################################################################
//#################### Mech::DamageZone ###########################
//##########################################################################
//
// vtable @0050bb90 (7 slots). Overridden slots vs the DamageZone base:
// slot 0 (dtor) -> ~Mech::DamageZone @0049d23c
// slot 3 (SetGraphic) -> SetGraphicState @0049c66c
// slot 6 (TakeDamage) -> TakeDamage @0049c690
// Slots 1,2,4,5 (@004173b8/@00417938/@0041ddd8/@0041dd98) inherit DamageZone.
//
class Mech__DamageZone :
public DamageZone
{
// The owning Mech reads zone state (structureLevel, criticalSubsystemCount,
// vital/destroyed flags) when routing damage + reporting -- the binary's Mech
// damage code (FUN_004a0230 / FUN_0049fb54 etc.) is a Mech method touching
// these zone fields directly.
friend class Mech;
friend class MechDeathHandler; // walks the descriptor table + applies the destroyed skin
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// LOD Support
//
protected:
void
LODDamageRouter(EntityID inflicting, Damage damage); // @0049c40c
int
RandomRedirect(); // @0049c600
typedef DZIntegerPlug IntegerPlug; // (was PlugOf<int>)
typedef DZIndexTable DamageZoneIndexTable; // (was TableOf<IntegerPlug*,int>)
typedef DZIndexTableIterator DamageZoneIndexTableIterator; // (was TableIteratorOf<...>)
// Zones which are artifacts have a table of their real children.
DamageZoneIndexTable redirectTable; // @0x160 ctor @00424767(,0,1)
// The result of the last random redirect
int lastHitZone; // @0x178 init RandomRedirect() or 0
Scalar lastDamageTime; // @0x17c init now() (ticks/TicksPerSec)
EntityID lastInflicting; // @0x180 copied from mech[+0x184]
//
// Zones which are real have a pointer (slot) back to their artifact parent.
// With the current art (1/30/96) no zone has both a parent and children,
// and many zones have neither.
DZSlot<Mech__DamageZone*> parentArtifactZone; // @0x188 ctor @0049ddc9 (vtable @0050bb7c)
// --- reconstruction-named state (decomp spellings; real DamageZone
// base uses damageLevel/damageZoneState/etc.) ------------------
// NOTE: structureLevel was a RE-DECLARATION of the engine DamageZone base
// field `damageLevel` (binary offset 0x158). Compiled as a subclass of the
// engine DamageZone, the re-declared copy landed at a different offset and
// stayed uninitialised (0xCDCDCDCD) while the engine ctor/TakeDamage wrote
// the *base* damageLevel -- so the death/vital logic read garbage. Removed;
// the reconstruction now uses the inherited `damageLevel` (the same field the
// binary's 0x158 IS). Likewise damageScale[5]/defaultArmorPoints are the
// inherited base members, not raw 0x144/0x140 offsets.
// graphicState/graphicStateAlarm/flags/state were RE-DECLARATIONS of engine
// DamageZone base fields (binary 0x010/0x064/0x0b8): the StateIndicators
// damageZoneState (2 states, @0x010) and damageZoneGraphicState (3 states,
// @0x064), and changedFlags. The engine ctor initialises them; the
// re-declared copies stayed uninitialised -> SetLevel on garbage crashed when
// a vital zone was destroyed. Removed; the reconstruction uses the inherited
// accessors: SetDamageZoneState/GetDamageZoneState (was graphicState) and
// SetGraphicState/GetGraphicState (was graphicStateAlarm/state).
void
UpdateLODDamageLevel(); // @0049c51c
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Descending Heirarchy when DamageZone Destroyed Support
//
void
ShortAttachedGenerators(); // inlined into TakeDamage @0049c690
void
SendSubsystemDamage(); // @0049c9a8
void
RecurseSegmentTable(Mech *my_mech); // @0049cad4
int
segmentIndex; // @0x194 GetSegmentIndex() result
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Damage Support
//
public:
void
TakeDamage(Damage& damage); // @0049c690 (vtable slot 6)
Subsystem*
CriticalHit(Damage &damage_data); // @0049ccc4
protected:
Logical
vitalDamageZone; // @0x198 streamed "VitalDamageZone"
Logical
descendOnDestruction; // @0x19c streamed "DescendOnDestruction"
Logical
destroySiblingsOnDestruction; // @0x1a0 streamed "DestroySiblingsOnDestruction"
Logical
leftLeg; // @0x1a4 "LegDamageZone == LeftLeg"
Logical
rightLeg; // @0x1a8 "LegDamageZone == RightLeg"
int
criticalSubsystemCount; // @0x1ac streamed count
Scalar
criticalWeightSum; // @0x1b0 sum of criticalWeight
MechCriticalSubsystem
**criticalSubsystems; // @0x1b4 array[criticalSubsystemCount]
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Damage-state descriptor table (binary this+0xd4). Streamed from the
// type-0x1e resource by LoadCriticalSubsystems (FUN_0041e4a8 -> FUN_0042a748);
// as this zone's damageLevel crosses each entry's threshold, MechDeathHandler
// fires the entry's explosion + applies its GraphicState (destroyed skin).
// Entries ascend by damageLevel. (Was un-built -- the loader was a stub.)
//
public:
struct DamageDescriptor // binary entry 0x1C (ctor FUN_0042a2c8)
{
Scalar damageLevel; // +0xc threshold [0,1]
int graphicState; // +0x10 GraphicState enum (Destroyed=1)
int effectResource; // +0x14 ExplosionModelFile resource id
Scalar timeDelay; // +0x18
};
// @0042a664: first entry whose threshold > level (0 if none / fully destroyed).
const DamageDescriptor *DescriptorForLevel(Scalar level) const;
// @0042a5f4: true iff some entry's threshold lies in (oldLevel, newLevel].
Logical DescriptorCrossed(Scalar oldLevel, Scalar newLevel) const;
// @0042a6c4: first entry whose GraphicState == gstate (0 if none).
const DamageDescriptor *DescriptorForGraphicState(int gstate) const;
Scalar GetStructureDamageLevel() const; // inherited damageLevel (binary +0x158)
// Apply a descriptor's GraphicState (Destroyed=1) to this zone -- the
// destroyed-skin swap the renderer reads (public wrapper so MechDeathHandler
// can drive it; the base SetGraphicState is otherwise unreachable from it).
void ApplyDamageGraphicState(int gstate);
// Restore this zone to full structure + intact skin -- the respawn heal
// (Mech::Reset, @0049fb74 resets every subsystem's structure).
void Heal();
protected:
std::vector<DamageDescriptor>
damageDescriptors; // binary this+0xd4 collection
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Graphic State Support
//
public:
// SetGraphicState (vtable slot 3) is NOT overridden -- the binary's override is
// byte-identical to the engine base DamageZone::SetGraphicState (sets
// damageZoneGraphicState + GraphicStateChangedFlag), so we use the base.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction / Destruction
//
public:
static const int NullDamageZone; // = -1
Mech__DamageZone( // @0049ce50
Mech *mech,
int damage_zone_index,
MemoryStream *stream
);
void SetLODParentPointers(); // @0049d1d0
void LoadCriticalSubsystems(MemoryStream *stream); // @0041e4a8 (crit table)
~Mech__DamageZone(); // @0049d23c
static Logical
CreateStreamedDamageZone( // @0049d304 (one zone)
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
);
static int
GetSegmentIndex( // @0049db20
CString damage_zone_name,
NotationFile *skl_file
);
};
//###########################################################################
//###########################################################################
// MechDeathHandler (@0042a984 / @0042aa2c)
//
// A per-frame handler built by the Mech ctor (cached at mech[0x214]) that turns
// subsystem/zone damage into VISIBLE destruction: each tick it walks the mech's
// damage zones and, as a zone's damageLevel crosses a descriptor threshold,
// spawns that entry's explosion effect at the zone + applies its GraphicState
// (the destroyed skin). The binary registers it on the mech's Performance list
// (mech+0xbc); the bring-up drive override does not tick that list, so
// Mech::PerformAndWatch drives Tick() directly. Effect spawn uses the
// established Explosion::Make port (the authentic effect-message manager, the
// 0xBD3 SubsystemMessageManager, is unreconstructed) -- see BTSpawnDamageEffect.
//###########################################################################
class MechDeathHandler
{
public:
MechDeathHandler(Mech *mech); // @0042a984
~MechDeathHandler(); // @0042a9f4
void Tick(); // @0042aa2c (the Performance)
private:
Mech *owner; // @0x14
std::vector<Scalar> lastLevel; // @0x10 per-zone last damageLevel cache
};
#endif