Follow-on tob70654d, 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 inb70654d) @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>
355 lines
16 KiB
C++
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
|