Files
BT411/game/reconstructed/mechsub.hpp
T
Joe DiPrimaandClaude Fable 5 819772f974 the rest of the Myomers wrapper: two live outputs recovered, and one branch that never fires
Follow-on to b70654d, which chained the base sim and stopped there. Decoding
the remaining 0x17c bytes of @004b8b9c turned up four more blocks the port had
dropped along with it. In binary order the registered Performance is:

  @004b8bab  chain PoweredSubsystemSimulation                  (fixed in b70654d)
  @004b8bb9  un-powered self-repair of this myomer's own zone   (dead -- see below)
  @004b8c1e  republish outputVoltage@0x344 from the source
  @004b8c5a  republish speedEffect@0x31C, the drive fed to the mover
  @004b8ceb  run the inner integrator, ONLY when outputVoltage > 0

speedEffect is the interesting one. AvailableOutput scales by the Mech base
speed and the wrapper divides by it again, so the two cancel and what lands in
+0x31C is a 0..1 FRACTION of full drive carrying the gear ratio, the thermal
degradation curve and the accumulated zone damage. Neither it nor outputVoltage
was ever written before: outputVoltage sat wherever the ctor left it and
speedEffect stayed pinned at its ctor 1.0f.

Un-stubbed DamageStructureLevel() while in here. It was `return 0.0f`, which
held AvailableOutput's (1 - damage) factor at 1, so a shot-up myomer drove
exactly as well as a fresh one. Routed to the base's GetSubsystemDamageLevel()
bridge, the same cell sensor.cpp:312 already reads. Measured dmg=0.6 -> speed=0.4.

@004b8bb9 does not work, and did not work in 1995 either. It builds a Damage
(Explosive, amount 0xbc343958 ~= -0.011f, impactPoint from owner+0x100, burst 1)
and calls the ZONE's TakeDamage -- vtable +0x18, not the subsystem's +0x24 -- so
the plain `damageLevel += amount * damageScale[type]`. Read on its own that is
"a myomer you power down slowly heals". But a subsystem's private zone is built
by the 2-arg `new DamageZone(this, 0)`, and DAMAGE.cpp:187-190 zeroes all five
damageScale entries; Reset never touches them and the only other writer is
Mech__DamageZone, the mech's streamed zones. The sum is always `+= amount * 0`.
Seeded a zone to 0.6, held it at NoVoltage for ~1500 ticks: damageLevel never
moved. Reconstructed and deliberately not "fixed" -- a working repair here would
be behavior we invented. The crit path reaches subsystem damage by writing
damageLevel directly, which is why subsystems still die.

That generalises, so it is written up in context/combat-damage.md on its own:
you cannot damage a subsystem's own zone through DamageZone::TakeDamage at all.
Anything reconstructed later that means to hurt a subsystem has to go the way
the crit path goes, or it will silently do nothing.

Verified, self-damage runs across two death/respawn cycles:
  torso    96/96 samples elec=4, zero dips
  myomers  96/97 elec=4 (the odd one is the first tick, before the machine runs)
  healthy  outV=10000 speed=1     un-powered  outV=0 speed=0

BT_MYOMERS_LOG probes the four outputs; BT_MYOMERS_REPAIR_TEST=<level> seeds the
zone and drops the subsystem into Manual so the repair branch can be watched
without the AutoConnect hunt restoring power a frame later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:06:56 -05:00

355 lines
16 KiB
C++

//===========================================================================//
// File: mechsub.hpp //
// Project: BattleTech Brick: Entity Manager //
// Contents: MechSubsystem -- BT damage-coupled base for all mech subsystems //
//---------------------------------------------------------------------------//
// Date Who Modification //
// -------- --- ---------------------------------------------------------- //
// --/--/95 ?? Initial coding. //
//---------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved //
// PROPRIETARY AND CONFIDENTIAL //
//===========================================================================//
//
// RECONSTRUCTED from the shipped binary. No mechsub.hpp survived, but the
// surviving MECHTECH.HPP `#include <mechsub.hpp>` and references
// `MechSubsystem::TechStatusTypeCount`, `MechSubsystem *monitoredSubsystem`,
// confirming a distinct BT class named MechSubsystem.
//
// RECONCILIATION VERDICT: MechSubsystem IS a distinct base class -- it is NOT
// an alias for the MUNGA `Subsystem` (RP/MUNGA/SUBSYSTM.HPP, which has no
// "Tech" status and no DamageZone coupling). It sits between MUNGA Subsystem
// and HeatableSubsystem:
//
// Simulation (MUNGA)
// Subsystem (MUNGA, SUBSYSTM.HPP -- we have it)
// MechSubsystem <-- THIS FILE (classID 0xBBB, vtable 0x50e210)
// HeatableSubsystem (heat.cpp -- string @0x50e700, classID 0xBBC?)
// HeatSink ... (heat.cpp / heatfamily_reslice.cpp)
// MechTech (mechtech.cpp -- : MechSubsystem? see note)
// <non-heatable mech subsystems>
//
// *** CLASSMAP CORRECTION (for the next reconciler) ***
// The heat.cpp / heat.hpp reconstruction attributed the cluster
// @0x4ac07c..@0x4ac9ec (vtable 0x50e210, dtor 0x4ac868) to its
// "Subsystem/HeatableSubsystem base". The string and behavioural evidence say
// that cluster is actually MechSubsystem:
// * CreateStreamedSubsystem @0x4ac9ec stamps classID 0xBBB and model size
// 0xE4, and parses ONLY damage/critical fields (PrintSimulationState,
// CollisionCriticalHitWeight, VideoObjectName, VitalSubsystem) -- NO
// thermal fields. (Thermal init is HeatSink's @0x4ae150.)
// * The ClassDerivations name string is "MechSubsystem" (@0x50def5,
// @0x50e2fd), distinct from "HeatableSubsystem" (@0x50e700).
// * The class couples the subsystem to a DamageZone (this+0xE0) and exposes
// the subsystem status/alarm with state names @0x50df17:
// Destroyed / Damaged / CoolantLeaking / Overheating / AmmoBurning /
// Jammed / BadPower (TechStatusTypeCount of these), plus the
// PrintSimulationState names DefaultState / Destroyed / Exploding.
// heat.cpp's `HeatableSubsystem(currentTemperature@0x114)` ctor @0x4ac644 does
// NOT write 0x114 -- it is in fact a MechSubsystem ctor. HeatableSubsystem is
// a (near-empty) layer above MechSubsystem; the temperature/heat fields belong
// to HeatSink (@0x4adda0).
//
// This reconstruction is BEST-EFFORT and overlaps heat.cpp; names of the
// MechSubsystem data members are inferred from usage. See mechsub.cpp for the
// per-method @ADDR evidence.
//
#if !defined(MECHSUB_HPP)
# define MECHSUB_HPP
#if !defined(SUBSYSTM_HPP)
# include <subsystm.hpp>
#endif
#if !defined(DAMAGE_HPP)
# include <damage.hpp>
#endif
#include <affnmtrx.hpp> // AffineMatrix
#include <average.hpp> // AverageOf<T>
#include <scalar.hpp> // Scalar
#include <alarm.hpp> // GaugeAlarm
#include "mechrecon.hpp" // reconstruction shim (proxies, artifact globals, type aliases)
//##################### Reconstruction type aliases #####################
#if !defined(BT_RECON_TYPE_ALIASES)
# define BT_RECON_TYPE_ALIASES
typedef AffineMatrix Matrix34; // 3x4 affine (AFFNMTRX.h)
typedef GaugeAlarm AlarmIndicator; // GAUGALRM.h
typedef AverageOf<Scalar> FilteredScalar; // 15-sample running average (AVERAGE.h)
#endif
//##################### Forward Class Declarations #######################
class Mech;
class NotationFile;
class Damage;
//###########################################################################
//##################### MechSubsystem Model Resource ####################
//###########################################################################
//
// Extends the base Subsystem resource. The mech-specific fields begin at +0x30
// in the parsed resource record (see MechSubsystem::CreateStreamedSubsystem
// @0x4ac9ec). Resource record size = 0xE4.
//
struct MechSubsystem__SubsystemResource:
public Subsystem::SubsystemResource
{
Scalar armorByFacing[5]; // +0x30 per-facing armour (default -1.0f)
Scalar structureReference; // +0x44 health/structure divisor (default -1.0f)
int vitalSubsystemIndex; // +0x48 "VitalSubsystem" segment (default -1)
char videoObjectName[128]; // +0x4C "VideoObjectName" (default "None")
ResourceDescription::ResourceID
alarmModel; // +0xCC (resolved id; the RECORD reserves a
// 12-byte field here -- @004ac9ec parses
// it via FUN_00408944 as 3 floats. The
// reconstruction only consumes the leading
// id, but the 12-byte footprint MUST be
// preserved or every field below slides
// 8 bytes low and the heat resource --
// which begins at +0xE4 -- misaligns.)
int _alarmModelReserved[2]; // +0xD0 remainder of the 12-byte 0xCC field
int printSimulationState; // +0xD8 "PrintSimulationState"
Scalar collisionCriticalHitWeight; // +0xDC "CollisionCriticalHitWeight"
Scalar criticalReference; // +0xE0 (default -1.0f)
};
//###########################################################################
//########################## MechSubsystem #############################
//###########################################################################
//
// Abstract BT base for every subsystem mounted in a mech. Owns a DamageZone
// (this+0xE0) representing its armour facings and structural integrity, a
// 3+-level status alarm (this+0x2C) reported by the cockpit MechTech, and the
// critical-hit / damage-distribution machinery.
// (vtable @0x50e210, ctors @0x4ac530 / @0x4ac644, dtor @0x4ac868,
// classID 0xBBB, resource model size 0xE4.)
//
class MechSubsystem:
public Subsystem
{
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Shared Data Support
//
public:
static Derivation ClassDerivations; // name string "MechSubsystem"
static SharedData DefaultData;
static Recon MessageHandlers; // SharedData ctor operand
enum { StateCount = 7 }; // TechStatusTypeCount
// Attribute Support (AUDIO_FIDELITY.md F15): the binary registers
// ConfigureActivePress as a MechSubsystem-BASE attribute (id 2,
// descriptor @0x50de5c -> subsystem+0x110) -- the configure-session
// flag every weapon's ConfigureMappables handler drives (0 open /
// -1 idle) and the authored configure-ticker audio watches at
// thresh=-1. Publishing here makes it resolve on EVERY subsystem
// (each reads its own +0x110) and restores the binary-authentic
// dense id numbering (children start at 3, not 2).
enum {
ConfigureActivePressAttributeID = Simulation::NextAttributeID, // 2
NextAttributeID // 3
};
static const IndexEntry AttributePointers[];
static AttributeIndexSet& GetAttributeIndex();
// alarm-changed callback (vtable slot +0x34) + host back-pointer
void OnAlarmChanged();
Mech *owner; // owning mech (== hostEntity)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Status model
//
public:
// Damage/condition status states (alarm @this+0x2C). Parsed from the
// state-name table @0x50df17 by ConfigureActivePress (@0x4ac194).
enum TechStatusType {
Destroyed = 0,
Damaged = 1,
CoolantLeaking = 2,
Overheating = 3,
AmmoBurning = 4,
Jammed = 5,
BadPower = 6,
TechStatusTypeCount // = 7 (referenced by MECHTECH.HPP)
};
// Simulation-state values stored at this+0x40 (printed by PrintState).
enum SimulationState {
DefaultState = 0,
DestroyedState = 1,
ExplodingState = 2
};
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Test Class Support
//
public:
static Logical
TestClass(Mech&);
Logical
TestInstance() const;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Construction and Destruction
//
public:
typedef MechSubsystem__SubsystemResource SubsystemResource;
// @0x4ac530 -- light ctor (name + classID); DamageZone named "None".
MechSubsystem(
Mech *owner,
int subsystem_ID,
const char *subsystem_name,
RegisteredClass::ClassID class_id,
SharedData &shared_data = DefaultData
);
// @0x4ac644 -- resource ctor; copies armorByFacing[5] -> DamageZone+0x144,
// structureReference -> DamageZone+0x140, and pre-inverts the armour
// scalars (1.0 / (armour * reference)) where above epsilon.
// (heat.cpp mislabels this as "HeatableSubsystem".)
MechSubsystem(
Mech *owner,
int subsystem_ID,
SubsystemResource *subsystem_resource,
SharedData &shared_data = DefaultData
);
~MechSubsystem(); // @0x4ac868
static int
CreateStreamedSubsystem( // @0x4ac9ec
NotationFile *model_file,
const char *model_name,
const char *subsystem_name,
SubsystemResource *subsystem_resource,
NotationFile *subsystem_file,
const ResourceDirectories *directories,
int passes = 1
);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Subsystem virtual overrides (slots on vtable @0x50e210)
//
public:
// VIRTUAL (#47): the binary dispatches slot 12 through the vtable --
// MechTech's TechnicalAssistance scan calls (*sub_vtbl+0x30) on every
// monitored subsystem. Non-virtual here made a MechSubsystem* call bind
// statically to this base tier, so ProjectileWeapon's Jammed/AmmoBurning
// bits (@004bbf88) never reached the scan. (HeatableSubsystem already
// declared its own virtual; this unifies the root.)
virtual LWord
GetStatusFlags(); // slot 12, @0x4ac144
void
ResetToInitialState(Logical powered); // slot 8, @0x4ac1d4
void
PrintState(); // slot 13, @0x4ac8c0
Logical
HandleMessage(int message); // @0x4ac0bc
virtual void
TakeDamage(Damage &damage); // slot via this+0x24 (see DamageDelta)
Logical
IsDamaged(); // @0x4ac9c8 -- NOVICE-experience lockout predicate (player+0x274 == 0), NOT a damage query (issue #2); see mechsub.cpp
// Critical-state readouts for ColorMapperCritical (CriticalConnection):
// the raw simulation state (DefaultState=0 / Destroyed=1 / Exploding=2) and
// the subsystem's own damage zone (a real DamageZone behind the proxy type;
// set in the ctor via `new DamageZone(...)`, mechsub.cpp).
int
GetSimulationState() const { return simulationState; }
ReconDamageZone *
GetDamageZoneProxy() const { return damageZone; }
// gauge-complete wave: this subsystem's OWN structural damage [0,1] (0=intact,
// 1=destroyed) -- the engine DamageZone::damageLevel of the zone the ctor built
// (new DamageZone(this,0) @0xE0). Read via the ENGINE type's NAMED member (not
// the ReconDamageZone proxy, whose offset-0 structureLevel aliases the vtable
// ptr as a float = garbage). The binary reads *(this[0x38]+0x158) here (== the
// same anchor UpdateCoolant/SensorSimulation use). Defined in mechsub.cpp.
Scalar GetSubsystemDamageLevel() const;
void SetSubsystemDamageLevel(Scalar level);
// task #2 (crit propagation): the zone-side crit paths
// (Mech__DamageZone::CriticalHit @0049ccc4 / SendSubsystemDamage
// @0049c9a8) reach these subsystem facets.
int IsVitalSubsystem() const { return vitalSubsystem; }
// @0x4ac07c -- run the virtual TakeDamage and return the resulting
// rise in the subsystem's own damageLevel (the crit allotment used).
Scalar ApplyDamageAndMeasure(Damage &damage);
// Apply a Damage straight to this subsystem's OWN zone -- the engine
// DamageZone::TakeDamage virtual (zone vtable +0x18), NOT the subsystem's
// own TakeDamage (this vtable +0x24, which is what ApplyDamageAndMeasure
// runs and which additionally cascades crits/vital checks). Myomers'
// Performance @004b8c16 calls the ZONE virtual directly, so it gets the
// plain `damageLevel += amount * damageScale[type]` and nothing else.
// Lives here because only a complete-DamageZone TU may deref the proxy.
void ApplyZoneDamage(Damage &damage);
// The destroyed-side effects SendSubsystemDamage applies when the
// subsystem's own zone hits 1.0: statusAlarm Destroyed(1) + the
// PrintState debug gate + the zone's state valve. [T1 @0049c9a8]
void ForceCriticalFailure();
// issue #22 (PORT, fresh-mech respawn emulation): undo everything
// ForceCriticalFailure/TakeDamage did to the subsystem's own private
// damage zone + status -- the binary respawn built a NEW mech, so a
// respawned subsystem was always pristine.
void RespawnRepair();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Internal helpers
//
protected:
// @0x4ac22c -- clear structure & both alarms to the inactive level.
void ClearStatus();
// @0x4ac274 -- distribute a critical hit across the contained critical
// subsystems, scaling damage by 1/criticalCount and logging
// "ammo explosion damaging ".
void DistributeCriticalHit(Damage &damage);
// @0x4ac194 -- map a status-state name string -> TechStatusType value
// using the name table @0x50de74. PUBLIC (was protected): the binary
// calls it cross-TU from BTL4GaugeAlarmManager::CreateGaugeAlarmStreamItem
// (@004cc294) to parse "gaugeAlarm Jammed = engEject"-style entries (#47).
public:
static Logical
LookupStatusType(const char *name, int *out_value);
protected:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Local data (byte offsets into the shipped object)
//
protected:
AlarmIndicator statusAlarm; // @0x2C this+0xb (condition/status alarm)
int simulationState; // @0x40 DefaultState/Destroyed/Exploding
ReconDamageZone *damageZone; // @0xE0 this[0x38] (0x160-byte zone)
int vitalSubsystem; // @0xE4 this[0x39] -- destroying this
// subsystem KILLS the mech (ctor: res+0x48
// "VitalSubsystem"==1; was misnamed
// videoObjectFlag) [T1 part_012.c:15776]
// task #6 CORRECTION: these two are the CONTROLS feed, not identity --
// the binary ctor chain stores its destination/message ctor pass-throughs
// here (FUN_004ac644 part_012.c:15772-15773), and ConfigMapGauge::Execute
// @004c6f1c reads BOTH for buttonGroup GetMapState. MechWeapon's ctor
// defaults the destination to &fireImpulse (@004b99a8 part_013.c:6886-9).
// The old names hostEntity/subsystemId2 (populated with owner/subsystem_ID)
// were a mislabel -- any reader would have classified every weapon as
// "mappedByOthers". (No port reader existed; verified before the rename.)
void *controlDestination; // @0xE8 this[0x3a] (ControlsButton* the direct maps write)
int controlMessageID; // @0xEC this[0x3b] (event-mapping message id; weapons 0)
friend void BTSubsystemControlFeed(void *, void **, int *, void **); // gauge bridge (task #6)
SubsystemResource *resource; // @0xF0 this[0x3c]
void *refCount; // @0xF4 this[0x3d]
ResourceDescription::ResourceID
alarmModel; // @0xF8 this[0x3e]
int printSimulationState; // @0x104 this[0x41]
Scalar criticalReference; // @0x108 this[0x42]
Scalar collisionCriticalHitWeight; // @0x10C this[0x43]
int configureActivePress; // @0x110 this[0x44] (default -1) -- the
// configure-session flag (was misnamed
// vitalSubsystemIndex; the reader is the
// authored audio configure-ticker, F15)
};
#endif