Files
BT411/context/decomp-reference.md
T
arcattackandClaude Opus 4.8 0bfb3d4ab3 Warp: restore the translocation-vortex look + fix texture-scroll precision collapse (task #52)
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>
2026-07-10 14:53:50 -05:00

12 KiB

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
decomp-reference Decomp Reference — resource types, ClassIDs, offsets, addresses, env gates established CLAUDE.md §10c; game/reconstructed/CLASSMAP.md; reference/decomp/all/part_*.c
subsystems
combat-damage
reconstruction-gotchas
build-and-run
gauges-hud
ClassID
resource-type
subsystem-roster
BTL4OPT
factory
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 IDFindResourceDescription(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 EntitySegments (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_004bffd0Mech::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