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>
10 KiB
id, title, status, source_sections, related_topics, key_terms, open_questions
| id | title | status | source_sections | related_topics | key_terms | open_questions | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| reconstruction-gotchas | Reconstruction Gotchas — the systemic bug classes (check these FIRST) | established | CLAUDE.md §5a, §10c; docs/HARD_PROBLEMS.md; docs/RESOURCE_AUDIT.md; gauge-wave notes |
|
|
|
Reconstruction Gotchas
The reconstruction is a layout + linkage problem as much as a logic problem. Our compiled
classes are NOT byte-identical to the 1995 binary, and the BT link uses /FORCE, so a whole
family of bugs is silent — garbage that happens to be non-fatal, or a runtime AV with no
link error. When a reconstructed class misbehaves, walk this checklist FIRST; the answer is
usually here, not in the logic.
The core rule (RULE: no stand-ins): the full game logic IS in the pseudocode — a "gap" is a reconstruction stub not yet filled, never a hole in the original. Never write placeholder logic for an apparent gap; read the decomp. (User: "there are no gaps, just work to be done.") Bring-up scaffolding (env-var paths, explosion-for-beam) is clearly marked and meant to be REPLACED, never to substitute for reading the decomp. [T2]
1. Shadow field — re-declaring an engine-base field (THE most common)
Symptom: a field reads 0xCDCDCDCD (fresh-heap fill) even though the ctor "sets" it; or an
object over-sizes past its factory alloc. Cause: the reconstruction re-declared a field the
engine base already owns, at the binary's offset. Two failures: (a) the copy shadows the
base — the engine ctor writes the base field, the reconstruction reads its own uninitialised
copy; (b) it lands at a different offset than the binary assumed.
Fix: delete the re-declaration; use the inherited member/accessor. Examples: Mech
damageZoneCount/damageZones (shadowed Entity's → zones never built); Mech__DamageZone
structureLevel→engine damageLevel; the whole HeatableSubsystem de-shadow
(statusFlags→simulationFlags, destroyed→simulationState, statusBits→ForceUpdate()).
[T2]
2. Wword(N) — a global scratch bank, NOT this+N
mechrecon.hpp:192 defines Wword(int i) as static BTVal bank[0x400]; return bank[i&0x3ff].
Any Wword(N) used for OBJECT access reads/writes a shared global, not this+i*4. e.g.
(Mech*)Wword(3) for a zone's owner → garbage; use GetOwningSimulation(). [T2]
3. Databinding trap — raw offsets read garbage
Our compiled layout != the 1995 binary, so *(T*)(obj+0xNN) reads garbage for ANY object we
compile. This is WHY shadow fields fail and why raw subsystem reads (e.g. a gauge reading
owner+0x438) return junk. Fix: use compiled named members/accessors; for a cross-TU raw
op, use a bridge (§8). A +0x128-style owner offset in subsystem code is the
subsystem-roster (subsystemArray), NOT the segment table — check every GetSegment(int)
in a reconstructed subsystem ctor. [T2]
4. Resource-struct layout mismatch (sibling of the shadow bug)
Symptom: RESOURCE fields read garbage (silently — a heatSinkIndex reading 10.0f).
Cause: *__SubsystemResource structs overlay pre-built 256-byte records loaded VERBATIM at
fixed offsets, so OUR struct must match the binary byte-for-byte. Breaks two ways: (a) wrong
inheritance base (the resource must mirror the CLASS hierarchy — HeatableSubsystem's resource
inherits MechSubsystem__SubsystemResource 0xE4, not Subsystem::SubsystemResource 0x30); (b)
under-sized fields (a 12-byte record field typed as a 4-byte ResourceID). Diagnose: log
compiled offsets (char*)&res->field - (char*)res vs the binary's; dump raw record bytes as
int+float. Lock: static_assert(offsetof(...)==0xNN) + static_assert(sizeof(...)==0xNN).
The 33-agent RESOURCE_AUDIT fixed 8 such bugs (docs/RESOURCE_AUDIT.md). [T2]
5. Alias / phantom / interior fields (object layout)
- Alias field: a subclass member re-declaring an inherited slot the ctor reuses under a new
name (Condenser
refrigerationOutput==inheritedmassScale@0x160; EmitteroutputVoltage==rechargeLevel@0x320). → delete, use the inherited name. - Alarm-interior field: a value the binary reads at
alarm+0x14modeled as a separate member (HeatSinkheatState@0x184==heatAlarm.GetLevel()). → route to the accessor. - Phantom field: a member at an offset PAST the object (Generator
shortFlag@0x25Cis really*(owner+0x190)+0x25cthe msg-manager). → remove; read the real source.GaugeAlarm54= 0x54 (the realAlarmIndicator; STATUS level at +0x14, sosubsystem+0x184 == heatAlarm+0x14 == GetLevel());SubsystemConnection= 0xC. [T1]
6. /FORCE trap — unresolved symbol → runtime AV, not a link error
/FORCE turns an unresolved external (or a prose-only vtable slot) into a runtime AV near
__ImageBase, NOT a link error. When a /FORCE build crashes with a garbage call target
near the image base, grep the link log for "unresolved external" — the "successful" build is
lying. Corollary: a bridge fn / a .data fn-ptr callback MUST have a real (stub) definition.
A SetVideoPathPriority defined in an anonymous namespace → internal linkage → unresolved in
another TU → stubbed by /FORCE → AV in LoadMissionImplementation. [T2]
7. Dtor-epilogue rule — do not reconstruct compiler glue
In a decompiled DESTRUCTOR, the trailing member-dtor calls (FUN_xxx(this+N, 2)), the base-dtor
call (FUN_xxx(this, 0)), and the (flags&1) && operator delete(this) tail are COMPILER GLUE.
Reconstruct only the body ABOVE them; C++ re-emits member+base destruction at the closing brace.
An explicit base-dtor call runs the whole ~JointedMover → ~Mover → ~Entity chain TWICE =
the P5 double-free (re-delete[]s collisionLists, re-runs DeletePlugs over the freed segment
table). ONE bug = BOTH the death-row crash AND the app-exit crash. [T2]
8. Bridges — the databinding-safe escape hatch
When TU A needs a raw-offset-safe op but its local RECON stubs collide with the real class
headers, put the op in a bridge: a free function in a complete-type TU, extern-declared in
A. Examples: BTResolveWeaponMuzzle (mech4.cpp — a complete-Mech TU with the segment API),
BTRecomputeCondenserValves (heatfamily_reslice.cpp — sees Condenser), BTResolveMessageBoard
(btplayer.cpp — complete BTPlayer), BTGetSubsystemAuxScreen (powersub.cpp — casts through the
real PoweredSubsystem). Keep the alloc SIZE + special-cache when swapping a factory case. [T2]
9. Message-handler chaining + entity validity
- A reconstructed class's
MessageHandlersset must be built chained to the parent's (Receiver::MessageHandlerSet(Entity::GetMessageHandlers())). An empty default-ctor set has no parent chain →Receiver::Receivefinds no handler → every inherited message (TakeDamage!) is silently dropped. [T2] Entity::Dispatchdelivers synchronously only for a VALID master; an invalid entityPosts a deferred event that never fires. A manually-spawned entity (no CheckLoad handshake) must callSetValidFlag()itself — else EVERY message defers forever (the replicant / spawned-dummy / MessageBoard-source class of bug). [T2]
10. Container-Execute must override (gauges)
The 2007 engine Gauge::Execute base is Fail("not overridden") → abort() (GAUGE.cpp:598);
GuardedExecute's SEH cannot catch abort(). So a container/parent gauge MUST override
Execute (even as a no-op) AND override BecameActive with a non-inactivating body (the
default GaugeBase::BecameActive inactivates). A GraphicGaugeBackground-derived widget
(PrepEngrScreen/BackgroundBitmap) has NO Execute virtual → the hazard doesn't apply; there the
overridden slot is BecameActive. [T2]
11. Dense-table hazard (attribute publishing)
AttributeIndexSet::Build leaves gap slots uninitialized and Find strcmps EVERY slot → a
published attribute table MUST be a dense prefix from the parent's NextAttributeID; a gap
AVs. Fill gaps with a shared read-only pad member. Same for a class's <Name>AttributeID enum.
[T2]
12. Verification gotchas (don't fool yourself)
- Lazy gauge build:
GaugeRenderer::BuildConfigurationFileruns LAZILY. A too-early process kill shows[gskip]=0/ "not built" even though the widget is fine — wait for the gauge window before concluding. (Cost a long detour this session.) [T2] - ReconStream is a no-op:
btl4gau3.cpp'sDebugStreamis theReconStreamwhoseoperator<<is{ return *this; }— it DISCARDS everything. Use the engineDEBUG_STREAM(what heat.cpp uses) for a log that reaches the BT_LOG file. [T2] - Head-on repro hides intermittent bugs: a straight ram gives 1 clean result; the bug shows on GLANCING/sliding/rough-terrain contact. Reproduce with an angled/terrain-crossing approach. [T2]
static_assertnot runtimeCheck: a runtimeCheck(sizeof<=alloc)in a factory bridge does NOT fail the build (it's a runtime assert → heap overflow at construction). Use a compile-timestatic_assertsizeof lock. [T2]- Engine-class new member: a NEW member on a 2007 engine class (d3d_OBJECT, DPLRenderer) MUST
be initialized in EVERY ctor init-list (debug heap fills 0xCDCDCDCD → an uninit flag reads
TRUE); and any device state a special draw path sets must be save/restored exactly. Deleting
stale
.objs fixes layout-mismatch corruption when a base class grows. [T2]
Diagnostic recipe (the standard loop)
- Read the RAW decomp
reference/decomp/all/part_*.cfor theFUN_xxxx. - Map
FUN_/DAT_/this+0xNNto engine symbols via BT headers + WinTesla MUNGA source +CLASSMAP.md+ RP's parallel code. - Write the REAL reconstruction;
static_assert-lock the layout. - Build; run env-gated; read
btl4.log; cdb on any crash (0xCDCDCDCD=uninit, 0xFEEEFEEE=freed). - For exhaustive multi-function analysis: a read-only Workflow (understand), then implement hands-on.
Key Relationships
- Applies to: every topic that reconstructs a class (subsystems, combat-damage, gauges-hud, locomotion).
- Uses: decomp-reference (offsets/ClassIDs), reconstruction-method (the loop).