The death/respawn "blue whirlwind" (tsphere) now matches the original cabinet
photo (capture.png): a smooth spinning lavender vortex with a bright core.
Root cause of the long-standing "radial spokes" artifact was NOT the warp code
but a general engine bug: L4D3D::SetTextureScrolling computed its texture-matrix
offset as scrollDelta * absolute_time, which grows unbounded and collapses UV
float precision -> a smooth scrolled cloud shatters into grainy radial steps.
Wrapped with fmodf(..., 1.0f) (identical under REPEAT tiling, full precision).
This also cleans the scrolling bexp beam grit and any other SCROLL material.
Visual reconstruction (verified against the real 45-vtx TSPHERE.BGF bicone,
offline-rasterized then ported):
- view ON-AXIS (eye centred on the throat) + spin in place -> concentric rings
(decomp FUN_00453dc4 does spin-about-local-Z + submit; the port had stubbed it)
- bintA cloud through a WIDE lavender ramp at full contrast (drawn as SKY);
no geometry "bands", no log-polar twist, no off-axis tornado (all discarded)
- tessellate the 12-facet bicone smooth; isotropic + trilinear; ramp baked into
the texture and drawn SELECTARG1(TEXTURE) to avoid double-tinting
Env knobs (BT_WARP_*) default to the verified values; BT_WARP_SELFTEST/SELFSHOT
are an off-by-default visual-verification harness (backbuffer frame dump).
Docs: new context/translocation-warp.md (geometry/material/visual/lifecycle/env);
reconstruction-gotchas.md gains the accumulated-time precision-collapse bug class;
rendering.md / multiplayer.md / decomp-reference.md / CLAUDE.md cross-linked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
12 KiB
Markdown
196 lines
12 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_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).
|
|
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).
|