# BattleTech recovered class map (authoritative)

Reconciled from the reconstruction agents (`heat`, `btplayer`, `mechweap`, `mechmppr`, `powersub`).
This is the single source of truth for the class hierarchy, vtable/ClassID/ctor addresses, and the
shared-base offset layout. Update as modules are recovered.

## Hierarchy
```
Subsystem  (MUNGA base — have source: RP/MUNGA/SUBSYSTM.HPP)
├─ MechSubsystem            vtable 0050e210  classID 0xBBB  ctors @4ac530(name+id)/@4ac644(resource)  dtor @4ac868  [mechsub.cpp]
│    *** DISTINCT BT BASE (NOT a MUNGA Subsystem alias).  MECHTECH.HPP `#include <mechsub.hpp>` + `MechSubsystem::TechStatusTypeCount`.
│    CORRECTION: heat.cpp folded this cluster (@4ac07c..@4ac9ec) into its "Subsystem/HeatableSubsystem base"; it is MechSubsystem.
│    CreateStreamedSubsystem @4ac9ec stamps classID 0xBBB / model 0xE4 and parses ONLY damage fields (PrintSimulationState,
│    CollisionCriticalHitWeight, VideoObjectName, VitalSubsystem) — NO thermal.  Couples subsystem→DamageZone@this+0xE0.
│    members: statusAlarm@0x2C (this+0xb), simulationState@0x40, damageZone*@0xE0, refCount@0xF4, alarmModel@0xF8,
│      printSimulationState@0x104, criticalReference@0x108, collisionCriticalHitWeight@0x10C, vitalSubsystemIndex@0x110(-1).
│    methods: GetStatusFlags@4ac144 (structureLevel tier), HandleMessage@4ac0bc, ResetToInitialState@4ac1d4, ClearStatus@4ac22c,
│      PrintState@4ac8c0 (DefaultState/Destroyed/Exploding), IsDamaged@4ac9c8 (mech bus down), ApplyDamageAndMeasure@4ac07c,
│      DistributeCriticalHit@4ac274 ("ammo explosion damaging"), LookupStatusType@4ac194 (name table @50de74).
│    TechStatusType states @50df17: Destroyed/Damaged/CoolantLeaking/Overheating/AmmoBurning/Jammed/BadPower (count=7).
│    Derivation name "MechSubsystem" @50def5/@50e2fd (distinct from "HeatableSubsystem" @50e700).
├─ HeatableSubsystem        ctor @4ac644   [heat.cpp]   members: currentTemperature@0x114, heatLoad@0x120
│    (REVISIT: string @50e700 confirms HeatableSubsystem is real but is a thin layer above MechSubsystem; the @4ac644 ctor
│     and vtable 0050e210 attributed here actually belong to MechSubsystem — temperature init is HeatSink's @4adda0.)
│  ├─ HeatWatcher           vtable 0050ed10  classID 0xBBF  ctor @4aeb40  [heatfamily_reslice]  watches another subsys temp → 3-lvl alarm
│  │  └─ PowerWatcher       vtable 0050f99c  classID 0xBD5  ctor @4b18a4  [powersub.cpp]
│  │     │   (shared base for the balance/aim subsystems below; CSS @4b198c stamps 0xBD5/size 0xF4 +
│  │     │    parses "MinVoltagePercent"@+0xF0; base ctor minVoltage@0x180, AlarmIndicator(5)@0x184;
│  │     │    per-frame watch @4b181c; slot9 @4b179c, slot10 reset @4b1804.)
│  │     ├─ HUD              vtable 00511510  classID 0xBD6  ctor @4b7f94  dtor @4b816c  CSS @4b81d4  [hud.cpp DONE]  size 0x2A4, model 0x104
│  │     │    cockpit heads-up display (torso-horizon gimbal + reticle/threat/status read-outs).  Confirms the
│  │     │    "Resolved conflict" note: @4b7f94 is the HUD, mislabeled "MechTech" by mech.cpp's 0xBD6 factory case.
│  │     │    DefaultData 00511078; resource +0xF4 FlickerRate, +0xF8 SegmentPageName(idx), +0xFC HorizontalMovementPerSecond, +0x100 HorizontalLimit.
│  │     │    members: flickerRate@0x1D8, torso/target read-outs@0x1DC-0x1EC, lockFlag@0x1F4, visible@0x1F8,
│  │     │      threatVector(Vec3)@0x1FC, compassHeading(Vec3)@0x208 (setter @4b7810), blink/slew accums@0x218-0x230,
│  │     │      linkedEntity@0x234(=Mech+0x438), statusAlarm(2)@0x238, horizontalTorsoOffset@0x294, HMPS@0x298, HorizontalLimit@0x29C, flickerActive@0x2A0.
│  │     │    overrides: HandleMessage@4b7780(s9, body not captured), ResetToInitialState@4b77bc(s10). HudSimulation@4b7830 (Performance, PTR@511170).
│  │     │    attr table @5110c0 (ids 4-0xd: RotationOfTorsoHorizontal/HorizontalTorsoLimit{Right,Left}/SpeedOfTorsoHorizontal/
│  │     │      RangeToTarget/Visible/Lock/HotBoxVector/ThreatVector/CompassHeading) — NB attr name<->offset bindings only
│  │     │      partially agree with ctor/sim usage (UNCERTAIN). TestInstance@4b81b8 (deriv @511088 "HUD"). UpdateFlicker@4b7ed4.
│  │     ├─ Searchlight      vtable 005114d4  classID 0xBD8  ctor @4b84dc  dtor @4b8568  CSS @4b85a8  [searchlight.cpp DONE]  model 0xF4
│  │     │    illumination subsystem (corroborated by btl4vid MakeMechRenderables "0xBD8 SearchLight").  DefaultData 00511198.
│  │     │    empty resource (CSS only stamps). members: lightState@0x1D8 ("LightState" attr id5),
│  │     │    commandedOn@0x1DC (res +0x28; toggled by "ToggleLamp" msg → ToggleLamp@4b860c), requestedOn@0x1E0, stateAlarm(2)@0x1E4.
│  │     │    overrides: ReadUpdate@4b83b8(s6), WriteUpdate@4b83f0(s7, net record tag 0x14); s9/s10 inherit PowerWatcher.
│  │     │    SearchlightSimulation@4b841c (Performance, PTR@511200; gates light by power@0x198 + heat@0x140). TestInstance@4b85f0 (deriv @5111a8).
│  │     ├─ ThermalSight     vtable 00511498  classID 0xBDE  ctor @4b8718  dtor @4b8794  CSS @4b87d4  [thermalsight.cpp DONE]  model 0xF4
│  │     │    cockpit thermal/IR vision mode. DefaultData 00511228. empty resource. members: thermalActive@0x1D8 ("LightState" attr id4),
│  │     │    requestedOn@0x1DC, stateAlarm(2)@0x1E0.  no s6/s7 override (inherits base); s9/s10 inherit PowerWatcher.
│  │     │    ThermalSightSimulation@4b8648 (Performance, PTR@511290): same power/heat gating as Searchlight, AND on state
│  │     │    change for the locally-viewed mech (PTR_DAT_004fd4b8+0x48) toggles the global "pvision" render flag via
│  │     │    FUN_0045fe44 (prints "pvision ON/OFF" @4f2f9d/@4f2fa8). TestInstance@4b881c (deriv @511238 "ThermalSight").
│  │     ├─ Gyroscope        vtable 00510abc  classID 0xBC4  ctor @4b3778  dtor @4b3e88  CSS @4b3eb4  [gyro.cpp DONE]
│  │     │     DefaultData 0050fdb0; resource size 0x21C. Balance/orientation/tip-over model.
│  │     │     Performance GyroscopeSimulation @4b275c (PTR @0050fe08). Members: exageration@0x1D8,
│  │     │       eyeOrientation@0x1DC, eyeAccel@0x230, eyeForce@0x23C, bodyOrientation@0x2B4,
│  │     │       rotationSpringConstant@0x2C0, rotationDampingConstant@0x2CC, bodyAccel@0x308, bodyForce@0x314,
│  │     │       maxAnimationNoise@0x39C, minAnimationNoise@0x3A0, rotationPerSecond@0x3A4, swayBias@0x3A8,
│  │     │       percentageOnNormal@0x3AC, OnDestruction@0x3B0, OnDegradation@0x3B4, OnFailure@0x3B8,
│  │     │       swayAngle@0x3BC, swayVelocity@0x3C0, swayActive@0x3C4, eyeJointNode@0x3C8, mechJointNode@0x3CC.
│  │     │     helpers: IntegrateEyeJoint@4b2ec0, IntegrateBody@4b30ec, WriteEyeJoint@4b33e0(@4b2eac thunk),
│  │     │       WriteMechJoint@4b34ec, UpdateAnimationNoise@4b357c; damage kicks @4b2d8c/@4b2de4/@4b2e50.
│  │     │     overrides: slot9 HandleDeath@4b2660(->4b179c), slot10 ResetToInitialState@4b2678.
│  │     │     CSS parses (all required, deg->rad on Rotation*Spring*): Percentage{Normal,Destruction,
│  │     │       Degradation,Failure}, RotationPerSecond, Exageration, M{ax,in}AnimationNoise, Spring/Damping
│  │     │       Constant{X,Y,Z}, {Pos,Neg}Spring{X,Y,Z}, Rotation{Spring,Damping}Constant{Pitch,Yaw,Roll},
│  │     │       Rotation{Pos,Neg}Spring{Pitch,Yaw,Roll}, {Collision,Ballistic,Explosive,Laser,Energy}Damage
│  │     │       Multiplier + …{Trans,PitchRoll,Yaw,Vibration}, EyeJoint, MechJoint (resolved in skeleton).
│  │     └─ Torso            vtable 0051103c  classID 0xBC5  ctor @4b6b0c  dtor @4b6fc0  CSS @4b6fec  [torso.cpp DONE]
│  │           DefaultData 00510af8; resource size 0x158. Torso twist(yaw)+elevation(pitch) aim model.
│  │           Performance TorsoSimulation @4b5cf0 (master, PTR @00510c10) / TorsoCopySimulation @4b65f8
│  │             (damaged copy, PTR @00510c1c). Members: currentTwist@0x1D8, horizontalLimitRight@0x1DC,
│  │             horizontalLimitLeft@0x1E0, currentElevation@0x1E4, twistVelocity@0x1E8, commands@0x1F8-0x208
│  │             (R/L/Up/Down/Center), statusFlags@0x20C, buttonAccelPerSec@0x210, buttonAccelStart@0x214,
│  │             targetTwist@0x218, twistAtUpdate@0x21C, verticalLimitTop@0x220, verticalLimitBottom@0x224,
│  │             twistRate@0x238, baseTwistRate@0x23C, baseElevRate@0x240, effTwistRate@0x244, effElevRate@0x248,
│  │             isDamagedCopy@0x24C, horizontalEnabled@0x250, lastUpdateTime@0x254, buttonRamp@0x26C,
│  │             recenterActive@0x274, horizontalJointNode@0x278, horizontalShadowJointNode@0x27C.
│  │           helpers: ComputeTargetTwist@4b6510, WriteJoints@4b66b4, UpdateJoints@4b67ec, Recenter@4b6918.
│  │           overrides: slot6 WriteUpdateRecord@4b6a78, slot9 HandleDeath@4b5be0(->4b179c),
│  │             slot10 ResetToInitialState@4b5bf8.  Angular resource values stored in radians (deg*PI/180).
│  │           CSS parses: TorsoHorizontalEnabled(bool,default True), ButtonAcceleration{PerSecond,StartValue},
│  │             {Horizontal,Vertical}RotationPerSecond, HorizontalLimit{Right,Left}, VerticalLimit{Top,Bottom},
│  │             TorsoHorizontalJoint + …ShadowJoint (resolved in skeleton, only when enabled).
│  └─ HeatSink              vtable 0050edc4   ctor @4adda0    [heat.cpp]
│     ├─ Condenser          vtable 0050ed88  classID 0xBBD  ctor @4ae568  [heatfamily_reslice]  refrigeration, 3-pos valve (NOT passive)
│     ├─ Reservoir          vtable 0050ecd4  classID 0xBC0  ctor @4af408  [heatfamily_reslice]  coolant source / InjectCoolant
│     ├─ HeatSinkBank?      vtable 0050ed50  classID 0xBBE  ctor @4ae8d0  [heatfamily_reslice]  aggregate (name unconfirmed)
│     ├─ Generator          vtable 0050fd74   classID 0xBC1  ctor @4b225c [powersub.cpp = gnrator.cpp]
│     │    RECONCILED: surviving GNRATOR.TCP describes THIS Generator (TestClass matches powersub.cpp; ResetToInitialState
│     │    chains HeatableSubsystem::ResetToInitialState + outputVoltage=0).  Original file was gnrator.cpp; binary linker-tagged
│     │    the cluster as powersub.cpp so reconstruction lives there.  [gnrator.cpp] = thin alias note (no new class).
│     │    GNRATOR.TCP reveals one method powersub.cpp omitted: Generator::ResetToInitialState() (slot 10) — fold into powersub.cpp.
│     └─ PoweredSubsystem   vtable 0050f9d8   classID 0xBC2  ctor @4b0f74 [powersub.cpp]
│        ├─ Sensor          vtable 0050fb0c   classID 0xBC3  ctor @4b1d18 [sensor.cpp]  size 0x328 (CORRECTED — was mislabeled "Myomers")
│        ├─ Myomers         vtable 005117dc classID 0xBC6  ctor @4b8fec [myomers.cpp]  size 0x358, resource 0x1B0  (the REAL Myomers — see correction below)
│        │    dtor @4b9114 · CSS @4b9140 (stamps 0xBC6/size 0x1B0) · DefaultData @0051154c.  Mech factory case 0xBC6 (alloc 0x358).
│        │    Artificial-muscle drive: a discrete SeekVoltage[] gear table → "SpeedEffect" drive fed to the JointedMover; throttled by heat + damage.
│        │    members: speedEffect@0x31C(1.0,attr0x12), currentSeekVoltageIndex@0x320(0x13), recommendedSeekVoltageIndex@0x324(0x14),
│        │      minSeekVoltageIndex@0x328(0x15), maxSeekVoltageIndex@0x32C(0x16), seekVoltage[5]@0x330(0x17), outputVoltage@0x344(0x18),
│        │      heatRange@0x348(=failTemp-degradeTemp), heatRangeSquared@0x34C, velocityEfficiency@0x350(res+0x190), accelerationEfficiency@0x354(res+0x194).
│        │    resource (size 0x1B0) adds +0x190 VelocityEfficiency, +0x194 AccelerationEfficiency, +0x198 SeekVoltage[5](fractions×ratedVoltage),
│        │      +0x1AC SeekVoltageRecommendedIndex (parser @4b9140).  attr table @00511588; string pool @00511660 ("Myomers"@0051166d).
│        │    vtable overrides vs PoweredSubsystem: slot0 dtor@4b9114, slot9 HandleMessage@4b8a8c(→4b0efc), slot10 ResetToInitialState@4b8aa4(→4b0e6c),
│        │      slot15 GetSpeedReading@4b8f94 (base 4b1780; km/h÷350 gauge readout, best-effort), slot16 HasAdequateVoltage@4b8f3c (overrides IsSourceShorted base 4b0b5c).
│        │    helpers: AvailableOutput@4b8ac0 (drive = MechBaseSpeed × V/recommendedSeekV × heatFactor × (1-damage)),
│        │      RegisterMaxOutput@4b8ef0 (→ Mech maxSpeed@0x7A0; loop @part_012:10322 over Mech myomer list@0x1EB),
│        │      MyomersSimulation@4b8d18 (Performance, per-tick work→heat accum@0x1C8, gated by FUN_004ad7d4; best-effort registration),
│        │      ConnectToMover@4b9550 / DisconnectFromMover@4b95b8 (route SpeedEffect@0x31C into Mech mover @**(Mech+0x128)),
│        │      ToggleSeekVoltage@4b8a48 (mappable cmd id 0x09; in binary, not in shards, best-effort).
│        │    EXCLUDED neighbors: gimbal/torso-horizon display @4b7f94..4b881c (base 4b18a4, DAT_00511228); MechWeapon trap @4b95ec.
│        └─ MechWeapon      vtable 00511d2c   classID 0xBCD  ctor @4b99a8 [mechweap.cpp]  resource 0x1BC
│           ├─ Emitter          vtable 00512078  classID 0xBC8  ctor @4bb120  [emitter.cpp]  beam/charge weapon base, resource 0x1DC
│           │  ├─ PPC            vtable 0051214c  classID 0xBD4  ctor @4bb888  (PPC.CPP survived; FireWeapon @4bb878 tail-calls Emitter::FireWeapon @4bace8)
│           │  └─ GaussRifle     vtable 00512934                ctor ~@4bd0xx (GAUSS.CPP survived; FireWeapon override sets outputVoltage=0)
│           └─ ProjectileWeapon  vtable 0051242c  classID 0xBCD*  ctor @4bc3fc  dtor @4bc688  CSS @4bc7cc  [projweap.cpp DONE]  resource 0x1D0, object 0x448
│              │   *abstract: CSS re-stamps MechWeapon's 0xBCD (no own registered ClassID); concrete MissileLauncher::CSS overwrites 0xBD0.
│              │   resource adds +0x1BC TracerInterval(int), +0x1C0 AmmoBin(idx,+2bias), +0x1C4 MinTimeOfFlight, +0x1C8 MinVoltagePercentToFire, +0x1CC MinJamChance.
│              │   members: TotalTimeToEject@0x3F0(3.0), TimeToEject@0x3F4, PercentOfEject@0x3F8, minJamChance@0x3FC, ejectState@0x400(-1),
│              │     minVoltagePercentToFire@0x404, tracerCounter@0x408, minTimeOfFlight@0x40C, launchVelocity(Vec3)@0x410, tracerModel@0x41C,
│              │     leadPosition(Pt3)@0x420, tracerOrigin(Vec3)@0x42C, tracerInterval@0x438, ammoBinLink(Connection,12B)@0x43C.
│              │   overrides: HandleMessage@4bcabc(s8,msg2→Jammed), ReadUpdate@4bbae0(s9 thin), Reset@4bbaf8(s10), GetStatusFlags@4bbf88(s12),
│              │     PrintState@4bc6c8(s13), slot16@4bbc20, FireWeapon@4bc104(s18). Performance(sim)@4bbd04. weaponAlarm states 5=Jammed,6=TriggerDuringJam,7=NoAmmo.
│              │     recovered helpers: UpdateEject@4bbb50, DrawFiringCharge@4bbc78, CheckForJam@4bbfcc, ComputeTimeOfFlight@4bc06c. NOT recovered(prologue only): 4bbc20/4bbd04/4bbf88/4bc104.
│              │   MechWeapon vtable = 19 slots (0–18); slots 14–18 are MechWeapon virtuals (16=4b0b5c,17=4b9c9c,18=FireWeapon pure-trap @4ba45c).
│              └─ MissileLauncher vtable 00512570  classID 0xBD0  ctor @4bcff0  dtor @4bd060  CSS @4bd08c  FireWeapon @4bcc60(s18)  [mislanch.cpp]
│                   (MISLANCH.HPP survived) resource 0x1E0: +0x1D0 MissileCount(int), +0x1D4 MuzzleVelocity(Vec3→overwrites launchVelocity@0x410). missileCount@0x448.
│  AmmoBin (HeatWatcher-derived)  vtable 0051286c  classID 0xBCB  ctor @4bd5c4  dtor @4bd6b0  CSS @4bd6f0  [ammobin.cpp DONE]  referenced by ProjectileWeapon@0x43C.
│     resource 0x104: +0xF0 AmmoClassID("ProjectileClassID"→0xBD1/"MissileClassID"→0xBBA), +0xF4 AmmoModelFile, +0xF8 ExplosionModelFile, +0xFC AmmoCount, +0x100 FeedRate.
│     members: ammoCount@0x180, feedTimer@0x184, ammoClassID@0x188, cookOffArmed@0x18C, cookOffTime@0x190,
│       ammoAlarm(6-lvl)@0x194(level@0x1A8), ammoModelFile@0x1E8, explosionModelFile@0x1EC, cookOffHeatSource@0x1F0(heatPerRound@0x1F4), initialAmmoCount@0x220, feedRate@0x224.
│     ammoAlarm levels 0=Feeding,1=Loaded,2=Empty,5=Dumped(transient); watch heatAlarm level @0x140(word0x50)==2(FAILURE)→arm cook-off.
│     methods: AmmoBinSimulation(Performance,ptr@00512654)@4bd394, FeedAmmo@4bd4f4(→1 if dispensed), DumpAmmo@4bd588, CookOff@4bd300,
│       HandleMessage(s8,msg1=arm cook-off)@4bdb94, ReadUpdate(s9,chains @4aea84,dump-on-destroy)@4bd2c0, ResetToInitialState(s10 empty per AMMOBIN.TCP), TestInstance@4bd1e0.
├─ MechTech                 vtable 0050e3a0   classID 0xBDC  ctor @4ad228  [mechtech.cpp]  size 0x104
│    subsystem-monitor / cockpit-diagnostics "technician"; matches surviving MECHTECH.HPP.
│    members: subsystemMonitors ChainOf@0xE4, wooHooMinimumDuration@0xF4, wooHooDurationRange@0xF8,
│    wooHooChance@0xFC, alarmModel@0x100.  Performance = TechnicalAssistance @4ad33c (ptr @0050e2b0).
│    dtor @4ad1b8 · TestInstance @4ad470 (deriv @0050e280) · CreateStreamedSubsystem @4ad48c · DefaultData @0050e270.
│    nested SubsystemMonitor : Plug — vtable 0050e3d0, ctor @4ad124, dtor @4ad180, size 0x80,
│      statusArray[7]@0xC (StatusInfo=0x10: currentStatus,wooHooed,startTime,wooHooEnd), monitoredSubsystem@0x7C;
│      static MemoryBlock AllocatedMemory @0050e248.  (resource size 0x40: alarmModel@0x30, wooHoo*@0x34/0x38/0x3C)
├─ MechControlsMapper       vtable 0050f45c   ctor @4b02f0   [mechmppr.cpp]   (NOT "PowerController" — label settled)
│    └─ L4MechControlsMapper   vtable 0051e440  ctor @4d17ac  dtor @4d1814  [btl4mppr.cpp]  (RP analog L4VTVControlsMapper/RPL4MPPR.cpp)
│         L4-app platform layer over the in-Mech mapper. members: targetRangeExponentDemand@0x1a0(2.0),
│         targetRangeExponent@0x1a4(2.0), previousMissionReviewMode@0x1a8; reuses input-staging slots
│         throttleForward@0x1ac/throttleReverse@0x1b0/leftPedal@0x1b4/rightPedal@0x1b8 (cleared by derived ctors).
│         InterpretControls@4d196c (throttle detent snap 1.0±0.05, BuildPilotArray, review-mode bit 0x200000,
│         target-range ramp range=250*2^exp, exp clamp[0,5]; then chains MechControlsMapper::InterpretControls @4afd10).
│         ZoomTargetRangeIn@4d1b64 / ZoomTargetRangeOut@4d1b9c (±1.0). NotifyOfDisplayModeChange-override@4d1ae4 (vtbl+0x4C; ex-"SetControlMode", (mode bits18-20,
│         table @0051dbe4={0x40000,0x80000,0x100000}). SetPresetMode@4d1b24 (group*5+item -> {clear,set} mask table @0051dbf0[15]).
│         EnterConfiguration@4d1840 (+0x38) / ExitConfiguration@4d18dc (+0x3C) (task #6 relabel (buttons 0x40..0x47, hat-skip 0x41->0x45,
│         mode 0x8000=ConfigReady). AddOrErase evt/dir RET no-ops @4d195c/@4d1964 (+0x40/+0x44; TM can't regroup). KeypressMessageHandler@4d1bf0
│         (shared dispatcher: zoom/preset/pilot-cycle/look keys + fake-button dev keys; default -> @4d141c).
│         file-scope helpers: SendFakeButtonEvent@4d13f0, SendFakeButtonEventPair@4d133c, DispatchDeveloperFakeEvent@4d141c (GeneratorA-D/Condenser1/LRM15/AmmoBinLRM15/Avionics/Hud test keys).
│         ├─ MechThrustmasterMapper  vtable 0051e3f0  ctor @4d21d0  dtor @4d22b4  TestInstance @4d22e0  ClassDerivations @0051dc78
│         │     InterpretControls@4d2150 (throttleForward/Reverse -> throttlePosition@0x11c/reverseThrust@0x124; pedals -> pedalsPosition@0x120).
│         │     ctor registers scalarGroup[+0xa00/+0x9e0] + keyboard[+0x1a0] msg 0x17(Keypress).  btl4app passes DefaultData &DAT_0051dc68.
│         └─ MechRIOMapper           vtable 0051e3a0  ctor @4d266c  dtor @4d27cc  TestInstance @4d27f8  ClassDerivations @0051dd00
│               InterpretControls@4d25c0 (pedals -> pedalsPosition@0x120). msg table @0051dd30: Aux{1,2,3}{Quad,Eng1-4}(ids 3..0x11)->SetPresetMode @4d22fc-4d24bc,
│               ZoomIn/Out(0x12/0x13)->@4d24dc/@4d24f8, Hotbox->@4d2574 (pilot select, hotbox btn table @0051dcd0={0x37..0x30}).
│               AddOrErase(Receiver*)@4d25e8 / AddOrErase(ControlsButton*)@4d262c (mode 0x10000).  btl4app passes DefaultData &DAT_0051dcf0.
│         NOTE: btl4app.cpp::MakeViewpointEntity builds these two passing MechControlsMapperClassID 0x7dc; both chain L4MechControlsMapper@4d17ac -> MechControlsMapper@4b02f0.
├─ SubsystemMessageManager  vtable 0050b954   classID 0xBD3  ctor @49bca4  [messmgr.cpp]  (MESSMGR.HPP survived)
│    per-mech message hub: subsystems post damage via AddDamageMessage @49b6d8; the Performance
│    ConsolidateAndSendDamage @49b784 (ptr @0050b6fc) folds them into commonDamageInformation, rebroadcasts
│    a ClassID-0x13 notify msg (msg vtable @0050b984, size 0x34), queues terrain/weapon explosions, then
│    SendQueuedExplosions @49baa0 flushes ClassID-0x5C explosion cmds to the graphics pipe (DAT_004efc94+0x60).
│    dtor @49bd7c (slot0) · CreateStreamedSubsystem @49be10 (factory "SubsystemMessageManagerClassID"→@49be10;
│      stamps 0xBD3/size 0x38; reads "TerrainHitExplosion"→res+0x30, "RendererCompensateTime"→res+0x34).
│    vtable: only slot0(~dtor) and slot10(@49c3de empty = ResetToInitialState) overridden; rest inherit Subsystem.
│    ClassDerivations static @0050b6cc.  Located @49bxxx (adjacent to BTMission@49b54c), NOT the 0x4b27xx region.
│    members: messageHelper*@0xE0 (0x160-byte engine obj, ctor FUN_0041de1c; not in HPP),
│      commonDamageInformation{entityHit@0xE4, damageZoneIndex@0xE8(init -1), impactPoint(Pt3)@0xEC},
│      rendererCompensateTime@0xF8, weaponExplosions(VChainOf,hdr0x18)@0xFC (link vtable 0050b938),
│      damageInformation(VChainOf<DamageInformation*,Scalar>,hdr0x18)@0x114 (link vtable 0050b91c),
│      terrainHitExplosionID@0x12C.  object >= 0x130.
│    nested DamageInformation : Plug — vtable 0050b9a8, dtor @49c3e3, size 0x14: damageType@0x0C, subsystemID@0x10.
│      (AddDamageMessage fills damageType=msg+0x2C, subsystemID=msg+0x5C, key=msg+0x30, impactPoint=msg+0x4C.)
└─ VoltageSourceConnection : SharedData       ctor @4b1bb1   [powersub.cpp]

JointedMover (MUNGA) → Mech  vtable 0050cfa8  MechClassID 0xBB9  ctor @4a1674  size 0x854  [mech.cpp + mech2.cpp(+mech3/4 TODO)]
  hosts the subsystem roster above (ctor switch over ClassIDs 0xBBD–0xBDE); RP analog = VTV
  damageZones[] roster @mech[0x120], count @mech[0x47]; built by CreateDamageZoneStream @4a474c (mech.cpp)
DamageZone (engine, DAMAGE.HPP) → Mech::DamageZone  vtable 0050bb90 (7 slots)  ctor @49ce50  size 0x1b8  [mechdmg.cpp]
  matches surviving MECHDMG.HPP.  Overrides slot0 ~dtor@49d23c, slot3 SetGraphicState@49c66c, slot6 TakeDamage@49c690
  (slots 1,2,4,5 = 4173b8/417938/41ddd8/41dd98 inherit DamageZone; base ctor @41df5c vtable 04e3cf0).
  members: redirectTable(DamageZoneIndexTable=TableOf<PlugOf<int>*,int>)@0x160, lastHitZone@0x178,
    lastDamageTime@0x17c, lastInflicting(EntityID)@0x180, parentArtifactZone(SlotOf)@0x188, segmentIndex@0x194,
    vitalDamageZone@0x198, descendOnDestruction@0x19c, destroySiblingsOnDestruction@0x1a0, leftLeg@0x1a4,
    rightLeg@0x1a8, criticalSubsystemCount@0x1ac, criticalWeightSum@0x1b0, criticalSubsystems**@0x1b4.
    (base DamageZone: owner Mech@0xc, graphic alarms@0x10/@0x64, dirty-flag@0xb8, 5 armour scalars@0x144..0x154, structureLevel@0x158)
  methods: SetLODParentPointers@49d1d0, LODDamageRouter@49c40c, RandomRedirect@49c600, UpdateLODDamageLevel@49c51c,
    SendSubsystemDamage@49c9a8, RecurseSegmentTable@49cad4, CriticalHit@49ccc4, ShortAttachedGenerators(inlined in TakeDamage),
    CreateStreamedDamageZone@49d304 (per-zone .dmg parser), GetSegmentIndex@49db20 (skl "dzone" walk).
  └─ MechCriticalSubsystem (SIGNATURED)  vtable 0050bb8c (1 slot=dtor)  ctor @49dd18  dtor @49dd44  TestInstance @49dd74  size 0x1c
       members: subsystemPlug(SlotOf<Subsystem*>)@0x04 (ctor@49dd7e/dtor@49dd9d vtable 0050bb84),
       damagePercentage@0x10, damagePercentageUsed@0x14, criticalWeight@0x18.
       parentArtifactZone slot vtable 0050bb7c (ctor@49ddc9/dtor@49dde8).  Generator-short uses Generator::ClassDerivations@50f4bc + ForceShortRecovery@4b11bc.
  NOTE: DMGTABLE (DamageZone lookup table: "PieSlice"/"Layer"/"RotateWithTorso", strings @50bc30..50bd00) is the
    ADJACENT but SEPARATE class cluster @49de14..~49f5fb.  NOW RECONSTRUCTED in [dmgtable.cpp].
    Three-level hit-location resolver, all : Plug:
      DamageLookupTable      vtable 0050bd84  ctor @49ea48  dtor @49eadc  size 0x30 — stack of armour LAYERS;
        ResolveHit @49eb54 (penetration depth → layer), BuildFromNotation @49ed28 (reads "Layer1".."LayerN").
      PieSlice               vtable 0050bd90  ctor @49e740                size 0x30 — angular sector wheel;
        SelectSlice @49e678 (torso-relative angle wrapped at 2π; orientationSource=owner+0x438, torso heading @+0x1d8),
        BuildFromNotation @49e88c (reads "RotateWithTorso" + "PieSlice" pages).
      DamageZonePercentTable vtable 0050bd9c  ctor @49deb0  dtor @49df80  size 0x2c — leaf weighted %-table;
        SelectZone @49de14 (uniform roll over cumulative %; entry+0xc=zoneIdx, entry+0x14=cumPct, percentages must sum to 1.0),
        BuildFromNotation @49e00c (reads "DamageZone" "%s %f" zone/pct lines), stream I/O @49e524/@49e5e4.
      entry comparator @49f301 (sorts by +0x14 within ~1e-4); template TableOf/PlugOf slots @49f204..@49f5b8
      (vtables 0050bd30/0050bd50/0050bd64 — engine instantiations).  _DAT_0049e72c = _DAT_0049e810 = 0x40c90fdb = 2π.
    Streamed from the .tbl "DamageLookupTable" by CreateDamageZoneStream (mech.cpp; @49ed28 invoked from part_012.c:11580,
    @49ea48 stream form from part_012.c:10423).  The dz_* name list @0x51bb70 is SectorDisplay UI data, NOT used by GetSegmentIndex.
Mission (MUNGA) → BTMission  ctor @49b54c  dtor @49b648  [btmssn.cpp, source survived BTMSSN.HPP/CPP]
  members: experienceLevel@0xe4, roleName@0xe8, teamName@0xec, advancedDamageOn@0xf0, missionTemperature@0xf4 (+273.15 C→K), patchName@0xf8;
  base Mission slots: colorName@0x20, badgeName@0x24, dropZoneName@0x28, gameModel@0x2c, dropZoneHost@0x3c, playerBitmapIndex@0x4c, scenarioRoles dict@0x50; BTMission::SetPlayerData @49b6bc = Fail stub
  └─ BTL4Mission  vtable 0051e870 = {dtor@4d2c04, SetPlayerData@4d2c30}  ctor @4d2be0  [btl4mssn.cpp]  (matches surviving BTL4MSSN.HPP; RP analog RPL4Mission)
       overrides SetPlayerData @4d2c30 (the only func the linker tagged to btl4mssn.cpp); TestInstance inlined out of OPT vtable
ScenarioRole : Node (SCNROLE.HPP survived)  ctor(role,model) @429a9c  members: damageInflictedModifier@0x0c, damageReceivedModifier@0x10, friendlyFirePenalty@0x14, damageBias@0x18, killBonus@0x1c, specialCaseDeathPenalty@0x20, roleName@0x24, returnFromDeath@0x28
L4Application → BTL4Application  vtable (OPT subclass 0x3eb)  ctor @4d34c4  [btl4app.cpp]  (matches surviving BTL4APP.HPP)
  └─ BTL4PlaybackApplication  vtable 0051f1a4  ctor @4d3be4  dtor @4d3ea0  TestInstance @4d40d8  [btl4pb.cpp]  (matches surviving BTL4PB.HPP; RP analog RPL4PlaybackApplication/RPL4PB.cpp)
       LBE mission-review/playback app. ClassDerivations @0051ee2c, DefaultData @0051ee5c, ClassID 0x3eb (L4ApplicationClassID).
       member timeBias@0xd4 (set in RunMission); spoolFile inherited @0x18. ctor(resource, spool_file) chains BTL4Application@4d34c4.
       overrides: MakeNetworkManager@4d3c14 (new L4PlaybackNetworkManager), Initialize@4d3c34, LoadBackgroundTasks@4d3c44
       (BTSpoolerTask + 4 stock tasks), ExecuteForeground@4d3d40 (review frame loop; controls only if DAT_004fd550==2),
       DispatchPacket@4d3ecc (route packet to NetworkClient), RunMissionMessageHandler@4d3ef8 (timeBias=Now-spool packet stamp),
       StopMissionMessageHandler@4d3f44 (winners-circle: park Mechs on rank dropzones "win1".."winN", SortAndReloadNameBitmaps, SetViewAngle 45deg).
       message table {RunMission@4d3ef8, StopMission@4d3f44}. winners_spot "win?" @0051ee63.
  BTSpoolerTask : SpoolerTask  vtable 0051f204  ctor @4d3b74  dtor @4d3bb8  [btl4pb.cpp]  (RP analog RPSpoolerTask)
       background task pumping recorded packets. DispatchPacket@4d3b90 -> playbackApp->DispatchPacket; GetTimeBias@4d3ba8 -> playbackApp->GetTimeBias.
       base SpoolerTask = munga_l4/L4SPLR.CPP (base stubs "shouldn't be here!" @0x4fda60).
GraphicGauge → MapDisplay   vtable 0051422c  ctor @4c1c24  [btl4rdr.cpp]  (matches surviving BTL4RDR.HPP)  radar/map
Gauge / GraphicGauge → cockpit gauge-widget library  [btl4gaug.cpp]  (NO header survived; reconstructed)
  RP analog = the MUNGA shared lib MUNGA_L4/L4GAUGE.cpp (ColorState/TwoState/BarGraph*/WipeGauge*/SegmentArc*);
  BattleTech forked that widget set into its own L4 module btl4gaug/gau2/gau3.obj instead of the shared lib.
  TU extent @4c2f94..@4c6394 (anchor @4c3f6c); btl4gau2.cpp begins @4c6798.
  Gauge-derived (palette tints, no GraphicsView; base ctor 00444308):
    ColorMapper            vtable 00518e10  ctor @4c37dc   palette-push Execute @4c3980
    ├─ ColorMapperArmor        vtable 00518dcc  ctor @4c3b98  Make @4c3aa4
    ├─ ColorMapperMultiArmor   vtable 00518d88  ctor @4c3d60  Make @4c3c48
    ├─ ColorMapperCritical     vtable 00518d44  ctor @4c3e40  Make @4c3ddc
    └─ ColorMapperHeat         vtable 00518d00  ctor @4c3f6c  Make @4c3f08  *** ANCHOR (assert BTL4GAUG.CPP:0x68a) ***
  GraphicGauge-derived (base ctor 00444818, GraphicsView@0x48):
    HorizTwoPartBar        vtable 00518cbc  ctor @4c4170  Make @4c407c  Execute @4c4340
    VertTwoPartBar         vtable 00518c78  ctor @4c4724  Make @4c462c  Execute @4c48fc
    VertNormalSlider       vtable 00518c34  ctor @4c4b84  Execute @4c4cc0
    OneOfSeveral           vtable 00518bf0  ctor @4c4d88  Execute @4c4f28   (base)
    ├─ OneOfSeveralInt         vtable 00518bac  ctor @4c5148  Make @4c5068
    ├─ OneOfSeveralPixInt      vtable 00518b68  ctor @4c52d8  Make @4c5204
    └─ OneOfSeveralStates      vtable 00518b24  ctor @4c5470  Make @4c5390
    HeadingPointer         vtable 00518ae0  ctor @4c562c  Make @4c554c  Execute @4c5914
    BitMapInverseWipe(?)   vtable 00518a9c  ctor @4c5b7c  Execute @4c5d08   (name best-effort; base of gau2 LeakGauge)
    BitMapInverseWipe(?)   vtable 00518a58  ctor @4c5e84  Execute @4c5fb8
    └─ ...Scalar(?)            vtable 00518a14  ctor @4c61c8
  ArcGauge-derived (MUNGA L4 arc bases 004745e0 / 00473f44):
    SegmentArc270          vtable 005189d0  ctor @4c6244
    SegmentArcRatio        vtable 0051898c  ctor @4c6394  Make @4c62fc
  File-private GaugeConnection feeds (base ctor 00444124; source@0x10 dest@0x14, Transfer writes *dest):
    Heat @4c3664 Transfer @4c3720 (reads HeatableSubsystem currentTemperature@0x114) ·
    Critical @4c3598/@4c3610 · Armor @4c33a4/@4c3430 · MultiArmor @4c346c/@4c34f4 ·
    State @4c3324/@4c3390 · TorsoHeading @4c3134/@4c31a0 · NameId @4c31ec/@4c3258 · AltId @4c3288/@4c32f4
    (NB: 4c3134/4c31ec/4c3288 are NOT file-private to btl4gaug — btl4gau2 reuses them; treat as shared gauge-lib connections.)
GraphicGauge / Gauge → cockpit COMPOSITE panel gauges  [btl4gau2.cpp]  (NO header survived; reconstructed)
  TU extent @4c6798..@4c9b50 (btl4gau3 begins @4c9bd0).  Name pool @0x519760; instance strings @0x519a..0x519d.
  These AGGREGATE btl4gaug primitives into one subsystem instrument (engineering / weapon-status HUD).
  GraphicGauge-derived (base ctor 00444818):
    SeekVoltageGraph      vtable 0051a1fc  ctor @4c6798  Execute @4c6934  (energy-weapon seek-voltage curve;
       reads CurrentSeekVoltageIndex/Min/Max/SeekVoltage; scale 230x187; helpers 4c6be4/4c6c30/4c6c6c)
    ConfigMapGauge        vtable 0051a1b8  ctor @4c6d80  Execute @4c6f1c  (joystick/config-mgr lamp; btjoy.pcc +
       cm_off/other/only/both.pcc via table DAT_00518eb8; string "ConfigMap")
    AnimatedSubsystemLamp vtable 0051a174  ctor @4c70a4  : OneOfSeveral + conn @4c3134  (best-effort; "CoolingLoop")
    AnimatedSourceLamp    vtable 0051a130  ctor @4c7160  : OneOfSeveral + conn @4c31ec  (best-effort; "PowerSource")
    ScalarBarGauge        vtable 0051a0e4  ctor @4c721c/@4c72ac : BarGraph(00472ef0) + conn @4c3288 / scalar 00474855
       (best-effort; "GeneratorVoltage", range 0..12000)
    GeneratorCluster      vtable 0051a0a0  Make @4c7368 ctor @4c746c  (5 children: VertTwoPartBar+ScalarBar+Leak+2 TwoState)
    SubsystemCluster      vtable 0051a020  ctor @4c8140  Execute @4c88e4  (base engineering panel; ~10 children)
    ├─ HeatSinkCluster        vtable 00519fd4  ctor @4c8a6c  Execute @4c8db0  (+4 fail TwoStates btehfail/btepfail/btesfail + numeric)
    └─ WeaponCluster          vtable 00519f38  ctor @4c8fc4  Execute @4c9290  (+SegmentArc270 recharge + ConfigMap; "WeaponCluster missing")
       ├─ EnergyWeaponCluster    vtable 00519ee8  ctor @4c93b0  (+SeekVoltageGraph "EngrGraph" + seek OneOfSeveralInt bteseek)
       └─ BallisticWeaponCluster vtable 00519e98  ctor @4c9558  Execute @4c9a38  (+2 ammo NumericInt + jam/fire TwoState +
            eject BitMapInverseWipeScalar bteejtm; tracks weaponAlarm==5 Jammed, ammo-bin @0x43C)
  Gauge-group-derived (non-graphic base ctor 004440d4, child list @0x24):
    PrepEngrScreen        vtable 0051a06c  Make @4c7b30 ctor @4c7bf0  Execute @4c7e48  (per-weapon engineering screen;
       walks mech crit-subsystem roster @0x124/@0x128, dispatch on ClassID 0xBC3/0xBC6/0xBC8/0xBD4/0xBCD/0xBD0/0xBCE)
GraphicGauge → multiplayer / tactical HUD displays  [btl4gau3.cpp]  (NO header survived; reconstructed)
  TU extent @4c9bd0..@4cbea0 (btl4grnd begins @4cbea0).  Instance strings @0x51bb34..0x51bb61.  Read the shared
  "Players" node (@00403ad0) and the live mech roster rather than a single subsystem.
    PlayerStatusMappingGroup  (no GraphicGauge base) build @4c9bd0 destroy @4c9ca8  — 28 ColorMapperArmor (dz_door table
       PTR_s_dz_door_0051a240) zone-tint group; adpal.pcc/adpal2.pcc; "PlayerStatusMappingGroup"
    SectorDisplay         vtable 0051beec  Make @4c9d44 ctor @4c9e10  Execute @4ca07c  (14-col bitmap grid + 2 numerics)
    TeamStatusDisplay     vtable 0051bea8  ctor @4ca208  Execute @4ca478  (8-row scoreboard; reads "Players")
    PilotList             vtable 0051be64  Make @4ca90c ctor @4ca958  Execute @4cabd0  ("PilotList"; 8 entries x3 numerics;
       lance lookup FUN_004b0898; helpers DrawMechIcon @4cad70 / DrawAmmoCount @4cae20)
    PlayerStatus          vtable 0051be20  Make @4cae90 ctor @4cb1a8  Execute @4cb358  ("PlayerStatus"; player# 1..8;
       CreateMutantPixelmap8 @4cafac recolours mech outline per player + PlayerStatusMappingGroup)
    MessageBoard          vtable 0051bddc  Make @4cb678 ctor @4cb704  Execute @4cb82c  ("MessageBoard"; rank/message strip cells)
GaugeAlarmManager → BTL4GaugeAlarmManager  [btl4galm.cpp]  (matches surviving BTL4GALM.HPP)
  *** NO distinct override body survived in BTL4OPT.EXE *** — only the MUNGA base GaugeAlarmManager is present
  (gaugalrm.cpp @00448928..@00448f48 in part_006.c): base vtable 0x4eed18 (Create=slot23 @00448ab8 trap, Read=slot24
  @00448ad4 trap, dtor slot0 @00448f1a); base drivers CreateGaugeAlarmStream @00448af0 (reads "gaugeAlarm" resource,
  prints "Model file '...' has undefined alarm type '...' - resource not built") and BuildAlarmsForModel @00448d00
  (makes GaugeAlarm @004489e8 size 0x28 vtable 0x4eed7c, calls the Read virtual per record).  No subclass vtable in
  vtables.tsv; no caller of base ctors @00448ebd/@00448edc/@00448efb; the linker's btl4galm.obj slot @4cc40c..@4cdac0
  holds an unrelated 3-D HUD-model builder (PNAME1..8.bgf).  btl4galm.cpp reconstructed from BTL4GALM.HPP signatures +
  the base calling contract (BEST-EFFORT / low confidence).
L4GaugeRenderer → BTL4GaugeRenderer  vtable 0051cebc  ctor @4cbea0  [btl4grnd.cpp]  (matches surviving BTL4GRND.HPP)
  HUD/cockpit gauge renderer manager (NOT a 3D ground renderer despite the file name).
  dtor @4cbf40 (slot0) · TestInstance @4cbf90 · NotifyOfNewInterestingEntity @4cbfa0 (slot9) ·
  NotifyOfBecomingUninterestingEntity @4cc028 (slot10).  RP analog = RPL4GaugeRenderer (RPL4GRND.cpp).
  ctor builds lampManager(this[0x7033]) + L4Warehouse(this[0x13]@0x4c) and calls
  BuildConfigurationFile("gauge\\l4gauge.cfg", BTL4MethodDescription@0051c910).
  Notify* route entities w/ a GaugeImageStream resource (type 0x12) into inherited
  movingEntities(@0x94, Movers) / staticEntities(@0xb0, Terrain).
  File-local BT lamp-manager subclass (vtable 0051ceb0: 4cc0e1/4cc294/4cc2fc) maps lamp names
  ("coolingLoop1"...) -> indices (helpers @4cc108/4cc148/4cc1a0); best-effort, not in header.
VideoRenderer (MUNGA L4 renderer mgr) → BTL4VideoRenderer   [btl4vid.cpp]  (NO header survived; reconstructed)
  The out-the-window 3D WORLD renderer (despite "btl4grnd"=gauges, THIS is the ground/world view).
  Class name confirmed by embedded string "BTL4VideoRenderer::Material name ..." @0051d6f8 and assert
  path d:\tesla\bt\bt_l4\BTL4VID.CPP.  RP analog = RPL4VideoRenderer (RPL4VID.cpp).  TU extent
  @4cc40c..@4d2bbc (btl4mssn.cpp begins @4d2be0).  Base label uncertain (MUNGA l4video.hpp names the
  concrete mgr "DPLRenderer" in the RP tree; this pre-DPL BT build drives the L4VIDRND.HPP
  VideoComponent/*Renderable hierarchy instead of dpl_DCS).
  CONFIDENT (assert-tagged file=btl4vid.cpp):
    @4cdac0 (549)  ReticleRenderable::AddWeapon  -- append weapon range/pip marker (cap 10:
       "Tried to display too many weapons" line 0x338); clamps range, computes screen pos, pre-builds
       two dpl2d_ display lists (pip ring + extended-range arc).  consts 0.012/0.014/0.7, range 1200.
    @4cef28 (6157) MakeMechRenderables  -- THE per-mech world builder (NOT a per-frame DrawScene as
       the brief guessed).  Walks the JointedMover EntitySegment table (entity@0x300, count@0x318)
       building Root/Hinge/Ball/BallTranslate/StaticChild renderables into a per-segment dcs_array;
       joint type from joint[0x10] (0,1,2=HingeXYZ 3=Static 4=Ball 5=BallTranslate); siteeyepoint→eye
       renderable (inside view); builds targeting reticle from HUD subsystem attrs (inside); walks
       subsystem roster (count@0x124, array@0x128, skips idx0 ControlsMapper) switching on ClassID:
       0xBC0 Reservoir, 0xBC8 Emitter beam, 0xBCD MechWeapon tracer, 0xBD4 PPC beam, 0xBD8 SearchLight.
    @4d0cc0 (1320) SetupMaterialSubstitutionList  -- reads "vehicletable" res (id 0x19), expands
       %color%/%badge%/%patch%/%serno% (serno = per-load '0'..'9'..'A'); parallels RP's version + patch/serno.
  BEST-EFFORT (file=? but in-cluster, by call graph):
    @4d0774  MakeEntityRenderables (ClassID dispatch; 0xBB9 Mech→Setup+@4cef28+@4d11e8, 0xBBA marker,
       0xBDA BTPlayer translocate/POV-start-end, default→VideoRenderer::MakeEntityRenderables @45e744).
    @4cc40c (5096) ReticleRenderable ctor (alloc 0x358; file-private reticle, BT variant of L4VIDRND
       ReticleRenderable).  @4cd918/@4cd938 reticle helpers.  @4d11e8 TearDownMaterialSubstitutionList.
    Inside-view member offsets touched: linkedEntity@0x40(GetLinkedEntity via 417ab4), present-flag@0xb0,
       fog@0xc8..0xd8, mainView@0x124, sceneRoot@0x12c(300), vehicleReticle@0x130, zones@0x2cc/0x2e4/0x318,
       jointToDCS socket@0x3a4.  ReticleRenderable AddWeapon arrays: count@0x38, color@0x64, sim@0xb4,
       mode@0x3c, rangeAttr@0x18c, rangeCache@0x1b4, pipListA@0x2b0/B@0x288; geom minRange@0x230,
       maxRange@0x22c, rangeScale@0x234, originX@0x1fc, originY@0x200, scaleY@0x204, biasX@0x208.
  EXCLUDED (inherited VideoRenderer/RendererOrigin base slots + the L4VIDRND renderable factory ctors
    @0x453xxx-0x460xxx, e.g. 455de4/453578/4537e8/4539b4/453ac4/45848c/4593c0/4590d8/456a68 — engine,
    not this TU).  dpl2d_ prims @0x487f34/0488xxx.  btl4mssn.cpp (@4d2be0+) is a separate TU.
Player (engine) → BTPlayer  vtable 00513300   ctor @4c0bc8   [btplayer.cpp]  size 0x294
  siblings (excluded from btplayer.cpp): BTTeam (vtable 5133dc), BTCameraDirector (vtable 5134c4)
CameraDirector (engine, DIRECTOR.HPP) → BTCameraDirector  vtable 005134c4  ctor @4c1408  size 0x238  [btdirect.cpp DONE]
  (matches surviving BTDIRECT.HPP; RP analog RPCameraDirector). Spectator-camera director: scans the first
  four player slots, picks the lowest-indexed one with a live vehicle, and tells the camera ship which
  mech transform to track; rotates the pick on a 30s timer.
  NOTE: CLASSMAP previously listed "vtable 5134b8" — that address is mid-string in the ClassDerivations
    name "BTCameraDirector" @005134b1; the real vtable starts 0xC later @005134c4 (17 slots).
  Only slot0 (~dtor @4c1484) is a BT override; slots 1..16 inherit CameraDirector/Director/Simulation.
  ClassDerivations @00513414 · DefaultData @00513444 · name str @005134b1.
  methods: ctor @4c1408 (chains base CameraDirector ctor @0042eec8; sets activePerformance=CreateBTCameraShip
    on master, i.e. (simFlags&0xc)!=4), Make @4c1454 (new 0x238), ~ @4c1484 (chains base dtor @0042efa4),
    TestInstance @4c14b0, CreateBTCameraShip @4c13d4 (initial Performance; chains base CreateCameraShip
    @0042ed68 then SetPerformance(&BeABTDirector)), BeABTDirector @4c1230 (steady Performance; BT override
    of base CameraDirector::BeADirector @0042ec3a — 4-player scan & 30.0f interval vs base 1-player/10.0f).
    NB: 4c1230 and 4c13d4 live in a Ghidra-undecompiled gap (@4c11b3..@4c1408); recovered via objdump.
  inherited CameraDirector base layout: activePerformance@0x1c, simulationFlags@0x28, cameraShip-build
    receiver@0x1fc(idx0x7f), followedVehicle Connection@0x20c(idx0x83, ctor @0042f121), switchTimer@0x218
    (idx0x86; base seeds -1.0f, base resets 10.0f, BT resets 30.0f), cameraShip*@0x21c(idx0x87, Dispatch
    target), viewpoint-cut FSM @0x220/0x224/0x228/0x22c with 10.0f@0x230 / 15.0f@0x234 (base @0042eb38).
  Base CameraDirector itself: vtable 004e6b4c, ctor @0042eec8, dtor @0042efa4, Make @0042ef74, size 0x238,
    DefaultData @004e6ad4, GetPlayerByIndex @0042efe4 (walks "Players" EntityGroup @004e6b3c, key +0x1cc).
  bt entity factory @4c15f0: creation request named "camera" (@00513549) → news BTCameraDirector (size 0x238);
    otherwise → news BTPlayer (ctor @4c0bc8, size 0x294). Same TU group as btplayer/btteam; not a BTCamDir member.
ScenarioRole (SCNROLE.HPP survived) — used by BTPlayer @0x208: damageInflictedModifier+0x0c, damageReceivedModifier+0x10

Missile flight-entity cluster (spawned by MissileLauncher::FireWeapon @4bcc60, streamed ClassID 0xBD0):
  Entity → Projectile  vtable 00512a5c  ctor @4be1bc  dtor @4be358  alloc @4be384 (size 0x340)  [projtile.* DONE — only PROJTILE.TCP survived]
        TestClass() takes NO arg (PROJTILE.TCP), TestInstance@4be3b4 (tag 0x512980). ctor registers into world segment table + snapshots launch descriptor.
        members (best-effort, past Entity base): modelUpdateContext@0x250(+bound update PTR_FUN_005129dc@0x254), world@0x300, currentSegment@0x304,
          segmentIndex@0x308(desc+0x84), sourceEntity@0x30C(desc+0x88), sourceWeapon@0x310(desc+0x8C), launchPosition@0x314(desc+0x90),
          launchVelocity@0x320(desc+0x9C), aimDirection@0x32C(desc+0xA8), modelIndex@0x338(desc+0xB4), damageRecord@0x33C(desc+0xB8).
        Performance variants PTR_LAB_005129e8(authoritative)/005129f4(ghost); base MoveAndCollide body past window — live integrator is Missile override @4bef78.
    └─ Missile  vtable 00512f2c  ctor @4bf5b4  dtor @4bf890  alloc @4bf8bc (size 0x368)  [missile.cpp]
         MoveAndCollide @4bef78 (age/guide/integrate/collide; collision FUN_0042291c, detonate FUN_004be078
           streamed ClassID 0x5C), WriteUpdateRecord @4bef4c (slot7, tag 0x78).
         Hosts 2 subsystems: this[0x49]=2 @0x124, roster @0x128 → {Seeker, MissileThruster}.
         enum {SeekerSubsystem=0, MissileThrusterSubsystem=1}.  members (best-effort):
           lifetime@0x340 ageFraction@0x344 thrustScale@0x348 targetConn@0x34C targetOffset@0x350
           age@0x35C explosionModel@0x360 detonationFlag@0x364.
  Subsystem → Seeker  vtable 00512ce8  ctor @4bec34  dtor @4bee44  [seeker.cpp]  (SEEKER.HPP survived)
         targetPosition@0xE4 creationPoint@0xF0 targetEntity@0xFC targetOffset@0x100
         rangeToTarget@0x10C rangeFromCreation@0x110 startingRangeToTarget@0x114.
         LeadTarget @4beae4 (lead window 200..300m, gain ~0.1).  No own resource fields
         (shared default record &DAT_00512bec).  attr IDs 0x341/0x345/0x34D (IndexEntry table @512d50).
         vtable = Subsystem base except slot0 dtor (no virtual overrides).
  Subsystem → MissileThruster  vtable 00512bbc  ctor @4be7c4  dtor @4be8bc  CSS @4bf8ec  [misthrst.* DONE]  (MISTHRST.HPP survived)
         burnTimeRemaining@0xE4 maxThrusterRotationRate@0xE8 (=Degree*PI/180, factor 0x3c8efa35 @4be8b8)
         thrusterAcceleration@0xEC thrusterClimb@0xF0 acceleration(Motion)@0xF4 (linear init (0,0,-thrusterAcceleration); angular@0x100 zero).
         vtable = Subsystem base except slot0 dtor (no virtual overrides; behaviour in Performance PTR_LAB_00512b20=MissileThrusterSimulation, folded into Missile::MoveAndCollide). TestInstance@4be8e8.
         resource: ctor reads +0x30 burnTime, +0x34 maxRotRate, +0x38 thrusterAccel, +0x3C thrusterClimb (Subsystem hdr 0x30).
         CSS @4bf8ec (record size 0x54, type-0xF model record, section "gamedata"): +0x40 MaxThrusterRotationRate, +0x44 BurnTime,
           +0x48 ThrusterAcceleration, +0x4C ThrusterClimb, +0x50 SplashRadius (extra, not in HPP). Missile ctor copies model+0x40..4C → resource+0x34/0x30/0x38/0x3C.
         attr IDs 0xE5/0xE9/0xED (IndexEntry table @512ad0).
  Subsystem region note: AmmoBin (above, classID 0xBCB) is HeatWatcher-derived NOT a bare Subsystem; CSS @4bd6f0 chains HeatWatcher::CSS @4aec54.
         Referenced by ProjectileWeapon as a SharedData connection (connection object ctor @4bcbb0, vtable 00512424) at launcher this[0x10f]@0x43C.

Clarification on the ProjectileWeapon vs Emitter split (mechweap.cpp branch, both : MechWeapon):
  Emitter           CSS @4bb478 parses DischargeTime/GraphicLength/SeekVoltage/SeekVoltageRecommendedIndex.
  ProjectileWeapon  CSS @4bc7cc parses TracerInterval/AmmoBin/MinTimeOfFlight/MinJamChance/MinVoltagePercentToFire.
  MissileLauncher   CSS @4bd08c (chains @4bc7cc) parses MissileCount(+0x1d0)/MuzzleVelocity(+0x1d4).
  MissileLauncher ctor @4bcff0 (chains @4bc3fc) divides damageData.amount by missileCount (per-missile damage).
```

## ClassID registry (from CreateStreamedSubsystem classID stamps)
`0xBB9` Mech · `0xBBD` Condenser · `0xBBE` HeatSinkBank? · `0xBBF` HeatWatcher · `0xBC0` Reservoir ·
`0xBC1` Generator · `0xBC2` PoweredSubsystem · `0xBC3` Sensor (was "Myomers" — corrected) ·
`0xBC6` Myomers (recovered, [myomers.cpp]; stamped by CreateStreamedSubsystem @4b9140 — the REAL Myomers) ·
`0xBC8` Emitter (recovered, [emitter.cpp]; stamped by CreateStreamedSubsystem @4bb478) ·
`0xBCB` AmmoBin (recovered, [ammobin.cpp]; stamped by CreateStreamedSubsystem @4bd6f0) · `0xBCD` MechWeapon ·
`0xBD4` PPC (recovered; stamped by PPC::CreateStreamedSubsystem @4bb8e0) ·
`0xBD3` SubsystemMessageManager (recovered, [messmgr.cpp]; stamped by CreateStreamedSubsystem @49be10) ·
`0xBC4` Gyroscope (recovered, [gyro.cpp]; CSS @4b3eb4, PowerWatcher-derived) ·
`0xBC5` Torso (recovered, [torso.cpp]; CSS @4b6fec, PowerWatcher-derived) ·
`0xBD5` PowerWatcher ·
`0xBD6` HUD (recovered, [hud.cpp]; CSS @4b81d4, PowerWatcher-derived; was mech.cpp's mislabeled "MechTech" 0xBD6 gimbal) ·
`0xBD8` Searchlight (recovered, [searchlight.cpp]; CSS @4b85a8, PowerWatcher-derived) ·
`0xBDE` ThermalSight (recovered, [thermalsight.cpp]; CSS @4b87d4, PowerWatcher-derived) ·
`0xBDC` MechTech (recovered, [mechtech.cpp]; stamped by CreateStreamedSubsystem @4ad48c)
NB: AmmoBin's "AmmoClassID" resource key also names two SPAWNED entity classes (not subsystem stamps):
`0xBD1` Projectile-round entity ("ProjectileClassID") · `0xBBA` Missile-round entity ("MissileClassID").
(Mech ctor streams the contiguous subsystem range 0xBBD–0xBDE; remaining gaps 0xBC7,
0xBC9–0xBCA, 0xBCC, 0xBCE–0xBD2, 0xBD7, 0xBD9–0xBDB, 0xBDD = not-yet-recovered subsystems: actuator/
jumpjet/cockpit/GaussRifle/ProjectileWeapon/MissileLauncher/etc.  NOTE: Sensor 0xBC3, Gyroscope 0xBC4,
Torso 0xBC5, Myomers 0xBC6, Emitter 0xBC8, AmmoBin 0xBCB, SubsystemMessageManager 0xBD3, HUD 0xBD6,
Searchlight 0xBD8 and ThermalSight 0xBDE are recovered, not gaps.)

## Resolved conflict
- `@0x4b02f0` / vtable `0050f45c`: **MechControlsMapper** (dtor slot0 = `4b044c`, mechmppr's; rest inherit
  Subsystem). `powersub`'s "PowerController" label was incorrect — it correctly did NOT emit the code.
- `@0x4b1d18` / vtable `0050fb0c` / classID `0xBC3`: **Sensor** (not "Myomers"). `powersub.cpp` inferred
  members outputVoltage/powered/voltageAvailable with no string backing. The shipped string pool
  @`0050fae0` ("Sensor\0RadarPercent\0SelfTest\0BadVoltage\0") and the AttributePointers table
  @`0050fa50` (ids 0x12/0x13/0x14 → offsets 0x31C/0x320/0x324) match SENSOR.HPP exactly:
  radarPercent@0x31C (1.0f), selfTest@0x320, badVoltage@0x324. CreateStreamedSubsystem @`4b1dcc`
  stamps classID 0xBC3 / model size 0x190 and parses NO extra fields (empty Sensor resource). [sensor.cpp]
- **The REAL Myomers is classID `0xBC6`, vtable `005117dc`, ctor `@4b8fec`** [myomers.cpp] — NOT the
  `@4b1d18`/`0050fb0c` class powersub mislabeled "Myomers" (that is Sensor, above).  Proven by the string
  pool @`00511660` ("Myomers"@`0051166d`, "ToggleSeekVoltage", "SpeedEffect", "CurrentSeekVoltageIndex",
  "RecommendedSeekVoltageIndex", "MinSeekVoltageIndex", "MaxSeekVoltageIndex", "SeekVoltage",
  "OutputVoltage", "VelocityEfficiency", "AccelerationEfficiency") and the attribute table @`00511588`
  (ids 0x12–0x18 → offsets 0x31C/0x320/0x324/0x328/0x32C/0x330/0x344; command id 0x09 ToggleSeekVoltage →
  handler `@4b8a48`).  CSS `@4b9140` stamps 0xBC6 / model size 0x1B0; Mech factory case 0xBC6 allocs 0x358.
  powersub's invented "outputVoltage/powered/voltageAvailable" triad has NO backing and is retired — the
  real members are the SpeedEffect / SeekVoltage[] drive table (see myomers.hpp).
- mech.cpp's `this[0x1f7]` "sensorSubsystem" (case 0xBBE, ctor `4ae8d0`, alloc **0x1E4**) is NOT the
  SENSOR.HPP Sensor (too small for a PoweredSubsystem, which is 0x31C alone). That cache slot holds a
  smaller HeatSink-family subsystem; the "Sensor" name on it is a mech.cpp mis-guess. The real Sensor
  is mech.cpp factory case **0xBC3** (ctor `4b1d18`, alloc 0x328).
- **MechTech is classID `0xBDC`, ctor `@4ad228`** (not `0xBD6`/`@4b7f94`).  mech.cpp's factory switch
  had two swapped labels: it called `@4ad228` "HeatableSubsystem" (case 0xBDC, alloc **0x104** = MechTech's
  size) and called `@4b7f94` "MechTech" (case 0xBD6, alloc 0x2a4).  `@4b7f94` is actually the HUD
  torso-horizon **gimbal display** (base `@4b18a4`, vtable `00511510`; CreateStreamedSubsystem `@4b81d4`
  parses "FlickerRate"/"HorizontalMovementPerSecond"/"HorizontalLimit") and is unrelated to MechTech.
  Evidence for `@4ad228` = MechTech: vtable `0050e3a0` adjacent to the literals "MechTech::SubsystemMonitors"
  @`0050e2f3` and "AlarmModel" @`0050e322`; CreateStreamedSubsystem `@4ad48c` stamps classID 0xBDC and
  reads "AlarmModel"; the ctor builds a ChainOf<SubsystemMonitor*> exactly as MECHTECH.HPP declares.
  Consequence: mech.cpp's `mechTechSubsystem = this[0x16d]` cache (set in the 0xBD6 case) actually points
  at the gimbal display, and the true MechTech (0xBDC case) is not cached — a mech.cpp bug to revisit.

## Shared base offset layout (use for all subsystem modules)
- `Subsystem` occupies the low offsets (owner Mech ptr commonly at this[0x34]/+0xD0).
- `HeatableSubsystem` adds: currentTemperature@0x114, degradation/failureTemperature@0x118/0x11C,
  heatLoad@0x120, coolant/thermal block 0x124–0x160, heat-state flag @0x184.
- `PoweredSubsystem` adds from @0x1D0: voltageSource, aux-screen fields, alarms @0x264/0x2B8,
  thermalResistivityCoefficient@0x30C, startTime@0x314.
- `MechWeapon` adds from @0x31C: fire/recharge/range/pip/damage(Damage@0x3A8)/heatCostToFire.
  NB: MechWeapon's recharge-level slot @0x320 is reused by `Emitter` as **outputVoltage** (the
  normalised discharge readiness; ==1.0 ⇒ fully charged, used in EMITTER.TCP).
- `Emitter` adds from @0x3F0 (object ~0x478): seekVoltageIndex@0x3F0, seekVoltageRecommendedIndex@0x3F4,
  seekVoltageCount@0x3FC, seekVoltage[5]@0x400, currentLevel@0x414, firingActive@0x418,
  beamOrientation@0x41C, beamScale@0x42C, beamLengthRatio@0x434, graphicLength@0x438, dischargeTime@0x43C,
  dischargeTimer@0x440, damageFraction@0x444, energyTotal@0x448, damagePortion@0x44C, heatPortion@0x450,
  energyCoefficient@0x454, seekRate@0x45C, beamEndpoint@0x460, beamFlag@0x46C, targetLocalFlag@0x470,
  targetEntity@0x474.  Firing state machine carried in weaponAlarm level @0x364 (0=Firing,2=Loaded,3=Loading,4=TriggerDuringLoad).

## Recovered resource structs (parsed by each CreateStreamedSubsystem)
- HeatableSubsystem/HeatSink: +0xE4 StartingTemperature, +0xE8 Degradation, +0xEC Failure, +0xF0 ThermalConductance, +0xF4 ThermalMass, +0xF8 HeatSink index
- PoweredSubsystem (size 0x190): +0xFC VoltageSource, ThermalResistivityCoefficient, AuxScreenNumber/Placement/Label[64], EngScreenLabel[64], StartTime
- Generator (size 0x10C): RatedVoltage, MaxTapCount, StartTime, ShortRecoveryTime
- MechWeapon (size 0x1BC): +0x190 RechargeRate, WeaponRange, ExplosionModelFile, DamageAmount, DamageType, HeatCostToFire, Pip*
- Emitter (size 0x1DC): +0x1BC GraphicLength, +0x1C0 DischargeTime, +0x1C4 SeekVoltage[5], +0x1D8 SeekVoltageRecommendedIndex (parser @4bb478)
