Files
BT411/context/decomp-reference.md
T
arcattackandClaude Fable 5 5dd35365c7 Glass input audit: RIO mapper id fix (panel banks live), keyboard reconciliation
Root cause of the dead on-screen panel: MechRIOMapper's message-id enum
chained off L4MechControlsMapper::NextMessageID (=0x19), registering all 18
RIO override handlers at 0x19-0x2a while the streamed .CTL EventMapping
records carry the binary's ids -- the binary RIO table @0051dd30 re-registers
the BASE aux/zoom ids 3..0x13 (Hotbox 0x1a @0051de98).  Every MFD-bank/zoom
press hit the base ConfigureMappableMessageHandler FAIL trap (26/72 buttons
dead; an abort() on a trap-armed pod).  Ids now pinned to the binary
numbering + static_assert-locked (btl4mppr.hpp) -- panel goes 26 -> 52
working buttons (8 await handler reconstruction, filed on Gitea; 12 have no
streamed mapping authored = authentically inert).

Keyboard reconciliation (glass-only): the merged map -- keyboard hosts the
~20 core gameplay actions on the CONTROLS.MAP keys, the panel covers every
pod address by click (right-click = hold latch):
- W/S throttle lever, A/D pedals, Q/E twist, R/F elevation, X all-stop,
  arrows drive; numpad flight cluster kept (Cyd)
- 1/2/3/4/Space/LCTRL/RCTRL fire, LALT reverse, B look-behind (0x41)
- M=0x18 mode cycle, N=0x15 display cycle, H=0x2C coolant flush (hold),
  C=0x2F Condenser1 valve, G=0x0E weapon-1 configure (Mfd1-Quad-gated)
- F5-F9 = Mfd2 bank 0x27-0x24 + 0x22 (page-gated gen A-D / gen mode)
- hardcoded: ` or V = view toggle, J/K/L = Mfd1/2/3 preset-page cycle
  (PadRIO edges -> gBTPresetCycle; no pod "cycle" button exists)
- PadRIO::SuppressKey: bound keys are swallowed from the typed 1995 channel
  (the btinput suppression pattern, glass side) -- ends the glass double
  dispatch ('w'=pilot select 0, 'a'..'g'=MFD2 presets, NUMPAD/F-key key-up
  VK aliases like VK_F5=0x74='t').  Unbound keys keep their authentic
  meanings (5/z presets, t-o pilot select, +/- zoom, F1/F2 align).

DISPLACED from Cyd's default bindings (each reachable on the panel or by a
bindings.txt edit -- flagging for Cyd's veto): number row -> secondary panel
(1-4 now fire), QWERTY row -> pilot keypad (dropped), arrows -> hat looks
(now drive), V/C/B -> fire 0x47/0x46/0x45 (now view/valve/look-behind),
LCTRL -> throttle-down (now fire).  XInput pad section untouched.

Audit by-products (KB updated): the always-active msg-4 records identified
(0x2C = Reservoir flush, 0x29-0x2F = condenser valves, 0x1A-0x1D =
GeneratorA-D ToggleGeneratorOnOff @004b1ed0 unreconstructed); 0x13 = Mech
DuckRequest (crouch), 0x28 = BalanceCoolant; Searchlight/ThermalSight
ToggleLamp handler-sets are default-constructed EMPTY; weapon Eng-page msgs
0x3/0xb = ToggleCooling / ToggleSeekVoltage / EjectAmmo, all unwired;
unhandled messages are SILENT (no [FAIL] -- census = BT_CTRLMAP_LOG dump x
handler tables).

Verified live (build-glass2, ARENA1): panel presets/zoom/display/flush;
W drive (speedDemand 61.5, gait advances), Q turn (BAS), M -> MID, A turn
(MID), Space fires, N display, J/K/L presets, H flush drain, C valve 1->5;
suppression ('w' swallowed, 't' flows, F5-keyup swallowed); BT_SHOT frame.
Un-regression: pod build (gates OFF) compiles 0 errors, forced-walk drives,
streamed ids unchanged; CONTROLS.MAP/btinput untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:35:55 -05:00

402 lines
33 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)`:
| 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} (the Eng-page msg-3 button on
every weapon/avionics — unwired in the reconstruction).
- **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; SetColor @004c6ee0 has NO caller (gauge dormant in the shipped build).
## 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_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).