Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.
Layout:
engine/ MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
models) + image codec; the minimal rp/ headers the audio HAL needs
game/ reconstructed BT logic + surviving-original BT source + fwd shims
+ WinMain launcher
content/ full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
docs/ format specs + reconstruction ledgers
reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
tools/ MP console emulator + map/resource scanners
One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 KiB
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). SetControlMode@4d1ae4 (mode bits18-20,
│ table @0051dbe4={0x40000,0x80000,0x100000}). SetPresetMode@4d1b24 (group*5+item -> {clear,set} mask table @0051dbf0[15]).
│ CreateTemporaryEventMappings@4d1840 / RemoveTemporaryEventMappings@4d18dc (buttons 0x40..0x47, hat-skip 0x41->0x45,
│ mode 0x8000=ConfigReady). NotifyOfControl/ConfigModeChange = no-ops @4d195c/@4d1964. 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/ vtable0050f45c: MechControlsMapper (dtor slot0 =4b044c, mechmppr's; rest inherit Subsystem).powersub's "PowerController" label was incorrect — it correctly did NOT emit the code.@0x4b1d18/ vtable0050fb0c/ classID0xBC3: Sensor (not "Myomers").powersub.cppinferred 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 @4b1dccstamps classID 0xBC3 / model size 0x190 and parses NO extra fields (empty Sensor resource). [sensor.cpp]- The REAL Myomers is classID
0xBC6, vtable005117dc, ctor@4b8fec[myomers.cpp] — NOT the@4b1d18/0050fb0cclass 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@4b9140stamps 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, ctor4ae8d0, 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 (ctor4b1d18, alloc 0x328). - MechTech is classID
0xBDC, ctor@4ad228(not0xBD6/@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).@4b7f94is actually the HUD torso-horizon gimbal display (base@4b18a4, vtable00511510; CreateStreamedSubsystem@4b81d4parses "FlickerRate"/"HorizontalMovementPerSecond"/"HorizontalLimit") and is unrelated to MechTech. Evidence for@4ad228= MechTech: vtable0050e3a0adjacent to the literals "MechTech::SubsystemMonitors" @0050e2f3and "AlarmModel" @0050e322; CreateStreamedSubsystem@4ad48cstamps classID 0xBDC and reads "AlarmModel"; the ctor builds a ChainOf<SubsystemMonitor*> exactly as MECHTECH.HPP declares. Consequence: mech.cpp'smechTechSubsystem = 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)
Subsystemoccupies the low offsets (owner Mech ptr commonly at this[0x34]/+0xD0).HeatableSubsystemadds: currentTemperature@0x114, degradation/failureTemperature@0x118/0x11C, heatLoad@0x120, coolant/thermal block 0x124–0x160, heat-state flag @0x184.PoweredSubsystemadds from @0x1D0: voltageSource, aux-screen fields, alarms @0x264/0x2B8, thermalResistivityCoefficient@0x30C, startTime@0x314.MechWeaponadds from @0x31C: fire/recharge/range/pip/damage(Damage@0x3A8)/heatCostToFire. NB: MechWeapon's recharge-level slot @0x320 is reused byEmitteras outputVoltage (the normalised discharge readiness; ==1.0 ⇒ fully charged, used in EMITTER.TCP).Emitteradds 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)