Files
BT412/context/decomp-reference.md
arcattackandClaude Fable 5 02cdfd6576 Torso: the TWIST goes LIVE -- electrical watchdog chain, centered crosshair, coherent controls (task #57/#58)
The MadCat torso twists, the view turns with it, and targeting follows.
Three reconstruction fronts closed:

THE ELECTRICAL WATCHDOG CHAIN (why the torso never powered up):
- PowerWatcher::UpdateWatch reconstructed (@004b181c, the REAL registered
  Performance -- PTR @0050f5fc; Ghidra missed the fn start): the watchdog
  MIRRORS the watched subsystem's electrical level (+0x278), brownout
  downgrade when gen output <= minVoltage% x rated.  @004b1804 relabeled
  ResetToInitialState (slot 10) -- the old "Simulation" tag was wrong.
- The factory watcher-CONNECT pass reconstructed (vtable slot +0x38,
  @004aee2c/@004b1a40 byte-identical, recovered from raw exe bytes):
  watchedLink.Add(roster[watchedSubsystem]) on the master node.  Was the
  SubProxy::Start() no-op -- every watchdog sat at 0 forever.
- MinVoltageScale = 0.01 (a 10-byte x87 literal @0x4b1924; was 1.0f =
  permanent brownout) and PowerWatcher's Derivation chains its REAL base
  HeatWatcher (the HeatableSubsystem stand-in broke IsDerivedFrom for the
  whole Torso/Searchlight/ThermalSight family).
- KB correction swept: derivation tag 0x50e604 = HEATWATCHER (not
  "HeatSink"); the btl4gaug heat-widget gate now tests it via the
  BTIsHeatWatcher bridge.

THE CROSSHAIR (task #58 forensics, 6-agent workflow + live probes):
- The VIEW is TORSO-MOUNTED: jointtorso -> jointeye -> siteeyepoint in
  every twist-capable .SKL; the camera + canopy ride the same hinge
  subtree through HingeRenderable's live matrix-stack compose -- ALREADY
  WORKING in the port.  The crosshair stays screen-centered (center IS
  the boresight); the twist reads on the tape carets/compass/radar.
- The real bug was the port's gBTAimX = tan(twist) slew (the falsified
  "body-mounted view" model): the camera already carried the twist, so
  the crosshair counter-slid to hull-forward and the fire ray with it.
  Deleted; the pick ray inherits the twist from the yawing eye basis.
- Two instrumentation traps documented (chase-eye-as-default-camera,
  BT_FORCE_TORSO clobbering real joints -> the hook now only fills
  unresolved ones); an over-correcting explicit eye compose was added on
  those false readings and retired the same day.

CONTROLS + REPLICATION:
- Q/E spring-center on release (the axis is a twist-RATE demand; the old
  hold-deflection model drifted forever); X also zeroes the axis and
  pulses the authentic torso Recenter (@004b6918).  M cycles control
  mode via the real CycleControlMode body.
- Torso update-record DIRECTION fixed: engine truth is Write=serialize /
  Read=apply; @004b6a78 is the READ (was mislabeled Write) and the
  missing WRITE @004b6a1c recovered from raw disasm (recordLength 0x1C,
  twist/vel/rate at +0x10/14/18) -- kills the replicant's 0xCDCDCDCD
  -140-degree ghost twist.
- Marching-ghost desync: 4 Standing-case guards zero stale reverse
  cycleSpeed (negative cadence passed the <= ZeroSpeed stop gate).
- Kill credit rerouted to the OBSERVED killer (lastInflictingID ->
  killer's player link) -- kills count, target K/D populates.

KB: subsystems.md (watcher chain), multiplayer.md (record direction),
combat-damage.md + gauges-hud.md + cockpit-view.md (torso-mounted view
re-correction), decomp-reference.md (new addresses + tag fix),
open-questions.md (dead capability-roster loops 2-4, snapshot CD read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:27:49 -05:00

288 lines
22 KiB
Markdown
Raw Permalink 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 |
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; INERT un-stub |
| 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** (recon MISLABELS it `controlsMapper`; the drive squats there — the real mapper is roster slot 0). NOT the valve/Myomers gate (that's the owning **BTPlayer** @mech+0x190). WAVE 8 = untangle + damage-hub. |
| 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`) + Myomers gate (`FUN_004ad7d4 →player+0x260`) read the owning player's game-mode flags (NOT the messmgr @0x434). `MECH_OWNING_PLAYER`. |
| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). Our recon MISLABELS this `controlsMapper` + the drive squats here; 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]
---
## 4. Damage delivery
- `Entity::TakeDamageMessage(id, size, inflictingEntityID, zone, Damage&)``target->Dispatch(&msg)`.
- Base `Entity::TakeDamageMessageHandler` **IGNORES zone==-1** — send a VALID zone index (the Mech cylinder-lookup override for unaimed hits is not reconstructed). [T1]
- `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)
opens with `call 0x4b0bd0` (the powered/heat step), like @004baa88. 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.
- 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 |
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).