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>
369 lines
14 KiB
C++
369 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
|
|
// PORT (respawn replication): latched when any zone is destroyed, so a
|
|
// REPLICANT observer can reverse the one-way wreck swap when the master's
|
|
// Mech::Reset heals + re-broadcasts the zones (falling damage edge). Not a
|
|
// binary field -- the authentic un-wreck rides the type-0 graphic-state hook
|
|
// (Mech::ReadUpdateRecord case 0), which our port drives via the zone levels.
|
|
Logical wasWrecked;
|
|
};
|
|
|
|
#endif
|