Adapt the spark-lesson / expert-seed.md progressive-context pattern to the bt411 port:
a thin router + context/*.md knowledge graph so the 2236-line CLAUDE.md's deep knowledge
loads on-demand instead of every session.
NON-DESTRUCTIVE foundation pass (live CLAUDE.md unchanged; proposed router is a draft):
- reference/glossary.yaml -- ~45 terms (engine/formats/scene/reconstruction)
- context/decomp-reference.md -- the quantitative hub: resource types, ClassID map,
mech offsets, damage delivery, weapon constants, env gates, tools
- context/reconstruction-gotchas.md -- the 12 systemic bug classes (conventions/DO-NOT hub)
- context/bgf-format.md -- geometry format + CONN/PCONN + LOD-sqrt3 + ramp shading
- context/gauges-hud.md -- the gauge/MFD system (the just-completed wave)
- context/open-questions.md -- deferred systems + get-from-Nick
- context/_ROUTER-DRAFT.md -- the proposed slim CLAUDE.md (identity, protocols,
quick-lookup, evidence tiers T0-T4, conventions, structure)
- phases/phase-01-context-restructure.md -- design + migration plan + status
docs/*.md ledgers stay as the DETAILED logs; context/*.md are the curated digests that
route into them. Remaining: ~10 topic files (project-overview, subsystems, combat-damage,
rendering, locomotion, wintesla-port, build-and-run, ...) then the CLAUDE.md swap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.4 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 |
|
|
|
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 | — | DEFERRED (WAVE 8; the valve/message control hub @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]
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 | messageManager |
the 0xBD3 SubsystemMessageManager (damage guard / valve+message route source) |
| +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::TakeDamageMessageHandlerIGNORES 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.
WeaponRange500 (ER laser),DamageAmount3.5,PipColor1.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 thresholdseekVoltage[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 |
Full render/locomotion gates (BT_RAMP, BT_MATPRI, BT_CULL, BT_SHADOW_*, BT_LODSEL, BT_ADDLOD, BT_PUNCH, …) are catalogued in rendering.
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 anyFUN_the assert-anchored exporter didn't capture (e.g.@0x4ae464MoveValve,@0x4ca07cExecute).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).