--- 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 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=` → 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=` | boot-time BGF loader probe, exits after | | `BT_LOG=` | per-instance log file (in content/) | | `BT_GAUGE_ATTR_LOG` | `[attr] OK/NULL` per config binding | | `BT_GAUGE_SKIP_LOG` | `[gskip] ` 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=` | OPT-IN external-camera fire-arc clamp (unset = authentic no-arc) | | `BT_START_INSIDE` | begin in the cockpit view (V toggles) | | `BT_FORCE_MODEL=` | 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=` | 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=` | 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=` | 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=` | 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 [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).