Files
arcattackandClaude Fable 5 092408041c Cockpit: the CONFIG-MODE weapon-regrouping session + the live-play fire fixes (tasks #6+#11)
TASK #6 -- the pod's in-cockpit weapon regrouping, fully reconstructed and
live-verified (hold-configure -> tap fire button -> toggle membership ->
release-commit):
- MechWeapon handlers id 9 ConfigureMappables @004b9550 / id 10 ChooseButton
  @004b95b8 (binary table @0x511860; the old "Myomers fns" mislabel swept)
  + the GetMessageHandlers() accessor chain through Emitter/ProjectileWeapon/
  MissileLauncher/GAUSS/PPC (empty per-class sets swallowed dispatch).
- Mapper vtable truth: +0x38 EnterConfiguration / +0x3C ExitConfiguration /
  +0x40,+0x44 AddOrErase evt/dir.  No "secondary vtable @0050f498", no
  "CreateTemporaryEventMappings" virtual (RP-name drift; swept incl CLASSMAP).
  L4 Enter/Exit rebuilt complete: StartMappableButtonsConfigure (the
  NonMapping 0x10000 <-> Mapping 0x8000 mode flip + the gauge's active-weapon
  latch) + the held-button re-arm + the 4 fire-button temp maps; only the RIO
  mapper implements the toggle (Thrustmaster no-ops = can't regroup).
- MechSubsystem +0xE8/+0xEC corrected to controlDestination/controlMessageID
  (ex hostEntity/subsystemId2 mislabel); MechWeapon ctor defaults the
  destination to &fireImpulse (@004b99a8).
- ConfigMapGauge state loop reconstructed (PE-recovered DAT_00518eb4 table;
  buttonGroup GetMapState sampler -- the "needs ModeManager" guard rationale
  was wrong).  Finding [T1]: the shipped binary NEVER enables this gauge (no
  SetColor caller) -- authentically dormant; BT_CONFIGMAP=1 is the dev enable.
- Dev harness: HOLD 'G' opens the session (BT_CONFIG_SLOT picks the weapon);
  BT_CONFIG_TEST scripts a headless verify.  @004afbc4 corrected to its real
  Fail-trap body (the guessed AddOrErase(NULL) body would corrupt groups).
- Bonus [T1]: the binary MechWeapon ATTRIBUTE table has ELEVEN entries
  (PercentDone..WeaponState) -- "TriggerState is the only one" was wrong.

TASK #11 -- the user-reported live-play regressions (both real bugs):
- PHANTOM FIRE: the mech4 bring-up shot block painted an explosion at the
  victim on its OWN 0.3s cadence whenever fire was held -- desynced once the
  authentic recharges landed.  Retired (the real impact visual flows from
  each discharge via the messmgr SubmitExplosion).
- WEAPON BRICKING (the "cuts out" bug): the Loading->Loaded snap window
  (+-0.01 around seekV) assumes the pod's LOCKED 60 fps; one port dt-spike
  jumps it, the byte-verified >1.0 clamp (_DAT_004ba830=0) zeroes readiness,
  and the weapon sticks in Loading at level ~10000 forever (observed live).
  Fixed with pod-frame (1/60s) sub-stepping of the binary's own Loading tick
  -- also fixes the I^2R integral over-heating generators on big steps.
  NEW GOTCHA CLASS recorded: reconstruction-gotchas #12 (frame-pacing trap).
- Duty-cycle measured (max-rate autofire): generators equilibrate in the
  degradation band (recharge stretch ~3.5-4.4x), the FailureHeat breaker
  never trips solo, PPCs sustain ~11.8-damage full-charge fire.  The slow
  kill pacing and recharge gaps are the AUTHENTIC heat economy; the pilot's
  counters (this regrouping UI, generator reassignment, coolant valves) are
  the reconstruction queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:39:35 -05:00

314 lines
14 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
// 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:
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 (bus-state query)
// 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);
// 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();
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 (ConfigureActivePress support).
static Logical
LookupStatusType(const char *name, int *out_value);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 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 vitalSubsystemIndex; // @0x110 this[0x44] (default -1)
};
#endif