Files
BT411/context/decomp-reference.md
T
arcattackandClaude Opus 4.8 89b4f53046 Gauges: trigger-config joystick is LIVE -- @004c6ee0 is LinkToEntity, not SetColor
The weapon panel's btjoy trigger-config display (ConfigMapGauge) never rendered
because the reconstruction labelled @004c6ee0 as "SetColor(int)" and, finding no
caller, concluded the gauge was authentically dormant (task #6), gating it
behind a BT_CONFIGMAP dev env.

The user challenged this (the PROGRAM/TRIGGER CONFIG button implies the display
visualizes your config).  Re-verification found the old analysis failed twice:
the "no caller" search looked for direct calls to a VIRTUAL, and its slot math
used the wrong vtable copy.  The real ConfigMapGauge vtable @0051a1b8, matched
against the T0 GaugeBase virtual roster (GAUGE.h), pins @004c6ee0 at slot 9
(+0x24) == GaugeBase::LinkToEntity, and @0x94 is the linkedEntity -- the
Execute gate is "not yet linked", not "disabled".  The engine broadcasts
LinkToEntity(viewpointEntity) to every gauge at viewpoint bind (APP.cpp:1277 ->
GaugeRenderer::LinkToEntity GAUGREND.cpp:3011), arming the gate -- the joystick
DOES render in the shipped game, showing per-trigger mapping state (solid =
mapped, matching the DOSBox reference).

Port fix: SetColor(int color) -> LinkToEntity(Entity*), color -> linkedEntity,
BT_CONFIGMAP dev enable deleted (the authentic path lights it).  Render-verified
with no env override: the joystick + mapped-trigger lamp draw on weapon panels.

KB swept per the correction mandate: gauges-hud.md, decomp-reference.md,
open-questions.md, GAUGE_COMPOSITE.md, VEHICLE_SUBSYSTEMS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:49:58 -05:00

420 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: decomp-reference
title: "Decomp Reference — resource types, ClassIDs, offsets, addresses, env gates"
status: established
source_sections: "CLAUDE.md §10c; game/reconstructed/CLASSMAP.md; reference/decomp/all/part_*.c"
related_topics: [subsystems, combat-damage, reconstruction-gotchas, build-and-run, gauges-hud]
key_terms: [ClassID, resource-type, subsystem-roster, BTL4OPT, factory]
open_questions:
- "0xBCE weapon family fully mapped? (GaussRifle : Emitter, FireWeapon is a no-op in this build)"
---
# Decomp Reference
The quantitative hub for the reconstruction: resource-type enum, ClassID map, mech struct
offsets, key `FUN_` addresses, and env gates. Cite a section here rather than re-deriving.
Everything is `[T1]` (decompiled + byte-verified) unless tagged otherwise. The living
source-of-truth is `reference/decomp/all/part_*.c`; the class map is
`game/reconstructed/CLASSMAP.md`.
---
## 1. Resource lookup
`ResourceFile::FindResourceDescription(name, type, index=-1)` (`FUN_00406ff8` = find-by-type-and-name;
`FUN_00407064` = `SearchList(resourceID, type)`). Lookup is **by NAME or by ID**. `Mech::ResolveAnimationClip(prefix,suffix)` builds a name and calls it.
⚠ A clip_id from `ResolveAnimationClip` is a **DIRECT resource ID**`FindResourceDescription(clip_id)`,
NOT `SearchList(clip_id, 16)` (which treats the arg as a LIST → walks garbage → AV). [T1]
### Resource type enum
| Type | Value | Holds |
|---|---|---|
| SkeletonStream | — | `.SKL` skeletons |
| DamageZoneStream | **0x14** | damage-zone records (Mech ctor reads count + array) |
| GaugeImageStream | **18** (0x12) | radar PIP vector shapes (.GIM linelist; keyed by the entity model's resource id -- `bhk1`=1025, `madcat`=401, …; 110 ship in BTL4.RES). task #17 CORRECTION: the old "ModelList = model/mech names" label was wrong -- the names matched because pips are stored under their model's name/id. |
| AnimationResourceType | **16 (0x10)** | animation clips (lowercase: `blhrrl`=904, `blhrrr`=905, `blhwwli`=910, `blhrrli`=916) |
| damage-zone segment list | 0x14 | per-zone segment list |
| critical-subsystem segment | 0x1e | crit subsystem segments |
| CameraStream | **27 (0x1b)** | per-map spectator camera network, resource named after the map (all 8 maps ship; arena1 = 62 cams). Loaded by the CameraShip system when no `cameras\<map>` dev-override FILE exists (`FUN_0042be3c``FUN_0042c078`; records `{count; per-cam {size; type 1\|2; pos+quat…}}` @0042ad54/@0042b9f8). [[open-questions]] §camera seat. [T1] |
Animation clip names in `BTL4.RES` are **lowercase**. [T1]
---
## 2. ClassID map (subsystem factory)
Base 3000 = **0xBB8**. The factory switches on the resource ClassID; the `case <Name>ClassID`
LABELS are **systematically mislabeled** — trust the `// FUN_004xxxxx` ctor-address comment
reconciled via `CLASSMAP.md`, not the case label. [T1]
| ClassID | Real class | ctor @ | Notes |
|---|---|---|---|
| 0xBB9 | **Mech** | @004a2d48 (`Mech::Make`) | sizeof 0x854 (binary); reconstructed 0x638 |
| 0xBBD | **Condenser** | @4ae568 | valve; `case`="?" |
| 0xBBE | **AggregateHeatSink** (heat-sink BANK) | @4ae8d0 | GUID 0x50e590, sizeof 0x1E4; `case`=SensorClassID |
| 0xBC0 | **Reservoir** | @4af408 | `case`=Condenser |
| 0xBC1 | **Generator** | @4b… | power bus |
| 0xBC2 | **PoweredSubsystem** | @004b0f74 | sizeof 0x31C |
| 0xBC3 | **Sensor** | — | `case`=MyomersClassID |
| 0xBC4 | **Gyroscope** | @004b3778 | LIVE (task #56: byte-exact ctor/integrators/writers; hit-bounce fan-out wired) |
| 0xBC5 | **Torso** | @004b6b0c | sizeof 0x280; `case`=SinkSource |
| 0xBC6 | **Myomers** | @4b8fec | `case`=Actuator; un-stubbed (`8b36440`, full recon; per-tick Sim SetPerformance call-site still best-effort) |
| 0xBC8 | **Emitter** | @004b1d18 | sizeof 0x478; energy weapon |
| 0xBCB | **AmmoBin** | — | `case`=JumpJet; sizeof 0x22C |
| 0xBCD | **ProjectileWeapon** | @4bc3fc | sizeof 0x448 (`case`≠ this — see VDATA.h) |
| 0xBCE | **GaussRifle** | @4bdcb4 | : Emitter; FireWeapon is a **no-op** in this build |
| 0xBD0 | **MissileLauncher** | @4bcff0 | sizeof 0x44C |
| 0xBD3 | **SubsystemMessageManager** | @0049bca4 | damage/explosion consolidation hub (ConsolidateAndSendDamage, weaponExplosions); cached to `Mech[0x10d]`=**0x434** (✅ task #7 `afefaee`: factory builds the real class + `mech.hpp` names it `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning **BTPlayer** @mech+0x190). |
| 0xBD4 | **PPC** (: Emitter) | — | same FireWeapon as Emitter; `factory calls it GaussRifle` |
| 0xBD6 | **HUD** | — | alloc 0x2a4; `case`=MechDisplay-ish |
| 0xBD8 | **Searchlight** | — | `case`=LegSubsystem |
| 0xBDC | **MechTech** | — | alloc 0x104 |
| 0xBDE | **ThermalSight** | — | `case`=MechDisplay |
Identify a class at runtime with `sub->GetClassID()` (== `sub+4`). [T1]
### Class hierarchy (the two branches that share only MechSubsystem)
- **Heat leaf:** `PoweredSubsystem : HeatSink : HeatableSubsystem : MechSubsystem` (Emitter/PPC/Sensor/Myomers/weapons chain here). [T1]
- **Watcher branch:** `Torso/Gyroscope/Searchlight/ThermalSight/HUD : PowerWatcher : HeatWatcher : MechSubsystem`; `AmmoBin : HeatWatcher`. Uses a 0xC connection + 0x54 alarm (NOT the 4/8-byte heat-leaf types). [T1]
### Derivation-tag addresses (`FUN_0041a1a4(obj, tag)` = IsDerivedFrom)
| Tag | Class | Evidence |
|---|---|---|
| **0x511830** | **MechWeapon**::ClassDerivations | The reticle AddWeapon loop (part_014.c:5386) hard-aborts on missing WeaponRange/PipPosition/… attrs for every member; part_012.c:9344 counts ready weapons; the Mech ctor roster at this[0x1ef] (`weaponRoster`, ex-mislabel "poweredSubsystems") ORs weapon `capabilityFlags`@+0x334. [T1] |
| 0x50f4bc | PoweredSubsystem | btl4gau2 gauge wave. [T1] |
| 0x50e3ec | HeatableSubsystem | btl4gaug widget gate. [T1] |
| 0x50e604 | **HeatWatcher** (task #57 CORRECTION — was "HeatSink") | `HeatWatcher::TestInstance` @4aec38 tests its OWN class vs this tag; the factory watcher-CONNECT loop (slot +0x38) selects on it; the btl4gaug widget gate ORs it after HeatableSubsystem (a HeatSink test there would be redundant — HeatSink : HeatableSubsystem). 0x50e590 = the HeatSinkBank (0xBBE) tag. [T1] |
| 0x51155c | roster at Mech this[0x1eb] (`heatableSubsystems` label) | tag ≠ 0x50e3ec — the label is UNVERIFIED [T4]; arbitrate before relying on it. |
| 0x50e4fc | roster at Mech this[499] (`damageableSubsystems` label) | [T3] |
| 0x50bdb4 | Mech | emitter.cpp destroyed-check. [T1] |
| 0x5121a8 | ProjectileWeapon-family (checked after 0x511830; reads ammo state @+0x364) | [T3] |
### Watcher electrical chain (task #57 — all [T1], several from RAW-BYTE recovery)
| Address | What |
|---|---|
| @004aec54 | `HeatWatcher::CreateStreamedSubsystem` — parses `WatchedSubsystem=<name>` → segment index **+2** at resource+0xE4 |
| @004aee2c / @004b1a40 | vtable slot 14 (+0x38) CONNECT bodies (HeatWatcher / PowerWatcher+Torso, byte-identical): master-gated `watchedLink(+0x114).Add(owner->roster[watchedSubsystem(+0x128)])`. **Ghidra missed both starts** — recovered from raw exe bytes (vtables.tsv rows have GAPS at skipped slots; dump vtable+slot*4 bytes when a slot looks absent) |
| @004b181c | `PowerWatcher::UpdateWatch` — THE registered Performance (PTR @0050f5fc); watchdogAlarm(+0x184) MIRRORS watched subsystem's electrical level (+0x278); brownout→1 when `gen outputVoltage(+0x1DC) <= minVoltage(+0x180) × ratedVoltage(+0x1D8)`. Torso sims call it first-line |
| @004b1804 | `PowerWatcher::ResetToInitialState` (slot 10) — old recon MISLABELED it "Simulation" |
| 0x4b1924 | `MinVoltageScale` — a **10-byte x87 literal** (`0a d7 a3 70 3d 0a d7 a3 f8 3f` = **0.01**); the port had 1.0f → permanent brownout |
| @004b6a78 | `Torso::ReadUpdateRecord` (slot 6; base 41bd34) — REPLICANT apply of twist extras +0x10/14/18. Old recon mislabeled it Write |
| @004b6a1c | `Torso::WriteUpdateRecord` (slot 7; base 41c500) — MASTER serialize; recordLength=0x1C; **Ghidra missed the start** |
| 41bd34 / 41c500 | base Subsystem Read (apply) / Write (produce) update-record fns — engine [T0]: **Write=produce, Read=apply**; don't flip |
---
## 3. Mech struct offsets (`Mech*`, binary layout)
Reconstructed access MUST use named members/accessors — these raw offsets are the BINARY's,
valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-gotchas]]). [T1]
| Offset | Field | Notes |
|---|---|---|
| +0x11c / +0x120 | `damageZoneCount` / `damageZones[]` | inherited Entity; Mech ctor populates the array |
| +0x124 / +0x128 | `subsystemCount` / `subsystemArray[]` | the [[subsystem-roster]] (NOT the segment table) |
| +0x190 | **owning `BTPlayer`** (`GetPlayerLink()`) | set by `FUN_0049f624` (mech↔player bind; also `player->playerVehicle(+0x1fc)=mech`). The valve guard (`FUN_004ac9c8 →player+0x274` = novice lockout) + heat-model gate (`FUN_004ad7d4 →player+0x260` = veteran/expert) read the owning player's EXPERIENCE flags — see [[experience-levels]] + the BTPlayer flag block below. `MECH_OWNING_PLAYER`. |
| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). ✅ task #7 (`afefaee`): `mech.hpp` names this `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0. |
| +0x260 / +0x26c | `localOrigin` = { Point3D linearPosition; Quaternion angularPosition } | ORIGIN.h:15; position@+0x100 in some subsystem views |
| +0x300 | segment table | skeleton `EntitySegment`s (muzzle sites, joints) |
| +0x37c | target world `Point3D` | aim source |
| +0x388 | target `Entity*` | HasActiveTarget() gate |
| +0x38c | targeted sub-zone (int) | -1 = whole |
| +0x42c | `shadowJointNode` (Joint*) | ShadowJointName @model+0xB4 |
| +0x438 | torso subsystem cache | HUD/mapper read it |
| +0x5b4 | HUD subsystem cache | (factory case 0xBD6 writes param_1[0x16d]) |
| +0x650 | `deathAnimationLatched` / crashed flag | |
| +0x7cc | `damageableSubsystems` chain | the condenser chain (GUID 0x50e4fc) RecomputeCondenserValves iterates |
`mech+0x100` (a subsystem-view of localOrigin.linearPosition): `.x@+0x100, .z@+0x108`. [T1]
### BTPlayer (0x294) experience/game-mode flag block (ctor @4c0bc8; see [[experience-levels]]) [T1]
Seeded on the master branch from `btMission(+0x1f8)->experienceLevel(+0xe4)` (egg `experience`,
parse @4d2f3e`BTMission+0xe4`) and `advancedDamageOn(+0xf0)`. The +0x1f8 mission pointer is
the inherited `Player::playerMission`. Port members (issue #2): `simLive`/`heatModelOn`/
`advancedDamageOn`(+2)/`levelFlag26c`/`levelFlag270`/`experienceLevel`; bridges
`BTPlayerExperienceSimLive`/`…HeatModelOn`/`BTPlayerRoleLocksAdvanced` (btplayer.cpp):
| Offset | Value by level (nov/std/vet/exp) | Verified readers |
|---|---|---|
| +0x25c | 0/1/1/1 — "sim live" (novice lockout) | jam roll @4bbfcc, searchlight ToggleLamp @4b860c, powersub handler @4b0efc, mech-ineffective eval part_012.c:9364 |
| +0x260 | 0/0/1/1 — **heat-model master switch** | `FUN_004ad7d4` → Myomers @4b8d18, Emitter, heat.cpp, mislanch, projweap heat |
| +0x264/+0x268 | = mission `advancedDamageOn` (both) | heat-family sim part_013.c:8757 |
| +0x26c | 0/1/1/0 | none located (supercharge-clamp hypothesis [T4]) |
| +0x270 | 0/1/1/1 | none located |
| +0x274 | raw level 0..3 | `FUN_004ac9c8` = (==0) novice predicate (valve guard), powersub @4b21d0 |
### Binary ATTRIBUTE TABLES (2026-07-16, walked from the image) [T1]
**Row format: 16-byte `{id, namePtr, memberOffset+1, 0}`** (MIND THE ALIGNMENT — an 8-off walk
invents a fake one-slot shift; verified against the live-tested ConfigureActivePress row
@0x50de54). Walker: find the attr NAME string in DATA, find the pointer to it, decode ±16-byte
rows until sanity fails (`python + struct`, see the session commits `cc2b109`/`21378ec`).
- **Mech entity table @0x50c0xx, ids 2156**: MaxAcceleration@0x344 (=forwardCycleRate),
CurrentSpeed@0x348 (=legCycleSpeed), MaxRunSpeed@0x34c (=run-cycle avg = the mapper's demand
multiplier), Collision{State,Normal,Speed,MaterialType}@0x450/0x4a8/0x4b4/0x4a4,
EyepointRotation@0x360 (the eye renderable's view rotation — the old `mechName@0x360` label is a
MISREAD), TargetReticle@0x36c, FootStep@0x394, AnimationState@0x39c, ReplicantAnimationState
@0x714, LinearSpeed/AngularSpeed/ClimbRate/AccelerationLastFrame@0x81c/0x824/0x828/0x82c (the
15-sample-ring velocity means + the accel snapshot), damage levels @0x354-0x35c, TestButton1-6
@0x328-0x33c, ReduceButton@0x340, Radar{Range,LinearPosition,AngularPosition}@0x404-0x40c,
**RearFiring@0x410** (OR of the weapons' rear flags; old `stateFlags` label was wrong),
RequestDuckAnimation@0x398, UnstablePercentage@0x3f0, SuperStop@0x3f4, DuckState@0x3f8,
IncomingLock@0x3fc, DistanceToMissile@0x400 (FLT_MAX far default; old `maxSpeed` label wrong).
- **HeatSink table @0x50e438, ids 312**: HeatSink@0x164, CurrentTemperature@0x114,
Degradation/FailureTemperature@0x118/0x11c, Normalized/DegradationPressure@0x120/0x124,
CoolantCapacity@0x128, CoolantMass@0x12c, CoolantMassLeakRate@0x130, **ReportLeak@0x138**
(= coolantActive, the leak hysteresis flag). Confirms every port heat binding.
- **Torso table, ids 315**: RotationOfTorsoVertical/Horizontal@0x1e4/0x1d8, HorizontalLimit
R/L@0x1dc/0x1e0, SpeedOfTorsoVertical/Horizontal@0x1ec/0x1e8 (|rate|, abs'd),
StickPosition@0x1f0, TorsoUp/Down/Left/Right/Center@0x1f8-0x208, MotionState@0x20c
(statusFlags; ==2 = the limit-hit frame).
- **MechWeapon rows**: RearFiring id 0x1B @0x334 = "the mount segment page name contains 'b'"
(marker @0x511aa2 — the back gun ports `sitelbgunport`/`siterbgunport`; ctor part_013.c:6913-30).
---
## 4. Damage delivery
- `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)``target->Dispatch(&msg)`.
- Base `Entity::TakeDamageMessageHandler` **IGNORES zone==-1** — the Mech STEP-6 cylinder-lookup override (✅ done, `d07ac7d`; `dmgtable.cpp` `ResolveHit` + `Mech::damageLookupTable`) resolves an unaimed (zone==-1) hit to a zone before base-routing. [T1/T2]
- `class Damage { damageType(enum Collision/Ballistic/Explosive/Laser/Energy), damageAmount, damageForce, surfaceNormal, impactPoint, burstCount }`. [T1]
- Weapon effect id: **"explode" = 13** (`Explosion::Make`). [T2]
- `DestroyEntityMessage(id,size)` removes an entity — but a killed mech STAYS (a WRECK); death = a STATE transition (`SetGraphicState(DestroyedGraphicState)`), NOT removal. Issuing removal-on-death is the P5 teardown bug (do not). [T2]
---
## 5. Weapon fire-rate constants (recovered)
From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed as x87 float10). [T1]
- `DischargeTime` = beam-on time. ER-medium laser (BLH) = **0.2 s**.
- `RechargeRate` = recharge time in **seconds exactly** (the two ln5 factors cancel): ER-S 1.0 / ER-M 2.0 / ER-L 4.0 / PPC 5.0.
- One BLH ER-M cycle = 0.2 + 2.0 = **2.2 s**; ~12 lasers staggered → aggregate ≈ 5/s.
- `WeaponRange` 500 (ER laser), `DamageAmount` 3.5, `PipColor` 1.0 0.0 0.0 (RED).
- Charge constants: `_DAT_004bb3c4`=1.0, `_DAT_004bb3b8`=1e-4, `_DAT_004bb3b4`=0.5; RatedVoltage (GENRATOR.SUB)=1e4; fire threshold `seekVoltage[rec]=0.8*RatedVoltage`.
- **The electrical chain (task #10, LANDED) [T1]:** ctor @004bb120 calibrates
`voltageScale@0x310 = (RechargeRate / -ln(1 - 1e-4*seekV[rec])) / EC`;
`ChargeTimeScale` @004b0d50 reads it back every tick x `(1 + thermalResistivityCoefficient
(res+0x100, emitters 0.0024) x max(0, srcT-srcT0))` -- hot generators charge slower;
`TrackSeekVoltage` @004ba838 puts the charging I^2R (`seekRate^2 x dtScale x dt`) into the
GENERATOR's `pendingHeat@0x1c8` (~3.5e8/full PPC charge). `ComputeOutputVoltage` @004ba738
snaps rechargeLevel@0x320 to 1.0 within 0.01. **FailureHeat consumers:** `this+0x184`
(own heatAlarm level) == 2 -> emitter @004baa88 resets firing + holds charge 0; ballistic
@004bbd36 pins `recoil=rechargeRate` + alarm 7.
- **@004bbd04 ProjectileWeaponSimulation FULLY RECOVERED (capstone disasm, Gitea #12,
2026-07-19) [T1] — PORT NOW IMPLEMENTS IT (landed 2026-07-19, projweap.cpp; solo-verified:
missiles fly, denial blip stays Loaded, recoil clamped, slot-17 dial animates; MP re-verify
pending)** — the old RivetGun-modeled body diverged; the authentic machine:
PoweredSubsystemSimulation@4b0bd0; trigger=CheckFireEdge@4b9608; UpdateEject@4bbb50;
**gate 1 @4bbd36: `simulationState@0x40==1` (destroyed — NOT simulationFlags@0x28!) ||
heatAlarm@0x184==2 || FUN_49fb54(owner disabled) → recoil=rechargeRate + alarm 7**;
gate 2 @4bbd71: bin=Resolve(this+0x43c)@417ab4; bin alarm LEVEL@+0x1A8 ∈{2,3} || bin
simulationState@0x40==1 → alarm 7 (re-pinned per frame while dry). switch(alarm@0x364):
**Loaded(2) @4bbec2**: on trigger — `viewFireEnable@0x3E0==0 || ownerTarget@[owner+0x388]==0
→ SetLevel(4);SetLevel(2)` (denial BLIP, stays Loaded, NO ammo pull); else
`bin->FeedAmmo@4bd4f4` (fail → stays Loaded silently) → SetLevel(0 Firing) →
**`updateModel@0x18 |= 1`** → vtbl+0x48 FireWeapon → (bin level==2 → SetLevel(7)) else
(CheckForJam@4bbfcc → SetLevel(5)) else SetLevel(3) → `updateModel|=1`
recoil=rechargeRate. **Loading(3) @4bbdd2**: trigger → blip 4→3; if
electricalState@0x278==4 { DrawFiringCharge@4bbc78; recoil<0 → clamp 0 + (bin level==1 →
SetLevel(2)) }; always vtbl+0x44. **Jammed(5) @4bbe8e**: trigger → 6→5;
recoil=rechargeRate. **7 (unavailable/NoAmmo) @4bbe4d**: trigger → blip 1;
re-SetLevel(7) unconditionally (roach motel — permanent once entered);
recoil=rechargeRate; vtbl+0x44. States 0/1/4/6 = transient audio blips (no case body).
**vtbl+0x44 (slot 17) @004b9c9c = `rechargeLevel@0x320 = (rechargeRate@0x3DC
recoil@0x3E8)/rechargeRate`** — the projectile recharge-dial writer (Emitter overrides this
slot with ComputeOutputVoltage@4ba738). **MissileLauncher::FireWeapon @004bcc60 carries NO
view/target/ammo gating and NO recoil set** (part_013.c:8741+: heat add + spawn descriptor
only) — ALL gating lives in the caller's Loaded case. ⚠ The DIRTY-for-replication bit is
`Simulation::updateModel@0x18 |= 1` (== engine `ForceUpdate()`), NOT
`simulationFlags@0x28 |= 1` — bit 0 of simulationFlags is the engine **DelayWatchersFlag**
(SIMULATE.h): setting it permanently skips the subsystem's ExecuteWatchers (audio watchers).
Cockpit power-routing
handlers @004b099c/@004b09e4/@004b0a2c/@004b0a74 (bind generator by authored id 1-4 via
@004b0b18) + @004b0abc (auto/manual, modeAlarm@0x2CC); attach/detach @004b0dd8/@004b0e30
(generator client capacity @0x1e4/count @0x1e8).
---
### The CONFIG-MODE session chain (task #6) [T1]
- MechWeapon handler table @0x511860: {9,"ConfigureMappables"→@004b9550}, {10,"ChooseButton"→@004b95b8}
(strings @0x511994/@0x5119A7). PoweredSubsystem table @0x50F4EC: ids 4-8 = SelectGeneratorA-D /
ToggleGeneratorMode → @004b099c/@004b09e4/@004b0a2c/@004b0a74/@004b0abc.
### Binary message tables decoded 2026-07-20 (glass input audit) [T1]
16-byte HandlerEntry {id, namePtr, fnPtr, 0} rows in section_dump. Per-receiver-class id spaces.
- **Mech (entity)** @0x50BDF8..: 0x12 TakeDamage@004a0230, 0x14 PlayerLink@0049f624, 0x15
RealMaxSpeed@0049f604, **0x16 BalanceCoolant@0049f728**, 0x17 SetBurningState@0049f674, 0x18
ClearBurningState@0049f700, **0x19 EjectPilot@0049f854**, **0x1a DuckRequest@0049fa00** (the
manual's CROUCH; streamed button 0x13 sends it — unreconstructed).
- **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. **Disassembled 2026-07-20**
(`tools/disas2.py 0x4ad6f8`): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle
priority" — that's the emergent effect, not the mechanism):
`if (FUN_004ac9c8(this)) return;` (== `BTPlayerRoleLocksAdvanced` NOVICE lockout, owner→
BTPlayer→`+0x274`==0) → `if (msg->dataContents(+0xc) <= 0) return;` (press only) → toggle
`coolantAvailable(+0x134)` 0↔1 and `coolantFlowScale(+0x15C)` 0.0f↔1.0f. So a pilot can cut a
subsystem's coolant flow (raising its heat) to free the shared loop for others. **UNWIRED cause
found:** the reconstruction's `PoweredSubsystem::GetMessageHandlers` chains straight onto the
`Receiver` root (powersub.cpp:144), SKIPPING `HeatSink``HeatableSubsystem` — so a weapon's
handler chain never reaches id 3. Faithful wiring = define `HeatableSubsystem::GetMessageHandlers`
{3, ToggleCooling} + restore the chain `PoweredSubsystem→HeatSink→HeatableSubsystem`. Sibling
template: `Condenser::MoveValveMessageHandler` (id 4, same novice guard). See [[experience-levels]].
- **Generator** @0x50FB90: {4, "ToggleGeneratorOnOff"→@004b1ed0} (streamed buttons 0x1A-0x1D →
GeneratorA-D — unreconstructed).
- **Searchlight** @0x51120C: {3, "ToggleLamp"→@004b860c}; PowerWatcher/ThermalSight-side variant
@0x51117C: {3, "ToggleLamp"→@004b838c}. (Reconstruction has Searchlight::ToggleLamp but the
MessageHandlers set is default-constructed EMPTY — unwired.)
- **Myomers-class** @0x51158C: {9, "ToggleSeekVoltage"→@004b8a48}; **energy-weapon** variant
@0x511DB8: {0xb, "ToggleSeekVoltage"→@004ba478}; **ammo-weapon** @0x512210: {0xb,
"EjectAmmo"→@004bb9b8}.
- **MechRIOMapper** @0x51DD30: RE-REGISTERS the base ids 3..0x13 (Aux1Quad..ZoomOut) with the
RIO override handlers @004d22fc..@004d24f8, + {0x19, Keypress→@004d2514} (RIO's OWN keypress,
unreconstructed — we use the shared L4 0x17 @004d1bf0) + {0x1a, Hotbox→@004d2574} @0x51DE98.
The reconstruction's enum chained these ids to 0x19-0x2a until 2026-07-20 — every streamed
MFD-bank/zoom press hit the base FAIL trap; now pinned + static_assert-locked (btl4mppr.hpp).
- Unhandled ids are SILENT: `Receiver::Receive` Find() returns NullHandler and does nothing.
- MechWeapon ATTRIBUTE table @0x511890: ELEVEN entries — 0x12 PercentDone@0x320, 0x13 TriggerState@0x31C,
0x14 DistanceToTarget@0x324, 0x15 TargetWithinRange@0x34C, 0x16 WeaponRange@0x328, 0x17 PipPosition,
0x18 PipColor, 0x19 PipExtendedRange, 0x1A EstimatedReadyTime@0x330, 0x1B RearFiring@0x334,
0x1C WeaponState@0x350.
- Mapper vtables: base @0050F45C (+0x38 Enter Fail@004b0280 / +0x3C Exit Fail@004b029c / +0x40
AddOrErase-evt Fail@004b02b8 / +0x44 AddOrErase-dir Fail@004b02d4 / +0x48,+0x4C Notify @004b048c/@004b0494);
L4 @0051E440 (+0x38=@004d1840, +0x3C=@004d18dc, +0x40/+0x44 = RET no-ops @004d195c/@004d1964,
+0x48/+0x4C=@004d1acc/@004d1ae4); Thrustmaster @0051E3F0 (inherits the no-ops); RIO @0051E3A0
(+0x40=@004d25e8, +0x44=@004d262c). @004afbc4 = a pure Fail thunk ("Unhandled button mapping!", 0x7a).
- ControlsManager vtbl: +0x20 StartMappableButtonsConfigure / +0x24 Stop / +0x28 AddOrEraseMappableButton
(T0 CONTROLS.cpp:291-351). Group vtbl: +0x0C Remove, +0x10 Update, +0x14 Add(evt), +0x18 Add(dir),
+0x1C AddOrErase(evt), +0x20 AddOrErase(dir), +0x24 GetMapState (unmapped=0/others=1/mine=2).
- Mode masks (T0 BTL4MODE.HPP): ModeMapping=0x8000 (bit 15) / ModeNonMapping=0x10000 (bit 16), mutually
exclusive. MechSubsystem +0xE8/+0xEC = controlDestination/controlMessageID (ctor pass-throughs;
MechWeapon defaults destination=&fireImpulse@0x31C per @004b99a8). Subsystem+0x110: 0/-1 config-session
flag written by handler 9 (reader unknown).
- ConfigMapGauge @004c6d80/@004c6f1c: table DAT_00518eb4 = {y,btn} {0x0d,0x45}{0x25,0x46}{0x3d,0x40}
{0x55,0x47}, blit x=0xc; @004c6ee0 = the virtual GaugeBase::LinkToEntity override (vtbl @0051a1b8
slot 9 +0x24; @0x94 = linkedEntity, the Execute gate), broadcast at viewpoint bind (APP.cpp:1277 →
GAUGREND.cpp:3011) — the gauge IS live in the shipped build (old "SetColor/no-caller/dormant" claim
was wrong; corrected 2026-07-21).
## 6. Env-var gates (runtime; default OFF unless noted)
The authentic stack is DEFAULT-ON (`BTEnvOn`, mechrecon.hpp): `BT_GAIT_CUTOVER`, `BT_GAIT_SM`,
`BT_COLLISION`, `BT_REAL_CONTROLS` default ON — set `=0` to fall back. ⚠ Most BOOLEAN gates check
`getenv(name) != NULL` or first-char `!= '0'`, so `BT_X=""` (empty) still ENABLES; use
`Remove-Item Env:\BT_X`. Value-parsing gates are NOT empty-enabled: `BT_AUTOFIRE` and
`BT_KEY_NOFOCUS` require `'1'`, `BT_GOTO` requires a non-empty value, and `BT_WARP_MIP` is
default-ON (`'0'` disables).
| Var | Effect |
|---|---|
| `BT_FORCE_THROTTLE` | auto-walk forward (no key) |
| `BT_SPAWN_ENEMY` | spawn a target mech 120u ahead along the spawn facing |
| `BT_AUTOFIRE=1` | hold the trigger (headless walk→fire→death harness; supersedes the dead `BT_FORCE_FIRE` — btl4main.cpp:310 set `fireForced` once at startup, but mech4.cpp:1567 unconditionally overwrites it every frame) |
| `BT_ASSERT_TO_DEBUGGER` | route CRT asserts to the debugger, not a modal box |
| `BT_HEAPCHECK` | whole-heap validation on every alloc/free (O(n²) at mission load — SLOW) |
| `BT_PROBE_BGF=<name\|ALL>` | boot-time BGF loader probe, exits after |
| `BT_LOG=<file>` | per-instance log file (in content/) |
| `BT_GAUGE_ATTR_LOG` | `[attr] <binding> OK/NULL` per config binding |
| `BT_GAUGE_SKIP_LOG` | `[gskip] <primitive>` per unregistered gauge widget the parse skips |
| `BT_VALVE_LOG` | condenser valve flow distribution |
| `BT_SECTOR_LOG` | SectorDisplay Make/Execute |
| `BT_NET_TRACE` | `[net-tx]/[net-rx]/[net-upd]` MP tracing |
| `BT_DEV_GAUGES` | render the 6 pod MFD surfaces in a separate dev window |
| `BT_AIM="x y"` | pin the reticle crosshair (reticle coords) — headless aim harness |
| `BT_AIM_LOG` | `[pick]` ray/box/hit diagnostics (Mech::PickRayHit) |
| `BT_FIRE_ARC=<deg>` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) |
| `BT_START_INSIDE` | begin in the cockpit view (V toggles) |
| `BT_FORCE_MODEL=<name>` | force the player mech (`madcat`/`owens`/…; btl4mssn.cpp — per-mech cockpit bring-up) |
| `BT_HIDE_COCKPIT` | hide the `*_cop` canopy shell (SHOWS by default — see [[cockpit-view]]) |
| `BT_COP_*` / `BT_EYE_FWD=<f>` | cockpit canopy + eye diagnostics (`FRAME` (unlit frame colour override, default 0.13,0.12,0.15) / `SINGLE` (single-sided diag; double-sided is default) / `PLATES=1` (disable the punch stencil-cut kit) / `FLIP` / `DEBUG` / `DUMP`; eye forward-push probe) — [[cockpit-view]] |
| `BT_SHOT=<path.png>` | periodic backbuffer dump (every 90 frames) — non-disruptive visual verification |
| `BT_TLOC_LOG` | translocation-warp lifecycle log (`[tloc] warp COLLAPSE/EXPAND/phase=…`) |
| `BT_WARP_*` | translocation-warp visual tuning — all default to the verified values (see [[translocation-warp]]): `EYE_UP=8.25` (on-axis), `SPIN=4`, `TWIST=0`, `CONTRAST=1.0`, `LO_*/HI_*` (lavender ramp), `BLUR=0`, `TESS=3`, `MIP=1`, `CULL=cw`, `ANISO=0` |
| `BT_WARP_SELFTEST` / `BT_WARP_SELFSHOT=<prefix>` | DIAG (off by default): force a held warp in a solo game / dump backbuffer frames to disk (visual-verification harness) |
| `BT_GYRO_LOG` | gyro bring-up log (gyro.cpp) |
| `BT_GYRO_TRACE` | per-frame hit-bounce integrator trace |
| `BT_CRIT_PROBE=<zone>` | hammer one own-mech zone every 4s (crit-propagation diag, task #2; mech4.cpp) |
| `BT_REPL_LOG` | replicant/MP replication log (mech4.cpp) |
| `BT_GOTO="enemy"\|"x z"` / `BT_GOTO_LOG` | self-driving beeline (to the enemy or a map coordinate) + its log |
| `BT_GOTO_STOP=<u>` / `BT_GOTO_THR=<0..1>` | beeline stop-radius / throttle overrides (Gitea #16 approach test: walk all the way in) |
| `BT_PARALLAX_LOG` | 2 Hz `[parallax]`: the pick point back-projected through the RENDER view (`BTProjectToRenderView`, L4VIDEO.cpp) — a healthy boresight holds (rx,ry)≈(0,0) at every range (Gitea #16; [[combat-damage]] Targeting) |
| `BT_MP_LOG` / `BT_MP_NET` / `BT_MP_FORCE_DMG` | MP diagnostics / forced cross-pod damage |
| `BT_AUTODRIVE=<0..1>` | headless throttle |
| `BT_KEY_NOFOCUS=1` | accept key input without window focus |
| `BT_KEY_STOP=1` | enable the dev stop-mission keystroke `'&'` (Shift+7) — OFF by default since the tester crash report (APP.cpp; [[reconstruction-gotchas]]) |
| `BT_KEY_LOG` | `[keych]` keyboard-event delivery trace (btl4mppr.cpp) |
| `BT_RADAR_LOG` | radar ctor probe + 1 Hz `[radar-wedge]` torso-twist rot trace ([[gauges-hud]]) |
| `BT_SPAWN_AT="x z"` | override the spawn position (mech.cpp — MP test rigs) |
| `BT_RXJIT` | inbound update-record inter-arrival jitter histogram (mech4.cpp — the single-box affinity artifact) |
| `BT_SLIDE` | locomotion slide diag (mech.cpp) |
| `BT_RELAY=<host:port\|auto>` | **D1 relay mode** — dial the relay/operator console instead of the P2P mesh; `auto` = LAN UDP-broadcast discovery on udp/15999 ([[multiplayer]] D1) |
| `BT_SELF=<[pilots] tag>` | claim a specific roster seat in relay mode; absent/`auto` = the relay assigns one (SEAT_REQUEST) |
| `BT_RELAY_TCP_ONLY=1` | disable the relay UDP channel (all unreliable traffic rides the TCP relay conn) |
| `BT_INPUT_LOG` | binding-engine trace: CONTROLS.MAP load, button/keypad emissions, pad connect, torso twist/elev axis ([[pod-hardware]] §Desktop input remap) |
| `BT_MODE_LOG` | `[mode] display notify` / `[mode] preset (g,i)` — the ModeManager mask after each secondary-view swap (Gitea #6) or upper-MFD preset-page swap (SetPresetMode, Gitea #9) (btl4mppr.cpp) |
| `BT_VIEWCYCLE_TEST=<frame>` / `BT_MODECYCLE_TEST=<frame>` | scripted verify (mech4.cpp): pulse a secondary-schematic cycle / a control-mode cycle at the frame and every 300 after — pixel-verifiable with `BT_DEV_GAUGES_DOCK`+`BT_SHOT` |
| `BT_PRESET_TEST=<frame>` | scripted verify (mech4.cpp, Gitea #9): from the frame, pulse an upper-MFD preset-page cycle on all three MFDs every 120 frames (Quad → populated Eng pages → Quad) — pixel-verifiable with `BT_DEV_GAUGES_DOCK`+`BT_SHOT` |
| `BT_PRESET_HOLD=<n>` | (with BT_PRESET_TEST, Gitea #11) stop after n pulses so a page is HELD — steady-state pixel verification; the 120-frame cycle phase-locks with BT_SHOT's 90-frame cadence, so cycling shots periodically capture the 1-frame page-switch transition |
| `BT_LOOK_TEST=<frame>` | scripted verify (mech4.cpp, Gitea #12): from the frame, HOLD the rear-view button 300 frames / release 300, repeating — drives mapper→BTCommitLookState→viewFireEnable so the projectile denied-shot blip (stay-Loaded, no launch) is log-verifiable headless |
| `BT_SEEK_LOG` | `[seek] BecameActive/replot` — SeekVoltageGraph activation sentinel + clear/replot events with the response-at-12kV value (Gitea #11, btl4gau2.cpp) |
| `BT_CTRLMAP_LOG` | streamed `.CTL` record audit (L4CTRL.cpp CreateStreamedMappings): Direct records with the resolved member offset + EVENT records (button→subsystem message; found the pod's secondary-panel Cycle buttons 0x15/0x18) |
| `BT_CAM_LOG` | camera-seat/broadcast trace: streamed camera network count, director pick + Players-group census, ship follow state, PlayerLink dispatch/receive, ranking-window render ([[multiplayer]] camera seat) |
| `BT_SPEC_LOG` | manual spec-audit dump at subsystem ctor: torso speed/limits, heat-sink count, reservoir capacity, per-subsystem coolant loop ([[pod-hardware]] §Spec cross-check) |
Full render/locomotion gates (BT_RAMP, BT_MATPRI, BT_CULL, BT_SHADOW_*, BT_LODSEL, BT_ADDLOD,
BT_PUNCH, …) are catalogued in [[rendering]]. Warp visuals: [[translocation-warp]].
**Translocation-warp addresses** [T1]: `POVTranslocateRenderable` = L4VIDRND.cpp:1749 / .h:638; its
per-frame Execute (spin about local Z + submit, NO texcoord/ramp work) = **`FUN_00453dc4`**
(`reference/decomp/all/part_007.c`), using axis-rotation helper `FUN_0040998c` (axis idx 2 = Z).
**Gyro damage fan-out** [T1, re-disassembled from raw bytes] (full story: [[cockpit-view]]):
**`FUN_004b2980`** (0x4b2980-0x4b2d8b, unexported gap) = `Gyroscope::ApplyDamageResponse` — args =
`(Gyroscope*, Damage BY VALUE 12 dwords)`, caller cleans 0x34; constants: dir-eps 1e-4, sign 0.5f
(@0x4b2d84), clamp 1.3f (@0x4b2d88); `vibrationDirection` = gyro+0x390 (0,1,0). Call sites: the
take-damage hub @0x4a02fb (hub = FUN_004a0230, gyro kick FIRST); crushable crunch @4aa81e (torque
0.4)/@4aa86c (up 0.2); gait jolt/rumble @4aa254/@4aa288/@4aa342 (DEFERRED [T3 gates]); firing
recoil @4bc136-4bc19c (damage>3 → (0,0.6,1.5) × damage/16; `gyroRumbleTimer` = mech+0x5c4 float,
ex-"clipLoadGuard"). Byte tooling: `scratchpad/dis_4b2980.py` / `dis_range.py` over
`reference/decomp/section_dump.txt`.
**PUNCH firmware decode** [T1] (the definitive mechanism, 2026-07-11): cmd 0x20 =
vr_damage_action (VR_PROT.H enum); the i860 firmware content/VREND.MNG (code @0xf0400000)
handler 'damageize' @0xf040f6f8 writes the token triple onto the geogroup's first THREE
geometry-list nodes' geometry_type@+8 IN FILE ORDER (superlist appends at tail @0xf040c620);
DPL_PRIV.H names the roles: f_t_dam(0x92)/sca(0x05)/undamage(0x04)/f_t_undam(0x17)/
damage_set(0x03). Punchize triple = mask, hull, reset-twin -- the visible surface is the HULL
with the MASK shapes stencil-cut through it. Disassembler: scratchpad/i860dis.py.
**Cockpit-eye / punch addresses** [T1] (full story: [[cockpit-view]]): eye ctor
**`FUN_004579a8`** (part_007.c:9274 = DPLEyeRenderable; offset = segment `GetBaseOffset`
segment+0x74, parent = parent-segment DCS; caller part_014.c:5525-66 gates on `"siteeyepoint"`);
matrix multiply **`FUN_0040b104(dst,A,B)` = `B × A`** (translation from B — operand-order trap!);
euler→rotation `FUN_0040ac6c`; per-frame view **`FUN_004c22c4`** (part_013.c:11742): eyeWorld from
the live view-DCS then **`FUN_0040b244` = affine INVERSE** → VIEW (+ uniform zoom) — NO LookAt;
eye euler-change refresh `FUN_00457b48` (part_007.c:9360). Punch: **`dpl_Punchize` =
`FUN_00490308`** (part_011.c:6219) → `FUN_0048e5b0``FUN_00492580(0x20,…)` board send, constant
tokens `{0x80000003,0x80000017,0x80000004}` (sibling `dpl_Damagize` `FUN_004902b0` = same cmd,
different triple); SV_SPECIAL PUNCH dispatch part_011.c:10209/10252 → callback LAB_004596ac.
Respawn SIM path: `DropZoneReply` `FUN_004bffd0``Mech::Reset` @0x4009fb74; `CreatePlayerVehicle`
`FUN_004bfcac`. See [[translocation-warp]], [[multiplayer]].
---
## 7. Tools
- `tools/disas2.py <VA> [len]` — capstone disassembly of BTL4OPT.EXE at a VA (recovers x87 math
Ghidra drops; folds known call targets + float constants). THE tool for any `FUN_` the
assert-anchored exporter didn't capture (e.g. `@0x4ae464` MoveValve, `@0x4ca07c` Execute).
- `tools/disas2.py`-style PE reads: `scratchpad/rdva.py` (4-byte VA read), `rdtbyte.py` (80-bit float10).
- `reference/ghidra_scripts/ExportBTSource.java` — the headless assert-anchored decompiler.
- `reference/ghidra_scripts/DecompVSS.java` — headless address-list decompiler (any skipped fn).
- `tools/btconsole.py` — the MP console emulator. `tools/mapscan.py`/`resscan.py` — content scanners.
---
## Key Relationships
- Feeds: [[subsystems]] (factory + ClassIDs), [[combat-damage]] (damage delivery), [[gauges-hud]] (attribute binding).
- Verified against: [[reconstruction-method]] (the decomp loop), [[reconstruction-gotchas]] (why raw offsets fail).