Re-disassembled the unexported gap 0x4b2980-0x4b2d8b byte-exact (capstone over
section_dump.txt) and reconstructed Gyroscope::ApplyDamageResponse:
- zero-damage + Collision(type 0) no-op; hit direction = Damage::damageForce
or a RANDOM horizontal fallback when ~zero (per-component sign roll then
value roll -- binary-legal until senders fill damageForce)
- direction rotated by the yaw-only torso-twist frame (placeRot=(0,twist,0)
-> euler->matrix, the FUN_00408744 raw row-dot reproduced verbatim)
- per-type scaling amount / damageMultiplier[type] * damageResponse[type]
.{trans,pitchRoll,yaw,vibration}; Explosive alone multiplies burstCount;
each clamped at 1.3f
- four kicks: impulse(trans, dir), torque(pitchRoll, dir), impulse(vibration,
vibrationDirection@0x390) and verticalImpulse(yaw, dir)
Layout corrections (byte-verified): gyro+0x390 is ONE Vector3D vibration axis
init (0,1,0) -- was mis-split as scalars animationOffset/Scale/Phase; mech
+0x5c4 is a FLOAT rumble-period countdown (gyroRumbleTimer) -- was mis-typed
int clipLoadGuard.
Call sites wired (binary-gated):
- take-damage hub: mech.cpp TakeDamageMessageHandler, FIRST action (@0x4a0264
in hub FUN_004a0230) -- an invalid-zone hit still bounces
- crushable-icon crunch: mech4.cpp, torque 0.4 along the bounce delta-v + up
impulse 0.2 (@4aa81e/@4aa86c)
- firing recoil: projweap.cpp FireWeapon, damage>3 -> impulse (0,0.6,-1.5)
x damage/16 (@4bc136-4bc19c) via the new Mech::GetGyroSubsystem accessor
DEFERRED (recorded in KB with the byte recipe): the alternate-gait engage
jolt + 0.4s rumble (@4aa158-4aa365) -- its gates are [T3]-flagged and it
mutates the #49/#50-stabilized gait machine.
Verified live: [gyro-dmg] fires per hit with correct type scaling; eyePosition
shows the damped bounce (ramp to ~0.015u, oscillating Y, decay to zero); kill
chain un-regressed; no NaN.
Also recorded in KB: the OFFSET-COCKPIT verification -- Thor's authored
jointeye tranx=+1.13 matches its canopy X-center exactly AND our in-game eye
reproduces it to the hundredth (the placement model is correct per-mech); and
the playable roster is 8 models (strider/raptor/firstrtr/blkjack have assets
but no model resource -- BT_FORCE_MODEL with those names = NULL-resource AV,
cdb-verified, not a code bug).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
221 lines
14 KiB
Markdown
221 lines
14 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) |
|
||
| ModelList | **18** | model/mech names (`bhk1`, `madcat`, …) |
|
||
| 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 | DEFERRED (NaN integrator; reverted to stub) |
|
||
| 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 | HeatSink | btl4gaug widget gate. [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] |
|
||
|
||
---
|
||
|
||
## 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`.
|
||
|
||
---
|
||
|
||
## 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. ⚠ Gates check
|
||
`getenv(name) != NULL`, so `BT_X=""` (empty) still ENABLES; use `Remove-Item Env:\BT_X`.
|
||
|
||
| Var | Effect |
|
||
|---|---|
|
||
| `BT_FORCE_THROTTLE` | auto-walk forward (no key) |
|
||
| `BT_SPAWN_ENEMY` | spawn a target mech 120u ahead along the spawn facing |
|
||
| `BT_FORCE_FIRE` | auto-fire on cooldown |
|
||
| `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 (`DOUBLE`/`FLIP`/`RAMP_L`/`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) |
|
||
|
||
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`.
|
||
|
||
**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).
|