Files
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00

372 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() {}
};
// (task #2: the DZSlot<T> stand-in is RETIRED -- the engine SlotOf<T>
// (engine/MUNGA/SLOT.h) is the real type; the binary vtables 0050bb84 /
// 0050bb7c ARE its <Subsystem*> / <Mech__DamageZone*> instantiations:
// FUN_004179d4 = Slot::Slot(Node*), FUN_00417a80 = AddImplementation
// (BIND), FUN_00417ab4 = GetCurrentPlug (READ). The stand-in's
// always-null Resolve() was why every critical plug read as unbound.)
//##########################################################################
//###################### 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)
SlotOf<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.
SlotOf<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
public:
// The zone's segment slot -- consumed by the @004d097c per-zone effect
// dispatcher (BTL4VideoRenderer::StartEntityEffectImplementation).
int EffectSegmentIndex() const { return segmentIndex; }
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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