The tester's ER small laser died permanently (recharge arc + ready dot dark, no fire) after idle seek-gear clicking. Root cause, byte-verified + headless- reproduced -- a latent bug in the SHIPPED ARCADE BINARY: - The charge integrates toward the GENERATOR voltage (@004ba838), not the gear target; "full" is only detected inside the +-1% snap window around seekVoltage[idx] (@004ba738), and overcharge reads ZERO (the byte-verified _DAT_004ba830 = 0.0 clamp). - Toggle seek while a charge is in flight and the level can land ABOVE the new gear's window: rechargeLevel pins to 0, the ==1.0 Loaded test can never fire, and charging continues to the generator ceiling where EVERY gear reads overcharged. Permanent brick; generator reselect cannot help. - The arcade dodged it by circumstance (locked 60 fps, rare seek use); the port's clickable seek button hits it in seconds of casual clicking. Fix (marked divergence in the Loading tick): overcharge counts as fully charged -> Loaded. The arcade's own discharge algebra says full == the gear's seek voltage, so an overcharged weapon IS full. Verified: the same 2.5-min BT_SEEKTEST abuse that bricked the laser pre-fix now fires 38 overcharge rescues and ends Loaded/pct=1. [seek] log now also prints the per-gear voltage table + gen voltages (diagnosis instrumentation). Ruled out on the way (documented in #21): overheat fuse (no firing occurred; a 4-min autofire+coolant-abuse run wouldn't fuse), generator detach (gen cycling + healthy siblings), seek-table corruption (table {6000,7000,8000, 9900} healthy). KB: decomp-reference.md records the arcade-latent-bug analysis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
439 lines
36 KiB
Markdown
439 lines
36 KiB
Markdown
---
|
||
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 21–56**: 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 3–12**: 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 3–15**: 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 (WIRED 2026-07-21, issue #20**: press-only, NO novice guard; sets every condenser valveState@0x1D0=1 then the shared redistribute @0049f788 == BTRecomputeCondenserValves; verify BT_BALTEST=1 + BT_VALVE_LOG=1**)**, 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}. **Both ToggleSeekVoltage handlers WIRED 2026-07-21 (issue #19)**:
|
||
Myomers' was an empty stub (body recovered by raw disasm of the untagged gap: guards @4ac9c8
|
||
novice + @4ad7d4 heat-model + press-only, then idx@0x320=(idx+1)%(max@0x32C+1), NO
|
||
ResetFiringState); Emitter's faithful body existed as the orphaned "AdvanceSeekVoltage" (zero
|
||
callers) -- both registered via MESSAGE_ENTRY (ids 9/0xb). Verify: BT_SEEKTEST=1 +
|
||
BT_SEEK_LOG=1 → `[seek] <name> -> gear N` cycling. EjectAmmo remains unwired.
|
||
**LATENT ARCADE BUG found + diverged (issue #21, 2026-07-21) [T1 byte-verified + T2 repro]:**
|
||
the Emitter charge integrates toward the GENERATOR voltage (@004ba838 `fld [src+0x1dc]`), full
|
||
is detected only inside the +-1% snap window around `seekVoltage[idx]` (ComputeOutputVoltage
|
||
@004ba738: `|rl-1|<=0.01 -> 1.0`), and OVERCHARGE reads ZERO (`rl>1.01 -> 0`,
|
||
`_DAT_004ba830 = 0.0` byte-verified). Toggle the seek gear while a charge is in flight and
|
||
the level lands ABOVE the new gear's window -> rechargeLevel pinned 0, the `==1.0` Loaded test
|
||
(`_DAT_004bac04`) never fires, charging continues to the generator ceiling where EVERY gear
|
||
reads overcharged -> the weapon is PERMANENTLY bricked (arc dark, ready-dot dark, no fire; gen
|
||
re-select cannot help). The arcade shipped this (locked 60 fps + rare seek use hid it); the
|
||
port's clickable seek button triggers it in seconds. **Deliberate divergence** (emitter.cpp
|
||
Loading tick): overcharge (`currentLevel > seekVoltage[idx]`) counts as fully charged ->
|
||
Loaded ("full == the gear's seek voltage" is the arcade's own discharge algebra). Repro +
|
||
verify: BT_SEEKTEST seek-abuse -- pre-fix deadlocks at pct=0/alarm=3; post-fix 38 rescues,
|
||
ends Loaded/pct=1. `[seek]` log prints the per-gear table + rescue events.
|
||
- **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).
|